resources: Give mediawiki.special.* files their own place in src/
authorTimo Tijhof <krinklemail@gmail.com>
Thu, 10 May 2018 17:38:34 +0000 (18:38 +0100)
committerJdlrobson <jrobson@wikimedia.org>
Fri, 11 May 2018 18:12:00 +0000 (18:12 +0000)
Bug: T193826
Change-Id: Id25cd18079f48308f6ab42207445bbbd74ed5fda

109 files changed:
resources/Resources.php
resources/src/mediawiki.special.apisandbox.styles.css [new file with mode: 0644]
resources/src/mediawiki.special.apisandbox/apisandbox.css [new file with mode: 0644]
resources/src/mediawiki.special.apisandbox/apisandbox.js [new file with mode: 0644]
resources/src/mediawiki.special.block.js [new file with mode: 0644]
resources/src/mediawiki.special.changecredentials.js [new file with mode: 0644]
resources/src/mediawiki.special.changeslist.css [new file with mode: 0644]
resources/src/mediawiki.special.changeslist.enhanced.css [new file with mode: 0644]
resources/src/mediawiki.special.changeslist.legend.css [new file with mode: 0644]
resources/src/mediawiki.special.changeslist.legend.js [new file with mode: 0644]
resources/src/mediawiki.special.changeslist.visitedstatus.js [new file with mode: 0644]
resources/src/mediawiki.special.comparepages.styles.less [new file with mode: 0644]
resources/src/mediawiki.special.contributions.js [new file with mode: 0644]
resources/src/mediawiki.special.edittags.js [new file with mode: 0644]
resources/src/mediawiki.special.edittags.styles.css [new file with mode: 0644]
resources/src/mediawiki.special.import.js [new file with mode: 0644]
resources/src/mediawiki.special.movePage.css [new file with mode: 0644]
resources/src/mediawiki.special.movePage.js [new file with mode: 0644]
resources/src/mediawiki.special.pageLanguage.js [new file with mode: 0644]
resources/src/mediawiki.special.pagesWithProp.css [new file with mode: 0644]
resources/src/mediawiki.special.preferences.ooui/editfont.js [new file with mode: 0644]
resources/src/mediawiki.special.preferences.ooui/tabs.js [new file with mode: 0644]
resources/src/mediawiki.special.preferences.styles.css [new file with mode: 0644]
resources/src/mediawiki.special.preferences.styles.ooui.css [new file with mode: 0644]
resources/src/mediawiki.special.preferences/confirmClose.js [new file with mode: 0644]
resources/src/mediawiki.special.preferences/convertmessagebox.js [new file with mode: 0644]
resources/src/mediawiki.special.preferences/personalEmail.js [new file with mode: 0644]
resources/src/mediawiki.special.preferences/tabs.legacy.js [new file with mode: 0644]
resources/src/mediawiki.special.preferences/timezone.js [new file with mode: 0644]
resources/src/mediawiki.special.recentchanges.js [new file with mode: 0644]
resources/src/mediawiki.special.revisionDelete.js [new file with mode: 0644]
resources/src/mediawiki.special.search.commonsInterwikiWidget.js [new file with mode: 0644]
resources/src/mediawiki.special.search.interwikiwidget.styles.less [new file with mode: 0644]
resources/src/mediawiki.special.search.styles.css [new file with mode: 0644]
resources/src/mediawiki.special.search/search.css [new file with mode: 0644]
resources/src/mediawiki.special.search/search.js [new file with mode: 0644]
resources/src/mediawiki.special.undelete.js [new file with mode: 0644]
resources/src/mediawiki.special.unwatchedPages/unwatchedPages.css [new file with mode: 0644]
resources/src/mediawiki.special.unwatchedPages/unwatchedPages.js [new file with mode: 0644]
resources/src/mediawiki.special.upload.styles.css [new file with mode: 0644]
resources/src/mediawiki.special.upload/templates/thumbnail.html [new file with mode: 0644]
resources/src/mediawiki.special.upload/upload.js [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.common.styles/images/icon-lock.png [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.common.styles/userlogin.css [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.login.styles/images/glyph-people-large.png [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.login.styles/login.css [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.signup.js [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.signup.styles/images/icon-contributors.png [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.signup.styles/images/icon-edits.png [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.signup.styles/images/icon-pages.png [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.signup.styles/signup.css [new file with mode: 0644]
resources/src/mediawiki.special.userrights.js [new file with mode: 0644]
resources/src/mediawiki.special.version.css [new file with mode: 0644]
resources/src/mediawiki.special.watchlist.js [new file with mode: 0644]
resources/src/mediawiki.special.watchlist.styles.css [new file with mode: 0644]
resources/src/mediawiki.special/images/glyph-people-large.png [deleted file]
resources/src/mediawiki.special/images/icon-contributors.png [deleted file]
resources/src/mediawiki.special/images/icon-edits.png [deleted file]
resources/src/mediawiki.special/images/icon-lock.png [deleted file]
resources/src/mediawiki.special/images/icon-pages.png [deleted file]
resources/src/mediawiki.special/mediawiki.special.apisandbox.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.apisandbox.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.apisandbox.top.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.block.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.changecredentials.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.changeslist.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.changeslist.visitedstatus.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.comparepages.styles.less [deleted file]
resources/src/mediawiki.special/mediawiki.special.contributions.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.edittags.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.edittags.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.import.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.movePage.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.movePage.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.pageLanguage.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.styles.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.recentchanges.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.revisionDelete.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.search.commonsInterwikiWidget.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.search.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.search.interwikiwidget.styles.less [deleted file]
resources/src/mediawiki.special/mediawiki.special.search.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.search.styles.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.undelete.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.upload.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.upload.styles.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.userlogin.common.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.userlogin.login.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.userrights.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.version.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.watchlist.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.watchlist.js [deleted file]
resources/src/mediawiki.special/templates/thumbnail.html [deleted file]

index ea4e5ea..d0bc1ba 100644 (file)
@@ -2001,11 +2001,11 @@ return [
        ],
        'mediawiki.special.apisandbox.styles' => [
                'targets' => [ 'desktop', 'mobile' ],
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.top.css',
+               'styles' => 'resources/src/mediawiki.special.apisandbox.styles.css',
        ],
        'mediawiki.special.apisandbox' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.css',
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.js',
+               'styles' => 'resources/src/mediawiki.special.apisandbox/apisandbox.css',
+               'scripts' => 'resources/src/mediawiki.special.apisandbox/apisandbox.js',
                'targets' => [ 'desktop', 'mobile' ],
                'dependencies' => [
                        'mediawiki.api',
@@ -2073,7 +2073,7 @@ return [
                ],
        ],
        'mediawiki.special.block' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.block.js',
+               'scripts' => 'resources/src/mediawiki.special.block.js',
                'dependencies' => [
                        'oojs-ui-core',
                        'oojs-ui.styles.icons-editing-core',
@@ -2086,7 +2086,7 @@ return [
                ],
        ],
        'mediawiki.special.changecredentials.js' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.changecredentials.js',
+               'scripts' => 'resources/src/mediawiki.special.changecredentials.js',
                'dependencies' => [
                        'mediawiki.api',
                        'mediawiki.htmlform.ooui'
@@ -2094,18 +2094,18 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.changeslist' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.css',
+               'styles' => 'resources/src/mediawiki.special.changeslist.css',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.changeslist.enhanced' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css',
+               'styles' => 'resources/src/mediawiki.special.changeslist.enhanced.css',
        ],
        'mediawiki.special.changeslist.legend' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css',
+               'styles' => 'resources/src/mediawiki.special.changeslist.legend.css',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.changeslist.legend.js' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js',
+               'scripts' => 'resources/src/mediawiki.special.changeslist.legend.js',
                'dependencies' => [
                        'jquery.makeCollapsible',
                        'mediawiki.cookie',
@@ -2113,20 +2113,20 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.changeslist.visitedstatus' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.visitedstatus.js',
+               'scripts' => 'resources/src/mediawiki.special.changeslist.visitedstatus.js',
        ],
        'mediawiki.special.comparepages.styles' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.comparepages.styles.less',
+               'styles' => 'resources/src/mediawiki.special.comparepages.styles.less',
        ],
        'mediawiki.special.contributions' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.contributions.js',
+               'scripts' => 'resources/src/mediawiki.special.contributions.js',
                'dependencies' => [
                        'mediawiki.widgets.DateInputWidget',
                        'mediawiki.jqueryMsg',
                ]
        ],
        'mediawiki.special.edittags' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.edittags.js',
+               'scripts' => 'resources/src/mediawiki.special.edittags.js',
                'dependencies' => [
                        'jquery.chosen',
                        'jquery.lengthLimit',
@@ -2137,38 +2137,38 @@ return [
                ],
        ],
        'mediawiki.special.edittags.styles' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.edittags.css',
+               'styles' => 'resources/src/mediawiki.special.edittags.styles.css',
        ],
        'mediawiki.special.import' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.import.js',
+               'scripts' => 'resources/src/mediawiki.special.import.js',
        ],
        'mediawiki.special.movePage' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.movePage.js',
+               'scripts' => 'resources/src/mediawiki.special.movePage.js',
                'dependencies' => [
                        'mediawiki.widgets.visibleLengthLimit',
                        'mediawiki.widgets',
                ],
        ],
        'mediawiki.special.movePage.styles' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.movePage.css',
+               'styles' => 'resources/src/mediawiki.special.movePage.css',
        ],
        'mediawiki.special.pageLanguage' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.pageLanguage.js',
+               'scripts' => 'resources/src/mediawiki.special.pageLanguage.js',
                'dependencies' => [
                        'oojs-ui-core',
                ],
        ],
        'mediawiki.special.pagesWithProp' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css',
+               'styles' => 'resources/src/mediawiki.special.pagesWithProp.css',
        ],
        'mediawiki.special.preferences' => [
                'targets' => [ 'desktop', 'mobile' ],
                'scripts' => [
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js',
+                       'resources/src/mediawiki.special.preferences/confirmClose.js',
+                       'resources/src/mediawiki.special.preferences/convertmessagebox.js',
+                       'resources/src/mediawiki.special.preferences/tabs.legacy.js',
+                       'resources/src/mediawiki.special.preferences/timezone.js',
+                       'resources/src/mediawiki.special.preferences/personalEmail.js',
                ],
                'messages' => [
                        'prefs-tabs-navigation-hint',
@@ -2184,17 +2184,19 @@ return [
        ],
        'mediawiki.special.preferences.styles' => [
                'targets' => [ 'desktop', 'mobile' ],
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css',
+               // legacy
+               'styles' => 'resources/src/mediawiki.special.preferences.styles.css',
        ],
        'mediawiki.special.preferences.ooui' => [
                'targets' => [ 'desktop', 'mobile' ],
                'scripts' => [
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js',
+                       // FIXME: This uses files already belonging to another module
+                       'resources/src/mediawiki.special.preferences/confirmClose.js',
+                       'resources/src/mediawiki.special.preferences/convertmessagebox.js',
+                       'resources/src/mediawiki.special.preferences.ooui/editfont.js',
+                       'resources/src/mediawiki.special.preferences.ooui/tabs.js',
+                       'resources/src/mediawiki.special.preferences/timezone.js',
+                       'resources/src/mediawiki.special.preferences/personalEmail.js',
                ],
                'messages' => [
                        'prefs-tabs-navigation-hint',
@@ -2213,14 +2215,14 @@ return [
        ],
        'mediawiki.special.preferences.styles.ooui' => [
                'targets' => [ 'desktop', 'mobile' ],
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.css',
+               'styles' => 'resources/src/mediawiki.special.preferences.styles.ooui.css',
        ],
        'mediawiki.special.recentchanges' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.recentchanges.js',
+               'scripts' => 'resources/src/mediawiki.special.recentchanges.js',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.revisionDelete' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.revisionDelete.js',
+               'scripts' => 'resources/src/mediawiki.special.revisionDelete.js',
                'messages' => [
                        // @todo Load this message in content language
                        'colon-separator',
@@ -2231,8 +2233,8 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.search' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.search.js',
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.search.css',
+               'scripts' => 'resources/src/mediawiki.special.search/search.js',
+               'styles' => 'resources/src/mediawiki.special.search/search.css',
                'dependencies' => 'mediawiki.widgets.SearchInputWidget',
                'messages' => [
                        'powersearch-togglelabel',
@@ -2241,7 +2243,7 @@ return [
                ],
        ],
        'mediawiki.special.search.commonsInterwikiWidget' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.search.commonsInterwikiWidget.js',
+               'scripts' => 'resources/src/mediawiki.special.search.commonsInterwikiWidget.js',
                'dependencies' => [
                        'mediawiki.api',
                        'mediawiki.Uri',
@@ -2254,24 +2256,23 @@ return [
                ],
        ],
        'mediawiki.special.search.interwikiwidget.styles' => [
-               'styles' => 'resources/src/mediawiki.special/'
-                       . 'mediawiki.special.search.interwikiwidget.styles.less',
+               'styles' => 'resources/src/mediawiki.special.search.interwikiwidget.styles.less',
                'targets' => [ 'desktop', 'mobile' ]
        ],
        'mediawiki.special.search.styles' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.search.styles.css',
+               'styles' => 'resources/src/mediawiki.special.search.styles.css',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.undelete' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.undelete.js',
+               'scripts' => 'resources/src/mediawiki.special.undelete.js',
                'dependencies' => [
                        'mediawiki.widgets.visibleLengthLimit',
                        'mediawiki.widgets',
                ],
        ],
        'mediawiki.special.unwatchedPages' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js',
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css',
+               'scripts' => 'resources/src/mediawiki.special.unwatchedPages/unwatchedPages.js',
+               'styles' => 'resources/src/mediawiki.special.unwatchedPages/unwatchedPages.css',
                'messages' => [
                        'addedwatchtext-short',
                        'removedwatchtext-short',
@@ -2291,9 +2292,9 @@ return [
        ],
        'mediawiki.special.upload' => [
                'templates' => [
-                       'thumbnail.html' => 'resources/src/mediawiki.special/templates/thumbnail.html',
+                       'thumbnail.html' => 'resources/src/mediawiki.special.upload/templates/thumbnail.html',
                ],
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.upload.js',
+               'scripts' => 'resources/src/mediawiki.special.upload/upload.js',
                'messages' => [
                        'widthheight',
                        'size-bytes',
@@ -2319,21 +2320,21 @@ return [
                ],
        ],
        'mediawiki.special.upload.styles' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.upload.styles.css',
+               'styles' => 'resources/src/mediawiki.special.upload.styles.css',
        ],
        'mediawiki.special.userlogin.common.styles' => [
                'targets' => [ 'desktop', 'mobile' ],
                'skinStyles' => [
-                       'default' => 'resources/src/mediawiki.special/mediawiki.special.userlogin.common.css',
+                       'default' => 'resources/src/mediawiki.special.userlogin.common.styles/userlogin.css',
                ],
        ],
        'mediawiki.special.userlogin.login.styles' => [
                'styles' => [
-                       'resources/src/mediawiki.special/mediawiki.special.userlogin.login.css',
+                       'resources/src/mediawiki.special.userlogin.login.styles/login.css',
                ],
        ],
        'mediawiki.special.userlogin.signup.js' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js',
+               'scripts' => 'resources/src/mediawiki.special.userlogin.signup.js',
                'messages' => [
                        'createacct-emailrequired',
                        'noname',
@@ -2348,18 +2349,18 @@ return [
        ],
        'mediawiki.special.userlogin.signup.styles' => [
                'styles' => [
-                       'resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css',
+                       'resources/src/mediawiki.special.userlogin.signup.styles/signup.css',
                ],
        ],
        'mediawiki.special.userrights' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.userrights.js',
+               'scripts' => 'resources/src/mediawiki.special.userrights.js',
                'dependencies' => [
                        'mediawiki.notification.convertmessagebox',
                        'jquery.lengthLimit',
                ],
        ],
        'mediawiki.special.watchlist' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.js',
+               'scripts' => 'resources/src/mediawiki.special.watchlist.js',
                'messages' => [
                        'addedwatchtext',
                        'addedwatchtext-talk',
@@ -2380,10 +2381,10 @@ return [
                ],
        ],
        'mediawiki.special.watchlist.styles' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.css',
+               'styles' => 'resources/src/mediawiki.special.watchlist.styles.css',
        ],
        'mediawiki.special.version' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.version.css',
+               'styles' => 'resources/src/mediawiki.special.version.css',
        ],
 
        /* MediaWiki Installer */
diff --git a/resources/src/mediawiki.special.apisandbox.styles.css b/resources/src/mediawiki.special.apisandbox.styles.css
new file mode 100644 (file)
index 0000000..4dc4c27
--- /dev/null
@@ -0,0 +1,3 @@
+.client-js .mw-apisandbox-nojs {
+       display: none;
+}
diff --git a/resources/src/mediawiki.special.apisandbox/apisandbox.css b/resources/src/mediawiki.special.apisandbox/apisandbox.css
new file mode 100644 (file)
index 0000000..fe5ac41
--- /dev/null
@@ -0,0 +1,110 @@
+.mw-apisandbox-toolbar {
+       background: #fff;
+       -webkit-position: sticky;
+       position: sticky;
+       top: 0;
+       margin-bottom: -1px;
+       padding: 0.5em 0;
+       border-bottom: 1px solid #a2a9b1;
+       text-align: right;
+       z-index: 1;
+}
+
+#mw-apisandbox-ui .mw-apisandbox-link {
+       display: none;
+}
+
+.mw-apisandbox-popup .oo-ui-popupWidget-body > .oo-ui-widget {
+       vertical-align: middle;
+}
+
+/* So DateTimeInputWidget's calendar popup works... */
+.mw-apisandbox-popup .oo-ui-popupWidget-popup,
+.mw-apisandbox-popup .oo-ui-popupWidget-body {
+       overflow: visible;
+}
+
+/* Display contents of the popup on a single line */
+.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body {
+       display: table;
+}
+
+.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > * {
+       display: table-cell;
+}
+
+.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > .oo-ui-buttonWidget {
+       padding-left: 0.5em;
+       width: 1%;
+}
+
+.mw-apisandbox-spacer {
+       display: inline-block;
+       height: 1px;
+       width: 5em;
+}
+
+.mw-apisandbox-help-field {
+       border-bottom: 1px solid rgba( 0, 0, 0, 0.1 );
+}
+
+.mw-apisandbox-help-field:last-child {
+       border-bottom: 0;
+}
+
+.mw-apisandbox-optionalWidget {
+       width: 100%;
+}
+
+.mw-apisandbox-optionalWidget.oo-ui-widget-disabled {
+       position: relative;
+       z-index: 0; /* New stacking context to prevent the cover from leaking out */
+}
+
+.mw-apisandbox-optionalWidget-cover {
+       position: absolute;
+       left: 0;
+       right: 0;
+       top: 0;
+       bottom: 0;
+       z-index: 2;
+       cursor: pointer;
+}
+
+.mw-apisandbox-optionalWidget-fields {
+       display: table;
+       width: 100%;
+}
+
+.mw-apisandbox-optionalWidget-widget,
+.mw-apisandbox-optionalWidget-checkbox {
+       display: table-cell;
+       vertical-align: middle;
+}
+
+.mw-apisandbox-optionalWidget-checkbox {
+       width: 1%; /* Will be expanded by content */
+       white-space: nowrap;
+       padding-left: 0.5em;
+}
+
+.mw-apisandbox-textInputCode .oo-ui-inputWidget-input {
+       font-family: monospace, monospace;
+       font-size: 0.8125em;
+       -moz-tab-size: 4;
+       tab-size: 4;
+}
+
+.mw-apisandbox-widget-field .oo-ui-textInputWidget {
+       /* Leave at least enough space for icon, indicator, and a sliver of text */
+       min-width: 6em;
+}
+
+.apihelp-deprecated {
+       font-weight: bold;
+       color: #d33;
+}
+
+.apihelp-deprecated-value .oo-ui-labelElement-label {
+       text-decoration: line-through;
+}
diff --git a/resources/src/mediawiki.special.apisandbox/apisandbox.js b/resources/src/mediawiki.special.apisandbox/apisandbox.js
new file mode 100644 (file)
index 0000000..523a62e
--- /dev/null
@@ -0,0 +1,1864 @@
+( function ( $, mw, OO ) {
+       'use strict';
+       var ApiSandbox, Util, WidgetMethods, Validators,
+               $content, panel, booklet, oldhash, windowManager,
+               formatDropdown,
+               api = new mw.Api(),
+               bookletPages = [],
+               availableFormats = {},
+               resultPage = null,
+               suppressErrors = true,
+               updatingBooklet = false,
+               pages = {},
+               moduleInfoCache = {},
+               baseRequestParams;
+
+       /**
+        * A wrapper for a widget that provides an enable/disable button
+        *
+        * @class
+        * @private
+        * @constructor
+        * @param {OO.ui.Widget} widget
+        * @param {Object} [config] Configuration options
+        */
+       function OptionalWidget( widget, config ) {
+               var k;
+
+               config = config || {};
+
+               this.widget = widget;
+               this.$cover = config.$cover ||
+                       $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-cover' );
+               this.checkbox = new OO.ui.CheckboxInputWidget( config.checkbox )
+                       .on( 'change', this.onCheckboxChange, [], this );
+
+               OptionalWidget[ 'super' ].call( this, config );
+
+               // Forward most methods for convenience
+               for ( k in this.widget ) {
+                       if ( $.isFunction( this.widget[ k ] ) && !this[ k ] ) {
+                               this[ k ] = this.widget[ k ].bind( this.widget );
+                       }
+               }
+
+               this.$cover.on( 'click', this.onOverlayClick.bind( this ) );
+
+               this.$element
+                       .addClass( 'mw-apisandbox-optionalWidget' )
+                       .append(
+                               this.$cover,
+                               $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-fields' ).append(
+                                       $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-widget' ).append(
+                                               widget.$element
+                                       ),
+                                       $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-checkbox' ).append(
+                                               this.checkbox.$element
+                                       )
+                               )
+                       );
+
+               this.setDisabled( widget.isDisabled() );
+       }
+       OO.inheritClass( OptionalWidget, OO.ui.Widget );
+       OptionalWidget.prototype.onCheckboxChange = function ( checked ) {
+               this.setDisabled( !checked );
+       };
+       OptionalWidget.prototype.onOverlayClick = function () {
+               this.setDisabled( false );
+               if ( $.isFunction( this.widget.focus ) ) {
+                       this.widget.focus();
+               }
+       };
+       OptionalWidget.prototype.setDisabled = function ( disabled ) {
+               OptionalWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
+               this.widget.setDisabled( this.isDisabled() );
+               this.checkbox.setSelected( !this.isDisabled() );
+               this.$cover.toggle( this.isDisabled() );
+               return this;
+       };
+
+       WidgetMethods = {
+               textInputWidget: {
+                       getApiValue: function () {
+                               return this.getValue();
+                       },
+                       setApiValue: function ( v ) {
+                               if ( v === undefined ) {
+                                       v = this.paramInfo[ 'default' ];
+                               }
+                               this.setValue( v );
+                       },
+                       apiCheckValid: function () {
+                               var that = this;
+                               return this.getValidity().then( function () {
+                                       return $.Deferred().resolve( true ).promise();
+                               }, function () {
+                                       return $.Deferred().resolve( false ).promise();
+                               } ).done( function ( ok ) {
+                                       ok = ok || suppressErrors;
+                                       that.setIcon( ok ? null : 'alert' );
+                                       that.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
+                               } );
+                       }
+               },
+
+               dateTimeInputWidget: {
+                       getValidity: function () {
+                               if ( !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '' ) {
+                                       return $.Deferred().resolve().promise();
+                               } else {
+                                       return $.Deferred().reject().promise();
+                               }
+                       }
+               },
+
+               tokenWidget: {
+                       alertTokenError: function ( code, error ) {
+                               windowManager.openWindow( 'errorAlert', {
+                                       title: Util.parseMsg( 'apisandbox-results-fixtoken-fail', this.paramInfo.tokentype ),
+                                       message: error,
+                                       actions: [
+                                               {
+                                                       action: 'accept',
+                                                       label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
+                                                       flags: 'primary'
+                                               }
+                                       ]
+                               } );
+                       },
+                       fetchToken: function () {
+                               this.pushPending();
+                               return api.getToken( this.paramInfo.tokentype )
+                                       .done( this.setApiValue.bind( this ) )
+                                       .fail( this.alertTokenError.bind( this ) )
+                                       .always( this.popPending.bind( this ) );
+                       },
+                       setApiValue: function ( v ) {
+                               WidgetMethods.textInputWidget.setApiValue.call( this, v );
+                               if ( v === '123ABC' ) {
+                                       this.fetchToken();
+                               }
+                       }
+               },
+
+               passwordWidget: {
+                       getApiValueForDisplay: function () {
+                               return '';
+                       }
+               },
+
+               toggleSwitchWidget: {
+                       getApiValue: function () {
+                               return this.getValue() ? 1 : undefined;
+                       },
+                       setApiValue: function ( v ) {
+                               this.setValue( Util.apiBool( v ) );
+                       },
+                       apiCheckValid: function () {
+                               return $.Deferred().resolve( true ).promise();
+                       }
+               },
+
+               dropdownWidget: {
+                       getApiValue: function () {
+                               var item = this.getMenu().findSelectedItem();
+                               return item === null ? undefined : item.getData();
+                       },
+                       setApiValue: function ( v ) {
+                               var menu = this.getMenu();
+
+                               if ( v === undefined ) {
+                                       v = this.paramInfo[ 'default' ];
+                               }
+                               if ( v === undefined ) {
+                                       menu.selectItem();
+                               } else {
+                                       menu.selectItemByData( String( v ) );
+                               }
+                       },
+                       apiCheckValid: function () {
+                               var ok = this.getApiValue() !== undefined || suppressErrors;
+                               this.setIcon( ok ? null : 'alert' );
+                               this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
+                               return $.Deferred().resolve( ok ).promise();
+                       }
+               },
+
+               tagWidget: {
+                       getApiValue: function () {
+                               var items = this.getValue();
+                               if ( items.join( '' ).indexOf( '|' ) === -1 ) {
+                                       return items.join( '|' );
+                               } else {
+                                       return '\x1f' + items.join( '\x1f' );
+                               }
+                       },
+                       setApiValue: function ( v ) {
+                               if ( v === undefined || v === '' || v === '\x1f' ) {
+                                       this.setValue( [] );
+                               } else {
+                                       v = String( v );
+                                       if ( v.indexOf( '\x1f' ) !== 0 ) {
+                                               this.setValue( v.split( '|' ) );
+                                       } else {
+                                               this.setValue( v.substr( 1 ).split( '\x1f' ) );
+                                       }
+                               }
+                       },
+                       apiCheckValid: function () {
+                               var ok = true,
+                                       pi = this.paramInfo;
+
+                               if ( !suppressErrors ) {
+                                       ok = this.getApiValue() !== undefined && !(
+                                               pi.allspecifier !== undefined &&
+                                               this.getValue().length > 1 &&
+                                               this.getValue().indexOf( pi.allspecifier ) !== -1
+                                       );
+                               }
+
+                               this.setIcon( ok ? null : 'alert' );
+                               this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
+                               return $.Deferred().resolve( ok ).promise();
+                       },
+                       createTagItemWidget: function ( data, label ) {
+                               var item = OO.ui.TagMultiselectWidget.prototype.createTagItemWidget.call( this, data, label );
+                               if ( this.paramInfo.deprecatedvalues &&
+                                       this.paramInfo.deprecatedvalues.indexOf( data ) >= 0
+                               ) {
+                                       item.$element.addClass( 'apihelp-deprecated-value' );
+                               }
+                               return item;
+                       }
+               },
+
+               optionalWidget: {
+                       getApiValue: function () {
+                               return this.isDisabled() ? undefined : this.widget.getApiValue();
+                       },
+                       setApiValue: function ( v ) {
+                               this.setDisabled( v === undefined );
+                               this.widget.setApiValue( v );
+                       },
+                       apiCheckValid: function () {
+                               if ( this.isDisabled() ) {
+                                       return $.Deferred().resolve( true ).promise();
+                               } else {
+                                       return this.widget.apiCheckValid();
+                               }
+                       }
+               },
+
+               submoduleWidget: {
+                       single: function () {
+                               var v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
+                               return v === undefined ? [] : [ { value: v, path: this.paramInfo.submodules[ v ] } ];
+                       },
+                       multi: function () {
+                               var map = this.paramInfo.submodules,
+                                       v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
+                               return v === undefined || v === '' ? [] : String( v ).split( '|' ).map( function ( v ) {
+                                       return { value: v, path: map[ v ] };
+                               } );
+                       }
+               },
+
+               uploadWidget: {
+                       getApiValueForDisplay: function () {
+                               return '...';
+                       },
+                       getApiValue: function () {
+                               return this.getValue();
+                       },
+                       setApiValue: function () {
+                               // Can't, sorry.
+                       },
+                       apiCheckValid: function () {
+                               var ok = this.getValue() !== null || suppressErrors;
+                               this.setIcon( ok ? null : 'alert' );
+                               this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
+                               return $.Deferred().resolve( ok ).promise();
+                       }
+               }
+       };
+
+       Validators = {
+               generic: function () {
+                       return !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '';
+               }
+       };
+
+       /**
+        * @class mw.special.ApiSandbox.Util
+        * @private
+        */
+       Util = {
+               /**
+                * Fetch API module info
+                *
+                * @param {string} module Module to fetch data for
+                * @return {jQuery.Promise}
+                */
+               fetchModuleInfo: function ( module ) {
+                       var apiPromise,
+                               deferred = $.Deferred();
+
+                       if ( moduleInfoCache.hasOwnProperty( module ) ) {
+                               return deferred
+                                       .resolve( moduleInfoCache[ module ] )
+                                       .promise( { abort: function () {} } );
+                       } else {
+                               apiPromise = api.post( {
+                                       action: 'paraminfo',
+                                       modules: module,
+                                       helpformat: 'html',
+                                       uselang: mw.config.get( 'wgUserLanguage' )
+                               } ).done( function ( data ) {
+                                       var info;
+
+                                       if ( data.warnings && data.warnings.paraminfo ) {
+                                               deferred.reject( '???', data.warnings.paraminfo[ '*' ] );
+                                               return;
+                                       }
+
+                                       info = data.paraminfo.modules;
+                                       if ( !info || info.length !== 1 || info[ 0 ].path !== module ) {
+                                               deferred.reject( '???', 'No module data returned' );
+                                               return;
+                                       }
+
+                                       moduleInfoCache[ module ] = info[ 0 ];
+                                       deferred.resolve( info[ 0 ] );
+                               } ).fail( function ( code, details ) {
+                                       if ( code === 'http' ) {
+                                               details = 'HTTP error: ' + details.exception;
+                                       } else if ( details.error ) {
+                                               details = details.error.info;
+                                       }
+                                       deferred.reject( code, details );
+                               } );
+                               return deferred
+                                       .promise( { abort: apiPromise.abort } );
+                       }
+               },
+
+               /**
+                * Mark all currently-in-use tokens as bad
+                */
+               markTokensBad: function () {
+                       var page, subpages, i,
+                               checkPages = [ pages.main ];
+
+                       while ( checkPages.length ) {
+                               page = checkPages.shift();
+
+                               if ( page.tokenWidget ) {
+                                       api.badToken( page.tokenWidget.paramInfo.tokentype );
+                               }
+
+                               subpages = page.getSubpages();
+                               for ( i = 0; i < subpages.length; i++ ) {
+                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
+                                               checkPages.push( pages[ subpages[ i ].key ] );
+                                       }
+                               }
+                       }
+               },
+
+               /**
+                * Test an API boolean
+                *
+                * @param {Mixed} value
+                * @return {boolean}
+                */
+               apiBool: function ( value ) {
+                       return value !== undefined && value !== false;
+               },
+
+               /**
+                * Create a widget for a parameter.
+                *
+                * @param {Object} pi Parameter info from API
+                * @param {Object} opts Additional options
+                * @return {OO.ui.Widget}
+                */
+               createWidgetForParameter: function ( pi, opts ) {
+                       var widget, innerWidget, finalWidget, items, $content, func,
+                               multiModeButton = null,
+                               multiModeInput = null,
+                               multiModeAllowed = false;
+
+                       opts = opts || {};
+
+                       switch ( pi.type ) {
+                               case 'boolean':
+                                       widget = new OO.ui.ToggleSwitchWidget();
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.toggleSwitchWidget );
+                                       pi.required = true; // Avoid wrapping in the non-required widget
+                                       break;
+
+                               case 'string':
+                               case 'user':
+                                       if ( Util.apiBool( pi.multi ) ) {
+                                               widget = new OO.ui.TagMultiselectWidget( {
+                                                       allowArbitrary: true,
+                                                       allowDuplicates: Util.apiBool( pi.allowsduplicates ),
+                                                       $overlay: true
+                                               } );
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.tagWidget );
+                                       } else {
+                                               widget = new OO.ui.TextInputWidget( {
+                                                       required: Util.apiBool( pi.required )
+                                               } );
+                                       }
+                                       if ( !Util.apiBool( pi.multi ) ) {
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.textInputWidget );
+                                               widget.setValidation( Validators.generic );
+                                       }
+                                       if ( pi.tokentype ) {
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.textInputWidget );
+                                               $.extend( widget, WidgetMethods.tokenWidget );
+                                       }
+                                       break;
+
+                               case 'text':
+                                       widget = new OO.ui.MultilineTextInputWidget( {
+                                               required: Util.apiBool( pi.required )
+                                       } );
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.textInputWidget );
+                                       widget.setValidation( Validators.generic );
+                                       break;
+
+                               case 'password':
+                                       widget = new OO.ui.TextInputWidget( {
+                                               type: 'password',
+                                               required: Util.apiBool( pi.required )
+                                       } );
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.textInputWidget );
+                                       $.extend( widget, WidgetMethods.passwordWidget );
+                                       widget.setValidation( Validators.generic );
+                                       multiModeAllowed = true;
+                                       multiModeInput = widget;
+                                       break;
+
+                               case 'integer':
+                                       widget = new OO.ui.NumberInputWidget( {
+                                               required: Util.apiBool( pi.required ),
+                                               isInteger: true
+                                       } );
+                                       widget.setIcon = widget.input.setIcon.bind( widget.input );
+                                       widget.setIconTitle = widget.input.setIconTitle.bind( widget.input );
+                                       widget.getValidity = widget.input.getValidity.bind( widget.input );
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.textInputWidget );
+                                       if ( Util.apiBool( pi.enforcerange ) ) {
+                                               widget.setRange( pi.min || -Infinity, pi.max || Infinity );
+                                       }
+                                       multiModeAllowed = true;
+                                       multiModeInput = widget;
+                                       break;
+
+                               case 'limit':
+                                       widget = new OO.ui.TextInputWidget( {
+                                               required: Util.apiBool( pi.required )
+                                       } );
+                                       widget.setValidation( function ( value ) {
+                                               var n, pi = this.paramInfo;
+
+                                               if ( value === 'max' ) {
+                                                       return true;
+                                               } else {
+                                                       n = +value;
+                                                       return !isNaN( n ) && isFinite( n ) &&
+                                                               Math.floor( n ) === n &&
+                                                               n >= pi.min && n <= pi.apiSandboxMax;
+                                               }
+                                       } );
+                                       pi.min = pi.min || 0;
+                                       pi.apiSandboxMax = mw.config.get( 'apihighlimits' ) ? pi.highmax : pi.max;
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.textInputWidget );
+                                       multiModeAllowed = true;
+                                       multiModeInput = widget;
+                                       break;
+
+                               case 'timestamp':
+                                       widget = new mw.widgets.datetime.DateTimeInputWidget( {
+                                               formatter: {
+                                                       format: '${year|0}-${month|0}-${day|0}T${hour|0}:${minute|0}:${second|0}${zone|short}'
+                                               },
+                                               required: Util.apiBool( pi.required ),
+                                               clearable: false
+                                       } );
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.textInputWidget );
+                                       $.extend( widget, WidgetMethods.dateTimeInputWidget );
+                                       multiModeAllowed = true;
+                                       break;
+
+                               case 'upload':
+                                       widget = new OO.ui.SelectFileWidget();
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.uploadWidget );
+                                       break;
+
+                               case 'namespace':
+                                       items = $.map( mw.config.get( 'wgFormattedNamespaces' ), function ( name, ns ) {
+                                               if ( ns === '0' ) {
+                                                       name = mw.message( 'blanknamespace' ).text();
+                                               }
+                                               return new OO.ui.MenuOptionWidget( { data: ns, label: name } );
+                                       } ).sort( function ( a, b ) {
+                                               return a.data - b.data;
+                                       } );
+                                       if ( Util.apiBool( pi.multi ) ) {
+                                               if ( pi.allspecifier !== undefined ) {
+                                                       items.unshift( new OO.ui.MenuOptionWidget( {
+                                                               data: pi.allspecifier,
+                                                               label: mw.message( 'apisandbox-multivalue-all-namespaces', pi.allspecifier ).text()
+                                                       } ) );
+                                               }
+
+                                               widget = new OO.ui.MenuTagMultiselectWidget( {
+                                                       menu: { items: items },
+                                                       $overlay: true
+                                               } );
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.tagWidget );
+                                       } else {
+                                               widget = new OO.ui.DropdownWidget( {
+                                                       menu: { items: items },
+                                                       $overlay: true
+                                               } );
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.dropdownWidget );
+                                       }
+                                       break;
+
+                               default:
+                                       if ( !Array.isArray( pi.type ) ) {
+                                               throw new Error( 'Unknown parameter type ' + pi.type );
+                                       }
+
+                                       items = pi.type.map( function ( v ) {
+                                               var config = {
+                                                       data: String( v ),
+                                                       label: String( v ),
+                                                       classes: []
+                                               };
+                                               if ( pi.deprecatedvalues && pi.deprecatedvalues.indexOf( v ) >= 0 ) {
+                                                       config.classes.push( 'apihelp-deprecated-value' );
+                                               }
+                                               return new OO.ui.MenuOptionWidget( config );
+                                       } );
+                                       if ( Util.apiBool( pi.multi ) ) {
+                                               if ( pi.allspecifier !== undefined ) {
+                                                       items.unshift( new OO.ui.MenuOptionWidget( {
+                                                               data: pi.allspecifier,
+                                                               label: mw.message( 'apisandbox-multivalue-all-values', pi.allspecifier ).text()
+                                                       } ) );
+                                               }
+
+                                               widget = new OO.ui.MenuTagMultiselectWidget( {
+                                                       menu: { items: items },
+                                                       $overlay: true
+                                               } );
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.tagWidget );
+                                               if ( Util.apiBool( pi.submodules ) ) {
+                                                       widget.getSubmodules = WidgetMethods.submoduleWidget.multi;
+                                                       widget.on( 'change', ApiSandbox.updateUI );
+                                               }
+                                       } else {
+                                               widget = new OO.ui.DropdownWidget( {
+                                                       menu: { items: items },
+                                                       $overlay: true
+                                               } );
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.dropdownWidget );
+                                               if ( Util.apiBool( pi.submodules ) ) {
+                                                       widget.getSubmodules = WidgetMethods.submoduleWidget.single;
+                                                       widget.getMenu().on( 'select', ApiSandbox.updateUI );
+                                               }
+                                               if ( pi.deprecatedvalues ) {
+                                                       widget.getMenu().on( 'select', function ( item ) {
+                                                               this.$element.toggleClass(
+                                                                       'apihelp-deprecated-value',
+                                                                       pi.deprecatedvalues.indexOf( item.data ) >= 0
+                                                               );
+                                                       }, [], widget );
+                                               }
+                                       }
+
+                                       break;
+                       }
+
+                       if ( Util.apiBool( pi.multi ) && multiModeAllowed ) {
+                               innerWidget = widget;
+
+                               multiModeButton = new OO.ui.ButtonWidget( {
+                                       label: mw.message( 'apisandbox-add-multi' ).text()
+                               } );
+                               $content = innerWidget.$element.add( multiModeButton.$element );
+
+                               widget = new OO.ui.PopupTagMultiselectWidget( {
+                                       allowArbitrary: true,
+                                       allowDuplicates: Util.apiBool( pi.allowsduplicates ),
+                                       $overlay: true,
+                                       popup: {
+                                               classes: [ 'mw-apisandbox-popup' ],
+                                               padded: true,
+                                               $content: $content
+                                       }
+                               } );
+                               widget.paramInfo = pi;
+                               $.extend( widget, WidgetMethods.tagWidget );
+
+                               func = function () {
+                                       if ( !innerWidget.isDisabled() ) {
+                                               innerWidget.apiCheckValid().done( function ( ok ) {
+                                                       if ( ok ) {
+                                                               widget.addTag( innerWidget.getApiValue() );
+                                                               innerWidget.setApiValue( undefined );
+                                                       }
+                                               } );
+                                               return false;
+                                       }
+                               };
+
+                               if ( multiModeInput ) {
+                                       multiModeInput.on( 'enter', func );
+                               }
+                               multiModeButton.on( 'click', func );
+                       }
+
+                       if ( Util.apiBool( pi.required ) || opts.nooptional ) {
+                               finalWidget = widget;
+                       } else {
+                               finalWidget = new OptionalWidget( widget );
+                               finalWidget.paramInfo = pi;
+                               $.extend( finalWidget, WidgetMethods.optionalWidget );
+                               if ( widget.getSubmodules ) {
+                                       finalWidget.getSubmodules = widget.getSubmodules.bind( widget );
+                                       finalWidget.on( 'disable', function () { setTimeout( ApiSandbox.updateUI ); } );
+                               }
+                               finalWidget.setDisabled( true );
+                       }
+
+                       widget.setApiValue( pi[ 'default' ] );
+
+                       return finalWidget;
+               },
+
+               /**
+                * Parse an HTML string and call Util.fixupHTML()
+                *
+                * @param {string} html HTML to parse
+                * @return {jQuery}
+                */
+               parseHTML: function ( html ) {
+                       var $ret = $( $.parseHTML( html ) );
+                       return Util.fixupHTML( $ret );
+               },
+
+               /**
+                * Parse an i18n message and call Util.fixupHTML()
+                *
+                * @param {string} key Key of message to get
+                * @param {...Mixed} parameters Values for $N replacements
+                * @return {jQuery}
+                */
+               parseMsg: function () {
+                       var $ret = mw.message.apply( mw.message, arguments ).parseDom();
+                       return Util.fixupHTML( $ret );
+               },
+
+               /**
+                * Fix HTML for ApiSandbox display
+                *
+                * Fixes are:
+                * - Add target="_blank" to any links
+                *
+                * @param {jQuery} $html DOM to process
+                * @return {jQuery}
+                */
+               fixupHTML: function ( $html ) {
+                       $html.filter( 'a' ).add( $html.find( 'a' ) )
+                               .filter( '[href]:not([target])' )
+                               .attr( 'target', '_blank' );
+                       return $html;
+               },
+
+               /**
+                * Format a request and return a bunch of menu option widgets
+                *
+                * @param {Object} displayParams Query parameters, sanitized for display.
+                * @param {Object} rawParams Query parameters. You should probably use displayParams instead.
+                * @return {OO.ui.MenuOptionWidget[]} Each item's data should be an OO.ui.FieldLayout
+                */
+               formatRequest: function ( displayParams, rawParams ) {
+                       var jsonInput,
+                               items = [
+                                       new OO.ui.MenuOptionWidget( {
+                                               label: Util.parseMsg( 'apisandbox-request-format-url-label' ),
+                                               data: new OO.ui.FieldLayout(
+                                                       new OO.ui.TextInputWidget( {
+                                                               readOnly: true,
+                                                               value: mw.util.wikiScript( 'api' ) + '?' + $.param( displayParams )
+                                                       } ), {
+                                                               label: Util.parseMsg( 'apisandbox-request-url-label' )
+                                                       }
+                                               )
+                                       } ),
+                                       new OO.ui.MenuOptionWidget( {
+                                               label: Util.parseMsg( 'apisandbox-request-format-json-label' ),
+                                               data: new OO.ui.FieldLayout(
+                                                       jsonInput = new OO.ui.MultilineTextInputWidget( {
+                                                               classes: [ 'mw-apisandbox-textInputCode' ],
+                                                               readOnly: true,
+                                                               autosize: true,
+                                                               maxRows: 6,
+                                                               value: JSON.stringify( displayParams, null, '\t' )
+                                                       } ), {
+                                                               label: Util.parseMsg( 'apisandbox-request-json-label' )
+                                                       }
+                                               ).on( 'toggle', function ( visible ) {
+                                                       if ( visible ) {
+                                                               // Call updatePosition instead of adjustSize
+                                                               // because the latter has weird caching
+                                                               // behavior and the former bypasses it.
+                                                               jsonInput.updatePosition();
+                                                       }
+                                               } )
+                                       } )
+                               ];
+
+                       mw.hook( 'apisandbox.formatRequest' ).fire( items, displayParams, rawParams );
+
+                       return items;
+               },
+
+               /**
+                * Event handler for when formatDropdown's selection changes
+                */
+               onFormatDropdownChange: function () {
+                       var i,
+                               menu = formatDropdown.getMenu(),
+                               items = menu.getItems(),
+                               selectedField = menu.findSelectedItem() ? menu.findSelectedItem().getData() : null;
+
+                       for ( i = 0; i < items.length; i++ ) {
+                               items[ i ].getData().toggle( items[ i ].getData() === selectedField );
+                       }
+               }
+       };
+
+       /**
+       * Interface to ApiSandbox UI
+       *
+       * @class mw.special.ApiSandbox
+       */
+       ApiSandbox = {
+               /**
+                * Initialize the UI
+                *
+                * Automatically called on $.ready()
+                */
+               init: function () {
+                       var $toolbar;
+
+                       $content = $( '#mw-apisandbox' );
+
+                       windowManager = new OO.ui.WindowManager();
+                       $( 'body' ).append( windowManager.$element );
+                       windowManager.addWindows( {
+                               errorAlert: new OO.ui.MessageDialog()
+                       } );
+
+                       $toolbar = $( '<div>' )
+                               .addClass( 'mw-apisandbox-toolbar' )
+                               .append(
+                                       new OO.ui.ButtonWidget( {
+                                               label: mw.message( 'apisandbox-submit' ).text(),
+                                               flags: [ 'primary', 'progressive' ]
+                                       } ).on( 'click', ApiSandbox.sendRequest ).$element,
+                                       new OO.ui.ButtonWidget( {
+                                               label: mw.message( 'apisandbox-reset' ).text(),
+                                               flags: 'destructive'
+                                       } ).on( 'click', ApiSandbox.resetUI ).$element
+                               );
+
+                       booklet = new OO.ui.BookletLayout( {
+                               expanded: false,
+                               outlined: true,
+                               autoFocus: false
+                       } );
+
+                       panel = new OO.ui.PanelLayout( {
+                               classes: [ 'mw-apisandbox-container' ],
+                               content: [ booklet ],
+                               expanded: false,
+                               framed: true
+                       } );
+
+                       pages.main = new ApiSandbox.PageLayout( { key: 'main', path: 'main' } );
+
+                       // Parse the current hash string
+                       if ( !ApiSandbox.loadFromHash() ) {
+                               ApiSandbox.updateUI();
+                       }
+
+                       $( window ).on( 'hashchange', ApiSandbox.loadFromHash );
+
+                       $content
+                               .empty()
+                               .append( $( '<p>' ).append( Util.parseMsg( 'apisandbox-intro' ) ) )
+                               .append(
+                                       $( '<div>' ).attr( 'id', 'mw-apisandbox-ui' )
+                                               .append( $toolbar )
+                                               .append( panel.$element )
+                               );
+               },
+
+               /**
+                * Update the current query when the page hash changes
+                *
+                * @return {boolean} Successful
+                */
+               loadFromHash: function () {
+                       var params, m, re,
+                               hash = location.hash;
+
+                       if ( oldhash === hash ) {
+                               return false;
+                       }
+                       oldhash = hash;
+                       if ( hash === '' ) {
+                               return false;
+                       }
+
+                       // I'm surprised this doesn't seem to exist in jQuery or mw.util.
+                       params = {};
+                       hash = hash.replace( /\+/g, '%20' );
+                       re = /([^&=#]+)=?([^&#]*)/g;
+                       while ( ( m = re.exec( hash ) ) ) {
+                               params[ decodeURIComponent( m[ 1 ] ) ] = decodeURIComponent( m[ 2 ] );
+                       }
+
+                       ApiSandbox.updateUI( params );
+                       return true;
+               },
+
+               /**
+                * Update the pages in the booklet
+                *
+                * @param {Object} [params] Optional query parameters to load
+                */
+               updateUI: function ( params ) {
+                       var i, page, subpages, j, removePages,
+                               addPages = [];
+
+                       if ( !$.isPlainObject( params ) ) {
+                               params = undefined;
+                       }
+
+                       if ( updatingBooklet ) {
+                               return;
+                       }
+                       updatingBooklet = true;
+                       try {
+                               if ( params !== undefined ) {
+                                       pages.main.loadQueryParams( params );
+                               }
+                               addPages.push( pages.main );
+                               if ( resultPage !== null ) {
+                                       addPages.push( resultPage );
+                               }
+                               pages.main.apiCheckValid();
+
+                               i = 0;
+                               while ( addPages.length ) {
+                                       page = addPages.shift();
+                                       if ( bookletPages[ i ] !== page ) {
+                                               for ( j = i; j < bookletPages.length; j++ ) {
+                                                       if ( bookletPages[ j ].getName() === page.getName() ) {
+                                                               bookletPages.splice( j, 1 );
+                                                       }
+                                               }
+                                               bookletPages.splice( i, 0, page );
+                                               booklet.addPages( [ page ], i );
+                                       }
+                                       i++;
+
+                                       if ( page.getSubpages ) {
+                                               subpages = page.getSubpages();
+                                               for ( j = 0; j < subpages.length; j++ ) {
+                                                       if ( !pages.hasOwnProperty( subpages[ j ].key ) ) {
+                                                               subpages[ j ].indentLevel = page.indentLevel + 1;
+                                                               pages[ subpages[ j ].key ] = new ApiSandbox.PageLayout( subpages[ j ] );
+                                                       }
+                                                       if ( params !== undefined ) {
+                                                               pages[ subpages[ j ].key ].loadQueryParams( params );
+                                                       }
+                                                       addPages.splice( j, 0, pages[ subpages[ j ].key ] );
+                                                       pages[ subpages[ j ].key ].apiCheckValid();
+                                               }
+                                       }
+                               }
+
+                               if ( bookletPages.length > i ) {
+                                       removePages = bookletPages.splice( i, bookletPages.length - i );
+                                       booklet.removePages( removePages );
+                               }
+
+                               if ( !booklet.getCurrentPageName() ) {
+                                       booklet.selectFirstSelectablePage();
+                               }
+                       } finally {
+                               updatingBooklet = false;
+                       }
+               },
+
+               /**
+                * Reset button handler
+                */
+               resetUI: function () {
+                       suppressErrors = true;
+                       pages = {
+                               main: new ApiSandbox.PageLayout( { key: 'main', path: 'main' } )
+                       };
+                       resultPage = null;
+                       ApiSandbox.updateUI();
+               },
+
+               /**
+                * Submit button handler
+                *
+                * @param {Object} [params] Use this set of params instead of those in the form fields.
+                *   The form fields will be updated to match.
+                */
+               sendRequest: function ( params ) {
+                       var page, subpages, i, query, $result, $focus,
+                               progress, $progressText, progressLoading,
+                               deferreds = [],
+                               paramsAreForced = !!params,
+                               displayParams = {},
+                               tokenWidgets = [],
+                               checkPages = [ pages.main ];
+
+                       // Blur any focused widget before submit, because
+                       // OO.ui.ButtonWidget doesn't take focus itself (T128054)
+                       $focus = $( '#mw-apisandbox-ui' ).find( document.activeElement );
+                       if ( $focus.length ) {
+                               $focus[ 0 ].blur();
+                       }
+
+                       suppressErrors = false;
+
+                       // save widget state in params (or load from it if we are forced)
+                       if ( paramsAreForced ) {
+                               ApiSandbox.updateUI( params );
+                       }
+                       params = {};
+                       while ( checkPages.length ) {
+                               page = checkPages.shift();
+                               if ( page.tokenWidget ) {
+                                       tokenWidgets.push( page.tokenWidget );
+                               }
+                               deferreds = deferreds.concat( page.apiCheckValid() );
+                               page.getQueryParams( params, displayParams );
+                               subpages = page.getSubpages();
+                               for ( i = 0; i < subpages.length; i++ ) {
+                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
+                                               checkPages.push( pages[ subpages[ i ].key ] );
+                                       }
+                               }
+                       }
+
+                       if ( !paramsAreForced ) {
+                               // forced params means we are continuing a query; the base query should be preserved
+                               baseRequestParams = $.extend( {}, params );
+                       }
+
+                       $.when.apply( $, deferreds ).done( function () {
+                               var formatItems, menu, selectedLabel, deferred, actions, errorCount;
+
+                               // Count how many times `value` occurs in `array`.
+                               function countValues( value, array ) {
+                                       var count, i;
+                                       count = 0;
+                                       for ( i = 0; i < array.length; i++ ) {
+                                               if ( array[ i ] === value ) {
+                                                       count++;
+                                               }
+                                       }
+                                       return count;
+                               }
+
+                               errorCount = countValues( false, arguments );
+                               if ( errorCount > 0 ) {
+                                       actions = [
+                                               {
+                                                       action: 'accept',
+                                                       label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
+                                                       flags: 'primary'
+                                               }
+                                       ];
+                                       if ( tokenWidgets.length ) {
+                                               // Check all token widgets' validity separately
+                                               deferred = $.when.apply( $, tokenWidgets.map( function ( w ) {
+                                                       return w.apiCheckValid();
+                                               } ) );
+
+                                               deferred.done( function () {
+                                                       // If only the tokens are invalid, offer to fix them
+                                                       var tokenErrorCount = countValues( false, arguments );
+                                                       if ( tokenErrorCount === errorCount ) {
+                                                               delete actions[ 0 ].flags;
+                                                               actions.push( {
+                                                                       action: 'fix',
+                                                                       label: mw.message( 'apisandbox-results-fixtoken' ).text(),
+                                                                       flags: 'primary'
+                                                               } );
+                                                       }
+                                               } );
+                                       } else {
+                                               deferred = $.Deferred().resolve();
+                                       }
+                                       deferred.always( function () {
+                                               windowManager.openWindow( 'errorAlert', {
+                                                       title: Util.parseMsg( 'apisandbox-submit-invalid-fields-title' ),
+                                                       message: Util.parseMsg( 'apisandbox-submit-invalid-fields-message' ),
+                                                       actions: actions
+                                               } ).closed.then( function ( data ) {
+                                                       if ( data && data.action === 'fix' ) {
+                                                               ApiSandbox.fixTokenAndResend();
+                                                       }
+                                               } );
+                                       } );
+                                       return;
+                               }
+
+                               query = $.param( displayParams );
+
+                               formatItems = Util.formatRequest( displayParams, params );
+
+                               // Force a 'fm' format with wrappedhtml=1, if available
+                               if ( params.format !== undefined ) {
+                                       if ( availableFormats.hasOwnProperty( params.format + 'fm' ) ) {
+                                               params.format = params.format + 'fm';
+                                       }
+                                       if ( params.format.substr( -2 ) === 'fm' ) {
+                                               params.wrappedhtml = 1;
+                                       }
+                               }
+
+                               progressLoading = false;
+                               $progressText = $( '<span>' ).text( mw.message( 'apisandbox-sending-request' ).text() );
+                               progress = new OO.ui.ProgressBarWidget( {
+                                       progress: false,
+                                       $content: $progressText
+                               } );
+
+                               $result = $( '<div>' )
+                                       .append( progress.$element );
+
+                               resultPage = page = new OO.ui.PageLayout( '|results|', { expanded: false } );
+                               page.setupOutlineItem = function () {
+                                       this.outlineItem.setLabel( mw.message( 'apisandbox-results' ).text() );
+                               };
+
+                               if ( !formatDropdown ) {
+                                       formatDropdown = new OO.ui.DropdownWidget( {
+                                               menu: { items: [] },
+                                               $overlay: true
+                                       } );
+                                       formatDropdown.getMenu().on( 'select', Util.onFormatDropdownChange );
+                               }
+
+                               menu = formatDropdown.getMenu();
+                               selectedLabel = menu.findSelectedItem() ? menu.findSelectedItem().getLabel() : '';
+                               if ( typeof selectedLabel !== 'string' ) {
+                                       selectedLabel = selectedLabel.text();
+                               }
+                               menu.clearItems().addItems( formatItems );
+                               menu.chooseItem( menu.getItemFromLabel( selectedLabel ) || menu.findFirstSelectableItem() );
+
+                               // Fire the event to update field visibilities
+                               Util.onFormatDropdownChange();
+
+                               page.$element.empty()
+                                       .append(
+                                               new OO.ui.FieldLayout(
+                                                       formatDropdown, {
+                                                               label: Util.parseMsg( 'apisandbox-request-selectformat-label' )
+                                                       }
+                                               ).$element,
+                                               formatItems.map( function ( item ) {
+                                                       return item.getData().$element;
+                                               } ),
+                                               $result
+                                       );
+                               ApiSandbox.updateUI();
+                               booklet.setPage( '|results|' );
+
+                               location.href = oldhash = '#' + query;
+
+                               api.post( params, {
+                                       contentType: 'multipart/form-data',
+                                       dataType: 'text',
+                                       xhr: function () {
+                                               var xhr = new window.XMLHttpRequest();
+                                               xhr.upload.addEventListener( 'progress', function ( e ) {
+                                                       if ( !progressLoading ) {
+                                                               if ( e.lengthComputable ) {
+                                                                       progress.setProgress( e.loaded * 100 / e.total );
+                                                               } else {
+                                                                       progress.setProgress( false );
+                                                               }
+                                                       }
+                                               } );
+                                               xhr.addEventListener( 'progress', function ( e ) {
+                                                       if ( !progressLoading ) {
+                                                               progressLoading = true;
+                                                               $progressText.text( mw.message( 'apisandbox-loading-results' ).text() );
+                                                       }
+                                                       if ( e.lengthComputable ) {
+                                                               progress.setProgress( e.loaded * 100 / e.total );
+                                                       } else {
+                                                               progress.setProgress( false );
+                                                       }
+                                               } );
+                                               return xhr;
+                                       }
+                               } )
+                                       .catch( function ( code, data, result, jqXHR ) {
+                                               var deferred = $.Deferred();
+
+                                               if ( code !== 'http' ) {
+                                                       // Not really an error, work around mw.Api thinking it is.
+                                                       deferred.resolve( result, jqXHR );
+                                               } else {
+                                                       // Just forward it.
+                                                       deferred.reject.apply( deferred, arguments );
+                                               }
+                                               return deferred.promise();
+                                       } )
+                                       .then( function ( data, jqXHR ) {
+                                               var m, loadTime, button, clear,
+                                                       ct = jqXHR.getResponseHeader( 'Content-Type' ),
+                                                       loginSuppressed = jqXHR.getResponseHeader( 'MediaWiki-Login-Suppressed' ) || 'false';
+
+                                               $result.empty();
+                                               if ( loginSuppressed !== 'false' ) {
+                                                       $( '<div>' )
+                                                               .addClass( 'warning' )
+                                                               .append( Util.parseMsg( 'apisandbox-results-login-suppressed' ) )
+                                                               .appendTo( $result );
+                                               }
+                                               if ( /^text\/mediawiki-api-prettyprint-wrapped(?:;|$)/.test( ct ) ) {
+                                                       data = JSON.parse( data );
+                                                       if ( data.modules.length ) {
+                                                               mw.loader.load( data.modules );
+                                                       }
+                                                       if ( data.status && data.status !== 200 ) {
+                                                               $( '<div>' )
+                                                                       .addClass( 'api-pretty-header api-pretty-status' )
+                                                                       .append( Util.parseMsg( 'api-format-prettyprint-status', data.status, data.statustext ) )
+                                                                       .appendTo( $result );
+                                                       }
+                                                       $result.append( Util.parseHTML( data.html ) );
+                                                       loadTime = data.time;
+                                               } else if ( ( m = data.match( /<pre[ >][\s\S]*<\/pre>/ ) ) ) {
+                                                       $result.append( Util.parseHTML( m[ 0 ] ) );
+                                                       if ( ( m = data.match( /"wgBackendResponseTime":\s*(\d+)/ ) ) ) {
+                                                               loadTime = parseInt( m[ 1 ], 10 );
+                                                       }
+                                               } else {
+                                                       $( '<pre>' )
+                                                               .addClass( 'api-pretty-content' )
+                                                               .text( data )
+                                                               .appendTo( $result );
+                                               }
+                                               if ( paramsAreForced || data[ 'continue' ] ) {
+                                                       $result.append(
+                                                               $( '<div>' ).append(
+                                                                       new OO.ui.ButtonWidget( {
+                                                                               label: mw.message( 'apisandbox-continue' ).text()
+                                                                       } ).on( 'click', function () {
+                                                                               ApiSandbox.sendRequest( $.extend( {}, baseRequestParams, data[ 'continue' ] ) );
+                                                                       } ).setDisabled( !data[ 'continue' ] ).$element,
+                                                                       ( clear = new OO.ui.ButtonWidget( {
+                                                                               label: mw.message( 'apisandbox-continue-clear' ).text()
+                                                                       } ).on( 'click', function () {
+                                                                               ApiSandbox.updateUI( baseRequestParams );
+                                                                               clear.setDisabled( true );
+                                                                               booklet.setPage( '|results|' );
+                                                                       } ).setDisabled( !paramsAreForced ) ).$element,
+                                                                       new OO.ui.PopupButtonWidget( {
+                                                                               $overlay: true,
+                                                                               framed: false,
+                                                                               icon: 'info',
+                                                                               popup: {
+                                                                                       $content: $( '<div>' ).append( Util.parseMsg( 'apisandbox-continue-help' ) ),
+                                                                                       padded: true,
+                                                                                       width: 'auto'
+                                                                               }
+                                                                       } ).$element
+                                                               )
+                                                       );
+                                               }
+                                               if ( typeof loadTime === 'number' ) {
+                                                       $result.append(
+                                                               $( '<div>' ).append(
+                                                                       new OO.ui.LabelWidget( {
+                                                                               label: mw.message( 'apisandbox-request-time', loadTime ).text()
+                                                                       } ).$element
+                                                               )
+                                                       );
+                                               }
+
+                                               if ( jqXHR.getResponseHeader( 'MediaWiki-API-Error' ) === 'badtoken' ) {
+                                                       // Flush all saved tokens in case one of them is the bad one.
+                                                       Util.markTokensBad();
+                                                       button = new OO.ui.ButtonWidget( {
+                                                               label: mw.message( 'apisandbox-results-fixtoken' ).text()
+                                                       } );
+                                                       button.on( 'click', ApiSandbox.fixTokenAndResend )
+                                                               .on( 'click', button.setDisabled, [ true ], button )
+                                                               .$element.appendTo( $result );
+                                               }
+                                       }, function ( code, data ) {
+                                               var details = 'HTTP error: ' + data.exception;
+                                               $result.empty()
+                                                       .append(
+                                                               new OO.ui.LabelWidget( {
+                                                                       label: mw.message( 'apisandbox-results-error', details ).text(),
+                                                                       classes: [ 'error' ]
+                                                               } ).$element
+                                                       );
+                                       } );
+                       } );
+               },
+
+               /**
+                * Handler for the "Correct token and resubmit" button
+                *
+                * Used on a 'badtoken' error, it re-fetches token parameters for all
+                * pages and then re-submits the query.
+                */
+               fixTokenAndResend: function () {
+                       var page, subpages, i, k,
+                               ok = true,
+                               tokenWait = { dummy: true },
+                               checkPages = [ pages.main ],
+                               success = function ( k ) {
+                                       delete tokenWait[ k ];
+                                       if ( ok && $.isEmptyObject( tokenWait ) ) {
+                                               ApiSandbox.sendRequest();
+                                       }
+                               },
+                               failure = function ( k ) {
+                                       delete tokenWait[ k ];
+                                       ok = false;
+                               };
+
+                       while ( checkPages.length ) {
+                               page = checkPages.shift();
+
+                               if ( page.tokenWidget ) {
+                                       k = page.apiModule + page.tokenWidget.paramInfo.name;
+                                       tokenWait[ k ] = page.tokenWidget.fetchToken();
+                                       tokenWait[ k ]
+                                               .done( success.bind( page.tokenWidget, k ) )
+                                               .fail( failure.bind( page.tokenWidget, k ) );
+                               }
+
+                               subpages = page.getSubpages();
+                               for ( i = 0; i < subpages.length; i++ ) {
+                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
+                                               checkPages.push( pages[ subpages[ i ].key ] );
+                                       }
+                               }
+                       }
+
+                       success( 'dummy', '' );
+               },
+
+               /**
+                * Reset validity indicators for all widgets
+                */
+               updateValidityIndicators: function () {
+                       var page, subpages, i,
+                               checkPages = [ pages.main ];
+
+                       while ( checkPages.length ) {
+                               page = checkPages.shift();
+                               page.apiCheckValid();
+                               subpages = page.getSubpages();
+                               for ( i = 0; i < subpages.length; i++ ) {
+                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
+                                               checkPages.push( pages[ subpages[ i ].key ] );
+                                       }
+                               }
+                       }
+               }
+       };
+
+       /**
+        * PageLayout for API modules
+        *
+        * @class
+        * @private
+        * @extends OO.ui.PageLayout
+        * @constructor
+        * @param {Object} [config] Configuration options
+        */
+       ApiSandbox.PageLayout = function ( config ) {
+               config = $.extend( { prefix: '', expanded: false }, config );
+               this.displayText = config.key;
+               this.apiModule = config.path;
+               this.prefix = config.prefix;
+               this.paramInfo = null;
+               this.apiIsValid = true;
+               this.loadFromQueryParams = null;
+               this.widgets = {};
+               this.tokenWidget = null;
+               this.indentLevel = config.indentLevel ? config.indentLevel : 0;
+               ApiSandbox.PageLayout[ 'super' ].call( this, config.key, config );
+               this.loadParamInfo();
+       };
+       OO.inheritClass( ApiSandbox.PageLayout, OO.ui.PageLayout );
+       ApiSandbox.PageLayout.prototype.setupOutlineItem = function () {
+               this.outlineItem.setLevel( this.indentLevel );
+               this.outlineItem.setLabel( this.displayText );
+               this.outlineItem.setIcon( this.apiIsValid || suppressErrors ? null : 'alert' );
+               this.outlineItem.setIconTitle(
+                       this.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
+               );
+       };
+
+       /**
+        * Fetch module information for this page's module, then create UI
+        */
+       ApiSandbox.PageLayout.prototype.loadParamInfo = function () {
+               var dynamicFieldset, dynamicParamNameWidget,
+                       that = this,
+                       removeDynamicParamWidget = function ( name, layout ) {
+                               dynamicFieldset.removeItems( [ layout ] );
+                               delete that.widgets[ name ];
+                       },
+                       addDynamicParamWidget = function () {
+                               var name, layout, widget, button;
+
+                               // Check name is filled in
+                               name = dynamicParamNameWidget.getValue().trim();
+                               if ( name === '' ) {
+                                       dynamicParamNameWidget.focus();
+                                       return;
+                               }
+
+                               if ( that.widgets[ name ] !== undefined ) {
+                                       windowManager.openWindow( 'errorAlert', {
+                                               title: Util.parseMsg( 'apisandbox-dynamic-error-exists', name ),
+                                               actions: [
+                                                       {
+                                                               action: 'accept',
+                                                               label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
+                                                               flags: 'primary'
+                                                       }
+                                               ]
+                                       } );
+                                       return;
+                               }
+
+                               widget = Util.createWidgetForParameter( {
+                                       name: name,
+                                       type: 'string',
+                                       'default': ''
+                               }, {
+                                       nooptional: true
+                               } );
+                               button = new OO.ui.ButtonWidget( {
+                                       icon: 'trash',
+                                       flags: 'destructive'
+                               } );
+                               layout = new OO.ui.ActionFieldLayout(
+                                       widget,
+                                       button,
+                                       {
+                                               label: name,
+                                               align: 'left'
+                                       }
+                               );
+                               button.on( 'click', removeDynamicParamWidget, [ name, layout ] );
+                               that.widgets[ name ] = widget;
+                               dynamicFieldset.addItems( [ layout ], dynamicFieldset.getItems().length - 1 );
+                               widget.focus();
+
+                               dynamicParamNameWidget.setValue( '' );
+                       };
+
+               this.$element.empty()
+                       .append( new OO.ui.ProgressBarWidget( {
+                               progress: false,
+                               text: mw.message( 'apisandbox-loading', this.displayText ).text()
+                       } ).$element );
+
+               Util.fetchModuleInfo( this.apiModule )
+                       .done( function ( pi ) {
+                               var prefix, i, j, descriptionContainer, widget, layoutConfig, button, widgetField, helpField, tmp, flag, count,
+                                       items = [],
+                                       deprecatedItems = [],
+                                       buttons = [],
+                                       filterFmModules = function ( v ) {
+                                               return v.substr( -2 ) !== 'fm' ||
+                                                       !availableFormats.hasOwnProperty( v.substr( 0, v.length - 2 ) );
+                                       },
+                                       widgetLabelOnClick = function () {
+                                               var f = this.getField();
+                                               if ( $.isFunction( f.setDisabled ) ) {
+                                                       f.setDisabled( false );
+                                               }
+                                               if ( $.isFunction( f.focus ) ) {
+                                                       f.focus();
+                                               }
+                                       };
+
+                               // This is something of a hack. We always want the 'format' and
+                               // 'action' parameters from the main module to be specified,
+                               // and for 'format' we also want to simplify the dropdown since
+                               // we always send the 'fm' variant.
+                               if ( that.apiModule === 'main' ) {
+                                       for ( i = 0; i < pi.parameters.length; i++ ) {
+                                               if ( pi.parameters[ i ].name === 'action' ) {
+                                                       pi.parameters[ i ].required = true;
+                                                       delete pi.parameters[ i ][ 'default' ];
+                                               }
+                                               if ( pi.parameters[ i ].name === 'format' ) {
+                                                       tmp = pi.parameters[ i ].type;
+                                                       for ( j = 0; j < tmp.length; j++ ) {
+                                                               availableFormats[ tmp[ j ] ] = true;
+                                                       }
+                                                       pi.parameters[ i ].type = tmp.filter( filterFmModules );
+                                                       pi.parameters[ i ][ 'default' ] = 'json';
+                                                       pi.parameters[ i ].required = true;
+                                               }
+                                       }
+                               }
+
+                               // Hide the 'wrappedhtml' parameter on format modules
+                               if ( pi.group === 'format' ) {
+                                       pi.parameters = pi.parameters.filter( function ( p ) {
+                                               return p.name !== 'wrappedhtml';
+                                       } );
+                               }
+
+                               that.paramInfo = pi;
+
+                               items.push( new OO.ui.FieldLayout(
+                                       new OO.ui.Widget( {} ).toggle( false ), {
+                                               align: 'top',
+                                               label: Util.parseHTML( pi.description )
+                                       }
+                               ) );
+
+                               if ( pi.helpurls.length ) {
+                                       buttons.push( new OO.ui.PopupButtonWidget( {
+                                               $overlay: true,
+                                               label: mw.message( 'apisandbox-helpurls' ).text(),
+                                               icon: 'help',
+                                               popup: {
+                                                       width: 'auto',
+                                                       padded: true,
+                                                       $content: $( '<ul>' ).append( pi.helpurls.map( function ( link ) {
+                                                               return $( '<li>' ).append( $( '<a>' )
+                                                                       .attr( { href: link, target: '_blank' } )
+                                                                       .text( link )
+                                                               );
+                                                       } ) )
+                                               }
+                                       } ) );
+                               }
+
+                               if ( pi.examples.length ) {
+                                       buttons.push( new OO.ui.PopupButtonWidget( {
+                                               $overlay: true,
+                                               label: mw.message( 'apisandbox-examples' ).text(),
+                                               icon: 'code',
+                                               popup: {
+                                                       width: 'auto',
+                                                       padded: true,
+                                                       $content: $( '<ul>' ).append( pi.examples.map( function ( example ) {
+                                                               var a = $( '<a>' )
+                                                                       .attr( 'href', '#' + example.query )
+                                                                       .html( example.description );
+                                                               a.find( 'a' ).contents().unwrap(); // Can't nest links
+                                                               return $( '<li>' ).append( a );
+                                                       } ) )
+                                               }
+                                       } ) );
+                               }
+
+                               if ( buttons.length ) {
+                                       items.push( new OO.ui.FieldLayout(
+                                               new OO.ui.ButtonGroupWidget( {
+                                                       items: buttons
+                                               } ), { align: 'top' }
+                                       ) );
+                               }
+
+                               if ( pi.parameters.length ) {
+                                       prefix = that.prefix + pi.prefix;
+                                       for ( i = 0; i < pi.parameters.length; i++ ) {
+                                               widget = Util.createWidgetForParameter( pi.parameters[ i ] );
+                                               that.widgets[ prefix + pi.parameters[ i ].name ] = widget;
+                                               if ( pi.parameters[ i ].tokentype ) {
+                                                       that.tokenWidget = widget;
+                                               }
+
+                                               descriptionContainer = $( '<div>' );
+
+                                               tmp = Util.parseHTML( pi.parameters[ i ].description );
+                                               tmp.filter( 'dl' ).makeCollapsible( {
+                                                       collapsed: true
+                                               } ).children( '.mw-collapsible-toggle' ).each( function () {
+                                                       var $this = $( this );
+                                                       $this.parent().prev( 'p' ).append( $this );
+                                               } );
+                                               descriptionContainer.append( $( '<div>' ).addClass( 'description' ).append( tmp ) );
+
+                                               if ( pi.parameters[ i ].info && pi.parameters[ i ].info.length ) {
+                                                       for ( j = 0; j < pi.parameters[ i ].info.length; j++ ) {
+                                                               descriptionContainer.append( $( '<div>' )
+                                                                       .addClass( 'info' )
+                                                                       .append( Util.parseHTML( pi.parameters[ i ].info[ j ] ) )
+                                                               );
+                                                       }
+                                               }
+                                               flag = true;
+                                               count = 1e100;
+                                               switch ( pi.parameters[ i ].type ) {
+                                                       case 'namespace':
+                                                               flag = false;
+                                                               count = mw.config.get( 'wgFormattedNamespaces' ).length;
+                                                               break;
+
+                                                       case 'limit':
+                                                               if ( pi.parameters[ i ].highmax !== undefined ) {
+                                                                       descriptionContainer.append( $( '<div>' )
+                                                                               .addClass( 'info' )
+                                                                               .append(
+                                                                                       Util.parseMsg(
+                                                                                               'api-help-param-limit2', pi.parameters[ i ].max, pi.parameters[ i ].highmax
+                                                                                       ),
+                                                                                       ' ',
+                                                                                       Util.parseMsg( 'apisandbox-param-limit' )
+                                                                               )
+                                                                       );
+                                                               } else {
+                                                                       descriptionContainer.append( $( '<div>' )
+                                                                               .addClass( 'info' )
+                                                                               .append(
+                                                                                       Util.parseMsg( 'api-help-param-limit', pi.parameters[ i ].max ),
+                                                                                       ' ',
+                                                                                       Util.parseMsg( 'apisandbox-param-limit' )
+                                                                               )
+                                                                       );
+                                                               }
+                                                               break;
+
+                                                       case 'integer':
+                                                               tmp = '';
+                                                               if ( pi.parameters[ i ].min !== undefined ) {
+                                                                       tmp += 'min';
+                                                               }
+                                                               if ( pi.parameters[ i ].max !== undefined ) {
+                                                                       tmp += 'max';
+                                                               }
+                                                               if ( tmp !== '' ) {
+                                                                       descriptionContainer.append( $( '<div>' )
+                                                                               .addClass( 'info' )
+                                                                               .append( Util.parseMsg(
+                                                                                       'api-help-param-integer-' + tmp,
+                                                                                       Util.apiBool( pi.parameters[ i ].multi ) ? 2 : 1,
+                                                                                       pi.parameters[ i ].min, pi.parameters[ i ].max
+                                                                               ) )
+                                                                       );
+                                                               }
+                                                               break;
+
+                                                       default:
+                                                               if ( Array.isArray( pi.parameters[ i ].type ) ) {
+                                                                       flag = false;
+                                                                       count = pi.parameters[ i ].type.length;
+                                                               }
+                                                               break;
+                                               }
+                                               if ( Util.apiBool( pi.parameters[ i ].multi ) ) {
+                                                       tmp = [];
+                                                       if ( flag && !( widget instanceof OO.ui.TagMultiselectWidget ) &&
+                                                               !(
+                                                                       widget instanceof OptionalWidget &&
+                                                                       widget.widget instanceof OO.ui.TagMultiselectWidget
+                                                               )
+                                                       ) {
+                                                               tmp.push( mw.message( 'api-help-param-multi-separate' ).parse() );
+                                                       }
+                                                       if ( count > pi.parameters[ i ].lowlimit ) {
+                                                               tmp.push(
+                                                                       mw.message( 'api-help-param-multi-max',
+                                                                               pi.parameters[ i ].lowlimit, pi.parameters[ i ].highlimit
+                                                                       ).parse()
+                                                               );
+                                                       }
+                                                       if ( tmp.length ) {
+                                                               descriptionContainer.append( $( '<div>' )
+                                                                       .addClass( 'info' )
+                                                                       .append( Util.parseHTML( tmp.join( ' ' ) ) )
+                                                               );
+                                                       }
+                                               }
+                                               if ( 'maxbytes' in pi.parameters[ i ] ) {
+                                                       descriptionContainer.append( $( '<div>' )
+                                                               .addClass( 'info' )
+                                                               .append( Util.parseMsg( 'api-help-param-maxbytes', pi.parameters[ i ].maxbytes ) )
+                                                       );
+                                               }
+                                               if ( 'maxchars' in pi.parameters[ i ] ) {
+                                                       descriptionContainer.append( $( '<div>' )
+                                                               .addClass( 'info' )
+                                                               .append( Util.parseMsg( 'api-help-param-maxchars', pi.parameters[ i ].maxchars ) )
+                                                       );
+                                               }
+                                               helpField = new OO.ui.FieldLayout(
+                                                       new OO.ui.Widget( {
+                                                               $content: '\xa0',
+                                                               classes: [ 'mw-apisandbox-spacer' ]
+                                                       } ), {
+                                                               align: 'inline',
+                                                               classes: [ 'mw-apisandbox-help-field' ],
+                                                               label: descriptionContainer
+                                                       }
+                                               );
+
+                                               layoutConfig = {
+                                                       align: 'left',
+                                                       classes: [ 'mw-apisandbox-widget-field' ],
+                                                       label: prefix + pi.parameters[ i ].name
+                                               };
+
+                                               if ( pi.parameters[ i ].tokentype ) {
+                                                       button = new OO.ui.ButtonWidget( {
+                                                               label: mw.message( 'apisandbox-fetch-token' ).text()
+                                                       } );
+                                                       button.on( 'click', widget.fetchToken, [], widget );
+
+                                                       widgetField = new OO.ui.ActionFieldLayout( widget, button, layoutConfig );
+                                               } else {
+                                                       widgetField = new OO.ui.FieldLayout( widget, layoutConfig );
+                                               }
+
+                                               // We need our own click handler on the widget label to
+                                               // turn off the disablement.
+                                               widgetField.$label.on( 'click', widgetLabelOnClick.bind( widgetField ) );
+
+                                               // Don't grey out the label when the field is disabled,
+                                               // it makes it too hard to read and our "disabled"
+                                               // isn't really disabled.
+                                               widgetField.onFieldDisable( false );
+                                               widgetField.onFieldDisable = $.noop;
+
+                                               if ( Util.apiBool( pi.parameters[ i ].deprecated ) ) {
+                                                       deprecatedItems.push( widgetField, helpField );
+                                               } else {
+                                                       items.push( widgetField, helpField );
+                                               }
+                                       }
+                               }
+
+                               if ( !pi.parameters.length && !Util.apiBool( pi.dynamicparameters ) ) {
+                                       items.push( new OO.ui.FieldLayout(
+                                               new OO.ui.Widget( {} ).toggle( false ), {
+                                                       align: 'top',
+                                                       label: Util.parseMsg( 'apisandbox-no-parameters' )
+                                               }
+                                       ) );
+                               }
+
+                               that.$element.empty();
+
+                               new OO.ui.FieldsetLayout( {
+                                       label: that.displayText
+                               } ).addItems( items )
+                                       .$element.appendTo( that.$element );
+
+                               if ( Util.apiBool( pi.dynamicparameters ) ) {
+                                       dynamicFieldset = new OO.ui.FieldsetLayout();
+                                       dynamicParamNameWidget = new OO.ui.TextInputWidget( {
+                                               placeholder: mw.message( 'apisandbox-dynamic-parameters-add-placeholder' ).text()
+                                       } ).on( 'enter', addDynamicParamWidget );
+                                       dynamicFieldset.addItems( [
+                                               new OO.ui.FieldLayout(
+                                                       new OO.ui.Widget( {} ).toggle( false ), {
+                                                               align: 'top',
+                                                               label: Util.parseHTML( pi.dynamicparameters )
+                                                       }
+                                               ),
+                                               new OO.ui.ActionFieldLayout(
+                                                       dynamicParamNameWidget,
+                                                       new OO.ui.ButtonWidget( {
+                                                               icon: 'add',
+                                                               flags: 'progressive'
+                                                       } ).on( 'click', addDynamicParamWidget ),
+                                                       {
+                                                               label: mw.message( 'apisandbox-dynamic-parameters-add-label' ).text(),
+                                                               align: 'left'
+                                                       }
+                                               )
+                                       ] );
+                                       $( '<fieldset>' )
+                                               .append(
+                                                       $( '<legend>' ).text( mw.message( 'apisandbox-dynamic-parameters' ).text() ),
+                                                       dynamicFieldset.$element
+                                               )
+                                               .appendTo( that.$element );
+                               }
+
+                               if ( deprecatedItems.length ) {
+                                       tmp = new OO.ui.FieldsetLayout().addItems( deprecatedItems ).toggle( false );
+                                       $( '<fieldset>' )
+                                               .append(
+                                                       $( '<legend>' ).append(
+                                                               new OO.ui.ToggleButtonWidget( {
+                                                                       label: mw.message( 'apisandbox-deprecated-parameters' ).text()
+                                                               } ).on( 'change', tmp.toggle, [], tmp ).$element
+                                                       ),
+                                                       tmp.$element
+                                               )
+                                               .appendTo( that.$element );
+                               }
+
+                               // Load stored params, if any, then update the booklet if we
+                               // have subpages (or else just update our valid-indicator).
+                               tmp = that.loadFromQueryParams;
+                               that.loadFromQueryParams = null;
+                               if ( $.isPlainObject( tmp ) ) {
+                                       that.loadQueryParams( tmp );
+                               }
+                               if ( that.getSubpages().length > 0 ) {
+                                       ApiSandbox.updateUI( tmp );
+                               } else {
+                                       that.apiCheckValid();
+                               }
+                       } ).fail( function ( code, detail ) {
+                               that.$element.empty()
+                                       .append(
+                                               new OO.ui.LabelWidget( {
+                                                       label: mw.message( 'apisandbox-load-error', that.apiModule, detail ).text(),
+                                                       classes: [ 'error' ]
+                                               } ).$element,
+                                               new OO.ui.ButtonWidget( {
+                                                       label: mw.message( 'apisandbox-retry' ).text()
+                                               } ).on( 'click', that.loadParamInfo, [], that ).$element
+                                       );
+                       } );
+       };
+
+       /**
+        * Check that all widgets on the page are in a valid state.
+        *
+        * @return {jQuery.Promise[]} One promise for each widget, resolved with `false` if invalid
+        */
+       ApiSandbox.PageLayout.prototype.apiCheckValid = function () {
+               var promises, that = this;
+
+               if ( this.paramInfo === null ) {
+                       return [];
+               } else {
+                       promises = $.map( this.widgets, function ( widget ) {
+                               return widget.apiCheckValid();
+                       } );
+                       $.when.apply( $, promises ).then( function () {
+                               that.apiIsValid = $.inArray( false, arguments ) === -1;
+                               if ( that.getOutlineItem() ) {
+                                       that.getOutlineItem().setIcon( that.apiIsValid || suppressErrors ? null : 'alert' );
+                                       that.getOutlineItem().setIconTitle(
+                                               that.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
+                                       );
+                               }
+                       } );
+                       return promises;
+               }
+       };
+
+       /**
+        * Load form fields from query parameters
+        *
+        * @param {Object} params
+        */
+       ApiSandbox.PageLayout.prototype.loadQueryParams = function ( params ) {
+               if ( this.paramInfo === null ) {
+                       this.loadFromQueryParams = params;
+               } else {
+                       $.each( this.widgets, function ( name, widget ) {
+                               var v = params.hasOwnProperty( name ) ? params[ name ] : undefined;
+                               widget.setApiValue( v );
+                       } );
+               }
+       };
+
+       /**
+        * Load query params from form fields
+        *
+        * @param {Object} params Write query parameters into this object
+        * @param {Object} displayParams Write query parameters for display into this object
+        */
+       ApiSandbox.PageLayout.prototype.getQueryParams = function ( params, displayParams ) {
+               $.each( this.widgets, function ( name, widget ) {
+                       var value = widget.getApiValue();
+                       if ( value !== undefined ) {
+                               params[ name ] = value;
+                               if ( $.isFunction( widget.getApiValueForDisplay ) ) {
+                                       value = widget.getApiValueForDisplay();
+                               }
+                               displayParams[ name ] = value;
+                       }
+               } );
+       };
+
+       /**
+        * Fetch a list of subpage names loaded by this page
+        *
+        * @return {Array}
+        */
+       ApiSandbox.PageLayout.prototype.getSubpages = function () {
+               var ret = [];
+               $.each( this.widgets, function ( name, widget ) {
+                       var submodules, i;
+                       if ( $.isFunction( widget.getSubmodules ) ) {
+                               submodules = widget.getSubmodules();
+                               for ( i = 0; i < submodules.length; i++ ) {
+                                       ret.push( {
+                                               key: name + '=' + submodules[ i ].value,
+                                               path: submodules[ i ].path,
+                                               prefix: widget.paramInfo.submoduleparamprefix || ''
+                                       } );
+                               }
+                       }
+               } );
+               return ret;
+       };
+
+       $( ApiSandbox.init );
+
+       module.exports = ApiSandbox;
+
+}( jQuery, mediaWiki, OO ) );
diff --git a/resources/src/mediawiki.special.block.js b/resources/src/mediawiki.special.block.js
new file mode 100644 (file)
index 0000000..180f040
--- /dev/null
@@ -0,0 +1,58 @@
+/*!
+ * JavaScript for Special:Block
+ */
+( function ( mw, $ ) {
+       // Like OO.ui.infuse(), but if the element doesn't exist, return null instead of throwing an exception.
+       function infuseOrNull( elem ) {
+               try {
+                       return OO.ui.infuse( elem );
+               } catch ( er ) {
+                       return null;
+               }
+       }
+
+       $( function () {
+               // This code is also loaded on the "block succeeded" page where there is no form,
+               // so username and expiry fields might also be missing.
+               var blockTargetWidget = infuseOrNull( 'mw-bi-target' ),
+                       anonOnlyField = infuseOrNull( $( '#mw-input-wpHardBlock' ).closest( '.oo-ui-fieldLayout' ) ),
+                       enableAutoblockField = infuseOrNull( $( '#mw-input-wpAutoBlock' ).closest( '.oo-ui-fieldLayout' ) ),
+                       hideUserField = infuseOrNull( $( '#mw-input-wpHideUser' ).closest( '.oo-ui-fieldLayout' ) ),
+                       watchUserField = infuseOrNull( $( '#mw-input-wpWatch' ).closest( '.oo-ui-fieldLayout' ) ),
+                       expiryWidget = infuseOrNull( 'mw-input-wpExpiry' );
+
+               function updateBlockOptions() {
+                       var blocktarget = blockTargetWidget.getValue().trim(),
+                               isEmpty = blocktarget === '',
+                               isIp = mw.util.isIPAddress( blocktarget, true ),
+                               isIpRange = isIp && blocktarget.match( /\/\d+$/ ),
+                               isNonEmptyIp = isIp && !isEmpty,
+                               expiryValue = expiryWidget.getValue(),
+                               // infinityValues  are the values the SpecialBlock class accepts as infinity (sf. wfIsInfinity)
+                               infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ],
+                               isIndefinite = infinityValues.indexOf( expiryValue ) !== -1;
+
+                       if ( enableAutoblockField ) {
+                               enableAutoblockField.toggle( !( isNonEmptyIp ) );
+                       }
+                       if ( hideUserField ) {
+                               hideUserField.toggle( !( isNonEmptyIp || !isIndefinite ) );
+                       }
+                       if ( anonOnlyField ) {
+                               anonOnlyField.toggle( !( !isIp && !isEmpty ) );
+                       }
+                       if ( watchUserField ) {
+                               watchUserField.toggle( !( isIpRange && !isEmpty ) );
+                       }
+               }
+
+               if ( blockTargetWidget ) {
+                       // Bind functions so they're checked whenever stuff changes
+                       blockTargetWidget.on( 'change', updateBlockOptions );
+                       expiryWidget.on( 'change', updateBlockOptions );
+
+                       // Call them now to set initial state (ie. Special:Block/Foobar?wpBlockExpiry=2+hours)
+                       updateBlockOptions();
+               }
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.changecredentials.js b/resources/src/mediawiki.special.changecredentials.js
new file mode 100644 (file)
index 0000000..ad8a4f4
--- /dev/null
@@ -0,0 +1,55 @@
+/*!
+ * JavaScript for change credentials form.
+ */
+( function ( mw, $, OO ) {
+       mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
+               var api = new mw.Api();
+
+               $root.find( '.mw-changecredentials-validate-password.oo-ui-fieldLayout' ).each( function () {
+                       var currentApiPromise,
+                               self = OO.ui.FieldLayout.static.infuse( $( this ) );
+
+                       self.getField().setValidation( function ( password ) {
+                               var d;
+
+                               if ( currentApiPromise ) {
+                                       currentApiPromise.abort();
+                                       currentApiPromise = undefined;
+                               }
+
+                               password = password.trim();
+
+                               if ( password === '' ) {
+                                       self.setErrors( [] );
+                                       return true;
+                               }
+
+                               d = $.Deferred();
+                               currentApiPromise = api.post( {
+                                       action: 'validatepassword',
+                                       password: password,
+                                       formatversion: 2,
+                                       errorformat: 'html',
+                                       errorsuselocal: true,
+                                       uselang: mw.config.get( 'wgUserLanguage' )
+                               } ).done( function ( resp ) {
+                                       var pwinfo = resp.validatepassword,
+                                               good = pwinfo.validity === 'Good',
+                                               errors = [];
+
+                                       currentApiPromise = undefined;
+
+                                       if ( !good ) {
+                                               pwinfo.validitymessages.map( function ( m ) {
+                                                       errors.push( new OO.ui.HtmlSnippet( m.html ) );
+                                               } );
+                                       }
+                                       self.setErrors( errors );
+                                       d.resolve( good );
+                               } ).fail( d.reject );
+
+                               return d.promise( { abort: currentApiPromise.abort } );
+                       } );
+               } );
+       } );
+}( mediaWiki, jQuery, OO ) );
diff --git a/resources/src/mediawiki.special.changeslist.css b/resources/src/mediawiki.special.changeslist.css
new file mode 100644 (file)
index 0000000..65860ea
--- /dev/null
@@ -0,0 +1,56 @@
+/*!
+ * Styling for Special:Watchlist and Special:RecentChanges
+ */
+
+.mw-changeslist-line-watched .mw-title {
+       font-weight: bold;
+}
+
+/*
+ * Titles, including username links, and also tag names
+ * are prone to getting jumbled up
+ * with other titles, usernames, etc. in mixed RTL-LTR environment.
+ */
+.mw-changeslist .mw-tag-marker,
+.mw-changeslist .mw-title {
+       unicode-bidi: embed;
+}
+
+/* Colored watchlist and recent changes numbers */
+.mw-plusminus-pos {
+       color: #006400; /* dark green */
+}
+
+.mw-plusminus-neg {
+       color: #8b0000; /* dark red */
+}
+
+.mw-plusminus-null {
+       color: #a2a9b1; /* gray */
+}
+
+/*
+ * Bidi-isolate these numbers.
+ * See https://phabricator.wikimedia.org/T93484
+ */
+.mw-plusminus-pos,
+.mw-plusminus-neg,
+.mw-plusminus-null {
+       unicode-bidi: -moz-isolate;
+       unicode-bidi: isolate;
+}
+
+/* Prevent FOUC if legend is initially collapsed */
+.mw-changeslist-legend.mw-collapsed .mw-collapsible-content {
+       display: none;
+}
+
+.mw-changeslist-legend.mw-collapsed {
+       margin-bottom: 0;
+}
+
+/* Prevent pushing down the content if legend is collapsed */
+.mw-changeslist-legend.mw-collapsed ~ ul:first-of-type > li:first-child,
+.mw-changeslist-legend.mw-collapsed + h4 + div > table.mw-changeslist-line:first-child {
+       clear: right;
+}
diff --git a/resources/src/mediawiki.special.changeslist.enhanced.css b/resources/src/mediawiki.special.changeslist.enhanced.css
new file mode 100644 (file)
index 0000000..cb11332
--- /dev/null
@@ -0,0 +1,69 @@
+/*!
+ * Styling for Special:Watchlist and Special:RecentChanges when preference 'usenewrc'
+ * a.k.a. Enhanced Recent Changes is enabled.
+ */
+
+table.mw-enhanced-rc {
+       border: 0;
+       border-spacing: 0;
+}
+
+table.mw-enhanced-rc th,
+table.mw-enhanced-rc td {
+       padding: 0;
+       vertical-align: top;
+}
+
+td.mw-enhanced-rc {
+       white-space: nowrap;
+       font-family: monospace, monospace;
+}
+
+.mw-enhanced-rc-time {
+       font-family: monospace, monospace;
+}
+
+table.mw-enhanced-rc td.mw-enhanced-rc-nested {
+       padding-left: 1em;
+}
+
+/* Show/hide arrows in enhanced changeslist */
+.mw-enhanced-rc .collapsible-expander {
+       float: none;
+}
+
+/* If JS is disabled, the arrows or the placeholder space shouldn't be shown */
+.client-nojs .mw-enhancedchanges-arrow-space {
+       display: none;
+}
+
+/*
+ * And if it's enabled, let's optimize the collapsing a little: hide the rows
+ * that would be hidden by jquery.makeCollapsible with CSS to save us some
+ * reflows and repaints. This doesn't work on browsers that don't fully support
+ * CSS2 (IE6), but it's okay, this will be done in JavaScript with old degraded
+ * performance instead.
+ */
+.client-js table.mw-enhanced-rc.mw-collapsed tr + tr {
+       display: none;
+}
+
+.mw-enhancedchanges-arrow {
+       padding-top: 2px;
+}
+
+.mw-enhancedchanges-arrow-space {
+       display: inline-block;
+       *display: inline; /* IE7 and below */
+       zoom: 1;
+       width: 15px;
+       height: 15px;
+}
+
+.mw-enhanced-watched .mw-enhanced-rc-time {
+       font-weight: bold;
+}
+
+span.changedby {
+       font-size: 95%;
+}
diff --git a/resources/src/mediawiki.special.changeslist.legend.css b/resources/src/mediawiki.special.changeslist.legend.css
new file mode 100644 (file)
index 0000000..14f6aee
--- /dev/null
@@ -0,0 +1,33 @@
+/*!
+ * Styling for changes list legend
+ */
+
+.mw-changeslist-legend {
+       float: right;
+       margin-left: 1em;
+       margin-bottom: 0.5em;
+       clear: right;
+       font-size: 85%;
+       line-height: 1.2em;
+       padding: 0.5em;
+       border: 1px solid #ddd;
+}
+
+.mw-changeslist-legend dl {
+       /* Parent element defines sufficient padding */
+       margin-bottom: 0;
+}
+
+.mw-changeslist-legend dt {
+       float: left;
+       margin: 0 0.5em 0 0;
+}
+
+.mw-changeslist-legend dd {
+       margin-left: 1.5em;
+}
+
+.mw-changeslist-legend dt,
+.mw-changeslist-legend dd {
+       line-height: 1.3em;
+}
diff --git a/resources/src/mediawiki.special.changeslist.legend.js b/resources/src/mediawiki.special.changeslist.legend.js
new file mode 100644 (file)
index 0000000..0792762
--- /dev/null
@@ -0,0 +1,24 @@
+/*!
+ * Script for changes list legend
+ */
+
+/* Remember the collapse state of the legend on recent changes and watchlist pages. */
+( function ( mw ) {
+       var
+               cookieName = 'changeslist-state',
+               // Expanded by default
+               doCollapsibleLegend = function ( $container ) {
+                       $container.find( '.mw-changeslist-legend' )
+                               .makeCollapsible( {
+                                       collapsed: mw.cookie.get( cookieName ) === 'collapsed'
+                               } )
+                               .on( 'beforeExpand.mw-collapsible', function () {
+                                       mw.cookie.set( cookieName, 'expanded' );
+                               } )
+                               .on( 'beforeCollapse.mw-collapsible', function () {
+                                       mw.cookie.set( cookieName, 'collapsed' );
+                               } );
+               };
+
+       mw.hook( 'wikipage.content' ).add( doCollapsibleLegend );
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki.special.changeslist.visitedstatus.js b/resources/src/mediawiki.special.changeslist.visitedstatus.js
new file mode 100644 (file)
index 0000000..6b25327
--- /dev/null
@@ -0,0 +1,12 @@
+/*!
+ * JavaScript for Special:Watchlist
+ */
+( function ( $ ) {
+       $( function () {
+               $( '.mw-changeslist-line-watched .mw-title a' ).on( 'click', function () {
+                       $( this )
+                               .closest( '.mw-changeslist-line-watched' )
+                               .removeClass( 'mw-changeslist-line-watched' );
+               } );
+       } );
+}( jQuery ) );
diff --git a/resources/src/mediawiki.special.comparepages.styles.less b/resources/src/mediawiki.special.comparepages.styles.less
new file mode 100644 (file)
index 0000000..87b7a8b
--- /dev/null
@@ -0,0 +1,19 @@
+@import 'mediawiki.mixins';
+
+.mw-special-ComparePages .mw-htmlform-ooui-wrapper {
+       width: 100%;
+}
+
+.mw-special-ComparePages .oo-ui-layout.oo-ui-panelLayout.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed {
+       float: left;
+       width: 49%;
+       .box-sizing( border-box );
+}
+
+.mw-special-ComparePages .oo-ui-layout.oo-ui-panelLayout.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed:nth-of-type( 2 ) {
+       margin-left: 2%;
+}
+
+.mw-special-ComparePages .mw-htmlform-submit-buttons {
+       clear: both;
+}
diff --git a/resources/src/mediawiki.special.contributions.js b/resources/src/mediawiki.special.contributions.js
new file mode 100644 (file)
index 0000000..f65a257
--- /dev/null
@@ -0,0 +1,12 @@
+( function ( mw, $ ) {
+       $( function () {
+               var startInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-start' ),
+                       endInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-end' );
+
+               startInput.on( 'deactivate', function ( userSelected ) {
+                       if ( userSelected ) {
+                               endInput.focus();
+                       }
+               } );
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.edittags.js b/resources/src/mediawiki.special.edittags.js
new file mode 100644 (file)
index 0000000..4f51e9b
--- /dev/null
@@ -0,0 +1,38 @@
+/*!
+ * JavaScript for Special:EditTags
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+                       summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+                       $wpReason = $( '#wpReason' ),
+                       $tagList = $( '#mw-edittags-tag-list' );
+
+               if ( $tagList.length ) {
+                       $tagList.chosen( {
+                               /* eslint-disable camelcase */
+                               placeholder_text_multiple: mw.msg( 'tags-edit-chosen-placeholder' ),
+                               no_results_text: mw.msg( 'tags-edit-chosen-no-results' )
+                               /* eslint-enable camelcase */
+                       } );
+               }
+
+               $( '#mw-edittags-remove-all' ).on( 'change', function ( e ) {
+                       $( '.mw-edittags-remove-checkbox' ).prop( 'checked', e.target.checked );
+               } );
+               $( '.mw-edittags-remove-checkbox' ).on( 'change', function ( e ) {
+                       if ( !e.target.checked ) {
+                               $( '#mw-edittags-remove-all' ).prop( 'checked', false );
+                       }
+               } );
+
+               // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+               // use maxLength because it's leaving room for log entry text.
+               if ( summaryCodePointLimit ) {
+                       $wpReason.codePointLimit();
+               } else if ( summaryByteLimit ) {
+                       $wpReason.byteLimit();
+               }
+       } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.edittags.styles.css b/resources/src/mediawiki.special.edittags.styles.css
new file mode 100644 (file)
index 0000000..204009c
--- /dev/null
@@ -0,0 +1,15 @@
+/*!
+ * Styling for Special:EditTags and action=editchangetags
+ */
+#mw-edittags-tags-selector td {
+       vertical-align: top;
+}
+
+#mw-edittags-tags-selector-multi td {
+       vertical-align: top;
+       padding-right: 1.5em;
+}
+
+#mw-edittags-tag-list {
+       min-width: 20em;
+}
diff --git a/resources/src/mediawiki.special.import.js b/resources/src/mediawiki.special.import.js
new file mode 100644 (file)
index 0000000..2cb96af
--- /dev/null
@@ -0,0 +1,37 @@
+/*!
+ * JavaScript for Special:Import
+ */
+( function ( $ ) {
+       var subprojectListAlreadyShown;
+       function updateImportSubprojectList() {
+               var $projectField = $( '#mw-import-table-interwiki #interwiki' ),
+                       $subprojectField = $projectField.parent().find( '#subproject' ),
+                       $selected = $projectField.find( ':selected' ),
+                       oldValue = $subprojectField.val(),
+                       option, options;
+
+               if ( $selected.attr( 'data-subprojects' ) ) {
+                       options = $selected.attr( 'data-subprojects' ).split( ' ' ).map( function ( el ) {
+                               option = document.createElement( 'option' );
+                               option.appendChild( document.createTextNode( el ) );
+                               option.setAttribute( 'value', el );
+                               if ( oldValue === el && subprojectListAlreadyShown === true ) {
+                                       option.setAttribute( 'selected', 'selected' );
+                               }
+                               return option;
+                       } );
+                       $subprojectField.show().empty().append( options );
+                       subprojectListAlreadyShown = true;
+               } else {
+                       $subprojectField.hide();
+               }
+       }
+
+       $( function () {
+               var $projectField = $( '#mw-import-table-interwiki #interwiki' );
+               if ( $projectField.length ) {
+                       $projectField.change( updateImportSubprojectList );
+                       updateImportSubprojectList();
+               }
+       } );
+}( jQuery ) );
diff --git a/resources/src/mediawiki.special.movePage.css b/resources/src/mediawiki.special.movePage.css
new file mode 100644 (file)
index 0000000..9428fed
--- /dev/null
@@ -0,0 +1,7 @@
+/*!
+ * Styles for Special:MovePage
+ */
+
+.movepage-wrapper {
+       width: 50em;
+}
diff --git a/resources/src/mediawiki.special.movePage.js b/resources/src/mediawiki.special.movePage.js
new file mode 100644 (file)
index 0000000..d828396
--- /dev/null
@@ -0,0 +1,23 @@
+/*!
+ * JavaScript for Special:MovePage
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+                       summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+                       wpReason = OO.ui.infuse( $( '#wpReason' ) );
+
+               // Infuse for pretty dropdown
+               OO.ui.infuse( $( '#wpNewTitle' ) );
+               // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+               if ( summaryCodePointLimit ) {
+                       mw.widgets.visibleCodePointLimit( wpReason, summaryCodePointLimit );
+               } else if ( summaryByteLimit ) {
+                       mw.widgets.visibleByteLimit( wpReason, summaryByteLimit );
+               }
+               // Infuse for nicer "help" popup
+               if ( $( '#wpMovetalk-field' ).length ) {
+                       OO.ui.infuse( $( '#wpMovetalk-field' ) );
+               }
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.pageLanguage.js b/resources/src/mediawiki.special.pageLanguage.js
new file mode 100644 (file)
index 0000000..edfbe1e
--- /dev/null
@@ -0,0 +1,11 @@
+/*!
+ * JavaScript module used on Special:PageLanguage
+ */
+( function ( $, OO ) {
+       $( function () {
+               // Select the 'Language select' option if user is trying to select language
+               OO.ui.infuse( 'mw-pl-languageselector' ).on( 'change', function () {
+                       OO.ui.infuse( 'mw-pl-options' ).setValue( '2' );
+               } );
+       } );
+}( jQuery, OO ) );
diff --git a/resources/src/mediawiki.special.pagesWithProp.css b/resources/src/mediawiki.special.pagesWithProp.css
new file mode 100644 (file)
index 0000000..7ef75d0
--- /dev/null
@@ -0,0 +1,4 @@
+/* Distinguish actual data from information about it being hidden visually */
+.prop-value-hidden {
+       font-style: italic;
+}
diff --git a/resources/src/mediawiki.special.preferences.ooui/editfont.js b/resources/src/mediawiki.special.preferences.ooui/editfont.js
new file mode 100644 (file)
index 0000000..fe48886
--- /dev/null
@@ -0,0 +1,32 @@
+/*!
+ * JavaScript for Special:Preferences: editfont field enhancements.
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var widget, lastValue;
+
+               try {
+                       widget = OO.ui.infuse( $( '#mw-input-wpeditfont' ) );
+               } catch ( err ) {
+                       // This preference could theoretically be disabled ($wgHiddenPrefs)
+                       return;
+               }
+
+               // Style options
+               widget.dropdownWidget.menu.items.forEach( function ( item ) {
+                       item.$label.addClass( 'mw-editfont-' + item.getData() );
+               } );
+
+               function updateLabel( value ) {
+                       // Style selected item label
+                       widget.dropdownWidget.$label
+                               .removeClass( 'mw-editfont-' + lastValue )
+                               .addClass( 'mw-editfont-' + value );
+                       lastValue = value;
+               }
+
+               widget.on( 'change', updateLabel );
+               updateLabel( widget.getValue() );
+
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.preferences.ooui/tabs.js b/resources/src/mediawiki.special.preferences.ooui/tabs.js
new file mode 100644 (file)
index 0000000..c948ff0
--- /dev/null
@@ -0,0 +1,138 @@
+/*!
+ * JavaScript for Special:Preferences: Tab navigation.
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var $preferences, tabs, wrapper, previousTab;
+
+               $preferences = $( '#preferences' );
+
+               // Make sure the accessibility tip is selectable so that screen reader users take notice,
+               // but hide it per default to reduce interface clutter. Also make sure it becomes visible
+               // when selected. Similar to jquery.mw-jump
+               $( '<div>' ).addClass( 'mw-navigation-hint' )
+                       .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
+                       .attr( 'tabIndex', 0 )
+                       .on( 'focus blur', function ( e ) {
+                               if ( e.type === 'blur' || e.type === 'focusout' ) {
+                                       $( this ).css( 'height', '0' );
+                               } else {
+                                       $( this ).css( 'height', 'auto' );
+                               }
+                       } ).prependTo( '#mw-content-text' );
+
+               tabs = new OO.ui.IndexLayout( {
+                       expanded: false,
+                       // Do not remove focus from the tabs menu after choosing a tab
+                       autoFocus: false
+               } );
+
+               mw.config.get( 'wgPreferencesTabs' ).forEach( function ( tabConfig ) {
+                       var panel, $panelContents;
+
+                       panel = new OO.ui.TabPanelLayout( tabConfig.name, {
+                               expanded: false,
+                               label: tabConfig.label
+                       } );
+                       $panelContents = $( '#mw-prefsection-' + tabConfig.name );
+
+                       // Hide the unnecessary PHP PanelLayouts
+                       // (Do not use .remove(), as that would remove event handlers for everything inside them)
+                       $panelContents.parent().detach();
+
+                       panel.$element.append( $panelContents );
+                       tabs.addTabPanels( [ panel ] );
+
+                       // Remove duplicate labels
+                       // (This must be after .addTabPanels(), otherwise the tab item doesn't exist yet)
+                       $panelContents.children( 'legend' ).remove();
+                       $panelContents.attr( 'aria-labelledby', panel.getTabItem().getElementId() );
+               } );
+
+               wrapper = new OO.ui.PanelLayout( {
+                       expanded: false,
+                       padded: false,
+                       framed: true
+               } );
+               wrapper.$element.append( tabs.$element );
+               $preferences.prepend( wrapper.$element );
+
+               function updateHash( panel ) {
+                       var scrollTop, active;
+                       // Handle hash manually to prevent jumping,
+                       // therefore save and restore scrollTop to prevent jumping.
+                       scrollTop = $( window ).scrollTop();
+                       // Changing the hash apparently causes keyboard focus to be lost?
+                       // Save and restore it. This makes no sense though.
+                       active = document.activeElement;
+                       location.hash = '#mw-prefsection-' + panel.getName();
+                       if ( active ) {
+                               active.focus();
+                       }
+                       $( window ).scrollTop( scrollTop );
+               }
+
+               tabs.on( 'set', updateHash );
+
+               /**
+                * @ignore
+                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
+                * @param {string} [mode] A hash will be set according to the current
+                *  open section. Set mode 'noHash' to supress this.
+                */
+               function switchPrefTab( name, mode ) {
+                       if ( mode === 'noHash' ) {
+                               tabs.off( 'set', updateHash );
+                       }
+                       tabs.setTabPanel( name );
+                       if ( mode === 'noHash' ) {
+                               tabs.on( 'set', updateHash );
+                       }
+               }
+
+               // Jump to correct section as indicated by the hash.
+               // This function is called onload and onhashchange.
+               function detectHash() {
+                       var hash = location.hash,
+                               matchedElement, parentSection;
+                       if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
+                               mw.storage.session.remove( 'mwpreferences-prevTab' );
+                               switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
+                       } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
+                               matchedElement = document.getElementById( hash.slice( 1 ) );
+                               parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
+                               if ( parentSection.length ) {
+                                       mw.storage.session.remove( 'mwpreferences-prevTab' );
+                                       // Switch to proper tab and scroll to selected item.
+                                       switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
+                                       matchedElement.scrollIntoView();
+                               }
+                       }
+               }
+
+               $( window ).on( 'hashchange', function () {
+                       var hash = location.hash;
+                       if ( hash.match( /^#mw-[\w-]+/ ) ) {
+                               detectHash();
+                       } else if ( hash === '' ) {
+                               switchPrefTab( 'personal', 'noHash' );
+                       }
+               } )
+                       // Run the function immediately to select the proper tab on startup.
+                       .trigger( 'hashchange' );
+
+               // Restore the active tab after saving the preferences
+               previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
+               if ( previousTab ) {
+                       switchPrefTab( previousTab, 'noHash' );
+                       // Deleting the key, the tab states should be reset until we press Save
+                       mw.storage.session.remove( 'mwpreferences-prevTab' );
+               }
+
+               $( '#mw-prefs-form' ).on( 'submit', function () {
+                       var value = tabs.getCurrentTabPanelName();
+                       mw.storage.session.set( 'mwpreferences-prevTab', value );
+               } );
+
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.preferences.styles.css b/resources/src/mediawiki.special.preferences.styles.css
new file mode 100644 (file)
index 0000000..33b630a
--- /dev/null
@@ -0,0 +1,47 @@
+/* Reuses colors from mediawiki.legacy/shared.css */
+.mw-email-not-authenticated .mw-input,
+.mw-email-none .mw-input {
+       border: 1px solid #fde29b;
+       background-color: #fdf1d1;
+       color: #000;
+}
+/* Authenticated email field has its own class too. Unstyled by default */
+/*
+.mw-email-authenticated .mw-input { }
+*/
+/* This breaks due to nolabel styling */
+#preferences > fieldset td.mw-label {
+       width: 20%;
+}
+
+#preferences > fieldset table {
+       width: 100%;
+}
+#preferences > fieldset table.mw-htmlform-matrix {
+       width: auto;
+}
+
+/* The CSS below is also for JS enabled version, because we want to prevent FOUC */
+
+/*
+ * Hide, but keep accessible for screen-readers.
+ * Like .mw-jump, #jump-to-nav from shared.css
+ */
+.client-js .mw-navigation-hint {
+       overflow: hidden;
+       height: 0;
+       zoom: 1;
+}
+
+.client-nojs #preftoc {
+       display: none;
+}
+
+.client-js #preferences > fieldset {
+       display: none;
+}
+
+/* Only the 1st tab is shown by default in JS mode */
+.client-js #preferences #mw-prefsection-personal {
+       display: block;
+}
diff --git a/resources/src/mediawiki.special.preferences.styles.ooui.css b/resources/src/mediawiki.special.preferences.styles.ooui.css
new file mode 100644 (file)
index 0000000..8810318
--- /dev/null
@@ -0,0 +1,118 @@
+/* Reuses colors from mediawiki.legacy/shared.css */
+.mw-email-not-authenticated .oo-ui-labelWidget,
+.mw-email-none .oo-ui-labelWidget {
+       border: 1px solid #fde29b;
+       background-color: #fdf1d1;
+       color: #000;
+       padding: 0.5em;
+}
+/* Authenticated email field has its own class too. Unstyled by default */
+/*
+.mw-email-authenticated .oo-ui-labelWidget { }
+*/
+
+/* This is needed because add extra buttons in a weird way */
+.mw-prefs-buttons .mw-htmlform-submit-buttons {
+       margin: 0;
+       display: inline;
+}
+
+.mw-prefs-buttons {
+       margin-top: 1em;
+}
+
+#prefcontrol {
+       margin-right: 0.5em;
+}
+
+/*
+ * Hide, but keep accessible for screen-readers.
+ * Like .mw-jump, #jump-to-nav from shared.css
+ */
+.client-js .mw-navigation-hint {
+       overflow: hidden;
+       height: 0;
+       zoom: 1;
+}
+
+/* Override OOUI styles so that dropdowns near the bottom of the form don't get clipped,
+ * e.g.'Appearance' / 'Threshold for stub link formatting'. This is hacky and bad, it would be
+ * better solved by setting overlays for the widgets, but we can't do it from PHP... */
+#preferences .oo-ui-panelLayout {
+       position: static;
+       overflow: visible;
+       -webkit-transform: none;
+       transform: none;
+}
+
+#preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
+       border-color: #c8ccd1;
+       border-width: 1px 0 0;
+       border-radius: 0;
+       padding-left: 0;
+       padding-right: 0;
+       box-shadow: none;
+}
+
+/* Tweak the margins to reduce the shifting of form contents
+ * after JS code loads and rearranges the page */
+.client-js #preferences > .oo-ui-panelLayout {
+       margin: 1em 0;
+}
+
+.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
+       margin-left: 0.25em;
+}
+
+.client-js #preferences .oo-ui-tabPanelLayout {
+       padding-top: 0.5em;
+       padding-bottom: 0.5em;
+}
+
+.client-js #preferences .oo-ui-tabPanelLayout .oo-ui-panelLayout-framed {
+       margin-left: 0;
+       margin-bottom: 0;
+       border: 0;
+       padding-top: 0;
+}
+
+.client-js #preferences > .oo-ui-panelLayout > .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header {
+       margin-bottom: 1em;
+}
+
+/* Make the "Basic information" section more compact */
+/* OOUI's `align: 'left'` for FieldLayouts sucks, so we do our own */
+#mw-htmlform-info > .oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header {
+       width: 20%;
+       display: inline-block;
+       vertical-align: middle;
+       padding: 0;
+}
+
+#mw-htmlform-info > .oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-help {
+       margin-right: 0;
+}
+
+#mw-htmlform-info > .oo-ui-fieldLayout.oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
+       width: 80%;
+       display: inline-block;
+       vertical-align: middle;
+}
+
+/* Expand the dropdown and textfield of "Time zone" field to the */
+/* usual maximum width and display them on separate lines. */
+#wpTimeCorrection .oo-ui-dropdownInputWidget,
+#wpTimeCorrection .oo-ui-textInputWidget {
+       display: block;
+       max-width: 50em;
+}
+
+#wpTimeCorrection .oo-ui-textInputWidget {
+       margin-top: 0.5em;
+}
+
+/* HACK: expand width of gadget descriptions.
+ * This should be moved to the Gadgets extension */
+#mw-htmlform-gadgets .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body {
+       max-width: none;
+}
diff --git a/resources/src/mediawiki.special.preferences/confirmClose.js b/resources/src/mediawiki.special.preferences/confirmClose.js
new file mode 100644 (file)
index 0000000..244154b
--- /dev/null
@@ -0,0 +1,86 @@
+/*!
+ * JavaScript for Special:Preferences: Enable save button and prevent the window being accidentally
+ * closed when any form field is changed.
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var allowCloseWindow, saveButton, restoreButton,
+                       oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
+
+               // Check if all of the form values are unchanged.
+               // (This function could be changed to infuse and check OOUI widgets, but that would only make it
+               // slower and more complicated. It works fine to treat them as HTML elements.)
+               function isPrefsChanged() {
+                       var inputs = $( '#mw-prefs-form :input[name]' ),
+                               input, $input, inputType,
+                               index, optIndex,
+                               opt;
+
+                       for ( index = 0; index < inputs.length; index++ ) {
+                               input = inputs[ index ];
+                               $input = $( input );
+
+                               // Different types of inputs have different methods for accessing defaults
+                               if ( $input.is( 'select' ) ) {
+                                       // <select> has the property defaultSelected for each option
+                                       for ( optIndex = 0; optIndex < input.options.length; optIndex++ ) {
+                                               opt = input.options[ optIndex ];
+                                               if ( opt.selected !== opt.defaultSelected ) {
+                                                       return true;
+                                               }
+                                       }
+                               } else if ( $input.is( 'input' ) || $input.is( 'textarea' ) ) {
+                                       // <input> has defaultValue or defaultChecked
+                                       inputType = input.type;
+                                       if ( inputType === 'radio' || inputType === 'checkbox' ) {
+                                               if ( input.checked !== input.defaultChecked ) {
+                                                       return true;
+                                               }
+                                       } else if ( input.value !== input.defaultValue ) {
+                                               return true;
+                                       }
+                               }
+                       }
+
+                       return false;
+               }
+
+               if ( oouiEnabled ) {
+                       saveButton = OO.ui.infuse( $( '#prefcontrol' ) );
+                       restoreButton = OO.ui.infuse( $( '#mw-prefs-restoreprefs' ) );
+
+                       // Disable the button to save preferences unless preferences have changed
+                       // Check if preferences have been changed before JS has finished loading
+                       saveButton.setDisabled( !isPrefsChanged() );
+                       $( '#preferences .oo-ui-fieldsetLayout' ).on( 'change keyup mouseup', function () {
+                               saveButton.setDisabled( !isPrefsChanged() );
+                       } );
+               } else {
+                       // Disable the button to save preferences unless preferences have changed
+                       // Check if preferences have been changed before JS has finished loading
+                       $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
+                       $( '#preferences > fieldset' ).on( 'change keyup mouseup', function () {
+                               $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
+                       } );
+               }
+
+               // Set up a message to notify users if they try to leave the page without
+               // saving.
+               allowCloseWindow = mw.confirmCloseWindow( {
+                       test: isPrefsChanged,
+                       message: mw.msg( 'prefswarning-warning', mw.msg( 'saveprefs' ) ),
+                       namespace: 'prefswarning'
+               } );
+               $( '#mw-prefs-form' ).on( 'submit', $.proxy( allowCloseWindow, 'release' ) );
+               if ( oouiEnabled ) {
+                       restoreButton.on( 'click', function () {
+                               allowCloseWindow.release();
+                               // The default behavior of events in OOUI is always prevented. Follow the link manually.
+                               // Note that middle-click etc. still works, as it doesn't emit a OOUI 'click' event.
+                               location.href = restoreButton.getHref();
+                       } );
+               } else {
+                       $( '#mw-prefs-restoreprefs' ).on( 'click', $.proxy( allowCloseWindow, 'release' ) );
+               }
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.preferences/convertmessagebox.js b/resources/src/mediawiki.special.preferences/convertmessagebox.js
new file mode 100644 (file)
index 0000000..e6b7432
--- /dev/null
@@ -0,0 +1,9 @@
+/*!
+ * JavaScript for Special:Preferences: Check for successbox to replace with notifications.
+ */
+( function ( $ ) {
+       $( function () {
+               var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' );
+               convertmessagebox();
+       } );
+}( jQuery ) );
diff --git a/resources/src/mediawiki.special.preferences/personalEmail.js b/resources/src/mediawiki.special.preferences/personalEmail.js
new file mode 100644 (file)
index 0000000..f934d59
--- /dev/null
@@ -0,0 +1,24 @@
+/*!
+ * JavaScript for Special:Preferences: Email preferences better UX
+ */
+( function ( $ ) {
+       $( function () {
+               var allowEmail, allowEmailFromNewUsers;
+
+               allowEmail = $( '#wpAllowEmail' );
+               allowEmailFromNewUsers = $( '#wpAllowEmailFromNewUsers' );
+
+               function toggleDisabled() {
+                       if ( allowEmail.is( ':checked' ) && allowEmail.is( ':enabled' ) ) {
+                               allowEmailFromNewUsers.prop( 'disabled', false );
+                       } else {
+                               allowEmailFromNewUsers.prop( 'disabled', true );
+                       }
+               }
+
+               if ( allowEmail ) {
+                       allowEmail.on( 'change', toggleDisabled );
+                       toggleDisabled();
+               }
+       } );
+}( jQuery ) );
diff --git a/resources/src/mediawiki.special.preferences/tabs.legacy.js b/resources/src/mediawiki.special.preferences/tabs.legacy.js
new file mode 100644 (file)
index 0000000..0d97d68
--- /dev/null
@@ -0,0 +1,143 @@
+/*!
+ * JavaScript for Special:Preferences: Tab navigation.
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var $preftoc, $preferences, $fieldsets, labelFunc, previousTab;
+
+               labelFunc = function () {
+                       return this.id.replace( /^mw-prefsection/g, 'preftab' );
+               };
+
+               $preftoc = $( '#preftoc' );
+               $preferences = $( '#preferences' );
+
+               $fieldsets = $preferences.children( 'fieldset' )
+                       .attr( {
+                               role: 'tabpanel',
+                               'aria-labelledby': labelFunc
+                       } );
+               $fieldsets.not( '#mw-prefsection-personal' )
+                       .hide()
+                       .attr( 'aria-hidden', 'true' );
+
+               // T115692: The following is kept for backwards compatibility with older skins
+               $preferences.addClass( 'jsprefs' );
+               $fieldsets.addClass( 'prefsection' );
+               $fieldsets.children( 'legend' ).addClass( 'mainLegend' );
+
+               // Make sure the accessibility tip is selectable so that screen reader users take notice,
+               // but hide it per default to reduce interface clutter. Also make sure it becomes visible
+               // when selected. Similar to jquery.mw-jump
+               $( '<div>' ).addClass( 'mw-navigation-hint' )
+                       .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
+                       .attr( 'tabIndex', 0 )
+                       .on( 'focus blur', function ( e ) {
+                               if ( e.type === 'blur' || e.type === 'focusout' ) {
+                                       $( this ).css( 'height', '0' );
+                               } else {
+                                       $( this ).css( 'height', 'auto' );
+                               }
+                       } ).insertBefore( $preftoc );
+
+               /**
+                * It uses document.getElementById for security reasons (HTML injections in $()).
+                *
+                * @ignore
+                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
+                * @param {string} [mode] A hash will be set according to the current
+                *  open section. Set mode 'noHash' to surpress this.
+                */
+               function switchPrefTab( name, mode ) {
+                       var $tab, scrollTop;
+                       // Handle hash manually to prevent jumping,
+                       // therefore save and restore scrollTop to prevent jumping.
+                       scrollTop = $( window ).scrollTop();
+                       if ( mode !== 'noHash' ) {
+                               location.hash = '#mw-prefsection-' + name;
+                       }
+                       $( window ).scrollTop( scrollTop );
+
+                       $preftoc.find( 'li' ).removeClass( 'selected' )
+                               .find( 'a' ).attr( {
+                                       tabIndex: -1,
+                                       'aria-selected': 'false'
+                               } );
+
+                       $tab = $( document.getElementById( 'preftab-' + name ) );
+                       if ( $tab.length ) {
+                               $tab.attr( {
+                                       tabIndex: 0,
+                                       'aria-selected': 'true'
+                               } ).focus()
+                                       .parent().addClass( 'selected' );
+
+                               $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
+                               $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' );
+                       }
+               }
+
+               // Enable keyboard users to use left and right keys to switch tabs
+               $preftoc.on( 'keydown', function ( event ) {
+                       var keyLeft = 37,
+                               keyRight = 39,
+                               $el;
+
+                       if ( event.keyCode === keyLeft ) {
+                               $el = $( '#preftoc li.selected' ).prev().find( 'a' );
+                       } else if ( event.keyCode === keyRight ) {
+                               $el = $( '#preftoc li.selected' ).next().find( 'a' );
+                       } else {
+                               return;
+                       }
+                       if ( $el.length > 0 ) {
+                               switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) );
+                       }
+               } );
+
+               // Jump to correct section as indicated by the hash.
+               // This function is called onload and onhashchange.
+               function detectHash() {
+                       var hash = location.hash,
+                               matchedElement, parentSection;
+                       if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
+                               mw.storage.session.remove( 'mwpreferences-prevTab' );
+                               switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
+                       } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
+                               matchedElement = document.getElementById( hash.slice( 1 ) );
+                               parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
+                               if ( parentSection.length ) {
+                                       mw.storage.session.remove( 'mwpreferences-prevTab' );
+                                       // Switch to proper tab and scroll to selected item.
+                                       switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
+                                       matchedElement.scrollIntoView();
+                               }
+                       }
+               }
+
+               $( window ).on( 'hashchange', function () {
+                       var hash = location.hash;
+                       if ( hash.match( /^#mw-[\w-]+/ ) ) {
+                               detectHash();
+                       } else if ( hash === '' ) {
+                               switchPrefTab( 'personal', 'noHash' );
+                       }
+               } )
+                       // Run the function immediately to select the proper tab on startup.
+                       .trigger( 'hashchange' );
+
+               // Restore the active tab after saving the preferences
+               previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
+               if ( previousTab ) {
+                       switchPrefTab( previousTab, 'noHash' );
+                       // Deleting the key, the tab states should be reset until we press Save
+                       mw.storage.session.remove( 'mwpreferences-prevTab' );
+               }
+
+               $( '#mw-prefs-form' ).on( 'submit', function () {
+                       var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
+                       mw.storage.session.set( 'mwpreferences-prevTab', value );
+               } );
+
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.preferences/timezone.js b/resources/src/mediawiki.special.preferences/timezone.js
new file mode 100644 (file)
index 0000000..a6ffae9
--- /dev/null
@@ -0,0 +1,111 @@
+/*!
+ * JavaScript for Special:Preferences: Timezone field enhancements.
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var $tzSelect, $tzTextbox, timezoneWidget, $localtimeHolder, servertime,
+                       oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
+
+               // Timezone functions.
+               // Guesses Timezone from browser and updates fields onchange.
+
+               if ( oouiEnabled ) {
+                       // This is identical to OO.ui.infuse( ... ), but it makes the class name of the result known.
+                       try {
+                               timezoneWidget = mw.widgets.SelectWithInputWidget.static.infuse( $( '#wpTimeCorrection' ) );
+                       } catch ( err ) {
+                               // This preference could theoretically be disabled ($wgHiddenPrefs)
+                               timezoneWidget = null;
+                       }
+               } else {
+                       $tzSelect = $( '#mw-input-wptimecorrection' );
+                       $tzTextbox = $( '#mw-input-wptimecorrection-other' );
+               }
+
+               $localtimeHolder = $( '#wpLocalTime' );
+               servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 );
+
+               function minutesToHours( min ) {
+                       var tzHour = Math.floor( Math.abs( min ) / 60 ),
+                               tzMin = Math.abs( min ) % 60,
+                               tzString = ( ( min >= 0 ) ? '' : '-' ) + ( ( tzHour < 10 ) ? '0' : '' ) + tzHour +
+                                       ':' + ( ( tzMin < 10 ) ? '0' : '' ) + tzMin;
+                       return tzString;
+               }
+
+               function hoursToMinutes( hour ) {
+                       var minutes,
+                               arr = hour.split( ':' );
+
+                       arr[ 0 ] = parseInt( arr[ 0 ], 10 );
+
+                       if ( arr.length === 1 ) {
+                               // Specification is of the form [-]XX
+                               minutes = arr[ 0 ] * 60;
+                       } else {
+                               // Specification is of the form [-]XX:XX
+                               minutes = Math.abs( arr[ 0 ] ) * 60 + parseInt( arr[ 1 ], 10 );
+                               if ( arr[ 0 ] < 0 ) {
+                                       minutes *= -1;
+                               }
+                       }
+                       // Gracefully handle non-numbers.
+                       if ( isNaN( minutes ) ) {
+                               return 0;
+                       } else {
+                               return minutes;
+                       }
+               }
+
+               function updateTimezoneSelection() {
+                       var minuteDiff, localTime,
+                               type = oouiEnabled ? timezoneWidget.dropdowninput.getValue() : $tzSelect.val(),
+                               val = oouiEnabled ? timezoneWidget.textinput.getValue() : $tzTextbox.val();
+
+                       if ( type === 'other' ) {
+                               // User specified time zone manually in <input>
+                               // Grab data from the textbox, parse it.
+                               minuteDiff = hoursToMinutes( val );
+                       } else {
+                               // Time zone not manually specified by user
+                               if ( type === 'guess' ) {
+                                       // Get browser timezone & fill it in
+                                       minuteDiff = -( new Date().getTimezoneOffset() );
+                                       if ( oouiEnabled ) {
+                                               timezoneWidget.textinput.setValue( minutesToHours( minuteDiff ) );
+                                               timezoneWidget.dropdowninput.setValue( 'other' );
+                                       } else {
+                                               $tzTextbox.val( minutesToHours( minuteDiff ) );
+                                               $tzSelect.val( 'other' );
+                                       }
+                               } else {
+                                       // Grab data from the dropdown value
+                                       minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;
+                               }
+                       }
+
+                       // Determine local time from server time and minutes difference, for display.
+                       localTime = servertime + minuteDiff;
+
+                       // Bring time within the [0,1440) range.
+                       localTime = ( ( localTime % 1440 ) + 1440 ) % 1440;
+
+                       $localtimeHolder.text( mw.language.convertNumber( minutesToHours( localTime ) ) );
+               }
+
+               if ( oouiEnabled ) {
+                       if ( timezoneWidget ) {
+                               timezoneWidget.dropdowninput.on( 'change', updateTimezoneSelection );
+                               timezoneWidget.textinput.on( 'change', updateTimezoneSelection );
+                               updateTimezoneSelection();
+                       }
+               } else {
+                       if ( $tzSelect.length && $tzTextbox.length ) {
+                               $tzSelect.change( updateTimezoneSelection );
+                               $tzTextbox.blur( updateTimezoneSelection );
+                               updateTimezoneSelection();
+                       }
+               }
+
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.recentchanges.js b/resources/src/mediawiki.special.recentchanges.js
new file mode 100644 (file)
index 0000000..29c0fea
--- /dev/null
@@ -0,0 +1,38 @@
+/*!
+ * JavaScript for Special:RecentChanges
+ */
+( function ( mw, $ ) {
+       var rc, $checkboxes, $select;
+
+       /**
+        * @class mw.special.recentchanges
+        * @singleton
+        */
+       rc = {
+               /**
+                * Handler to disable/enable the namespace selector checkboxes when the
+                * special 'all' namespace is selected/unselected respectively.
+                */
+               updateCheckboxes: function () {
+                       // The option element for the 'all' namespace has an empty value
+                       var isAllNS = $select.val() === '';
+
+                       // Iterates over checkboxes and propagate the selected option
+                       $checkboxes.prop( 'disabled', isAllNS );
+               },
+
+               init: function () {
+                       $select = $( '#namespace' );
+                       $checkboxes = $( '#nsassociated, #nsinvert' );
+
+                       // Bind to change event, and trigger once to set the initial state of the checkboxes.
+                       rc.updateCheckboxes();
+                       $select.change( rc.updateCheckboxes );
+               }
+       };
+
+       $( rc.init );
+
+       module.exports = rc;
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.revisionDelete.js b/resources/src/mediawiki.special.revisionDelete.js
new file mode 100644 (file)
index 0000000..cad9db0
--- /dev/null
@@ -0,0 +1,29 @@
+/*!
+ * JavaScript for Special:RevisionDelete
+ */
+( function ( mw, $ ) {
+       var colonSeparator = mw.message( 'colon-separator' ).text(),
+               summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+               summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+               $wpRevDeleteReasonList = $( '#wpRevDeleteReasonList' ),
+               $wpReason = $( '#wpReason' ),
+               filterFn = function ( input ) {
+                       // Should be built the same as in SpecialRevisionDelete::submit()
+                       var comment = $wpRevDeleteReasonList.val();
+                       if ( comment === 'other' ) {
+                               comment = input;
+                       } else if ( input !== '' ) {
+                               // Entry from drop down menu + additional comment
+                               comment += colonSeparator + input;
+                       }
+                       return comment;
+               };
+
+       // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+       if ( summaryCodePointLimit ) {
+               $wpReason.codePointLimit( summaryCodePointLimit, filterFn );
+       } else if ( summaryByteLimit ) {
+               $wpReason.byteLimit( summaryByteLimit, filterFn );
+       }
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.search.commonsInterwikiWidget.js b/resources/src/mediawiki.special.search.commonsInterwikiWidget.js
new file mode 100644 (file)
index 0000000..648bf67
--- /dev/null
@@ -0,0 +1,78 @@
+( function ( mw, $ ) {
+
+       var api = new mw.Api(),
+               pageUrl = new mw.Uri(),
+               imagesText = new mw.Message( mw.messages, 'searchprofile-images' ),
+               moreResultsText = new mw.Message( mw.messages, 'search-interwiki-more-results' );
+
+       function itemTemplate( results ) {
+
+               var resultOutput = '', i, result, imageCaption, imageThumbnailSrc;
+
+               for ( i = 0; i < results.length; i++ ) {
+                       result = results[ i ];
+                       imageCaption = mw.html.element( 'span', { 'class': 'iw-result__mini-gallery__caption' }, result.title );
+                       imageThumbnailSrc = ( result.thumbnail ) ? result.thumbnail.source : '';
+                       resultOutput += '<div class="iw-result__mini-gallery">' +
+                                               /* escaping response content */
+                                               mw.html.element( 'a', {
+                                                       href: '/wiki/' + result.title,
+                                                       'class': 'iw-result__mini-gallery__image',
+                                                       style: 'background-image: url(' + imageThumbnailSrc + ');'
+                                               }, new mw.html.Raw( imageCaption ) ) +
+                                       '</div>';
+               }
+
+               return resultOutput;
+       }
+
+       function itemWrapperTemplate( pageQuery, itemTemplateOutput ) {
+
+               return '<li class="iw-resultset iw-resultset--image" data-iw-resultset-pos="0">' +
+                               '<div class="iw-result__header">' +
+                                       '<strong>' + imagesText.escaped() + '</strong>' +
+                               '</div>' +
+                               '<div class="iw-result__content">' +
+                               /* template output has been sanitized by mw.html.element */
+                               itemTemplateOutput +
+                               '</div>' +
+                               '<div class="iw-result__footer">' +
+                                       '<a href="/w/index.php?title=Special:Search&search=' + encodeURIComponent( pageQuery ) + '&fulltext=1&profile=images">' +
+                                               moreResultsText.escaped() +
+                                       '</a>' +
+                               '</div>' +
+                       '</li>';
+
+       }
+
+       api.get( {
+               action: 'query',
+               generator: 'search',
+               gsrsearch: pageUrl.query.search,
+               gsrnamespace: mw.config.get( 'wgNamespaceIds' ).file,
+               gsrlimit: 3,
+               prop: 'pageimages',
+               pilimit: 3,
+               piprop: 'thumbnail',
+               pithumbsize: 300,
+               formatversion: 2
+       } ).done( function ( resp ) {
+               var results = ( resp.query && resp.query.pages ) ? resp.query.pages : false,
+                       multimediaWidgetTemplate;
+
+               if ( !results ) {
+                       return;
+               }
+
+               results.sort( function ( a, b ) {
+                       return a.index - b.index;
+               } );
+
+               multimediaWidgetTemplate = itemWrapperTemplate( pageUrl.query.search, itemTemplate( results ) );
+               /* we really only need to wait for document ready for DOM manipulation */
+               $( function () {
+                       $( '.iw-results' ).append( multimediaWidgetTemplate );
+               } );
+       } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.search.interwikiwidget.styles.less b/resources/src/mediawiki.special.search.interwikiwidget.styles.less
new file mode 100644 (file)
index 0000000..8ec2735
--- /dev/null
@@ -0,0 +1,121 @@
+/* interwiki search results */
+/*==========================*/
+
+@import 'mediawiki.ui/variables.less';
+@import 'mediawiki.mixins';
+
+.mw-searchresults-has-iw {
+
+       .iw-headline {
+               font-weight: bold;
+       }
+
+       .iw-results {
+               list-style: none;
+               margin: 0;
+       }
+
+       .iw-resultset {
+               .box-sizing(border-box);
+               padding: 0.5em;
+               vertical-align: top;
+               width: 100%;
+               float: left;
+               background-color: @colorGray15;
+               margin-bottom: 1em;
+               word-break: break-word;
+       }
+
+       .iw-result__title {
+               font-size: 108%; /* matching regular search title */
+       }
+
+       .iw-result:after,
+       .iw-result__content:after { /* clearfix */
+               visibility: hidden;
+               display: block;
+               font-size: 0;
+               content: ' ';
+               clear: both;
+               height: 0;
+       }
+
+       .iw-result__footer {
+               float: right;
+               font-size: 97%; /* matching main search result font-size */
+               margin-top: 0.5em;
+       }
+       .iw-result__footer a {
+               vertical-align: middle;
+               font-style: italic;
+       }
+
+       .oo-ui-icon-favicon {
+               padding-right: 1em;
+       }
+
+       /* image search result */
+       .iw-result__mini-gallery {
+               position: relative;
+               float: left;
+               width: 100%;
+               height: 200px;
+               .box-sizing(border-box);
+               padding: 0.25rem;
+       }
+
+       /* second and third images are small */
+       .iw-result__mini-gallery:nth-child( 2 ),
+       .iw-result__mini-gallery:nth-child( 3 ) { /* stylelint-disable-line indentation */
+               width: 50%;
+               height: 100px;
+       }
+
+       .iw-result__mini-gallery__image {
+               display: block;
+               position: relative;
+               width: 100%;
+               height: 100%;
+               background-size: 100% auto;
+               background-size: cover;
+               background-repeat: no-repeat;
+               background-position: center center;
+       }
+
+       /* image gallery text */
+       .iw-result__mini-gallery__image > .iw-result__mini-gallery__caption {
+               visibility: hidden;
+               position: absolute;
+               bottom: 0;
+               left: 0;
+               text-align: center;
+               color: #fff;
+               font-size: 0.8em;
+               padding: 0.5em;
+               background-color: rgba( 0, 0, 0, 0.5 );
+       }
+
+       .iw-result__mini-gallery__image:hover > .iw-result__mini-gallery__caption {
+               visibility: visible;
+       }
+
+       /* tablet and up */
+
+       @media only screen and ( min-width: @deviceWidthTablet ) {
+
+               #mw-interwiki-results {
+                       width: 30%;
+                       display: inline-block; /* used to align interwiki sidebar with the top of the main search results */
+                       margin-left: 8%; /* since inline-block causes whitespace issues, this is 8 instead of 10% */
+               }
+               .mw-search-createlink,
+               .mw-search-nonefound,
+               .mw-search-results,
+               .mw-search-interwiki-header {
+                       float: left;
+                       width: 60%;
+                       clear: left;
+                       max-width: 60%;
+               }
+       }
+}
diff --git a/resources/src/mediawiki.special.search.styles.css b/resources/src/mediawiki.special.search.styles.css
new file mode 100644 (file)
index 0000000..ea9b987
--- /dev/null
@@ -0,0 +1,166 @@
+/* Special:Search */
+
+/*
+ * Fixes sister projects box moving down the extract
+ * of the first result (bug #16886).
+ * It only happens when the window is small and
+ * This changes slightly the layout for big screens
+ * where there was space for the extracts and the
+ * sister projects and thus it showed like in any
+ * other browser.
+ *
+ * This will only affect IE 7 and lower
+ */
+.searchresult {
+       display: inline !ie;
+}
+.searchresults {
+       margin: 1em 0 1em 0.4em;
+}
+/* needs extra specificity to override `.mw-body p` selector */
+.mw-body .mw-search-nonefound {
+       margin: 0;
+}
+
+.searchdidyoumean em,
+.searchmatch {
+       font-weight: bold;
+}
+
+.mw-search-results {
+       margin: 0;
+       max-width: 38em;
+}
+
+.mw-search-visualclear {
+       clear: both;
+}
+.mw-search-results li {
+       padding-bottom: 1.2em;
+       list-style: none;
+       list-style-image: none;
+}
+.mw-search-results li a {
+       font-size: 108%;
+}
+.mw-search-result-data {
+       color: #008000;
+       font-size: 97%;
+}
+.mw-search-profile-tabs {
+       background-color: #f8f9fa;
+       margin-top: 1em;
+       border: 1px solid #c8ccd1;
+       border-radius: 2px;
+}
+.search-types {
+       float: left;
+       padding-left: 0.25em;
+}
+.search-types ul {
+       margin: 0;
+       padding: 0;
+       list-style: none;
+}
+.search-types li {
+       float: left;
+       margin: 0;
+       padding: 0;
+}
+.search-types a {
+       display: block;
+       padding: 0.5em;
+}
+.search-types .current a {
+       color: #222;
+       cursor: default;
+}
+.search-types .current a:hover {
+       text-decoration: none;
+}
+.results-info {
+       float: right;
+       padding: 0.5em;
+       padding-right: 0.75em;
+       color: #54595d;
+       font-size: 95%;
+}
+#mw-search-top-table div.oo-ui-actionFieldLayout {
+       float: left;
+       width: 100%;
+}
+
+/* Advanced options menu */
+/*==========================*/
+
+#mw-searchoptions {
+       /* Support: Firefox, needs `clear: both` on `fieldset` when zoom level > 100%, see T176499 */
+       clear: both;
+       padding: 0.5em 0.75em 0.75em 0.75em;
+       background-color: #f8f9fa;
+       margin: -1px 0 0;
+       border: 1px solid #c8ccd1;
+       border-radius: 0 0 2px 2px;
+}
+#mw-searchoptions legend {
+       display: none;
+}
+#mw-searchoptions h4 {
+       padding: 0;
+       margin: 0;
+       float: left;
+}
+#mw-searchoptions table {
+       float: left;
+       margin-right: 3em;
+       border-collapse: collapse;
+}
+#mw-searchoptions table td {
+       padding: 0 1em 0 0;
+       white-space: nowrap;
+}
+#mw-searchoptions .divider {
+       clear: both;
+       border-bottom: 1px solid #eaecf0;
+       padding-top: 0.5em;
+       margin-bottom: 0.5em;
+}
+#mw-search-menu {
+       padding-left: 6em;
+       font-size: 85%;
+}
+
+#mw-search-interwiki {
+       float: right;
+       width: 18em;
+       border: 1px solid #a2a9b1;
+       margin-top: 2ex;
+}
+
+.searchalttitle,
+#mw-search-interwiki li {
+       font-size: 95%;
+}
+.mw-search-interwiki-more {
+       float: right;
+       font-size: 90%;
+}
+#mw-search-interwiki-caption {
+       text-align: center;
+       font-weight: bold;
+       font-size: 95%;
+}
+.mw-search-interwiki-project {
+       font-size: 97%;
+       text-align: left;
+       padding: 0.15em 0.15em 0.2em 0.2em;
+       background-color: #eaecf0;
+       border-top: 1px solid #c8ccd1;
+}
+
+.searchdidyoumean {
+       font-size: 127%;
+       margin-top: 0.8em;
+       /* Note that this color won't affect the link, as desired. */
+       color: #d33;
+}
diff --git a/resources/src/mediawiki.special.search/search.css b/resources/src/mediawiki.special.search/search.css
new file mode 100644 (file)
index 0000000..aad784e
--- /dev/null
@@ -0,0 +1,9 @@
+#mw-search-togglebox {
+       float: right;
+}
+#mw-search-togglebox label {
+       margin-right: 0.25em;
+}
+#mw-search-togglebox input {
+       margin-left: 0.25em;
+}
diff --git a/resources/src/mediawiki.special.search/search.js b/resources/src/mediawiki.special.search/search.js
new file mode 100644 (file)
index 0000000..e809f2e
--- /dev/null
@@ -0,0 +1,60 @@
+/*!
+ * JavaScript for Special:Search
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var $checkboxes, $headerLinks, updateHeaderLinks, searchWidget;
+
+               // Emulate HTML5 autofocus behavior in non HTML5 compliant browsers
+               if ( !( 'autofocus' in document.createElement( 'input' ) ) ) {
+                       $( 'input[autofocus]' ).eq( 0 ).focus();
+               }
+
+               // Create check all/none button
+               $checkboxes = $( '#powersearch input[id^=mw-search-ns]' );
+               $( '#mw-search-togglebox' ).append(
+                       $( '<label>' )
+                               .text( mw.msg( 'powersearch-togglelabel' ) )
+               ).append(
+                       $( '<input>' ).attr( 'type', 'button' )
+                               .attr( 'id', 'mw-search-toggleall' )
+                               .prop( 'value', mw.msg( 'powersearch-toggleall' ) )
+                               .click( function () {
+                                       $checkboxes.prop( 'checked', true );
+                               } )
+               ).append(
+                       $( '<input>' ).attr( 'type', 'button' )
+                               .attr( 'id', 'mw-search-togglenone' )
+                               .prop( 'value', mw.msg( 'powersearch-togglenone' ) )
+                               .click( function () {
+                                       $checkboxes.prop( 'checked', false );
+                               } )
+               );
+
+               // Change the header search links to what user entered
+               $headerLinks = $( '.search-types a' );
+               searchWidget = OO.ui.infuse( 'searchText' );
+               updateHeaderLinks = function ( value ) {
+                       $headerLinks.each( function () {
+                               var parts = $( this ).attr( 'href' ).split( 'search=' ),
+                                       lastpart = '',
+                                       prefix = 'search=';
+                               if ( parts.length > 1 && parts[ 1 ].indexOf( '&' ) !== -1 ) {
+                                       lastpart = parts[ 1 ].slice( parts[ 1 ].indexOf( '&' ) );
+                               } else {
+                                       prefix = '&search=';
+                               }
+                               this.href = parts[ 0 ] + prefix + encodeURIComponent( value ) + lastpart;
+                       } );
+               };
+               searchWidget.on( 'change', updateHeaderLinks );
+               updateHeaderLinks( searchWidget.getValue() );
+
+               // When saving settings, use the proper request method (POST instead of GET).
+               $( '#mw-search-powersearch-remember' ).change( function () {
+                       this.form.method = this.checked ? 'post' : 'get';
+               } ).trigger( 'change' );
+
+       } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.undelete.js b/resources/src/mediawiki.special.undelete.js
new file mode 100644 (file)
index 0000000..e3cf598
--- /dev/null
@@ -0,0 +1,23 @@
+/*!
+ * JavaScript for Special:Undelete
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+                       summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+                       wpComment = OO.ui.infuse( $( '#wpComment' ).closest( '.oo-ui-widget' ) );
+
+               $( '#mw-undelete-invert' ).click( function () {
+                       $( '.mw-undelete-revlist input[type="checkbox"]' ).prop( 'checked', function ( i, val ) {
+                               return !val;
+                       } );
+               } );
+
+               // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+               if ( summaryCodePointLimit ) {
+                       mw.widgets.visibleCodePointLimit( wpComment, summaryCodePointLimit );
+               } else if ( summaryByteLimit ) {
+                       mw.widgets.visibleByteLimit( wpComment, summaryByteLimit );
+               }
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.unwatchedPages/unwatchedPages.css b/resources/src/mediawiki.special.unwatchedPages/unwatchedPages.css
new file mode 100644 (file)
index 0000000..69fec08
--- /dev/null
@@ -0,0 +1,7 @@
+.mw-watched-item {
+       text-decoration: line-through;
+}
+
+.mw-watch-link-disabled {
+       pointer-events: none;
+}
diff --git a/resources/src/mediawiki.special.unwatchedPages/unwatchedPages.js b/resources/src/mediawiki.special.unwatchedPages/unwatchedPages.js
new file mode 100644 (file)
index 0000000..0886f8c
--- /dev/null
@@ -0,0 +1,49 @@
+/*!
+ * JavaScript for Special:UnwatchedPages
+ */
+( function ( mw, $ ) {
+       $( function () {
+               $( 'a.mw-watch-link' ).click( function ( e ) {
+                       var promise,
+                               api = new mw.Api(),
+                               $link = $( this ),
+                               $subjectLink = $link.closest( 'li' ).children( 'a' ).eq( 0 ),
+                               title = mw.util.getParamValue( 'title', $link.attr( 'href' ) );
+                       // nice format
+                       title = mw.Title.newFromText( title ).toText();
+                       $link.addClass( 'mw-watch-link-disabled' );
+
+                       // Preload the notification module for mw.notify
+                       mw.loader.load( 'mediawiki.notification' );
+
+                       // Use the class to determine whether to watch or unwatch
+                       if ( !$subjectLink.hasClass( 'mw-watched-item' ) ) {
+                               $link.text( mw.msg( 'watching' ) );
+                               promise = api.watch( title ).done( function () {
+                                       $subjectLink.addClass( 'mw-watched-item' );
+                                       $link.text( mw.msg( 'unwatch' ) );
+                                       mw.notify( mw.msg( 'addedwatchtext-short', title ) );
+                               } ).fail( function () {
+                                       $link.text( mw.msg( 'watch' ) );
+                                       mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
+                               } );
+                       } else {
+                               $link.text( mw.msg( 'unwatching' ) );
+                               promise = api.unwatch( title ).done( function () {
+                                       $subjectLink.removeClass( 'mw-watched-item' );
+                                       $link.text( mw.msg( 'watch' ) );
+                                       mw.notify( mw.msg( 'removedwatchtext-short', title ) );
+                               } ).fail( function () {
+                                       $link.text( mw.msg( 'unwatch' ) );
+                                       mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
+                               } );
+                       }
+
+                       promise.always( function () {
+                               $link.removeClass( 'mw-watch-link-disabled' );
+                       } );
+
+                       e.preventDefault();
+               } );
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.upload.styles.css b/resources/src/mediawiki.special.upload.styles.css
new file mode 100644 (file)
index 0000000..626a7e8
--- /dev/null
@@ -0,0 +1,15 @@
+/*!
+ * Styling for Special:Upload
+ */
+.mw-destfile-warning {
+       border: 1px solid #fde29b;
+       padding: 0.5em 1em;
+       margin-bottom: 1em;
+       color: #705000;
+       background-color: #fdf1d1;
+}
+
+p.mw-upload-editlicenses {
+       font-size: 90%;
+       text-align: right;
+}
diff --git a/resources/src/mediawiki.special.upload/templates/thumbnail.html b/resources/src/mediawiki.special.upload/templates/thumbnail.html
new file mode 100644 (file)
index 0000000..bf0e701
--- /dev/null
@@ -0,0 +1,8 @@
+<div id="mw-upload-thumbnail" class="thumb tright">
+       <div class="thumbinner">
+               <div class="thumbcaption">
+                       <div class="filename"></div>
+                       <div class="fileinfo"></div>
+               </div>
+       </div>
+</div>
diff --git a/resources/src/mediawiki.special.upload/upload.js b/resources/src/mediawiki.special.upload/upload.js
new file mode 100644 (file)
index 0000000..144659a
--- /dev/null
@@ -0,0 +1,654 @@
+/**
+ * JavaScript for Special:Upload
+ *
+ * @private
+ * @class mw.special.upload
+ * @singleton
+ */
+
+/* global Uint8Array */
+
+( function ( mw, $ ) {
+       var uploadWarning, uploadTemplatePreview,
+               ajaxUploadDestCheck = mw.config.get( 'wgAjaxUploadDestCheck' ),
+               $license = $( '#wpLicense' );
+
+       window.wgUploadWarningObj = uploadWarning = {
+               responseCache: { '': '&nbsp;' },
+               nameToCheck: '',
+               typing: false,
+               delay: 500, // ms
+               timeoutID: false,
+
+               keypress: function () {
+                       if ( !ajaxUploadDestCheck ) {
+                               return;
+                       }
+
+                       // Find file to upload
+                       if ( !$( '#wpDestFile' ).length || !$( '#wpDestFile-warning' ).length ) {
+                               return;
+                       }
+
+                       this.nameToCheck = $( '#wpDestFile' ).val();
+
+                       // Clear timer
+                       if ( this.timeoutID ) {
+                               clearTimeout( this.timeoutID );
+                       }
+                       // Check response cache
+                       if ( this.responseCache.hasOwnProperty( this.nameToCheck ) ) {
+                               this.setWarning( this.responseCache[ this.nameToCheck ] );
+                               return;
+                       }
+
+                       this.timeoutID = setTimeout( function () {
+                               uploadWarning.timeout();
+                       }, this.delay );
+               },
+
+               checkNow: function ( fname ) {
+                       if ( !ajaxUploadDestCheck ) {
+                               return;
+                       }
+                       if ( this.timeoutID ) {
+                               clearTimeout( this.timeoutID );
+                       }
+                       this.nameToCheck = fname;
+                       this.timeout();
+               },
+
+               timeout: function () {
+                       var $spinnerDestCheck, title;
+                       if ( !ajaxUploadDestCheck || this.nameToCheck.trim() === '' ) {
+                               return;
+                       }
+                       $spinnerDestCheck = $.createSpinner().insertAfter( '#wpDestFile' );
+                       title = mw.Title.newFromText( this.nameToCheck, mw.config.get( 'wgNamespaceIds' ).file );
+
+                       ( new mw.Api() ).get( {
+                               formatversion: 2,
+                               action: 'query',
+                               // If title is empty, user input is invalid, the API call will produce details about why
+                               titles: [ title ? title.getPrefixedText() : this.nameToCheck ],
+                               prop: 'imageinfo',
+                               iiprop: 'uploadwarning',
+                               errorformat: 'html',
+                               errorlang: mw.config.get( 'wgUserLanguage' )
+                       } ).done( function ( result ) {
+                               var
+                                       resultOut = '',
+                                       page = result.query.pages[ 0 ];
+                               if ( page.imageinfo ) {
+                                       resultOut = page.imageinfo[ 0 ].html;
+                               } else if ( page.invalidreason ) {
+                                       resultOut = page.invalidreason.html;
+                               }
+                               uploadWarning.processResult( resultOut, uploadWarning.nameToCheck );
+                       } ).always( function () {
+                               $spinnerDestCheck.remove();
+                       } );
+               },
+
+               processResult: function ( result, fileName ) {
+                       this.setWarning( result );
+                       this.responseCache[ fileName ] = result;
+               },
+
+               setWarning: function ( warning ) {
+                       var $warningBox = $( '#wpDestFile-warning' ),
+                               $warning = $( $.parseHTML( warning ) );
+                       mw.hook( 'wikipage.content' ).fire( $warning );
+                       $warningBox.empty().append( $warning );
+
+                       // Set a value in the form indicating that the warning is acknowledged and
+                       // doesn't need to be redisplayed post-upload
+                       if ( !warning ) {
+                               $( '#wpDestFileWarningAck' ).val( '' );
+                               $warningBox.removeAttr( 'class' );
+                       } else {
+                               $( '#wpDestFileWarningAck' ).val( '1' );
+                               $warningBox.attr( 'class', 'mw-destfile-warning' );
+                       }
+
+               }
+       };
+
+       window.wgUploadTemplatePreviewObj = uploadTemplatePreview = {
+
+               responseCache: { '': '' },
+
+               /**
+                * @param {jQuery} $element The element whose .val() will be previewed
+                * @param {jQuery} $previewContainer The container to display the preview in
+                */
+               getPreview: function ( $element, $previewContainer ) {
+                       var template = $element.val(),
+                               $spinner;
+
+                       if ( this.responseCache.hasOwnProperty( template ) ) {
+                               this.showPreview( this.responseCache[ template ], $previewContainer );
+                               return;
+                       }
+
+                       $spinner = $.createSpinner().insertAfter( $element );
+
+                       ( new mw.Api() ).parse( '{{' + template + '}}', {
+                               title: $( '#wpDestFile' ).val() || 'File:Sample.jpg',
+                               prop: 'text',
+                               pst: true,
+                               uselang: mw.config.get( 'wgUserLanguage' )
+                       } ).done( function ( result ) {
+                               uploadTemplatePreview.processResult( result, template, $previewContainer );
+                       } ).always( function () {
+                               $spinner.remove();
+                       } );
+               },
+
+               processResult: function ( result, template, $previewContainer ) {
+                       this.responseCache[ template ] = result;
+                       this.showPreview( this.responseCache[ template ], $previewContainer );
+               },
+
+               showPreview: function ( preview, $previewContainer ) {
+                       $previewContainer.html( preview );
+               }
+
+       };
+
+       $( function () {
+               // AJAX wpDestFile warnings
+               if ( ajaxUploadDestCheck ) {
+                       // Insert an event handler that fetches upload warnings when wpDestFile
+                       // has been changed
+                       $( '#wpDestFile' ).change( function () {
+                               uploadWarning.checkNow( $( this ).val() );
+                       } );
+                       // Insert a row where the warnings will be displayed just below the
+                       // wpDestFile row
+                       $( '#mw-htmlform-description tbody' ).append(
+                               $( '<tr>' ).append(
+                                       $( '<td>' )
+                                               .attr( 'id', 'wpDestFile-warning' )
+                                               .attr( 'colspan', 2 )
+                               )
+                       );
+               }
+
+               if ( mw.config.get( 'wgAjaxLicensePreview' ) && $license.length ) {
+                       // License selector check
+                       $license.change( function () {
+                               // We might show a preview
+                               uploadTemplatePreview.getPreview( $license, $( '#mw-license-preview' ) );
+                       } );
+
+                       // License selector table row
+                       $license.closest( 'tr' ).after(
+                               $( '<tr>' ).append(
+                                       $( '<td>' ),
+                                       $( '<td>' ).attr( 'id', 'mw-license-preview' )
+                               )
+                       );
+               }
+
+               // fillDestFile setup
+               mw.config.get( 'wgUploadSourceIds' ).forEach( function ( sourceId ) {
+                       $( '#' + sourceId ).change( function () {
+                               var path, slash, backslash, fname;
+                               if ( !mw.config.get( 'wgUploadAutoFill' ) ) {
+                                       return;
+                               }
+                               // Remove any previously flagged errors
+                               $( '#mw-upload-permitted' ).attr( 'class', '' );
+                               $( '#mw-upload-prohibited' ).attr( 'class', '' );
+
+                               path = $( this ).val();
+                               // Find trailing part
+                               slash = path.lastIndexOf( '/' );
+                               backslash = path.lastIndexOf( '\\' );
+                               if ( slash === -1 && backslash === -1 ) {
+                                       fname = path;
+                               } else if ( slash > backslash ) {
+                                       fname = path.slice( slash + 1 );
+                               } else {
+                                       fname = path.slice( backslash + 1 );
+                               }
+
+                               // Clear the filename if it does not have a valid extension.
+                               // URLs are less likely to have a useful extension, so don't include them in the
+                               // extension check.
+                               if (
+                                       mw.config.get( 'wgCheckFileExtensions' ) &&
+                                       mw.config.get( 'wgStrictFileExtensions' ) &&
+                                       Array.isArray( mw.config.get( 'wgFileExtensions' ) ) &&
+                                       $( this ).attr( 'id' ) !== 'wpUploadFileURL'
+                               ) {
+                                       if (
+                                               fname.lastIndexOf( '.' ) === -1 ||
+                                               mw.config.get( 'wgFileExtensions' ).map( function ( element ) {
+                                                       return element.toLowerCase();
+                                               } ).indexOf( fname.slice( fname.lastIndexOf( '.' ) + 1 ).toLowerCase() ) === -1
+                                       ) {
+                                               // Not a valid extension
+                                               // Clear the upload and set mw-upload-permitted to error
+                                               $( this ).val( '' );
+                                               $( '#mw-upload-permitted' ).attr( 'class', 'error' );
+                                               $( '#mw-upload-prohibited' ).attr( 'class', 'error' );
+                                               // Clear wpDestFile as well
+                                               $( '#wpDestFile' ).val( '' );
+
+                                               return false;
+                                       }
+                               }
+
+                               // Replace spaces by underscores
+                               fname = fname.replace( / /g, '_' );
+                               // Capitalise first letter if needed
+                               if ( mw.config.get( 'wgCapitalizeUploads' ) ) {
+                                       fname = fname[ 0 ].toUpperCase() + fname.slice( 1 );
+                               }
+
+                               // Output result
+                               if ( $( '#wpDestFile' ).length ) {
+                                       // Call decodeURIComponent function to remove possible URL-encoded characters
+                                       // from the file name (T32390). Especially likely with upload-form-url.
+                                       // decodeURIComponent can throw an exception if input is invalid utf-8
+                                       try {
+                                               $( '#wpDestFile' ).val( decodeURIComponent( fname ) );
+                                       } catch ( err ) {
+                                               $( '#wpDestFile' ).val( fname );
+                                       }
+                                       uploadWarning.checkNow( fname );
+                               }
+                       } );
+               } );
+       } );
+
+       // Add a preview to the upload form
+       $( function () {
+               /**
+                * Is the FileAPI available with sufficient functionality?
+                *
+                * @return {boolean}
+                */
+               function hasFileAPI() {
+                       return window.FileReader !== undefined;
+               }
+
+               /**
+                * Check if this is a recognizable image type...
+                * Also excludes files over 10M to avoid going insane on memory usage.
+                *
+                * TODO: Is there a way we can ask the browser what's supported in `<img>`s?
+                *
+                * TODO: Put SVG back after working around Firefox 7 bug <https://phabricator.wikimedia.org/T33643>
+                *
+                * @param {File} file
+                * @return {boolean}
+                */
+               function fileIsPreviewable( file ) {
+                       var known = [ 'image/png', 'image/gif', 'image/jpeg', 'image/svg+xml' ],
+                               tooHuge = 10 * 1024 * 1024;
+                       return ( known.indexOf( file.type ) !== -1 ) && file.size > 0 && file.size < tooHuge;
+               }
+
+               /**
+                * Format a file size attractively.
+                *
+                * TODO: Match numeric formatting
+                *
+                * @param {number} s
+                * @return {string}
+                */
+               function prettySize( s ) {
+                       var sizeMsgs = [ 'size-bytes', 'size-kilobytes', 'size-megabytes', 'size-gigabytes' ];
+                       while ( s >= 1024 && sizeMsgs.length > 1 ) {
+                               s /= 1024;
+                               sizeMsgs = sizeMsgs.slice( 1 );
+                       }
+                       return mw.msg( sizeMsgs[ 0 ], Math.round( s ) );
+               }
+
+               /**
+                * Start loading a file into memory; when complete, pass it as a
+                * data URL to the callback function. If the callbackBinary is set it will
+                * first be read as binary and afterwards as data URL. Useful if you want
+                * to do preprocessing on the binary data first.
+                *
+                * @param {File} file
+                * @param {Function} callback
+                * @param {Function} callbackBinary
+                */
+               function fetchPreview( file, callback, callbackBinary ) {
+                       var reader = new FileReader();
+                       if ( callbackBinary && 'readAsBinaryString' in reader ) {
+                               // To fetch JPEG metadata we need a binary string; start there.
+                               // TODO
+                               reader.onload = function () {
+                                       callbackBinary( reader.result );
+
+                                       // Now run back through the regular code path.
+                                       fetchPreview( file, callback );
+                               };
+                               reader.readAsBinaryString( file );
+                       } else if ( callbackBinary && 'readAsArrayBuffer' in reader ) {
+                               // readAsArrayBuffer replaces readAsBinaryString
+                               // However, our JPEG metadata library wants a string.
+                               // So, this is going to be an ugly conversion.
+                               reader.onload = function () {
+                                       var i,
+                                               buffer = new Uint8Array( reader.result ),
+                                               string = '';
+                                       for ( i = 0; i < buffer.byteLength; i++ ) {
+                                               string += String.fromCharCode( buffer[ i ] );
+                                       }
+                                       callbackBinary( string );
+
+                                       // Now run back through the regular code path.
+                                       fetchPreview( file, callback );
+                               };
+                               reader.readAsArrayBuffer( file );
+                       } else if ( 'URL' in window && 'createObjectURL' in window.URL ) {
+                               // Supported in Firefox 4.0 and above <https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL>
+                               // WebKit has it in a namespace for now but that's ok. ;)
+                               //
+                               // Lifetime of this URL is until document close, which is fine
+                               // for Special:Upload -- if this code gets used on longer-running
+                               // pages, add a revokeObjectURL() when it's no longer needed.
+                               //
+                               // Prefer this over readAsDataURL for Firefox 7 due to bug reading
+                               // some SVG files from data URIs <https://bugzilla.mozilla.org/show_bug.cgi?id=694165>
+                               callback( window.URL.createObjectURL( file ) );
+                       } else {
+                               // This ends up decoding the file to base-64 and back again, which
+                               // feels horribly inefficient.
+                               reader.onload = function () {
+                                       callback( reader.result );
+                               };
+                               reader.readAsDataURL( file );
+                       }
+               }
+
+               /**
+                * Clear the file upload preview area.
+                */
+               function clearPreview() {
+                       $( '#mw-upload-thumbnail' ).remove();
+               }
+
+               /**
+                * Show a thumbnail preview of PNG, JPEG, GIF, and SVG files prior to upload
+                * in browsers supporting HTML5 FileAPI.
+                *
+                * As of this writing, known good:
+                *
+                * - Firefox 3.6+
+                * - Chrome 7.something
+                *
+                * TODO: Check file size limits and warn of likely failures
+                *
+                * @param {File} file
+                */
+               function showPreview( file ) {
+                       var $canvas,
+                               ctx,
+                               meta,
+                               previewSize = 180,
+                               $spinner = $.createSpinner( { size: 'small', type: 'block' } )
+                                       .css( { width: previewSize, height: previewSize } ),
+                               thumb = mw.template.get( 'mediawiki.special.upload', 'thumbnail.html' ).render();
+
+                       thumb
+                               .find( '.filename' ).text( file.name ).end()
+                               .find( '.fileinfo' ).text( prettySize( file.size ) ).end()
+                               .find( '.thumbinner' ).prepend( $spinner ).end();
+
+                       $canvas = $( '<canvas>' ).attr( { width: previewSize, height: previewSize } );
+                       ctx = $canvas[ 0 ].getContext( '2d' );
+                       $( '#mw-htmlform-source' ).parent().prepend( thumb );
+
+                       fetchPreview( file, function ( dataURL ) {
+                               var img = new Image(),
+                                       rotation = 0;
+
+                               if ( meta && meta.tiff && meta.tiff.Orientation ) {
+                                       rotation = ( 360 - ( function () {
+                                               // See BitmapHandler class in PHP
+                                               switch ( meta.tiff.Orientation.value ) {
+                                                       case 8:
+                                                               return 90;
+                                                       case 3:
+                                                               return 180;
+                                                       case 6:
+                                                               return 270;
+                                                       default:
+                                                               return 0;
+                                               }
+                                       }() ) ) % 360;
+                               }
+
+                               img.onload = function () {
+                                       var info, width, height, x, y, dx, dy, logicalWidth, logicalHeight;
+
+                                       // Fit the image within the previewSizexpreviewSize box
+                                       if ( img.width > img.height ) {
+                                               width = previewSize;
+                                               height = img.height / img.width * previewSize;
+                                       } else {
+                                               height = previewSize;
+                                               width = img.width / img.height * previewSize;
+                                       }
+                                       // Determine the offset required to center the image
+                                       dx = ( 180 - width ) / 2;
+                                       dy = ( 180 - height ) / 2;
+                                       switch ( rotation ) {
+                                               // If a rotation is applied, the direction of the axis
+                                               // changes as well. You can derive the values below by
+                                               // drawing on paper an axis system, rotate it and see
+                                               // where the positive axis direction is
+                                               case 0:
+                                                       x = dx;
+                                                       y = dy;
+                                                       logicalWidth = img.width;
+                                                       logicalHeight = img.height;
+                                                       break;
+                                               case 90:
+
+                                                       x = dx;
+                                                       y = dy - previewSize;
+                                                       logicalWidth = img.height;
+                                                       logicalHeight = img.width;
+                                                       break;
+                                               case 180:
+                                                       x = dx - previewSize;
+                                                       y = dy - previewSize;
+                                                       logicalWidth = img.width;
+                                                       logicalHeight = img.height;
+                                                       break;
+                                               case 270:
+                                                       x = dx - previewSize;
+                                                       y = dy;
+                                                       logicalWidth = img.height;
+                                                       logicalHeight = img.width;
+                                                       break;
+                                       }
+
+                                       ctx.clearRect( 0, 0, 180, 180 );
+                                       ctx.rotate( rotation / 180 * Math.PI );
+                                       ctx.drawImage( img, x, y, width, height );
+                                       $spinner.replaceWith( $canvas );
+
+                                       // Image size
+                                       info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) +
+                                               ', ' + prettySize( file.size );
+
+                                       $( '#mw-upload-thumbnail .fileinfo' ).text( info );
+                               };
+                               img.onerror = function () {
+                                       // Can happen for example for invalid SVG files
+                                       clearPreview();
+                               };
+                               img.src = dataURL;
+                       }, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) {
+                               var jpegmeta = mw.loader.require( 'mediawiki.libs.jpegmeta' );
+                               try {
+                                       meta = jpegmeta( data, file.fileName );
+                                       // eslint-disable-next-line no-underscore-dangle, camelcase
+                                       meta._binary_data = null;
+                               } catch ( e ) {
+                                       meta = null;
+                               }
+                       } : null );
+               }
+
+               /**
+                * Check if the file does not exceed the maximum size
+                *
+                * @param {File} file
+                * @return {boolean}
+                */
+               function checkMaxUploadSize( file ) {
+                       var maxSize, $error;
+
+                       function getMaxUploadSize( type ) {
+                               var sizes = mw.config.get( 'wgMaxUploadSize' );
+
+                               if ( sizes[ type ] !== undefined ) {
+                                       return sizes[ type ];
+                               }
+                               return sizes[ '*' ];
+                       }
+
+                       $( '.mw-upload-source-error' ).remove();
+
+                       maxSize = getMaxUploadSize( 'file' );
+                       if ( file.size > maxSize ) {
+                               $error = $( '<p class="error mw-upload-source-error" id="wpSourceTypeFile-error">' +
+                                       mw.message( 'largefileserver', file.size, maxSize ).escaped() + '</p>' );
+
+                               $( '#wpUploadFile' ).after( $error );
+
+                               return false;
+                       }
+
+                       return true;
+               }
+
+               /* Initialization */
+               if ( hasFileAPI() ) {
+                       // Update thumbnail when the file selection control is updated.
+                       $( '#wpUploadFile' ).change( function () {
+                               var file;
+                               clearPreview();
+                               if ( this.files && this.files.length ) {
+                                       // Note: would need to be updated to handle multiple files.
+                                       file = this.files[ 0 ];
+
+                                       if ( !checkMaxUploadSize( file ) ) {
+                                               return;
+                                       }
+
+                                       if ( fileIsPreviewable( file ) ) {
+                                               showPreview( file );
+                                       }
+                               }
+                       } );
+               }
+       } );
+
+       // Disable all upload source fields except the selected one
+       $( function () {
+               var $rows = $( '.mw-htmlform-field-UploadSourceField' );
+
+               $rows.on( 'change', 'input[type="radio"]', function ( e ) {
+                       var currentRow = e.delegateTarget;
+
+                       if ( !this.checked ) {
+                               return;
+                       }
+
+                       $( '.mw-upload-source-error' ).remove();
+
+                       // Enable selected upload method
+                       $( currentRow ).find( 'input' ).prop( 'disabled', false );
+
+                       // Disable inputs of other upload methods
+                       // (except for the radio button to re-enable it)
+                       $rows
+                               .not( currentRow )
+                               .find( 'input[type!="radio"]' )
+                               .prop( 'disabled', true );
+               } );
+
+               // Set initial state
+               if ( !$( '#wpSourceTypeurl' ).prop( 'checked' ) ) {
+                       $( '#wpUploadFileURL' ).prop( 'disabled', true );
+               }
+       } );
+
+       $( function () {
+               // Prevent losing work
+               var allowCloseWindow,
+                       $uploadForm = $( '#mw-upload-form' );
+
+               if ( !mw.user.options.get( 'useeditwarning' ) ) {
+                       // If the user doesn't want edit warnings, don't set things up.
+                       return;
+               }
+
+               $uploadForm.data( 'origtext', $uploadForm.serialize() );
+
+               allowCloseWindow = mw.confirmCloseWindow( {
+                       test: function () {
+                               return $( '#wpUploadFile' ).get( 0 ).files.length !== 0 ||
+                                       $uploadForm.data( 'origtext' ) !== $uploadForm.serialize();
+                       },
+
+                       message: mw.msg( 'editwarning-warning' ),
+                       namespace: 'uploadwarning'
+               } );
+
+               $uploadForm.submit( function () {
+                       allowCloseWindow.release();
+               } );
+       } );
+
+       // Add tabindex to mw-editTools
+       $( function () {
+               // Function to change tabindex for all links within mw-editTools
+               function setEditTabindex( $val ) {
+                       $( '.mw-editTools' ).find( 'a' ).each( function () {
+                               $( this ).attr( 'tabindex', $val );
+                       } );
+               }
+
+               // Change tabindex to 0 if user pressed spaced or enter while focused
+               $( '.mw-editTools' ).on( 'keypress', function ( e ) {
+                       // Don't continue if pressed key was not enter or spacebar
+                       if ( e.which !== 13 && e.which !== 32 ) {
+                               return;
+                       }
+
+                       // Change tabindex only when main div has focus
+                       if ( $( this ).is( ':focus' ) ) {
+                               $( this ).find( 'a' ).first().focus();
+                               setEditTabindex( '0' );
+                       }
+               } );
+
+               // Reset tabindex for elements when user focused out mw-editTools
+               $( '.mw-editTools' ).on( 'focusout', function ( e ) {
+                       // Don't continue if relatedTarget is within mw-editTools
+                       if ( e.relatedTarget !== null && $( e.relatedTarget ).closest( '.mw-editTools' ).length > 0 ) {
+                               return;
+                       }
+
+                       // Reset tabindex back to -1
+                       setEditTabindex( '-1' );
+               } );
+
+               // Set initial tabindex for mw-editTools to 0 and to -1 for all links
+               $( '.mw-editTools' ).attr( 'tabindex', '0' );
+               setEditTabindex( '-1' );
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.userlogin.common.styles/images/icon-lock.png b/resources/src/mediawiki.special.userlogin.common.styles/images/icon-lock.png
new file mode 100644 (file)
index 0000000..03f0eec
Binary files /dev/null and b/resources/src/mediawiki.special.userlogin.common.styles/images/icon-lock.png differ
diff --git a/resources/src/mediawiki.special.userlogin.common.styles/userlogin.css b/resources/src/mediawiki.special.userlogin.common.styles/userlogin.css
new file mode 100644 (file)
index 0000000..2366249
--- /dev/null
@@ -0,0 +1,75 @@
+/* User login and signup forms */
+.mw-ui-vform .mw-form-related-link-container {
+       margin-bottom: 0.5em;
+       text-align: center;
+}
+
+.mw-ui-vform .mw-secure {
+       /* @embed */
+       background: url( images/icon-lock.png ) no-repeat left center;
+       margin: 0 0 0 1px;
+       padding: 0 0 0 11px;
+}
+
+/*
+ * When inside the VForm style, disable the border that Vector and other skins
+ * put on the div surrounding the login/create account form.
+ * Also disable the margin and padding that Vector puts around the form.
+ */
+.mw-ui-container #userloginForm,
+.mw-ui-container #userlogin {
+       border: 0;
+       margin: 0;
+       padding: 0;
+}
+
+/* Reposition and resize language links, which appear on a per-wiki basis */
+.mw-ui-container #languagelinks {
+       margin-bottom: 2em;
+       font-size: 0.8em;
+}
+
+/* Put some space under template's header, which may contain CAPTCHA HTML. */
+section.mw-form-header {
+       margin-bottom: 10px;
+}
+
+/* shuffled CAPTCHA */
+#wpCaptchaWord {
+       margin-top: 6px;
+}
+
+.fancycaptcha-captcha-container {
+       background-color: #f8f9fa;
+       margin-bottom: 15px;
+       border: 1px solid #c8ccd1;
+       border-radius: 2px;
+       padding: 8px;
+       text-align: center;
+}
+
+.mw-createacct-captcha-assisted {
+       display: block;
+       margin-top: 0.5em;
+}
+
+/* Put a border around the fancycaptcha-image-container. */
+.fancycaptcha-captcha-and-reload {
+       border: 1px solid #c8ccd1;
+       border-radius: 2px 2px 0 0;
+       /* Other display formats end up too wide */
+       display: table-cell;
+       width: 270px;
+       background-color: #fff;
+}
+
+.fancycaptcha-captcha-container .mw-ui-input {
+       margin-top: -1px;
+       border-color: #c8ccd1;
+       border-radius: 0 0 2px 2px;
+}
+
+/* Make the fancycaptcha-image-container full-width within its parent. */
+.fancycaptcha-image-container {
+       width: 100%;
+}
diff --git a/resources/src/mediawiki.special.userlogin.login.styles/images/glyph-people-large.png b/resources/src/mediawiki.special.userlogin.login.styles/images/glyph-people-large.png
new file mode 100644 (file)
index 0000000..cba3caf
Binary files /dev/null and b/resources/src/mediawiki.special.userlogin.login.styles/images/glyph-people-large.png differ
diff --git a/resources/src/mediawiki.special.userlogin.login.styles/login.css b/resources/src/mediawiki.special.userlogin.login.styles/login.css
new file mode 100644 (file)
index 0000000..fe013bc
--- /dev/null
@@ -0,0 +1,29 @@
+/* The login form invites users to create an account */
+#mw-createaccount-cta {
+       width: 20em;
+       /* @embed */
+       background: url( images/glyph-people-large.png ) no-repeat 50%;
+       margin: 0 auto;
+       padding-top: 7.8em;
+       font-weight: bold;
+}
+
+/* Login Button, following 'ButtonWidget (progressive)' from OOUI */
+#mw-createaccount-join {
+       background-color: #f8f9fa;
+       color: #36c;
+}
+#mw-createaccount-join:hover {
+       background-color: #fff;
+       border-color: #859ecc;
+       box-shadow: none;
+}
+#mw-createaccount-join:active {
+       background-color: #eff3fa;
+       color: #2a4b8d;
+       border-color: #2a4b8d;
+}
+#mw-createaccount-join:focus {
+       border-color: #36c;
+       box-shadow: inset 0 0 0 1px #36c;
+}
diff --git a/resources/src/mediawiki.special.userlogin.signup.js b/resources/src/mediawiki.special.userlogin.signup.js
new file mode 100644 (file)
index 0000000..8a61afb
--- /dev/null
@@ -0,0 +1,122 @@
+/*!
+ * JavaScript for signup form.
+ */
+( function ( mw, $ ) {
+       // When sending password by email, hide the password input fields.
+       $( function () {
+               // Always required if checked, otherwise it depends, so we use the original
+               var $emailLabel = $( 'label[for="wpEmail"]' ),
+                       originalText = $emailLabel.text(),
+                       requiredText = mw.message( 'createacct-emailrequired' ).text(),
+                       $createByMailCheckbox = $( '#wpCreateaccountMail' ),
+                       $beforePwds = $( '.mw-row-password:first' ).prev(),
+                       $pwds;
+
+               function updateForCheckbox() {
+                       var checked = $createByMailCheckbox.prop( 'checked' );
+                       if ( checked ) {
+                               $pwds = $( '.mw-row-password' ).detach();
+                               $emailLabel.text( requiredText );
+                       } else {
+                               if ( $pwds ) {
+                                       $beforePwds.after( $pwds );
+                                       $pwds = null;
+                               }
+                               $emailLabel.text( originalText );
+                       }
+               }
+
+               $createByMailCheckbox.on( 'change', updateForCheckbox );
+               updateForCheckbox();
+       } );
+
+       // Check if the username is invalid or already taken
+       mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
+               var $usernameInput = $root.find( '#wpName2' ),
+                       $passwordInput = $root.find( '#wpPassword2' ),
+                       $emailInput = $root.find( '#wpEmail' ),
+                       $realNameInput = $root.find( '#wpRealName' ),
+                       api = new mw.Api(),
+                       usernameChecker, passwordChecker;
+
+               function checkUsername( username ) {
+                       // We could just use .then() if we didn't have to pass on .abort()…
+                       var d, apiPromise;
+
+                       d = $.Deferred();
+                       apiPromise = api.get( {
+                               action: 'query',
+                               list: 'users',
+                               ususers: username,
+                               usprop: 'cancreate',
+                               formatversion: 2,
+                               errorformat: 'html',
+                               errorsuselocal: true,
+                               uselang: mw.config.get( 'wgUserLanguage' )
+                       } )
+                               .done( function ( resp ) {
+                                       var userinfo = resp.query.users[ 0 ];
+
+                                       if ( resp.query.users.length !== 1 || userinfo.invalid ) {
+                                               d.resolve( { valid: false, messages: [ mw.message( 'noname' ).parseDom() ] } );
+                                       } else if ( userinfo.userid !== undefined ) {
+                                               d.resolve( { valid: false, messages: [ mw.message( 'userexists' ).parseDom() ] } );
+                                       } else if ( !userinfo.cancreate ) {
+                                               d.resolve( {
+                                                       valid: false,
+                                                       messages: userinfo.cancreateerror ? userinfo.cancreateerror.map( function ( m ) {
+                                                               return m.html;
+                                                       } ) : []
+                                               } );
+                                       } else {
+                                               d.resolve( { valid: true, messages: [] } );
+                                       }
+                               } )
+                               .fail( d.reject );
+
+                       return d.promise( { abort: apiPromise.abort } );
+               }
+
+               function checkPassword() {
+                       // We could just use .then() if we didn't have to pass on .abort()…
+                       var apiPromise,
+                               d = $.Deferred();
+
+                       if ( $usernameInput.val().trim() === '' ) {
+                               d.resolve( { valid: true, messages: [] } );
+                               return d.promise();
+                       }
+
+                       apiPromise = api.post( {
+                               action: 'validatepassword',
+                               user: $usernameInput.val(),
+                               password: $passwordInput.val(),
+                               email: $emailInput.val() || '',
+                               realname: $realNameInput.val() || '',
+                               formatversion: 2,
+                               errorformat: 'html',
+                               errorsuselocal: true,
+                               uselang: mw.config.get( 'wgUserLanguage' )
+                       } )
+                               .done( function ( resp ) {
+                                       var pwinfo = resp.validatepassword || {};
+
+                                       d.resolve( {
+                                               valid: pwinfo.validity === 'Good',
+                                               messages: pwinfo.validitymessages ? pwinfo.validitymessages.map( function ( m ) {
+                                                       return m.html;
+                                               } ) : []
+                                       } );
+                               } )
+                               .fail( d.reject );
+
+                       return d.promise( { abort: apiPromise.abort } );
+               }
+
+               usernameChecker = new mw.htmlform.Checker( $usernameInput, checkUsername );
+               usernameChecker.attach();
+
+               passwordChecker = new mw.htmlform.Checker( $passwordInput, checkPassword );
+               passwordChecker.attach( $usernameInput.add( $emailInput ).add( $realNameInput ) );
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-contributors.png b/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-contributors.png
new file mode 100644 (file)
index 0000000..30bf53a
Binary files /dev/null and b/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-contributors.png differ
diff --git a/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-edits.png b/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-edits.png
new file mode 100644 (file)
index 0000000..17508f9
Binary files /dev/null and b/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-edits.png differ
diff --git a/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-pages.png b/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-pages.png
new file mode 100644 (file)
index 0000000..8e37278
Binary files /dev/null and b/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-pages.png differ
diff --git a/resources/src/mediawiki.special.userlogin.signup.styles/signup.css b/resources/src/mediawiki.special.userlogin.signup.styles/signup.css
new file mode 100644 (file)
index 0000000..3cfa5a8
--- /dev/null
@@ -0,0 +1,67 @@
+/* Disable the underline that Vector puts on h2 headings, and bold them. */
+.mw-ui-container h2 {
+       border: 0;
+       font-weight: bold;
+}
+
+/* Benefits column CSS to the right (if it fits) of the form. */
+.mw-ui-container #userloginForm {
+       float: left;
+       /* Override the right margin of the form to give space in case a benefits
+        * column appears to the side. */
+       margin-right: 100px;
+       /* Override `.mw-body-content` to ensure useful, readable paragraphs */
+       line-height: 1.4;
+}
+
+.mw-createacct-benefits-container {
+       /* Keeps this column compact and close to the form, but tends to squish contents. */
+       float: left;
+}
+
+.mw-createacct-benefits-container h2 {
+       margin-bottom: 30px;
+}
+
+.mw-number-text.icon-edits {
+       /* @embed */
+       background: url( images/icon-edits.png ) no-repeat left center;
+}
+
+.mw-number-text.icon-pages {
+       /* @embed */
+       background: url( images/icon-pages.png ) no-repeat left center;
+}
+
+.mw-number-text.icon-contributors {
+       /* @embed */
+       background: url( images/icon-contributors.png ) no-repeat left center;
+}
+
+/*
+ * Special font for numbers in benefits, same as Vector's `@content-heading-font-family`.
+ * Needs an ID so that it's more specific than Vector's div#content h3.
+ */
+#bodyContent .mw-number-text h3 {
+       color: #222;
+       margin: 0;
+       padding: 0;
+       font-family: 'Linux Libertine', 'Georgia', 'Times', serif;
+       font-weight: normal;
+       font-size: 2.2em;
+       line-height: 1.2;
+       text-align: center;
+}
+
+/* Contains a “headlined” number and explanatory text, with space for an icon */
+.mw-number-text {
+       display: block;
+       font-size: 1.2em;
+       color: #444;
+       margin-top: 1em;
+       /* 80px wide icon plus "margin" */
+       padding: 0 0 0 95px;
+       /* Matches max icon height, ensures icon emblem is visible */
+       min-height: 75px;
+       text-align: center;
+}
diff --git a/resources/src/mediawiki.special.userrights.js b/resources/src/mediawiki.special.userrights.js
new file mode 100644 (file)
index 0000000..487e63a
--- /dev/null
@@ -0,0 +1,25 @@
+/*!
+ * JavaScript for Special:UserRights
+ */
+( function ( mw, $ ) {
+       var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' ),
+               summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+               summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+               $wpReason = $( '#wpReason' );
+
+       // Replace successbox with notifications
+       convertmessagebox();
+
+       // Dynamically show/hide the "other time" input under each dropdown
+       $( '.mw-userrights-nested select' ).on( 'change', function ( e ) {
+               $( e.target.parentNode ).find( 'input' ).toggle( $( e.target ).val() === 'other' );
+       } );
+
+       // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+       if ( summaryCodePointLimit ) {
+               $wpReason.codePointLimit( summaryCodePointLimit );
+       } else if ( summaryByteLimit ) {
+               $wpReason.byteLimit( summaryByteLimit );
+       }
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.version.css b/resources/src/mediawiki.special.version.css
new file mode 100644 (file)
index 0000000..1b8581a
--- /dev/null
@@ -0,0 +1,39 @@
+/*!
+ * Styling for Special:Version
+ */
+.mw-version-ext-name,
+.mw-version-library-name {
+       font-weight: bold;
+}
+
+.mw-version-ext-license,
+.mw-version-ext-vcs-timestamp {
+       white-space: nowrap;
+}
+
+th.mw-version-ext-col-label {
+       font-size: 0.9em;
+}
+
+.mw-version-ext-vcs-version {
+       unicode-bidi: embed;
+}
+
+.mw-version-credits {
+       column-width: 18em;
+       -moz-column-width: 18em;
+       -webkit-column-width: 18em;
+}
+
+.mw-version-credits ul {
+       margin-top: 0;
+       margin-bottom: 0;
+}
+
+.mw-version-license-info strong {
+       font-weight: normal;
+}
+
+.mw-version-license-info em {
+       font-style: normal;
+}
diff --git a/resources/src/mediawiki.special.watchlist.js b/resources/src/mediawiki.special.watchlist.js
new file mode 100644 (file)
index 0000000..565ed2c
--- /dev/null
@@ -0,0 +1,158 @@
+/*!
+ * JavaScript for Special:Watchlist
+ */
+( function ( mw, $, OO ) {
+       $( function () {
+               var api = new mw.Api(), $progressBar, $resetForm = $( '#mw-watchlist-resetbutton' );
+
+               // If the user wants to reset their watchlist, use an API call to do so (no reload required)
+               // Adapted from a user script by User:NQ of English Wikipedia
+               // (User:NQ/WatchlistResetConfirm.js)
+               $resetForm.submit( function ( event ) {
+                       var $button = $resetForm.find( 'input[name=mw-watchlist-reset-submit]' );
+
+                       event.preventDefault();
+
+                       // Disable reset button to prevent multiple concurrent requests
+                       $button.prop( 'disabled', true );
+
+                       if ( !$progressBar ) {
+                               $progressBar = new OO.ui.ProgressBarWidget( { progress: false } ).$element;
+                               $progressBar.css( {
+                                       position: 'absolute', width: '100%'
+                               } );
+                       }
+                       // Show progress bar
+                       $resetForm.append( $progressBar );
+
+                       // Use action=setnotificationtimestamp to mark all as visited,
+                       // then set all watchlist lines accordingly
+                       api.postWithToken( 'csrf', {
+                               formatversion: 2, action: 'setnotificationtimestamp', entirewatchlist: true
+                       } ).done( function () {
+                               // Enable button again
+                               $button.prop( 'disabled', false );
+                               // Hide the button because further clicks can not generate any visual changes
+                               $button.css( 'visibility', 'hidden' );
+                               $progressBar.detach();
+                               $( '.mw-changeslist-line-watched' )
+                                       .removeClass( 'mw-changeslist-line-watched' )
+                                       .addClass( 'mw-changeslist-line-not-watched' );
+                       } ).fail( function () {
+                               // On error, fall back to server-side reset
+                               // First remove this submit listener and then re-submit the form
+                               $resetForm.off( 'submit' ).submit();
+                       } );
+               } );
+
+               // if the user wishes to reload the watchlist whenever a filter changes
+               if ( mw.user.options.get( 'watchlistreloadautomatically' ) ) {
+                       // add a listener on all form elements in the header form
+                       $( '#mw-watchlist-form input, #mw-watchlist-form select' ).on( 'change', function () {
+                               // submit the form when one of the input fields is modified
+                               $( '#mw-watchlist-form' ).submit();
+                       } );
+               }
+
+               if ( mw.user.options.get( 'watchlistunwatchlinks' ) ) {
+                       // Watch/unwatch toggle link:
+                       // If a page is on the watchlist, a '×' is shown which, when clicked, removes the page from the watchlist.
+                       // After unwatching a page, the '×' becomes a '+', which if clicked re-watches the page.
+                       // Unwatched page entries are struck through and have lowered opacity.
+                       $( '.mw-changeslist' ).on( 'click', '.mw-unwatch-link, .mw-watch-link', function ( event ) {
+                               var $unwatchLink = $( this ), // EnhancedChangesList uses <table> for each row, while OldChangesList uses <li> for each row
+                                       $watchlistLine = $unwatchLink.closest( 'li, table' )
+                                               .find( '[data-target-page]' ),
+                                       pageTitle = $watchlistLine.data( 'targetPage' ),
+                                       isTalk = mw.Title.newFromText( pageTitle ).getNamespaceId() % 2 === 1;
+
+                               // Utility function for looping through each watchlist line that matches
+                               // a certain page or its associated page (e.g. Talk)
+                               function forEachMatchingTitle( title, callback ) {
+
+                                       var titleObj = mw.Title.newFromText( title ),
+                                               pageNamespaceId = titleObj.getNamespaceId(),
+                                               isTalk = pageNamespaceId % 2 === 1,
+                                               associatedTitle = mw.Title.makeTitle( isTalk ? pageNamespaceId - 1 : pageNamespaceId + 1,
+                                                       titleObj.getMainText() ).getPrefixedText();
+                                       $( '.mw-changeslist-line' ).each( function () {
+                                               var $this = $( this ), $row, $unwatchLink;
+
+                                               $this.find( '[data-target-page]' ).each( function () {
+                                                       var $this = $( this ), rowTitle = $this.data( 'targetPage' );
+                                                       if ( rowTitle === title || rowTitle === associatedTitle ) {
+
+                                                               // EnhancedChangesList groups log entries by performer rather than target page. Therefore...
+                                                               // * If using OldChangesList, use the <li>
+                                                               // * If using EnhancedChangesList and $this is part of a grouped log entry, use the <td> sub-entry
+                                                               // * If using EnhancedChangesList and $this is not part of a grouped log entry, use the <table> grouped entry
+                                                               $row =
+                                                                       $this.closest(
+                                                                               'li, table.mw-collapsible.mw-changeslist-log td[data-target-page], table' );
+                                                               $unwatchLink = $row.find( '.mw-unwatch-link, .mw-watch-link' );
+
+                                                               callback( rowTitle, $row, $unwatchLink );
+                                                       }
+                                               } );
+                                       } );
+                               }
+
+                               // Preload the notification module for mw.notify
+                               mw.loader.load( 'mediawiki.notification' );
+
+                               // Depending on whether we are watching or unwatching, for each entry of the page (and its associated page i.e. Talk),
+                               // change the text, tooltip, and non-JS href of the (un)watch button, and update the styling of the watchlist entry.
+                               if ( $unwatchLink.hasClass( 'mw-unwatch-link' ) ) {
+                                       api.unwatch( pageTitle )
+                                               .done( function () {
+                                                       forEachMatchingTitle( pageTitle,
+                                                               function ( rowPageTitle, $row, $rowUnwatchLink ) {
+                                                                       $rowUnwatchLink
+                                                                               .text( mw.msg( 'watchlist-unwatch-undo' ) )
+                                                                               .attr( 'title', mw.msg( 'tooltip-ca-watch' ) )
+                                                                               .attr( 'href',
+                                                                                       mw.util.getUrl( rowPageTitle, { action: 'watch' } ) )
+                                                                               .removeClass( 'mw-unwatch-link loading' )
+                                                                               .addClass( 'mw-watch-link' );
+                                                                       $row.find(
+                                                                               '.mw-changeslist-line-inner, .mw-enhanced-rc-nested' )
+                                                                               .addBack( '.mw-enhanced-rc-nested' ) // For matching log sub-entry
+                                                                               .addClass( 'mw-changelist-line-inner-unwatched' );
+                                                               } );
+
+                                                       mw.notify(
+                                                               mw.message( isTalk ? 'removedwatchtext-talk' : 'removedwatchtext',
+                                                                       pageTitle ), { tag: 'watch-self' } );
+                                               } );
+                               } else {
+                                       api.watch( pageTitle )
+                                               .then( function () {
+                                                       forEachMatchingTitle( pageTitle,
+                                                               function ( rowPageTitle, $row, $rowUnwatchLink ) {
+                                                                       $rowUnwatchLink
+                                                                               .text( mw.msg( 'watchlist-unwatch' ) )
+                                                                               .attr( 'title', mw.msg( 'tooltip-ca-unwatch' ) )
+                                                                               .attr( 'href',
+                                                                                       mw.util.getUrl( rowPageTitle, { action: 'unwatch' } ) )
+                                                                               .removeClass( 'mw-watch-link loading' )
+                                                                               .addClass( 'mw-unwatch-link' );
+                                                                       $row.find( '.mw-changelist-line-inner-unwatched' )
+                                                                               .addBack( '.mw-enhanced-rc-nested' )
+                                                                               .removeClass( 'mw-changelist-line-inner-unwatched' );
+                                                               } );
+
+                                                       mw.notify(
+                                                               mw.message( isTalk ? 'addedwatchtext-talk' : 'addedwatchtext',
+                                                                       pageTitle ), { tag: 'watch-self' } );
+                                               } );
+                               }
+
+                               event.preventDefault();
+                               event.stopPropagation();
+                               $unwatchLink.blur();
+                       } );
+               }
+       } );
+
+}( mediaWiki, jQuery, OO )
+);
diff --git a/resources/src/mediawiki.special.watchlist.styles.css b/resources/src/mediawiki.special.watchlist.styles.css
new file mode 100644 (file)
index 0000000..c9861c2
--- /dev/null
@@ -0,0 +1,15 @@
+/*!
+ * Styling for elements generated by JavaScript on Special:Watchlist
+ */
+.mw-changelist-line-inner-unwatched {
+       text-decoration: line-through;
+       opacity: 0.5;
+}
+
+span.mw-changeslist-line-prefix {
+       display: inline-block;
+}
+/* This can be either a span or a table cell */
+.mw-changeslist-line-prefix {
+       width: 1.25em;
+}
diff --git a/resources/src/mediawiki.special/images/glyph-people-large.png b/resources/src/mediawiki.special/images/glyph-people-large.png
deleted file mode 100644 (file)
index cba3caf..0000000
Binary files a/resources/src/mediawiki.special/images/glyph-people-large.png and /dev/null differ
diff --git a/resources/src/mediawiki.special/images/icon-contributors.png b/resources/src/mediawiki.special/images/icon-contributors.png
deleted file mode 100644 (file)
index 30bf53a..0000000
Binary files a/resources/src/mediawiki.special/images/icon-contributors.png and /dev/null differ
diff --git a/resources/src/mediawiki.special/images/icon-edits.png b/resources/src/mediawiki.special/images/icon-edits.png
deleted file mode 100644 (file)
index 17508f9..0000000
Binary files a/resources/src/mediawiki.special/images/icon-edits.png and /dev/null differ
diff --git a/resources/src/mediawiki.special/images/icon-lock.png b/resources/src/mediawiki.special/images/icon-lock.png
deleted file mode 100644 (file)
index 03f0eec..0000000
Binary files a/resources/src/mediawiki.special/images/icon-lock.png and /dev/null differ
diff --git a/resources/src/mediawiki.special/images/icon-pages.png b/resources/src/mediawiki.special/images/icon-pages.png
deleted file mode 100644 (file)
index 8e37278..0000000
Binary files a/resources/src/mediawiki.special/images/icon-pages.png and /dev/null differ
diff --git a/resources/src/mediawiki.special/mediawiki.special.apisandbox.css b/resources/src/mediawiki.special/mediawiki.special.apisandbox.css
deleted file mode 100644 (file)
index fe5ac41..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-.mw-apisandbox-toolbar {
-       background: #fff;
-       -webkit-position: sticky;
-       position: sticky;
-       top: 0;
-       margin-bottom: -1px;
-       padding: 0.5em 0;
-       border-bottom: 1px solid #a2a9b1;
-       text-align: right;
-       z-index: 1;
-}
-
-#mw-apisandbox-ui .mw-apisandbox-link {
-       display: none;
-}
-
-.mw-apisandbox-popup .oo-ui-popupWidget-body > .oo-ui-widget {
-       vertical-align: middle;
-}
-
-/* So DateTimeInputWidget's calendar popup works... */
-.mw-apisandbox-popup .oo-ui-popupWidget-popup,
-.mw-apisandbox-popup .oo-ui-popupWidget-body {
-       overflow: visible;
-}
-
-/* Display contents of the popup on a single line */
-.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body {
-       display: table;
-}
-
-.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > * {
-       display: table-cell;
-}
-
-.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > .oo-ui-buttonWidget {
-       padding-left: 0.5em;
-       width: 1%;
-}
-
-.mw-apisandbox-spacer {
-       display: inline-block;
-       height: 1px;
-       width: 5em;
-}
-
-.mw-apisandbox-help-field {
-       border-bottom: 1px solid rgba( 0, 0, 0, 0.1 );
-}
-
-.mw-apisandbox-help-field:last-child {
-       border-bottom: 0;
-}
-
-.mw-apisandbox-optionalWidget {
-       width: 100%;
-}
-
-.mw-apisandbox-optionalWidget.oo-ui-widget-disabled {
-       position: relative;
-       z-index: 0; /* New stacking context to prevent the cover from leaking out */
-}
-
-.mw-apisandbox-optionalWidget-cover {
-       position: absolute;
-       left: 0;
-       right: 0;
-       top: 0;
-       bottom: 0;
-       z-index: 2;
-       cursor: pointer;
-}
-
-.mw-apisandbox-optionalWidget-fields {
-       display: table;
-       width: 100%;
-}
-
-.mw-apisandbox-optionalWidget-widget,
-.mw-apisandbox-optionalWidget-checkbox {
-       display: table-cell;
-       vertical-align: middle;
-}
-
-.mw-apisandbox-optionalWidget-checkbox {
-       width: 1%; /* Will be expanded by content */
-       white-space: nowrap;
-       padding-left: 0.5em;
-}
-
-.mw-apisandbox-textInputCode .oo-ui-inputWidget-input {
-       font-family: monospace, monospace;
-       font-size: 0.8125em;
-       -moz-tab-size: 4;
-       tab-size: 4;
-}
-
-.mw-apisandbox-widget-field .oo-ui-textInputWidget {
-       /* Leave at least enough space for icon, indicator, and a sliver of text */
-       min-width: 6em;
-}
-
-.apihelp-deprecated {
-       font-weight: bold;
-       color: #d33;
-}
-
-.apihelp-deprecated-value .oo-ui-labelElement-label {
-       text-decoration: line-through;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.apisandbox.js b/resources/src/mediawiki.special/mediawiki.special.apisandbox.js
deleted file mode 100644 (file)
index 523a62e..0000000
+++ /dev/null
@@ -1,1864 +0,0 @@
-( function ( $, mw, OO ) {
-       'use strict';
-       var ApiSandbox, Util, WidgetMethods, Validators,
-               $content, panel, booklet, oldhash, windowManager,
-               formatDropdown,
-               api = new mw.Api(),
-               bookletPages = [],
-               availableFormats = {},
-               resultPage = null,
-               suppressErrors = true,
-               updatingBooklet = false,
-               pages = {},
-               moduleInfoCache = {},
-               baseRequestParams;
-
-       /**
-        * A wrapper for a widget that provides an enable/disable button
-        *
-        * @class
-        * @private
-        * @constructor
-        * @param {OO.ui.Widget} widget
-        * @param {Object} [config] Configuration options
-        */
-       function OptionalWidget( widget, config ) {
-               var k;
-
-               config = config || {};
-
-               this.widget = widget;
-               this.$cover = config.$cover ||
-                       $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-cover' );
-               this.checkbox = new OO.ui.CheckboxInputWidget( config.checkbox )
-                       .on( 'change', this.onCheckboxChange, [], this );
-
-               OptionalWidget[ 'super' ].call( this, config );
-
-               // Forward most methods for convenience
-               for ( k in this.widget ) {
-                       if ( $.isFunction( this.widget[ k ] ) && !this[ k ] ) {
-                               this[ k ] = this.widget[ k ].bind( this.widget );
-                       }
-               }
-
-               this.$cover.on( 'click', this.onOverlayClick.bind( this ) );
-
-               this.$element
-                       .addClass( 'mw-apisandbox-optionalWidget' )
-                       .append(
-                               this.$cover,
-                               $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-fields' ).append(
-                                       $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-widget' ).append(
-                                               widget.$element
-                                       ),
-                                       $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-checkbox' ).append(
-                                               this.checkbox.$element
-                                       )
-                               )
-                       );
-
-               this.setDisabled( widget.isDisabled() );
-       }
-       OO.inheritClass( OptionalWidget, OO.ui.Widget );
-       OptionalWidget.prototype.onCheckboxChange = function ( checked ) {
-               this.setDisabled( !checked );
-       };
-       OptionalWidget.prototype.onOverlayClick = function () {
-               this.setDisabled( false );
-               if ( $.isFunction( this.widget.focus ) ) {
-                       this.widget.focus();
-               }
-       };
-       OptionalWidget.prototype.setDisabled = function ( disabled ) {
-               OptionalWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
-               this.widget.setDisabled( this.isDisabled() );
-               this.checkbox.setSelected( !this.isDisabled() );
-               this.$cover.toggle( this.isDisabled() );
-               return this;
-       };
-
-       WidgetMethods = {
-               textInputWidget: {
-                       getApiValue: function () {
-                               return this.getValue();
-                       },
-                       setApiValue: function ( v ) {
-                               if ( v === undefined ) {
-                                       v = this.paramInfo[ 'default' ];
-                               }
-                               this.setValue( v );
-                       },
-                       apiCheckValid: function () {
-                               var that = this;
-                               return this.getValidity().then( function () {
-                                       return $.Deferred().resolve( true ).promise();
-                               }, function () {
-                                       return $.Deferred().resolve( false ).promise();
-                               } ).done( function ( ok ) {
-                                       ok = ok || suppressErrors;
-                                       that.setIcon( ok ? null : 'alert' );
-                                       that.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
-                               } );
-                       }
-               },
-
-               dateTimeInputWidget: {
-                       getValidity: function () {
-                               if ( !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '' ) {
-                                       return $.Deferred().resolve().promise();
-                               } else {
-                                       return $.Deferred().reject().promise();
-                               }
-                       }
-               },
-
-               tokenWidget: {
-                       alertTokenError: function ( code, error ) {
-                               windowManager.openWindow( 'errorAlert', {
-                                       title: Util.parseMsg( 'apisandbox-results-fixtoken-fail', this.paramInfo.tokentype ),
-                                       message: error,
-                                       actions: [
-                                               {
-                                                       action: 'accept',
-                                                       label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
-                                                       flags: 'primary'
-                                               }
-                                       ]
-                               } );
-                       },
-                       fetchToken: function () {
-                               this.pushPending();
-                               return api.getToken( this.paramInfo.tokentype )
-                                       .done( this.setApiValue.bind( this ) )
-                                       .fail( this.alertTokenError.bind( this ) )
-                                       .always( this.popPending.bind( this ) );
-                       },
-                       setApiValue: function ( v ) {
-                               WidgetMethods.textInputWidget.setApiValue.call( this, v );
-                               if ( v === '123ABC' ) {
-                                       this.fetchToken();
-                               }
-                       }
-               },
-
-               passwordWidget: {
-                       getApiValueForDisplay: function () {
-                               return '';
-                       }
-               },
-
-               toggleSwitchWidget: {
-                       getApiValue: function () {
-                               return this.getValue() ? 1 : undefined;
-                       },
-                       setApiValue: function ( v ) {
-                               this.setValue( Util.apiBool( v ) );
-                       },
-                       apiCheckValid: function () {
-                               return $.Deferred().resolve( true ).promise();
-                       }
-               },
-
-               dropdownWidget: {
-                       getApiValue: function () {
-                               var item = this.getMenu().findSelectedItem();
-                               return item === null ? undefined : item.getData();
-                       },
-                       setApiValue: function ( v ) {
-                               var menu = this.getMenu();
-
-                               if ( v === undefined ) {
-                                       v = this.paramInfo[ 'default' ];
-                               }
-                               if ( v === undefined ) {
-                                       menu.selectItem();
-                               } else {
-                                       menu.selectItemByData( String( v ) );
-                               }
-                       },
-                       apiCheckValid: function () {
-                               var ok = this.getApiValue() !== undefined || suppressErrors;
-                               this.setIcon( ok ? null : 'alert' );
-                               this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
-                               return $.Deferred().resolve( ok ).promise();
-                       }
-               },
-
-               tagWidget: {
-                       getApiValue: function () {
-                               var items = this.getValue();
-                               if ( items.join( '' ).indexOf( '|' ) === -1 ) {
-                                       return items.join( '|' );
-                               } else {
-                                       return '\x1f' + items.join( '\x1f' );
-                               }
-                       },
-                       setApiValue: function ( v ) {
-                               if ( v === undefined || v === '' || v === '\x1f' ) {
-                                       this.setValue( [] );
-                               } else {
-                                       v = String( v );
-                                       if ( v.indexOf( '\x1f' ) !== 0 ) {
-                                               this.setValue( v.split( '|' ) );
-                                       } else {
-                                               this.setValue( v.substr( 1 ).split( '\x1f' ) );
-                                       }
-                               }
-                       },
-                       apiCheckValid: function () {
-                               var ok = true,
-                                       pi = this.paramInfo;
-
-                               if ( !suppressErrors ) {
-                                       ok = this.getApiValue() !== undefined && !(
-                                               pi.allspecifier !== undefined &&
-                                               this.getValue().length > 1 &&
-                                               this.getValue().indexOf( pi.allspecifier ) !== -1
-                                       );
-                               }
-
-                               this.setIcon( ok ? null : 'alert' );
-                               this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
-                               return $.Deferred().resolve( ok ).promise();
-                       },
-                       createTagItemWidget: function ( data, label ) {
-                               var item = OO.ui.TagMultiselectWidget.prototype.createTagItemWidget.call( this, data, label );
-                               if ( this.paramInfo.deprecatedvalues &&
-                                       this.paramInfo.deprecatedvalues.indexOf( data ) >= 0
-                               ) {
-                                       item.$element.addClass( 'apihelp-deprecated-value' );
-                               }
-                               return item;
-                       }
-               },
-
-               optionalWidget: {
-                       getApiValue: function () {
-                               return this.isDisabled() ? undefined : this.widget.getApiValue();
-                       },
-                       setApiValue: function ( v ) {
-                               this.setDisabled( v === undefined );
-                               this.widget.setApiValue( v );
-                       },
-                       apiCheckValid: function () {
-                               if ( this.isDisabled() ) {
-                                       return $.Deferred().resolve( true ).promise();
-                               } else {
-                                       return this.widget.apiCheckValid();
-                               }
-                       }
-               },
-
-               submoduleWidget: {
-                       single: function () {
-                               var v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
-                               return v === undefined ? [] : [ { value: v, path: this.paramInfo.submodules[ v ] } ];
-                       },
-                       multi: function () {
-                               var map = this.paramInfo.submodules,
-                                       v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
-                               return v === undefined || v === '' ? [] : String( v ).split( '|' ).map( function ( v ) {
-                                       return { value: v, path: map[ v ] };
-                               } );
-                       }
-               },
-
-               uploadWidget: {
-                       getApiValueForDisplay: function () {
-                               return '...';
-                       },
-                       getApiValue: function () {
-                               return this.getValue();
-                       },
-                       setApiValue: function () {
-                               // Can't, sorry.
-                       },
-                       apiCheckValid: function () {
-                               var ok = this.getValue() !== null || suppressErrors;
-                               this.setIcon( ok ? null : 'alert' );
-                               this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
-                               return $.Deferred().resolve( ok ).promise();
-                       }
-               }
-       };
-
-       Validators = {
-               generic: function () {
-                       return !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '';
-               }
-       };
-
-       /**
-        * @class mw.special.ApiSandbox.Util
-        * @private
-        */
-       Util = {
-               /**
-                * Fetch API module info
-                *
-                * @param {string} module Module to fetch data for
-                * @return {jQuery.Promise}
-                */
-               fetchModuleInfo: function ( module ) {
-                       var apiPromise,
-                               deferred = $.Deferred();
-
-                       if ( moduleInfoCache.hasOwnProperty( module ) ) {
-                               return deferred
-                                       .resolve( moduleInfoCache[ module ] )
-                                       .promise( { abort: function () {} } );
-                       } else {
-                               apiPromise = api.post( {
-                                       action: 'paraminfo',
-                                       modules: module,
-                                       helpformat: 'html',
-                                       uselang: mw.config.get( 'wgUserLanguage' )
-                               } ).done( function ( data ) {
-                                       var info;
-
-                                       if ( data.warnings && data.warnings.paraminfo ) {
-                                               deferred.reject( '???', data.warnings.paraminfo[ '*' ] );
-                                               return;
-                                       }
-
-                                       info = data.paraminfo.modules;
-                                       if ( !info || info.length !== 1 || info[ 0 ].path !== module ) {
-                                               deferred.reject( '???', 'No module data returned' );
-                                               return;
-                                       }
-
-                                       moduleInfoCache[ module ] = info[ 0 ];
-                                       deferred.resolve( info[ 0 ] );
-                               } ).fail( function ( code, details ) {
-                                       if ( code === 'http' ) {
-                                               details = 'HTTP error: ' + details.exception;
-                                       } else if ( details.error ) {
-                                               details = details.error.info;
-                                       }
-                                       deferred.reject( code, details );
-                               } );
-                               return deferred
-                                       .promise( { abort: apiPromise.abort } );
-                       }
-               },
-
-               /**
-                * Mark all currently-in-use tokens as bad
-                */
-               markTokensBad: function () {
-                       var page, subpages, i,
-                               checkPages = [ pages.main ];
-
-                       while ( checkPages.length ) {
-                               page = checkPages.shift();
-
-                               if ( page.tokenWidget ) {
-                                       api.badToken( page.tokenWidget.paramInfo.tokentype );
-                               }
-
-                               subpages = page.getSubpages();
-                               for ( i = 0; i < subpages.length; i++ ) {
-                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
-                                               checkPages.push( pages[ subpages[ i ].key ] );
-                                       }
-                               }
-                       }
-               },
-
-               /**
-                * Test an API boolean
-                *
-                * @param {Mixed} value
-                * @return {boolean}
-                */
-               apiBool: function ( value ) {
-                       return value !== undefined && value !== false;
-               },
-
-               /**
-                * Create a widget for a parameter.
-                *
-                * @param {Object} pi Parameter info from API
-                * @param {Object} opts Additional options
-                * @return {OO.ui.Widget}
-                */
-               createWidgetForParameter: function ( pi, opts ) {
-                       var widget, innerWidget, finalWidget, items, $content, func,
-                               multiModeButton = null,
-                               multiModeInput = null,
-                               multiModeAllowed = false;
-
-                       opts = opts || {};
-
-                       switch ( pi.type ) {
-                               case 'boolean':
-                                       widget = new OO.ui.ToggleSwitchWidget();
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.toggleSwitchWidget );
-                                       pi.required = true; // Avoid wrapping in the non-required widget
-                                       break;
-
-                               case 'string':
-                               case 'user':
-                                       if ( Util.apiBool( pi.multi ) ) {
-                                               widget = new OO.ui.TagMultiselectWidget( {
-                                                       allowArbitrary: true,
-                                                       allowDuplicates: Util.apiBool( pi.allowsduplicates ),
-                                                       $overlay: true
-                                               } );
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.tagWidget );
-                                       } else {
-                                               widget = new OO.ui.TextInputWidget( {
-                                                       required: Util.apiBool( pi.required )
-                                               } );
-                                       }
-                                       if ( !Util.apiBool( pi.multi ) ) {
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.textInputWidget );
-                                               widget.setValidation( Validators.generic );
-                                       }
-                                       if ( pi.tokentype ) {
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.textInputWidget );
-                                               $.extend( widget, WidgetMethods.tokenWidget );
-                                       }
-                                       break;
-
-                               case 'text':
-                                       widget = new OO.ui.MultilineTextInputWidget( {
-                                               required: Util.apiBool( pi.required )
-                                       } );
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.textInputWidget );
-                                       widget.setValidation( Validators.generic );
-                                       break;
-
-                               case 'password':
-                                       widget = new OO.ui.TextInputWidget( {
-                                               type: 'password',
-                                               required: Util.apiBool( pi.required )
-                                       } );
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.textInputWidget );
-                                       $.extend( widget, WidgetMethods.passwordWidget );
-                                       widget.setValidation( Validators.generic );
-                                       multiModeAllowed = true;
-                                       multiModeInput = widget;
-                                       break;
-
-                               case 'integer':
-                                       widget = new OO.ui.NumberInputWidget( {
-                                               required: Util.apiBool( pi.required ),
-                                               isInteger: true
-                                       } );
-                                       widget.setIcon = widget.input.setIcon.bind( widget.input );
-                                       widget.setIconTitle = widget.input.setIconTitle.bind( widget.input );
-                                       widget.getValidity = widget.input.getValidity.bind( widget.input );
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.textInputWidget );
-                                       if ( Util.apiBool( pi.enforcerange ) ) {
-                                               widget.setRange( pi.min || -Infinity, pi.max || Infinity );
-                                       }
-                                       multiModeAllowed = true;
-                                       multiModeInput = widget;
-                                       break;
-
-                               case 'limit':
-                                       widget = new OO.ui.TextInputWidget( {
-                                               required: Util.apiBool( pi.required )
-                                       } );
-                                       widget.setValidation( function ( value ) {
-                                               var n, pi = this.paramInfo;
-
-                                               if ( value === 'max' ) {
-                                                       return true;
-                                               } else {
-                                                       n = +value;
-                                                       return !isNaN( n ) && isFinite( n ) &&
-                                                               Math.floor( n ) === n &&
-                                                               n >= pi.min && n <= pi.apiSandboxMax;
-                                               }
-                                       } );
-                                       pi.min = pi.min || 0;
-                                       pi.apiSandboxMax = mw.config.get( 'apihighlimits' ) ? pi.highmax : pi.max;
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.textInputWidget );
-                                       multiModeAllowed = true;
-                                       multiModeInput = widget;
-                                       break;
-
-                               case 'timestamp':
-                                       widget = new mw.widgets.datetime.DateTimeInputWidget( {
-                                               formatter: {
-                                                       format: '${year|0}-${month|0}-${day|0}T${hour|0}:${minute|0}:${second|0}${zone|short}'
-                                               },
-                                               required: Util.apiBool( pi.required ),
-                                               clearable: false
-                                       } );
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.textInputWidget );
-                                       $.extend( widget, WidgetMethods.dateTimeInputWidget );
-                                       multiModeAllowed = true;
-                                       break;
-
-                               case 'upload':
-                                       widget = new OO.ui.SelectFileWidget();
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.uploadWidget );
-                                       break;
-
-                               case 'namespace':
-                                       items = $.map( mw.config.get( 'wgFormattedNamespaces' ), function ( name, ns ) {
-                                               if ( ns === '0' ) {
-                                                       name = mw.message( 'blanknamespace' ).text();
-                                               }
-                                               return new OO.ui.MenuOptionWidget( { data: ns, label: name } );
-                                       } ).sort( function ( a, b ) {
-                                               return a.data - b.data;
-                                       } );
-                                       if ( Util.apiBool( pi.multi ) ) {
-                                               if ( pi.allspecifier !== undefined ) {
-                                                       items.unshift( new OO.ui.MenuOptionWidget( {
-                                                               data: pi.allspecifier,
-                                                               label: mw.message( 'apisandbox-multivalue-all-namespaces', pi.allspecifier ).text()
-                                                       } ) );
-                                               }
-
-                                               widget = new OO.ui.MenuTagMultiselectWidget( {
-                                                       menu: { items: items },
-                                                       $overlay: true
-                                               } );
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.tagWidget );
-                                       } else {
-                                               widget = new OO.ui.DropdownWidget( {
-                                                       menu: { items: items },
-                                                       $overlay: true
-                                               } );
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.dropdownWidget );
-                                       }
-                                       break;
-
-                               default:
-                                       if ( !Array.isArray( pi.type ) ) {
-                                               throw new Error( 'Unknown parameter type ' + pi.type );
-                                       }
-
-                                       items = pi.type.map( function ( v ) {
-                                               var config = {
-                                                       data: String( v ),
-                                                       label: String( v ),
-                                                       classes: []
-                                               };
-                                               if ( pi.deprecatedvalues && pi.deprecatedvalues.indexOf( v ) >= 0 ) {
-                                                       config.classes.push( 'apihelp-deprecated-value' );
-                                               }
-                                               return new OO.ui.MenuOptionWidget( config );
-                                       } );
-                                       if ( Util.apiBool( pi.multi ) ) {
-                                               if ( pi.allspecifier !== undefined ) {
-                                                       items.unshift( new OO.ui.MenuOptionWidget( {
-                                                               data: pi.allspecifier,
-                                                               label: mw.message( 'apisandbox-multivalue-all-values', pi.allspecifier ).text()
-                                                       } ) );
-                                               }
-
-                                               widget = new OO.ui.MenuTagMultiselectWidget( {
-                                                       menu: { items: items },
-                                                       $overlay: true
-                                               } );
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.tagWidget );
-                                               if ( Util.apiBool( pi.submodules ) ) {
-                                                       widget.getSubmodules = WidgetMethods.submoduleWidget.multi;
-                                                       widget.on( 'change', ApiSandbox.updateUI );
-                                               }
-                                       } else {
-                                               widget = new OO.ui.DropdownWidget( {
-                                                       menu: { items: items },
-                                                       $overlay: true
-                                               } );
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.dropdownWidget );
-                                               if ( Util.apiBool( pi.submodules ) ) {
-                                                       widget.getSubmodules = WidgetMethods.submoduleWidget.single;
-                                                       widget.getMenu().on( 'select', ApiSandbox.updateUI );
-                                               }
-                                               if ( pi.deprecatedvalues ) {
-                                                       widget.getMenu().on( 'select', function ( item ) {
-                                                               this.$element.toggleClass(
-                                                                       'apihelp-deprecated-value',
-                                                                       pi.deprecatedvalues.indexOf( item.data ) >= 0
-                                                               );
-                                                       }, [], widget );
-                                               }
-                                       }
-
-                                       break;
-                       }
-
-                       if ( Util.apiBool( pi.multi ) && multiModeAllowed ) {
-                               innerWidget = widget;
-
-                               multiModeButton = new OO.ui.ButtonWidget( {
-                                       label: mw.message( 'apisandbox-add-multi' ).text()
-                               } );
-                               $content = innerWidget.$element.add( multiModeButton.$element );
-
-                               widget = new OO.ui.PopupTagMultiselectWidget( {
-                                       allowArbitrary: true,
-                                       allowDuplicates: Util.apiBool( pi.allowsduplicates ),
-                                       $overlay: true,
-                                       popup: {
-                                               classes: [ 'mw-apisandbox-popup' ],
-                                               padded: true,
-                                               $content: $content
-                                       }
-                               } );
-                               widget.paramInfo = pi;
-                               $.extend( widget, WidgetMethods.tagWidget );
-
-                               func = function () {
-                                       if ( !innerWidget.isDisabled() ) {
-                                               innerWidget.apiCheckValid().done( function ( ok ) {
-                                                       if ( ok ) {
-                                                               widget.addTag( innerWidget.getApiValue() );
-                                                               innerWidget.setApiValue( undefined );
-                                                       }
-                                               } );
-                                               return false;
-                                       }
-                               };
-
-                               if ( multiModeInput ) {
-                                       multiModeInput.on( 'enter', func );
-                               }
-                               multiModeButton.on( 'click', func );
-                       }
-
-                       if ( Util.apiBool( pi.required ) || opts.nooptional ) {
-                               finalWidget = widget;
-                       } else {
-                               finalWidget = new OptionalWidget( widget );
-                               finalWidget.paramInfo = pi;
-                               $.extend( finalWidget, WidgetMethods.optionalWidget );
-                               if ( widget.getSubmodules ) {
-                                       finalWidget.getSubmodules = widget.getSubmodules.bind( widget );
-                                       finalWidget.on( 'disable', function () { setTimeout( ApiSandbox.updateUI ); } );
-                               }
-                               finalWidget.setDisabled( true );
-                       }
-
-                       widget.setApiValue( pi[ 'default' ] );
-
-                       return finalWidget;
-               },
-
-               /**
-                * Parse an HTML string and call Util.fixupHTML()
-                *
-                * @param {string} html HTML to parse
-                * @return {jQuery}
-                */
-               parseHTML: function ( html ) {
-                       var $ret = $( $.parseHTML( html ) );
-                       return Util.fixupHTML( $ret );
-               },
-
-               /**
-                * Parse an i18n message and call Util.fixupHTML()
-                *
-                * @param {string} key Key of message to get
-                * @param {...Mixed} parameters Values for $N replacements
-                * @return {jQuery}
-                */
-               parseMsg: function () {
-                       var $ret = mw.message.apply( mw.message, arguments ).parseDom();
-                       return Util.fixupHTML( $ret );
-               },
-
-               /**
-                * Fix HTML for ApiSandbox display
-                *
-                * Fixes are:
-                * - Add target="_blank" to any links
-                *
-                * @param {jQuery} $html DOM to process
-                * @return {jQuery}
-                */
-               fixupHTML: function ( $html ) {
-                       $html.filter( 'a' ).add( $html.find( 'a' ) )
-                               .filter( '[href]:not([target])' )
-                               .attr( 'target', '_blank' );
-                       return $html;
-               },
-
-               /**
-                * Format a request and return a bunch of menu option widgets
-                *
-                * @param {Object} displayParams Query parameters, sanitized for display.
-                * @param {Object} rawParams Query parameters. You should probably use displayParams instead.
-                * @return {OO.ui.MenuOptionWidget[]} Each item's data should be an OO.ui.FieldLayout
-                */
-               formatRequest: function ( displayParams, rawParams ) {
-                       var jsonInput,
-                               items = [
-                                       new OO.ui.MenuOptionWidget( {
-                                               label: Util.parseMsg( 'apisandbox-request-format-url-label' ),
-                                               data: new OO.ui.FieldLayout(
-                                                       new OO.ui.TextInputWidget( {
-                                                               readOnly: true,
-                                                               value: mw.util.wikiScript( 'api' ) + '?' + $.param( displayParams )
-                                                       } ), {
-                                                               label: Util.parseMsg( 'apisandbox-request-url-label' )
-                                                       }
-                                               )
-                                       } ),
-                                       new OO.ui.MenuOptionWidget( {
-                                               label: Util.parseMsg( 'apisandbox-request-format-json-label' ),
-                                               data: new OO.ui.FieldLayout(
-                                                       jsonInput = new OO.ui.MultilineTextInputWidget( {
-                                                               classes: [ 'mw-apisandbox-textInputCode' ],
-                                                               readOnly: true,
-                                                               autosize: true,
-                                                               maxRows: 6,
-                                                               value: JSON.stringify( displayParams, null, '\t' )
-                                                       } ), {
-                                                               label: Util.parseMsg( 'apisandbox-request-json-label' )
-                                                       }
-                                               ).on( 'toggle', function ( visible ) {
-                                                       if ( visible ) {
-                                                               // Call updatePosition instead of adjustSize
-                                                               // because the latter has weird caching
-                                                               // behavior and the former bypasses it.
-                                                               jsonInput.updatePosition();
-                                                       }
-                                               } )
-                                       } )
-                               ];
-
-                       mw.hook( 'apisandbox.formatRequest' ).fire( items, displayParams, rawParams );
-
-                       return items;
-               },
-
-               /**
-                * Event handler for when formatDropdown's selection changes
-                */
-               onFormatDropdownChange: function () {
-                       var i,
-                               menu = formatDropdown.getMenu(),
-                               items = menu.getItems(),
-                               selectedField = menu.findSelectedItem() ? menu.findSelectedItem().getData() : null;
-
-                       for ( i = 0; i < items.length; i++ ) {
-                               items[ i ].getData().toggle( items[ i ].getData() === selectedField );
-                       }
-               }
-       };
-
-       /**
-       * Interface to ApiSandbox UI
-       *
-       * @class mw.special.ApiSandbox
-       */
-       ApiSandbox = {
-               /**
-                * Initialize the UI
-                *
-                * Automatically called on $.ready()
-                */
-               init: function () {
-                       var $toolbar;
-
-                       $content = $( '#mw-apisandbox' );
-
-                       windowManager = new OO.ui.WindowManager();
-                       $( 'body' ).append( windowManager.$element );
-                       windowManager.addWindows( {
-                               errorAlert: new OO.ui.MessageDialog()
-                       } );
-
-                       $toolbar = $( '<div>' )
-                               .addClass( 'mw-apisandbox-toolbar' )
-                               .append(
-                                       new OO.ui.ButtonWidget( {
-                                               label: mw.message( 'apisandbox-submit' ).text(),
-                                               flags: [ 'primary', 'progressive' ]
-                                       } ).on( 'click', ApiSandbox.sendRequest ).$element,
-                                       new OO.ui.ButtonWidget( {
-                                               label: mw.message( 'apisandbox-reset' ).text(),
-                                               flags: 'destructive'
-                                       } ).on( 'click', ApiSandbox.resetUI ).$element
-                               );
-
-                       booklet = new OO.ui.BookletLayout( {
-                               expanded: false,
-                               outlined: true,
-                               autoFocus: false
-                       } );
-
-                       panel = new OO.ui.PanelLayout( {
-                               classes: [ 'mw-apisandbox-container' ],
-                               content: [ booklet ],
-                               expanded: false,
-                               framed: true
-                       } );
-
-                       pages.main = new ApiSandbox.PageLayout( { key: 'main', path: 'main' } );
-
-                       // Parse the current hash string
-                       if ( !ApiSandbox.loadFromHash() ) {
-                               ApiSandbox.updateUI();
-                       }
-
-                       $( window ).on( 'hashchange', ApiSandbox.loadFromHash );
-
-                       $content
-                               .empty()
-                               .append( $( '<p>' ).append( Util.parseMsg( 'apisandbox-intro' ) ) )
-                               .append(
-                                       $( '<div>' ).attr( 'id', 'mw-apisandbox-ui' )
-                                               .append( $toolbar )
-                                               .append( panel.$element )
-                               );
-               },
-
-               /**
-                * Update the current query when the page hash changes
-                *
-                * @return {boolean} Successful
-                */
-               loadFromHash: function () {
-                       var params, m, re,
-                               hash = location.hash;
-
-                       if ( oldhash === hash ) {
-                               return false;
-                       }
-                       oldhash = hash;
-                       if ( hash === '' ) {
-                               return false;
-                       }
-
-                       // I'm surprised this doesn't seem to exist in jQuery or mw.util.
-                       params = {};
-                       hash = hash.replace( /\+/g, '%20' );
-                       re = /([^&=#]+)=?([^&#]*)/g;
-                       while ( ( m = re.exec( hash ) ) ) {
-                               params[ decodeURIComponent( m[ 1 ] ) ] = decodeURIComponent( m[ 2 ] );
-                       }
-
-                       ApiSandbox.updateUI( params );
-                       return true;
-               },
-
-               /**
-                * Update the pages in the booklet
-                *
-                * @param {Object} [params] Optional query parameters to load
-                */
-               updateUI: function ( params ) {
-                       var i, page, subpages, j, removePages,
-                               addPages = [];
-
-                       if ( !$.isPlainObject( params ) ) {
-                               params = undefined;
-                       }
-
-                       if ( updatingBooklet ) {
-                               return;
-                       }
-                       updatingBooklet = true;
-                       try {
-                               if ( params !== undefined ) {
-                                       pages.main.loadQueryParams( params );
-                               }
-                               addPages.push( pages.main );
-                               if ( resultPage !== null ) {
-                                       addPages.push( resultPage );
-                               }
-                               pages.main.apiCheckValid();
-
-                               i = 0;
-                               while ( addPages.length ) {
-                                       page = addPages.shift();
-                                       if ( bookletPages[ i ] !== page ) {
-                                               for ( j = i; j < bookletPages.length; j++ ) {
-                                                       if ( bookletPages[ j ].getName() === page.getName() ) {
-                                                               bookletPages.splice( j, 1 );
-                                                       }
-                                               }
-                                               bookletPages.splice( i, 0, page );
-                                               booklet.addPages( [ page ], i );
-                                       }
-                                       i++;
-
-                                       if ( page.getSubpages ) {
-                                               subpages = page.getSubpages();
-                                               for ( j = 0; j < subpages.length; j++ ) {
-                                                       if ( !pages.hasOwnProperty( subpages[ j ].key ) ) {
-                                                               subpages[ j ].indentLevel = page.indentLevel + 1;
-                                                               pages[ subpages[ j ].key ] = new ApiSandbox.PageLayout( subpages[ j ] );
-                                                       }
-                                                       if ( params !== undefined ) {
-                                                               pages[ subpages[ j ].key ].loadQueryParams( params );
-                                                       }
-                                                       addPages.splice( j, 0, pages[ subpages[ j ].key ] );
-                                                       pages[ subpages[ j ].key ].apiCheckValid();
-                                               }
-                                       }
-                               }
-
-                               if ( bookletPages.length > i ) {
-                                       removePages = bookletPages.splice( i, bookletPages.length - i );
-                                       booklet.removePages( removePages );
-                               }
-
-                               if ( !booklet.getCurrentPageName() ) {
-                                       booklet.selectFirstSelectablePage();
-                               }
-                       } finally {
-                               updatingBooklet = false;
-                       }
-               },
-
-               /**
-                * Reset button handler
-                */
-               resetUI: function () {
-                       suppressErrors = true;
-                       pages = {
-                               main: new ApiSandbox.PageLayout( { key: 'main', path: 'main' } )
-                       };
-                       resultPage = null;
-                       ApiSandbox.updateUI();
-               },
-
-               /**
-                * Submit button handler
-                *
-                * @param {Object} [params] Use this set of params instead of those in the form fields.
-                *   The form fields will be updated to match.
-                */
-               sendRequest: function ( params ) {
-                       var page, subpages, i, query, $result, $focus,
-                               progress, $progressText, progressLoading,
-                               deferreds = [],
-                               paramsAreForced = !!params,
-                               displayParams = {},
-                               tokenWidgets = [],
-                               checkPages = [ pages.main ];
-
-                       // Blur any focused widget before submit, because
-                       // OO.ui.ButtonWidget doesn't take focus itself (T128054)
-                       $focus = $( '#mw-apisandbox-ui' ).find( document.activeElement );
-                       if ( $focus.length ) {
-                               $focus[ 0 ].blur();
-                       }
-
-                       suppressErrors = false;
-
-                       // save widget state in params (or load from it if we are forced)
-                       if ( paramsAreForced ) {
-                               ApiSandbox.updateUI( params );
-                       }
-                       params = {};
-                       while ( checkPages.length ) {
-                               page = checkPages.shift();
-                               if ( page.tokenWidget ) {
-                                       tokenWidgets.push( page.tokenWidget );
-                               }
-                               deferreds = deferreds.concat( page.apiCheckValid() );
-                               page.getQueryParams( params, displayParams );
-                               subpages = page.getSubpages();
-                               for ( i = 0; i < subpages.length; i++ ) {
-                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
-                                               checkPages.push( pages[ subpages[ i ].key ] );
-                                       }
-                               }
-                       }
-
-                       if ( !paramsAreForced ) {
-                               // forced params means we are continuing a query; the base query should be preserved
-                               baseRequestParams = $.extend( {}, params );
-                       }
-
-                       $.when.apply( $, deferreds ).done( function () {
-                               var formatItems, menu, selectedLabel, deferred, actions, errorCount;
-
-                               // Count how many times `value` occurs in `array`.
-                               function countValues( value, array ) {
-                                       var count, i;
-                                       count = 0;
-                                       for ( i = 0; i < array.length; i++ ) {
-                                               if ( array[ i ] === value ) {
-                                                       count++;
-                                               }
-                                       }
-                                       return count;
-                               }
-
-                               errorCount = countValues( false, arguments );
-                               if ( errorCount > 0 ) {
-                                       actions = [
-                                               {
-                                                       action: 'accept',
-                                                       label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
-                                                       flags: 'primary'
-                                               }
-                                       ];
-                                       if ( tokenWidgets.length ) {
-                                               // Check all token widgets' validity separately
-                                               deferred = $.when.apply( $, tokenWidgets.map( function ( w ) {
-                                                       return w.apiCheckValid();
-                                               } ) );
-
-                                               deferred.done( function () {
-                                                       // If only the tokens are invalid, offer to fix them
-                                                       var tokenErrorCount = countValues( false, arguments );
-                                                       if ( tokenErrorCount === errorCount ) {
-                                                               delete actions[ 0 ].flags;
-                                                               actions.push( {
-                                                                       action: 'fix',
-                                                                       label: mw.message( 'apisandbox-results-fixtoken' ).text(),
-                                                                       flags: 'primary'
-                                                               } );
-                                                       }
-                                               } );
-                                       } else {
-                                               deferred = $.Deferred().resolve();
-                                       }
-                                       deferred.always( function () {
-                                               windowManager.openWindow( 'errorAlert', {
-                                                       title: Util.parseMsg( 'apisandbox-submit-invalid-fields-title' ),
-                                                       message: Util.parseMsg( 'apisandbox-submit-invalid-fields-message' ),
-                                                       actions: actions
-                                               } ).closed.then( function ( data ) {
-                                                       if ( data && data.action === 'fix' ) {
-                                                               ApiSandbox.fixTokenAndResend();
-                                                       }
-                                               } );
-                                       } );
-                                       return;
-                               }
-
-                               query = $.param( displayParams );
-
-                               formatItems = Util.formatRequest( displayParams, params );
-
-                               // Force a 'fm' format with wrappedhtml=1, if available
-                               if ( params.format !== undefined ) {
-                                       if ( availableFormats.hasOwnProperty( params.format + 'fm' ) ) {
-                                               params.format = params.format + 'fm';
-                                       }
-                                       if ( params.format.substr( -2 ) === 'fm' ) {
-                                               params.wrappedhtml = 1;
-                                       }
-                               }
-
-                               progressLoading = false;
-                               $progressText = $( '<span>' ).text( mw.message( 'apisandbox-sending-request' ).text() );
-                               progress = new OO.ui.ProgressBarWidget( {
-                                       progress: false,
-                                       $content: $progressText
-                               } );
-
-                               $result = $( '<div>' )
-                                       .append( progress.$element );
-
-                               resultPage = page = new OO.ui.PageLayout( '|results|', { expanded: false } );
-                               page.setupOutlineItem = function () {
-                                       this.outlineItem.setLabel( mw.message( 'apisandbox-results' ).text() );
-                               };
-
-                               if ( !formatDropdown ) {
-                                       formatDropdown = new OO.ui.DropdownWidget( {
-                                               menu: { items: [] },
-                                               $overlay: true
-                                       } );
-                                       formatDropdown.getMenu().on( 'select', Util.onFormatDropdownChange );
-                               }
-
-                               menu = formatDropdown.getMenu();
-                               selectedLabel = menu.findSelectedItem() ? menu.findSelectedItem().getLabel() : '';
-                               if ( typeof selectedLabel !== 'string' ) {
-                                       selectedLabel = selectedLabel.text();
-                               }
-                               menu.clearItems().addItems( formatItems );
-                               menu.chooseItem( menu.getItemFromLabel( selectedLabel ) || menu.findFirstSelectableItem() );
-
-                               // Fire the event to update field visibilities
-                               Util.onFormatDropdownChange();
-
-                               page.$element.empty()
-                                       .append(
-                                               new OO.ui.FieldLayout(
-                                                       formatDropdown, {
-                                                               label: Util.parseMsg( 'apisandbox-request-selectformat-label' )
-                                                       }
-                                               ).$element,
-                                               formatItems.map( function ( item ) {
-                                                       return item.getData().$element;
-                                               } ),
-                                               $result
-                                       );
-                               ApiSandbox.updateUI();
-                               booklet.setPage( '|results|' );
-
-                               location.href = oldhash = '#' + query;
-
-                               api.post( params, {
-                                       contentType: 'multipart/form-data',
-                                       dataType: 'text',
-                                       xhr: function () {
-                                               var xhr = new window.XMLHttpRequest();
-                                               xhr.upload.addEventListener( 'progress', function ( e ) {
-                                                       if ( !progressLoading ) {
-                                                               if ( e.lengthComputable ) {
-                                                                       progress.setProgress( e.loaded * 100 / e.total );
-                                                               } else {
-                                                                       progress.setProgress( false );
-                                                               }
-                                                       }
-                                               } );
-                                               xhr.addEventListener( 'progress', function ( e ) {
-                                                       if ( !progressLoading ) {
-                                                               progressLoading = true;
-                                                               $progressText.text( mw.message( 'apisandbox-loading-results' ).text() );
-                                                       }
-                                                       if ( e.lengthComputable ) {
-                                                               progress.setProgress( e.loaded * 100 / e.total );
-                                                       } else {
-                                                               progress.setProgress( false );
-                                                       }
-                                               } );
-                                               return xhr;
-                                       }
-                               } )
-                                       .catch( function ( code, data, result, jqXHR ) {
-                                               var deferred = $.Deferred();
-
-                                               if ( code !== 'http' ) {
-                                                       // Not really an error, work around mw.Api thinking it is.
-                                                       deferred.resolve( result, jqXHR );
-                                               } else {
-                                                       // Just forward it.
-                                                       deferred.reject.apply( deferred, arguments );
-                                               }
-                                               return deferred.promise();
-                                       } )
-                                       .then( function ( data, jqXHR ) {
-                                               var m, loadTime, button, clear,
-                                                       ct = jqXHR.getResponseHeader( 'Content-Type' ),
-                                                       loginSuppressed = jqXHR.getResponseHeader( 'MediaWiki-Login-Suppressed' ) || 'false';
-
-                                               $result.empty();
-                                               if ( loginSuppressed !== 'false' ) {
-                                                       $( '<div>' )
-                                                               .addClass( 'warning' )
-                                                               .append( Util.parseMsg( 'apisandbox-results-login-suppressed' ) )
-                                                               .appendTo( $result );
-                                               }
-                                               if ( /^text\/mediawiki-api-prettyprint-wrapped(?:;|$)/.test( ct ) ) {
-                                                       data = JSON.parse( data );
-                                                       if ( data.modules.length ) {
-                                                               mw.loader.load( data.modules );
-                                                       }
-                                                       if ( data.status && data.status !== 200 ) {
-                                                               $( '<div>' )
-                                                                       .addClass( 'api-pretty-header api-pretty-status' )
-                                                                       .append( Util.parseMsg( 'api-format-prettyprint-status', data.status, data.statustext ) )
-                                                                       .appendTo( $result );
-                                                       }
-                                                       $result.append( Util.parseHTML( data.html ) );
-                                                       loadTime = data.time;
-                                               } else if ( ( m = data.match( /<pre[ >][\s\S]*<\/pre>/ ) ) ) {
-                                                       $result.append( Util.parseHTML( m[ 0 ] ) );
-                                                       if ( ( m = data.match( /"wgBackendResponseTime":\s*(\d+)/ ) ) ) {
-                                                               loadTime = parseInt( m[ 1 ], 10 );
-                                                       }
-                                               } else {
-                                                       $( '<pre>' )
-                                                               .addClass( 'api-pretty-content' )
-                                                               .text( data )
-                                                               .appendTo( $result );
-                                               }
-                                               if ( paramsAreForced || data[ 'continue' ] ) {
-                                                       $result.append(
-                                                               $( '<div>' ).append(
-                                                                       new OO.ui.ButtonWidget( {
-                                                                               label: mw.message( 'apisandbox-continue' ).text()
-                                                                       } ).on( 'click', function () {
-                                                                               ApiSandbox.sendRequest( $.extend( {}, baseRequestParams, data[ 'continue' ] ) );
-                                                                       } ).setDisabled( !data[ 'continue' ] ).$element,
-                                                                       ( clear = new OO.ui.ButtonWidget( {
-                                                                               label: mw.message( 'apisandbox-continue-clear' ).text()
-                                                                       } ).on( 'click', function () {
-                                                                               ApiSandbox.updateUI( baseRequestParams );
-                                                                               clear.setDisabled( true );
-                                                                               booklet.setPage( '|results|' );
-                                                                       } ).setDisabled( !paramsAreForced ) ).$element,
-                                                                       new OO.ui.PopupButtonWidget( {
-                                                                               $overlay: true,
-                                                                               framed: false,
-                                                                               icon: 'info',
-                                                                               popup: {
-                                                                                       $content: $( '<div>' ).append( Util.parseMsg( 'apisandbox-continue-help' ) ),
-                                                                                       padded: true,
-                                                                                       width: 'auto'
-                                                                               }
-                                                                       } ).$element
-                                                               )
-                                                       );
-                                               }
-                                               if ( typeof loadTime === 'number' ) {
-                                                       $result.append(
-                                                               $( '<div>' ).append(
-                                                                       new OO.ui.LabelWidget( {
-                                                                               label: mw.message( 'apisandbox-request-time', loadTime ).text()
-                                                                       } ).$element
-                                                               )
-                                                       );
-                                               }
-
-                                               if ( jqXHR.getResponseHeader( 'MediaWiki-API-Error' ) === 'badtoken' ) {
-                                                       // Flush all saved tokens in case one of them is the bad one.
-                                                       Util.markTokensBad();
-                                                       button = new OO.ui.ButtonWidget( {
-                                                               label: mw.message( 'apisandbox-results-fixtoken' ).text()
-                                                       } );
-                                                       button.on( 'click', ApiSandbox.fixTokenAndResend )
-                                                               .on( 'click', button.setDisabled, [ true ], button )
-                                                               .$element.appendTo( $result );
-                                               }
-                                       }, function ( code, data ) {
-                                               var details = 'HTTP error: ' + data.exception;
-                                               $result.empty()
-                                                       .append(
-                                                               new OO.ui.LabelWidget( {
-                                                                       label: mw.message( 'apisandbox-results-error', details ).text(),
-                                                                       classes: [ 'error' ]
-                                                               } ).$element
-                                                       );
-                                       } );
-                       } );
-               },
-
-               /**
-                * Handler for the "Correct token and resubmit" button
-                *
-                * Used on a 'badtoken' error, it re-fetches token parameters for all
-                * pages and then re-submits the query.
-                */
-               fixTokenAndResend: function () {
-                       var page, subpages, i, k,
-                               ok = true,
-                               tokenWait = { dummy: true },
-                               checkPages = [ pages.main ],
-                               success = function ( k ) {
-                                       delete tokenWait[ k ];
-                                       if ( ok && $.isEmptyObject( tokenWait ) ) {
-                                               ApiSandbox.sendRequest();
-                                       }
-                               },
-                               failure = function ( k ) {
-                                       delete tokenWait[ k ];
-                                       ok = false;
-                               };
-
-                       while ( checkPages.length ) {
-                               page = checkPages.shift();
-
-                               if ( page.tokenWidget ) {
-                                       k = page.apiModule + page.tokenWidget.paramInfo.name;
-                                       tokenWait[ k ] = page.tokenWidget.fetchToken();
-                                       tokenWait[ k ]
-                                               .done( success.bind( page.tokenWidget, k ) )
-                                               .fail( failure.bind( page.tokenWidget, k ) );
-                               }
-
-                               subpages = page.getSubpages();
-                               for ( i = 0; i < subpages.length; i++ ) {
-                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
-                                               checkPages.push( pages[ subpages[ i ].key ] );
-                                       }
-                               }
-                       }
-
-                       success( 'dummy', '' );
-               },
-
-               /**
-                * Reset validity indicators for all widgets
-                */
-               updateValidityIndicators: function () {
-                       var page, subpages, i,
-                               checkPages = [ pages.main ];
-
-                       while ( checkPages.length ) {
-                               page = checkPages.shift();
-                               page.apiCheckValid();
-                               subpages = page.getSubpages();
-                               for ( i = 0; i < subpages.length; i++ ) {
-                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
-                                               checkPages.push( pages[ subpages[ i ].key ] );
-                                       }
-                               }
-                       }
-               }
-       };
-
-       /**
-        * PageLayout for API modules
-        *
-        * @class
-        * @private
-        * @extends OO.ui.PageLayout
-        * @constructor
-        * @param {Object} [config] Configuration options
-        */
-       ApiSandbox.PageLayout = function ( config ) {
-               config = $.extend( { prefix: '', expanded: false }, config );
-               this.displayText = config.key;
-               this.apiModule = config.path;
-               this.prefix = config.prefix;
-               this.paramInfo = null;
-               this.apiIsValid = true;
-               this.loadFromQueryParams = null;
-               this.widgets = {};
-               this.tokenWidget = null;
-               this.indentLevel = config.indentLevel ? config.indentLevel : 0;
-               ApiSandbox.PageLayout[ 'super' ].call( this, config.key, config );
-               this.loadParamInfo();
-       };
-       OO.inheritClass( ApiSandbox.PageLayout, OO.ui.PageLayout );
-       ApiSandbox.PageLayout.prototype.setupOutlineItem = function () {
-               this.outlineItem.setLevel( this.indentLevel );
-               this.outlineItem.setLabel( this.displayText );
-               this.outlineItem.setIcon( this.apiIsValid || suppressErrors ? null : 'alert' );
-               this.outlineItem.setIconTitle(
-                       this.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
-               );
-       };
-
-       /**
-        * Fetch module information for this page's module, then create UI
-        */
-       ApiSandbox.PageLayout.prototype.loadParamInfo = function () {
-               var dynamicFieldset, dynamicParamNameWidget,
-                       that = this,
-                       removeDynamicParamWidget = function ( name, layout ) {
-                               dynamicFieldset.removeItems( [ layout ] );
-                               delete that.widgets[ name ];
-                       },
-                       addDynamicParamWidget = function () {
-                               var name, layout, widget, button;
-
-                               // Check name is filled in
-                               name = dynamicParamNameWidget.getValue().trim();
-                               if ( name === '' ) {
-                                       dynamicParamNameWidget.focus();
-                                       return;
-                               }
-
-                               if ( that.widgets[ name ] !== undefined ) {
-                                       windowManager.openWindow( 'errorAlert', {
-                                               title: Util.parseMsg( 'apisandbox-dynamic-error-exists', name ),
-                                               actions: [
-                                                       {
-                                                               action: 'accept',
-                                                               label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
-                                                               flags: 'primary'
-                                                       }
-                                               ]
-                                       } );
-                                       return;
-                               }
-
-                               widget = Util.createWidgetForParameter( {
-                                       name: name,
-                                       type: 'string',
-                                       'default': ''
-                               }, {
-                                       nooptional: true
-                               } );
-                               button = new OO.ui.ButtonWidget( {
-                                       icon: 'trash',
-                                       flags: 'destructive'
-                               } );
-                               layout = new OO.ui.ActionFieldLayout(
-                                       widget,
-                                       button,
-                                       {
-                                               label: name,
-                                               align: 'left'
-                                       }
-                               );
-                               button.on( 'click', removeDynamicParamWidget, [ name, layout ] );
-                               that.widgets[ name ] = widget;
-                               dynamicFieldset.addItems( [ layout ], dynamicFieldset.getItems().length - 1 );
-                               widget.focus();
-
-                               dynamicParamNameWidget.setValue( '' );
-                       };
-
-               this.$element.empty()
-                       .append( new OO.ui.ProgressBarWidget( {
-                               progress: false,
-                               text: mw.message( 'apisandbox-loading', this.displayText ).text()
-                       } ).$element );
-
-               Util.fetchModuleInfo( this.apiModule )
-                       .done( function ( pi ) {
-                               var prefix, i, j, descriptionContainer, widget, layoutConfig, button, widgetField, helpField, tmp, flag, count,
-                                       items = [],
-                                       deprecatedItems = [],
-                                       buttons = [],
-                                       filterFmModules = function ( v ) {
-                                               return v.substr( -2 ) !== 'fm' ||
-                                                       !availableFormats.hasOwnProperty( v.substr( 0, v.length - 2 ) );
-                                       },
-                                       widgetLabelOnClick = function () {
-                                               var f = this.getField();
-                                               if ( $.isFunction( f.setDisabled ) ) {
-                                                       f.setDisabled( false );
-                                               }
-                                               if ( $.isFunction( f.focus ) ) {
-                                                       f.focus();
-                                               }
-                                       };
-
-                               // This is something of a hack. We always want the 'format' and
-                               // 'action' parameters from the main module to be specified,
-                               // and for 'format' we also want to simplify the dropdown since
-                               // we always send the 'fm' variant.
-                               if ( that.apiModule === 'main' ) {
-                                       for ( i = 0; i < pi.parameters.length; i++ ) {
-                                               if ( pi.parameters[ i ].name === 'action' ) {
-                                                       pi.parameters[ i ].required = true;
-                                                       delete pi.parameters[ i ][ 'default' ];
-                                               }
-                                               if ( pi.parameters[ i ].name === 'format' ) {
-                                                       tmp = pi.parameters[ i ].type;
-                                                       for ( j = 0; j < tmp.length; j++ ) {
-                                                               availableFormats[ tmp[ j ] ] = true;
-                                                       }
-                                                       pi.parameters[ i ].type = tmp.filter( filterFmModules );
-                                                       pi.parameters[ i ][ 'default' ] = 'json';
-                                                       pi.parameters[ i ].required = true;
-                                               }
-                                       }
-                               }
-
-                               // Hide the 'wrappedhtml' parameter on format modules
-                               if ( pi.group === 'format' ) {
-                                       pi.parameters = pi.parameters.filter( function ( p ) {
-                                               return p.name !== 'wrappedhtml';
-                                       } );
-                               }
-
-                               that.paramInfo = pi;
-
-                               items.push( new OO.ui.FieldLayout(
-                                       new OO.ui.Widget( {} ).toggle( false ), {
-                                               align: 'top',
-                                               label: Util.parseHTML( pi.description )
-                                       }
-                               ) );
-
-                               if ( pi.helpurls.length ) {
-                                       buttons.push( new OO.ui.PopupButtonWidget( {
-                                               $overlay: true,
-                                               label: mw.message( 'apisandbox-helpurls' ).text(),
-                                               icon: 'help',
-                                               popup: {
-                                                       width: 'auto',
-                                                       padded: true,
-                                                       $content: $( '<ul>' ).append( pi.helpurls.map( function ( link ) {
-                                                               return $( '<li>' ).append( $( '<a>' )
-                                                                       .attr( { href: link, target: '_blank' } )
-                                                                       .text( link )
-                                                               );
-                                                       } ) )
-                                               }
-                                       } ) );
-                               }
-
-                               if ( pi.examples.length ) {
-                                       buttons.push( new OO.ui.PopupButtonWidget( {
-                                               $overlay: true,
-                                               label: mw.message( 'apisandbox-examples' ).text(),
-                                               icon: 'code',
-                                               popup: {
-                                                       width: 'auto',
-                                                       padded: true,
-                                                       $content: $( '<ul>' ).append( pi.examples.map( function ( example ) {
-                                                               var a = $( '<a>' )
-                                                                       .attr( 'href', '#' + example.query )
-                                                                       .html( example.description );
-                                                               a.find( 'a' ).contents().unwrap(); // Can't nest links
-                                                               return $( '<li>' ).append( a );
-                                                       } ) )
-                                               }
-                                       } ) );
-                               }
-
-                               if ( buttons.length ) {
-                                       items.push( new OO.ui.FieldLayout(
-                                               new OO.ui.ButtonGroupWidget( {
-                                                       items: buttons
-                                               } ), { align: 'top' }
-                                       ) );
-                               }
-
-                               if ( pi.parameters.length ) {
-                                       prefix = that.prefix + pi.prefix;
-                                       for ( i = 0; i < pi.parameters.length; i++ ) {
-                                               widget = Util.createWidgetForParameter( pi.parameters[ i ] );
-                                               that.widgets[ prefix + pi.parameters[ i ].name ] = widget;
-                                               if ( pi.parameters[ i ].tokentype ) {
-                                                       that.tokenWidget = widget;
-                                               }
-
-                                               descriptionContainer = $( '<div>' );
-
-                                               tmp = Util.parseHTML( pi.parameters[ i ].description );
-                                               tmp.filter( 'dl' ).makeCollapsible( {
-                                                       collapsed: true
-                                               } ).children( '.mw-collapsible-toggle' ).each( function () {
-                                                       var $this = $( this );
-                                                       $this.parent().prev( 'p' ).append( $this );
-                                               } );
-                                               descriptionContainer.append( $( '<div>' ).addClass( 'description' ).append( tmp ) );
-
-                                               if ( pi.parameters[ i ].info && pi.parameters[ i ].info.length ) {
-                                                       for ( j = 0; j < pi.parameters[ i ].info.length; j++ ) {
-                                                               descriptionContainer.append( $( '<div>' )
-                                                                       .addClass( 'info' )
-                                                                       .append( Util.parseHTML( pi.parameters[ i ].info[ j ] ) )
-                                                               );
-                                                       }
-                                               }
-                                               flag = true;
-                                               count = 1e100;
-                                               switch ( pi.parameters[ i ].type ) {
-                                                       case 'namespace':
-                                                               flag = false;
-                                                               count = mw.config.get( 'wgFormattedNamespaces' ).length;
-                                                               break;
-
-                                                       case 'limit':
-                                                               if ( pi.parameters[ i ].highmax !== undefined ) {
-                                                                       descriptionContainer.append( $( '<div>' )
-                                                                               .addClass( 'info' )
-                                                                               .append(
-                                                                                       Util.parseMsg(
-                                                                                               'api-help-param-limit2', pi.parameters[ i ].max, pi.parameters[ i ].highmax
-                                                                                       ),
-                                                                                       ' ',
-                                                                                       Util.parseMsg( 'apisandbox-param-limit' )
-                                                                               )
-                                                                       );
-                                                               } else {
-                                                                       descriptionContainer.append( $( '<div>' )
-                                                                               .addClass( 'info' )
-                                                                               .append(
-                                                                                       Util.parseMsg( 'api-help-param-limit', pi.parameters[ i ].max ),
-                                                                                       ' ',
-                                                                                       Util.parseMsg( 'apisandbox-param-limit' )
-                                                                               )
-                                                                       );
-                                                               }
-                                                               break;
-
-                                                       case 'integer':
-                                                               tmp = '';
-                                                               if ( pi.parameters[ i ].min !== undefined ) {
-                                                                       tmp += 'min';
-                                                               }
-                                                               if ( pi.parameters[ i ].max !== undefined ) {
-                                                                       tmp += 'max';
-                                                               }
-                                                               if ( tmp !== '' ) {
-                                                                       descriptionContainer.append( $( '<div>' )
-                                                                               .addClass( 'info' )
-                                                                               .append( Util.parseMsg(
-                                                                                       'api-help-param-integer-' + tmp,
-                                                                                       Util.apiBool( pi.parameters[ i ].multi ) ? 2 : 1,
-                                                                                       pi.parameters[ i ].min, pi.parameters[ i ].max
-                                                                               ) )
-                                                                       );
-                                                               }
-                                                               break;
-
-                                                       default:
-                                                               if ( Array.isArray( pi.parameters[ i ].type ) ) {
-                                                                       flag = false;
-                                                                       count = pi.parameters[ i ].type.length;
-                                                               }
-                                                               break;
-                                               }
-                                               if ( Util.apiBool( pi.parameters[ i ].multi ) ) {
-                                                       tmp = [];
-                                                       if ( flag && !( widget instanceof OO.ui.TagMultiselectWidget ) &&
-                                                               !(
-                                                                       widget instanceof OptionalWidget &&
-                                                                       widget.widget instanceof OO.ui.TagMultiselectWidget
-                                                               )
-                                                       ) {
-                                                               tmp.push( mw.message( 'api-help-param-multi-separate' ).parse() );
-                                                       }
-                                                       if ( count > pi.parameters[ i ].lowlimit ) {
-                                                               tmp.push(
-                                                                       mw.message( 'api-help-param-multi-max',
-                                                                               pi.parameters[ i ].lowlimit, pi.parameters[ i ].highlimit
-                                                                       ).parse()
-                                                               );
-                                                       }
-                                                       if ( tmp.length ) {
-                                                               descriptionContainer.append( $( '<div>' )
-                                                                       .addClass( 'info' )
-                                                                       .append( Util.parseHTML( tmp.join( ' ' ) ) )
-                                                               );
-                                                       }
-                                               }
-                                               if ( 'maxbytes' in pi.parameters[ i ] ) {
-                                                       descriptionContainer.append( $( '<div>' )
-                                                               .addClass( 'info' )
-                                                               .append( Util.parseMsg( 'api-help-param-maxbytes', pi.parameters[ i ].maxbytes ) )
-                                                       );
-                                               }
-                                               if ( 'maxchars' in pi.parameters[ i ] ) {
-                                                       descriptionContainer.append( $( '<div>' )
-                                                               .addClass( 'info' )
-                                                               .append( Util.parseMsg( 'api-help-param-maxchars', pi.parameters[ i ].maxchars ) )
-                                                       );
-                                               }
-                                               helpField = new OO.ui.FieldLayout(
-                                                       new OO.ui.Widget( {
-                                                               $content: '\xa0',
-                                                               classes: [ 'mw-apisandbox-spacer' ]
-                                                       } ), {
-                                                               align: 'inline',
-                                                               classes: [ 'mw-apisandbox-help-field' ],
-                                                               label: descriptionContainer
-                                                       }
-                                               );
-
-                                               layoutConfig = {
-                                                       align: 'left',
-                                                       classes: [ 'mw-apisandbox-widget-field' ],
-                                                       label: prefix + pi.parameters[ i ].name
-                                               };
-
-                                               if ( pi.parameters[ i ].tokentype ) {
-                                                       button = new OO.ui.ButtonWidget( {
-                                                               label: mw.message( 'apisandbox-fetch-token' ).text()
-                                                       } );
-                                                       button.on( 'click', widget.fetchToken, [], widget );
-
-                                                       widgetField = new OO.ui.ActionFieldLayout( widget, button, layoutConfig );
-                                               } else {
-                                                       widgetField = new OO.ui.FieldLayout( widget, layoutConfig );
-                                               }
-
-                                               // We need our own click handler on the widget label to
-                                               // turn off the disablement.
-                                               widgetField.$label.on( 'click', widgetLabelOnClick.bind( widgetField ) );
-
-                                               // Don't grey out the label when the field is disabled,
-                                               // it makes it too hard to read and our "disabled"
-                                               // isn't really disabled.
-                                               widgetField.onFieldDisable( false );
-                                               widgetField.onFieldDisable = $.noop;
-
-                                               if ( Util.apiBool( pi.parameters[ i ].deprecated ) ) {
-                                                       deprecatedItems.push( widgetField, helpField );
-                                               } else {
-                                                       items.push( widgetField, helpField );
-                                               }
-                                       }
-                               }
-
-                               if ( !pi.parameters.length && !Util.apiBool( pi.dynamicparameters ) ) {
-                                       items.push( new OO.ui.FieldLayout(
-                                               new OO.ui.Widget( {} ).toggle( false ), {
-                                                       align: 'top',
-                                                       label: Util.parseMsg( 'apisandbox-no-parameters' )
-                                               }
-                                       ) );
-                               }
-
-                               that.$element.empty();
-
-                               new OO.ui.FieldsetLayout( {
-                                       label: that.displayText
-                               } ).addItems( items )
-                                       .$element.appendTo( that.$element );
-
-                               if ( Util.apiBool( pi.dynamicparameters ) ) {
-                                       dynamicFieldset = new OO.ui.FieldsetLayout();
-                                       dynamicParamNameWidget = new OO.ui.TextInputWidget( {
-                                               placeholder: mw.message( 'apisandbox-dynamic-parameters-add-placeholder' ).text()
-                                       } ).on( 'enter', addDynamicParamWidget );
-                                       dynamicFieldset.addItems( [
-                                               new OO.ui.FieldLayout(
-                                                       new OO.ui.Widget( {} ).toggle( false ), {
-                                                               align: 'top',
-                                                               label: Util.parseHTML( pi.dynamicparameters )
-                                                       }
-                                               ),
-                                               new OO.ui.ActionFieldLayout(
-                                                       dynamicParamNameWidget,
-                                                       new OO.ui.ButtonWidget( {
-                                                               icon: 'add',
-                                                               flags: 'progressive'
-                                                       } ).on( 'click', addDynamicParamWidget ),
-                                                       {
-                                                               label: mw.message( 'apisandbox-dynamic-parameters-add-label' ).text(),
-                                                               align: 'left'
-                                                       }
-                                               )
-                                       ] );
-                                       $( '<fieldset>' )
-                                               .append(
-                                                       $( '<legend>' ).text( mw.message( 'apisandbox-dynamic-parameters' ).text() ),
-                                                       dynamicFieldset.$element
-                                               )
-                                               .appendTo( that.$element );
-                               }
-
-                               if ( deprecatedItems.length ) {
-                                       tmp = new OO.ui.FieldsetLayout().addItems( deprecatedItems ).toggle( false );
-                                       $( '<fieldset>' )
-                                               .append(
-                                                       $( '<legend>' ).append(
-                                                               new OO.ui.ToggleButtonWidget( {
-                                                                       label: mw.message( 'apisandbox-deprecated-parameters' ).text()
-                                                               } ).on( 'change', tmp.toggle, [], tmp ).$element
-                                                       ),
-                                                       tmp.$element
-                                               )
-                                               .appendTo( that.$element );
-                               }
-
-                               // Load stored params, if any, then update the booklet if we
-                               // have subpages (or else just update our valid-indicator).
-                               tmp = that.loadFromQueryParams;
-                               that.loadFromQueryParams = null;
-                               if ( $.isPlainObject( tmp ) ) {
-                                       that.loadQueryParams( tmp );
-                               }
-                               if ( that.getSubpages().length > 0 ) {
-                                       ApiSandbox.updateUI( tmp );
-                               } else {
-                                       that.apiCheckValid();
-                               }
-                       } ).fail( function ( code, detail ) {
-                               that.$element.empty()
-                                       .append(
-                                               new OO.ui.LabelWidget( {
-                                                       label: mw.message( 'apisandbox-load-error', that.apiModule, detail ).text(),
-                                                       classes: [ 'error' ]
-                                               } ).$element,
-                                               new OO.ui.ButtonWidget( {
-                                                       label: mw.message( 'apisandbox-retry' ).text()
-                                               } ).on( 'click', that.loadParamInfo, [], that ).$element
-                                       );
-                       } );
-       };
-
-       /**
-        * Check that all widgets on the page are in a valid state.
-        *
-        * @return {jQuery.Promise[]} One promise for each widget, resolved with `false` if invalid
-        */
-       ApiSandbox.PageLayout.prototype.apiCheckValid = function () {
-               var promises, that = this;
-
-               if ( this.paramInfo === null ) {
-                       return [];
-               } else {
-                       promises = $.map( this.widgets, function ( widget ) {
-                               return widget.apiCheckValid();
-                       } );
-                       $.when.apply( $, promises ).then( function () {
-                               that.apiIsValid = $.inArray( false, arguments ) === -1;
-                               if ( that.getOutlineItem() ) {
-                                       that.getOutlineItem().setIcon( that.apiIsValid || suppressErrors ? null : 'alert' );
-                                       that.getOutlineItem().setIconTitle(
-                                               that.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
-                                       );
-                               }
-                       } );
-                       return promises;
-               }
-       };
-
-       /**
-        * Load form fields from query parameters
-        *
-        * @param {Object} params
-        */
-       ApiSandbox.PageLayout.prototype.loadQueryParams = function ( params ) {
-               if ( this.paramInfo === null ) {
-                       this.loadFromQueryParams = params;
-               } else {
-                       $.each( this.widgets, function ( name, widget ) {
-                               var v = params.hasOwnProperty( name ) ? params[ name ] : undefined;
-                               widget.setApiValue( v );
-                       } );
-               }
-       };
-
-       /**
-        * Load query params from form fields
-        *
-        * @param {Object} params Write query parameters into this object
-        * @param {Object} displayParams Write query parameters for display into this object
-        */
-       ApiSandbox.PageLayout.prototype.getQueryParams = function ( params, displayParams ) {
-               $.each( this.widgets, function ( name, widget ) {
-                       var value = widget.getApiValue();
-                       if ( value !== undefined ) {
-                               params[ name ] = value;
-                               if ( $.isFunction( widget.getApiValueForDisplay ) ) {
-                                       value = widget.getApiValueForDisplay();
-                               }
-                               displayParams[ name ] = value;
-                       }
-               } );
-       };
-
-       /**
-        * Fetch a list of subpage names loaded by this page
-        *
-        * @return {Array}
-        */
-       ApiSandbox.PageLayout.prototype.getSubpages = function () {
-               var ret = [];
-               $.each( this.widgets, function ( name, widget ) {
-                       var submodules, i;
-                       if ( $.isFunction( widget.getSubmodules ) ) {
-                               submodules = widget.getSubmodules();
-                               for ( i = 0; i < submodules.length; i++ ) {
-                                       ret.push( {
-                                               key: name + '=' + submodules[ i ].value,
-                                               path: submodules[ i ].path,
-                                               prefix: widget.paramInfo.submoduleparamprefix || ''
-                                       } );
-                               }
-                       }
-               } );
-               return ret;
-       };
-
-       $( ApiSandbox.init );
-
-       module.exports = ApiSandbox;
-
-}( jQuery, mediaWiki, OO ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.apisandbox.top.css b/resources/src/mediawiki.special/mediawiki.special.apisandbox.top.css
deleted file mode 100644 (file)
index 4dc4c27..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-.client-js .mw-apisandbox-nojs {
-       display: none;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.block.js b/resources/src/mediawiki.special/mediawiki.special.block.js
deleted file mode 100644 (file)
index 180f040..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-/*!
- * JavaScript for Special:Block
- */
-( function ( mw, $ ) {
-       // Like OO.ui.infuse(), but if the element doesn't exist, return null instead of throwing an exception.
-       function infuseOrNull( elem ) {
-               try {
-                       return OO.ui.infuse( elem );
-               } catch ( er ) {
-                       return null;
-               }
-       }
-
-       $( function () {
-               // This code is also loaded on the "block succeeded" page where there is no form,
-               // so username and expiry fields might also be missing.
-               var blockTargetWidget = infuseOrNull( 'mw-bi-target' ),
-                       anonOnlyField = infuseOrNull( $( '#mw-input-wpHardBlock' ).closest( '.oo-ui-fieldLayout' ) ),
-                       enableAutoblockField = infuseOrNull( $( '#mw-input-wpAutoBlock' ).closest( '.oo-ui-fieldLayout' ) ),
-                       hideUserField = infuseOrNull( $( '#mw-input-wpHideUser' ).closest( '.oo-ui-fieldLayout' ) ),
-                       watchUserField = infuseOrNull( $( '#mw-input-wpWatch' ).closest( '.oo-ui-fieldLayout' ) ),
-                       expiryWidget = infuseOrNull( 'mw-input-wpExpiry' );
-
-               function updateBlockOptions() {
-                       var blocktarget = blockTargetWidget.getValue().trim(),
-                               isEmpty = blocktarget === '',
-                               isIp = mw.util.isIPAddress( blocktarget, true ),
-                               isIpRange = isIp && blocktarget.match( /\/\d+$/ ),
-                               isNonEmptyIp = isIp && !isEmpty,
-                               expiryValue = expiryWidget.getValue(),
-                               // infinityValues  are the values the SpecialBlock class accepts as infinity (sf. wfIsInfinity)
-                               infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ],
-                               isIndefinite = infinityValues.indexOf( expiryValue ) !== -1;
-
-                       if ( enableAutoblockField ) {
-                               enableAutoblockField.toggle( !( isNonEmptyIp ) );
-                       }
-                       if ( hideUserField ) {
-                               hideUserField.toggle( !( isNonEmptyIp || !isIndefinite ) );
-                       }
-                       if ( anonOnlyField ) {
-                               anonOnlyField.toggle( !( !isIp && !isEmpty ) );
-                       }
-                       if ( watchUserField ) {
-                               watchUserField.toggle( !( isIpRange && !isEmpty ) );
-                       }
-               }
-
-               if ( blockTargetWidget ) {
-                       // Bind functions so they're checked whenever stuff changes
-                       blockTargetWidget.on( 'change', updateBlockOptions );
-                       expiryWidget.on( 'change', updateBlockOptions );
-
-                       // Call them now to set initial state (ie. Special:Block/Foobar?wpBlockExpiry=2+hours)
-                       updateBlockOptions();
-               }
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.changecredentials.js b/resources/src/mediawiki.special/mediawiki.special.changecredentials.js
deleted file mode 100644 (file)
index ad8a4f4..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*!
- * JavaScript for change credentials form.
- */
-( function ( mw, $, OO ) {
-       mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
-               var api = new mw.Api();
-
-               $root.find( '.mw-changecredentials-validate-password.oo-ui-fieldLayout' ).each( function () {
-                       var currentApiPromise,
-                               self = OO.ui.FieldLayout.static.infuse( $( this ) );
-
-                       self.getField().setValidation( function ( password ) {
-                               var d;
-
-                               if ( currentApiPromise ) {
-                                       currentApiPromise.abort();
-                                       currentApiPromise = undefined;
-                               }
-
-                               password = password.trim();
-
-                               if ( password === '' ) {
-                                       self.setErrors( [] );
-                                       return true;
-                               }
-
-                               d = $.Deferred();
-                               currentApiPromise = api.post( {
-                                       action: 'validatepassword',
-                                       password: password,
-                                       formatversion: 2,
-                                       errorformat: 'html',
-                                       errorsuselocal: true,
-                                       uselang: mw.config.get( 'wgUserLanguage' )
-                               } ).done( function ( resp ) {
-                                       var pwinfo = resp.validatepassword,
-                                               good = pwinfo.validity === 'Good',
-                                               errors = [];
-
-                                       currentApiPromise = undefined;
-
-                                       if ( !good ) {
-                                               pwinfo.validitymessages.map( function ( m ) {
-                                                       errors.push( new OO.ui.HtmlSnippet( m.html ) );
-                                               } );
-                                       }
-                                       self.setErrors( errors );
-                                       d.resolve( good );
-                               } ).fail( d.reject );
-
-                               return d.promise( { abort: currentApiPromise.abort } );
-                       } );
-               } );
-       } );
-}( mediaWiki, jQuery, OO ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.css b/resources/src/mediawiki.special/mediawiki.special.changeslist.css
deleted file mode 100644 (file)
index 65860ea..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*!
- * Styling for Special:Watchlist and Special:RecentChanges
- */
-
-.mw-changeslist-line-watched .mw-title {
-       font-weight: bold;
-}
-
-/*
- * Titles, including username links, and also tag names
- * are prone to getting jumbled up
- * with other titles, usernames, etc. in mixed RTL-LTR environment.
- */
-.mw-changeslist .mw-tag-marker,
-.mw-changeslist .mw-title {
-       unicode-bidi: embed;
-}
-
-/* Colored watchlist and recent changes numbers */
-.mw-plusminus-pos {
-       color: #006400; /* dark green */
-}
-
-.mw-plusminus-neg {
-       color: #8b0000; /* dark red */
-}
-
-.mw-plusminus-null {
-       color: #a2a9b1; /* gray */
-}
-
-/*
- * Bidi-isolate these numbers.
- * See https://phabricator.wikimedia.org/T93484
- */
-.mw-plusminus-pos,
-.mw-plusminus-neg,
-.mw-plusminus-null {
-       unicode-bidi: -moz-isolate;
-       unicode-bidi: isolate;
-}
-
-/* Prevent FOUC if legend is initially collapsed */
-.mw-changeslist-legend.mw-collapsed .mw-collapsible-content {
-       display: none;
-}
-
-.mw-changeslist-legend.mw-collapsed {
-       margin-bottom: 0;
-}
-
-/* Prevent pushing down the content if legend is collapsed */
-.mw-changeslist-legend.mw-collapsed ~ ul:first-of-type > li:first-child,
-.mw-changeslist-legend.mw-collapsed + h4 + div > table.mw-changeslist-line:first-child {
-       clear: right;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css b/resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css
deleted file mode 100644 (file)
index cb11332..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*!
- * Styling for Special:Watchlist and Special:RecentChanges when preference 'usenewrc'
- * a.k.a. Enhanced Recent Changes is enabled.
- */
-
-table.mw-enhanced-rc {
-       border: 0;
-       border-spacing: 0;
-}
-
-table.mw-enhanced-rc th,
-table.mw-enhanced-rc td {
-       padding: 0;
-       vertical-align: top;
-}
-
-td.mw-enhanced-rc {
-       white-space: nowrap;
-       font-family: monospace, monospace;
-}
-
-.mw-enhanced-rc-time {
-       font-family: monospace, monospace;
-}
-
-table.mw-enhanced-rc td.mw-enhanced-rc-nested {
-       padding-left: 1em;
-}
-
-/* Show/hide arrows in enhanced changeslist */
-.mw-enhanced-rc .collapsible-expander {
-       float: none;
-}
-
-/* If JS is disabled, the arrows or the placeholder space shouldn't be shown */
-.client-nojs .mw-enhancedchanges-arrow-space {
-       display: none;
-}
-
-/*
- * And if it's enabled, let's optimize the collapsing a little: hide the rows
- * that would be hidden by jquery.makeCollapsible with CSS to save us some
- * reflows and repaints. This doesn't work on browsers that don't fully support
- * CSS2 (IE6), but it's okay, this will be done in JavaScript with old degraded
- * performance instead.
- */
-.client-js table.mw-enhanced-rc.mw-collapsed tr + tr {
-       display: none;
-}
-
-.mw-enhancedchanges-arrow {
-       padding-top: 2px;
-}
-
-.mw-enhancedchanges-arrow-space {
-       display: inline-block;
-       *display: inline; /* IE7 and below */
-       zoom: 1;
-       width: 15px;
-       height: 15px;
-}
-
-.mw-enhanced-watched .mw-enhanced-rc-time {
-       font-weight: bold;
-}
-
-span.changedby {
-       font-size: 95%;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css b/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css
deleted file mode 100644 (file)
index 14f6aee..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*!
- * Styling for changes list legend
- */
-
-.mw-changeslist-legend {
-       float: right;
-       margin-left: 1em;
-       margin-bottom: 0.5em;
-       clear: right;
-       font-size: 85%;
-       line-height: 1.2em;
-       padding: 0.5em;
-       border: 1px solid #ddd;
-}
-
-.mw-changeslist-legend dl {
-       /* Parent element defines sufficient padding */
-       margin-bottom: 0;
-}
-
-.mw-changeslist-legend dt {
-       float: left;
-       margin: 0 0.5em 0 0;
-}
-
-.mw-changeslist-legend dd {
-       margin-left: 1.5em;
-}
-
-.mw-changeslist-legend dt,
-.mw-changeslist-legend dd {
-       line-height: 1.3em;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js b/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js
deleted file mode 100644 (file)
index 0792762..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-/*!
- * Script for changes list legend
- */
-
-/* Remember the collapse state of the legend on recent changes and watchlist pages. */
-( function ( mw ) {
-       var
-               cookieName = 'changeslist-state',
-               // Expanded by default
-               doCollapsibleLegend = function ( $container ) {
-                       $container.find( '.mw-changeslist-legend' )
-                               .makeCollapsible( {
-                                       collapsed: mw.cookie.get( cookieName ) === 'collapsed'
-                               } )
-                               .on( 'beforeExpand.mw-collapsible', function () {
-                                       mw.cookie.set( cookieName, 'expanded' );
-                               } )
-                               .on( 'beforeCollapse.mw-collapsible', function () {
-                                       mw.cookie.set( cookieName, 'collapsed' );
-                               } );
-               };
-
-       mw.hook( 'wikipage.content' ).add( doCollapsibleLegend );
-}( mediaWiki ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.visitedstatus.js b/resources/src/mediawiki.special/mediawiki.special.changeslist.visitedstatus.js
deleted file mode 100644 (file)
index 6b25327..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-/*!
- * JavaScript for Special:Watchlist
- */
-( function ( $ ) {
-       $( function () {
-               $( '.mw-changeslist-line-watched .mw-title a' ).on( 'click', function () {
-                       $( this )
-                               .closest( '.mw-changeslist-line-watched' )
-                               .removeClass( 'mw-changeslist-line-watched' );
-               } );
-       } );
-}( jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.comparepages.styles.less b/resources/src/mediawiki.special/mediawiki.special.comparepages.styles.less
deleted file mode 100644 (file)
index 87b7a8b..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-@import 'mediawiki.mixins';
-
-.mw-special-ComparePages .mw-htmlform-ooui-wrapper {
-       width: 100%;
-}
-
-.mw-special-ComparePages .oo-ui-layout.oo-ui-panelLayout.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed {
-       float: left;
-       width: 49%;
-       .box-sizing( border-box );
-}
-
-.mw-special-ComparePages .oo-ui-layout.oo-ui-panelLayout.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed:nth-of-type( 2 ) {
-       margin-left: 2%;
-}
-
-.mw-special-ComparePages .mw-htmlform-submit-buttons {
-       clear: both;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.contributions.js b/resources/src/mediawiki.special/mediawiki.special.contributions.js
deleted file mode 100644 (file)
index f65a257..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-( function ( mw, $ ) {
-       $( function () {
-               var startInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-start' ),
-                       endInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-end' );
-
-               startInput.on( 'deactivate', function ( userSelected ) {
-                       if ( userSelected ) {
-                               endInput.focus();
-                       }
-               } );
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.edittags.css b/resources/src/mediawiki.special/mediawiki.special.edittags.css
deleted file mode 100644 (file)
index 204009c..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-/*!
- * Styling for Special:EditTags and action=editchangetags
- */
-#mw-edittags-tags-selector td {
-       vertical-align: top;
-}
-
-#mw-edittags-tags-selector-multi td {
-       vertical-align: top;
-       padding-right: 1.5em;
-}
-
-#mw-edittags-tag-list {
-       min-width: 20em;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.edittags.js b/resources/src/mediawiki.special/mediawiki.special.edittags.js
deleted file mode 100644 (file)
index 4f51e9b..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*!
- * JavaScript for Special:EditTags
- */
-( function ( mw, $ ) {
-       $( function () {
-               var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
-                       summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
-                       $wpReason = $( '#wpReason' ),
-                       $tagList = $( '#mw-edittags-tag-list' );
-
-               if ( $tagList.length ) {
-                       $tagList.chosen( {
-                               /* eslint-disable camelcase */
-                               placeholder_text_multiple: mw.msg( 'tags-edit-chosen-placeholder' ),
-                               no_results_text: mw.msg( 'tags-edit-chosen-no-results' )
-                               /* eslint-enable camelcase */
-                       } );
-               }
-
-               $( '#mw-edittags-remove-all' ).on( 'change', function ( e ) {
-                       $( '.mw-edittags-remove-checkbox' ).prop( 'checked', e.target.checked );
-               } );
-               $( '.mw-edittags-remove-checkbox' ).on( 'change', function ( e ) {
-                       if ( !e.target.checked ) {
-                               $( '#mw-edittags-remove-all' ).prop( 'checked', false );
-                       }
-               } );
-
-               // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
-               // use maxLength because it's leaving room for log entry text.
-               if ( summaryCodePointLimit ) {
-                       $wpReason.codePointLimit();
-               } else if ( summaryByteLimit ) {
-                       $wpReason.byteLimit();
-               }
-       } );
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.import.js b/resources/src/mediawiki.special/mediawiki.special.import.js
deleted file mode 100644 (file)
index 2cb96af..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*!
- * JavaScript for Special:Import
- */
-( function ( $ ) {
-       var subprojectListAlreadyShown;
-       function updateImportSubprojectList() {
-               var $projectField = $( '#mw-import-table-interwiki #interwiki' ),
-                       $subprojectField = $projectField.parent().find( '#subproject' ),
-                       $selected = $projectField.find( ':selected' ),
-                       oldValue = $subprojectField.val(),
-                       option, options;
-
-               if ( $selected.attr( 'data-subprojects' ) ) {
-                       options = $selected.attr( 'data-subprojects' ).split( ' ' ).map( function ( el ) {
-                               option = document.createElement( 'option' );
-                               option.appendChild( document.createTextNode( el ) );
-                               option.setAttribute( 'value', el );
-                               if ( oldValue === el && subprojectListAlreadyShown === true ) {
-                                       option.setAttribute( 'selected', 'selected' );
-                               }
-                               return option;
-                       } );
-                       $subprojectField.show().empty().append( options );
-                       subprojectListAlreadyShown = true;
-               } else {
-                       $subprojectField.hide();
-               }
-       }
-
-       $( function () {
-               var $projectField = $( '#mw-import-table-interwiki #interwiki' );
-               if ( $projectField.length ) {
-                       $projectField.change( updateImportSubprojectList );
-                       updateImportSubprojectList();
-               }
-       } );
-}( jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.movePage.css b/resources/src/mediawiki.special/mediawiki.special.movePage.css
deleted file mode 100644 (file)
index 9428fed..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-/*!
- * Styles for Special:MovePage
- */
-
-.movepage-wrapper {
-       width: 50em;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.movePage.js b/resources/src/mediawiki.special/mediawiki.special.movePage.js
deleted file mode 100644 (file)
index d828396..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/*!
- * JavaScript for Special:MovePage
- */
-( function ( mw, $ ) {
-       $( function () {
-               var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
-                       summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
-                       wpReason = OO.ui.infuse( $( '#wpReason' ) );
-
-               // Infuse for pretty dropdown
-               OO.ui.infuse( $( '#wpNewTitle' ) );
-               // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
-               if ( summaryCodePointLimit ) {
-                       mw.widgets.visibleCodePointLimit( wpReason, summaryCodePointLimit );
-               } else if ( summaryByteLimit ) {
-                       mw.widgets.visibleByteLimit( wpReason, summaryByteLimit );
-               }
-               // Infuse for nicer "help" popup
-               if ( $( '#wpMovetalk-field' ).length ) {
-                       OO.ui.infuse( $( '#wpMovetalk-field' ) );
-               }
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.pageLanguage.js b/resources/src/mediawiki.special/mediawiki.special.pageLanguage.js
deleted file mode 100644 (file)
index edfbe1e..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-/*!
- * JavaScript module used on Special:PageLanguage
- */
-( function ( $, OO ) {
-       $( function () {
-               // Select the 'Language select' option if user is trying to select language
-               OO.ui.infuse( 'mw-pl-languageselector' ).on( 'change', function () {
-                       OO.ui.infuse( 'mw-pl-options' ).setValue( '2' );
-               } );
-       } );
-}( jQuery, OO ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css b/resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css
deleted file mode 100644 (file)
index 7ef75d0..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-/* Distinguish actual data from information about it being hidden visually */
-.prop-value-hidden {
-       font-style: italic;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js b/resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js
deleted file mode 100644 (file)
index 244154b..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: Enable save button and prevent the window being accidentally
- * closed when any form field is changed.
- */
-( function ( mw, $ ) {
-       $( function () {
-               var allowCloseWindow, saveButton, restoreButton,
-                       oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
-
-               // Check if all of the form values are unchanged.
-               // (This function could be changed to infuse and check OOUI widgets, but that would only make it
-               // slower and more complicated. It works fine to treat them as HTML elements.)
-               function isPrefsChanged() {
-                       var inputs = $( '#mw-prefs-form :input[name]' ),
-                               input, $input, inputType,
-                               index, optIndex,
-                               opt;
-
-                       for ( index = 0; index < inputs.length; index++ ) {
-                               input = inputs[ index ];
-                               $input = $( input );
-
-                               // Different types of inputs have different methods for accessing defaults
-                               if ( $input.is( 'select' ) ) {
-                                       // <select> has the property defaultSelected for each option
-                                       for ( optIndex = 0; optIndex < input.options.length; optIndex++ ) {
-                                               opt = input.options[ optIndex ];
-                                               if ( opt.selected !== opt.defaultSelected ) {
-                                                       return true;
-                                               }
-                                       }
-                               } else if ( $input.is( 'input' ) || $input.is( 'textarea' ) ) {
-                                       // <input> has defaultValue or defaultChecked
-                                       inputType = input.type;
-                                       if ( inputType === 'radio' || inputType === 'checkbox' ) {
-                                               if ( input.checked !== input.defaultChecked ) {
-                                                       return true;
-                                               }
-                                       } else if ( input.value !== input.defaultValue ) {
-                                               return true;
-                                       }
-                               }
-                       }
-
-                       return false;
-               }
-
-               if ( oouiEnabled ) {
-                       saveButton = OO.ui.infuse( $( '#prefcontrol' ) );
-                       restoreButton = OO.ui.infuse( $( '#mw-prefs-restoreprefs' ) );
-
-                       // Disable the button to save preferences unless preferences have changed
-                       // Check if preferences have been changed before JS has finished loading
-                       saveButton.setDisabled( !isPrefsChanged() );
-                       $( '#preferences .oo-ui-fieldsetLayout' ).on( 'change keyup mouseup', function () {
-                               saveButton.setDisabled( !isPrefsChanged() );
-                       } );
-               } else {
-                       // Disable the button to save preferences unless preferences have changed
-                       // Check if preferences have been changed before JS has finished loading
-                       $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
-                       $( '#preferences > fieldset' ).on( 'change keyup mouseup', function () {
-                               $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
-                       } );
-               }
-
-               // Set up a message to notify users if they try to leave the page without
-               // saving.
-               allowCloseWindow = mw.confirmCloseWindow( {
-                       test: isPrefsChanged,
-                       message: mw.msg( 'prefswarning-warning', mw.msg( 'saveprefs' ) ),
-                       namespace: 'prefswarning'
-               } );
-               $( '#mw-prefs-form' ).on( 'submit', $.proxy( allowCloseWindow, 'release' ) );
-               if ( oouiEnabled ) {
-                       restoreButton.on( 'click', function () {
-                               allowCloseWindow.release();
-                               // The default behavior of events in OOUI is always prevented. Follow the link manually.
-                               // Note that middle-click etc. still works, as it doesn't emit a OOUI 'click' event.
-                               location.href = restoreButton.getHref();
-                       } );
-               } else {
-                       $( '#mw-prefs-restoreprefs' ).on( 'click', $.proxy( allowCloseWindow, 'release' ) );
-               }
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js b/resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js
deleted file mode 100644 (file)
index e6b7432..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: Check for successbox to replace with notifications.
- */
-( function ( $ ) {
-       $( function () {
-               var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' );
-               convertmessagebox();
-       } );
-}( jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js b/resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js
deleted file mode 100644 (file)
index fe48886..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: editfont field enhancements.
- */
-( function ( mw, $ ) {
-       $( function () {
-               var widget, lastValue;
-
-               try {
-                       widget = OO.ui.infuse( $( '#mw-input-wpeditfont' ) );
-               } catch ( err ) {
-                       // This preference could theoretically be disabled ($wgHiddenPrefs)
-                       return;
-               }
-
-               // Style options
-               widget.dropdownWidget.menu.items.forEach( function ( item ) {
-                       item.$label.addClass( 'mw-editfont-' + item.getData() );
-               } );
-
-               function updateLabel( value ) {
-                       // Style selected item label
-                       widget.dropdownWidget.$label
-                               .removeClass( 'mw-editfont-' + lastValue )
-                               .addClass( 'mw-editfont-' + value );
-                       lastValue = value;
-               }
-
-               widget.on( 'change', updateLabel );
-               updateLabel( widget.getValue() );
-
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js b/resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js
deleted file mode 100644 (file)
index f934d59..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: Email preferences better UX
- */
-( function ( $ ) {
-       $( function () {
-               var allowEmail, allowEmailFromNewUsers;
-
-               allowEmail = $( '#wpAllowEmail' );
-               allowEmailFromNewUsers = $( '#wpAllowEmailFromNewUsers' );
-
-               function toggleDisabled() {
-                       if ( allowEmail.is( ':checked' ) && allowEmail.is( ':enabled' ) ) {
-                               allowEmailFromNewUsers.prop( 'disabled', false );
-                       } else {
-                               allowEmailFromNewUsers.prop( 'disabled', true );
-                       }
-               }
-
-               if ( allowEmail ) {
-                       allowEmail.on( 'change', toggleDisabled );
-                       toggleDisabled();
-               }
-       } );
-}( jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.styles.css b/resources/src/mediawiki.special/mediawiki.special.preferences.styles.css
deleted file mode 100644 (file)
index 8810318..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-/* Reuses colors from mediawiki.legacy/shared.css */
-.mw-email-not-authenticated .oo-ui-labelWidget,
-.mw-email-none .oo-ui-labelWidget {
-       border: 1px solid #fde29b;
-       background-color: #fdf1d1;
-       color: #000;
-       padding: 0.5em;
-}
-/* Authenticated email field has its own class too. Unstyled by default */
-/*
-.mw-email-authenticated .oo-ui-labelWidget { }
-*/
-
-/* This is needed because add extra buttons in a weird way */
-.mw-prefs-buttons .mw-htmlform-submit-buttons {
-       margin: 0;
-       display: inline;
-}
-
-.mw-prefs-buttons {
-       margin-top: 1em;
-}
-
-#prefcontrol {
-       margin-right: 0.5em;
-}
-
-/*
- * Hide, but keep accessible for screen-readers.
- * Like .mw-jump, #jump-to-nav from shared.css
- */
-.client-js .mw-navigation-hint {
-       overflow: hidden;
-       height: 0;
-       zoom: 1;
-}
-
-/* Override OOUI styles so that dropdowns near the bottom of the form don't get clipped,
- * e.g.'Appearance' / 'Threshold for stub link formatting'. This is hacky and bad, it would be
- * better solved by setting overlays for the widgets, but we can't do it from PHP... */
-#preferences .oo-ui-panelLayout {
-       position: static;
-       overflow: visible;
-       -webkit-transform: none;
-       transform: none;
-}
-
-#preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
-       border-color: #c8ccd1;
-       border-width: 1px 0 0;
-       border-radius: 0;
-       padding-left: 0;
-       padding-right: 0;
-       box-shadow: none;
-}
-
-/* Tweak the margins to reduce the shifting of form contents
- * after JS code loads and rearranges the page */
-.client-js #preferences > .oo-ui-panelLayout {
-       margin: 1em 0;
-}
-
-.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
-       margin-left: 0.25em;
-}
-
-.client-js #preferences .oo-ui-tabPanelLayout {
-       padding-top: 0.5em;
-       padding-bottom: 0.5em;
-}
-
-.client-js #preferences .oo-ui-tabPanelLayout .oo-ui-panelLayout-framed {
-       margin-left: 0;
-       margin-bottom: 0;
-       border: 0;
-       padding-top: 0;
-}
-
-.client-js #preferences > .oo-ui-panelLayout > .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header {
-       margin-bottom: 1em;
-}
-
-/* Make the "Basic information" section more compact */
-/* OOUI's `align: 'left'` for FieldLayouts sucks, so we do our own */
-#mw-htmlform-info > .oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header {
-       width: 20%;
-       display: inline-block;
-       vertical-align: middle;
-       padding: 0;
-}
-
-#mw-htmlform-info > .oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-help {
-       margin-right: 0;
-}
-
-#mw-htmlform-info > .oo-ui-fieldLayout.oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
-       width: 80%;
-       display: inline-block;
-       vertical-align: middle;
-}
-
-/* Expand the dropdown and textfield of "Time zone" field to the */
-/* usual maximum width and display them on separate lines. */
-#wpTimeCorrection .oo-ui-dropdownInputWidget,
-#wpTimeCorrection .oo-ui-textInputWidget {
-       display: block;
-       max-width: 50em;
-}
-
-#wpTimeCorrection .oo-ui-textInputWidget {
-       margin-top: 0.5em;
-}
-
-/* HACK: expand width of gadget descriptions.
- * This should be moved to the Gadgets extension */
-#mw-htmlform-gadgets .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body {
-       max-width: none;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css b/resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css
deleted file mode 100644 (file)
index 33b630a..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/* Reuses colors from mediawiki.legacy/shared.css */
-.mw-email-not-authenticated .mw-input,
-.mw-email-none .mw-input {
-       border: 1px solid #fde29b;
-       background-color: #fdf1d1;
-       color: #000;
-}
-/* Authenticated email field has its own class too. Unstyled by default */
-/*
-.mw-email-authenticated .mw-input { }
-*/
-/* This breaks due to nolabel styling */
-#preferences > fieldset td.mw-label {
-       width: 20%;
-}
-
-#preferences > fieldset table {
-       width: 100%;
-}
-#preferences > fieldset table.mw-htmlform-matrix {
-       width: auto;
-}
-
-/* The CSS below is also for JS enabled version, because we want to prevent FOUC */
-
-/*
- * Hide, but keep accessible for screen-readers.
- * Like .mw-jump, #jump-to-nav from shared.css
- */
-.client-js .mw-navigation-hint {
-       overflow: hidden;
-       height: 0;
-       zoom: 1;
-}
-
-.client-nojs #preftoc {
-       display: none;
-}
-
-.client-js #preferences > fieldset {
-       display: none;
-}
-
-/* Only the 1st tab is shown by default in JS mode */
-.client-js #preferences #mw-prefsection-personal {
-       display: block;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js b/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js
deleted file mode 100644 (file)
index c948ff0..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: Tab navigation.
- */
-( function ( mw, $ ) {
-       $( function () {
-               var $preferences, tabs, wrapper, previousTab;
-
-               $preferences = $( '#preferences' );
-
-               // Make sure the accessibility tip is selectable so that screen reader users take notice,
-               // but hide it per default to reduce interface clutter. Also make sure it becomes visible
-               // when selected. Similar to jquery.mw-jump
-               $( '<div>' ).addClass( 'mw-navigation-hint' )
-                       .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
-                       .attr( 'tabIndex', 0 )
-                       .on( 'focus blur', function ( e ) {
-                               if ( e.type === 'blur' || e.type === 'focusout' ) {
-                                       $( this ).css( 'height', '0' );
-                               } else {
-                                       $( this ).css( 'height', 'auto' );
-                               }
-                       } ).prependTo( '#mw-content-text' );
-
-               tabs = new OO.ui.IndexLayout( {
-                       expanded: false,
-                       // Do not remove focus from the tabs menu after choosing a tab
-                       autoFocus: false
-               } );
-
-               mw.config.get( 'wgPreferencesTabs' ).forEach( function ( tabConfig ) {
-                       var panel, $panelContents;
-
-                       panel = new OO.ui.TabPanelLayout( tabConfig.name, {
-                               expanded: false,
-                               label: tabConfig.label
-                       } );
-                       $panelContents = $( '#mw-prefsection-' + tabConfig.name );
-
-                       // Hide the unnecessary PHP PanelLayouts
-                       // (Do not use .remove(), as that would remove event handlers for everything inside them)
-                       $panelContents.parent().detach();
-
-                       panel.$element.append( $panelContents );
-                       tabs.addTabPanels( [ panel ] );
-
-                       // Remove duplicate labels
-                       // (This must be after .addTabPanels(), otherwise the tab item doesn't exist yet)
-                       $panelContents.children( 'legend' ).remove();
-                       $panelContents.attr( 'aria-labelledby', panel.getTabItem().getElementId() );
-               } );
-
-               wrapper = new OO.ui.PanelLayout( {
-                       expanded: false,
-                       padded: false,
-                       framed: true
-               } );
-               wrapper.$element.append( tabs.$element );
-               $preferences.prepend( wrapper.$element );
-
-               function updateHash( panel ) {
-                       var scrollTop, active;
-                       // Handle hash manually to prevent jumping,
-                       // therefore save and restore scrollTop to prevent jumping.
-                       scrollTop = $( window ).scrollTop();
-                       // Changing the hash apparently causes keyboard focus to be lost?
-                       // Save and restore it. This makes no sense though.
-                       active = document.activeElement;
-                       location.hash = '#mw-prefsection-' + panel.getName();
-                       if ( active ) {
-                               active.focus();
-                       }
-                       $( window ).scrollTop( scrollTop );
-               }
-
-               tabs.on( 'set', updateHash );
-
-               /**
-                * @ignore
-                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
-                * @param {string} [mode] A hash will be set according to the current
-                *  open section. Set mode 'noHash' to supress this.
-                */
-               function switchPrefTab( name, mode ) {
-                       if ( mode === 'noHash' ) {
-                               tabs.off( 'set', updateHash );
-                       }
-                       tabs.setTabPanel( name );
-                       if ( mode === 'noHash' ) {
-                               tabs.on( 'set', updateHash );
-                       }
-               }
-
-               // Jump to correct section as indicated by the hash.
-               // This function is called onload and onhashchange.
-               function detectHash() {
-                       var hash = location.hash,
-                               matchedElement, parentSection;
-                       if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
-                               mw.storage.session.remove( 'mwpreferences-prevTab' );
-                               switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
-                       } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
-                               matchedElement = document.getElementById( hash.slice( 1 ) );
-                               parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
-                               if ( parentSection.length ) {
-                                       mw.storage.session.remove( 'mwpreferences-prevTab' );
-                                       // Switch to proper tab and scroll to selected item.
-                                       switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
-                                       matchedElement.scrollIntoView();
-                               }
-                       }
-               }
-
-               $( window ).on( 'hashchange', function () {
-                       var hash = location.hash;
-                       if ( hash.match( /^#mw-[\w-]+/ ) ) {
-                               detectHash();
-                       } else if ( hash === '' ) {
-                               switchPrefTab( 'personal', 'noHash' );
-                       }
-               } )
-                       // Run the function immediately to select the proper tab on startup.
-                       .trigger( 'hashchange' );
-
-               // Restore the active tab after saving the preferences
-               previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
-               if ( previousTab ) {
-                       switchPrefTab( previousTab, 'noHash' );
-                       // Deleting the key, the tab states should be reset until we press Save
-                       mw.storage.session.remove( 'mwpreferences-prevTab' );
-               }
-
-               $( '#mw-prefs-form' ).on( 'submit', function () {
-                       var value = tabs.getCurrentTabPanelName();
-                       mw.storage.session.set( 'mwpreferences-prevTab', value );
-               } );
-
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js b/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js
deleted file mode 100644 (file)
index 0d97d68..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: Tab navigation.
- */
-( function ( mw, $ ) {
-       $( function () {
-               var $preftoc, $preferences, $fieldsets, labelFunc, previousTab;
-
-               labelFunc = function () {
-                       return this.id.replace( /^mw-prefsection/g, 'preftab' );
-               };
-
-               $preftoc = $( '#preftoc' );
-               $preferences = $( '#preferences' );
-
-               $fieldsets = $preferences.children( 'fieldset' )
-                       .attr( {
-                               role: 'tabpanel',
-                               'aria-labelledby': labelFunc
-                       } );
-               $fieldsets.not( '#mw-prefsection-personal' )
-                       .hide()
-                       .attr( 'aria-hidden', 'true' );
-
-               // T115692: The following is kept for backwards compatibility with older skins
-               $preferences.addClass( 'jsprefs' );
-               $fieldsets.addClass( 'prefsection' );
-               $fieldsets.children( 'legend' ).addClass( 'mainLegend' );
-
-               // Make sure the accessibility tip is selectable so that screen reader users take notice,
-               // but hide it per default to reduce interface clutter. Also make sure it becomes visible
-               // when selected. Similar to jquery.mw-jump
-               $( '<div>' ).addClass( 'mw-navigation-hint' )
-                       .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
-                       .attr( 'tabIndex', 0 )
-                       .on( 'focus blur', function ( e ) {
-                               if ( e.type === 'blur' || e.type === 'focusout' ) {
-                                       $( this ).css( 'height', '0' );
-                               } else {
-                                       $( this ).css( 'height', 'auto' );
-                               }
-                       } ).insertBefore( $preftoc );
-
-               /**
-                * It uses document.getElementById for security reasons (HTML injections in $()).
-                *
-                * @ignore
-                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
-                * @param {string} [mode] A hash will be set according to the current
-                *  open section. Set mode 'noHash' to surpress this.
-                */
-               function switchPrefTab( name, mode ) {
-                       var $tab, scrollTop;
-                       // Handle hash manually to prevent jumping,
-                       // therefore save and restore scrollTop to prevent jumping.
-                       scrollTop = $( window ).scrollTop();
-                       if ( mode !== 'noHash' ) {
-                               location.hash = '#mw-prefsection-' + name;
-                       }
-                       $( window ).scrollTop( scrollTop );
-
-                       $preftoc.find( 'li' ).removeClass( 'selected' )
-                               .find( 'a' ).attr( {
-                                       tabIndex: -1,
-                                       'aria-selected': 'false'
-                               } );
-
-                       $tab = $( document.getElementById( 'preftab-' + name ) );
-                       if ( $tab.length ) {
-                               $tab.attr( {
-                                       tabIndex: 0,
-                                       'aria-selected': 'true'
-                               } ).focus()
-                                       .parent().addClass( 'selected' );
-
-                               $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
-                               $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' );
-                       }
-               }
-
-               // Enable keyboard users to use left and right keys to switch tabs
-               $preftoc.on( 'keydown', function ( event ) {
-                       var keyLeft = 37,
-                               keyRight = 39,
-                               $el;
-
-                       if ( event.keyCode === keyLeft ) {
-                               $el = $( '#preftoc li.selected' ).prev().find( 'a' );
-                       } else if ( event.keyCode === keyRight ) {
-                               $el = $( '#preftoc li.selected' ).next().find( 'a' );
-                       } else {
-                               return;
-                       }
-                       if ( $el.length > 0 ) {
-                               switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) );
-                       }
-               } );
-
-               // Jump to correct section as indicated by the hash.
-               // This function is called onload and onhashchange.
-               function detectHash() {
-                       var hash = location.hash,
-                               matchedElement, parentSection;
-                       if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
-                               mw.storage.session.remove( 'mwpreferences-prevTab' );
-                               switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
-                       } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
-                               matchedElement = document.getElementById( hash.slice( 1 ) );
-                               parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
-                               if ( parentSection.length ) {
-                                       mw.storage.session.remove( 'mwpreferences-prevTab' );
-                                       // Switch to proper tab and scroll to selected item.
-                                       switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
-                                       matchedElement.scrollIntoView();
-                               }
-                       }
-               }
-
-               $( window ).on( 'hashchange', function () {
-                       var hash = location.hash;
-                       if ( hash.match( /^#mw-[\w-]+/ ) ) {
-                               detectHash();
-                       } else if ( hash === '' ) {
-                               switchPrefTab( 'personal', 'noHash' );
-                       }
-               } )
-                       // Run the function immediately to select the proper tab on startup.
-                       .trigger( 'hashchange' );
-
-               // Restore the active tab after saving the preferences
-               previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
-               if ( previousTab ) {
-                       switchPrefTab( previousTab, 'noHash' );
-                       // Deleting the key, the tab states should be reset until we press Save
-                       mw.storage.session.remove( 'mwpreferences-prevTab' );
-               }
-
-               $( '#mw-prefs-form' ).on( 'submit', function () {
-                       var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
-                       mw.storage.session.set( 'mwpreferences-prevTab', value );
-               } );
-
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js b/resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js
deleted file mode 100644 (file)
index a6ffae9..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: Timezone field enhancements.
- */
-( function ( mw, $ ) {
-       $( function () {
-               var $tzSelect, $tzTextbox, timezoneWidget, $localtimeHolder, servertime,
-                       oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
-
-               // Timezone functions.
-               // Guesses Timezone from browser and updates fields onchange.
-
-               if ( oouiEnabled ) {
-                       // This is identical to OO.ui.infuse( ... ), but it makes the class name of the result known.
-                       try {
-                               timezoneWidget = mw.widgets.SelectWithInputWidget.static.infuse( $( '#wpTimeCorrection' ) );
-                       } catch ( err ) {
-                               // This preference could theoretically be disabled ($wgHiddenPrefs)
-                               timezoneWidget = null;
-                       }
-               } else {
-                       $tzSelect = $( '#mw-input-wptimecorrection' );
-                       $tzTextbox = $( '#mw-input-wptimecorrection-other' );
-               }
-
-               $localtimeHolder = $( '#wpLocalTime' );
-               servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 );
-
-               function minutesToHours( min ) {
-                       var tzHour = Math.floor( Math.abs( min ) / 60 ),
-                               tzMin = Math.abs( min ) % 60,
-                               tzString = ( ( min >= 0 ) ? '' : '-' ) + ( ( tzHour < 10 ) ? '0' : '' ) + tzHour +
-                                       ':' + ( ( tzMin < 10 ) ? '0' : '' ) + tzMin;
-                       return tzString;
-               }
-
-               function hoursToMinutes( hour ) {
-                       var minutes,
-                               arr = hour.split( ':' );
-
-                       arr[ 0 ] = parseInt( arr[ 0 ], 10 );
-
-                       if ( arr.length === 1 ) {
-                               // Specification is of the form [-]XX
-                               minutes = arr[ 0 ] * 60;
-                       } else {
-                               // Specification is of the form [-]XX:XX
-                               minutes = Math.abs( arr[ 0 ] ) * 60 + parseInt( arr[ 1 ], 10 );
-                               if ( arr[ 0 ] < 0 ) {
-                                       minutes *= -1;
-                               }
-                       }
-                       // Gracefully handle non-numbers.
-                       if ( isNaN( minutes ) ) {
-                               return 0;
-                       } else {
-                               return minutes;
-                       }
-               }
-
-               function updateTimezoneSelection() {
-                       var minuteDiff, localTime,
-                               type = oouiEnabled ? timezoneWidget.dropdowninput.getValue() : $tzSelect.val(),
-                               val = oouiEnabled ? timezoneWidget.textinput.getValue() : $tzTextbox.val();
-
-                       if ( type === 'other' ) {
-                               // User specified time zone manually in <input>
-                               // Grab data from the textbox, parse it.
-                               minuteDiff = hoursToMinutes( val );
-                       } else {
-                               // Time zone not manually specified by user
-                               if ( type === 'guess' ) {
-                                       // Get browser timezone & fill it in
-                                       minuteDiff = -( new Date().getTimezoneOffset() );
-                                       if ( oouiEnabled ) {
-                                               timezoneWidget.textinput.setValue( minutesToHours( minuteDiff ) );
-                                               timezoneWidget.dropdowninput.setValue( 'other' );
-                                       } else {
-                                               $tzTextbox.val( minutesToHours( minuteDiff ) );
-                                               $tzSelect.val( 'other' );
-                                       }
-                               } else {
-                                       // Grab data from the dropdown value
-                                       minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;
-                               }
-                       }
-
-                       // Determine local time from server time and minutes difference, for display.
-                       localTime = servertime + minuteDiff;
-
-                       // Bring time within the [0,1440) range.
-                       localTime = ( ( localTime % 1440 ) + 1440 ) % 1440;
-
-                       $localtimeHolder.text( mw.language.convertNumber( minutesToHours( localTime ) ) );
-               }
-
-               if ( oouiEnabled ) {
-                       if ( timezoneWidget ) {
-                               timezoneWidget.dropdowninput.on( 'change', updateTimezoneSelection );
-                               timezoneWidget.textinput.on( 'change', updateTimezoneSelection );
-                               updateTimezoneSelection();
-                       }
-               } else {
-                       if ( $tzSelect.length && $tzTextbox.length ) {
-                               $tzSelect.change( updateTimezoneSelection );
-                               $tzTextbox.blur( updateTimezoneSelection );
-                               updateTimezoneSelection();
-                       }
-               }
-
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.recentchanges.js b/resources/src/mediawiki.special/mediawiki.special.recentchanges.js
deleted file mode 100644 (file)
index 29c0fea..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*!
- * JavaScript for Special:RecentChanges
- */
-( function ( mw, $ ) {
-       var rc, $checkboxes, $select;
-
-       /**
-        * @class mw.special.recentchanges
-        * @singleton
-        */
-       rc = {
-               /**
-                * Handler to disable/enable the namespace selector checkboxes when the
-                * special 'all' namespace is selected/unselected respectively.
-                */
-               updateCheckboxes: function () {
-                       // The option element for the 'all' namespace has an empty value
-                       var isAllNS = $select.val() === '';
-
-                       // Iterates over checkboxes and propagate the selected option
-                       $checkboxes.prop( 'disabled', isAllNS );
-               },
-
-               init: function () {
-                       $select = $( '#namespace' );
-                       $checkboxes = $( '#nsassociated, #nsinvert' );
-
-                       // Bind to change event, and trigger once to set the initial state of the checkboxes.
-                       rc.updateCheckboxes();
-                       $select.change( rc.updateCheckboxes );
-               }
-       };
-
-       $( rc.init );
-
-       module.exports = rc;
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.revisionDelete.js b/resources/src/mediawiki.special/mediawiki.special.revisionDelete.js
deleted file mode 100644 (file)
index cad9db0..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/*!
- * JavaScript for Special:RevisionDelete
- */
-( function ( mw, $ ) {
-       var colonSeparator = mw.message( 'colon-separator' ).text(),
-               summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
-               summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
-               $wpRevDeleteReasonList = $( '#wpRevDeleteReasonList' ),
-               $wpReason = $( '#wpReason' ),
-               filterFn = function ( input ) {
-                       // Should be built the same as in SpecialRevisionDelete::submit()
-                       var comment = $wpRevDeleteReasonList.val();
-                       if ( comment === 'other' ) {
-                               comment = input;
-                       } else if ( input !== '' ) {
-                               // Entry from drop down menu + additional comment
-                               comment += colonSeparator + input;
-                       }
-                       return comment;
-               };
-
-       // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
-       if ( summaryCodePointLimit ) {
-               $wpReason.codePointLimit( summaryCodePointLimit, filterFn );
-       } else if ( summaryByteLimit ) {
-               $wpReason.byteLimit( summaryByteLimit, filterFn );
-       }
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.commonsInterwikiWidget.js b/resources/src/mediawiki.special/mediawiki.special.search.commonsInterwikiWidget.js
deleted file mode 100644 (file)
index 648bf67..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-( function ( mw, $ ) {
-
-       var api = new mw.Api(),
-               pageUrl = new mw.Uri(),
-               imagesText = new mw.Message( mw.messages, 'searchprofile-images' ),
-               moreResultsText = new mw.Message( mw.messages, 'search-interwiki-more-results' );
-
-       function itemTemplate( results ) {
-
-               var resultOutput = '', i, result, imageCaption, imageThumbnailSrc;
-
-               for ( i = 0; i < results.length; i++ ) {
-                       result = results[ i ];
-                       imageCaption = mw.html.element( 'span', { 'class': 'iw-result__mini-gallery__caption' }, result.title );
-                       imageThumbnailSrc = ( result.thumbnail ) ? result.thumbnail.source : '';
-                       resultOutput += '<div class="iw-result__mini-gallery">' +
-                                               /* escaping response content */
-                                               mw.html.element( 'a', {
-                                                       href: '/wiki/' + result.title,
-                                                       'class': 'iw-result__mini-gallery__image',
-                                                       style: 'background-image: url(' + imageThumbnailSrc + ');'
-                                               }, new mw.html.Raw( imageCaption ) ) +
-                                       '</div>';
-               }
-
-               return resultOutput;
-       }
-
-       function itemWrapperTemplate( pageQuery, itemTemplateOutput ) {
-
-               return '<li class="iw-resultset iw-resultset--image" data-iw-resultset-pos="0">' +
-                               '<div class="iw-result__header">' +
-                                       '<strong>' + imagesText.escaped() + '</strong>' +
-                               '</div>' +
-                               '<div class="iw-result__content">' +
-                               /* template output has been sanitized by mw.html.element */
-                               itemTemplateOutput +
-                               '</div>' +
-                               '<div class="iw-result__footer">' +
-                                       '<a href="/w/index.php?title=Special:Search&search=' + encodeURIComponent( pageQuery ) + '&fulltext=1&profile=images">' +
-                                               moreResultsText.escaped() +
-                                       '</a>' +
-                               '</div>' +
-                       '</li>';
-
-       }
-
-       api.get( {
-               action: 'query',
-               generator: 'search',
-               gsrsearch: pageUrl.query.search,
-               gsrnamespace: mw.config.get( 'wgNamespaceIds' ).file,
-               gsrlimit: 3,
-               prop: 'pageimages',
-               pilimit: 3,
-               piprop: 'thumbnail',
-               pithumbsize: 300,
-               formatversion: 2
-       } ).done( function ( resp ) {
-               var results = ( resp.query && resp.query.pages ) ? resp.query.pages : false,
-                       multimediaWidgetTemplate;
-
-               if ( !results ) {
-                       return;
-               }
-
-               results.sort( function ( a, b ) {
-                       return a.index - b.index;
-               } );
-
-               multimediaWidgetTemplate = itemWrapperTemplate( pageUrl.query.search, itemTemplate( results ) );
-               /* we really only need to wait for document ready for DOM manipulation */
-               $( function () {
-                       $( '.iw-results' ).append( multimediaWidgetTemplate );
-               } );
-       } );
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.css b/resources/src/mediawiki.special/mediawiki.special.search.css
deleted file mode 100644 (file)
index aad784e..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-#mw-search-togglebox {
-       float: right;
-}
-#mw-search-togglebox label {
-       margin-right: 0.25em;
-}
-#mw-search-togglebox input {
-       margin-left: 0.25em;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.interwikiwidget.styles.less b/resources/src/mediawiki.special/mediawiki.special.search.interwikiwidget.styles.less
deleted file mode 100644 (file)
index 8ec2735..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-/* interwiki search results */
-/*==========================*/
-
-@import 'mediawiki.ui/variables.less';
-@import 'mediawiki.mixins';
-
-.mw-searchresults-has-iw {
-
-       .iw-headline {
-               font-weight: bold;
-       }
-
-       .iw-results {
-               list-style: none;
-               margin: 0;
-       }
-
-       .iw-resultset {
-               .box-sizing(border-box);
-               padding: 0.5em;
-               vertical-align: top;
-               width: 100%;
-               float: left;
-               background-color: @colorGray15;
-               margin-bottom: 1em;
-               word-break: break-word;
-       }
-
-       .iw-result__title {
-               font-size: 108%; /* matching regular search title */
-       }
-
-       .iw-result:after,
-       .iw-result__content:after { /* clearfix */
-               visibility: hidden;
-               display: block;
-               font-size: 0;
-               content: ' ';
-               clear: both;
-               height: 0;
-       }
-
-       .iw-result__footer {
-               float: right;
-               font-size: 97%; /* matching main search result font-size */
-               margin-top: 0.5em;
-       }
-       .iw-result__footer a {
-               vertical-align: middle;
-               font-style: italic;
-       }
-
-       .oo-ui-icon-favicon {
-               padding-right: 1em;
-       }
-
-       /* image search result */
-       .iw-result__mini-gallery {
-               position: relative;
-               float: left;
-               width: 100%;
-               height: 200px;
-               .box-sizing(border-box);
-               padding: 0.25rem;
-       }
-
-       /* second and third images are small */
-       .iw-result__mini-gallery:nth-child( 2 ),
-       .iw-result__mini-gallery:nth-child( 3 ) { /* stylelint-disable-line indentation */
-               width: 50%;
-               height: 100px;
-       }
-
-       .iw-result__mini-gallery__image {
-               display: block;
-               position: relative;
-               width: 100%;
-               height: 100%;
-               background-size: 100% auto;
-               background-size: cover;
-               background-repeat: no-repeat;
-               background-position: center center;
-       }
-
-       /* image gallery text */
-       .iw-result__mini-gallery__image > .iw-result__mini-gallery__caption {
-               visibility: hidden;
-               position: absolute;
-               bottom: 0;
-               left: 0;
-               text-align: center;
-               color: #fff;
-               font-size: 0.8em;
-               padding: 0.5em;
-               background-color: rgba( 0, 0, 0, 0.5 );
-       }
-
-       .iw-result__mini-gallery__image:hover > .iw-result__mini-gallery__caption {
-               visibility: visible;
-       }
-
-       /* tablet and up */
-
-       @media only screen and ( min-width: @deviceWidthTablet ) {
-
-               #mw-interwiki-results {
-                       width: 30%;
-                       display: inline-block; /* used to align interwiki sidebar with the top of the main search results */
-                       margin-left: 8%; /* since inline-block causes whitespace issues, this is 8 instead of 10% */
-               }
-               .mw-search-createlink,
-               .mw-search-nonefound,
-               .mw-search-results,
-               .mw-search-interwiki-header {
-                       float: left;
-                       width: 60%;
-                       clear: left;
-                       max-width: 60%;
-               }
-       }
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.js b/resources/src/mediawiki.special/mediawiki.special.search.js
deleted file mode 100644 (file)
index e809f2e..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*!
- * JavaScript for Special:Search
- */
-( function ( mw, $ ) {
-       $( function () {
-               var $checkboxes, $headerLinks, updateHeaderLinks, searchWidget;
-
-               // Emulate HTML5 autofocus behavior in non HTML5 compliant browsers
-               if ( !( 'autofocus' in document.createElement( 'input' ) ) ) {
-                       $( 'input[autofocus]' ).eq( 0 ).focus();
-               }
-
-               // Create check all/none button
-               $checkboxes = $( '#powersearch input[id^=mw-search-ns]' );
-               $( '#mw-search-togglebox' ).append(
-                       $( '<label>' )
-                               .text( mw.msg( 'powersearch-togglelabel' ) )
-               ).append(
-                       $( '<input>' ).attr( 'type', 'button' )
-                               .attr( 'id', 'mw-search-toggleall' )
-                               .prop( 'value', mw.msg( 'powersearch-toggleall' ) )
-                               .click( function () {
-                                       $checkboxes.prop( 'checked', true );
-                               } )
-               ).append(
-                       $( '<input>' ).attr( 'type', 'button' )
-                               .attr( 'id', 'mw-search-togglenone' )
-                               .prop( 'value', mw.msg( 'powersearch-togglenone' ) )
-                               .click( function () {
-                                       $checkboxes.prop( 'checked', false );
-                               } )
-               );
-
-               // Change the header search links to what user entered
-               $headerLinks = $( '.search-types a' );
-               searchWidget = OO.ui.infuse( 'searchText' );
-               updateHeaderLinks = function ( value ) {
-                       $headerLinks.each( function () {
-                               var parts = $( this ).attr( 'href' ).split( 'search=' ),
-                                       lastpart = '',
-                                       prefix = 'search=';
-                               if ( parts.length > 1 && parts[ 1 ].indexOf( '&' ) !== -1 ) {
-                                       lastpart = parts[ 1 ].slice( parts[ 1 ].indexOf( '&' ) );
-                               } else {
-                                       prefix = '&search=';
-                               }
-                               this.href = parts[ 0 ] + prefix + encodeURIComponent( value ) + lastpart;
-                       } );
-               };
-               searchWidget.on( 'change', updateHeaderLinks );
-               updateHeaderLinks( searchWidget.getValue() );
-
-               // When saving settings, use the proper request method (POST instead of GET).
-               $( '#mw-search-powersearch-remember' ).change( function () {
-                       this.form.method = this.checked ? 'post' : 'get';
-               } ).trigger( 'change' );
-
-       } );
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.styles.css b/resources/src/mediawiki.special/mediawiki.special.search.styles.css
deleted file mode 100644 (file)
index ea9b987..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-/* Special:Search */
-
-/*
- * Fixes sister projects box moving down the extract
- * of the first result (bug #16886).
- * It only happens when the window is small and
- * This changes slightly the layout for big screens
- * where there was space for the extracts and the
- * sister projects and thus it showed like in any
- * other browser.
- *
- * This will only affect IE 7 and lower
- */
-.searchresult {
-       display: inline !ie;
-}
-.searchresults {
-       margin: 1em 0 1em 0.4em;
-}
-/* needs extra specificity to override `.mw-body p` selector */
-.mw-body .mw-search-nonefound {
-       margin: 0;
-}
-
-.searchdidyoumean em,
-.searchmatch {
-       font-weight: bold;
-}
-
-.mw-search-results {
-       margin: 0;
-       max-width: 38em;
-}
-
-.mw-search-visualclear {
-       clear: both;
-}
-.mw-search-results li {
-       padding-bottom: 1.2em;
-       list-style: none;
-       list-style-image: none;
-}
-.mw-search-results li a {
-       font-size: 108%;
-}
-.mw-search-result-data {
-       color: #008000;
-       font-size: 97%;
-}
-.mw-search-profile-tabs {
-       background-color: #f8f9fa;
-       margin-top: 1em;
-       border: 1px solid #c8ccd1;
-       border-radius: 2px;
-}
-.search-types {
-       float: left;
-       padding-left: 0.25em;
-}
-.search-types ul {
-       margin: 0;
-       padding: 0;
-       list-style: none;
-}
-.search-types li {
-       float: left;
-       margin: 0;
-       padding: 0;
-}
-.search-types a {
-       display: block;
-       padding: 0.5em;
-}
-.search-types .current a {
-       color: #222;
-       cursor: default;
-}
-.search-types .current a:hover {
-       text-decoration: none;
-}
-.results-info {
-       float: right;
-       padding: 0.5em;
-       padding-right: 0.75em;
-       color: #54595d;
-       font-size: 95%;
-}
-#mw-search-top-table div.oo-ui-actionFieldLayout {
-       float: left;
-       width: 100%;
-}
-
-/* Advanced options menu */
-/*==========================*/
-
-#mw-searchoptions {
-       /* Support: Firefox, needs `clear: both` on `fieldset` when zoom level > 100%, see T176499 */
-       clear: both;
-       padding: 0.5em 0.75em 0.75em 0.75em;
-       background-color: #f8f9fa;
-       margin: -1px 0 0;
-       border: 1px solid #c8ccd1;
-       border-radius: 0 0 2px 2px;
-}
-#mw-searchoptions legend {
-       display: none;
-}
-#mw-searchoptions h4 {
-       padding: 0;
-       margin: 0;
-       float: left;
-}
-#mw-searchoptions table {
-       float: left;
-       margin-right: 3em;
-       border-collapse: collapse;
-}
-#mw-searchoptions table td {
-       padding: 0 1em 0 0;
-       white-space: nowrap;
-}
-#mw-searchoptions .divider {
-       clear: both;
-       border-bottom: 1px solid #eaecf0;
-       padding-top: 0.5em;
-       margin-bottom: 0.5em;
-}
-#mw-search-menu {
-       padding-left: 6em;
-       font-size: 85%;
-}
-
-#mw-search-interwiki {
-       float: right;
-       width: 18em;
-       border: 1px solid #a2a9b1;
-       margin-top: 2ex;
-}
-
-.searchalttitle,
-#mw-search-interwiki li {
-       font-size: 95%;
-}
-.mw-search-interwiki-more {
-       float: right;
-       font-size: 90%;
-}
-#mw-search-interwiki-caption {
-       text-align: center;
-       font-weight: bold;
-       font-size: 95%;
-}
-.mw-search-interwiki-project {
-       font-size: 97%;
-       text-align: left;
-       padding: 0.15em 0.15em 0.2em 0.2em;
-       background-color: #eaecf0;
-       border-top: 1px solid #c8ccd1;
-}
-
-.searchdidyoumean {
-       font-size: 127%;
-       margin-top: 0.8em;
-       /* Note that this color won't affect the link, as desired. */
-       color: #d33;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.undelete.js b/resources/src/mediawiki.special/mediawiki.special.undelete.js
deleted file mode 100644 (file)
index e3cf598..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/*!
- * JavaScript for Special:Undelete
- */
-( function ( mw, $ ) {
-       $( function () {
-               var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
-                       summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
-                       wpComment = OO.ui.infuse( $( '#wpComment' ).closest( '.oo-ui-widget' ) );
-
-               $( '#mw-undelete-invert' ).click( function () {
-                       $( '.mw-undelete-revlist input[type="checkbox"]' ).prop( 'checked', function ( i, val ) {
-                               return !val;
-                       } );
-               } );
-
-               // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
-               if ( summaryCodePointLimit ) {
-                       mw.widgets.visibleCodePointLimit( wpComment, summaryCodePointLimit );
-               } else if ( summaryByteLimit ) {
-                       mw.widgets.visibleByteLimit( wpComment, summaryByteLimit );
-               }
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css b/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css
deleted file mode 100644 (file)
index 69fec08..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-.mw-watched-item {
-       text-decoration: line-through;
-}
-
-.mw-watch-link-disabled {
-       pointer-events: none;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js b/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js
deleted file mode 100644 (file)
index 0886f8c..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/*!
- * JavaScript for Special:UnwatchedPages
- */
-( function ( mw, $ ) {
-       $( function () {
-               $( 'a.mw-watch-link' ).click( function ( e ) {
-                       var promise,
-                               api = new mw.Api(),
-                               $link = $( this ),
-                               $subjectLink = $link.closest( 'li' ).children( 'a' ).eq( 0 ),
-                               title = mw.util.getParamValue( 'title', $link.attr( 'href' ) );
-                       // nice format
-                       title = mw.Title.newFromText( title ).toText();
-                       $link.addClass( 'mw-watch-link-disabled' );
-
-                       // Preload the notification module for mw.notify
-                       mw.loader.load( 'mediawiki.notification' );
-
-                       // Use the class to determine whether to watch or unwatch
-                       if ( !$subjectLink.hasClass( 'mw-watched-item' ) ) {
-                               $link.text( mw.msg( 'watching' ) );
-                               promise = api.watch( title ).done( function () {
-                                       $subjectLink.addClass( 'mw-watched-item' );
-                                       $link.text( mw.msg( 'unwatch' ) );
-                                       mw.notify( mw.msg( 'addedwatchtext-short', title ) );
-                               } ).fail( function () {
-                                       $link.text( mw.msg( 'watch' ) );
-                                       mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
-                               } );
-                       } else {
-                               $link.text( mw.msg( 'unwatching' ) );
-                               promise = api.unwatch( title ).done( function () {
-                                       $subjectLink.removeClass( 'mw-watched-item' );
-                                       $link.text( mw.msg( 'watch' ) );
-                                       mw.notify( mw.msg( 'removedwatchtext-short', title ) );
-                               } ).fail( function () {
-                                       $link.text( mw.msg( 'unwatch' ) );
-                                       mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
-                               } );
-                       }
-
-                       promise.always( function () {
-                               $link.removeClass( 'mw-watch-link-disabled' );
-                       } );
-
-                       e.preventDefault();
-               } );
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.upload.js b/resources/src/mediawiki.special/mediawiki.special.upload.js
deleted file mode 100644 (file)
index 144659a..0000000
+++ /dev/null
@@ -1,654 +0,0 @@
-/**
- * JavaScript for Special:Upload
- *
- * @private
- * @class mw.special.upload
- * @singleton
- */
-
-/* global Uint8Array */
-
-( function ( mw, $ ) {
-       var uploadWarning, uploadTemplatePreview,
-               ajaxUploadDestCheck = mw.config.get( 'wgAjaxUploadDestCheck' ),
-               $license = $( '#wpLicense' );
-
-       window.wgUploadWarningObj = uploadWarning = {
-               responseCache: { '': '&nbsp;' },
-               nameToCheck: '',
-               typing: false,
-               delay: 500, // ms
-               timeoutID: false,
-
-               keypress: function () {
-                       if ( !ajaxUploadDestCheck ) {
-                               return;
-                       }
-
-                       // Find file to upload
-                       if ( !$( '#wpDestFile' ).length || !$( '#wpDestFile-warning' ).length ) {
-                               return;
-                       }
-
-                       this.nameToCheck = $( '#wpDestFile' ).val();
-
-                       // Clear timer
-                       if ( this.timeoutID ) {
-                               clearTimeout( this.timeoutID );
-                       }
-                       // Check response cache
-                       if ( this.responseCache.hasOwnProperty( this.nameToCheck ) ) {
-                               this.setWarning( this.responseCache[ this.nameToCheck ] );
-                               return;
-                       }
-
-                       this.timeoutID = setTimeout( function () {
-                               uploadWarning.timeout();
-                       }, this.delay );
-               },
-
-               checkNow: function ( fname ) {
-                       if ( !ajaxUploadDestCheck ) {
-                               return;
-                       }
-                       if ( this.timeoutID ) {
-                               clearTimeout( this.timeoutID );
-                       }
-                       this.nameToCheck = fname;
-                       this.timeout();
-               },
-
-               timeout: function () {
-                       var $spinnerDestCheck, title;
-                       if ( !ajaxUploadDestCheck || this.nameToCheck.trim() === '' ) {
-                               return;
-                       }
-                       $spinnerDestCheck = $.createSpinner().insertAfter( '#wpDestFile' );
-                       title = mw.Title.newFromText( this.nameToCheck, mw.config.get( 'wgNamespaceIds' ).file );
-
-                       ( new mw.Api() ).get( {
-                               formatversion: 2,
-                               action: 'query',
-                               // If title is empty, user input is invalid, the API call will produce details about why
-                               titles: [ title ? title.getPrefixedText() : this.nameToCheck ],
-                               prop: 'imageinfo',
-                               iiprop: 'uploadwarning',
-                               errorformat: 'html',
-                               errorlang: mw.config.get( 'wgUserLanguage' )
-                       } ).done( function ( result ) {
-                               var
-                                       resultOut = '',
-                                       page = result.query.pages[ 0 ];
-                               if ( page.imageinfo ) {
-                                       resultOut = page.imageinfo[ 0 ].html;
-                               } else if ( page.invalidreason ) {
-                                       resultOut = page.invalidreason.html;
-                               }
-                               uploadWarning.processResult( resultOut, uploadWarning.nameToCheck );
-                       } ).always( function () {
-                               $spinnerDestCheck.remove();
-                       } );
-               },
-
-               processResult: function ( result, fileName ) {
-                       this.setWarning( result );
-                       this.responseCache[ fileName ] = result;
-               },
-
-               setWarning: function ( warning ) {
-                       var $warningBox = $( '#wpDestFile-warning' ),
-                               $warning = $( $.parseHTML( warning ) );
-                       mw.hook( 'wikipage.content' ).fire( $warning );
-                       $warningBox.empty().append( $warning );
-
-                       // Set a value in the form indicating that the warning is acknowledged and
-                       // doesn't need to be redisplayed post-upload
-                       if ( !warning ) {
-                               $( '#wpDestFileWarningAck' ).val( '' );
-                               $warningBox.removeAttr( 'class' );
-                       } else {
-                               $( '#wpDestFileWarningAck' ).val( '1' );
-                               $warningBox.attr( 'class', 'mw-destfile-warning' );
-                       }
-
-               }
-       };
-
-       window.wgUploadTemplatePreviewObj = uploadTemplatePreview = {
-
-               responseCache: { '': '' },
-
-               /**
-                * @param {jQuery} $element The element whose .val() will be previewed
-                * @param {jQuery} $previewContainer The container to display the preview in
-                */
-               getPreview: function ( $element, $previewContainer ) {
-                       var template = $element.val(),
-                               $spinner;
-
-                       if ( this.responseCache.hasOwnProperty( template ) ) {
-                               this.showPreview( this.responseCache[ template ], $previewContainer );
-                               return;
-                       }
-
-                       $spinner = $.createSpinner().insertAfter( $element );
-
-                       ( new mw.Api() ).parse( '{{' + template + '}}', {
-                               title: $( '#wpDestFile' ).val() || 'File:Sample.jpg',
-                               prop: 'text',
-                               pst: true,
-                               uselang: mw.config.get( 'wgUserLanguage' )
-                       } ).done( function ( result ) {
-                               uploadTemplatePreview.processResult( result, template, $previewContainer );
-                       } ).always( function () {
-                               $spinner.remove();
-                       } );
-               },
-
-               processResult: function ( result, template, $previewContainer ) {
-                       this.responseCache[ template ] = result;
-                       this.showPreview( this.responseCache[ template ], $previewContainer );
-               },
-
-               showPreview: function ( preview, $previewContainer ) {
-                       $previewContainer.html( preview );
-               }
-
-       };
-
-       $( function () {
-               // AJAX wpDestFile warnings
-               if ( ajaxUploadDestCheck ) {
-                       // Insert an event handler that fetches upload warnings when wpDestFile
-                       // has been changed
-                       $( '#wpDestFile' ).change( function () {
-                               uploadWarning.checkNow( $( this ).val() );
-                       } );
-                       // Insert a row where the warnings will be displayed just below the
-                       // wpDestFile row
-                       $( '#mw-htmlform-description tbody' ).append(
-                               $( '<tr>' ).append(
-                                       $( '<td>' )
-                                               .attr( 'id', 'wpDestFile-warning' )
-                                               .attr( 'colspan', 2 )
-                               )
-                       );
-               }
-
-               if ( mw.config.get( 'wgAjaxLicensePreview' ) && $license.length ) {
-                       // License selector check
-                       $license.change( function () {
-                               // We might show a preview
-                               uploadTemplatePreview.getPreview( $license, $( '#mw-license-preview' ) );
-                       } );
-
-                       // License selector table row
-                       $license.closest( 'tr' ).after(
-                               $( '<tr>' ).append(
-                                       $( '<td>' ),
-                                       $( '<td>' ).attr( 'id', 'mw-license-preview' )
-                               )
-                       );
-               }
-
-               // fillDestFile setup
-               mw.config.get( 'wgUploadSourceIds' ).forEach( function ( sourceId ) {
-                       $( '#' + sourceId ).change( function () {
-                               var path, slash, backslash, fname;
-                               if ( !mw.config.get( 'wgUploadAutoFill' ) ) {
-                                       return;
-                               }
-                               // Remove any previously flagged errors
-                               $( '#mw-upload-permitted' ).attr( 'class', '' );
-                               $( '#mw-upload-prohibited' ).attr( 'class', '' );
-
-                               path = $( this ).val();
-                               // Find trailing part
-                               slash = path.lastIndexOf( '/' );
-                               backslash = path.lastIndexOf( '\\' );
-                               if ( slash === -1 && backslash === -1 ) {
-                                       fname = path;
-                               } else if ( slash > backslash ) {
-                                       fname = path.slice( slash + 1 );
-                               } else {
-                                       fname = path.slice( backslash + 1 );
-                               }
-
-                               // Clear the filename if it does not have a valid extension.
-                               // URLs are less likely to have a useful extension, so don't include them in the
-                               // extension check.
-                               if (
-                                       mw.config.get( 'wgCheckFileExtensions' ) &&
-                                       mw.config.get( 'wgStrictFileExtensions' ) &&
-                                       Array.isArray( mw.config.get( 'wgFileExtensions' ) ) &&
-                                       $( this ).attr( 'id' ) !== 'wpUploadFileURL'
-                               ) {
-                                       if (
-                                               fname.lastIndexOf( '.' ) === -1 ||
-                                               mw.config.get( 'wgFileExtensions' ).map( function ( element ) {
-                                                       return element.toLowerCase();
-                                               } ).indexOf( fname.slice( fname.lastIndexOf( '.' ) + 1 ).toLowerCase() ) === -1
-                                       ) {
-                                               // Not a valid extension
-                                               // Clear the upload and set mw-upload-permitted to error
-                                               $( this ).val( '' );
-                                               $( '#mw-upload-permitted' ).attr( 'class', 'error' );
-                                               $( '#mw-upload-prohibited' ).attr( 'class', 'error' );
-                                               // Clear wpDestFile as well
-                                               $( '#wpDestFile' ).val( '' );
-
-                                               return false;
-                                       }
-                               }
-
-                               // Replace spaces by underscores
-                               fname = fname.replace( / /g, '_' );
-                               // Capitalise first letter if needed
-                               if ( mw.config.get( 'wgCapitalizeUploads' ) ) {
-                                       fname = fname[ 0 ].toUpperCase() + fname.slice( 1 );
-                               }
-
-                               // Output result
-                               if ( $( '#wpDestFile' ).length ) {
-                                       // Call decodeURIComponent function to remove possible URL-encoded characters
-                                       // from the file name (T32390). Especially likely with upload-form-url.
-                                       // decodeURIComponent can throw an exception if input is invalid utf-8
-                                       try {
-                                               $( '#wpDestFile' ).val( decodeURIComponent( fname ) );
-                                       } catch ( err ) {
-                                               $( '#wpDestFile' ).val( fname );
-                                       }
-                                       uploadWarning.checkNow( fname );
-                               }
-                       } );
-               } );
-       } );
-
-       // Add a preview to the upload form
-       $( function () {
-               /**
-                * Is the FileAPI available with sufficient functionality?
-                *
-                * @return {boolean}
-                */
-               function hasFileAPI() {
-                       return window.FileReader !== undefined;
-               }
-
-               /**
-                * Check if this is a recognizable image type...
-                * Also excludes files over 10M to avoid going insane on memory usage.
-                *
-                * TODO: Is there a way we can ask the browser what's supported in `<img>`s?
-                *
-                * TODO: Put SVG back after working around Firefox 7 bug <https://phabricator.wikimedia.org/T33643>
-                *
-                * @param {File} file
-                * @return {boolean}
-                */
-               function fileIsPreviewable( file ) {
-                       var known = [ 'image/png', 'image/gif', 'image/jpeg', 'image/svg+xml' ],
-                               tooHuge = 10 * 1024 * 1024;
-                       return ( known.indexOf( file.type ) !== -1 ) && file.size > 0 && file.size < tooHuge;
-               }
-
-               /**
-                * Format a file size attractively.
-                *
-                * TODO: Match numeric formatting
-                *
-                * @param {number} s
-                * @return {string}
-                */
-               function prettySize( s ) {
-                       var sizeMsgs = [ 'size-bytes', 'size-kilobytes', 'size-megabytes', 'size-gigabytes' ];
-                       while ( s >= 1024 && sizeMsgs.length > 1 ) {
-                               s /= 1024;
-                               sizeMsgs = sizeMsgs.slice( 1 );
-                       }
-                       return mw.msg( sizeMsgs[ 0 ], Math.round( s ) );
-               }
-
-               /**
-                * Start loading a file into memory; when complete, pass it as a
-                * data URL to the callback function. If the callbackBinary is set it will
-                * first be read as binary and afterwards as data URL. Useful if you want
-                * to do preprocessing on the binary data first.
-                *
-                * @param {File} file
-                * @param {Function} callback
-                * @param {Function} callbackBinary
-                */
-               function fetchPreview( file, callback, callbackBinary ) {
-                       var reader = new FileReader();
-                       if ( callbackBinary && 'readAsBinaryString' in reader ) {
-                               // To fetch JPEG metadata we need a binary string; start there.
-                               // TODO
-                               reader.onload = function () {
-                                       callbackBinary( reader.result );
-
-                                       // Now run back through the regular code path.
-                                       fetchPreview( file, callback );
-                               };
-                               reader.readAsBinaryString( file );
-                       } else if ( callbackBinary && 'readAsArrayBuffer' in reader ) {
-                               // readAsArrayBuffer replaces readAsBinaryString
-                               // However, our JPEG metadata library wants a string.
-                               // So, this is going to be an ugly conversion.
-                               reader.onload = function () {
-                                       var i,
-                                               buffer = new Uint8Array( reader.result ),
-                                               string = '';
-                                       for ( i = 0; i < buffer.byteLength; i++ ) {
-                                               string += String.fromCharCode( buffer[ i ] );
-                                       }
-                                       callbackBinary( string );
-
-                                       // Now run back through the regular code path.
-                                       fetchPreview( file, callback );
-                               };
-                               reader.readAsArrayBuffer( file );
-                       } else if ( 'URL' in window && 'createObjectURL' in window.URL ) {
-                               // Supported in Firefox 4.0 and above <https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL>
-                               // WebKit has it in a namespace for now but that's ok. ;)
-                               //
-                               // Lifetime of this URL is until document close, which is fine
-                               // for Special:Upload -- if this code gets used on longer-running
-                               // pages, add a revokeObjectURL() when it's no longer needed.
-                               //
-                               // Prefer this over readAsDataURL for Firefox 7 due to bug reading
-                               // some SVG files from data URIs <https://bugzilla.mozilla.org/show_bug.cgi?id=694165>
-                               callback( window.URL.createObjectURL( file ) );
-                       } else {
-                               // This ends up decoding the file to base-64 and back again, which
-                               // feels horribly inefficient.
-                               reader.onload = function () {
-                                       callback( reader.result );
-                               };
-                               reader.readAsDataURL( file );
-                       }
-               }
-
-               /**
-                * Clear the file upload preview area.
-                */
-               function clearPreview() {
-                       $( '#mw-upload-thumbnail' ).remove();
-               }
-
-               /**
-                * Show a thumbnail preview of PNG, JPEG, GIF, and SVG files prior to upload
-                * in browsers supporting HTML5 FileAPI.
-                *
-                * As of this writing, known good:
-                *
-                * - Firefox 3.6+
-                * - Chrome 7.something
-                *
-                * TODO: Check file size limits and warn of likely failures
-                *
-                * @param {File} file
-                */
-               function showPreview( file ) {
-                       var $canvas,
-                               ctx,
-                               meta,
-                               previewSize = 180,
-                               $spinner = $.createSpinner( { size: 'small', type: 'block' } )
-                                       .css( { width: previewSize, height: previewSize } ),
-                               thumb = mw.template.get( 'mediawiki.special.upload', 'thumbnail.html' ).render();
-
-                       thumb
-                               .find( '.filename' ).text( file.name ).end()
-                               .find( '.fileinfo' ).text( prettySize( file.size ) ).end()
-                               .find( '.thumbinner' ).prepend( $spinner ).end();
-
-                       $canvas = $( '<canvas>' ).attr( { width: previewSize, height: previewSize } );
-                       ctx = $canvas[ 0 ].getContext( '2d' );
-                       $( '#mw-htmlform-source' ).parent().prepend( thumb );
-
-                       fetchPreview( file, function ( dataURL ) {
-                               var img = new Image(),
-                                       rotation = 0;
-
-                               if ( meta && meta.tiff && meta.tiff.Orientation ) {
-                                       rotation = ( 360 - ( function () {
-                                               // See BitmapHandler class in PHP
-                                               switch ( meta.tiff.Orientation.value ) {
-                                                       case 8:
-                                                               return 90;
-                                                       case 3:
-                                                               return 180;
-                                                       case 6:
-                                                               return 270;
-                                                       default:
-                                                               return 0;
-                                               }
-                                       }() ) ) % 360;
-                               }
-
-                               img.onload = function () {
-                                       var info, width, height, x, y, dx, dy, logicalWidth, logicalHeight;
-
-                                       // Fit the image within the previewSizexpreviewSize box
-                                       if ( img.width > img.height ) {
-                                               width = previewSize;
-                                               height = img.height / img.width * previewSize;
-                                       } else {
-                                               height = previewSize;
-                                               width = img.width / img.height * previewSize;
-                                       }
-                                       // Determine the offset required to center the image
-                                       dx = ( 180 - width ) / 2;
-                                       dy = ( 180 - height ) / 2;
-                                       switch ( rotation ) {
-                                               // If a rotation is applied, the direction of the axis
-                                               // changes as well. You can derive the values below by
-                                               // drawing on paper an axis system, rotate it and see
-                                               // where the positive axis direction is
-                                               case 0:
-                                                       x = dx;
-                                                       y = dy;
-                                                       logicalWidth = img.width;
-                                                       logicalHeight = img.height;
-                                                       break;
-                                               case 90:
-
-                                                       x = dx;
-                                                       y = dy - previewSize;
-                                                       logicalWidth = img.height;
-                                                       logicalHeight = img.width;
-                                                       break;
-                                               case 180:
-                                                       x = dx - previewSize;
-                                                       y = dy - previewSize;
-                                                       logicalWidth = img.width;
-                                                       logicalHeight = img.height;
-                                                       break;
-                                               case 270:
-                                                       x = dx - previewSize;
-                                                       y = dy;
-                                                       logicalWidth = img.height;
-                                                       logicalHeight = img.width;
-                                                       break;
-                                       }
-
-                                       ctx.clearRect( 0, 0, 180, 180 );
-                                       ctx.rotate( rotation / 180 * Math.PI );
-                                       ctx.drawImage( img, x, y, width, height );
-                                       $spinner.replaceWith( $canvas );
-
-                                       // Image size
-                                       info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) +
-                                               ', ' + prettySize( file.size );
-
-                                       $( '#mw-upload-thumbnail .fileinfo' ).text( info );
-                               };
-                               img.onerror = function () {
-                                       // Can happen for example for invalid SVG files
-                                       clearPreview();
-                               };
-                               img.src = dataURL;
-                       }, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) {
-                               var jpegmeta = mw.loader.require( 'mediawiki.libs.jpegmeta' );
-                               try {
-                                       meta = jpegmeta( data, file.fileName );
-                                       // eslint-disable-next-line no-underscore-dangle, camelcase
-                                       meta._binary_data = null;
-                               } catch ( e ) {
-                                       meta = null;
-                               }
-                       } : null );
-               }
-
-               /**
-                * Check if the file does not exceed the maximum size
-                *
-                * @param {File} file
-                * @return {boolean}
-                */
-               function checkMaxUploadSize( file ) {
-                       var maxSize, $error;
-
-                       function getMaxUploadSize( type ) {
-                               var sizes = mw.config.get( 'wgMaxUploadSize' );
-
-                               if ( sizes[ type ] !== undefined ) {
-                                       return sizes[ type ];
-                               }
-                               return sizes[ '*' ];
-                       }
-
-                       $( '.mw-upload-source-error' ).remove();
-
-                       maxSize = getMaxUploadSize( 'file' );
-                       if ( file.size > maxSize ) {
-                               $error = $( '<p class="error mw-upload-source-error" id="wpSourceTypeFile-error">' +
-                                       mw.message( 'largefileserver', file.size, maxSize ).escaped() + '</p>' );
-
-                               $( '#wpUploadFile' ).after( $error );
-
-                               return false;
-                       }
-
-                       return true;
-               }
-
-               /* Initialization */
-               if ( hasFileAPI() ) {
-                       // Update thumbnail when the file selection control is updated.
-                       $( '#wpUploadFile' ).change( function () {
-                               var file;
-                               clearPreview();
-                               if ( this.files && this.files.length ) {
-                                       // Note: would need to be updated to handle multiple files.
-                                       file = this.files[ 0 ];
-
-                                       if ( !checkMaxUploadSize( file ) ) {
-                                               return;
-                                       }
-
-                                       if ( fileIsPreviewable( file ) ) {
-                                               showPreview( file );
-                                       }
-                               }
-                       } );
-               }
-       } );
-
-       // Disable all upload source fields except the selected one
-       $( function () {
-               var $rows = $( '.mw-htmlform-field-UploadSourceField' );
-
-               $rows.on( 'change', 'input[type="radio"]', function ( e ) {
-                       var currentRow = e.delegateTarget;
-
-                       if ( !this.checked ) {
-                               return;
-                       }
-
-                       $( '.mw-upload-source-error' ).remove();
-
-                       // Enable selected upload method
-                       $( currentRow ).find( 'input' ).prop( 'disabled', false );
-
-                       // Disable inputs of other upload methods
-                       // (except for the radio button to re-enable it)
-                       $rows
-                               .not( currentRow )
-                               .find( 'input[type!="radio"]' )
-                               .prop( 'disabled', true );
-               } );
-
-               // Set initial state
-               if ( !$( '#wpSourceTypeurl' ).prop( 'checked' ) ) {
-                       $( '#wpUploadFileURL' ).prop( 'disabled', true );
-               }
-       } );
-
-       $( function () {
-               // Prevent losing work
-               var allowCloseWindow,
-                       $uploadForm = $( '#mw-upload-form' );
-
-               if ( !mw.user.options.get( 'useeditwarning' ) ) {
-                       // If the user doesn't want edit warnings, don't set things up.
-                       return;
-               }
-
-               $uploadForm.data( 'origtext', $uploadForm.serialize() );
-
-               allowCloseWindow = mw.confirmCloseWindow( {
-                       test: function () {
-                               return $( '#wpUploadFile' ).get( 0 ).files.length !== 0 ||
-                                       $uploadForm.data( 'origtext' ) !== $uploadForm.serialize();
-                       },
-
-                       message: mw.msg( 'editwarning-warning' ),
-                       namespace: 'uploadwarning'
-               } );
-
-               $uploadForm.submit( function () {
-                       allowCloseWindow.release();
-               } );
-       } );
-
-       // Add tabindex to mw-editTools
-       $( function () {
-               // Function to change tabindex for all links within mw-editTools
-               function setEditTabindex( $val ) {
-                       $( '.mw-editTools' ).find( 'a' ).each( function () {
-                               $( this ).attr( 'tabindex', $val );
-                       } );
-               }
-
-               // Change tabindex to 0 if user pressed spaced or enter while focused
-               $( '.mw-editTools' ).on( 'keypress', function ( e ) {
-                       // Don't continue if pressed key was not enter or spacebar
-                       if ( e.which !== 13 && e.which !== 32 ) {
-                               return;
-                       }
-
-                       // Change tabindex only when main div has focus
-                       if ( $( this ).is( ':focus' ) ) {
-                               $( this ).find( 'a' ).first().focus();
-                               setEditTabindex( '0' );
-                       }
-               } );
-
-               // Reset tabindex for elements when user focused out mw-editTools
-               $( '.mw-editTools' ).on( 'focusout', function ( e ) {
-                       // Don't continue if relatedTarget is within mw-editTools
-                       if ( e.relatedTarget !== null && $( e.relatedTarget ).closest( '.mw-editTools' ).length > 0 ) {
-                               return;
-                       }
-
-                       // Reset tabindex back to -1
-                       setEditTabindex( '-1' );
-               } );
-
-               // Set initial tabindex for mw-editTools to 0 and to -1 for all links
-               $( '.mw-editTools' ).attr( 'tabindex', '0' );
-               setEditTabindex( '-1' );
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.upload.styles.css b/resources/src/mediawiki.special/mediawiki.special.upload.styles.css
deleted file mode 100644 (file)
index 626a7e8..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-/*!
- * Styling for Special:Upload
- */
-.mw-destfile-warning {
-       border: 1px solid #fde29b;
-       padding: 0.5em 1em;
-       margin-bottom: 1em;
-       color: #705000;
-       background-color: #fdf1d1;
-}
-
-p.mw-upload-editlicenses {
-       font-size: 90%;
-       text-align: right;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.common.css b/resources/src/mediawiki.special/mediawiki.special.userlogin.common.css
deleted file mode 100644 (file)
index 2366249..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-/* User login and signup forms */
-.mw-ui-vform .mw-form-related-link-container {
-       margin-bottom: 0.5em;
-       text-align: center;
-}
-
-.mw-ui-vform .mw-secure {
-       /* @embed */
-       background: url( images/icon-lock.png ) no-repeat left center;
-       margin: 0 0 0 1px;
-       padding: 0 0 0 11px;
-}
-
-/*
- * When inside the VForm style, disable the border that Vector and other skins
- * put on the div surrounding the login/create account form.
- * Also disable the margin and padding that Vector puts around the form.
- */
-.mw-ui-container #userloginForm,
-.mw-ui-container #userlogin {
-       border: 0;
-       margin: 0;
-       padding: 0;
-}
-
-/* Reposition and resize language links, which appear on a per-wiki basis */
-.mw-ui-container #languagelinks {
-       margin-bottom: 2em;
-       font-size: 0.8em;
-}
-
-/* Put some space under template's header, which may contain CAPTCHA HTML. */
-section.mw-form-header {
-       margin-bottom: 10px;
-}
-
-/* shuffled CAPTCHA */
-#wpCaptchaWord {
-       margin-top: 6px;
-}
-
-.fancycaptcha-captcha-container {
-       background-color: #f8f9fa;
-       margin-bottom: 15px;
-       border: 1px solid #c8ccd1;
-       border-radius: 2px;
-       padding: 8px;
-       text-align: center;
-}
-
-.mw-createacct-captcha-assisted {
-       display: block;
-       margin-top: 0.5em;
-}
-
-/* Put a border around the fancycaptcha-image-container. */
-.fancycaptcha-captcha-and-reload {
-       border: 1px solid #c8ccd1;
-       border-radius: 2px 2px 0 0;
-       /* Other display formats end up too wide */
-       display: table-cell;
-       width: 270px;
-       background-color: #fff;
-}
-
-.fancycaptcha-captcha-container .mw-ui-input {
-       margin-top: -1px;
-       border-color: #c8ccd1;
-       border-radius: 0 0 2px 2px;
-}
-
-/* Make the fancycaptcha-image-container full-width within its parent. */
-.fancycaptcha-image-container {
-       width: 100%;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.login.css b/resources/src/mediawiki.special/mediawiki.special.userlogin.login.css
deleted file mode 100644 (file)
index fe013bc..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/* The login form invites users to create an account */
-#mw-createaccount-cta {
-       width: 20em;
-       /* @embed */
-       background: url( images/glyph-people-large.png ) no-repeat 50%;
-       margin: 0 auto;
-       padding-top: 7.8em;
-       font-weight: bold;
-}
-
-/* Login Button, following 'ButtonWidget (progressive)' from OOUI */
-#mw-createaccount-join {
-       background-color: #f8f9fa;
-       color: #36c;
-}
-#mw-createaccount-join:hover {
-       background-color: #fff;
-       border-color: #859ecc;
-       box-shadow: none;
-}
-#mw-createaccount-join:active {
-       background-color: #eff3fa;
-       color: #2a4b8d;
-       border-color: #2a4b8d;
-}
-#mw-createaccount-join:focus {
-       border-color: #36c;
-       box-shadow: inset 0 0 0 1px #36c;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css b/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css
deleted file mode 100644 (file)
index 3cfa5a8..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/* Disable the underline that Vector puts on h2 headings, and bold them. */
-.mw-ui-container h2 {
-       border: 0;
-       font-weight: bold;
-}
-
-/* Benefits column CSS to the right (if it fits) of the form. */
-.mw-ui-container #userloginForm {
-       float: left;
-       /* Override the right margin of the form to give space in case a benefits
-        * column appears to the side. */
-       margin-right: 100px;
-       /* Override `.mw-body-content` to ensure useful, readable paragraphs */
-       line-height: 1.4;
-}
-
-.mw-createacct-benefits-container {
-       /* Keeps this column compact and close to the form, but tends to squish contents. */
-       float: left;
-}
-
-.mw-createacct-benefits-container h2 {
-       margin-bottom: 30px;
-}
-
-.mw-number-text.icon-edits {
-       /* @embed */
-       background: url( images/icon-edits.png ) no-repeat left center;
-}
-
-.mw-number-text.icon-pages {
-       /* @embed */
-       background: url( images/icon-pages.png ) no-repeat left center;
-}
-
-.mw-number-text.icon-contributors {
-       /* @embed */
-       background: url( images/icon-contributors.png ) no-repeat left center;
-}
-
-/*
- * Special font for numbers in benefits, same as Vector's `@content-heading-font-family`.
- * Needs an ID so that it's more specific than Vector's div#content h3.
- */
-#bodyContent .mw-number-text h3 {
-       color: #222;
-       margin: 0;
-       padding: 0;
-       font-family: 'Linux Libertine', 'Georgia', 'Times', serif;
-       font-weight: normal;
-       font-size: 2.2em;
-       line-height: 1.2;
-       text-align: center;
-}
-
-/* Contains a “headlined” number and explanatory text, with space for an icon */
-.mw-number-text {
-       display: block;
-       font-size: 1.2em;
-       color: #444;
-       margin-top: 1em;
-       /* 80px wide icon plus "margin" */
-       padding: 0 0 0 95px;
-       /* Matches max icon height, ensures icon emblem is visible */
-       min-height: 75px;
-       text-align: center;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js b/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js
deleted file mode 100644 (file)
index 8a61afb..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-/*!
- * JavaScript for signup form.
- */
-( function ( mw, $ ) {
-       // When sending password by email, hide the password input fields.
-       $( function () {
-               // Always required if checked, otherwise it depends, so we use the original
-               var $emailLabel = $( 'label[for="wpEmail"]' ),
-                       originalText = $emailLabel.text(),
-                       requiredText = mw.message( 'createacct-emailrequired' ).text(),
-                       $createByMailCheckbox = $( '#wpCreateaccountMail' ),
-                       $beforePwds = $( '.mw-row-password:first' ).prev(),
-                       $pwds;
-
-               function updateForCheckbox() {
-                       var checked = $createByMailCheckbox.prop( 'checked' );
-                       if ( checked ) {
-                               $pwds = $( '.mw-row-password' ).detach();
-                               $emailLabel.text( requiredText );
-                       } else {
-                               if ( $pwds ) {
-                                       $beforePwds.after( $pwds );
-                                       $pwds = null;
-                               }
-                               $emailLabel.text( originalText );
-                       }
-               }
-
-               $createByMailCheckbox.on( 'change', updateForCheckbox );
-               updateForCheckbox();
-       } );
-
-       // Check if the username is invalid or already taken
-       mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
-               var $usernameInput = $root.find( '#wpName2' ),
-                       $passwordInput = $root.find( '#wpPassword2' ),
-                       $emailInput = $root.find( '#wpEmail' ),
-                       $realNameInput = $root.find( '#wpRealName' ),
-                       api = new mw.Api(),
-                       usernameChecker, passwordChecker;
-
-               function checkUsername( username ) {
-                       // We could just use .then() if we didn't have to pass on .abort()…
-                       var d, apiPromise;
-
-                       d = $.Deferred();
-                       apiPromise = api.get( {
-                               action: 'query',
-                               list: 'users',
-                               ususers: username,
-                               usprop: 'cancreate',
-                               formatversion: 2,
-                               errorformat: 'html',
-                               errorsuselocal: true,
-                               uselang: mw.config.get( 'wgUserLanguage' )
-                       } )
-                               .done( function ( resp ) {
-                                       var userinfo = resp.query.users[ 0 ];
-
-                                       if ( resp.query.users.length !== 1 || userinfo.invalid ) {
-                                               d.resolve( { valid: false, messages: [ mw.message( 'noname' ).parseDom() ] } );
-                                       } else if ( userinfo.userid !== undefined ) {
-                                               d.resolve( { valid: false, messages: [ mw.message( 'userexists' ).parseDom() ] } );
-                                       } else if ( !userinfo.cancreate ) {
-                                               d.resolve( {
-                                                       valid: false,
-                                                       messages: userinfo.cancreateerror ? userinfo.cancreateerror.map( function ( m ) {
-                                                               return m.html;
-                                                       } ) : []
-                                               } );
-                                       } else {
-                                               d.resolve( { valid: true, messages: [] } );
-                                       }
-                               } )
-                               .fail( d.reject );
-
-                       return d.promise( { abort: apiPromise.abort } );
-               }
-
-               function checkPassword() {
-                       // We could just use .then() if we didn't have to pass on .abort()…
-                       var apiPromise,
-                               d = $.Deferred();
-
-                       if ( $usernameInput.val().trim() === '' ) {
-                               d.resolve( { valid: true, messages: [] } );
-                               return d.promise();
-                       }
-
-                       apiPromise = api.post( {
-                               action: 'validatepassword',
-                               user: $usernameInput.val(),
-                               password: $passwordInput.val(),
-                               email: $emailInput.val() || '',
-                               realname: $realNameInput.val() || '',
-                               formatversion: 2,
-                               errorformat: 'html',
-                               errorsuselocal: true,
-                               uselang: mw.config.get( 'wgUserLanguage' )
-                       } )
-                               .done( function ( resp ) {
-                                       var pwinfo = resp.validatepassword || {};
-
-                                       d.resolve( {
-                                               valid: pwinfo.validity === 'Good',
-                                               messages: pwinfo.validitymessages ? pwinfo.validitymessages.map( function ( m ) {
-                                                       return m.html;
-                                               } ) : []
-                                       } );
-                               } )
-                               .fail( d.reject );
-
-                       return d.promise( { abort: apiPromise.abort } );
-               }
-
-               usernameChecker = new mw.htmlform.Checker( $usernameInput, checkUsername );
-               usernameChecker.attach();
-
-               passwordChecker = new mw.htmlform.Checker( $passwordInput, checkPassword );
-               passwordChecker.attach( $usernameInput.add( $emailInput ).add( $realNameInput ) );
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.userrights.js b/resources/src/mediawiki.special/mediawiki.special.userrights.js
deleted file mode 100644 (file)
index 487e63a..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-/*!
- * JavaScript for Special:UserRights
- */
-( function ( mw, $ ) {
-       var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' ),
-               summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
-               summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
-               $wpReason = $( '#wpReason' );
-
-       // Replace successbox with notifications
-       convertmessagebox();
-
-       // Dynamically show/hide the "other time" input under each dropdown
-       $( '.mw-userrights-nested select' ).on( 'change', function ( e ) {
-               $( e.target.parentNode ).find( 'input' ).toggle( $( e.target ).val() === 'other' );
-       } );
-
-       // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
-       if ( summaryCodePointLimit ) {
-               $wpReason.codePointLimit( summaryCodePointLimit );
-       } else if ( summaryByteLimit ) {
-               $wpReason.byteLimit( summaryByteLimit );
-       }
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.version.css b/resources/src/mediawiki.special/mediawiki.special.version.css
deleted file mode 100644 (file)
index 1b8581a..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-/*!
- * Styling for Special:Version
- */
-.mw-version-ext-name,
-.mw-version-library-name {
-       font-weight: bold;
-}
-
-.mw-version-ext-license,
-.mw-version-ext-vcs-timestamp {
-       white-space: nowrap;
-}
-
-th.mw-version-ext-col-label {
-       font-size: 0.9em;
-}
-
-.mw-version-ext-vcs-version {
-       unicode-bidi: embed;
-}
-
-.mw-version-credits {
-       column-width: 18em;
-       -moz-column-width: 18em;
-       -webkit-column-width: 18em;
-}
-
-.mw-version-credits ul {
-       margin-top: 0;
-       margin-bottom: 0;
-}
-
-.mw-version-license-info strong {
-       font-weight: normal;
-}
-
-.mw-version-license-info em {
-       font-style: normal;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.watchlist.css b/resources/src/mediawiki.special/mediawiki.special.watchlist.css
deleted file mode 100644 (file)
index c9861c2..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-/*!
- * Styling for elements generated by JavaScript on Special:Watchlist
- */
-.mw-changelist-line-inner-unwatched {
-       text-decoration: line-through;
-       opacity: 0.5;
-}
-
-span.mw-changeslist-line-prefix {
-       display: inline-block;
-}
-/* This can be either a span or a table cell */
-.mw-changeslist-line-prefix {
-       width: 1.25em;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.watchlist.js b/resources/src/mediawiki.special/mediawiki.special.watchlist.js
deleted file mode 100644 (file)
index 565ed2c..0000000
+++ /dev/null
@@ -1,158 +0,0 @@
-/*!
- * JavaScript for Special:Watchlist
- */
-( function ( mw, $, OO ) {
-       $( function () {
-               var api = new mw.Api(), $progressBar, $resetForm = $( '#mw-watchlist-resetbutton' );
-
-               // If the user wants to reset their watchlist, use an API call to do so (no reload required)
-               // Adapted from a user script by User:NQ of English Wikipedia
-               // (User:NQ/WatchlistResetConfirm.js)
-               $resetForm.submit( function ( event ) {
-                       var $button = $resetForm.find( 'input[name=mw-watchlist-reset-submit]' );
-
-                       event.preventDefault();
-
-                       // Disable reset button to prevent multiple concurrent requests
-                       $button.prop( 'disabled', true );
-
-                       if ( !$progressBar ) {
-                               $progressBar = new OO.ui.ProgressBarWidget( { progress: false } ).$element;
-                               $progressBar.css( {
-                                       position: 'absolute', width: '100%'
-                               } );
-                       }
-                       // Show progress bar
-                       $resetForm.append( $progressBar );
-
-                       // Use action=setnotificationtimestamp to mark all as visited,
-                       // then set all watchlist lines accordingly
-                       api.postWithToken( 'csrf', {
-                               formatversion: 2, action: 'setnotificationtimestamp', entirewatchlist: true
-                       } ).done( function () {
-                               // Enable button again
-                               $button.prop( 'disabled', false );
-                               // Hide the button because further clicks can not generate any visual changes
-                               $button.css( 'visibility', 'hidden' );
-                               $progressBar.detach();
-                               $( '.mw-changeslist-line-watched' )
-                                       .removeClass( 'mw-changeslist-line-watched' )
-                                       .addClass( 'mw-changeslist-line-not-watched' );
-                       } ).fail( function () {
-                               // On error, fall back to server-side reset
-                               // First remove this submit listener and then re-submit the form
-                               $resetForm.off( 'submit' ).submit();
-                       } );
-               } );
-
-               // if the user wishes to reload the watchlist whenever a filter changes
-               if ( mw.user.options.get( 'watchlistreloadautomatically' ) ) {
-                       // add a listener on all form elements in the header form
-                       $( '#mw-watchlist-form input, #mw-watchlist-form select' ).on( 'change', function () {
-                               // submit the form when one of the input fields is modified
-                               $( '#mw-watchlist-form' ).submit();
-                       } );
-               }
-
-               if ( mw.user.options.get( 'watchlistunwatchlinks' ) ) {
-                       // Watch/unwatch toggle link:
-                       // If a page is on the watchlist, a '×' is shown which, when clicked, removes the page from the watchlist.
-                       // After unwatching a page, the '×' becomes a '+', which if clicked re-watches the page.
-                       // Unwatched page entries are struck through and have lowered opacity.
-                       $( '.mw-changeslist' ).on( 'click', '.mw-unwatch-link, .mw-watch-link', function ( event ) {
-                               var $unwatchLink = $( this ), // EnhancedChangesList uses <table> for each row, while OldChangesList uses <li> for each row
-                                       $watchlistLine = $unwatchLink.closest( 'li, table' )
-                                               .find( '[data-target-page]' ),
-                                       pageTitle = $watchlistLine.data( 'targetPage' ),
-                                       isTalk = mw.Title.newFromText( pageTitle ).getNamespaceId() % 2 === 1;
-
-                               // Utility function for looping through each watchlist line that matches
-                               // a certain page or its associated page (e.g. Talk)
-                               function forEachMatchingTitle( title, callback ) {
-
-                                       var titleObj = mw.Title.newFromText( title ),
-                                               pageNamespaceId = titleObj.getNamespaceId(),
-                                               isTalk = pageNamespaceId % 2 === 1,
-                                               associatedTitle = mw.Title.makeTitle( isTalk ? pageNamespaceId - 1 : pageNamespaceId + 1,
-                                                       titleObj.getMainText() ).getPrefixedText();
-                                       $( '.mw-changeslist-line' ).each( function () {
-                                               var $this = $( this ), $row, $unwatchLink;
-
-                                               $this.find( '[data-target-page]' ).each( function () {
-                                                       var $this = $( this ), rowTitle = $this.data( 'targetPage' );
-                                                       if ( rowTitle === title || rowTitle === associatedTitle ) {
-
-                                                               // EnhancedChangesList groups log entries by performer rather than target page. Therefore...
-                                                               // * If using OldChangesList, use the <li>
-                                                               // * If using EnhancedChangesList and $this is part of a grouped log entry, use the <td> sub-entry
-                                                               // * If using EnhancedChangesList and $this is not part of a grouped log entry, use the <table> grouped entry
-                                                               $row =
-                                                                       $this.closest(
-                                                                               'li, table.mw-collapsible.mw-changeslist-log td[data-target-page], table' );
-                                                               $unwatchLink = $row.find( '.mw-unwatch-link, .mw-watch-link' );
-
-                                                               callback( rowTitle, $row, $unwatchLink );
-                                                       }
-                                               } );
-                                       } );
-                               }
-
-                               // Preload the notification module for mw.notify
-                               mw.loader.load( 'mediawiki.notification' );
-
-                               // Depending on whether we are watching or unwatching, for each entry of the page (and its associated page i.e. Talk),
-                               // change the text, tooltip, and non-JS href of the (un)watch button, and update the styling of the watchlist entry.
-                               if ( $unwatchLink.hasClass( 'mw-unwatch-link' ) ) {
-                                       api.unwatch( pageTitle )
-                                               .done( function () {
-                                                       forEachMatchingTitle( pageTitle,
-                                                               function ( rowPageTitle, $row, $rowUnwatchLink ) {
-                                                                       $rowUnwatchLink
-                                                                               .text( mw.msg( 'watchlist-unwatch-undo' ) )
-                                                                               .attr( 'title', mw.msg( 'tooltip-ca-watch' ) )
-                                                                               .attr( 'href',
-                                                                                       mw.util.getUrl( rowPageTitle, { action: 'watch' } ) )
-                                                                               .removeClass( 'mw-unwatch-link loading' )
-                                                                               .addClass( 'mw-watch-link' );
-                                                                       $row.find(
-                                                                               '.mw-changeslist-line-inner, .mw-enhanced-rc-nested' )
-                                                                               .addBack( '.mw-enhanced-rc-nested' ) // For matching log sub-entry
-                                                                               .addClass( 'mw-changelist-line-inner-unwatched' );
-                                                               } );
-
-                                                       mw.notify(
-                                                               mw.message( isTalk ? 'removedwatchtext-talk' : 'removedwatchtext',
-                                                                       pageTitle ), { tag: 'watch-self' } );
-                                               } );
-                               } else {
-                                       api.watch( pageTitle )
-                                               .then( function () {
-                                                       forEachMatchingTitle( pageTitle,
-                                                               function ( rowPageTitle, $row, $rowUnwatchLink ) {
-                                                                       $rowUnwatchLink
-                                                                               .text( mw.msg( 'watchlist-unwatch' ) )
-                                                                               .attr( 'title', mw.msg( 'tooltip-ca-unwatch' ) )
-                                                                               .attr( 'href',
-                                                                                       mw.util.getUrl( rowPageTitle, { action: 'unwatch' } ) )
-                                                                               .removeClass( 'mw-watch-link loading' )
-                                                                               .addClass( 'mw-unwatch-link' );
-                                                                       $row.find( '.mw-changelist-line-inner-unwatched' )
-                                                                               .addBack( '.mw-enhanced-rc-nested' )
-                                                                               .removeClass( 'mw-changelist-line-inner-unwatched' );
-                                                               } );
-
-                                                       mw.notify(
-                                                               mw.message( isTalk ? 'addedwatchtext-talk' : 'addedwatchtext',
-                                                                       pageTitle ), { tag: 'watch-self' } );
-                                               } );
-                               }
-
-                               event.preventDefault();
-                               event.stopPropagation();
-                               $unwatchLink.blur();
-                       } );
-               }
-       } );
-
-}( mediaWiki, jQuery, OO )
-);
diff --git a/resources/src/mediawiki.special/templates/thumbnail.html b/resources/src/mediawiki.special/templates/thumbnail.html
deleted file mode 100644 (file)
index bf0e701..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<div id="mw-upload-thumbnail" class="thumb tright">
-       <div class="thumbinner">
-               <div class="thumbcaption">
-                       <div class="filename"></div>
-                       <div class="fileinfo"></div>
-               </div>
-       </div>
-</div>