Merge "Allow 'all:' on all wikis in addition to 'searchall' translation"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 11 May 2018 22:05:22 +0000 (22:05 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 11 May 2018 22:05:22 +0000 (22:05 +0000)
123 files changed:
includes/DefaultSettings.php
includes/api/i18n/he.json
includes/specials/SpecialPreferences.php
languages/i18n/bg.json
languages/i18n/cv.json
languages/i18n/he.json
languages/i18n/ig.json
languages/i18n/lb.json
languages/i18n/oc.json
languages/i18n/pt-br.json
languages/i18n/sl.json
languages/i18n/tr.json
languages/i18n/zh-hant.json
package.json
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 dcf648e..18c941e 100644 (file)
@@ -7864,10 +7864,6 @@ $wgActionFilteredLogs = [
                'autocreate' => [ 'autocreate' ],
                'byemail' => [ 'byemail' ],
        ],
-       'patrol' => [
-               'patrol' => [ 'patrol' ],
-               'autopatrol' => [ 'autopatrol' ],
-       ],
        'protect' => [
                'protect' => [ 'protect' ],
                'modify' => [ 'modify' ],
index 4e59ef4..a748b52 100644 (file)
        "apihelp-query+recentchanges-param-end": "באיזה חותם זמן להפסיק לרשום.",
        "apihelp-query+recentchanges-param-namespace": "לסנן את השינויים רק למרחבי השם האלה.",
        "apihelp-query+recentchanges-param-user": "לרשום רק שינויים של המשתמש הזה.",
-       "apihelp-query+recentchanges-param-excludeuser": "Don't list changes by this user",
+       "apihelp-query+recentchanges-param-excludeuser": "לא לרשום שינויים ממשתמש זה.",
        "apihelp-query+recentchanges-param-tag": "לרשום רק שינויים שמתויגים עם התג הזה.",
        "apihelp-query+recentchanges-param-prop": "לכלול פריטי מידע נוספים:",
        "apihelp-query+recentchanges-paramvalue-prop-user": "הוספת המשתמש האחראי על העריכה ותיוג אם זאת כתובת IP.",
        "apihelp-query+siteinfo-paramvalue-prop-namespacealiases": "רשימת כינויי מרחבי שם רשומים.",
        "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "רשימת כינויים דפים מיוחדים.",
        "apihelp-query+siteinfo-paramvalue-prop-magicwords": "רשימות מילות קסם וכינוייהן.",
-       "apihelp-query+siteinfo-paramvalue-prop-statistics": "×\94×\97×\96ר×\96ת ×¡×\98×\98×\99ס×\98×\99ק×\95ת אתר.",
+       "apihelp-query+siteinfo-paramvalue-prop-statistics": "×\94×\97×\96רת ×¡×\98×\98×\99ס×\98×\99ק×\95ת ×©×\9c ×\94אתר.",
        "apihelp-query+siteinfo-paramvalue-prop-interwikimap": "החזרת מפת בינוויקי (אפשר שתהיה מסוננת, אפשר שתהיה מותאמת מקומית באמצעות <var>$1inlanguagecode</var>).",
        "apihelp-query+siteinfo-paramvalue-prop-dbrepllag": "החזרת שרת מסד־נתונים עם שיהוי השכפול הגבוה ביותר.",
        "apihelp-query+siteinfo-paramvalue-prop-usergroups": "החזרת קבוצות משתמשים וההרשאות המשויכות.",
        "apihelp-query+watchlist-param-end": "באיזה חותם זמן להפסיק לרשום.",
        "apihelp-query+watchlist-param-namespace": "סינון שינויים רק למרחבי השם שניתנו.",
        "apihelp-query+watchlist-param-user": "לרשום רק שינויים של המשתמש הזה.",
-       "apihelp-query+watchlist-param-excludeuser": "Don't list changes by this user",
+       "apihelp-query+watchlist-param-excludeuser": "לא לרשום שינויים ממשתמש זה.",
        "apihelp-query+watchlist-param-limit": "כמה תוצאות סך הכול להחזיר בכל בקשה.",
        "apihelp-query+watchlist-param-prop": "אילו מאפיינים נוספים לקבל:",
        "apihelp-query+watchlist-paramvalue-prop-ids": "הוספת מזהי גסה ומזהי דף.",
        "apihelp-rollback-param-title": "שם הדף לשחזור. לא יכול לשמש יחד עם <var>$1pageid</var>.",
        "apihelp-rollback-param-pageid": "מזהה הדף לשחזור. לא יכול לשמש יחד עם <var>$1title</var>.",
        "apihelp-rollback-param-tags": "אילו תגים להחיל על השחזור.",
-       "apihelp-rollback-param-user": "שם המשתמשים שהעריכות שלו תשוחזרנה.",
+       "apihelp-rollback-param-user": "שם המשתמש שהעריכות שלו תשוחזרנה.",
        "apihelp-rollback-param-summary": "תקציר עריכה מותאם. אם ריק, ישמש תקציר לפי בררת מחדל.",
        "apihelp-rollback-param-markbot": "לסמן את העריכות ששוחזרו ואת השחזור בתור עריכות בוט.",
        "apihelp-rollback-param-watchlist": "הוספה או הסרה של הדף ללא תנאי מרשימת המעקב של המשתמש הנוכחי, להשתמש בהעדפות או לא לשנות את המעקב.",
        "apihelp-setnotificationtimestamp-example-page": "אתחול מצב ההודעה עבור <kbd>Main Page</kbd>.",
        "apihelp-setnotificationtimestamp-example-pagetimestamp": "הגדרת חותם־הזמן להודעה ל־<kbd>Main page</kbd> כך שכל העריכות מאז 1 בינואר 2012 מוגדרות בתור כאלה שלא נצפו.",
        "apihelp-setnotificationtimestamp-example-allpages": "אתחול מצב ההודעה עבור דפים במרחב השם <kbd>{{ns:user}}</kbd>.",
-       "apihelp-setpagelanguage-summary": "שנ×\94 ×\90ת ×\94שפ×\94 ×©×\9c ×\93×£",
-       "apihelp-setpagelanguage-extended-description-disabled": "ש×\99× ×\95×\99 ×\94שפ×\94 ×©×\9c ×\93×£ ×\9c×\90 ×\9e×\95רש×\94 ×\91×\95×\95×\99ק×\99 ×\96×\94.\n\n×\94פע×\9c ×\90ת <var>[[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> ×¢×\9c ×\9eנת ×\9c×\94שת×\9eש ×\91פע×\95×\9c×\94 ×\96×\95",
+       "apihelp-setpagelanguage-summary": "ש×\99× ×\95×\99 ×\94שפ×\94 ×©×\9c ×\93×£.",
+       "apihelp-setpagelanguage-extended-description-disabled": "×\9c×\90 × ×\99ת×\9f ×\9cשנ×\95ת ×©×¤×\95ת ×©×\9c ×\93פ×\99×\9d ×\91×\90תר ×\94×\95×\95×\99ק×\99 ×\94×\96×\94.\n\n×\99ש ×\9c×\94פע×\99×\9c ×\90ת <var dir=\"ltr\">[[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> ×¢×\9cÖ¾×\9eנת ×\9c×\94שת×\9eש ×\91פע×\95×\9c×\94 ×\96×\95.",
        "apihelp-setpagelanguage-param-title": "כותרת הדף שאת שפתו ברצונך לשנות. לא אפשרי להשתמש באפשרות עם <var>$1pageid</var>.",
        "apihelp-setpagelanguage-param-pageid": "מזהה הדף שאת שפתו ברצונך לשנות. לא אפשרי להשתמש באפשרות עם <var>$1title</var>.",
        "apihelp-setpagelanguage-param-lang": "קוד השפה של השפה שאליה צריך לשנות את הדף. יש להשתמש ב־<kbd>default</kbd> כדי לאתחל את הדף לשפת בררת המחדל של הוויקי.",
        "apihelp-phpfm-summary": "לפלוט נתונים בתסדיר PHP מוסדר (עם הדפסה יפה ב־HTML).",
        "apihelp-rawfm-summary": "לפלוט את הנתונים, כולל אלמנטים לניפוי שגיאות, בתסדיר JSON (עם הדפסה יפה ב־HTML).",
        "apihelp-xml-summary": "לפלוט נתונים בתסדיר XML.",
-       "apihelp-xml-param-xslt": "×\90×\9d ×¦×\95×\99×\9f, ×\99ש ×\9c×\94×\95ס×\99×£ ×\90ת ×©×\9d ×\94×\93×£ ×\9b×\92×\99×\9c×\99×\95×\9f ×¢×\99צ×\95×\91 XSL. ×¢×\9c ×\94ער×\9a ×\9c×\94×\99×\95ת ×\9b×\95תרת ×\91 {{ns:MediaWiki}} ×\91×\9eר×\97×\91 ×©×\9d ×\94×\9eשת×\9eש, ×\94×\9eסת×\99×\99×\9d ×\91-  <code>.xsl</code>.",
+       "apihelp-xml-param-xslt": "×\90×\9d ×¦×\95×\99×\9f, ×\94×\93×£ ×\99ת×\95×\95סף ×\9b×\92×\99×\9c×\99×\95×\9f XSL. ×\94ער×\9a ×\97×\99×\99×\91 ×\9c×\94×\99×\95ת ×\9b×\95תרת ×\91×\9eר×\97×\91 ×\94ש×\9d \"{{ns:MediaWiki}}\" ×©×\9eסת×\99×\99×\9eת ×\91Ö¾<code dir=\"ltr\">.xsl</code>.",
        "apihelp-xml-param-includexmlnamespace": "אם זה צוין, מוסיף מרחב שם של XML.",
        "apihelp-xmlfm-summary": "לפלוט נתונים בתסדיר XML (עם הדפסה יפה ב־HTML).",
        "api-format-title": "תוצאה של API של מדיה־ויקי",
index f67fe9f..1cfcffa 100644 (file)
@@ -36,8 +36,6 @@ class SpecialPreferences extends SpecialPage {
 
        function __construct() {
                parent::__construct( 'Preferences' );
-
-               $this->oouiEnabled = self::isOouiEnabled( $this->getContext() );
        }
 
        /**
@@ -56,6 +54,8 @@ class SpecialPreferences extends SpecialPage {
        }
 
        public function execute( $par ) {
+               $this->oouiEnabled = static::isOouiEnabled( $this->getContext() );
+
                $this->setHeaders();
                $this->outputHeader();
                $out = $this->getOutput();
index b4b31f2..2d55d32 100644 (file)
        "filedelete-intro-old": "Изтривате версията на <strong>[[Media:$1|$1]]</strong> към [$4 $3, $2].",
        "filedelete-comment": "Причина:",
        "filedelete-submit": "Изтриване",
-       "filedelete-success": "Файлът '''$1''' беше изтрит.",
+       "filedelete-success": "Файлът <strong>$1</strong> беше изтрит.",
        "filedelete-success-old": "Версията на <strong>[[Media:$1|$1]]</strong> към $3, $2 е била изтрита.",
        "filedelete-nofile": "Файлът <strong>$1</strong> не съществува.",
-       "filedelete-nofile-old": "Не съществува архивна версия на '''$1''' с указаните параметри.",
+       "filedelete-nofile-old": "Не съществува архивна версия на <strong>$1</strong> с указаните параметри.",
        "filedelete-otherreason": "Друга/допълнителна причина:",
        "filedelete-reason-otherlist": "Друга причина",
        "filedelete-reason-dropdown": "*Общи причини за изтриване\n** Нарушение на авторските права\n** Файлът се повтаря",
        "protect-othertime": "Друг срок:",
        "protect-othertime-op": "друг срок",
        "protect-existing-expiry": "Оставащо време: $2, $3",
-       "protect-existing-expiry-infinity": "Existing expiration time: безсрочно",
+       "protect-existing-expiry-infinity": "Оставащо време: безсрочно",
        "protect-otherreason": "Друга/допълнителна причина:",
        "protect-otherreason-op": "Друга причина",
        "protect-dropdown": "* Стандартни причини за защита на страници\n** Чест обект на вандализъм\n** Чест обект на спам\n** Редакторска война\n** Страница, изискваща много сървърни ресурси",
        "restriction-level-all": "всички",
        "undelete": "Преглед на изтрити страници",
        "undeletepage": "Преглед и възстановяване на изтрити страници",
-       "undeletepagetitle": "'''По-долу е показан списък на изтритите версии на [[:$1|$1]]'''.",
+       "undeletepagetitle": "<strong>По-долу е показан списък на изтритите версии на [[:$1|$1]]/strong>.",
        "viewdeletedpage": "Преглед на изтрити страници",
        "undeletepagetext": "{{PLURAL:$1|Следната страница беше изтрита, но все още се намира в архива и може да бъде възстановена|Следните $1 страници бяха изтрити, но все още се намират в архива и могат да бъдат възстановени}}. Архивът може да се почиства от време на време.",
        "undelete-fieldset-title": "Възстановяване на версии",
index 946c6c9..4de9d8c 100644 (file)
        "tooltip-pt-logout": "Сеансне пĕтер",
        "tooltip-pt-createaccount": "Аккаунт ту та системӑна кӗр. Паллах, унсӑрах та юрать, анчах та аккаунтпа кӗни лайӑхрах.",
        "tooltip-ca-talk": "Статьяна сӳтсе явасси",
-       "tooltip-ca-edit": "Эле тӳрлет",
+       "tooltip-ca-edit": "Ð\9aÄ\83на тӳрлет",
        "tooltip-ca-addsection": "Çĕнĕ пай ту",
        "tooltip-ca-viewsource": "Ку страницӑна эсир улӑштарма пултараймастӑр. Ӑна мӗнле ҫырнине кӑна пӑхма пултаратӑр.",
-       "tooltip-ca-history": "Эле Ñ\83лÓ\91Ñ\88Ñ\82аÑ\80нин ÐºÑ\83н-Ò«Ñ\83лÓ\97",
+       "tooltip-ca-history": "Ð\9aÑ\83нÄ\83н Ñ\83лÓ\91Ñ\88Ä\83нниÑ\81ем",
        "tooltip-ca-protect": "Улӑшратусенчен сыхласси",
        "tooltip-ca-delete": "Страницӑна кӑларса пӑрахмалли",
        "tooltip-ca-move": "Страницӑна урӑх ҫӗре куҫарасси",
index 0946de2..a7b3738 100644 (file)
        "listduplicatedfiles-summary": "זוהי רשימה של קבצים שהגרסה החדשה ביותר שלהם זהה לגרסה החדשה ביותר של קובץ אחר כלשהו. רק קבצים מקומיים נבדקים לצורך זה.",
        "listduplicatedfiles-entry": "לקובץ [[:File:$1|$1]] יש [[$3|{{PLURAL:$2|עותק זהה|$2 עותקים זהים}}]].",
        "unusedtemplates": "תבניות שאינן בשימוש",
-       "unusedtemplatestext": "דף זה מכיל רשימה של כל הדפים במרחב השם {{ns:template}} שאינם נכללים בדף אחר. אנא זכרו לבדוק את הקישורים האחרים לתבניות לפני שתמחקו אותן.",
+       "unusedtemplatestext": "דף זה מכיל רשימה של כל הדפים במרחב השם \"{{ns:template}}\" שאינם נכללים בדף אחר.\nיש לזכור לבדוק את הקישורים האחרים לתבניות לפני מחיקתן.",
        "unusedtemplateswlh": "קישורים אחרים",
        "randompage": "דף אקראי",
        "randompage-nopages": "אין דפים {{PLURAL:$2|במרחב השם הבא|במרחבי השם הבאים}}: $1.",
        "brokenredirects-edit": "עריכה",
        "brokenredirects-delete": "מחיקה",
        "withoutinterwiki": "דפים ללא קישורי שפה",
-       "withoutinterwiki-summary": "הדפים הבאים אינם מקשרים לגרסאות שלהם בשפות אחרות:",
+       "withoutinterwiki-summary": "הדפים הבאים אינם מקשרים לגרסאות שלהם בשפות אחרות.",
        "withoutinterwiki-legend": "תחילית",
        "withoutinterwiki-submit": "הצגה",
        "fewestrevisions": "הדפים בעלי מספר העריכות הנמוך ביותר",
        "nbytes": "{{PLURAL:$1|בית אחד|$1 בתים}}",
        "ncategories": "{{PLURAL:$1|קטגוריה אחת|$1 קטגוריות}}",
-       "ninterwikis": "{{PLURAL:$1|ק×\99ש×\95ר ×\91×\99× ×\95×\95×\99ק×\99 ×§חד|$1 קישורי בינוויקי}}",
+       "ninterwikis": "{{PLURAL:$1|ק×\99ש×\95ר ×\91×\99× ×\95×\95×\99ק×\99 ×\90חד|$1 קישורי בינוויקי}}",
        "nlinks": "{{PLURAL:$1|קישור אחד|$1 קישורים}}",
        "nmembers": "{{PLURAL:$1|דף אחד|$1 דפים}}",
        "nmemberschanged": "$1 ← {{PLURAL:$2|חבר אחד|$2 חברים}}",
        "nrevisions": "{{PLURAL:$1|גרסה אחת|$1 גרסאות}}",
        "nimagelinks": "בשימוש {{PLURAL:$1|בדף אחד|ב־$1 דפים}}",
        "ntransclusions": "בשימוש {{PLURAL:$1|בדף אחד|ב־$1 דפים}}",
-       "specialpage-empty": "אין תוצאות.",
+       "specialpage-empty": "אין תוצאות בדיווח התחזוקה הזה.",
        "lonelypages": "דפים יתומים",
-       "lonelypagestext": "×\94×\93פ×\99×\9d ×\94×\91×\90×\99×\9d ×\90×\99× ×\9d ×\9eק×\95שר×\99×\9d ×\95×\90×\99× ×\9d ×\9e×\95×\9b×\9c×\9c×\99×\9d ×\91×\93פ×\99×\9d ×\90×\97ר×\99×\9d ×\91×\90תר {{SITENAME}}.",
+       "lonelypagestext": "×\94×\93פ×\99×\9d ×\94×\91×\90×\99×\9d ×\90×\99× ×\9d ×\9eק×\95שר×\99×\9d ×\9e×\93פ×\99×\9d ×\90×\97ר×\99×\9d ×\91{{GRAMMAR:ת×\97×\99×\9c×\99ת|{{SITENAME}}}} ×\95×\90×\99× ×\9d ×\9e×\95×\9b×\9c×\9c×\99×\9d ×\91×\94×\9d.",
        "uncategorizedpages": "דפים חסרי קטגוריה",
        "uncategorizedcategories": "קטגוריות חסרות קטגוריה",
        "uncategorizedimages": "קבצים חסרי קטגוריה",
index 9674bdc..3e4ed63 100644 (file)
        "special-characters-group-latin": "Latin",
        "special-characters-group-latinextended": "Latin dọsàrà",
        "special-characters-group-ipa": "IPA",
-       "special-characters-group-symbols": "Nkárí",
+       "special-characters-group-symbols": "Akàrà",
        "special-characters-group-greek": "Greek",
        "special-characters-group-cyrillic": "Cyrillic",
        "special-characters-group-arabic": "Arabiki",
index 22d4b62..d3bc002 100644 (file)
        "recentchangeslinked-feed": "Ännerungen op verlinkt Säiten",
        "recentchangeslinked-toolbox": "Ännerungen op verlinkt Säiten",
        "recentchangeslinked-title": "Ännerungen a Verbindung mat \"$1\"",
-       "recentchangeslinked-summary": "Gitt den Numm vun enger Säit a fir Ännerungen Säiten ze gesinn op déi oder vun deene gelinkt gëtt. Ännerungen op Säite vun [[Special:Watchlist|Ärer Iwwerwaachungslëscht]] si <strong>fett</strong> geschriwwen.",
+       "recentchangeslinked-summary": "Gitt den Numm vun enger Säit a fir Ännerungen op Säiten ze gesinn op déi oder vun deene gelinkt gëtt. (Fir d'Membere vun enger Kategorie ze gesinn gitt {{ns:category}}:Numm vun der Kategorie, an.) Ännerungen op Säite vun [[Special:Watchlist|Ärer Iwwerwaachungslëscht]] si <strong>fett</strong> geschriwwen.",
        "recentchangeslinked-page": "Säitennumm:",
        "recentchangeslinked-to": "Weis Ännerungen zu de verlinkte Säiten aplaz vun der gefroter Säit",
        "recentchanges-page-added-to-category": "[[:$1]] an d'Kategorie dobäigesat",
index f11e4fd..d1dbadb 100644 (file)
        "content-json-empty-object": "Objècte void",
        "content-json-empty-array": "Tablèu void",
        "duplicate-args-category": "Paginas utilizant d'arguments duplicats dins los apèls de modèl",
+       "duplicate-args-category-desc": "La pagina conten de cridas a patrons qu'emplagan d'arguments duplicats, coma  <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> or <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
        "expensive-parserfunction-warning": "Atencion : Aquesta pagina conten tròp d’apèls dispendioses de foncions del parser.\n\nI deurià aver mens de {{PLURAL:$2|ampèl|ampèls}}, e actualament {{PLURAL:$1|i a $1 ampèl|i a $1 ampèls}}..",
        "expensive-parserfunction-category": "Paginas amb tròp d’apèls dispendioses de foncions parsaires",
        "post-expand-template-inclusion-warning": "Atencion : Aquesta pagina conten tròp d'inclusions de modèls.\nD'unas inclusions seràn pas efectuadas.",
        "post-expand-template-argument-warning": "Atencion : Aquesta pagina conten al mens un paramètre de modèl que l'inclusion es renduda impossibla. Aprèp extension, aqueste auriá produit un resultat tròp long, doncas, es pas estat inclús.",
        "post-expand-template-argument-category": "Paginas que contenon al mens un paramètre de modèl pas evaluat",
        "parser-template-loop-warning": "Modèl en bocla detectat : [[$1]]",
+       "template-loop-category": "Paginas amb boclas de patron",
+       "template-loop-category-desc": "La pagina conten una bocla dins lo patron, es a dire, un patron que se sona el meteis recursivament.",
+       "template-loop-warning": "<strong>Attencion:</strong> Aquesta pagina sona [[:$1]. Aquò es l'encausa d'una bocla de patron (una sonada infinida resursiva).",
        "parser-template-recursion-depth-warning": "Limit de longor de la recursion del modèl depassat ($1)",
        "language-converter-depth-warning": "Limit de prigondor del convertissor de lenga depassada ($1)",
        "node-count-exceeded-category": "Paginas ont nombre de nosèls es depassat",
        "prefs-watchlist-edits": "Nombre maximal de modificacions d'afichar dins la lista de seguiment :",
        "prefs-watchlist-edits-max": "Nombre maximum : 1000",
        "prefs-watchlist-token": "Geton per la lista de seguiment :",
+       "prefs-watchlist-managetokens": "Administrar los getons",
        "prefs-misc": "Preferéncias divèrsas",
        "prefs-resetpass": "Modificar lo senhal",
        "prefs-changeemail": "Cambiar o suprimir l'adreça electronica",
        "recentchangescount": "Nombre de modificacions d'afichar per defauta dins los cambiaments recents, los istorics e los logs :",
        "prefs-help-recentchangescount": "Nombre maximum : 1000",
        "prefs-help-watchlist-token2": "Aquí la clau secreta del flux Web de vòstra lista de seguiment.\nTota persona que la coneis poirà legir vòstra lista de seguiment, doncas, la comuniquetz pas.\nSe necessari, [[Special:ResetTokens|clicatz aicí per la reïnicializar]].",
+       "prefs-help-tokenmanagement": "Podètz veire e tornar inicializar la clau secreta del vòstre compte que pòt accedir al flux Web de la vòstre lista de seguit. Tota persona que coneis la clau poirà legir la vòstra lista, alara la compartissètz pas",
        "savedprefs": "Las preferéncias son estadas salvadas.",
        "savedrights": "Los dreits d'utilizaire de {{GENDER:$1|$1}} son estats enregistrats.",
        "timezonelegend": "Fus orari :",
index 44ded2b..7de9e7c 100644 (file)
        "botpasswords-restriction-failed": "Restrições de senha de robô evitam esta autenticação.",
        "botpasswords-invalid-name": "O nome de usuário especificado não contém o separador de senha de robô (\"$1\").",
        "botpasswords-not-exist": "O usuário \"$1\" não possui uma senha de robô \"$2\".",
+       "botpasswords-needs-reset": "A palavra-passe de robô, para o robô de nome \"$2\" {{GENDER:$1|do usuário|da usuária}} \"$1\" deve ser redefinida.",
        "resetpass_forbidden": "As senhas não podem ser alteradas",
        "resetpass_forbidden-reason": "Senhas não podem ser alteradas: $1",
        "resetpass-no-info": "Você precisa estar autenticado para acessar esta página diretamente.",
        "recentchangescount": "Número de edições a apresentar por omissão nas mudanças recentes, nos historiais de páginas e nos registos:",
        "prefs-help-recentchangescount": "Número máximo: 1000",
        "prefs-help-watchlist-token2": "Esta é a senha secreta para o feed da Web com sua lista de tokens vigiados.\nQualquer pessoa que descobrir esta senha será capaz de ler sua lista, então não a compartilhe.\nSe você precisar [[Special:ResetTokens|você pode redefini-lo]].",
-       "prefs-help-tokenmanagement": "Você pode ver e redefinir a chave secreta para sua conta que pode acessar o feed da Web da sua lista de vigilância. Qualquer pessoa que conheça a chave poderá ler sua lista de observação, então não compartilhe.",
+       "prefs-help-tokenmanagement": "Pode ver e repor a chave secreta da sua conta que permite aceder ao feed da sua lista de páginas vigiadas. Qualquer pessoa que conheça a chave será capaz de ler a sua lista de páginas vigiadas, por isso não a partilhe.",
        "savedprefs": "As suas preferências foram salvas.",
        "savedrights": "Os grupos {{GENDER:$1|do usuário|da usuária}} $1 foram gravados.",
        "timezonelegend": "Fuso horário:",
        "recentchangeslinked-feed": "Mudanças relacionadas",
        "recentchangeslinked-toolbox": "Mudanças relacionadas",
        "recentchangeslinked-title": "Mudanças relacionadas com “$1”",
-       "recentchangeslinked-summary": "Digite um nome de página para ver as alterações nas páginas vinculadas ou a partir dessa página. (Para ver membros de uma categoria, digite Categoria: Nome da categoria). Mudanças nas páginas em [[Special:Watchlist|lista de páginas vigiadas]] são exibidas em <strong>negrito<strong>",
+       "recentchangeslinked-summary": "Introduza o nome de uma página para ver as mudanças a todas as páginas que contêm hiperligações para ela ou para as quais a página fornecida contém hiperligações (para ver as que pertencem a uma categoria, introduza {{ns:category}}:Nome da categoria). As mudanças às suas [[Special:Watchlist|páginas vigiadas]] aparecem a <strong>negrito</strong>.",
        "recentchangeslinked-page": "Nome da página:",
        "recentchangeslinked-to": "Inversamente, mostrar mudanças nas páginas que contêm ligações para esta",
        "recentchanges-page-added-to-category": "[[:$1]]adicionada à categoria",
index f3e9296..4858907 100644 (file)
        "botpasswords-existing": "Obstoječa gesla botov",
        "botpasswords-createnew": "Ustvari novo geslo bota",
        "botpasswords-editexisting": "Uredi obstoječe geslo bota",
+       "botpasswords-label-needsreset": "(geslo mora biti ponastavljeno)",
        "botpasswords-label-appid": "Ime bota:",
        "botpasswords-label-create": "Ustvari",
        "botpasswords-label-update": "Posodobi",
        "botpasswords-restriction-failed": "Omejitve gesla bota preprečujejo to prijavo.",
        "botpasswords-invalid-name": "Navedeno uporabniško ime ne vsebuje ločila za geslo bota (»$1«).",
        "botpasswords-not-exist": "Uporabnik »$1« nima gesla bota z imenom »$2«.",
+       "botpasswords-needs-reset": "Geslo bota »$2« {{GENDER:$1|uporabnika|uporabnice}} »$1« mora biti ponastavljeno.",
        "resetpass_forbidden": "Gesla ne morete spremeniti",
        "resetpass_forbidden-reason": "Gesel nismo mogli spremeniti: $1",
        "resetpass-no-info": "Za neposreden dostop do te strani morate biti prijavljeni.",
index 0226e5c..db9fe96 100644 (file)
        "botpasswords-existing": "Mevcut bot parolaları",
        "botpasswords-createnew": "Yeni bir bot parolası oluştur",
        "botpasswords-editexisting": "Mevcut bir bot parolasını düzenle",
+       "botpasswords-label-needsreset": "(parolanın sıfırlanması gerekiyor)",
        "botpasswords-label-appid": "Bot ismi:",
        "botpasswords-label-create": "Oluştur",
        "botpasswords-label-update": "Güncelle",
        "botpasswords-no-provider": "BotPasswordsSessionProvider kullanılamaz.",
        "botpasswords-restriction-failed": "Bot parolası kısıtlamaları bu oturum açma işlemini önlemektedir.",
        "botpasswords-invalid-name": "Belirtilen kullanıcı adı bot parolası ayırıcısı içermiyor (\"$1\").",
+       "botpasswords-needs-reset": "\"$1\" {{GENDER:$1|kullanıcısına}} ait \"$2\" adlı bot için bot parolası sıfırlanmalı.",
        "resetpass_forbidden": "Parolalar değiştirilememektedir",
        "resetpass_forbidden-reason": "Parolalar değiştirilemez: $1",
        "resetpass-no-info": "Bu sayfaya doğrudan erişmek için oturum açmanız gereklidir.",
index 3a14a15..deeee82 100644 (file)
        "botpasswords-existing": "已存在機器人密碼",
        "botpasswords-createnew": "建立新機器人密碼",
        "botpasswords-editexisting": "編輯已存在的機器人密碼",
+       "botpasswords-label-needsreset": "(密碼需要重新設定)",
        "botpasswords-label-appid": "機器人名稱:",
        "botpasswords-label-create": "建立",
        "botpasswords-label-update": "更新",
        "prefs-watchlist-edits": "監視清單中顯示的變更數量上限:",
        "prefs-watchlist-edits-max": "數量上限:1000",
        "prefs-watchlist-token": "監視清單金鑰:",
+       "prefs-watchlist-managetokens": "管理令牌",
        "prefs-misc": "其他",
        "prefs-resetpass": "變更密碼",
        "prefs-changeemail": "變更或移除電子郵件地址",
index f928f09..1f23b1a 100644 (file)
@@ -15,7 +15,7 @@
     "grunt": "1.0.1",
     "grunt-banana-checker": "0.6.0",
     "grunt-contrib-copy": "1.0.0",
-    "grunt-contrib-watch": "1.0.0",
+    "grunt-contrib-watch": "1.0.1",
     "grunt-eslint": "20.1.0",
     "grunt-jsonlint": "1.1.0",
     "grunt-karma": "2.0.0",
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>