summaryrefslogtreecommitdiff
path: root/resources/src
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2014-12-27 15:41:37 +0100
committerPierre Schmitz <pierre@archlinux.de>2014-12-31 11:43:28 +0100
commitc1f9b1f7b1b77776192048005dcc66dcf3df2bfb (patch)
tree2b38796e738dd74cb42ecd9bfd151803108386bc /resources/src
parentb88ab0086858470dd1f644e64cb4e4f62bb2be9b (diff)
Update to MediaWiki 1.24.1
Diffstat (limited to 'resources/src')
-rw-r--r--resources/src/es5-skip.js18
-rw-r--r--resources/src/jquery.json-deprecate.js8
-rw-r--r--resources/src/jquery.tipsy/images/tipsy.pngbin0 -> 133 bytes
-rw-r--r--resources/src/jquery.tipsy/jquery.tipsy.css73
-rw-r--r--resources/src/jquery.tipsy/jquery.tipsy.js259
-rw-r--r--resources/src/jquery/images/jquery.arrowSteps.divider-ltr.pngbin0 -> 126 bytes
-rw-r--r--resources/src/jquery/images/jquery.arrowSteps.divider-rtl.pngbin0 -> 127 bytes
-rw-r--r--resources/src/jquery/images/jquery.arrowSteps.head-ltr.pngbin0 -> 303 bytes
-rw-r--r--resources/src/jquery/images/jquery.arrowSteps.head-rtl.pngbin0 -> 311 bytes
-rw-r--r--resources/src/jquery/images/jquery.arrowSteps.tail-ltr.pngbin0 -> 222 bytes
-rw-r--r--resources/src/jquery/images/jquery.arrowSteps.tail-rtl.pngbin0 -> 219 bytes
-rw-r--r--resources/src/jquery/images/marker.pngbin0 -> 472 bytes
-rw-r--r--resources/src/jquery/images/mask.pngbin0 -> 1795 bytes
-rw-r--r--resources/src/jquery/images/sort_both.gifbin0 -> 1184 bytes
-rw-r--r--resources/src/jquery/images/sort_down.gifbin0 -> 1174 bytes
-rw-r--r--resources/src/jquery/images/sort_none.gifbin0 -> 462 bytes
-rw-r--r--resources/src/jquery/images/sort_up.gifbin0 -> 1174 bytes
-rw-r--r--resources/src/jquery/images/spinner-large.gifbin0 -> 1788 bytes
-rw-r--r--resources/src/jquery/images/spinner.gifbin0 -> 1819 bytes
-rw-r--r--resources/src/jquery/images/wheel.pngbin0 -> 11505 bytes
-rw-r--r--resources/src/jquery/jquery.accessKeyLabel.js200
-rw-r--r--resources/src/jquery/jquery.arrowSteps.css45
-rw-r--r--resources/src/jquery/jquery.arrowSteps.js98
-rw-r--r--resources/src/jquery/jquery.autoEllipsis.js168
-rw-r--r--resources/src/jquery/jquery.badge.css36
-rw-r--r--resources/src/jquery/jquery.badge.js88
-rw-r--r--resources/src/jquery/jquery.byteLength.js40
-rw-r--r--resources/src/jquery/jquery.byteLimit.js235
-rw-r--r--resources/src/jquery/jquery.checkboxShiftClick.js43
-rw-r--r--resources/src/jquery/jquery.client.js301
-rw-r--r--resources/src/jquery/jquery.color.js55
-rw-r--r--resources/src/jquery/jquery.colorUtil.js262
-rw-r--r--resources/src/jquery/jquery.confirmable.css28
-rw-r--r--resources/src/jquery/jquery.confirmable.js170
-rw-r--r--resources/src/jquery/jquery.confirmable.mediawiki.js14
-rw-r--r--resources/src/jquery/jquery.expandableField.js140
-rw-r--r--resources/src/jquery/jquery.farbtastic.css54
-rw-r--r--resources/src/jquery/jquery.farbtastic.js286
-rw-r--r--resources/src/jquery/jquery.footHovzer.css6
-rw-r--r--resources/src/jquery/jquery.footHovzer.js66
-rw-r--r--resources/src/jquery/jquery.getAttrs.js42
-rw-r--r--resources/src/jquery/jquery.hidpi.js129
-rw-r--r--resources/src/jquery/jquery.highlightText.js73
-rw-r--r--resources/src/jquery/jquery.localize.js170
-rw-r--r--resources/src/jquery/jquery.makeCollapsible.css27
-rw-r--r--resources/src/jquery/jquery.makeCollapsible.js404
-rw-r--r--resources/src/jquery/jquery.mw-jump.js15
-rw-r--r--resources/src/jquery/jquery.mwExtension.js122
-rw-r--r--resources/src/jquery/jquery.placeholder.js229
-rw-r--r--resources/src/jquery/jquery.qunit.completenessTest.js305
-rw-r--r--resources/src/jquery/jquery.spinner.css40
-rw-r--r--resources/src/jquery/jquery.spinner.js112
-rw-r--r--resources/src/jquery/jquery.suggestions.css76
-rw-r--r--resources/src/jquery/jquery.suggestions.js684
-rw-r--r--resources/src/jquery/jquery.tabIndex.js57
-rw-r--r--resources/src/jquery/jquery.tablesorter.css17
-rw-r--r--resources/src/jquery/jquery.tablesorter.js1161
-rw-r--r--resources/src/jquery/jquery.textSelection.js572
-rw-r--r--resources/src/json-skip.js4
-rw-r--r--resources/src/mediawiki.action/images/green-checkmark.pngbin0 -> 681 bytes
-rw-r--r--resources/src/mediawiki.action/images/nextredirect-ltr.pngbin0 -> 121 bytes
-rw-r--r--resources/src/mediawiki.action/images/nextredirect-rtl.pngbin0 -> 121 bytes
-rw-r--r--resources/src/mediawiki.action/images/redirect-ltr.pngbin0 -> 128 bytes
-rw-r--r--resources/src/mediawiki.action/images/redirect-rtl.pngbin0 -> 132 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css17
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js54
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.css18
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js59
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.js217
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.preview.js165
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.styles.css44
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_bold.pngbin0 -> 533 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_headline.pngbin0 -> 484 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_italic.pngbin0 -> 532 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_link.pngbin0 -> 557 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_nowiki.pngbin0 -> 874 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_bold.pngbin0 -> 550 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_italic.pngbin0 -> 539 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_link.pngbin0 -> 419 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_bold.pngbin0 -> 255 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_italic.pngbin0 -> 260 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_bold.pngbin0 -> 250 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_extlink.pngbin0 -> 435 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_headline.pngbin0 -> 440 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_hr.pngbin0 -> 200 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_image.pngbin0 -> 483 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_italic.pngbin0 -> 250 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_link.pngbin0 -> 280 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_media.pngbin0 -> 728 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_nowiki.pngbin0 -> 322 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_sig.pngbin0 -> 920 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_bold.pngbin0 -> 459 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_headline.pngbin0 -> 392 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_italic.pngbin0 -> 512 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_link.pngbin0 -> 485 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_nowiki.pngbin0 -> 874 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/LICENSE7
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/button_italic.pngbin0 -> 368 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/LICENSE17
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_bold.pngbin0 -> 254 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_italic.pngbin0 -> 423 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_link.pngbin0 -> 278 bytes
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.edit.toolbar/mediawiki.action.edit.toolbar.less42
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.history.css4
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.history.diff.css96
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.history.js111
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.dblClickEdit.js12
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.metadata.css6
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.metadata.js45
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.postEdit.css76
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.postEdit.js86
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.redirect.js65
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.redirectPage.css53
-rw-r--r--resources/src/mediawiki.action/mediawiki.action.view.rightClickEdit.js26
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.category.js146
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.edit.js87
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.js397
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.login.js54
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.parse.js45
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.watch.js79
-rw-r--r--resources/src/mediawiki.hidpi-skip.js4
-rw-r--r--resources/src/mediawiki.language/languages/bs.js19
-rw-r--r--resources/src/mediawiki.language/languages/dsb.js19
-rw-r--r--resources/src/mediawiki.language/languages/fi.js47
-rw-r--r--resources/src/mediawiki.language/languages/ga.js38
-rw-r--r--resources/src/mediawiki.language/languages/he.js29
-rw-r--r--resources/src/mediawiki.language/languages/hsb.js19
-rw-r--r--resources/src/mediawiki.language/languages/hu.js23
-rw-r--r--resources/src/mediawiki.language/languages/hy.js29
-rw-r--r--resources/src/mediawiki.language/languages/la.js50
-rw-r--r--resources/src/mediawiki.language/languages/os.js69
-rw-r--r--resources/src/mediawiki.language/languages/ru.js57
-rw-r--r--resources/src/mediawiki.language/languages/sl.js19
-rw-r--r--resources/src/mediawiki.language/languages/uk.js37
-rw-r--r--resources/src/mediawiki.language/mediawiki.cldr.js32
-rw-r--r--resources/src/mediawiki.language/mediawiki.language.fallback.js35
-rw-r--r--resources/src/mediawiki.language/mediawiki.language.init.js81
-rw-r--r--resources/src/mediawiki.language/mediawiki.language.js171
-rw-r--r--resources/src/mediawiki.language/mediawiki.language.months.js56
-rw-r--r--resources/src/mediawiki.language/mediawiki.language.numbers.js258
-rw-r--r--resources/src/mediawiki.legacy/ajax.js194
-rw-r--r--resources/src/mediawiki.legacy/commonPrint.css435
-rw-r--r--resources/src/mediawiki.legacy/images/ajax-loader.gifbin0 -> 1788 bytes
-rw-r--r--resources/src/mediawiki.legacy/images/checker.pngbin0 -> 81 bytes
-rw-r--r--resources/src/mediawiki.legacy/images/feed-icon.pngbin0 -> 542 bytes
-rw-r--r--resources/src/mediawiki.legacy/images/feed-icon.svg1
-rw-r--r--resources/src/mediawiki.legacy/images/help-question-hover.gifbin0 -> 1246 bytes
-rw-r--r--resources/src/mediawiki.legacy/images/help-question.gifbin0 -> 126 bytes
-rw-r--r--resources/src/mediawiki.legacy/images/question.pngbin0 -> 316 bytes
-rw-r--r--resources/src/mediawiki.legacy/images/question.svg1
-rw-r--r--resources/src/mediawiki.legacy/images/spinner.gifbin0 -> 1819 bytes
-rw-r--r--resources/src/mediawiki.legacy/oldshared.css489
-rw-r--r--resources/src/mediawiki.legacy/protect.js240
-rw-r--r--resources/src/mediawiki.legacy/shared.css1167
-rw-r--r--resources/src/mediawiki.legacy/wikibits.js204
-rw-r--r--resources/src/mediawiki.less/mediawiki.mixins.animation.less12
-rw-r--r--resources/src/mediawiki.less/mediawiki.mixins.less61
-rw-r--r--resources/src/mediawiki.less/mediawiki.mixins.rotation.less33
-rw-r--r--resources/src/mediawiki.less/mediawiki.ui/mixins.less122
-rw-r--r--resources/src/mediawiki.less/mediawiki.ui/variables.less62
-rw-r--r--resources/src/mediawiki.libs/CLDRPluralRuleParser.js475
-rw-r--r--resources/src/mediawiki.libs/mediawiki.libs.jpegmeta.js737
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.gallery.js212
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.image.pagination.js101
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js65
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.ready.js64
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.startup.js33
-rw-r--r--resources/src/mediawiki.page/mediawiki.page.watch.ajax.js178
-rw-r--r--resources/src/mediawiki.skinning/content.css227
-rw-r--r--resources/src/mediawiki.skinning/content.externallinks.css102
-rw-r--r--resources/src/mediawiki.skinning/content.parsoid.less131
-rw-r--r--resources/src/mediawiki.skinning/elements.css273
-rw-r--r--resources/src/mediawiki.skinning/images/audio-ltr.pngbin0 -> 401 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/audio-ltr.svg8
-rw-r--r--resources/src/mediawiki.skinning/images/audio-rtl.pngbin0 -> 417 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/audio-rtl.svg8
-rw-r--r--resources/src/mediawiki.skinning/images/chat-ltr.pngbin0 -> 304 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/chat-ltr.svg6
-rw-r--r--resources/src/mediawiki.skinning/images/chat-rtl.pngbin0 -> 297 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/chat-rtl.svg6
-rw-r--r--resources/src/mediawiki.skinning/images/document-ltr.pngbin0 -> 275 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/document-ltr.svg5
-rw-r--r--resources/src/mediawiki.skinning/images/document-rtl.pngbin0 -> 264 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/document-rtl.svg5
-rw-r--r--resources/src/mediawiki.skinning/images/external link icons.svg697
-rw-r--r--resources/src/mediawiki.skinning/images/external-ltr.pngbin0 -> 403 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/external-ltr.svg8
-rw-r--r--resources/src/mediawiki.skinning/images/external-rtl.pngbin0 -> 411 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/external-rtl.svg8
-rw-r--r--resources/src/mediawiki.skinning/images/ftp-ltr.pngbin0 -> 356 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/ftp-ltr.svg9
-rw-r--r--resources/src/mediawiki.skinning/images/ftp-rtl.pngbin0 -> 334 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/ftp-rtl.svg9
-rw-r--r--resources/src/mediawiki.skinning/images/magnify-clip-ltr.pngbin0 -> 204 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/magnify-clip-rtl.pngbin0 -> 149 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/mail.pngbin0 -> 334 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/mail.svg7
-rw-r--r--resources/src/mediawiki.skinning/images/video.pngbin0 -> 358 bytes
-rw-r--r--resources/src/mediawiki.skinning/images/video.svg14
-rw-r--r--resources/src/mediawiki.skinning/interface.css68
-rw-r--r--resources/src/mediawiki.special/images/glyph-people-large.pngbin0 -> 1663 bytes
-rw-r--r--resources/src/mediawiki.special/images/icon-contributors.pngbin0 -> 1169 bytes
-rw-r--r--resources/src/mediawiki.special/images/icon-edits.pngbin0 -> 780 bytes
-rw-r--r--resources/src/mediawiki.special/images/icon-lock.pngbin0 -> 172 bytes
-rw-r--r--resources/src/mediawiki.special/images/icon-pages.pngbin0 -> 528 bytes
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.block.css11
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.block.js45
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeemail.css19
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeemail.js52
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeslist.css7
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css61
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css29
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js25
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.css120
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.import.js35
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js36
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.js9
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.movePage.js6
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.pageLanguage.js9
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css4
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.preferences.css21
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.preferences.js305
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.recentchanges.js39
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.search.css173
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.search.js58
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.undelete.js11
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css9
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js52
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.upload.js565
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.common.css66
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.common.js72
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.login.css22
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css66
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js140
-rw-r--r--resources/src/mediawiki.special/mediawiki.special.version.css14
-rw-r--r--resources/src/mediawiki.ui/components/anchors.less77
-rw-r--r--resources/src/mediawiki.ui/components/buttons.less276
-rw-r--r--resources/src/mediawiki.ui/components/checkbox.less100
-rw-r--r--resources/src/mediawiki.ui/components/forms.less166
-rw-r--r--resources/src/mediawiki.ui/components/images/checked.pngbin0 -> 327 bytes
-rw-r--r--resources/src/mediawiki.ui/components/images/checked.svg1
-rw-r--r--resources/src/mediawiki.ui/components/inputs.less126
-rw-r--r--resources/src/mediawiki.ui/components/utilities.less47
-rw-r--r--resources/src/mediawiki.ui/default.less5
-rw-r--r--resources/src/mediawiki.ui/styleguide.md11
-rw-r--r--resources/src/mediawiki/images/arrow-collapsed-ltr.pngbin0 -> 133 bytes
-rw-r--r--resources/src/mediawiki/images/arrow-collapsed-ltr.svg1
-rw-r--r--resources/src/mediawiki/images/arrow-collapsed-rtl.pngbin0 -> 136 bytes
-rw-r--r--resources/src/mediawiki/images/arrow-collapsed-rtl.svg1
-rw-r--r--resources/src/mediawiki/images/arrow-expanded.pngbin0 -> 134 bytes
-rw-r--r--resources/src/mediawiki/images/arrow-expanded.svg1
-rw-r--r--resources/src/mediawiki/images/arrow-sort-ascending.pngbin0 -> 244 bytes
-rw-r--r--resources/src/mediawiki/images/arrow-sort-ascending.svg1
-rw-r--r--resources/src/mediawiki/images/arrow-sort-descending.pngbin0 -> 245 bytes
-rw-r--r--resources/src/mediawiki/images/arrow-sort-descending.svg1
-rw-r--r--resources/src/mediawiki/images/pager-arrow-disabled-fastforward-ltr.pngbin0 -> 323 bytes
-rw-r--r--resources/src/mediawiki/images/pager-arrow-disabled-fastforward-rtl.pngbin0 -> 318 bytes
-rw-r--r--resources/src/mediawiki/images/pager-arrow-disabled-forward-ltr.pngbin0 -> 307 bytes
-rw-r--r--resources/src/mediawiki/images/pager-arrow-disabled-forward-rtl.pngbin0 -> 301 bytes
-rw-r--r--resources/src/mediawiki/images/pager-arrow-fastforward-ltr.pngbin0 -> 342 bytes
-rw-r--r--resources/src/mediawiki/images/pager-arrow-fastforward-rtl.pngbin0 -> 352 bytes
-rw-r--r--resources/src/mediawiki/images/pager-arrow-forward-ltr.pngbin0 -> 337 bytes
-rw-r--r--resources/src/mediawiki/images/pager-arrow-forward-rtl.pngbin0 -> 330 bytes
-rw-r--r--resources/src/mediawiki/mediawiki.Title.js939
-rw-r--r--resources/src/mediawiki/mediawiki.Uri.js403
-rw-r--r--resources/src/mediawiki/mediawiki.content.json.css53
-rw-r--r--resources/src/mediawiki/mediawiki.cookie.js126
-rw-r--r--resources/src/mediawiki/mediawiki.debug.init.js3
-rw-r--r--resources/src/mediawiki/mediawiki.debug.js391
-rw-r--r--resources/src/mediawiki/mediawiki.debug.less189
-rw-r--r--resources/src/mediawiki/mediawiki.debug.profile.css45
-rw-r--r--resources/src/mediawiki/mediawiki.debug.profile.js556
-rw-r--r--resources/src/mediawiki/mediawiki.feedback.css9
-rw-r--r--resources/src/mediawiki/mediawiki.feedback.js320
-rw-r--r--resources/src/mediawiki/mediawiki.feedback.spinner.gifbin0 -> 1108 bytes
-rw-r--r--resources/src/mediawiki/mediawiki.hidpi.js5
-rw-r--r--resources/src/mediawiki/mediawiki.hlist.css78
-rw-r--r--resources/src/mediawiki/mediawiki.hlist.js31
-rw-r--r--resources/src/mediawiki/mediawiki.htmlform.js408
-rw-r--r--resources/src/mediawiki/mediawiki.icon.less19
-rw-r--r--resources/src/mediawiki/mediawiki.inspect.js284
-rw-r--r--resources/src/mediawiki/mediawiki.jqueryMsg.js1251
-rw-r--r--resources/src/mediawiki/mediawiki.jqueryMsg.peg85
-rw-r--r--resources/src/mediawiki/mediawiki.js2399
-rw-r--r--resources/src/mediawiki/mediawiki.log.js84
-rw-r--r--resources/src/mediawiki/mediawiki.notification.css27
-rw-r--r--resources/src/mediawiki/mediawiki.notification.hideForPrint.css3
-rw-r--r--resources/src/mediawiki/mediawiki.notification.js523
-rw-r--r--resources/src/mediawiki/mediawiki.notify.js27
-rw-r--r--resources/src/mediawiki/mediawiki.pager.tablePager.less84
-rw-r--r--resources/src/mediawiki/mediawiki.searchSuggest.css24
-rw-r--r--resources/src/mediawiki/mediawiki.searchSuggest.js199
-rw-r--r--resources/src/mediawiki/mediawiki.toc.js60
-rw-r--r--resources/src/mediawiki/mediawiki.user.js258
-rw-r--r--resources/src/mediawiki/mediawiki.util.js531
-rw-r--r--resources/src/polyfill-object-create.js62
-rw-r--r--resources/src/startup.js62
297 files changed, 28962 insertions, 0 deletions
diff --git a/resources/src/es5-skip.js b/resources/src/es5-skip.js
new file mode 100644
index 00000000..a4039d89
--- /dev/null
+++ b/resources/src/es5-skip.js
@@ -0,0 +1,18 @@
+/*!
+ * Skip function for es5-shim module.
+ *
+ * Test for strict mode as a proxy for full ES5 function support (but not syntax)
+ * Per http://kangax.github.io/compat-table/es5/ this is a reasonable shortcut
+ * that still allows this to be as short as possible (there are no browsers we
+ * support that have strict mode, but lack other features).
+ *
+ * Do explicitly test for Function#bind because of PhantomJS (which implements
+ * strict mode, but lacks Function#bind).
+ *
+ * IE9 supports all features except strict mode, so loading es5-shim should be close to
+ * a no-op but does increase page payload).
+ */
+return ( function () {
+ 'use strict';
+ return !this && !!Function.prototype.bind;
+}() );
diff --git a/resources/src/jquery.json-deprecate.js b/resources/src/jquery.json-deprecate.js
new file mode 100644
index 00000000..f38decd9
--- /dev/null
+++ b/resources/src/jquery.json-deprecate.js
@@ -0,0 +1,8 @@
+( function ( mw, $ ) {
+ // @deprecated since 1.24. The 'jquery.json' module will be removed in MW 1.25. Use the 'json' module.
+
+ mw.log.deprecate( $, 'toJSON', $.toJSON, 'Use JSON.stringify instead (module "json" for polyfill).' );
+ mw.log.deprecate( $, 'evalJSON', $.evalJSON, 'Use JSON.parse instead (module "json" for polyfill).' );
+ mw.log.deprecate( $, 'secureEvalJSON', $.secureEvalJSON, 'Use JSON.parse instead (module "json" for polyfill).' );
+ mw.log.deprecate( $, 'quoteString', $.quoteString, 'Use JSON.stringify instead (module "json" for polyfill).' );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/jquery.tipsy/images/tipsy.png b/resources/src/jquery.tipsy/images/tipsy.png
new file mode 100644
index 00000000..ef17cc32
--- /dev/null
+++ b/resources/src/jquery.tipsy/images/tipsy.png
Binary files differ
diff --git a/resources/src/jquery.tipsy/jquery.tipsy.css b/resources/src/jquery.tipsy/jquery.tipsy.css
new file mode 100644
index 00000000..6471516d
--- /dev/null
+++ b/resources/src/jquery.tipsy/jquery.tipsy.css
@@ -0,0 +1,73 @@
+.tipsy {
+ padding: 5px;
+ position: absolute;
+ z-index: 100000;
+ cursor: default;
+}
+.tipsy-inner {
+ padding: 5px 8px 4px 8px;
+ /*background-color: #e8f2f8;*/
+ background-color: #ffffff;
+ border: solid 1px #a7d7f9;
+ color: black;
+ max-width: 15em;
+ border-radius: 4px;
+ /*
+ -moz-box-shadow: 0px 2px 8px #cccccc;
+ -webkit-box-shadow: 0px 2px 8px #cccccc;
+ box-shadow: 0px 2px 8px #cccccc;
+ -ms-filter: "progid:DXImageTransform.Microsoft.DropShadow(OffX=0, OffY=2, Strength=6, Direction=90, Color='#cccccc')";
+ filter: progid:DXImageTransform.Microsoft.DropShadow(OffX=0, OffY=2, Strength=6, Direction=90, Color='#cccccc');
+ */
+}
+.tipsy-arrow {
+ position: absolute;
+ /* @embed */
+ background: url('images/tipsy.png') no-repeat top left;
+ width: 11px;
+ height: 6px;
+}
+/* @noflip */ .tipsy-n .tipsy-arrow {
+ top: 0px;
+ left: 50%;
+ margin-left: -5px;
+}
+/* @noflip */ .tipsy-nw .tipsy-arrow {
+ top: 1px;
+ left: 10px;
+}
+/* @noflip */ .tipsy-ne .tipsy-arrow {
+ top: 1px;
+ right: 10px;
+}
+/* @noflip */ .tipsy-s .tipsy-arrow {
+ bottom: 0px;
+ left: 50%;
+ margin-left: -5px;
+ background-position: bottom left;
+}
+/* @noflip */ .tipsy-sw .tipsy-arrow {
+ bottom: 0px;
+ left: 10px;
+ background-position: bottom left;
+}
+/* @noflip */ .tipsy-se .tipsy-arrow {
+ bottom: 0px;
+ right: 10px;
+ background-position: bottom left;
+}
+/* @noflip */ .tipsy-e .tipsy-arrow {
+ top: 50%;
+ margin-top: -5px;
+ right: 1px;
+ width: 5px;
+ height: 11px;
+ background-position: top right;
+}
+/* @noflip */ .tipsy-w .tipsy-arrow {
+ top: 50%;
+ margin-top: -5px;
+ left: 0px;
+ width: 6px;
+ height: 11px;
+}
diff --git a/resources/src/jquery.tipsy/jquery.tipsy.js b/resources/src/jquery.tipsy/jquery.tipsy.js
new file mode 100644
index 00000000..58a99a59
--- /dev/null
+++ b/resources/src/jquery.tipsy/jquery.tipsy.js
@@ -0,0 +1,259 @@
+// tipsy, facebook style tooltips for jquery
+// version 1.0.0a*
+// (c) 2008-2010 jason frame [jason@onehackoranother.com]
+// released under the MIT license
+
+// * This installation of tipsy includes several local modifications to both Javascript and CSS.
+// Please be careful when upgrading.
+
+(function($) {
+
+ function maybeCall(thing, ctx) {
+ return (typeof thing == 'function') ? (thing.call(ctx)) : thing;
+ }
+
+ function Tipsy(element, options) {
+ this.$element = $(element);
+ this.options = options;
+ this.enabled = true;
+ this.fixTitle();
+ }
+
+ Tipsy.prototype = {
+ show: function() {
+ var title = this.getTitle();
+ if (title && this.enabled) {
+ var $tip = this.tip();
+
+ $tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title);
+ $tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity
+ if (this.options.className) {
+ $tip.addClass(maybeCall(this.options.className, this.$element[0]));
+ }
+ $tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).appendTo(document.body);
+
+ var pos = $.extend({}, this.$element.offset(), {
+ width: this.$element[0].offsetWidth,
+ height: this.$element[0].offsetHeight
+ });
+
+ var gravity = (typeof this.options.gravity == 'function')
+ ? this.options.gravity.call(this.$element[0])
+ : this.options.gravity;
+
+ // Attach css classes before checking height/width so they
+ // can be applied.
+ $tip.addClass('tipsy-' + gravity);
+ if (this.options.className) {
+ $tip.addClass(maybeCall(this.options.className, this.$element[0]));
+ }
+
+ var actualWidth = $tip[0].offsetWidth, actualHeight = $tip[0].offsetHeight;
+ var tp;
+ switch (gravity.charAt(0)) {
+ case 'n':
+ tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2};
+ break;
+ case 's':
+ tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2};
+ break;
+ case 'e':
+ tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset};
+ break;
+ case 'w':
+ tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset};
+ break;
+ }
+
+ if (gravity.length == 2) {
+ if (gravity.charAt(1) == 'w') {
+ if (this.options.center) {
+ tp.left = pos.left + pos.width / 2 - 15;
+ } else {
+ tp.left = pos.left;
+ }
+ } else {
+ if (this.options.center) {
+ tp.left = pos.left + pos.width / 2 - actualWidth + 15;
+ } else {
+ tp.left = pos.left + pos.width;
+ }
+ }
+ }
+ $tip.css(tp);
+
+ if (this.options.fade) {
+ $tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity}, 100);
+ } else {
+ $tip.css({visibility: 'visible', opacity: this.options.opacity});
+ }
+ }
+ },
+
+ hide: function() {
+ if (this.options.fade) {
+ this.tip().stop().fadeOut(100, function() { $(this).remove(); });
+ } else {
+ this.tip().remove();
+ }
+ },
+
+
+ fixTitle: function() {
+ var $e = this.$element;
+ if ($e.attr('title') || typeof($e.attr('original-title')) != 'string') {
+ $e.attr('original-title', $e.attr('title') || '').removeAttr('title');
+ }
+ },
+
+ getTitle: function() {
+ var title, $e = this.$element, o = this.options;
+ this.fixTitle();
+ if (typeof o.title == 'string') {
+ title = $e.attr(o.title == 'title' ? 'original-title' : o.title);
+ } else if (typeof o.title == 'function') {
+ title = o.title.call($e[0]);
+ }
+ title = ('' + title).replace(/(^\s*|\s*$)/, "");
+ return title || o.fallback;
+ },
+
+ tip: function() {
+ if (!this.$tip) {
+ this.$tip = $('<div class="tipsy"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"></div>');
+ }
+ return this.$tip;
+ },
+
+ validate: function() {
+ if (!this.$element[0].parentNode) {
+ this.hide();
+ this.$element = null;
+ this.options = null;
+ }
+ },
+
+ enable: function() { this.enabled = true; },
+ disable: function() { this.enabled = false; },
+ toggleEnabled: function() { this.enabled = !this.enabled; }
+ };
+
+ $.fn.tipsy = function(options) {
+
+ if (options === true) {
+ return this.data('tipsy');
+ } else if (typeof options == 'string') {
+ var tipsy = this.data('tipsy');
+ if (tipsy) tipsy[options]();
+ return this;
+ }
+
+ options = $.extend({}, $.fn.tipsy.defaults, options);
+
+ function get(ele) {
+ var tipsy = $.data(ele, 'tipsy');
+ if (!tipsy) {
+ tipsy = new Tipsy(ele, $.fn.tipsy.elementOptions(ele, options));
+ $.data(ele, 'tipsy', tipsy);
+ }
+ return tipsy;
+ }
+
+ function enter() {
+ var tipsy = get(this);
+ tipsy.hoverState = 'in';
+ if (options.delayIn == 0) {
+ tipsy.show();
+ } else {
+ tipsy.fixTitle();
+ setTimeout(function() { if (tipsy.hoverState == 'in') tipsy.show(); }, options.delayIn);
+ }
+ };
+
+ function leave() {
+ var tipsy = get(this);
+ tipsy.hoverState = 'out';
+ if (options.delayOut == 0) {
+ tipsy.hide();
+ } else {
+ setTimeout(function() { if (tipsy.hoverState == 'out') tipsy.hide(); }, options.delayOut);
+ }
+ };
+
+ if (!options.live) this.each(function() { get(this); });
+
+ if (options.trigger != 'manual') {
+ var binder = options.live ? 'live' : 'bind',
+ eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus',
+ eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur';
+ this[binder](eventIn, enter)[binder](eventOut, leave);
+ }
+
+ return this;
+
+ };
+
+ $.fn.tipsy.defaults = {
+ className: null,
+ delayIn: 0,
+ delayOut: 0,
+ fade: true,
+ fallback: '',
+ gravity: 'n',
+ center: true,
+ html: false,
+ live: false,
+ offset: 0,
+ opacity: 1.0,
+ title: 'title',
+ trigger: 'hover'
+ };
+
+ // Overwrite this method to provide options on a per-element basis.
+ // For example, you could store the gravity in a 'tipsy-gravity' attribute:
+ // return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' });
+ // (remember - do not modify 'options' in place!)
+ $.fn.tipsy.elementOptions = function(ele, options) {
+ return $.metadata ? $.extend({}, options, $(ele).metadata()) : options;
+ };
+
+ $.fn.tipsy.autoNS = function() {
+ return $(this).offset().top > ($(document).scrollTop() + $(window).height() / 2) ? 's' : 'n';
+ };
+
+ $.fn.tipsy.autoWE = function() {
+ return $(this).offset().left > ($(document).scrollLeft() + $(window).width() / 2) ? 'e' : 'w';
+ };
+
+ /**
+ * yields a closure of the supplied parameters, producing a function that takes
+ * no arguments and is suitable for use as an autogravity function like so:
+ *
+ * @param margin (int) - distance from the viewable region edge that an
+ * element should be before setting its tooltip's gravity to be away
+ * from that edge.
+ * @param prefer (string, e.g. 'n', 'sw', 'w') - the direction to prefer
+ * if there are no viewable region edges effecting the tooltip's
+ * gravity. It will try to vary from this minimally, for example,
+ * if 'sw' is preferred and an element is near the right viewable
+ * region edge, but not the top edge, it will set the gravity for
+ * that element's tooltip to be 'se', preserving the southern
+ * component.
+ */
+ $.fn.tipsy.autoBounds = function(margin, prefer) {
+ return function() {
+ var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)},
+ boundTop = $(document).scrollTop() + margin,
+ boundLeft = $(document).scrollLeft() + margin,
+ $this = $(this);
+
+ if ($this.offset().top < boundTop) dir.ns = 'n';
+ if ($this.offset().left < boundLeft) dir.ew = 'w';
+ if ($(window).width() + $(document).scrollLeft() - $this.offset().left < margin) dir.ew = 'e';
+ if ($(window).height() + $(document).scrollTop() - $this.offset().top < margin) dir.ns = 's';
+
+ return dir.ns + (dir.ew ? dir.ew : '');
+ }
+ };
+
+})(jQuery);
diff --git a/resources/src/jquery/images/jquery.arrowSteps.divider-ltr.png b/resources/src/jquery/images/jquery.arrowSteps.divider-ltr.png
new file mode 100644
index 00000000..84ed2a2d
--- /dev/null
+++ b/resources/src/jquery/images/jquery.arrowSteps.divider-ltr.png
Binary files differ
diff --git a/resources/src/jquery/images/jquery.arrowSteps.divider-rtl.png b/resources/src/jquery/images/jquery.arrowSteps.divider-rtl.png
new file mode 100644
index 00000000..7cfbfeba
--- /dev/null
+++ b/resources/src/jquery/images/jquery.arrowSteps.divider-rtl.png
Binary files differ
diff --git a/resources/src/jquery/images/jquery.arrowSteps.head-ltr.png b/resources/src/jquery/images/jquery.arrowSteps.head-ltr.png
new file mode 100644
index 00000000..eb070280
--- /dev/null
+++ b/resources/src/jquery/images/jquery.arrowSteps.head-ltr.png
Binary files differ
diff --git a/resources/src/jquery/images/jquery.arrowSteps.head-rtl.png b/resources/src/jquery/images/jquery.arrowSteps.head-rtl.png
new file mode 100644
index 00000000..7ea2fdb5
--- /dev/null
+++ b/resources/src/jquery/images/jquery.arrowSteps.head-rtl.png
Binary files differ
diff --git a/resources/src/jquery/images/jquery.arrowSteps.tail-ltr.png b/resources/src/jquery/images/jquery.arrowSteps.tail-ltr.png
new file mode 100644
index 00000000..3ad990b6
--- /dev/null
+++ b/resources/src/jquery/images/jquery.arrowSteps.tail-ltr.png
Binary files differ
diff --git a/resources/src/jquery/images/jquery.arrowSteps.tail-rtl.png b/resources/src/jquery/images/jquery.arrowSteps.tail-rtl.png
new file mode 100644
index 00000000..1d3048ef
--- /dev/null
+++ b/resources/src/jquery/images/jquery.arrowSteps.tail-rtl.png
Binary files differ
diff --git a/resources/src/jquery/images/marker.png b/resources/src/jquery/images/marker.png
new file mode 100644
index 00000000..19efb6ce
--- /dev/null
+++ b/resources/src/jquery/images/marker.png
Binary files differ
diff --git a/resources/src/jquery/images/mask.png b/resources/src/jquery/images/mask.png
new file mode 100644
index 00000000..fe08de0e
--- /dev/null
+++ b/resources/src/jquery/images/mask.png
Binary files differ
diff --git a/resources/src/jquery/images/sort_both.gif b/resources/src/jquery/images/sort_both.gif
new file mode 100644
index 00000000..50ad15a0
--- /dev/null
+++ b/resources/src/jquery/images/sort_both.gif
Binary files differ
diff --git a/resources/src/jquery/images/sort_down.gif b/resources/src/jquery/images/sort_down.gif
new file mode 100644
index 00000000..ec4f41b0
--- /dev/null
+++ b/resources/src/jquery/images/sort_down.gif
Binary files differ
diff --git a/resources/src/jquery/images/sort_none.gif b/resources/src/jquery/images/sort_none.gif
new file mode 100644
index 00000000..edd07e58
--- /dev/null
+++ b/resources/src/jquery/images/sort_none.gif
Binary files differ
diff --git a/resources/src/jquery/images/sort_up.gif b/resources/src/jquery/images/sort_up.gif
new file mode 100644
index 00000000..80189185
--- /dev/null
+++ b/resources/src/jquery/images/sort_up.gif
Binary files differ
diff --git a/resources/src/jquery/images/spinner-large.gif b/resources/src/jquery/images/spinner-large.gif
new file mode 100644
index 00000000..72203fdd
--- /dev/null
+++ b/resources/src/jquery/images/spinner-large.gif
Binary files differ
diff --git a/resources/src/jquery/images/spinner.gif b/resources/src/jquery/images/spinner.gif
new file mode 100644
index 00000000..6146be4e
--- /dev/null
+++ b/resources/src/jquery/images/spinner.gif
Binary files differ
diff --git a/resources/src/jquery/images/wheel.png b/resources/src/jquery/images/wheel.png
new file mode 100644
index 00000000..7e53103e
--- /dev/null
+++ b/resources/src/jquery/images/wheel.png
Binary files differ
diff --git a/resources/src/jquery/jquery.accessKeyLabel.js b/resources/src/jquery/jquery.accessKeyLabel.js
new file mode 100644
index 00000000..7b49cb2d
--- /dev/null
+++ b/resources/src/jquery/jquery.accessKeyLabel.js
@@ -0,0 +1,200 @@
+/**
+ * jQuery plugin to update the tooltip to show the correct access key
+ *
+ * @class jQuery.plugin.accessKeyLabel
+ */
+( function ( $, mw ) {
+
+// Cached access key prefix for used browser
+var cachedAccessKeyPrefix,
+
+ // Wether to use 'test-' instead of correct prefix (used for testing)
+ useTestPrefix = false,
+
+ // tag names which can have a label tag
+ // https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Form-associated_content
+ labelable = 'button, input, textarea, keygen, meter, output, progress, select';
+
+/**
+ * Get the prefix for the access key for browsers that don't support accessKeyLabel.
+ *
+ * For browsers that support accessKeyLabel, #getAccessKeyLabel never calls here.
+ *
+ * @private
+ * @param {Object} [ua] An object with a 'userAgent' and 'platform' property.
+ * @return {string} Access key prefix
+ */
+function getAccessKeyPrefix( ua ) {
+ // use cached prefix if possible
+ if ( !ua && cachedAccessKeyPrefix ) {
+ return cachedAccessKeyPrefix;
+ }
+
+ var profile = $.client.profile( ua ),
+ accessKeyPrefix = 'alt-';
+
+ // Opera on any platform
+ if ( profile.name === 'opera' ) {
+ accessKeyPrefix = 'shift-esc-';
+
+ // Chrome on any platform
+ } else if ( profile.name === 'chrome' ) {
+ accessKeyPrefix = (
+ profile.platform === 'mac'
+ // Chrome on Mac
+ ? 'ctrl-option-'
+ // Chrome on Windows or Linux
+ // (both alt- and alt-shift work, but alt with E, D, F etc does not
+ // work since they are browser shortcuts)
+ : 'alt-shift-'
+ );
+
+ // Non-Windows Safari with webkit_version > 526
+ } else if ( profile.platform !== 'win'
+ && profile.name === 'safari'
+ && profile.layoutVersion > 526
+ ) {
+ accessKeyPrefix = 'ctrl-alt-';
+
+ // Safari/Konqueror on any platform, or any browser on Mac
+ // (but not Safari on Windows)
+ } else if ( !( profile.platform === 'win' && profile.name === 'safari' )
+ && ( profile.name === 'safari'
+ || profile.platform === 'mac'
+ || profile.name === 'konqueror' )
+ ) {
+ accessKeyPrefix = 'ctrl-';
+
+ // Firefox/Iceweasel 2.x and later
+ } else if ( ( profile.name === 'firefox' || profile.name === 'iceweasel' )
+ && profile.versionBase > '1'
+ ) {
+ accessKeyPrefix = 'alt-shift-';
+ }
+
+ // cache prefix
+ if ( !ua ) {
+ cachedAccessKeyPrefix = accessKeyPrefix;
+ }
+ return accessKeyPrefix;
+}
+
+/**
+ * Get the access key label for an element.
+ *
+ * Will use native accessKeyLabel if available (currently only in Firefox 8+),
+ * falls back to #getAccessKeyPrefix.
+ *
+ * @private
+ * @param {HTMLElement} element Element to get the label for
+ * @return {string} Access key label
+ */
+function getAccessKeyLabel( element ) {
+ // abort early if no access key
+ if ( !element.accessKey ) {
+ return '';
+ }
+ // use accessKeyLabel if possible
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#dom-accesskeylabel
+ if ( !useTestPrefix && element.accessKeyLabel ) {
+ return element.accessKeyLabel;
+ }
+ return ( useTestPrefix ? 'test-' : getAccessKeyPrefix() ) + element.accessKey;
+}
+
+/**
+ * Update the title for an element (on the element with the access key or it's label) to show
+ * the correct access key label.
+ *
+ * @private
+ * @param {HTMLElement} element Element with the accesskey
+ * @param {HTMLElement} titleElement Element with the title to update (may be the same as `element`)
+ */
+function updateTooltipOnElement( element, titleElement ) {
+ var array = ( mw.msg( 'word-separator' ) + mw.msg( 'brackets' ) ).split( '$1' ),
+ regexp = new RegExp( $.map( array, $.escapeRE ).join( '.*?' ) + '$' ),
+ oldTitle = titleElement.title,
+ rawTitle = oldTitle.replace( regexp, '' ),
+ newTitle = rawTitle,
+ accessKeyLabel = getAccessKeyLabel( element );
+
+ // don't add a title if the element didn't have one before
+ if ( !oldTitle ) {
+ return;
+ }
+
+ if ( accessKeyLabel ) {
+ // Should be build the same as in Linker::titleAttrib
+ newTitle += mw.msg( 'word-separator' ) + mw.msg( 'brackets', accessKeyLabel );
+ }
+ if ( oldTitle !== newTitle ) {
+ titleElement.title = newTitle;
+ }
+}
+
+/**
+ * Update the title for an element to show the correct access key label.
+ *
+ * @private
+ * @param {HTMLElement} element Element with the accesskey
+ */
+function updateTooltip( element ) {
+ var id, $element, $label, $labelParent;
+ updateTooltipOnElement( element, element );
+
+ // update associated label if there is one
+ $element = $( element );
+ if ( $element.is( labelable ) ) {
+ // Search it using 'for' attribute
+ id = element.id.replace( /"/g, '\\"' );
+ if ( id ) {
+ $label = $( 'label[for="' + id + '"]' );
+ if ( $label.length === 1 ) {
+ updateTooltipOnElement( element, $label[0] );
+ }
+ }
+
+ // Search it as parent, because the form control can also be inside the label element itself
+ $labelParent = $element.parents( 'label' );
+ if ( $labelParent.length === 1 ) {
+ updateTooltipOnElement( element, $labelParent[0] );
+ }
+ }
+}
+
+/**
+ * Update the titles for all elements in a jQuery selection.
+ *
+ * @return {jQuery}
+ * @chainable
+ */
+$.fn.updateTooltipAccessKeys = function () {
+ return this.each( function () {
+ updateTooltip( this );
+ } );
+};
+
+/**
+ * Exposed for testing.
+ *
+ * @method updateTooltipAccessKeys_getAccessKeyPrefix
+ * @inheritdoc #getAccessKeyPrefix
+ */
+$.fn.updateTooltipAccessKeys.getAccessKeyPrefix = getAccessKeyPrefix;
+
+/**
+ * Switch test mode on and off.
+ *
+ * @method updateTooltipAccessKeys_setTestMode
+ * @param {boolean} mode New mode
+ */
+$.fn.updateTooltipAccessKeys.setTestMode = function ( mode ) {
+ useTestPrefix = mode;
+};
+
+/**
+ * @class jQuery
+ * @mixins jQuery.plugin.accessKeyLabel
+ */
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/jquery/jquery.arrowSteps.css b/resources/src/jquery/jquery.arrowSteps.css
new file mode 100644
index 00000000..f8f6e951
--- /dev/null
+++ b/resources/src/jquery/jquery.arrowSteps.css
@@ -0,0 +1,45 @@
+.arrowSteps {
+ list-style-type: none;
+ list-style-image: none;
+ border: 1px solid #666666;
+ position: relative;
+}
+
+.arrowSteps li {
+ float: left;
+ padding: 0px;
+ margin: 0px;
+ border: 0 none;
+}
+
+.arrowSteps li div {
+ padding: 0.5em;
+ text-align: center;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.arrowSteps li.arrow div {
+ /* @embed */
+ background: url(images/jquery.arrowSteps.divider-ltr.png) no-repeat right center;
+}
+
+/* applied to the element preceding the highlighted step */
+.arrowSteps li.arrow.tail div {
+ /* @embed */
+ background: url(images/jquery.arrowSteps.tail-ltr.png) no-repeat right center;
+}
+
+/* this applies to all highlighted, including the last */
+.arrowSteps li.head div {
+ /* @embed */
+ background: url(images/jquery.arrowSteps.head-ltr.png) no-repeat left center;
+ font-weight: bold;
+}
+
+/* this applies to all highlighted arrows except the last */
+.arrowSteps li.arrow.head div {
+ /* TODO: eliminate duplication of jquery.arrowSteps.head.png embedding */
+ /* @embed */
+ background: url(images/jquery.arrowSteps.head-ltr.png) no-repeat right center;
+}
diff --git a/resources/src/jquery/jquery.arrowSteps.js b/resources/src/jquery/jquery.arrowSteps.js
new file mode 100644
index 00000000..f8641e10
--- /dev/null
+++ b/resources/src/jquery/jquery.arrowSteps.js
@@ -0,0 +1,98 @@
+/*!
+ * jQuery arrowSteps plugin
+ * Copyright Neil Kandalgaonkar, 2010
+ *
+ * This work is licensed under the terms of the GNU General Public License,
+ * version 2 or later.
+ * (see http://www.fsf.org/licensing/licenses/gpl.html).
+ * Derivative works and later versions of the code must be free software
+ * licensed under the same or a compatible license.
+ */
+
+/**
+ * @class jQuery.plugin.arrowSteps
+ */
+( function ( $ ) {
+ /**
+ * Show users their progress through a series of steps, via a row of items that fit
+ * together like arrows. One item can be highlighted at a time.
+ *
+ * <ul id="robin-hood-daffy">
+ * <li id="guard"><div>Guard!</div></li>
+ * <li id="turn"><div>Turn!</div></li>
+ * <li id="parry"><div>Parry!</div></li>
+ * <li id="dodge"><div>Dodge!</div></li>
+ * <li id="spin"><div>Spin!</div></li>
+ * <li id="ha"><div>Ha!</div></li>
+ * <li id="thrust"><div>Thrust!</div></li>
+ * </ul>
+ *
+ * <script>
+ * $( '#robin-hood-daffy' ).arrowSteps();
+ * </script>
+ *
+ * @return {jQuery}
+ * @chainable
+ */
+ $.fn.arrowSteps = function () {
+ var $steps, width, arrowWidth, $stepDiv,
+ $el = this,
+ paddingSide = $( 'body' ).hasClass( 'rtl' ) ? 'padding-left' : 'padding-right';
+
+ $el.addClass( 'arrowSteps' );
+ $steps = $el.find( 'li' );
+
+ width = parseInt( 100 / $steps.length, 10 );
+ $steps.css( 'width', width + '%' );
+
+ // Every step except the last one has an arrow pointing forward:
+ // at the right hand side in LTR languages, and at the left hand side in RTL.
+ // Also add in the padding for the calculated arrow width.
+ $stepDiv = $steps.filter( ':not(:last-child)' ).addClass( 'arrow' ).find( 'div' );
+
+ // Execute when complete page is fully loaded, including all frames, objects and images
+ $( window ).load( function () {
+ arrowWidth = parseInt( $el.outerHeight(), 10 );
+ $stepDiv.css( paddingSide, arrowWidth.toString() + 'px' );
+ } );
+
+ $el.data( 'arrowSteps', $steps );
+
+ return this;
+ };
+
+ /**
+ * Highlights the element selected by the selector.
+ *
+ * $( '#robin-hood-daffy' ).arrowStepsHighlight( '#guard' );
+ * // 'Guard!' is highlighted.
+ *
+ * // ... user completes the 'guard' step ...
+ *
+ * $( '#robin-hood-daffy' ).arrowStepsHighlight( '#turn' );
+ * // 'Turn!' is highlighted.
+ *
+ * @param {string} selector
+ */
+ $.fn.arrowStepsHighlight = function ( selector ) {
+ var $previous,
+ $steps = this.data( 'arrowSteps' );
+ $.each( $steps, function ( i, step ) {
+ var $step = $( step );
+ if ( $step.is( selector ) ) {
+ if ($previous) {
+ $previous.addClass( 'tail' );
+ }
+ $step.addClass( 'head' );
+ } else {
+ $step.removeClass( 'head tail lasthead' );
+ }
+ $previous = $step;
+ } );
+ };
+
+ /**
+ * @class jQuery
+ * @mixins jQuery.plugin.arrowSteps
+ */
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.autoEllipsis.js b/resources/src/jquery/jquery.autoEllipsis.js
new file mode 100644
index 00000000..9a196b5d
--- /dev/null
+++ b/resources/src/jquery/jquery.autoEllipsis.js
@@ -0,0 +1,168 @@
+/**
+ * @class jQuery.plugin.autoEllipsis
+ */
+( function ( $ ) {
+
+var
+ // Cache ellipsed substrings for every string-width-position combination
+ cache = {},
+
+ // Use a separate cache when match highlighting is enabled
+ matchTextCache = {};
+
+/**
+ * Automatically truncate the plain text contents of an element and add an ellipsis
+ *
+ * @param {Object} options
+ * @param {'center'|'left'|'right'} [options.position='center'] Where to remove text.
+ * @param {boolean} [options.tooltip=false] Whether to show a tooltip with the remainder
+ * of the text.
+ * @param {boolean} [options.restoreText=false] Whether to save the text for restoring
+ * later.
+ * @param {boolean} [options.hasSpan=false] Whether the element is already a container,
+ * or if the library should create a new container for it.
+ * @param {string|null} [options.matchText=null] Text to highlight, e.g. search terms.
+ * @return {jQuery}
+ * @chainable
+ */
+$.fn.autoEllipsis = function ( options ) {
+ options = $.extend( {
+ position: 'center',
+ tooltip: false,
+ restoreText: false,
+ hasSpan: false,
+ matchText: null
+ }, options );
+
+ return this.each( function () {
+ var $trimmableText,
+ text, trimmableText, w, pw,
+ l, r, i, side, m,
+ // container element - used for measuring against
+ $container = $( this );
+
+ if ( options.restoreText ) {
+ if ( !$container.data( 'autoEllipsis.originalText' ) ) {
+ $container.data( 'autoEllipsis.originalText', $container.text() );
+ } else {
+ $container.text( $container.data( 'autoEllipsis.originalText' ) );
+ }
+ }
+
+ // trimmable text element - only the text within this element will be trimmed
+ if ( options.hasSpan ) {
+ $trimmableText = $container.children( options.selector );
+ } else {
+ $trimmableText = $( '<span>' )
+ .css( 'whiteSpace', 'nowrap' )
+ .text( $container.text() );
+ $container
+ .empty()
+ .append( $trimmableText );
+ }
+
+ text = $container.text();
+ trimmableText = $trimmableText.text();
+ w = $container.width();
+ pw = 0;
+
+ // Try cache
+ if ( options.matchText ) {
+ if ( !( text in matchTextCache ) ) {
+ matchTextCache[text] = {};
+ }
+ if ( !( options.matchText in matchTextCache[text] ) ) {
+ matchTextCache[text][options.matchText] = {};
+ }
+ if ( !( w in matchTextCache[text][options.matchText] ) ) {
+ matchTextCache[text][options.matchText][w] = {};
+ }
+ if ( options.position in matchTextCache[text][options.matchText][w] ) {
+ $container.html( matchTextCache[text][options.matchText][w][options.position] );
+ if ( options.tooltip ) {
+ $container.attr( 'title', text );
+ }
+ return;
+ }
+ } else {
+ if ( !( text in cache ) ) {
+ cache[text] = {};
+ }
+ if ( !( w in cache[text] ) ) {
+ cache[text][w] = {};
+ }
+ if ( options.position in cache[text][w] ) {
+ $container.html( cache[text][w][options.position] );
+ if ( options.tooltip ) {
+ $container.attr( 'title', text );
+ }
+ return;
+ }
+ }
+
+ if ( $trimmableText.width() + pw > w ) {
+ switch ( options.position ) {
+ case 'right':
+ // Use binary search-like technique for efficiency
+ l = 0;
+ r = trimmableText.length;
+ do {
+ m = Math.ceil( ( l + r ) / 2 );
+ $trimmableText.text( trimmableText.slice( 0, m ) + '...' );
+ if ( $trimmableText.width() + pw > w ) {
+ // Text is too long
+ r = m - 1;
+ } else {
+ l = m;
+ }
+ } while ( l < r );
+ $trimmableText.text( trimmableText.slice( 0, l ) + '...' );
+ break;
+ case 'center':
+ // TODO: Use binary search like for 'right'
+ i = [Math.round( trimmableText.length / 2 ), Math.round( trimmableText.length / 2 )];
+ // Begin with making the end shorter
+ side = 1;
+ while ( $trimmableText.outerWidth() + pw > w && i[0] > 0 ) {
+ $trimmableText.text( trimmableText.slice( 0, i[0] ) + '...' + trimmableText.slice( i[1] ) );
+ // Alternate between trimming the end and begining
+ if ( side === 0 ) {
+ // Make the begining shorter
+ i[0]--;
+ side = 1;
+ } else {
+ // Make the end shorter
+ i[1]++;
+ side = 0;
+ }
+ }
+ break;
+ case 'left':
+ // TODO: Use binary search like for 'right'
+ r = 0;
+ while ( $trimmableText.outerWidth() + pw > w && r < trimmableText.length ) {
+ $trimmableText.text( '...' + trimmableText.slice( r ) );
+ r++;
+ }
+ break;
+ }
+ }
+ if ( options.tooltip ) {
+ $container.attr( 'title', text );
+ }
+ if ( options.matchText ) {
+ $container.highlightText( options.matchText );
+ matchTextCache[text][options.matchText][w][options.position] = $container.html();
+ } else {
+ cache[text][w][options.position] = $container.html();
+ }
+
+ } );
+};
+
+/**
+ * @class jQuery
+ * @mixins jQuery.plugin.autoEllipsis
+ */
+
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.badge.css b/resources/src/jquery/jquery.badge.css
new file mode 100644
index 00000000..fa7ea702
--- /dev/null
+++ b/resources/src/jquery/jquery.badge.css
@@ -0,0 +1,36 @@
+.mw-badge {
+ min-width: 7px;
+ border-radius: 2px;
+ padding: 1px 4px;
+ text-align: center;
+ font-size: 12px;
+ line-height: 12px;
+ background-color: #d2d2d2;
+ cursor: pointer;
+}
+
+.mw-badge-content {
+ font-weight: bold;
+ color: white;
+ vertical-align: baseline;
+ text-shadow: 0 1px rgba(0, 0, 0, 0.4);
+}
+
+.mw-badge-inline {
+ margin-left: 3px;
+ display: inline-block;
+ /* Hack for IE6 and IE7 (bug 47926) */
+ zoom: 1;
+ *display: inline;
+
+}
+.mw-badge-overlay {
+ position: absolute;
+ bottom: -1px;
+ right: -3px;
+ z-index: 50;
+}
+
+.mw-badge-important {
+ background-color: #cc0000;
+}
diff --git a/resources/src/jquery/jquery.badge.js b/resources/src/jquery/jquery.badge.js
new file mode 100644
index 00000000..023b6e28
--- /dev/null
+++ b/resources/src/jquery/jquery.badge.js
@@ -0,0 +1,88 @@
+/*!
+ * jQuery Badge plugin
+ *
+ * @license MIT
+ *
+ * @author Ryan Kaldari <rkaldari@wikimedia.org>, 2012
+ * @author Andrew Garrett <agarrett@wikimedia.org>, 2012
+ * @author Marius Hoch <hoo@online.de>, 2012
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * This program is distributed WITHOUT ANY WARRANTY.
+ */
+
+/**
+ * @class jQuery.plugin.badge
+ */
+( function ( $, mw ) {
+ /**
+ * Put a badge on an item on the page. The badge container will be appended to the selected element(s).
+ *
+ * $element.badge( text );
+ * $element.badge( 5 );
+ * $element.badge( '100+' );
+ * $element.badge( text, inline );
+ * $element.badge( 'New', true );
+ *
+ * @param {number|string} text The value to display in the badge. If the value is falsey (0,
+ * null, false, '', etc.), any existing badge will be removed.
+ * @param {boolean} [inline=true] True if the badge should be displayed inline, false
+ * if the badge should overlay the parent element.
+ * @param {boolean} [displayZero=false] True if the number zero should be displayed,
+ * false if the number zero should result in the badge being hidden
+ * @return {jQuery}
+ * @chainable
+ */
+ $.fn.badge = function ( text, inline, displayZero ) {
+ var $badge = this.find( '.mw-badge' ),
+ badgeStyleClass = 'mw-badge-' + ( inline ? 'inline' : 'overlay' ),
+ isImportant = true, displayBadge = true;
+
+ // If we're displaying zero, ensure style to be non-important
+ if ( mw.language.convertNumber( text, true ) === 0 ) {
+ isImportant = false;
+ if ( !displayZero ) {
+ displayBadge = false;
+ }
+ // If text is falsey (besides 0), hide the badge
+ } else if ( !text ) {
+ displayBadge = false;
+ }
+
+ if ( displayBadge ) {
+ // If a badge already exists, reuse it
+ if ( $badge.length ) {
+ $badge
+ .toggleClass( 'mw-badge-important', isImportant )
+ .find( '.mw-badge-content' )
+ .text( text );
+ } else {
+ // Otherwise, create a new badge with the specified text and style
+ $badge = $( '<div class="mw-badge"></div>' )
+ .addClass( badgeStyleClass )
+ .toggleClass( 'mw-badge-important', isImportant )
+ .append(
+ $( '<span class="mw-badge-content"></span>' ).text( text )
+ )
+ .appendTo( this );
+ }
+ } else {
+ $badge.remove();
+ }
+ return this;
+ };
+
+ /**
+ * @class jQuery
+ * @mixins jQuery.plugin.badge
+ */
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/jquery/jquery.byteLength.js b/resources/src/jquery/jquery.byteLength.js
new file mode 100644
index 00000000..7fe25ee3
--- /dev/null
+++ b/resources/src/jquery/jquery.byteLength.js
@@ -0,0 +1,40 @@
+/**
+ * @class jQuery.plugin.byteLength
+ * @author Jan Paul Posma, 2011
+ * @author Timo Tijhof, 2012
+ * @author David Chan, 2013
+ */
+
+/**
+ * Calculate the byte length of a string (accounting for UTF-8).
+ *
+ * @static
+ * @inheritable
+ * @param {string} str
+ * @return {string}
+ */
+jQuery.byteLength = function ( str ) {
+ // This basically figures out how many bytes a UTF-16 string (which is what js sees)
+ // will take in UTF-8 by replacing a 2 byte character with 2 *'s, etc, and counting that.
+ // Note, surrogate (\uD800-\uDFFF) characters are counted as 2 bytes, since there's two of them
+ // and the actual character takes 4 bytes in UTF-8 (2*2=4). Might not work perfectly in
+ // edge cases such as illegal sequences, but that should never happen.
+
+ // https://en.wikipedia.org/wiki/UTF-8#Description
+ // The mapping from UTF-16 code units to UTF-8 bytes is as follows:
+ // > Range 0000-007F: codepoints that become 1 byte of UTF-8
+ // > Range 0080-07FF: codepoints that become 2 bytes of UTF-8
+ // > Range 0800-D7FF: codepoints that become 3 bytes of UTF-8
+ // > Range D800-DFFF: Surrogates (each pair becomes 4 bytes of UTF-8)
+ // > Range E000-FFFF: codepoints that become 3 bytes of UTF-8 (continued)
+
+ return str
+ .replace( /[\u0080-\u07FF\uD800-\uDFFF]/g, '**' )
+ .replace( /[\u0800-\uD7FF\uE000-\uFFFF]/g, '***' )
+ .length;
+};
+
+/**
+ * @class jQuery
+ * @mixins jQuery.plugin.byteLength
+ */
diff --git a/resources/src/jquery/jquery.byteLimit.js b/resources/src/jquery/jquery.byteLimit.js
new file mode 100644
index 00000000..5551232a
--- /dev/null
+++ b/resources/src/jquery/jquery.byteLimit.js
@@ -0,0 +1,235 @@
+/**
+ * @class jQuery.plugin.byteLimit
+ */
+( function ( $ ) {
+
+ /**
+ * Utility function to trim down a string, based on byteLimit
+ * and given a safe start position. It supports insertion anywhere
+ * in the string, so "foo" to "fobaro" if limit is 4 will result in
+ * "fobo", not "foba". Basically emulating the native maxlength by
+ * reconstructing where the insertion occurred.
+ *
+ * @private
+ * @param {string} safeVal Known value that was previously returned by this
+ * function, if none, pass empty string.
+ * @param {string} newVal New value that may have to be trimmed down.
+ * @param {number} byteLimit Number of bytes the value may be in size.
+ * @param {Function} [fn] See jQuery.byteLimit.
+ * @return {Object}
+ * @return {string} return.newVal
+ * @return {boolean} return.trimmed
+ */
+ function trimValForByteLength( safeVal, newVal, byteLimit, fn ) {
+ var startMatches, endMatches, matchesLen, inpParts,
+ oldVal = safeVal;
+
+ // Run the hook if one was provided, but only on the length
+ // assessment. The value itself is not to be affected by the hook.
+ if ( $.byteLength( fn ? fn( newVal ) : newVal ) <= byteLimit ) {
+ // Limit was not reached, just remember the new value
+ // and let the user continue.
+ return {
+ newVal: newVal,
+ trimmed: false
+ };
+ }
+
+ // Current input is longer than the active limit.
+ // Figure out what was added and limit the addition.
+ startMatches = 0;
+ endMatches = 0;
+
+ // It is important that we keep the search within the range of
+ // the shortest string's length.
+ // Imagine a user adds text that matches the end of the old value
+ // (e.g. "foo" -> "foofoo"). startMatches would be 3, but without
+ // limiting both searches to the shortest length, endMatches would
+ // also be 3.
+ matchesLen = Math.min( newVal.length, oldVal.length );
+
+ // Count same characters from the left, first.
+ // (if "foo" -> "foofoo", assume addition was at the end).
+ while (
+ startMatches < matchesLen &&
+ oldVal.charAt( startMatches ) === newVal.charAt( startMatches )
+ ) {
+ startMatches += 1;
+ }
+
+ while (
+ endMatches < ( matchesLen - startMatches ) &&
+ oldVal.charAt( oldVal.length - 1 - endMatches ) === newVal.charAt( newVal.length - 1 - endMatches )
+ ) {
+ endMatches += 1;
+ }
+
+ inpParts = [
+ // Same start
+ newVal.slice( 0, startMatches ),
+ // Inserted content
+ newVal.slice( startMatches, newVal.length - endMatches ),
+ // Same end
+ newVal.slice( newVal.length - endMatches )
+ ];
+
+ // Chop off characters from the end of the "inserted content" string
+ // until the limit is statisfied.
+ if ( fn ) {
+ // stop, when there is nothing to slice - bug 41450
+ while ( $.byteLength( fn( inpParts.join( '' ) ) ) > byteLimit && inpParts[1].length > 0 ) {
+ inpParts[1] = inpParts[1].slice( 0, -1 );
+ }
+ } else {
+ while ( $.byteLength( inpParts.join( '' ) ) > byteLimit ) {
+ inpParts[1] = inpParts[1].slice( 0, -1 );
+ }
+ }
+
+ newVal = inpParts.join( '' );
+
+ return {
+ newVal: newVal,
+ trimmed: true
+ };
+ }
+
+ var eventKeys = [
+ 'keyup.byteLimit',
+ 'keydown.byteLimit',
+ 'change.byteLimit',
+ 'mouseup.byteLimit',
+ 'cut.byteLimit',
+ 'paste.byteLimit',
+ 'focus.byteLimit',
+ 'blur.byteLimit'
+ ].join( ' ' );
+
+ /**
+ * Enforces a byte limit on an input field, so that UTF-8 entries are counted as well,
+ * when, for example, a database field has a byte limit rather than a character limit.
+ * Plugin rationale: Browser has native maxlength for number of characters, this plugin
+ * exists to limit number of bytes instead.
+ *
+ * Can be called with a custom limit (to use that limit instead of the maxlength attribute
+ * value), a filter function (in case the limit should apply to something other than the
+ * exact input value), or both. Order of parameters is important!
+ *
+ * @param {number} [limit] Limit to enforce, fallsback to maxLength-attribute,
+ * called with fetched value as argument.
+ * @param {Function} [fn] Function to call on the string before assessing the length.
+ * @return {jQuery}
+ * @chainable
+ */
+ $.fn.byteLimit = function ( limit, fn ) {
+ // If the first argument is the function,
+ // set fn to the first argument's value and ignore the second argument.
+ if ( $.isFunction( limit ) ) {
+ fn = limit;
+ limit = undefined;
+ // Either way, verify it is a function so we don't have to call
+ // isFunction again after this.
+ } else if ( !fn || !$.isFunction( fn ) ) {
+ fn = undefined;
+ }
+
+ // The following is specific to each element in the collection.
+ return this.each( function ( i, el ) {
+ var $el, elLimit, prevSafeVal;
+
+ $el = $( el );
+
+ // If no limit was passed to byteLimit(), use the maxlength value.
+ // Can't re-use 'limit' variable because it's in the higher scope
+ // that would affect the next each() iteration as well.
+ // Note that we use attribute to read the value instead of property,
+ // because in Chrome the maxLength property by default returns the
+ // highest supported value (no indication that it is being enforced
+ // by choice). We don't want to bind all of this for some ridiculously
+ // high default number, unless it was explicitly set in the HTML.
+ // Also cast to a (primitive) number (most commonly because the maxlength
+ // attribute contains a string, but theoretically the limit parameter
+ // could be something else as well).
+ elLimit = Number( limit === undefined ? $el.attr( 'maxlength' ) : limit );
+
+ // If there is no (valid) limit passed or found in the property,
+ // skip this. The < 0 check is required for Firefox, which returns
+ // -1 (instead of undefined) for maxLength if it is not set.
+ if ( !elLimit || elLimit < 0 ) {
+ return;
+ }
+
+ if ( fn ) {
+ // Save function for reference
+ $el.data( 'byteLimit.callback', fn );
+ }
+
+ // Remove old event handlers (if there are any)
+ $el.off( '.byteLimit' );
+
+ if ( fn ) {
+ // Disable the native maxLength (if there is any), because it interferes
+ // with the (differently calculated) byte limit.
+ // Aside from being differently calculated (average chars with byteLimit
+ // is lower), we also support a callback which can make it to allow longer
+ // values (e.g. count "Foo" from "User:Foo").
+ // maxLength is a strange property. Removing or setting the property to
+ // undefined directly doesn't work. Instead, it can only be unset internally
+ // by the browser when removing the associated attribute (Firefox/Chrome).
+ // http://code.google.com/p/chromium/issues/detail?id=136004
+ $el.removeAttr( 'maxlength' );
+
+ } else {
+ // If we don't have a callback the bytelimit can only be lower than the charlimit
+ // (that is, there are no characters less than 1 byte in size). So lets (re-)enforce
+ // the native limit for efficiency when possible (it will make the while-loop below
+ // faster by there being less left to interate over).
+ $el.attr( 'maxlength', elLimit );
+ }
+
+ // Safe base value, used to determine the path between the previous state
+ // and the state that triggered the event handler below - and enforce the
+ // limit approppiately (e.g. don't chop from the end if text was inserted
+ // at the beginning of the string).
+ prevSafeVal = '';
+
+ // We need to listen to after the change has already happened because we've
+ // learned that trying to guess the new value and canceling the event
+ // accordingly doesn't work because the new value is not always as simple as:
+ // oldValue + String.fromCharCode( e.which ); because of cut, paste, select-drag
+ // replacements, and custom input methods and what not.
+ // Even though we only trim input after it was changed (never prevent it), we do
+ // listen on events that input text, because there are cases where the text has
+ // changed while text is being entered and keyup/change will not be fired yet
+ // (such as holding down a single key, fires keydown, and after each keydown,
+ // we can trim the previous one).
+ // See http://www.w3.org/TR/DOM-Level-3-Events/#events-keyboard-event-order for
+ // the order and characteristics of the key events.
+ $el.on( eventKeys, function () {
+ var res = trimValForByteLength(
+ prevSafeVal,
+ this.value,
+ elLimit,
+ fn
+ );
+
+ // Only set value property if it was trimmed, because whenever the
+ // value property is set, the browser needs to re-initiate the text context,
+ // which moves the cursor at the end the input, moving it away from wherever it was.
+ // This is a side-effect of limiting after the fact.
+ if ( res.trimmed === true ) {
+ this.value = res.newVal;
+ }
+ // Always adjust prevSafeVal to reflect the input value. Not doing this could cause
+ // trimValForByteLength to compare the new value to an empty string instead of the
+ // old value, resulting in trimming always from the end (bug 40850).
+ prevSafeVal = res.newVal;
+ } );
+ } );
+ };
+
+ /**
+ * @class jQuery
+ * @mixins jQuery.plugin.byteLimit
+ */
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.checkboxShiftClick.js b/resources/src/jquery/jquery.checkboxShiftClick.js
new file mode 100644
index 00000000..d99e9f0a
--- /dev/null
+++ b/resources/src/jquery/jquery.checkboxShiftClick.js
@@ -0,0 +1,43 @@
+/**
+ * @class jQuery.plugin.checkboxShiftClick
+ */
+( function ( $ ) {
+
+ /**
+ * Enable checkboxes to be checked or unchecked in a row by clicking one,
+ * holding shift and clicking another one.
+ *
+ * @return {jQuery}
+ * @chainable
+ */
+ $.fn.checkboxShiftClick = function () {
+ var prevCheckbox = null,
+ $box = this;
+ // When our boxes are clicked..
+ $box.click( function ( e ) {
+ // And one has been clicked before...
+ if ( prevCheckbox !== null && e.shiftKey ) {
+ // Check or uncheck this one and all in-between checkboxes,
+ // except for disabled ones
+ $box
+ .slice(
+ Math.min( $box.index( prevCheckbox ), $box.index( e.target ) ),
+ Math.max( $box.index( prevCheckbox ), $box.index( e.target ) ) + 1
+ )
+ .filter( function () {
+ return !this.disabled;
+ } )
+ .prop( 'checked', !!e.target.checked );
+ }
+ // Either way, update the prevCheckbox variable to the one clicked now
+ prevCheckbox = e.target;
+ } );
+ return $box;
+ };
+
+ /**
+ * @class jQuery
+ * @mixins jQuery.plugin.checkboxShiftClick
+ */
+
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.client.js b/resources/src/jquery/jquery.client.js
new file mode 100644
index 00000000..662a6887
--- /dev/null
+++ b/resources/src/jquery/jquery.client.js
@@ -0,0 +1,301 @@
+/**
+ * User-agent detection
+ *
+ * @class jQuery.client
+ * @singleton
+ */
+( function ( $ ) {
+
+ /**
+ * @private
+ * @property {Object} profileCache Keyed by userAgent string,
+ * value is the parsed $.client.profile object for that user agent.
+ */
+ var profileCache = {};
+
+ $.client = {
+
+ /**
+ * Get an object containing information about the client.
+ *
+ * @param {Object} [nav] An object with a 'userAgent' and 'platform' property.
+ * Defaults to the global `navigator` object.
+ * @return {Object} The resulting client object will be in the following format:
+ *
+ * {
+ * 'name': 'firefox',
+ * 'layout': 'gecko',
+ * 'layoutVersion': 20101026,
+ * 'platform': 'linux'
+ * 'version': '3.5.1',
+ * 'versionBase': '3',
+ * 'versionNumber': 3.5,
+ * }
+ */
+ profile: function ( nav ) {
+ /*jshint boss: true */
+
+ if ( nav === undefined ) {
+ nav = window.navigator;
+ }
+
+ // Use the cached version if possible
+ if ( profileCache[ nav.userAgent + '|' + nav.platform ] !== undefined ) {
+ return profileCache[ nav.userAgent + '|' + nav.platform ];
+ }
+
+ var
+ versionNumber,
+ key = nav.userAgent + '|' + nav.platform,
+
+ // Configuration
+
+ // Name of browsers or layout engines we don't recognize
+ uk = 'unknown',
+ // Generic version digit
+ x = 'x',
+ // Strings found in user agent strings that need to be conformed
+ wildUserAgents = ['Opera', 'Navigator', 'Minefield', 'KHTML', 'Chrome', 'PLAYSTATION 3', 'Iceweasel'],
+ // Translations for conforming user agent strings
+ userAgentTranslations = [
+ // Tons of browsers lie about being something they are not
+ [/(Firefox|MSIE|KHTML,?\slike\sGecko|Konqueror)/, ''],
+ // Chrome lives in the shadow of Safari still
+ ['Chrome Safari', 'Chrome'],
+ // KHTML is the layout engine not the browser - LIES!
+ ['KHTML', 'Konqueror'],
+ // Firefox nightly builds
+ ['Minefield', 'Firefox'],
+ // This helps keep different versions consistent
+ ['Navigator', 'Netscape'],
+ // This prevents version extraction issues, otherwise translation would happen later
+ ['PLAYSTATION 3', 'PS3']
+ ],
+ // Strings which precede a version number in a user agent string - combined and used as
+ // match 1 in version detection
+ versionPrefixes = [
+ 'camino', 'chrome', 'firefox', 'iceweasel', 'netscape', 'netscape6', 'opera', 'version', 'konqueror',
+ 'lynx', 'msie', 'safari', 'ps3', 'android'
+ ],
+ // Used as matches 2, 3 and 4 in version extraction - 3 is used as actual version number
+ versionSuffix = '(\\/|\\;?\\s|)([a-z0-9\\.\\+]*?)(\\;|dev|rel|\\)|\\s|$)',
+ // Names of known browsers
+ names = [
+ 'camino', 'chrome', 'firefox', 'iceweasel', 'netscape', 'konqueror', 'lynx', 'msie', 'opera',
+ 'safari', 'ipod', 'iphone', 'blackberry', 'ps3', 'rekonq', 'android'
+ ],
+ // Tanslations for conforming browser names
+ nameTranslations = [],
+ // Names of known layout engines
+ layouts = ['gecko', 'konqueror', 'msie', 'trident', 'opera', 'webkit'],
+ // Translations for conforming layout names
+ layoutTranslations = [ ['konqueror', 'khtml'], ['msie', 'trident'], ['opera', 'presto'] ],
+ // Names of supported layout engines for version number
+ layoutVersions = ['applewebkit', 'gecko', 'trident'],
+ // Names of known operating systems
+ platforms = ['win', 'wow64', 'mac', 'linux', 'sunos', 'solaris', 'iphone'],
+ // Translations for conforming operating system names
+ platformTranslations = [ ['sunos', 'solaris'], ['wow64', 'win'] ],
+
+ /**
+ * Performs multiple replacements on a string
+ * @ignore
+ */
+ translate = function ( source, translations ) {
+ var i;
+ for ( i = 0; i < translations.length; i++ ) {
+ source = source.replace( translations[i][0], translations[i][1] );
+ }
+ return source;
+ },
+
+ // Pre-processing
+
+ ua = nav.userAgent,
+ match,
+ name = uk,
+ layout = uk,
+ layoutversion = uk,
+ platform = uk,
+ version = x;
+
+ if ( match = new RegExp( '(' + wildUserAgents.join( '|' ) + ')' ).exec( ua ) ) {
+ // Takes a userAgent string and translates given text into something we can more easily work with
+ ua = translate( ua, userAgentTranslations );
+ }
+ // Everything will be in lowercase from now on
+ ua = ua.toLowerCase();
+
+ // Extraction
+
+ if ( match = new RegExp( '(' + names.join( '|' ) + ')' ).exec( ua ) ) {
+ name = translate( match[1], nameTranslations );
+ }
+ if ( match = new RegExp( '(' + layouts.join( '|' ) + ')' ).exec( ua ) ) {
+ layout = translate( match[1], layoutTranslations );
+ }
+ if ( match = new RegExp( '(' + layoutVersions.join( '|' ) + ')\\\/(\\d+)').exec( ua ) ) {
+ layoutversion = parseInt( match[2], 10 );
+ }
+ if ( match = new RegExp( '(' + platforms.join( '|' ) + ')' ).exec( nav.platform.toLowerCase() ) ) {
+ platform = translate( match[1], platformTranslations );
+ }
+ if ( match = new RegExp( '(' + versionPrefixes.join( '|' ) + ')' + versionSuffix ).exec( ua ) ) {
+ version = match[3];
+ }
+
+ // Edge Cases -- did I mention about how user agent string lie?
+
+ // Decode Safari's crazy 400+ version numbers
+ if ( name === 'safari' && version > 400 ) {
+ version = '2.0';
+ }
+ // Expose Opera 10's lies about being Opera 9.8
+ if ( name === 'opera' && version >= 9.8 ) {
+ match = ua.match( /\bversion\/([0-9\.]*)/ );
+ if ( match && match[1] ) {
+ version = match[1];
+ } else {
+ version = '10';
+ }
+ }
+ // And Opera 15's lies about being Chrome
+ if ( name === 'chrome' && ( match = ua.match( /\bopr\/([0-9\.]*)/ ) ) ) {
+ if ( match[1] ) {
+ name = 'opera';
+ version = match[1];
+ }
+ }
+ // And IE 11's lies about being not being IE
+ if ( layout === 'trident' && layoutversion >= 7 && ( match = ua.match( /\brv[ :\/]([0-9\.]*)/ ) ) ) {
+ if ( match[1] ) {
+ name = 'msie';
+ version = match[1];
+ }
+ }
+ // And Amazon Silk's lies about being Android on mobile or Safari on desktop
+ if ( match = ua.match( /\bsilk\/([0-9.\-_]*)/ ) ) {
+ if ( match[1] ) {
+ name = 'silk';
+ version = match[1];
+ }
+ }
+
+ versionNumber = parseFloat( version, 10 ) || 0.0;
+
+ // Caching
+
+ return profileCache[ key ] = {
+ name: name,
+ layout: layout,
+ layoutVersion: layoutversion,
+ platform: platform,
+ version: version,
+ versionBase: ( version !== x ? Math.floor( versionNumber ).toString() : x ),
+ versionNumber: versionNumber
+ };
+ },
+
+ /**
+ * Checks the current browser against a support map object.
+ *
+ * Version numbers passed as numeric values will be compared like numbers (1.2 > 1.11).
+ * Version numbers passed as string values will be compared using a simple component-wise
+ * algorithm, similar to PHP's version_compare ('1.2' < '1.11').
+ *
+ * A browser map is in the following format:
+ *
+ * {
+ * // Multiple rules with configurable operators
+ * 'msie': [['>=', 7], ['!=', 9]],
+ * // Match no versions
+ * 'iphone': false,
+ * // Match any version
+ * 'android': null
+ * }
+ *
+ * It can optionally be split into ltr/rtl sections:
+ *
+ * {
+ * 'ltr': {
+ * 'android': null,
+ * 'iphone': false
+ * },
+ * 'rtl': {
+ * 'android': false,
+ * // rules are not inherited from ltr
+ * 'iphone': false
+ * }
+ * }
+ *
+ * @param {Object} map Browser support map
+ * @param {Object} [profile] A client-profile object
+ * @param {boolean} [exactMatchOnly=false] Only return true if the browser is matched, otherwise
+ * returns true if the browser is not found.
+ *
+ * @return {boolean} The current browser is in the support map
+ */
+ test: function ( map, profile, exactMatchOnly ) {
+ /*jshint evil: true */
+
+ var conditions, dir, i, op, val, j, pieceVersion, pieceVal, compare;
+ profile = $.isPlainObject( profile ) ? profile : $.client.profile();
+ if ( map.ltr && map.rtl ) {
+ dir = $( 'body' ).is( '.rtl' ) ? 'rtl' : 'ltr';
+ map = map[dir];
+ }
+ // Check over each browser condition to determine if we are running in a compatible client
+ if ( typeof map !== 'object' || map[profile.name] === undefined ) {
+ // Not found, return true if exactMatchOnly not set, false otherwise
+ return !exactMatchOnly;
+ }
+ conditions = map[profile.name];
+ if ( conditions === false ) {
+ // Match no versions
+ return false;
+ }
+ if ( conditions === null ) {
+ // Match all versions
+ return true;
+ }
+ for ( i = 0; i < conditions.length; i++ ) {
+ op = conditions[i][0];
+ val = conditions[i][1];
+ if ( typeof val === 'string' ) {
+ // Perform a component-wise comparison of versions, similar to PHP's version_compare
+ // but simpler. '1.11' is larger than '1.2'.
+ pieceVersion = profile.version.toString().split( '.' );
+ pieceVal = val.split( '.' );
+ // Extend with zeroes to equal length
+ while ( pieceVersion.length < pieceVal.length ) {
+ pieceVersion.push( '0' );
+ }
+ while ( pieceVal.length < pieceVersion.length ) {
+ pieceVal.push( '0' );
+ }
+ // Compare components
+ compare = 0;
+ for ( j = 0; j < pieceVersion.length; j++ ) {
+ if ( Number( pieceVersion[j] ) < Number( pieceVal[j] ) ) {
+ compare = -1;
+ break;
+ } else if ( Number( pieceVersion[j] ) > Number( pieceVal[j] ) ) {
+ compare = 1;
+ break;
+ }
+ }
+ // compare will be -1, 0 or 1, depending on comparison result
+ if ( !( eval( '' + compare + op + '0' ) ) ) {
+ return false;
+ }
+ } else if ( typeof val === 'number' ) {
+ if ( !( eval( 'profile.versionNumber' + op + val ) ) ) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ };
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.color.js b/resources/src/jquery/jquery.color.js
new file mode 100644
index 00000000..04f8047b
--- /dev/null
+++ b/resources/src/jquery/jquery.color.js
@@ -0,0 +1,55 @@
+/**
+ * jQuery Color Animations
+ *
+ * @author John Resig, 2007
+ * @author Krinkle, 2011
+ * Released under the MIT and GPL licenses.
+ *
+ * - 2011-01-05: Forked for MediaWiki. See also jQuery.colorUtil plugin
+ */
+( function ( $ ) {
+
+ function getColor( elem, attr ) {
+ /*jshint boss:true */
+ var color;
+
+ do {
+ color = $.css( elem, attr );
+
+ // Keep going until we find an element that has color, or we hit the body
+ if ( color !== '' && color !== 'transparent' || $.nodeName( elem, 'body' ) ) {
+ break;
+ }
+
+ attr = 'backgroundColor';
+ } while ( elem = elem.parentNode );
+
+ return $.colorUtil.getRGB( color );
+ }
+
+ // We override the animation for all of these color styles
+ $.each([
+ 'backgroundColor',
+ 'borderBottomColor',
+ 'borderLeftColor',
+ 'borderRightColor',
+ 'borderTopColor',
+ 'color',
+ 'outlineColor'
+ ], function ( i, attr ) {
+ $.fx.step[attr] = function ( fx ) {
+ if ( !fx.colorInit ) {
+ fx.start = getColor( fx.elem, attr );
+ fx.end = $.colorUtil.getRGB( fx.end );
+ fx.colorInit = true;
+ }
+
+ fx.elem.style[attr] = 'rgb(' + [
+ Math.max( Math.min( parseInt( (fx.pos * (fx.end[0] - fx.start[0])) + fx.start[0], 10 ), 255 ), 0 ),
+ Math.max( Math.min( parseInt( (fx.pos * (fx.end[1] - fx.start[1])) + fx.start[1], 10 ), 255 ), 0 ),
+ Math.max( Math.min( parseInt( (fx.pos * (fx.end[2] - fx.start[2])) + fx.start[2], 10 ), 255 ), 0 )
+ ].join( ',' ) + ')';
+ };
+ } );
+
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.colorUtil.js b/resources/src/jquery/jquery.colorUtil.js
new file mode 100644
index 00000000..a6ff8bc8
--- /dev/null
+++ b/resources/src/jquery/jquery.colorUtil.js
@@ -0,0 +1,262 @@
+/*!
+ * jQuery Color Utilities
+ *
+ * Released under the MIT and GPL licenses.
+ *
+ * Mostly based on other plugins and functions (linted and optimized a little).
+ * Sources cited inline.
+ */
+( function ( $ ) {
+ /**
+ * @class jQuery.colorUtil
+ * @singleton
+ */
+ $.colorUtil = {
+
+ /**
+ * Parse CSS color strings looking for color tuples
+ *
+ * Based on highlightFade by Blair Mitchelmore
+ * <http://jquery.offput.ca/highlightFade/>
+ *
+ * @param {Array|string} color
+ * @return {Array}
+ */
+ getRGB: function ( color ) {
+ /*jshint boss:true */
+ var result;
+
+ // Check if we're already dealing with an array of colors
+ if ( color && $.isArray( color ) && color.length === 3 ) {
+ return color;
+ }
+
+ // Look for rgb(num,num,num)
+ if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)) {
+ return [
+ parseInt( result[1], 10 ),
+ parseInt( result[2], 10 ),
+ parseInt( result[3], 10 )
+ ];
+ }
+
+ // Look for rgb(num%,num%,num%)
+ if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)) {
+ return [
+ parseFloat( result[1] ) * 2.55,
+ parseFloat( result[2] ) * 2.55,
+ parseFloat( result[3] ) * 2.55
+ ];
+ }
+
+ // Look for #a0b1c2
+ if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)) {
+ return [
+ parseInt( result[1], 16 ),
+ parseInt( result[2], 16 ),
+ parseInt( result[3], 16 )
+ ];
+ }
+
+ // Look for #fff
+ if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)) {
+ return [
+ parseInt( result[1] + result[1], 16 ),
+ parseInt( result[2] + result[2], 16 ),
+ parseInt( result[3] + result[3], 16)
+ ];
+ }
+
+ // Look for rgba(0, 0, 0, 0) == transparent in Safari 3
+ if (result = /rgba\(0, 0, 0, 0\)/.exec(color)) {
+ return $.colorUtil.colors.transparent;
+ }
+
+ // Otherwise, we're most likely dealing with a named color
+ return $.colorUtil.colors[$.trim(color).toLowerCase()];
+ },
+
+ /**
+ * Named color map
+ *
+ * Based on Interface by Stefan Petre
+ * <http://interface.eyecon.ro/>
+ *
+ * @property {Object}
+ */
+ colors: {
+ aqua: [0, 255, 255],
+ azure: [240, 255, 255],
+ beige: [245, 245, 220],
+ black: [0, 0, 0],
+ blue: [0, 0, 255],
+ brown: [165, 42, 42],
+ cyan: [0, 255, 255],
+ darkblue: [0, 0, 139],
+ darkcyan: [0, 139, 139],
+ darkgrey: [169, 169, 169],
+ darkgreen: [0, 100, 0],
+ darkkhaki: [189, 183, 107],
+ darkmagenta: [139, 0, 139],
+ darkolivegreen: [85, 107, 47],
+ darkorange: [255, 140, 0],
+ darkorchid: [153, 50, 204],
+ darkred: [139, 0, 0],
+ darksalmon: [233, 150, 122],
+ darkviolet: [148, 0, 211],
+ fuchsia: [255, 0, 255],
+ gold: [255, 215, 0],
+ green: [0, 128, 0],
+ indigo: [75, 0, 130],
+ khaki: [240, 230, 140],
+ lightblue: [173, 216, 230],
+ lightcyan: [224, 255, 255],
+ lightgreen: [144, 238, 144],
+ lightgrey: [211, 211, 211],
+ lightpink: [255, 182, 193],
+ lightyellow: [255, 255, 224],
+ lime: [0, 255, 0],
+ magenta: [255, 0, 255],
+ maroon: [128, 0, 0],
+ navy: [0, 0, 128],
+ olive: [128, 128, 0],
+ orange: [255, 165, 0],
+ pink: [255, 192, 203],
+ purple: [128, 0, 128],
+ violet: [128, 0, 128],
+ red: [255, 0, 0],
+ silver: [192, 192, 192],
+ white: [255, 255, 255],
+ yellow: [255, 255, 0],
+ transparent: [255, 255, 255]
+ },
+
+ /**
+ * Convert an RGB color value to HSL.
+ *
+ * Conversion formula based on
+ * <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>
+ *
+ * Adapted from <https://en.wikipedia.org/wiki/HSL_color_space>.
+ *
+ * Assumes `r`, `g`, and `b` are contained in the set `[0, 255]` and
+ * returns `h`, `s`, and `l` in the set `[0, 1]`.
+ *
+ * @param {number} r The red color value
+ * @param {number} g The green color value
+ * @param {number} b The blue color value
+ * @return {number[]} The HSL representation
+ */
+ rgbToHsl: function ( r, g, b ) {
+ r = r / 255;
+ g = g / 255;
+ b = b / 255;
+
+ var d,
+ max = Math.max( r, g, b ),
+ min = Math.min( r, g, b ),
+ h,
+ s,
+ l = (max + min) / 2;
+
+ if ( max === min ) {
+ // achromatic
+ h = s = 0;
+ } else {
+ d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch ( max ) {
+ case r:
+ h = (g - b) / d + (g < b ? 6 : 0);
+ break;
+ case g:
+ h = (b - r) / d + 2;
+ break;
+ case b:
+ h = (r - g) / d + 4;
+ break;
+ }
+ h /= 6;
+ }
+
+ return [h, s, l];
+ },
+
+ /**
+ * Convert an HSL color value to RGB.
+ *
+ * Conversion formula based on
+ * <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>
+ *
+ * Adapted from <https://en.wikipedia.org/wiki/HSL_color_space>.
+ *
+ * Assumes `h`, `s`, and `l` are contained in the set `[0, 1]` and
+ * returns `r`, `g`, and `b` in the set `[0, 255]`.
+ *
+ * @param {number} h The hue
+ * @param {number} s The saturation
+ * @param {number} l The lightness
+ * @return {number[]} The RGB representation
+ */
+ hslToRgb: function ( h, s, l ) {
+ var r, g, b, hue2rgb, q, p;
+
+ if ( s === 0 ) {
+ r = g = b = l; // achromatic
+ } else {
+ hue2rgb = function ( p, q, t ) {
+ if ( t < 0 ) {
+ t += 1;
+ }
+ if ( t > 1 ) {
+ t -= 1;
+ }
+ if ( t < 1 / 6 ) {
+ return p + (q - p) * 6 * t;
+ }
+ if ( t < 1 / 2 ) {
+ return q;
+ }
+ if ( t < 2 / 3 ) {
+ return p + (q - p) * (2 / 3 - t) * 6;
+ }
+ return p;
+ };
+
+ q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ p = 2 * l - q;
+ r = hue2rgb( p, q, h + 1 / 3 );
+ g = hue2rgb( p, q, h );
+ b = hue2rgb( p, q, h - 1 / 3 );
+ }
+
+ return [r * 255, g * 255, b * 255];
+ },
+
+ /**
+ * Get a brighter or darker rgb() value string.
+ *
+ * Usage:
+ *
+ * $.colorUtil.getColorBrightness( 'red', +0.1 );
+ * // > "rgb(255,50,50)"
+ * $.colorUtil.getColorBrightness( 'rgb(200,50,50)', -0.2 );
+ * // > "rgb(118,29,29)"
+ *
+ * @param {Mixed} currentColor Current value in css
+ * @param {number} mod Wanted brightness modification between -1 and 1
+ * @return {string} Like `'rgb(r,g,b)'`
+ */
+ getColorBrightness: function ( currentColor, mod ) {
+ var rgbArr = $.colorUtil.getRGB( currentColor ),
+ hslArr = $.colorUtil.rgbToHsl(rgbArr[0], rgbArr[1], rgbArr[2] );
+ rgbArr = $.colorUtil.hslToRgb(hslArr[0], hslArr[1], hslArr[2] + mod);
+
+ return 'rgb(' +
+ [parseInt( rgbArr[0], 10), parseInt( rgbArr[1], 10 ), parseInt( rgbArr[2], 10 )].join( ',' ) +
+ ')';
+ }
+
+ };
+
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.confirmable.css b/resources/src/jquery/jquery.confirmable.css
new file mode 100644
index 00000000..de690726
--- /dev/null
+++ b/resources/src/jquery/jquery.confirmable.css
@@ -0,0 +1,28 @@
+.jquery-confirmable-button {
+ /* Automatically flipped */
+ margin-left: 1ex;
+}
+
+.jquery-confirmable-wrapper {
+ /* Line breaks within the interface text are unpleasant */
+ white-space: nowrap;
+ /* Hiding the original text when it slides to the left */
+ overflow: hidden;
+}
+
+.jquery-confirmable-wrapper,
+.jquery-confirmable-element,
+.jquery-confirmable-interface {
+ /* We need inline-block to be able to size the elements and calculate their dimensions */
+ display: inline-block;
+ /* inline-block elements in this context align to baseline by default */
+ vertical-align: bottom;
+}
+
+.jquery-confirmable-element {
+ transition: margin 250ms cubic-bezier(0.2, 0.8, 0.2, 0.8);
+}
+
+.jquery-confirmable-interface {
+ transition: width 250ms cubic-bezier(0.2, 0.8, 0.2, 0.8);
+}
diff --git a/resources/src/jquery/jquery.confirmable.js b/resources/src/jquery/jquery.confirmable.js
new file mode 100644
index 00000000..339e65a4
--- /dev/null
+++ b/resources/src/jquery/jquery.confirmable.js
@@ -0,0 +1,170 @@
+/**
+ * jQuery confirmable plugin
+ *
+ * Released under the MIT License.
+ *
+ * @author Bartosz Dziewoński
+ *
+ * @class jQuery.plugin.confirmable
+ */
+( function ( $ ) {
+ var identity = function ( data ) {
+ return data;
+ };
+
+ /**
+ * Enable inline confirmation for given clickable element (like `<a />` or `<button />`).
+ *
+ * An additional inline confirmation step being shown before the default action is carried out on
+ * click.
+ *
+ * Calling `.confirmable( { handler: function () { … } } )` will fire the handler only after the
+ * confirmation step.
+ *
+ * The element will have the `jquery-confirmable-element` class added to it when it's clicked for
+ * the first time, which has `white-space: nowrap;` and `display: inline-block;` defined in CSS.
+ * If the computed values for the element are different when you make it confirmable, you might
+ * encounter unexpected behavior.
+ *
+ * @param {Object} [options]
+ * @param {string} [options.events='click'] Events to hook to.
+ * @param {Function} [options.wrapperCallback] Callback to fire when preparing confirmable
+ * interface. Receives the interface jQuery object as the only parameter.
+ * @param {Function} [options.buttonCallback] Callback to fire when preparing confirmable buttons.
+ * It is fired separately for the 'Yes' and 'No' button. Receives the button jQuery object as
+ * the first parameter and 'yes' or 'no' as the second.
+ * @param {Function} [options.handler] Callback to fire when the action is confirmed (user clicks
+ * the 'Yes' button).
+ * @param {string} [options.i18n] Text to use for interface elements.
+ * @param {string} [options.i18n.space] Word separator to place between the three text messages.
+ * @param {string} [options.i18n.confirm] Text to use for the confirmation question.
+ * @param {string} [options.i18n.yes] Text to use for the 'Yes' button.
+ * @param {string} [options.i18n.no] Text to use for the 'No' button.
+ *
+ * @chainable
+ */
+ $.fn.confirmable = function ( options ) {
+ options = $.extend( true, {}, $.fn.confirmable.defaultOptions, options || {} );
+
+ return this.on( options.events, function ( e ) {
+ var $element, $text, $buttonYes, $buttonNo, $wrapper, $interface, $elementClone,
+ interfaceWidth, elementWidth, rtl, positionOffscreen, positionRestore, sideMargin;
+
+ $element = $( this );
+
+ if ( $element.data( 'jquery-confirmable-button' ) ) {
+ // We're running on a clone of this element that represents the 'Yes' or 'No' button.
+ // (This should never happen for the 'No' case unless calling code does bad things.)
+ return;
+ }
+
+ // Only prevent native event handling. Stopping other JavaScript event handlers
+ // is impossible because they might have already run (we have no control over the order).
+ e.preventDefault();
+
+ rtl = $element.css( 'direction' ) === 'rtl';
+ if ( rtl ) {
+ positionOffscreen = { position: 'absolute', right: '-9999px' };
+ positionRestore = { position: '', right: '' };
+ sideMargin = 'marginRight';
+ } else {
+ positionOffscreen = { position: 'absolute', left: '-9999px' };
+ positionRestore = { position: '', left: '' };
+ sideMargin = 'marginLeft';
+ }
+
+ if ( $element.hasClass( 'jquery-confirmable-element' ) ) {
+ $wrapper = $element.closest( '.jquery-confirmable-wrapper' );
+ $interface = $wrapper.find( '.jquery-confirmable-interface' );
+ $text = $interface.find( '.jquery-confirmable-text' );
+ $buttonYes = $interface.find( '.jquery-confirmable-button-yes' );
+ $buttonNo = $interface.find( '.jquery-confirmable-button-no' );
+
+ interfaceWidth = $interface.data( 'jquery-confirmable-width' );
+ elementWidth = $element.data( 'jquery-confirmable-width' );
+ } else {
+ $elementClone = $element.clone( true );
+ $element.addClass( 'jquery-confirmable-element' );
+
+ elementWidth = $element.width();
+ $element.data( 'jquery-confirmable-width', elementWidth );
+
+ $wrapper = $( '<span>' )
+ .addClass( 'jquery-confirmable-wrapper' );
+ $element.wrap( $wrapper );
+
+ // Build the mini-dialog
+ $text = $( '<span>' )
+ .addClass( 'jquery-confirmable-text' )
+ .text( options.i18n.confirm );
+
+ // Clone original element along with event handlers to easily replicate its behavior.
+ // We could fiddle with .trigger() etc., but that is troublesome especially since
+ // Safari doesn't implement .click() on <a> links and jQuery follows suit.
+ $buttonYes = $elementClone.clone( true )
+ .addClass( 'jquery-confirmable-button jquery-confirmable-button-yes' )
+ .data( 'jquery-confirmable-button', true )
+ .text( options.i18n.yes );
+ if ( options.handler ) {
+ $buttonYes.on( options.events, options.handler );
+ }
+ $buttonYes = options.buttonCallback( $buttonYes, 'yes' );
+
+ // Clone it without any events and prevent default action to represent the 'No' button.
+ $buttonNo = $elementClone.clone( false )
+ .addClass( 'jquery-confirmable-button jquery-confirmable-button-no' )
+ .data( 'jquery-confirmable-button', true )
+ .text( options.i18n.no )
+ .on( options.events, function ( e ) {
+ $element.css( sideMargin, 0 );
+ $interface.css( 'width', 0 );
+ e.preventDefault();
+ } );
+ $buttonNo = options.buttonCallback( $buttonNo, 'no' );
+
+ // Prevent memory leaks
+ $elementClone.remove();
+
+ $interface = $( '<span>' )
+ .addClass( 'jquery-confirmable-interface' )
+ .append( $text, options.i18n.space, $buttonYes, options.i18n.space, $buttonNo );
+ $interface = options.wrapperCallback( $interface );
+
+ // Render offscreen to measure real width
+ $interface.css( positionOffscreen );
+ // Insert it in the correct place while we're at it
+ $element.after( $interface );
+ interfaceWidth = $interface.width();
+ $interface.data( 'jquery-confirmable-width', interfaceWidth );
+ $interface.css( positionRestore );
+
+ // Hide to animate the transition later
+ $interface.css( 'width', 0 );
+ }
+
+ // Hide element, show interface. This triggers both transitions.
+ // In a timeout to trigger the 'width' transition.
+ setTimeout( function () {
+ $element.css( sideMargin, -elementWidth );
+ $interface.css( 'width', interfaceWidth );
+ }, 1 );
+ } );
+ };
+
+ /**
+ * Default options. Overridable primarily for internationalisation handling.
+ * @property {Object} defaultOptions
+ */
+ $.fn.confirmable.defaultOptions = {
+ events: 'click',
+ wrapperCallback: identity,
+ buttonCallback: identity,
+ handler: null,
+ i18n: {
+ space: ' ',
+ confirm: 'Are you sure?',
+ yes: 'Yes',
+ no: 'No'
+ }
+ };
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.confirmable.mediawiki.js b/resources/src/jquery/jquery.confirmable.mediawiki.js
new file mode 100644
index 00000000..d4a106e3
--- /dev/null
+++ b/resources/src/jquery/jquery.confirmable.mediawiki.js
@@ -0,0 +1,14 @@
+/*!
+ * jQuery confirmable plugin customization for MediaWiki
+ *
+ * This file serves to inject our localised messages into it.
+ */
+
+( function ( mw, $ ) {
+ $.fn.confirmable.defaultOptions.i18n = {
+ space: mw.message( 'word-separator' ).text(),
+ confirm: mw.message( 'confirmable-confirm', mw.user ).text(),
+ yes: mw.message( 'confirmable-yes' ).text(),
+ no: mw.message( 'confirmable-no' ).text()
+ };
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/jquery/jquery.expandableField.js b/resources/src/jquery/jquery.expandableField.js
new file mode 100644
index 00000000..732cc6ec
--- /dev/null
+++ b/resources/src/jquery/jquery.expandableField.js
@@ -0,0 +1,140 @@
+/**
+ * This plugin provides functionality to expand a text box on focus to double it's current width
+ *
+ * Usage:
+ *
+ * Set options:
+ * $('#textbox').expandableField( { option1: value1, option2: value2 } );
+ * $('#textbox').expandableField( option, value );
+ * Get option:
+ * value = $('#textbox').expandableField( option );
+ * Initialize:
+ * $('#textbox').expandableField();
+ *
+ * Options:
+ *
+ */
+( function ( $ ) {
+
+ $.expandableField = {
+ /**
+ * Expand the field, make the callback
+ */
+ expandField: function ( e, context ) {
+ context.config.beforeExpand.call( context.data.$field, context );
+ context.data.$field
+ .animate( { 'width': context.data.expandedWidth }, 'fast', function () {
+ context.config.afterExpand.call( this, context );
+ } );
+ },
+ /**
+ * Condense the field, make the callback
+ */
+ condenseField: function ( e, context ) {
+ context.config.beforeCondense.call( context.data.$field, context );
+ context.data.$field
+ .animate( { 'width': context.data.condensedWidth }, 'fast', function () {
+ context.config.afterCondense.call( this, context );
+ } );
+ },
+ /**
+ * Sets the value of a property, and updates the widget accordingly
+ * @param property String Name of property
+ * @param value Mixed Value to set property with
+ */
+ configure: function ( context, property, value ) {
+ // TODO: Validate creation using fallback values
+ context.config[property] = value;
+ }
+
+ };
+
+ $.fn.expandableField = function () {
+
+ // Multi-context fields
+ var returnValue,
+ args = arguments;
+
+ $( this ).each( function () {
+ var key, context, timeout;
+
+ /* Construction / Loading */
+
+ context = $( this ).data( 'expandableField-context' );
+
+ // TODO: Do we need to check both null and undefined?
+ if ( context === undefined || context === null ) {
+ context = {
+ config: {
+ // callback function for before collapse
+ beforeCondense: function () {},
+
+ // callback function for before expand
+ beforeExpand: function () {},
+
+ // callback function for after collapse
+ afterCondense: function () {},
+
+ // callback function for after expand
+ afterExpand: function () {},
+
+ // Whether the field should expand to the left or the right -- defaults to left
+ expandToLeft: true
+ }
+ };
+ }
+
+ /* API */
+ // Handle various calling styles
+ if ( args.length > 0 ) {
+ if ( typeof args[0] === 'object' ) {
+ // Apply set of properties
+ for ( key in args[0] ) {
+ $.expandableField.configure( context, key, args[0][key] );
+ }
+ } else if ( typeof args[0] === 'string' ) {
+ if ( args.length > 1 ) {
+ // Set property values
+ $.expandableField.configure( context, args[0], args[1] );
+
+ // TODO: Do we need to check both null and undefined?
+ } else if ( returnValue === null || returnValue === undefined ) {
+ // Get property values, but don't give access to internal data - returns only the first
+ returnValue = ( args[0] in context.config ? undefined : context.config[args[0]] );
+ }
+ }
+ }
+
+ /* Initialization */
+
+ if ( context.data === undefined ) {
+ context.data = {
+ // The width of the field in it's condensed state
+ condensedWidth: $( this ).width(),
+
+ // The width of the field in it's expanded state
+ expandedWidth: $( this ).width() * 2,
+
+ // Reference to the field
+ $field: $( this )
+ };
+
+ $( this )
+ .addClass( 'expandableField' )
+ .focus( function ( e ) {
+ clearTimeout( timeout );
+ $.expandableField.expandField( e, context );
+ } )
+ .blur( function ( e ) {
+ timeout = setTimeout( function () {
+ $.expandableField.condenseField( e, context );
+ }, 250 );
+ } );
+ }
+ // Store the context for next time
+ $( this ).data( 'expandableField-context', context );
+ } );
+ return returnValue !== undefined ? returnValue : $(this);
+ };
+
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.farbtastic.css b/resources/src/jquery/jquery.farbtastic.css
new file mode 100644
index 00000000..1c6428f8
--- /dev/null
+++ b/resources/src/jquery/jquery.farbtastic.css
@@ -0,0 +1,54 @@
+/**
+ * Farbtastic Color Picker 1.2
+ * © 2008 Steven Wittens
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+.farbtastic {
+ position: relative;
+}
+.farbtastic * {
+ position: absolute;
+ cursor: crosshair;
+}
+.farbtastic, .farbtastic .wheel {
+ width: 195px;
+ height: 195px;
+}
+.farbtastic .color, .farbtastic .overlay {
+ top: 47px;
+ left: 47px;
+ width: 101px;
+ height: 101px;
+}
+.farbtastic .wheel {
+ /* @embed */
+ background: url(images/wheel.png) no-repeat;
+ width: 195px;
+ height: 195px;
+}
+.farbtastic .overlay {
+ /* @embed */
+ background: url(images/mask.png) no-repeat;
+}
+.farbtastic .marker {
+ width: 17px;
+ height: 17px;
+ margin: -8px 0 0 -8px;
+ overflow: hidden;
+ /* @embed */
+ background: url(images/marker.png) no-repeat;
+}
+
diff --git a/resources/src/jquery/jquery.farbtastic.js b/resources/src/jquery/jquery.farbtastic.js
new file mode 100644
index 00000000..d7024cc8
--- /dev/null
+++ b/resources/src/jquery/jquery.farbtastic.js
@@ -0,0 +1,286 @@
+/**
+ * Farbtastic Color Picker 1.2
+ * © 2008 Steven Wittens
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+//Adapted to uniform style with jQuery UI widgets and slightly change behavior
+//TODO:
+// - remove duplicated code by replacing it with jquery.colorUtils and modern jQuery
+// - uniform code style
+
+jQuery.fn.farbtastic = function (callback) {
+ $.farbtastic(this, callback);
+ return this;
+};
+
+jQuery.farbtastic = function (container, callback) {
+ var container = $(container).get(0);
+ return container.farbtastic || (container.farbtastic = new jQuery._farbtastic(container, callback));
+}
+
+jQuery._farbtastic = function (container, callback) {
+ // Store farbtastic object
+ var fb = this;
+
+ // Insert markup
+ $(container).html('<div class="farbtastic ui-widget-content"><div class="color"></div><div class="wheel"></div><div class="overlay"></div><div class="h-marker marker"></div><div class="sl-marker marker"></div></div>');
+ $(container).addClass('ui-widget');
+ var e = $('.farbtastic', container);
+ fb.wheel = $('.wheel', container).get(0);
+ // Dimensions
+ fb.radius = 84;
+ fb.square = 100;
+ fb.width = 194;
+
+ // Fix background PNGs in IE6
+ if (navigator.appVersion.match(/MSIE [0-6]\./)) {
+ $('*', e).each(function () {
+ if (this.currentStyle.backgroundImage != 'none') {
+ var image = this.currentStyle.backgroundImage;
+ image = this.currentStyle.backgroundImage.slice(5, image.length - 2);
+ $(this).css({
+ 'backgroundImage': 'none',
+ 'filter': "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='" + image + "')"
+ });
+ }
+ });
+ }
+
+ /**
+ * Link to the given element(s) or callback.
+ */
+ fb.linkTo = function (callback) {
+ // Unbind previous nodes
+ if (typeof fb.callback == 'object') {
+ $(fb.callback).unbind('keyup', fb.updateValue);
+ }
+
+ // Reset color
+ fb.color = null;
+
+ // Bind callback or elements
+ if (typeof callback == 'function') {
+ fb.callback = callback;
+ }
+ else if (typeof callback == 'object' || typeof callback == 'string') {
+ fb.callback = $(callback);
+ fb.callback.bind('keyup', fb.updateValue);
+ if (fb.callback.get(0).value) {
+ fb.setColor(fb.callback.get(0).value);
+ }
+ }
+ return this;
+ }
+ fb.updateValue = function (event) {
+ if (this.value != fb.color) {
+ fb.setColor(this.value);
+ }
+ }
+
+ /**
+ * Change color with HTML syntax #123456
+ */
+ fb.setColor = function (color) {
+ var rgb = $.colorUtil.getRGB( color );
+ if (fb.color != color && rgb) {
+ rgb = rgb.slice( 0 ); //make a clone
+ //TODO: rewrite code so that this is not needed
+ rgb[0] /= 255;
+ rgb[1] /= 255;
+ rgb[2] /= 255;
+ fb.color = color;
+ fb.rgb = rgb;
+ fb.hsl = fb.RGBToHSL(fb.rgb);
+ fb.updateDisplay();
+ }
+ return this;
+ }
+
+ /**
+ * Change color with HSL triplet [0..1, 0..1, 0..1]
+ */
+ fb.setHSL = function (hsl) {
+ fb.hsl = hsl;
+ fb.rgb = fb.HSLToRGB(hsl);
+ fb.color = fb.pack(fb.rgb);
+ fb.updateDisplay();
+ return this;
+ }
+
+ /////////////////////////////////////////////////////
+
+ /**
+ * Retrieve the coordinates of the given event relative to the center
+ * of the widget.
+ */
+ fb.widgetCoords = function (event) {
+ var ref = $( fb.wheel ).offset();
+ return {
+ x: event.pageX - ref.left - fb.width / 2,
+ y: event.pageY - ref.top - fb.width / 2
+ };
+ }
+
+ /**
+ * Mousedown handler
+ */
+ fb.mousedown = function (event) {
+ // Capture mouse
+ if (!document.dragging) {
+ $(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup);
+ document.dragging = true;
+ }
+
+ // Check which area is being dragged
+ var pos = fb.widgetCoords(event);
+ fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) * 2 > fb.square;
+
+ // Process
+ fb.mousemove(event);
+ return false;
+ }
+
+ /**
+ * Mousemove handler
+ */
+ fb.mousemove = function (event) {
+ // Get coordinates relative to color picker center
+ var pos = fb.widgetCoords(event);
+
+ // Set new HSL parameters
+ if (fb.circleDrag) {
+ var hue = Math.atan2(pos.x, -pos.y) / 6.28;
+ if (hue < 0) hue += 1;
+ fb.setHSL([hue, fb.hsl[1], fb.hsl[2]]);
+ }
+ else {
+ var sat = Math.max(0, Math.min(1, -(pos.x / fb.square) + .5));
+ var lum = Math.max(0, Math.min(1, -(pos.y / fb.square) + .5));
+ fb.setHSL([fb.hsl[0], sat, lum]);
+ }
+ return false;
+ }
+
+ /**
+ * Mouseup handler
+ */
+ fb.mouseup = function () {
+ // Uncapture mouse
+ $(document).unbind('mousemove', fb.mousemove);
+ $(document).unbind('mouseup', fb.mouseup);
+ document.dragging = false;
+ }
+
+ /**
+ * Update the markers and styles
+ */
+ fb.updateDisplay = function () {
+ // Markers
+ var angle = fb.hsl[0] * 6.28;
+ $('.h-marker', e).css({
+ left: Math.round(Math.sin(angle) * fb.radius + fb.width / 2) + 'px',
+ top: Math.round(-Math.cos(angle) * fb.radius + fb.width / 2) + 'px'
+ });
+
+ $('.sl-marker', e).css({
+ left: Math.round(fb.square * (.5 - fb.hsl[1]) + fb.width / 2) + 'px',
+ top: Math.round(fb.square * (.5 - fb.hsl[2]) + fb.width / 2) + 'px'
+ });
+
+ // Saturation/Luminance gradient
+ $('.color', e).css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5])));
+
+ // Linked elements or callback
+ if (typeof fb.callback == 'object') {
+ // Set background/foreground color
+ $(fb.callback).css({
+ backgroundColor: fb.color,
+ color: fb.hsl[2] > 0.5 ? '#000' : '#fff'
+ });
+
+ // Change linked value
+ $(fb.callback).each(function() {
+ if ( $( this ).val() != fb.color) {
+ $( this ).val( fb.color ).change();
+ }
+ });
+ }
+ else if (typeof fb.callback == 'function') {
+ fb.callback.call(fb, fb.color);
+ }
+ }
+
+ /* Various color utility functions */
+ fb.pack = function (rgb) {
+ var r = Math.round(rgb[0] * 255);
+ var g = Math.round(rgb[1] * 255);
+ var b = Math.round(rgb[2] * 255);
+ return '#' + (r < 16 ? '0' : '') + r.toString(16) +
+ (g < 16 ? '0' : '') + g.toString(16) +
+ (b < 16 ? '0' : '') + b.toString(16);
+ }
+
+ fb.HSLToRGB = function (hsl) {
+ var m1, m2, r, g, b;
+ var h = hsl[0], s = hsl[1], l = hsl[2];
+ m2 = (l <= 0.5) ? l * (s + 1) : l + s - l*s;
+ m1 = l * 2 - m2;
+ return [this.hueToRGB(m1, m2, h+0.33333),
+ this.hueToRGB(m1, m2, h),
+ this.hueToRGB(m1, m2, h-0.33333)];
+ }
+
+ fb.hueToRGB = function (m1, m2, h) {
+ h = (h < 0) ? h + 1 : ((h > 1) ? h - 1 : h);
+ if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
+ if (h * 2 < 1) return m2;
+ if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6;
+ return m1;
+ }
+
+ fb.RGBToHSL = function (rgb) {
+ var min, max, delta, h, s, l;
+ var r = rgb[0], g = rgb[1], b = rgb[2];
+ min = Math.min(r, Math.min(g, b));
+ max = Math.max(r, Math.max(g, b));
+ delta = max - min;
+ l = (min + max) / 2;
+ s = 0;
+ if (l > 0 && l < 1) {
+ s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l));
+ }
+ h = 0;
+ if (delta > 0) {
+ if (max == r && max != g) h += (g - b) / delta;
+ if (max == g && max != b) h += (2 + (b - r) / delta);
+ if (max == b && max != r) h += (4 + (r - g) / delta);
+ h /= 6;
+ }
+ return [h, s, l];
+ }
+
+ // Install mousedown handler (the others are set on the document on-demand)
+ $('*', e).mousedown(fb.mousedown);
+
+ // Init color
+ fb.setColor('#000000');
+
+ // Set linked elements/callback
+ if (callback) {
+ fb.linkTo(callback);
+ }
+}
diff --git a/resources/src/jquery/jquery.footHovzer.css b/resources/src/jquery/jquery.footHovzer.css
new file mode 100644
index 00000000..77d9514c
--- /dev/null
+++ b/resources/src/jquery/jquery.footHovzer.css
@@ -0,0 +1,6 @@
+#jquery-foot-hovzer {
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ z-index: 1000;
+}
diff --git a/resources/src/jquery/jquery.footHovzer.js b/resources/src/jquery/jquery.footHovzer.js
new file mode 100644
index 00000000..de745c33
--- /dev/null
+++ b/resources/src/jquery/jquery.footHovzer.js
@@ -0,0 +1,66 @@
+/**
+ * @class jQuery.plugin.footHovzer
+ */
+( function ( $ ) {
+ var $hovzer, footHovzer, prevHeight, newHeight;
+
+ function getHovzer() {
+ if ( $hovzer === undefined ) {
+ $hovzer = $( '<div id="jquery-foot-hovzer"></div>' ).appendTo( 'body' );
+ }
+ return $hovzer;
+ }
+
+ /**
+ * Utility to stack stuff in an overlay fixed on the bottom of the page.
+ *
+ * Usage:
+ *
+ * var hovzer = $.getFootHovzer();
+ * hovzer.$.append( $myCollection );
+ * hovzer.update();
+ *
+ * @static
+ * @inheritable
+ * @return {jQuery.footHovzer}
+ */
+ $.getFootHovzer = function () {
+ footHovzer.$ = getHovzer();
+ return footHovzer;
+ };
+
+ /**
+ * @private
+ * @class jQuery.footHovzer
+ */
+ footHovzer = {
+
+ /**
+ * @property {jQuery} $ The stack container
+ */
+
+ /**
+ * Update dimensions of stack to account for changes in the subtree.
+ */
+ update: function () {
+ var $body;
+
+ $body = $( 'body' );
+ if ( prevHeight === undefined ) {
+ prevHeight = getHovzer().outerHeight( /* includeMargin = */ true );
+ $body.css( 'paddingBottom', '+=' + prevHeight + 'px' );
+ } else {
+ newHeight = getHovzer().outerHeight( true );
+ $body.css( 'paddingBottom', ( parseFloat( $body.css( 'paddingBottom' ) ) - prevHeight ) + newHeight );
+
+ prevHeight = newHeight;
+ }
+ }
+ };
+
+ /**
+ * @class jQuery
+ * @mixins jQuery.plugin.footHovzer
+ */
+
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.getAttrs.js b/resources/src/jquery/jquery.getAttrs.js
new file mode 100644
index 00000000..c44831c4
--- /dev/null
+++ b/resources/src/jquery/jquery.getAttrs.js
@@ -0,0 +1,42 @@
+/**
+ * @class jQuery.plugin.getAttrs
+ */
+
+/**
+ * Get the attributes of an element directy as a plain object.
+ *
+ * If there are more elements in the collection, like most jQuery get/read methods,
+ * this method will use the first element in the collection.
+ *
+ * In IE6, the `attributes` map of a node includes *all* allowed attributes
+ * for an element (including those not set). Those will have values like
+ * `undefined`, `null`, `0`, `false`, `""` or `"inherit"`.
+ *
+ * However there may be attributes genuinely set to one of those values, and there
+ * is no way to distinguish between attributes set to that and those not set and
+ * it being the default. If you need them, set `all` to `true`. They are filtered out
+ * by default.
+ *
+ * @param {boolean} [all=false]
+ * @return {Object}
+ */
+jQuery.fn.getAttrs = function ( all ) {
+ var map = this[0].attributes,
+ attrs = {},
+ len = map.length,
+ i, v;
+
+ for ( i = 0; i < len; i++ ) {
+ v = map[i].nodeValue;
+ if ( all || ( v && v !== 'inherit' ) ) {
+ attrs[ map[i].nodeName ] = v;
+ }
+ }
+
+ return attrs;
+};
+
+/**
+ * @class jQuery
+ * @mixins jQuery.plugin.getAttrs
+ */
diff --git a/resources/src/jquery/jquery.hidpi.js b/resources/src/jquery/jquery.hidpi.js
new file mode 100644
index 00000000..4ecfeb88
--- /dev/null
+++ b/resources/src/jquery/jquery.hidpi.js
@@ -0,0 +1,129 @@
+/**
+ * Responsive images based on `srcset` and `window.devicePixelRatio` emulation where needed.
+ *
+ * Call `.hidpi()` on a document or part of a document to proces image srcsets within that section.
+ *
+ * `$.devicePixelRatio()` can be used as a substitute for `window.devicePixelRatio`.
+ * It provides a familiar interface to retrieve the pixel ratio for browsers that don't
+ * implement `window.devicePixelRatio` but do have a different way of getting it.
+ *
+ * @class jQuery.plugin.hidpi
+ */
+( function ( $ ) {
+
+/**
+ * Get reported or approximate device pixel ratio.
+ *
+ * - 1.0 means 1 CSS pixel is 1 hardware pixel
+ * - 2.0 means 1 CSS pixel is 2 hardware pixels
+ * - etc.
+ *
+ * Uses `window.devicePixelRatio` if available, or CSS media queries on IE.
+ *
+ * @static
+ * @inheritable
+ * @return {number} Device pixel ratio
+ */
+$.devicePixelRatio = function () {
+ if ( window.devicePixelRatio !== undefined ) {
+ // Most web browsers:
+ // * WebKit (Safari, Chrome, Android browser, etc)
+ // * Opera
+ // * Firefox 18+
+ return window.devicePixelRatio;
+ } else if ( window.msMatchMedia !== undefined ) {
+ // Windows 8 desktops / tablets, probably Windows Phone 8
+ //
+ // IE 10 doesn't report pixel ratio directly, but we can get the
+ // screen DPI and divide by 96. We'll bracket to [1, 1.5, 2.0] for
+ // simplicity, but you may get different values depending on zoom
+ // factor, size of screen and orientation in Metro IE.
+ if ( window.msMatchMedia( '(min-resolution: 192dpi)' ).matches ) {
+ return 2;
+ } else if ( window.msMatchMedia( '(min-resolution: 144dpi)' ).matches ) {
+ return 1.5;
+ } else {
+ return 1;
+ }
+ } else {
+ // Legacy browsers...
+ // Assume 1 if unknown.
+ return 1;
+ }
+};
+
+/**
+ * Implement responsive images based on srcset attributes, if browser has no
+ * native srcset support.
+ *
+ * @return {jQuery} This selection
+ * @chainable
+ */
+$.fn.hidpi = function () {
+ var $target = this,
+ // @todo add support for dpi media query checks on Firefox, IE
+ devicePixelRatio = $.devicePixelRatio(),
+ testImage = new Image();
+
+ if ( devicePixelRatio > 1 && testImage.srcset === undefined ) {
+ // No native srcset support.
+ $target.find( 'img' ).each( function () {
+ var $img = $( this ),
+ srcset = $img.attr( 'srcset' ),
+ match;
+ if ( typeof srcset === 'string' && srcset !== '' ) {
+ match = $.matchSrcSet( devicePixelRatio, srcset );
+ if (match !== null ) {
+ $img.attr( 'src', match );
+ }
+ }
+ });
+ }
+
+ return $target;
+};
+
+/**
+ * Match a srcset entry for the given device pixel ratio
+ *
+ * Exposed for testing.
+ *
+ * @private
+ * @static
+ * @param {number} devicePixelRatio
+ * @param {string} srcset
+ * @return {Mixed} null or the matching src string
+ */
+$.matchSrcSet = function ( devicePixelRatio, srcset ) {
+ var candidates,
+ candidate,
+ bits,
+ src,
+ i,
+ ratioStr,
+ ratio,
+ selectedRatio = 1,
+ selectedSrc = null;
+ candidates = srcset.split( / *, */ );
+ for ( i = 0; i < candidates.length; i++ ) {
+ candidate = candidates[i];
+ bits = candidate.split( / +/ );
+ src = bits[0];
+ if ( bits.length > 1 && bits[1].charAt( bits[1].length - 1 ) === 'x' ) {
+ ratioStr = bits[1].slice( 0, -1 );
+ ratio = parseFloat( ratioStr );
+ if ( ratio <= devicePixelRatio && ratio > selectedRatio ) {
+ selectedRatio = ratio;
+ selectedSrc = src;
+ }
+ }
+ }
+ return selectedSrc;
+};
+
+/**
+ * @class jQuery
+ * @mixins jQuery.plugin.hidpi
+ */
+
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.highlightText.js b/resources/src/jquery/jquery.highlightText.js
new file mode 100644
index 00000000..13382182
--- /dev/null
+++ b/resources/src/jquery/jquery.highlightText.js
@@ -0,0 +1,73 @@
+/**
+ * Plugin that highlights matched word partials in a given element.
+ * TODO: Add a function for restoring the previous text.
+ * TODO: Accept mappings for converting shortcuts like WP: to Wikipedia:.
+ */
+( function ( $ ) {
+
+ $.highlightText = {
+
+ // Split our pattern string at spaces and run our highlight function on the results
+ splitAndHighlight: function ( node, pat ) {
+ var i,
+ patArray = pat.split( ' ' );
+ for ( i = 0; i < patArray.length; i++ ) {
+ if ( patArray[i].length === 0 ) {
+ continue;
+ }
+ $.highlightText.innerHighlight( node, patArray[i] );
+ }
+ return node;
+ },
+
+ // scans a node looking for the pattern and wraps a span around each match
+ innerHighlight: function ( node, pat ) {
+ var i, match, pos, spannode, middlebit, middleclone;
+ // if this is a text node
+ if ( node.nodeType === 3 ) {
+ // TODO - need to be smarter about the character matching here.
+ // non latin characters can make regex think a new word has begun: do not use \b
+ // http://stackoverflow.com/questions/3787072/regex-wordwrap-with-utf8-characters-in-js
+ // look for an occurrence of our pattern and store the starting position
+ match = node.data.match( new RegExp( '(^|\\s)' + $.escapeRE( pat ), 'i' ) );
+ if ( match ) {
+ pos = match.index + match[1].length; // include length of any matched spaces
+ // create the span wrapper for the matched text
+ spannode = document.createElement( 'span' );
+ spannode.className = 'highlight';
+ // shave off the characters preceding the matched text
+ middlebit = node.splitText( pos );
+ // shave off any unmatched text off the end
+ middlebit.splitText( pat.length );
+ // clone for appending to our span
+ middleclone = middlebit.cloneNode( true );
+ // append the matched text node to the span
+ spannode.appendChild( middleclone );
+ // replace the matched node, with our span-wrapped clone of the matched node
+ middlebit.parentNode.replaceChild( spannode, middlebit );
+ }
+ // if this is an element with childnodes, and not a script, style or an element we created
+ } else if ( node.nodeType === 1
+ && node.childNodes
+ && !/(script|style)/i.test( node.tagName )
+ && !( node.tagName.toLowerCase() === 'span'
+ && node.className.match( /\bhighlight/ )
+ )
+ ) {
+ for ( i = 0; i < node.childNodes.length; ++i ) {
+ // call the highlight function for each child node
+ $.highlightText.innerHighlight( node.childNodes[i], pat );
+ }
+ }
+ }
+ };
+
+ $.fn.highlightText = function ( matchString ) {
+ return this.each( function () {
+ var $el = $( this );
+ $el.data( 'highlightText', { originalText: $el.text() } );
+ $.highlightText.splitAndHighlight( this, matchString );
+ } );
+ };
+
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.localize.js b/resources/src/jquery/jquery.localize.js
new file mode 100644
index 00000000..0b423545
--- /dev/null
+++ b/resources/src/jquery/jquery.localize.js
@@ -0,0 +1,170 @@
+/**
+ * @class jQuery.plugin.localize
+ */
+( function ( $, mw ) {
+
+/**
+ * Gets a localized message, using parameters from options if present.
+ * @ignore
+ *
+ * @param {Object} options
+ * @param {string} key
+ * @return {string} Localized message
+ */
+function msg( options, key ) {
+ var args = options.params[key] || [];
+ // Format: mw.msg( key [, p1, p2, ...] )
+ args.unshift( options.prefix + ( options.keys[key] || key ) );
+ return mw.msg.apply( mw, args );
+}
+
+/**
+ * Localizes a DOM selection by replacing <html:msg /> elements with localized text and adding
+ * localized title and alt attributes to elements with title-msg and alt-msg attributes
+ * respectively.
+ *
+ * Call on a selection of HTML which contains `<html:msg key="message-key" />` elements or elements
+ * with title-msg="message-key", alt-msg="message-key" or placeholder-msg="message-key" attributes.
+ * `<html:msg />` elements will be replaced with localized text, *-msg attributes will be replaced
+ * with attributes that do not have the "-msg" suffix and contain a localized message.
+ *
+ * Example:
+ * // Messages: { 'title': 'Awesome', 'desc': 'Cat doing backflip' 'search' contains 'Search' }
+ * var html = '\
+ * <p>\
+ * <html:msg key="title" />\
+ * <img src="something.jpg" title-msg="title" alt-msg="desc" />\
+ * <input type="text" placeholder-msg="search" />\
+ * </p>';
+ * $( 'body' ).append( $( html ).localize() );
+ *
+ * Appends something like this to the body...
+ * <p>
+ * Awesome
+ * <img src="something.jpg" title="Awesome" alt="Cat doing backflip" />
+ * <input type="text" placeholder="Search" />
+ * </p>
+ *
+ * Arguments can be passed into uses of a message using the params property of the options object
+ * given to .localize(). Multiple messages can be given parameters, because the params property is
+ * an object keyed by the message key to apply the parameters to, each containing an array of
+ * parameters to use. The limitation is that you can not use different parameters to individual uses
+ * of a message in the same selection being localized - they will all recieve the same parameters.
+ *
+ * Example:
+ * // Messages: { 'easy-as': 'Easy as $1 $2 $3.' }
+ * var html = '<p><html:msg key="easy-as" /></p>';
+ * $( 'body' ).append( $( html ).localize( { 'params': { 'easy-as': ['a', 'b', 'c'] } } ) );
+ *
+ * Appends something like this to the body...
+ * <p>Easy as a, b, c</p>
+ *
+ * Raw HTML content can be used, instead of it being escaped as text. To do this, just use the raw
+ * attribute on a msg element.
+ *
+ * Example:
+ * // Messages: { 'hello': '<b><i>Hello</i> $1!</b>' }
+ * var html = '\
+ * <p>\
+ * <!-- escaped: --><html:msg key="hello" />\
+ * <!-- raw: --><html:msg key="hello" raw />\
+ * </p>';
+ * $( 'body' ).append( $( html ).localize( { 'params': { 'hello': ['world'] } } ) );
+ *
+ * Appends something like this to the body...
+ * <p>
+ * <!-- escaped: -->&lt;b&gt;&lt;i&gt;Hello&lt;/i&gt; world!&lt;/b&gt;
+ * <!-- raw: --><b><i>Hello</i> world!</b>
+ * </p>
+ *
+ * Message keys can also be remapped, allowing the same generic template to be used with a variety
+ * of messages. This is important for improving re-usability of templates.
+ *
+ * Example:
+ * // Messages: { 'good-afternoon': 'Good afternoon' }
+ * var html = '<p><html:msg key="greeting" /></p>';
+ * $( 'body' ).append( $( html ).localize( { 'keys': { 'greeting': 'good-afternoon' } } ) );
+ *
+ * Appends something like this to the body...
+ * <p>Good afternoon</p>
+ *
+ * Message keys can also be prefixed globally, which is handy when writing extensions, where by
+ * convention all messages are prefixed with the extension's name.
+ *
+ * Example:
+ * // Messages: { 'teleportation-warning': 'You may not get there all in one piece.' }
+ * var html = '<p><html:msg key="warning" /></p>';
+ * $( 'body' ).append( $( html ).localize( { 'prefix': 'teleportation-' } ) );
+ *
+ * Appends something like this to the body...
+ * <p>You may not get there all in one piece.</p>
+ *
+ * @param {Object} options Map of options to be used while localizing
+ * @param {string} options.prefix String to prepend to all message keys
+ * @param {Object} options.keys Message key aliases, used for remapping keys to a template
+ * @param {Object} options.params Lists of parameters to use with certain message keys
+ * @return {jQuery}
+ * @chainable
+ */
+$.fn.localize = function ( options ) {
+ var $target = this,
+ attributes = ['title', 'alt', 'placeholder'];
+
+ // Extend options
+ options = $.extend( {
+ prefix: '',
+ keys: {},
+ params: {}
+ }, options );
+
+ // Elements
+ // Ok, so here's the story on this selector. In IE 6/7, searching for 'msg' turns up the
+ // 'html:msg', but searching for 'html:msg' doesn't. In later IE and other browsers, searching
+ // for 'html:msg' turns up the 'html:msg', but searching for 'msg' doesn't. So searching for
+ // both 'msg' and 'html:msg' seems to get the job done. This feels pretty icky, though.
+ $target.find( 'msg,html\\:msg' ).each( function () {
+ var $el = $( this );
+ // Escape by default
+ if ( $el.attr( 'raw' ) ) {
+ $el.html( msg( options, $el.attr( 'key' ) ) );
+ } else {
+ $el.text( msg( options, $el.attr( 'key' ) ) );
+ }
+ // Remove wrapper
+ $el.replaceWith( $el.html() );
+ } );
+
+ // Attributes
+ // Note: there's no way to prevent escaping of values being injected into attributes, this is
+ // on purpose, not a design flaw.
+ $.each( attributes, function ( i, attr ) {
+ var msgAttr = attr + '-msg';
+ $target.find( '[' + msgAttr + ']' ).each( function () {
+ var $el = $( this );
+ $el.attr( attr, msg( options, $el.attr( msgAttr ) ) ).removeAttr( msgAttr );
+ } );
+ } );
+
+ // HTML, Text for elements which cannot have children e.g. OPTION
+ $target.find( '[data-msg-text]' ).each( function () {
+ var $el = $( this );
+ $el.text( msg( options, $el.attr( 'data-msg-text' ) ) );
+ } );
+
+ $target.find( '[data-msg-html]' ).each( function () {
+ var $el = $( this );
+ $el.html( msg( options, $el.attr( 'data-msg-html' ) ) );
+ } );
+
+ return $target;
+};
+
+// Let IE know about the msg tag before it's used...
+document.createElement( 'msg' );
+
+/**
+ * @class jQuery
+ * @mixins jQuery.plugin.localize
+ */
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/jquery/jquery.makeCollapsible.css b/resources/src/jquery/jquery.makeCollapsible.css
new file mode 100644
index 00000000..0f471509
--- /dev/null
+++ b/resources/src/jquery/jquery.makeCollapsible.css
@@ -0,0 +1,27 @@
+/* See also jquery.makeCollapsible.js */
+.mw-collapsible-toggle {
+ float: right;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.mw-customtoggle,
+.mw-collapsible-toggle {
+ cursor: pointer;
+}
+
+/* collapse links in captions should be inline */
+caption .mw-collapsible-toggle {
+ float: none;
+}
+
+/* list-items go as wide as their parent element, don't float them inside list items */
+li .mw-collapsible-toggle {
+ float: none;
+}
+
+/* the added list item should have no list-style */
+.mw-collapsible-toggle-li {
+ list-style: none;
+}
diff --git a/resources/src/jquery/jquery.makeCollapsible.js b/resources/src/jquery/jquery.makeCollapsible.js
new file mode 100644
index 00000000..c4e25203
--- /dev/null
+++ b/resources/src/jquery/jquery.makeCollapsible.js
@@ -0,0 +1,404 @@
+/**
+ * jQuery makeCollapsible
+ *
+ * Dual licensed:
+ * - CC BY 3.0 <http://creativecommons.org/licenses/by/3.0>
+ * - GPL2 <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
+ *
+ * @class jQuery.plugin.makeCollapsible
+ */
+( function ( $, mw ) {
+
+ /**
+ * Handler for a click on a collapsible toggler.
+ *
+ * @private
+ * @param {jQuery} $collapsible
+ * @param {string} action The action this function will take ('expand' or 'collapse').
+ * @param {jQuery|null} [$defaultToggle]
+ * @param {Object|undefined} [options]
+ */
+ function toggleElement( $collapsible, action, $defaultToggle, options ) {
+ var $collapsibleContent, $containers, hookCallback;
+ options = options || {};
+
+ // Validate parameters
+
+ // $collapsible must be an instance of jQuery
+ if ( !$collapsible.jquery ) {
+ return;
+ }
+ if ( action !== 'expand' && action !== 'collapse' ) {
+ // action must be string with 'expand' or 'collapse'
+ return;
+ }
+ if ( $defaultToggle === undefined ) {
+ $defaultToggle = null;
+ }
+ if ( $defaultToggle !== null && !$defaultToggle.jquery ) {
+ // is optional (may be undefined), but if defined it must be an instance of jQuery.
+ // If it's not, abort right away.
+ // After this $defaultToggle is either null or a valid jQuery instance.
+ return;
+ }
+
+ // Trigger a custom event to allow callers to hook to the collapsing/expanding,
+ // allowing the module to be testable, and making it possible to
+ // e.g. implement persistence via cookies
+ $collapsible.trigger( action === 'expand' ? 'beforeExpand.mw-collapsible' : 'beforeCollapse.mw-collapsible' );
+ hookCallback = function () {
+ $collapsible.trigger( action === 'expand' ? 'afterExpand.mw-collapsible' : 'afterCollapse.mw-collapsible' );
+ };
+
+ // Handle different kinds of elements
+
+ if ( !options.plainMode && $collapsible.is( 'table' ) ) {
+ // Tables
+ // If there is a caption, hide all rows; otherwise, only hide body rows
+ if ( $collapsible.find( '> caption' ).length ) {
+ $containers = $collapsible.find( '> * > tr' );
+ } else {
+ $containers = $collapsible.find( '> tbody > tr' );
+ }
+ if ( $defaultToggle ) {
+ // Exclude table row containing togglelink
+ $containers = $containers.not( $defaultToggle.closest( 'tr' ) );
+ }
+
+ if ( action === 'collapse' ) {
+ // Hide all table rows of this table
+ // Slide doesn't work with tables, but fade does as of jQuery 1.1.3
+ // http://stackoverflow.com/questions/467336#920480
+ if ( options.instantHide ) {
+ $containers.hide();
+ hookCallback();
+ } else {
+ $containers.stop( true, true ).fadeOut().promise().done( hookCallback );
+ }
+ } else {
+ $containers.stop( true, true ).fadeIn().promise().done( hookCallback );
+ }
+
+ } else if ( !options.plainMode && ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) ) {
+ // Lists
+ $containers = $collapsible.find( '> li' );
+ if ( $defaultToggle ) {
+ // Exclude list-item containing togglelink
+ $containers = $containers.not( $defaultToggle.parent() );
+ }
+
+ if ( action === 'collapse' ) {
+ if ( options.instantHide ) {
+ $containers.hide();
+ hookCallback();
+ } else {
+ $containers.stop( true, true ).slideUp().promise().done( hookCallback );
+ }
+ } else {
+ $containers.stop( true, true ).slideDown().promise().done( hookCallback );
+ }
+
+ } else {
+ // Everything else: <div>, <p> etc.
+ $collapsibleContent = $collapsible.find( '> .mw-collapsible-content' );
+
+ // If a collapsible-content is defined, act on it
+ if ( !options.plainMode && $collapsibleContent.length ) {
+ if ( action === 'collapse' ) {
+ if ( options.instantHide ) {
+ $collapsibleContent.hide();
+ hookCallback();
+ } else {
+ $collapsibleContent.slideUp().promise().done( hookCallback );
+ }
+ } else {
+ $collapsibleContent.slideDown().promise().done( hookCallback );
+ }
+
+ // Otherwise assume this is a customcollapse with a remote toggle
+ // .. and there is no collapsible-content because the entire element should be toggled
+ } else {
+ if ( action === 'collapse' ) {
+ if ( options.instantHide ) {
+ $collapsible.hide();
+ hookCallback();
+ } else {
+ if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) {
+ $collapsible.fadeOut().promise().done( hookCallback );
+ } else {
+ $collapsible.slideUp().promise().done( hookCallback );
+ }
+ }
+ } else {
+ if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) {
+ $collapsible.fadeIn().promise().done( hookCallback );
+ } else {
+ $collapsible.slideDown().promise().done( hookCallback );
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Handle clicking/keypressing on the collapsible element toggle and other
+ * situations where a collapsible element is toggled (e.g. the initial
+ * toggle for collapsed ones).
+ *
+ * @private
+ * @param {jQuery} $toggle the clickable toggle itself
+ * @param {jQuery} $collapsible the collapsible element
+ * @param {jQuery.Event|null} e either the event or null if unavailable
+ * @param {Object|undefined} options
+ */
+ function togglingHandler( $toggle, $collapsible, e, options ) {
+ var wasCollapsed, $textContainer, collapseText, expandText;
+
+ if ( options === undefined ) {
+ options = {};
+ }
+
+ if ( e ) {
+ if ( e.type === 'click' && options.linksPassthru && $.nodeName( e.target, 'a' ) ) {
+ // Don't fire if a link was clicked, if requested (for premade togglers by default)
+ return;
+ } else if ( e.type === 'keypress' && e.which !== 13 && e.which !== 32 ) {
+ // Only handle keypresses on the "Enter" or "Space" keys
+ return;
+ } else {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+
+ // This allows the element to be hidden on initial toggle without fiddling with the class
+ if ( options.wasCollapsed !== undefined ) {
+ wasCollapsed = options.wasCollapsed;
+ } else {
+ wasCollapsed = $collapsible.hasClass( 'mw-collapsed' );
+ }
+
+ // Toggle the state of the collapsible element (that is, expand or collapse)
+ $collapsible.toggleClass( 'mw-collapsed', !wasCollapsed );
+
+ // Toggle the mw-collapsible-toggle classes, if requested (for default and premade togglers by default)
+ if ( options.toggleClasses ) {
+ $toggle
+ .toggleClass( 'mw-collapsible-toggle-collapsed', !wasCollapsed )
+ .toggleClass( 'mw-collapsible-toggle-expanded', wasCollapsed );
+ }
+
+ // Toggle the text ("Show"/"Hide"), if requested (for default togglers by default)
+ if ( options.toggleText ) {
+ collapseText = options.toggleText.collapseText;
+ expandText = options.toggleText.expandText;
+
+ $textContainer = $toggle.find( '> a' );
+ if ( !$textContainer.length ) {
+ $textContainer = $toggle;
+ }
+ $textContainer.text( wasCollapsed ? collapseText : expandText );
+ }
+
+ // And finally toggle the element state itself
+ toggleElement( $collapsible, wasCollapsed ? 'expand' : 'collapse', $toggle, options );
+ }
+
+ /**
+ * Enable collapsible-functionality on all elements in the collection.
+ *
+ * - Will prevent binding twice to the same element.
+ * - Initial state is expanded by default, this can be overriden by adding class
+ * "mw-collapsed" to the "mw-collapsible" element.
+ * - Elements made collapsible have jQuery data "mw-made-collapsible" set to true.
+ * - The inner content is wrapped in a "div.mw-collapsible-content" (except for tables and lists).
+ *
+ * @param {Object} [options]
+ * @param {string} [options.collapseText] Text used for the toggler, when clicking it would
+ * collapse the element. Default: the 'data-collapsetext' attribute of the
+ * collapsible element or the content of 'collapsible-collapse' message.
+ * @param {string} [options.expandText] Text used for the toggler, when clicking it would
+ * expand the element. Default: the 'data-expandtext' attribute of the
+ * collapsible element or the content of 'collapsible-expand' message.
+ * @param {boolean} [options.collapsed] Whether to collapse immediately. By default
+ * collapse only if the elements has the 'mw-collapsible' class.
+ * @param {jQuery} [options.$customTogglers] Elements to be used as togglers
+ * for this collapsible element. By default, if the collapsible element
+ * has an id attribute like 'mw-customcollapsible-XXX', elements with a
+ * *class* of 'mw-customtoggle-XXX' are made togglers for it.
+ * @param {boolean} [options.plainMode=false] Whether to use a "plain mode" when making the
+ * element collapsible - that is, hide entire tables and lists (instead
+ * of hiding only all rows but first of tables, and hiding each list
+ * item separately for lists) and don't wrap other elements in
+ * div.mw-collapsible-content. May only be used with custom togglers.
+ * @return {jQuery}
+ * @chainable
+ */
+ $.fn.makeCollapsible = function ( options ) {
+ if ( options === undefined ) {
+ options = {};
+ }
+
+ return this.each( function () {
+ var $collapsible, collapseText, expandText, $caption, $toggle, actionHandler, buildDefaultToggleLink,
+ premadeToggleHandler, $toggleLink, $firstItem, collapsibleId, $customTogglers, firstval;
+
+ // Ensure class "mw-collapsible" is present in case .makeCollapsible()
+ // is called on element(s) that don't have it yet.
+ $collapsible = $( this ).addClass( 'mw-collapsible' );
+
+ // Return if it has been enabled already.
+ if ( $collapsible.data( 'mw-made-collapsible' ) ) {
+ return;
+ } else {
+ $collapsible.data( 'mw-made-collapsible', true );
+ }
+
+ // Use custom text or default?
+ collapseText = options.collapseText || $collapsible.attr( 'data-collapsetext' ) || mw.msg( 'collapsible-collapse' );
+ expandText = options.expandText || $collapsible.attr( 'data-expandtext' ) || mw.msg( 'collapsible-expand' );
+
+ // Default click/keypress handler and toggle link to use when none is present
+ actionHandler = function ( e, opts ) {
+ var defaultOpts = {
+ toggleClasses: true,
+ toggleText: { collapseText: collapseText, expandText: expandText }
+ };
+ opts = $.extend( defaultOpts, options, opts );
+ togglingHandler( $( this ), $collapsible, e, opts );
+ };
+ // Default toggle link. Only build it when needed to avoid jQuery memory leaks (event data).
+ buildDefaultToggleLink = function () {
+ return $( '<a href="#"></a>' )
+ .text( collapseText )
+ .wrap( '<span class="mw-collapsible-toggle"></span>' )
+ .parent()
+ .prepend( '<span class="mw-collapsible-bracket">[</span>' )
+ .append( '<span class="mw-collapsible-bracket">]</span>' )
+ .on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler );
+ };
+
+ // Default handler for clicking on premade toggles
+ premadeToggleHandler = function ( e, opts ) {
+ var defaultOpts = { toggleClasses: true, linksPassthru: true };
+ opts = $.extend( defaultOpts, options, opts );
+ togglingHandler( $( this ), $collapsible, e, opts );
+ };
+
+ // Check if this element has a custom position for the toggle link
+ // (ie. outside the container or deeper inside the tree)
+ if ( options.$customTogglers ) {
+ $customTogglers = $( options.$customTogglers );
+ } else {
+ collapsibleId = $collapsible.attr( 'id' ) || '';
+ if ( collapsibleId.indexOf( 'mw-customcollapsible-' ) === 0 ) {
+ $customTogglers = $( '.' + collapsibleId.replace( 'mw-customcollapsible', 'mw-customtoggle' ) )
+ .addClass( 'mw-customtoggle' );
+ }
+ }
+
+ // Add event handlers to custom togglers or create our own ones
+ if ( $customTogglers && $customTogglers.length ) {
+ actionHandler = function ( e, opts ) {
+ var defaultOpts = {};
+ opts = $.extend( defaultOpts, options, opts );
+ togglingHandler( $( this ), $collapsible, e, opts );
+ };
+
+ $toggleLink = $customTogglers
+ .on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler )
+ .prop( 'tabIndex', 0 );
+
+ } else {
+ // If this is not a custom case, do the default: wrap the
+ // contents and add the toggle link. Different elements are
+ // treated differently.
+ if ( $collapsible.is( 'table' ) ) {
+
+ // If the table has a caption, collapse to the caption
+ // as opposed to the first row
+ $caption = $collapsible.find( '> caption' );
+ if ( $caption.length ) {
+ $toggle = $caption.find( '> .mw-collapsible-toggle' );
+
+ // If there is no toggle link, add it to the end of the caption
+ if ( !$toggle.length ) {
+ $toggleLink = buildDefaultToggleLink().appendTo( $caption );
+ } else {
+ actionHandler = premadeToggleHandler;
+ $toggleLink = $toggle.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler )
+ .prop( 'tabIndex', 0 );
+ }
+ } else {
+ // The toggle-link will be in one the the cells (td or th) of the first row
+ $firstItem = $collapsible.find( 'tr:first th, tr:first td' );
+ $toggle = $firstItem.find( '> .mw-collapsible-toggle' );
+
+ // If theres no toggle link, add it to the last cell
+ if ( !$toggle.length ) {
+ $toggleLink = buildDefaultToggleLink().prependTo( $firstItem.eq( -1 ) );
+ } else {
+ actionHandler = premadeToggleHandler;
+ $toggleLink = $toggle.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler )
+ .prop( 'tabIndex', 0 );
+ }
+ }
+
+ } else if ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) {
+ // The toggle-link will be in the first list-item
+ $firstItem = $collapsible.find( 'li:first' );
+ $toggle = $firstItem.find( '> .mw-collapsible-toggle' );
+
+ // If theres no toggle link, add it
+ if ( !$toggle.length ) {
+ // Make sure the numeral order doesn't get messed up, force the first (soon to be second) item
+ // to be "1". Except if the value-attribute is already used.
+ // If no value was set WebKit returns "", Mozilla returns '-1', others return 0, null or undefined.
+ firstval = $firstItem.prop( 'value' );
+ if ( firstval === undefined || !firstval || firstval === '-1' || firstval === -1 ) {
+ $firstItem.prop( 'value', '1' );
+ }
+ $toggleLink = buildDefaultToggleLink();
+ $toggleLink.wrap( '<li class="mw-collapsible-toggle-li"></li>' ).parent().prependTo( $collapsible );
+ } else {
+ actionHandler = premadeToggleHandler;
+ $toggleLink = $toggle.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler )
+ .prop( 'tabIndex', 0 );
+ }
+
+ } else { // <div>, <p> etc.
+
+ // The toggle-link will be the first child of the element
+ $toggle = $collapsible.find( '> .mw-collapsible-toggle' );
+
+ // If a direct child .content-wrapper does not exists, create it
+ if ( !$collapsible.find( '> .mw-collapsible-content' ).length ) {
+ $collapsible.wrapInner( '<div class="mw-collapsible-content"></div>' );
+ }
+
+ // If theres no toggle link, add it
+ if ( !$toggle.length ) {
+ $toggleLink = buildDefaultToggleLink().prependTo( $collapsible );
+ } else {
+ actionHandler = premadeToggleHandler;
+ $toggleLink = $toggle.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler )
+ .prop( 'tabIndex', 0 );
+ }
+ }
+ }
+
+ // Initial state
+ if ( options.collapsed || $collapsible.hasClass( 'mw-collapsed' ) ) {
+ // One toggler can hook to multiple elements, and one element can have
+ // multiple togglers. This is the sanest way to handle that.
+ actionHandler.call( $toggleLink.get( 0 ), null, { instantHide: true, wasCollapsed: false } );
+ }
+ } );
+ };
+
+ /**
+ * @class jQuery
+ * @mixins jQuery.plugin.makeCollapsible
+ */
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/jquery/jquery.mw-jump.js b/resources/src/jquery/jquery.mw-jump.js
new file mode 100644
index 00000000..5eae0bec
--- /dev/null
+++ b/resources/src/jquery/jquery.mw-jump.js
@@ -0,0 +1,15 @@
+/**
+ * JavaScript to show jump links to motor-impaired users when they are focused.
+ */
+jQuery( function ( $ ) {
+
+ $( '.mw-jump' ).on( 'focus blur', 'a', function ( e ) {
+ // Confusingly jQuery leaves e.type as focusout for delegated blur events
+ if ( e.type === 'blur' || e.type === 'focusout' ) {
+ $( this ).closest( '.mw-jump' ).css( { height: 0 } );
+ } else {
+ $( this ).closest( '.mw-jump' ).css( { height: 'auto' } );
+ }
+ } );
+
+} );
diff --git a/resources/src/jquery/jquery.mwExtension.js b/resources/src/jquery/jquery.mwExtension.js
new file mode 100644
index 00000000..dc7aaa45
--- /dev/null
+++ b/resources/src/jquery/jquery.mwExtension.js
@@ -0,0 +1,122 @@
+/*
+ * JavaScript backwards-compatibility alternatives and other convenience functions
+ */
+( function ( $ ) {
+
+ $.extend( {
+ trimLeft: function ( str ) {
+ return str === null ? '' : str.toString().replace( /^\s+/, '' );
+ },
+ trimRight: function ( str ) {
+ return str === null ?
+ '' : str.toString().replace( /\s+$/, '' );
+ },
+ ucFirst: function ( str ) {
+ return str.charAt( 0 ).toUpperCase() + str.slice( 1 );
+ },
+ escapeRE: function ( str ) {
+ return str.replace ( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' );
+ },
+ isDomElement: function ( el ) {
+ return !!el && !!el.nodeType;
+ },
+ isEmpty: function ( v ) {
+ var key;
+ if ( v === '' || v === 0 || v === '0' || v === null
+ || v === false || v === undefined )
+ {
+ return true;
+ }
+ // the for-loop could potentially contain prototypes
+ // to avoid that we check it's length first
+ if ( v.length === 0 ) {
+ return true;
+ }
+ if ( typeof v === 'object' ) {
+ for ( key in v ) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ },
+ compareArray: function ( arrThis, arrAgainst ) {
+ if ( arrThis.length !== arrAgainst.length ) {
+ return false;
+ }
+ for ( var i = 0; i < arrThis.length; i++ ) {
+ if ( $.isArray( arrThis[i] ) ) {
+ if ( !$.compareArray( arrThis[i], arrAgainst[i] ) ) {
+ return false;
+ }
+ } else if ( arrThis[i] !== arrAgainst[i] ) {
+ return false;
+ }
+ }
+ return true;
+ },
+ compareObject: function ( objectA, objectB ) {
+ var prop, type;
+
+ // Do a simple check if the types match
+ if ( typeof objectA === typeof objectB ) {
+
+ // Only loop over the contents if it really is an object
+ if ( typeof objectA === 'object' ) {
+ // If they are aliases of the same object (ie. mw and mediaWiki) return now
+ if ( objectA === objectB ) {
+ return true;
+ } else {
+ // Iterate over each property
+ for ( prop in objectA ) {
+ // Check if this property is also present in the other object
+ if ( prop in objectB ) {
+ // Compare the types of the properties
+ type = typeof objectA[prop];
+ if ( type === typeof objectB[prop] ) {
+ // Recursively check objects inside this one
+ switch ( type ) {
+ case 'object' :
+ if ( !$.compareObject( objectA[prop], objectB[prop] ) ) {
+ return false;
+ }
+ break;
+ case 'function' :
+ // Functions need to be strings to compare them properly
+ if ( objectA[prop].toString() !== objectB[prop].toString() ) {
+ return false;
+ }
+ break;
+ default:
+ // Strings, numbers
+ if ( objectA[prop] !== objectB[prop] ) {
+ return false;
+ }
+ break;
+ }
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ // Check for properties in B but not in A
+ // This is about 15% faster (tested in Safari 5 and Firefox 3.6)
+ // ...than incrementing a count variable in the above and below loops
+ // See also: https://www.mediawiki.org/wiki/ResourceLoader/Default_modules/compareObject_test#Results
+ for ( prop in objectB ) {
+ if ( !( prop in objectA ) ) {
+ return false;
+ }
+ }
+ }
+ }
+ } else {
+ return false;
+ }
+ return true;
+ }
+ } );
+
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.placeholder.js b/resources/src/jquery/jquery.placeholder.js
new file mode 100644
index 00000000..d4580190
--- /dev/null
+++ b/resources/src/jquery/jquery.placeholder.js
@@ -0,0 +1,229 @@
+/**
+ * HTML5 placeholder emulation for jQuery plugin
+ *
+ * This will automatically use the HTML5 placeholder attribute if supported, or emulate this behavior if not.
+ *
+ * This is a fork from Mathias Bynens' jquery.placeholder as of this commit
+ * https://github.com/mathiasbynens/jquery-placeholder/blob/47f05d400e2dd16b59d144141a2cf54a9a77c502/jquery.placeholder.js
+ *
+ * @author Mathias Bynens <http://mathiasbynens.be/>
+ * @author Trevor Parscal <tparscal@wikimedia.org>, 2012
+ * @author Krinkle <krinklemail@gmail.com>, 2012
+ * @author Alex Ivanov <alexivanov97@gmail.com>, 2013
+ * @version 2.1.0
+ * @license MIT
+ */
+(function ($) {
+
+ var isInputSupported = 'placeholder' in document.createElement('input'),
+ isTextareaSupported = 'placeholder' in document.createElement('textarea'),
+ prototype = $.fn,
+ valHooks = $.valHooks,
+ propHooks = $.propHooks,
+ hooks,
+ placeholder;
+
+ if (isInputSupported && isTextareaSupported) {
+
+ placeholder = prototype.placeholder = function (text) {
+ var hasArgs = arguments.length;
+
+ if (hasArgs) {
+ changePlaceholder.call(this, text);
+ }
+
+ return this;
+ };
+
+ placeholder.input = placeholder.textarea = true;
+
+ } else {
+
+ placeholder = prototype.placeholder = function (text) {
+ var $this = this,
+ hasArgs = arguments.length;
+
+ if (hasArgs) {
+ changePlaceholder.call(this, text);
+ }
+
+ $this
+ .filter((isInputSupported ? 'textarea' : ':input') + '[placeholder]')
+ .filter(function () {
+ return !$(this).data('placeholder-enabled');
+ })
+ .bind({
+ 'focus.placeholder drop.placeholder': clearPlaceholder,
+ 'blur.placeholder': setPlaceholder
+ })
+ .data('placeholder-enabled', true)
+ .trigger('blur.placeholder');
+ return $this;
+ };
+
+ placeholder.input = isInputSupported;
+ placeholder.textarea = isTextareaSupported;
+
+ hooks = {
+ 'get': function (element) {
+ var $element = $(element),
+ $passwordInput = $element.data('placeholder-password');
+ if ($passwordInput) {
+ return $passwordInput[0].value;
+ }
+
+ return $element.data('placeholder-enabled') && $element.hasClass('placeholder') ? '' : element.value;
+ },
+ 'set': function (element, value) {
+ var $element = $(element),
+ $passwordInput = $element.data('placeholder-password');
+ if ($passwordInput) {
+ $passwordInput[0].value = value;
+ return value;
+ }
+
+ if (!$element.data('placeholder-enabled')) {
+ element.value = value;
+ return value;
+ }
+ if (!value) {
+ element.value = value;
+ // Issue #56: Setting the placeholder causes problems if the element continues to have focus.
+ if (element !== safeActiveElement()) {
+ // We can't use `triggerHandler` here because of dummy text/password inputs :(
+ setPlaceholder.call(element);
+ }
+ } else if ($element.hasClass('placeholder')) {
+ if (!clearPlaceholder.call(element, true, value)) {
+ element.value = value;
+ }
+ } else {
+ element.value = value;
+ }
+ // `set` can not return `undefined`; see http://jsapi.info/jquery/1.7.1/val#L2363
+ return $element;
+ }
+ };
+
+ if (!isInputSupported) {
+ valHooks.input = hooks;
+ propHooks.value = hooks;
+ }
+ if (!isTextareaSupported) {
+ valHooks.textarea = hooks;
+ propHooks.value = hooks;
+ }
+
+ $(function () {
+ // Look for forms
+ $(document).delegate('form', 'submit.placeholder', function () {
+ // Clear the placeholder values so they don't get submitted
+ var $inputs = $('.placeholder', this).each(clearPlaceholder);
+ setTimeout(function () {
+ $inputs.each(setPlaceholder);
+ }, 10);
+ });
+ });
+
+ // Clear placeholder values upon page reload
+ $(window).bind('beforeunload.placeholder', function () {
+ $('.placeholder').each(function () {
+ this.value = '';
+ });
+ });
+
+ }
+
+ function args(elem) {
+ // Return an object of element attributes
+ var newAttrs = {},
+ rinlinejQuery = /^jQuery\d+$/;
+ $.each(elem.attributes, function (i, attr) {
+ if (attr.specified && !rinlinejQuery.test(attr.name)) {
+ newAttrs[attr.name] = attr.value;
+ }
+ });
+ return newAttrs;
+ }
+
+ function clearPlaceholder(event, value) {
+ var input = this,
+ $input = $(input);
+ if (input.value === $input.attr('placeholder') && $input.hasClass('placeholder')) {
+ if ($input.data('placeholder-password')) {
+ $input = $input.hide().next().show().attr('id', $input.removeAttr('id').data('placeholder-id'));
+ // If `clearPlaceholder` was called from `$.valHooks.input.set`
+ if (event === true) {
+ $input[0].value = value;
+ return value;
+ }
+ $input.focus();
+ } else {
+ input.value = '';
+ $input.removeClass('placeholder');
+ if (input === safeActiveElement()) {
+ input.select();
+ }
+ }
+ }
+ }
+
+ function setPlaceholder() {
+ var $replacement,
+ input = this,
+ $input = $(input),
+ id = this.id;
+ if (!input.value) {
+ if (input.type === 'password') {
+ if (!$input.data('placeholder-textinput')) {
+ try {
+ $replacement = $input.clone().attr({ 'type': 'text' });
+ } catch (e) {
+ $replacement = $('<input>').attr($.extend(args(this), { 'type': 'text' }));
+ }
+ $replacement
+ .removeAttr('name')
+ .data({
+ 'placeholder-password': $input,
+ 'placeholder-id': id
+ })
+ .bind('focus.placeholder drop.placeholder', clearPlaceholder);
+ $input
+ .data({
+ 'placeholder-textinput': $replacement,
+ 'placeholder-id': id
+ })
+ .before($replacement);
+ }
+ $input = $input.removeAttr('id').hide().prev().attr('id', id).show();
+ // Note: `$input[0] != input` now!
+ }
+ $input.addClass('placeholder');
+ $input[0].value = $input.attr('placeholder');
+ } else {
+ $input.removeClass('placeholder');
+ }
+ }
+
+ function safeActiveElement() {
+ // Avoid IE9 `document.activeElement` of death
+ // https://github.com/mathiasbynens/jquery-placeholder/pull/99
+ try {
+ return document.activeElement;
+ } catch (err) {}
+ }
+
+ function changePlaceholder(text) {
+ var hasArgs = arguments.length,
+ $input = this;
+ if (hasArgs) {
+ if ($input.attr('placeholder') !== text) {
+ $input.prop('placeholder', text);
+ if ($input.hasClass('placeholder')) {
+ $input[0].value = text;
+ }
+ }
+ }
+ }
+
+}(jQuery));
diff --git a/resources/src/jquery/jquery.qunit.completenessTest.js b/resources/src/jquery/jquery.qunit.completenessTest.js
new file mode 100644
index 00000000..8d38401e
--- /dev/null
+++ b/resources/src/jquery/jquery.qunit.completenessTest.js
@@ -0,0 +1,305 @@
+/**
+ * jQuery QUnit CompletenessTest 0.4
+ *
+ * Tests the completeness of test suites for object oriented javascript
+ * libraries. Written to be used in environments with jQuery and QUnit.
+ * Requires jQuery 1.7.2 or higher.
+ *
+ * Built for and tested with:
+ * - Chrome 19
+ * - Firefox 4
+ * - Safari 5
+ *
+ * @author Timo Tijhof, 2011-2012
+ */
+( function ( mw, $ ) {
+ 'use strict';
+
+ var util,
+ hasOwn = Object.prototype.hasOwnProperty,
+ log = (window.console && window.console.log)
+ ? function () { return window.console.log.apply(window.console, arguments); }
+ : function () {};
+
+ // Simplified version of a few jQuery methods, except that they don't
+ // call other jQuery methods. Required to be able to run the CompletenessTest
+ // on jQuery itself as well.
+ util = {
+ keys: Object.keys || function ( object ) {
+ var key, keys = [];
+ for ( key in object ) {
+ if ( hasOwn.call( object, key ) ) {
+ keys.push( key );
+ }
+ }
+ return keys;
+ },
+ each: function ( object, callback ) {
+ var name;
+ for ( name in object ) {
+ if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
+ break;
+ }
+ }
+ },
+ // $.type and $.isEmptyObject are safe as is, they don't call
+ // other $.* methods. Still need to be derefenced into `util`
+ // since the CompletenessTest will overload them with spies.
+ type: $.type,
+ isEmptyObject: $.isEmptyObject
+ };
+
+ /**
+ * CompletenessTest
+ * @constructor
+ *
+ * @example
+ * var myTester = new CompletenessTest( myLib );
+ * @param masterVariable {Object} The root variable that contains all object
+ * members. CompletenessTest will recursively traverse objects and keep track
+ * of all methods.
+ * @param ignoreFn {Function} Optionally pass a function to filter out certain
+ * methods. Example: You may want to filter out instances of jQuery or some
+ * other constructor. Otherwise "missingTests" will include all methods that
+ * were not called from that instance.
+ */
+ function CompletenessTest( masterVariable, ignoreFn ) {
+ var warn,
+ that = this;
+
+ // Keep track in these objects. Keyed by strings with the
+ // method names (ie. 'my.foo', 'my.bar', etc.) values are boolean true.
+ this.injectionTracker = {};
+ this.methodCallTracker = {};
+ this.missingTests = {};
+
+ this.ignoreFn = ignoreFn === undefined ? function () { return false; } : ignoreFn;
+
+ // Lazy limit in case something weird happends (like recurse (part of) ourself).
+ this.lazyLimit = 2000;
+ this.lazyCounter = 0;
+
+ // Bind begin and end to QUnit.
+ QUnit.begin( function () {
+ // Suppress warnings (e.g. deprecation notices for accessing the properties)
+ warn = mw.log.warn;
+ mw.log.warn = $.noop;
+
+ that.walkTheObject( masterVariable, null, masterVariable, [] );
+ log( 'CompletenessTest/walkTheObject', that );
+
+ // Restore warnings
+ mw.log.warn = warn;
+ warn = undefined;
+ });
+
+ QUnit.done( function () {
+ that.populateMissingTests();
+ log( 'CompletenessTest/populateMissingTests', that );
+
+ var toolbar, testResults, cntTotal, cntCalled, cntMissing;
+
+ cntTotal = util.keys( that.injectionTracker ).length;
+ cntCalled = util.keys( that.methodCallTracker ).length;
+ cntMissing = util.keys( that.missingTests ).length;
+
+ function makeTestResults( blob, title, style ) {
+ var elOutputWrapper, elTitle, elContainer, elList, elFoot;
+
+ elTitle = document.createElement( 'strong' );
+ elTitle.textContent = title || 'Values';
+
+ elList = document.createElement( 'ul' );
+ util.each( blob, function ( key ) {
+ var elItem = document.createElement( 'li' );
+ elItem.textContent = key;
+ elList.appendChild( elItem );
+ });
+
+ elFoot = document.createElement( 'p' );
+ elFoot.innerHTML = '<em>&mdash; CompletenessTest</em>';
+
+ elContainer = document.createElement( 'div' );
+ elContainer.appendChild( elTitle );
+ elContainer.appendChild( elList );
+ elContainer.appendChild( elFoot );
+
+ elOutputWrapper = document.getElementById( 'qunit-completenesstest' );
+ if ( !elOutputWrapper ) {
+ elOutputWrapper = document.createElement( 'div' );
+ elOutputWrapper.id = 'qunit-completenesstest';
+ }
+ elOutputWrapper.appendChild( elContainer );
+
+ util.each( style, function ( key, value ) {
+ elOutputWrapper.style[key] = value;
+ });
+ return elOutputWrapper;
+ }
+
+ if ( cntMissing === 0 ) {
+ // Good
+ testResults = makeTestResults(
+ {},
+ 'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. No missing tests!',
+ {
+ backgroundColor: '#D2E0E6',
+ color: '#366097',
+ paddingTop: '1em',
+ paddingRight: '1em',
+ paddingBottom: '1em',
+ paddingLeft: '1em'
+ }
+ );
+ } else {
+ // Bad
+ testResults = makeTestResults(
+ that.missingTests,
+ 'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. ' + cntMissing + ' methods not covered:',
+ {
+ backgroundColor: '#EE5757',
+ color: 'black',
+ paddingTop: '1em',
+ paddingRight: '1em',
+ paddingBottom: '1em',
+ paddingLeft: '1em'
+ }
+ );
+ }
+
+ toolbar = document.getElementById( 'qunit-testrunner-toolbar' );
+ if ( toolbar ) {
+ toolbar.insertBefore( testResults, toolbar.firstChild );
+ }
+ });
+
+ return this;
+ }
+
+ /* Public methods */
+ CompletenessTest.fn = CompletenessTest.prototype = {
+
+ /**
+ * CompletenessTest.fn.walkTheObject
+ *
+ * This function recursively walks through the given object, calling itself as it goes.
+ * Depending on the action it either injects our listener into the methods, or
+ * reads from our tracker and records which methods have not been called by the test suite.
+ *
+ * @param currName {String|Null} Name of the given object member (Initially this is null).
+ * @param currVar {mixed} The variable to check (initially an object,
+ * further down it could be anything).
+ * @param masterVariable {Object} Throughout our interation, always keep track of the master/root.
+ * Initially this is the same as currVar.
+ * @param parentPathArray {Array} Array of names that indicate our breadcrumb path starting at
+ * masterVariable. Not including currName.
+ */
+ walkTheObject: function ( currObj, currName, masterVariable, parentPathArray ) {
+ var key, currVal, type,
+ ct = this,
+ currPathArray = parentPathArray;
+
+ if ( currName ) {
+ currPathArray.push( currName );
+ currVal = currObj[currName];
+ } else {
+ currName = '(root)';
+ currVal = currObj;
+ }
+
+ type = util.type( currVal );
+
+ // Hard ignores
+ if ( this.ignoreFn( currVal, this, currPathArray ) ) {
+ return null;
+ }
+
+ // Handle the lazy limit
+ this.lazyCounter++;
+ if ( this.lazyCounter > this.lazyLimit ) {
+ log( 'CompletenessTest.fn.walkTheObject> Limit reached: ' + this.lazyCounter, currPathArray );
+ return null;
+ }
+
+ // Functions
+ if ( type === 'function' ) {
+ // Don't put a spy in constructor functions as it messes with
+ // instanceof etc.
+ if ( !currVal.prototype || util.isEmptyObject( currVal.prototype ) ) {
+ this.injectionTracker[ currPathArray.join( '.' ) ] = true;
+ this.injectCheck( currObj, currName, function () {
+ ct.methodCallTracker[ currPathArray.join( '.' ) ] = true;
+ } );
+ }
+ }
+
+ // Recursively. After all, this is the *completeness* test
+ // This also traverses static properties and the prototype of a constructor
+ if ( type === 'object' || type === 'function' ) {
+ for ( key in currVal ) {
+ if ( hasOwn.call( currVal, key ) ) {
+ this.walkTheObject( currVal, key, masterVariable, currPathArray.slice() );
+ }
+ }
+ }
+ },
+
+ populateMissingTests: function () {
+ var ct = this;
+ util.each( ct.injectionTracker, function ( key ) {
+ ct.hasTest( key );
+ });
+ },
+
+ /**
+ * CompletenessTest.fn.hasTest
+ *
+ * Checks if the given method name (ie. 'my.foo.bar')
+ * was called during the test suite (as far as the tracker knows).
+ * If not it adds it to missingTests.
+ *
+ * @param fnName {String}
+ * @return {Boolean}
+ */
+ hasTest: function ( fnName ) {
+ if ( !( fnName in this.methodCallTracker ) ) {
+ this.missingTests[fnName] = true;
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * CompletenessTest.fn.injectCheck
+ *
+ * Injects a function (such as a spy that updates methodCallTracker when
+ * it's called) inside another function.
+ *
+ * @param masterVariable {Object}
+ * @param objectPathArray {Array}
+ * @param injectFn {Function}
+ */
+ injectCheck: function ( obj, key, injectFn ) {
+ var spy,
+ val = obj[ key ];
+
+ spy = function () {
+ injectFn();
+ return val.apply( this, arguments );
+ };
+
+ // Make the spy inherit from the original so that its static methods are also
+ // visible in the spy (e.g. when we inject a check into mw.log, mw.log.warn
+ // must remain accessible).
+ /*jshint proto:true */
+ spy.__proto__ = val;
+
+ // Objects are by reference, members (unless objects) are not.
+ obj[ key ] = spy;
+ }
+ };
+
+ /* Expose */
+ window.CompletenessTest = CompletenessTest;
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/jquery/jquery.spinner.css b/resources/src/jquery/jquery.spinner.css
new file mode 100644
index 00000000..a9e06dbe
--- /dev/null
+++ b/resources/src/jquery/jquery.spinner.css
@@ -0,0 +1,40 @@
+.mw-spinner {
+ background-color: transparent;
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+
+.mw-spinner-small {
+ /* @embed */
+ background-image: url(images/spinner.gif);
+ height: 20px;
+ width: 20px;
+ /* Avoid issues with .mw-spinner-block when floated without width. */
+ min-width: 20px;
+}
+
+.mw-spinner-large {
+ /* @embed */
+ background-image: url(images/spinner-large.gif);
+ height: 32px;
+ width: 32px;
+ /* Avoid issues with .mw-spinner-block when floated without width. */
+ min-width: 32px;
+}
+
+.mw-spinner-block {
+ display: block;
+ /* This overrides width from .mw-spinner-large / .mw-spinner-small,
+ * This is where the min-width kicks in.
+ */
+ width: 100%;
+}
+
+.mw-spinner-inline {
+ display: inline-block;
+ vertical-align: middle;
+
+ /* IE < 8 */
+ zoom: 1;
+ *display: inline;
+}
diff --git a/resources/src/jquery/jquery.spinner.js b/resources/src/jquery/jquery.spinner.js
new file mode 100644
index 00000000..361d3e08
--- /dev/null
+++ b/resources/src/jquery/jquery.spinner.js
@@ -0,0 +1,112 @@
+/**
+ * jQuery Spinner
+ *
+ * Simple jQuery plugin to create, inject and remove spinners.
+ *
+ * @class jQuery.plugin.spinner
+ */
+( function ( $ ) {
+
+ // Default options for new spinners,
+ // stored outside the function to share between calls.
+ var defaults = {
+ id: undefined,
+ size: 'small',
+ type: 'inline'
+ };
+
+ $.extend( {
+ /**
+ * Create a spinner element
+ *
+ * The argument is an object with options used to construct the spinner (see below).
+ *
+ * It is a good practice to keep a reference to the created spinner to be able to remove it
+ * later. Alternatively, one can use the 'id' option and #removeSpinner (but make sure to choose
+ * an id that's unlikely to cause conflicts, e.g. with extensions, gadgets or user scripts).
+ *
+ * CSS classes used:
+ *
+ * - .mw-spinner for every spinner
+ * - .mw-spinner-small / .mw-spinner-large for size
+ * - .mw-spinner-block / .mw-spinner-inline for display types
+ *
+ * Example:
+ *
+ * // Create a large spinner reserving all available horizontal space.
+ * var $spinner = $.createSpinner( { size: 'large', type: 'block' } );
+ * // Insert above page content.
+ * $( '#mw-content-text' ).prepend( $spinner );
+ *
+ * // Place a small inline spinner next to the "Save" button
+ * var $spinner = $.createSpinner( { size: 'small', type: 'inline' } );
+ * // Alternatively, just `$.createSpinner();` as these are the default options.
+ * $( '#wpSave' ).after( $spinner );
+ *
+ * // The following two are equivalent:
+ * $.createSpinner( 'magic' );
+ * $.createSpinner( { id: 'magic' } );
+ *
+ * @static
+ * @inheritable
+ * @param {Object|string} [opts] Options. If a string is given, it will be treated as the value
+ * of the `id` option. If an object is given, the possible option keys are:
+ * @param {string} [opts.id] If given, spinner will be given an id of "mw-spinner-{id}".
+ * @param {string} [opts.size='small'] 'small' or 'large' for a 20-pixel or 32-pixel spinner.
+ * @param {string} [opts.type='inline'] 'inline' or 'block'. Inline creates an inline-block with
+ * width and height equal to spinner size. Block is a block-level element with width 100%,
+ * height equal to spinner size.
+ * @return {jQuery}
+ */
+ createSpinner: function ( opts ) {
+ if ( opts !== undefined && $.type( opts ) !== 'object' ) {
+ opts = {
+ id: opts
+ };
+ }
+
+ opts = $.extend( {}, defaults, opts );
+
+ var $spinner = $( '<div>', { 'class': 'mw-spinner', 'title': '...' } );
+ if ( opts.id !== undefined ) {
+ $spinner.attr( 'id', 'mw-spinner-' + opts.id );
+ }
+
+ $spinner.addClass( opts.size === 'large' ? 'mw-spinner-large' : 'mw-spinner-small' );
+ $spinner.addClass( opts.type === 'block' ? 'mw-spinner-block' : 'mw-spinner-inline' );
+
+ return $spinner;
+ },
+
+ /**
+ * Remove a spinner element
+ *
+ * @static
+ * @inheritable
+ * @param {string} id Id of the spinner, as passed to #createSpinner
+ * @return {jQuery} The (now detached) spinner element
+ */
+ removeSpinner: function ( id ) {
+ return $( '#mw-spinner-' + id ).remove();
+ }
+ } );
+
+ /**
+ * Inject a spinner after each element in the collection
+ *
+ * Inserts spinner as siblings (not children) of the target elements.
+ * Collection contents remain unchanged.
+ *
+ * @param {Object|string} [opts] See #createSpinner
+ * @return {jQuery}
+ */
+ $.fn.injectSpinner = function ( opts ) {
+ return this.after( $.createSpinner( opts ) );
+ };
+
+ /**
+ * @class jQuery
+ * @mixins jQuery.plugin.spinner
+ */
+
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.suggestions.css b/resources/src/jquery/jquery.suggestions.css
new file mode 100644
index 00000000..15cd9264
--- /dev/null
+++ b/resources/src/jquery/jquery.suggestions.css
@@ -0,0 +1,76 @@
+/* suggestions plugin */
+
+.suggestions {
+ overflow: hidden;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 0;
+ border: none;
+ z-index: 1099;
+ padding: 0;
+ margin: -1px 0 0 0;
+}
+
+.suggestions-special {
+ position: relative;
+ background-color: white;
+ cursor: pointer;
+ border: solid 1px #aaaaaa;
+ padding: 0;
+ margin: 0;
+ margin-top: -2px;
+ display: none;
+ padding: 0.25em 0.25em;
+ line-height: 1.25em;
+}
+
+.suggestions-results {
+ background-color: white;
+ cursor: pointer;
+ border: solid 1px #aaaaaa;
+ padding: 0;
+ margin: 0;
+}
+
+.suggestions-result {
+ color: black;
+ margin: 0;
+ line-height: 1.5em;
+ padding: 0.01em 0.25em;
+ text-align: left;
+ /* Apply ellipsis to suggestions */
+ overflow: hidden;
+ -o-text-overflow: ellipsis; /* Opera 9 to 10 */
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.suggestions-result-current {
+ background-color: #4C59A6;
+ color: white;
+}
+
+.suggestions-special .special-label {
+ color: gray;
+ text-align: left;
+}
+
+.suggestions-special .special-query {
+ color: black;
+ font-style: italic;
+ text-align: left;
+}
+
+.suggestions-special .special-hover {
+ background-color: silver;
+}
+
+.suggestions-result-current .special-label,
+.suggestions-result-current .special-query {
+ color: white;
+}
+
+.highlight {
+ font-weight: bold;
+}
diff --git a/resources/src/jquery/jquery.suggestions.js b/resources/src/jquery/jquery.suggestions.js
new file mode 100644
index 00000000..3369cde2
--- /dev/null
+++ b/resources/src/jquery/jquery.suggestions.js
@@ -0,0 +1,684 @@
+/**
+ * This plugin provides a generic way to add suggestions to a text box.
+ *
+ * Usage:
+ *
+ * Set options:
+ * $( '#textbox' ).suggestions( { option1: value1, option2: value2 } );
+ * $( '#textbox' ).suggestions( option, value );
+ * Get option:
+ * value = $( '#textbox' ).suggestions( option );
+ * Initialize:
+ * $( '#textbox' ).suggestions();
+ *
+ * Options:
+ *
+ * fetch(query): Callback that should fetch suggestions and set the suggestions property.
+ * Executed in the context of the textbox
+ * Type: Function
+ * cancel: Callback function to call when any pending asynchronous suggestions fetches
+ * should be canceled. Executed in the context of the textbox
+ * Type: Function
+ * special: Set of callbacks for rendering and selecting
+ * Type: Object of Functions 'render' and 'select'
+ * result: Set of callbacks for rendering and selecting
+ * Type: Object of Functions 'render' and 'select'
+ * $region: jQuery selection of element to place the suggestions below and match width of
+ * Type: jQuery Object, Default: $( this )
+ * suggestions: Suggestions to display
+ * Type: Array of strings
+ * maxRows: Maximum number of suggestions to display at one time
+ * Type: Number, Range: 1 - 100, Default: 7
+ * delay: Number of ms to wait for the user to stop typing
+ * Type: Number, Range: 0 - 1200, Default: 120
+ * cache: Whether to cache results from a fetch
+ * Type: Boolean, Default: false
+ * cacheMaxAge: Number of ms to cache results from a fetch
+ * Type: Number, Range: 1 - Infinity, Default: 60000 (1 minute)
+ * submitOnClick: Whether to submit the form containing the textbox when a suggestion is clicked
+ * Type: Boolean, Default: false
+ * maxExpandFactor: Maximum suggestions box width relative to the textbox width. If set
+ * to e.g. 2, the suggestions box will never be grown beyond 2 times the width of the textbox.
+ * Type: Number, Range: 1 - infinity, Default: 3
+ * expandFrom: Which direction to offset the suggestion box from.
+ * Values 'start' and 'end' translate to left and right respectively depending on the
+ * directionality of the current document, according to $( 'html' ).css( 'direction' ).
+ * Type: String, default: 'auto', options: 'left', 'right', 'start', 'end', 'auto'.
+ * positionFromLeft: Sets expandFrom=left, for backwards compatibility
+ * Type: Boolean, Default: true
+ * highlightInput: Whether to hightlight matched portions of the input or not
+ * Type: Boolean, Default: false
+ */
+( function ( $ ) {
+
+var hasOwn = Object.hasOwnProperty;
+
+$.suggestions = {
+ /**
+ * Cancel any delayed maybeFetch() call and callback the context so
+ * they can cancel any async fetching if they use AJAX or something.
+ */
+ cancel: function ( context ) {
+ if ( context.data.timerID !== null ) {
+ clearTimeout( context.data.timerID );
+ }
+ if ( $.isFunction( context.config.cancel ) ) {
+ context.config.cancel.call( context.data.$textbox );
+ }
+ },
+
+ /**
+ * Hide the element with suggestions and clean up some state.
+ */
+ hide: function ( context ) {
+ // Remove any highlights, including on "special" items
+ context.data.$container.find( '.suggestions-result-current' ).removeClass( 'suggestions-result-current' );
+ // Hide the container
+ context.data.$container.hide();
+ },
+
+ /**
+ * Restore the text the user originally typed in the textbox, before it
+ * was overwritten by highlight(). This restores the value the currently
+ * displayed suggestions are based on, rather than the value just before
+ * highlight() overwrote it; the former is arguably slightly more sensible.
+ */
+ restore: function ( context ) {
+ context.data.$textbox.val( context.data.prevText );
+ },
+
+ /**
+ * Ask the user-specified callback for new suggestions. Any previous delayed
+ * call to this function still pending will be canceled. If the value in the
+ * textbox is empty or hasn't changed since the last time suggestions were fetched,
+ * this function does nothing.
+ * @param {Boolean} delayed Whether or not to delay this by the currently configured amount of time
+ */
+ update: function ( context, delayed ) {
+ function maybeFetch() {
+ var val = context.data.$textbox.val(),
+ cache = context.data.cache,
+ cacheHit;
+
+ // Only fetch if the value in the textbox changed and is not empty, or if the results were hidden
+ // if the textbox is empty then clear the result div, but leave other settings intouched
+ if ( val.length === 0 ) {
+ $.suggestions.hide( context );
+ context.data.prevText = '';
+ } else if (
+ val !== context.data.prevText ||
+ !context.data.$container.is( ':visible' )
+ ) {
+ context.data.prevText = val;
+ // Try cache first
+ if ( context.config.cache && hasOwn.call( cache, val ) ) {
+ if ( +new Date() - cache[ val ].timestamp < context.config.cacheMaxAge ) {
+ context.data.$textbox.suggestions( 'suggestions', cache[ val ].suggestions );
+ cacheHit = true;
+ } else {
+ // Cache expired
+ delete cache[ val ];
+ }
+ }
+ if ( !cacheHit && typeof context.config.fetch === 'function' ) {
+ context.config.fetch.call(
+ context.data.$textbox,
+ val,
+ function ( suggestions ) {
+ context.data.$textbox.suggestions( 'suggestions', suggestions );
+ if ( context.config.cache ) {
+ cache[ val ] = {
+ suggestions: suggestions,
+ timestamp: +new Date()
+ };
+ }
+ }
+ );
+ }
+ }
+
+ // Always update special rendering
+ $.suggestions.special( context );
+ }
+
+ // Cancels any delayed maybeFetch call, and invokes context.config.cancel.
+ $.suggestions.cancel( context );
+
+ if ( delayed ) {
+ // To avoid many started/aborted requests while typing, we're gonna take a short
+ // break before trying to fetch data.
+ context.data.timerID = setTimeout( maybeFetch, context.config.delay );
+ } else {
+ maybeFetch();
+ }
+ },
+
+ special: function ( context ) {
+ // Allow custom rendering - but otherwise don't do any rendering
+ if ( typeof context.config.special.render === 'function' ) {
+ // Wait for the browser to update the value
+ setTimeout( function () {
+ // Render special
+ var $special = context.data.$container.find( '.suggestions-special' );
+ context.config.special.render.call( $special, context.data.$textbox.val(), context );
+ }, 1 );
+ }
+ },
+
+ /**
+ * Sets the value of a property, and updates the widget accordingly
+ * @param property String Name of property
+ * @param value Mixed Value to set property with
+ */
+ configure: function ( context, property, value ) {
+ var newCSS,
+ $result, $results, $spanForWidth, childrenWidth,
+ i, expWidth, maxWidth, text;
+
+ // Validate creation using fallback values
+ switch ( property ) {
+ case 'fetch':
+ case 'cancel':
+ case 'special':
+ case 'result':
+ case '$region':
+ case 'expandFrom':
+ context.config[property] = value;
+ break;
+ case 'suggestions':
+ context.config[property] = value;
+ // Update suggestions
+ if ( context.data !== undefined ) {
+ if ( context.data.$textbox.val().length === 0 ) {
+ // Hide the div when no suggestion exist
+ $.suggestions.hide( context );
+ } else {
+ // Rebuild the suggestions list
+ context.data.$container.show();
+ // Update the size and position of the list
+ newCSS = {
+ top: context.config.$region.offset().top + context.config.$region.outerHeight(),
+ bottom: 'auto',
+ width: context.config.$region.outerWidth(),
+ height: 'auto'
+ };
+
+ // Process expandFrom, after this it is set to left or right.
+ context.config.expandFrom = ( function ( expandFrom ) {
+ var regionWidth, docWidth, regionCenter, docCenter,
+ docDir = $( document.documentElement ).css( 'direction' ),
+ $region = context.config.$region;
+
+ // Backwards compatible
+ if ( context.config.positionFromLeft ) {
+ expandFrom = 'left';
+
+ // Catch invalid values, default to 'auto'
+ } else if ( $.inArray( expandFrom, ['left', 'right', 'start', 'end', 'auto'] ) === -1 ) {
+ expandFrom = 'auto';
+ }
+
+ if ( expandFrom === 'auto' ) {
+ if ( $region.data( 'searchsuggest-expand-dir' ) ) {
+ // If the markup explicitly contains a direction, use it.
+ expandFrom = $region.data( 'searchsuggest-expand-dir' );
+ } else {
+ regionWidth = $region.outerWidth();
+ docWidth = $( document ).width();
+ if ( regionWidth > ( 0.85 * docWidth ) ) {
+ // If the input size takes up more than 85% of the document horizontally
+ // expand the suggestions to the writing direction's native end.
+ expandFrom = 'start';
+ } else {
+ // Calculate the center points of the input and document
+ regionCenter = $region.offset().left + regionWidth / 2;
+ docCenter = docWidth / 2;
+ if ( Math.abs( regionCenter - docCenter ) < ( 0.10 * docCenter ) ) {
+ // If the input's center is within 10% of the document center
+ // use the writing direction's native end.
+ expandFrom = 'start';
+ } else {
+ // Otherwise expand the input from the closest side of the page,
+ // towards the side of the page with the most free open space
+ expandFrom = regionCenter > docCenter ? 'right' : 'left';
+ }
+ }
+ }
+ }
+
+ if ( expandFrom === 'start' ) {
+ expandFrom = docDir === 'rtl' ? 'right' : 'left';
+
+ } else if ( expandFrom === 'end' ) {
+ expandFrom = docDir === 'rtl' ? 'left' : 'right';
+ }
+
+ return expandFrom;
+
+ }( context.config.expandFrom ) );
+
+ if ( context.config.expandFrom === 'left' ) {
+ // Expand from left
+ newCSS.left = context.config.$region.offset().left;
+ newCSS.right = 'auto';
+ } else {
+ // Expand from right
+ newCSS.left = 'auto';
+ newCSS.right = $( 'body' ).width() - ( context.config.$region.offset().left + context.config.$region.outerWidth() );
+ }
+
+ context.data.$container.css( newCSS );
+ $results = context.data.$container.children( '.suggestions-results' );
+ $results.empty();
+ expWidth = -1;
+ for ( i = 0; i < context.config.suggestions.length; i++ ) {
+ /*jshint loopfunc:true */
+ text = context.config.suggestions[i];
+ $result = $( '<div>' )
+ .addClass( 'suggestions-result' )
+ .attr( 'rel', i )
+ .data( 'text', context.config.suggestions[i] )
+ .mousemove( function () {
+ context.data.selectedWithMouse = true;
+ $.suggestions.highlight(
+ context,
+ $( this ).closest( '.suggestions-results .suggestions-result' ),
+ false
+ );
+ } )
+ .appendTo( $results );
+ // Allow custom rendering
+ if ( typeof context.config.result.render === 'function' ) {
+ context.config.result.render.call( $result, context.config.suggestions[i], context );
+ } else {
+ $result.text( text );
+ }
+
+ if ( context.config.highlightInput ) {
+ $result.highlightText( context.data.prevText );
+ }
+
+ // Widen results box if needed (new width is only calculated here, applied later).
+
+ // The monstrosity below accomplishes two things:
+ // * Wraps the text contents in a DOM element, so that we can know its width. There is
+ // no way to directly access the width of a text node, and we can't use the parent
+ // node width as it has text-overflow: ellipsis; and overflow: hidden; applied to
+ // it, which trims it to a smaller width.
+ // * Temporarily applies position: absolute; to the wrapper to pull it out of normal
+ // document flow. Otherwise the CSS text-overflow: ellipsis; and overflow: hidden;
+ // rules would cause some browsers (at least all versions of IE from 6 to 11) to
+ // still report the "trimmed" width. This should not be done in regular CSS
+ // stylesheets as we don't want this rule to apply to other <span> elements, like
+ // the ones generated by jquery.highlightText.
+ $spanForWidth = $result.wrapInner( '<span>' ).children();
+ childrenWidth = $spanForWidth.css( 'position', 'absolute' ).outerWidth();
+ $spanForWidth.contents().unwrap();
+
+ if ( childrenWidth > $result.width() && childrenWidth > expWidth ) {
+ // factor in any padding, margin, or border space on the parent
+ expWidth = childrenWidth + ( context.data.$container.width() - $result.width() );
+ }
+ }
+
+ // Apply new width for results box, if any
+ if ( expWidth > context.data.$container.width() ) {
+ maxWidth = context.config.maxExpandFactor * context.data.$textbox.width();
+ context.data.$container.width( Math.min( expWidth, maxWidth ) );
+ }
+ }
+ }
+ break;
+ case 'maxRows':
+ context.config[property] = Math.max( 1, Math.min( 100, value ) );
+ break;
+ case 'delay':
+ context.config[property] = Math.max( 0, Math.min( 1200, value ) );
+ break;
+ case 'cacheMaxAge':
+ context.config[property] = Math.max( 1, value );
+ break;
+ case 'maxExpandFactor':
+ context.config[property] = Math.max( 1, value );
+ break;
+ case 'cache':
+ case 'submitOnClick':
+ case 'positionFromLeft':
+ case 'highlightInput':
+ context.config[property] = !!value;
+ break;
+ }
+ },
+
+ /**
+ * Highlight a result in the results table
+ * @param result <tr> to highlight: jQuery object, or 'prev' or 'next'
+ * @param updateTextbox If true, put the suggestion in the textbox
+ */
+ highlight: function ( context, result, updateTextbox ) {
+ var selected = context.data.$container.find( '.suggestions-result-current' );
+ if ( !result.get || selected.get( 0 ) !== result.get( 0 ) ) {
+ if ( result === 'prev' ) {
+ if ( selected.hasClass( 'suggestions-special' ) ) {
+ result = context.data.$container.find( '.suggestions-result:last' );
+ } else {
+ result = selected.prev();
+ if ( !( result.length && result.hasClass( 'suggestions-result' ) ) ) {
+ // there is something in the DOM between selected element and the wrapper, bypass it
+ result = selected.parents( '.suggestions-results > *' ).prev().find( '.suggestions-result' ).eq( 0 );
+ }
+
+ if ( selected.length === 0 ) {
+ // we are at the beginning, so lets jump to the last item
+ if ( context.data.$container.find( '.suggestions-special' ).html() !== '' ) {
+ result = context.data.$container.find( '.suggestions-special' );
+ } else {
+ result = context.data.$container.find( '.suggestions-results .suggestions-result:last' );
+ }
+ }
+ }
+ } else if ( result === 'next' ) {
+ if ( selected.length === 0 ) {
+ // No item selected, go to the first one
+ result = context.data.$container.find( '.suggestions-results .suggestions-result:first' );
+ if ( result.length === 0 && context.data.$container.find( '.suggestions-special' ).html() !== '' ) {
+ // No suggestion exists, go to the special one directly
+ result = context.data.$container.find( '.suggestions-special' );
+ }
+ } else {
+ result = selected.next();
+ if ( !( result.length && result.hasClass( 'suggestions-result' ) ) ) {
+ // there is something in the DOM between selected element and the wrapper, bypass it
+ result = selected.parents( '.suggestions-results > *' ).next().find( '.suggestions-result' ).eq( 0 );
+ }
+
+ if ( selected.hasClass( 'suggestions-special' ) ) {
+ result = $( [] );
+ } else if (
+ result.length === 0 &&
+ context.data.$container.find( '.suggestions-special' ).html() !== ''
+ ) {
+ // We were at the last item, jump to the specials!
+ result = context.data.$container.find( '.suggestions-special' );
+ }
+ }
+ }
+ selected.removeClass( 'suggestions-result-current' );
+ result.addClass( 'suggestions-result-current' );
+ }
+ if ( updateTextbox ) {
+ if ( result.length === 0 || result.is( '.suggestions-special' ) ) {
+ $.suggestions.restore( context );
+ } else {
+ context.data.$textbox.val( result.data( 'text' ) );
+ // .val() doesn't call any event handlers, so
+ // let the world know what happened
+ context.data.$textbox.change();
+ }
+ context.data.$textbox.trigger( 'change' );
+ }
+ },
+
+ /**
+ * Respond to keypress event
+ * @param key Integer Code of key pressed
+ */
+ keypress: function ( e, context, key ) {
+ var selected,
+ wasVisible = context.data.$container.is( ':visible' ),
+ preventDefault = false;
+
+ switch ( key ) {
+ // Arrow down
+ case 40:
+ if ( wasVisible ) {
+ $.suggestions.highlight( context, 'next', true );
+ context.data.selectedWithMouse = false;
+ } else {
+ $.suggestions.update( context, false );
+ }
+ preventDefault = true;
+ break;
+ // Arrow up
+ case 38:
+ if ( wasVisible ) {
+ $.suggestions.highlight( context, 'prev', true );
+ context.data.selectedWithMouse = false;
+ }
+ preventDefault = wasVisible;
+ break;
+ // Escape
+ case 27:
+ $.suggestions.hide( context );
+ $.suggestions.restore( context );
+ $.suggestions.cancel( context );
+ context.data.$textbox.trigger( 'change' );
+ preventDefault = wasVisible;
+ break;
+ // Enter
+ case 13:
+ preventDefault = wasVisible;
+ selected = context.data.$container.find( '.suggestions-result-current' );
+ $.suggestions.hide( context );
+ if ( selected.length === 0 || context.data.selectedWithMouse ) {
+ // If nothing is selected or if something was selected with the mouse
+ // cancel any current requests and allow the form to be submitted
+ // (simply don't prevent default behavior).
+ $.suggestions.cancel( context );
+ preventDefault = false;
+ } else if ( selected.is( '.suggestions-special' ) ) {
+ if ( typeof context.config.special.select === 'function' ) {
+ // Allow the callback to decide whether to prevent default or not
+ if ( context.config.special.select.call( selected, context.data.$textbox ) === true ) {
+ preventDefault = false;
+ }
+ }
+ } else {
+ $.suggestions.highlight( context, selected, true );
+
+ if ( typeof context.config.result.select === 'function' ) {
+ // Allow the callback to decide whether to prevent default or not
+ if ( context.config.result.select.call( selected, context.data.$textbox ) === true ) {
+ preventDefault = false;
+ }
+ }
+ }
+ break;
+ default:
+ $.suggestions.update( context, true );
+ break;
+ }
+ if ( preventDefault ) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+};
+$.fn.suggestions = function () {
+
+ // Multi-context fields
+ var returnValue,
+ args = arguments;
+
+ $( this ).each( function () {
+ var context, key;
+
+ /* Construction / Loading */
+
+ context = $( this ).data( 'suggestions-context' );
+ if ( context === undefined || context === null ) {
+ context = {
+ config: {
+ fetch: function () {},
+ cancel: function () {},
+ special: {},
+ result: {},
+ $region: $( this ),
+ suggestions: [],
+ maxRows: 7,
+ delay: 120,
+ cache: false,
+ cacheMaxAge: 60000,
+ submitOnClick: false,
+ maxExpandFactor: 3,
+ expandFrom: 'auto',
+ highlightInput: false
+ }
+ };
+ }
+
+ /* API */
+
+ // Handle various calling styles
+ if ( args.length > 0 ) {
+ if ( typeof args[0] === 'object' ) {
+ // Apply set of properties
+ for ( key in args[0] ) {
+ $.suggestions.configure( context, key, args[0][key] );
+ }
+ } else if ( typeof args[0] === 'string' ) {
+ if ( args.length > 1 ) {
+ // Set property values
+ $.suggestions.configure( context, args[0], args[1] );
+ } else if ( returnValue === null || returnValue === undefined ) {
+ // Get property values, but don't give access to internal data - returns only the first
+ returnValue = ( args[0] in context.config ? undefined : context.config[args[0]] );
+ }
+ }
+ }
+
+ /* Initialization */
+
+ if ( context.data === undefined ) {
+ context.data = {
+ // ID of running timer
+ timerID: null,
+
+ // Text in textbox when suggestions were last fetched
+ prevText: null,
+
+ // Cache of fetched suggestions
+ cache: {},
+
+ // Number of results visible without scrolling
+ visibleResults: 0,
+
+ // Suggestion the last mousedown event occurred on
+ mouseDownOn: $( [] ),
+ $textbox: $( this ),
+ selectedWithMouse: false
+ };
+
+ context.data.$container = $( '<div>' )
+ .css( 'display', 'none' )
+ .addClass( 'suggestions' )
+ .append(
+ $( '<div>' ).addClass( 'suggestions-results' )
+ // Can't use click() because the container div is hidden when the
+ // textbox loses focus. Instead, listen for a mousedown followed
+ // by a mouseup on the same div.
+ .mousedown( function ( e ) {
+ context.data.mouseDownOn = $( e.target ).closest( '.suggestions-results .suggestions-result' );
+ } )
+ .mouseup( function ( e ) {
+ var $result = $( e.target ).closest( '.suggestions-results .suggestions-result' ),
+ $other = context.data.mouseDownOn;
+
+ context.data.mouseDownOn = $( [] );
+ if ( $result.get( 0 ) !== $other.get( 0 ) ) {
+ return;
+ }
+ // Do not interfere with non-left clicks or if modifier keys are pressed (e.g. ctrl-click).
+ if ( !( e.which !== 1 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) ) {
+ $.suggestions.highlight( context, $result, true );
+ if ( typeof context.config.result.select === 'function' ) {
+ context.config.result.select.call( $result, context.data.$textbox );
+ }
+ // This will hide the link we're just clicking on, which causes problems
+ // when done synchronously in at least Firefox 3.6 (bug 62858).
+ setTimeout( function () {
+ $.suggestions.hide( context );
+ }, 0 );
+ }
+ // Always bring focus to the textbox, as that's probably where the user expects it
+ // if they were just typing.
+ context.data.$textbox.focus();
+ } )
+ )
+ .append(
+ $( '<div>' ).addClass( 'suggestions-special' )
+ // Can't use click() because the container div is hidden when the
+ // textbox loses focus. Instead, listen for a mousedown followed
+ // by a mouseup on the same div.
+ .mousedown( function ( e ) {
+ context.data.mouseDownOn = $( e.target ).closest( '.suggestions-special' );
+ } )
+ .mouseup( function ( e ) {
+ var $special = $( e.target ).closest( '.suggestions-special' ),
+ $other = context.data.mouseDownOn;
+
+ context.data.mouseDownOn = $( [] );
+ if ( $special.get( 0 ) !== $other.get( 0 ) ) {
+ return;
+ }
+ // Do not interfere with non-left clicks or if modifier keys are pressed (e.g. ctrl-click).
+ if ( !( e.which !== 1 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) ) {
+ if ( typeof context.config.special.select === 'function' ) {
+ context.config.special.select.call( $special, context.data.$textbox );
+ }
+ // This will hide the link we're just clicking on, which causes problems
+ // when done synchronously in at least Firefox 3.6 (bug 62858).
+ setTimeout( function () {
+ $.suggestions.hide( context );
+ }, 0 );
+ }
+ // Always bring focus to the textbox, as that's probably where the user expects it
+ // if they were just typing.
+ context.data.$textbox.focus();
+ } )
+ .mousemove( function ( e ) {
+ context.data.selectedWithMouse = true;
+ $.suggestions.highlight(
+ context, $( e.target ).closest( '.suggestions-special' ), false
+ );
+ } )
+ )
+ .appendTo( $( 'body' ) );
+
+ $( this )
+ // Stop browser autocomplete from interfering
+ .attr( 'autocomplete', 'off' )
+ .keydown( function ( e ) {
+ // Store key pressed to handle later
+ context.data.keypressed = e.which;
+ context.data.keypressedCount = 0;
+ } )
+ .keypress( function ( e ) {
+ context.data.keypressedCount++;
+ $.suggestions.keypress( e, context, context.data.keypressed );
+ } )
+ .keyup( function ( e ) {
+ // Some browsers won't throw keypress() for arrow keys. If we got a keydown and a keyup without a
+ // keypress in between, solve it
+ if ( context.data.keypressedCount === 0 ) {
+ $.suggestions.keypress( e, context, context.data.keypressed );
+ }
+ } )
+ .blur( function () {
+ // When losing focus because of a mousedown
+ // on a suggestion, don't hide the suggestions
+ if ( context.data.mouseDownOn.length > 0 ) {
+ return;
+ }
+ $.suggestions.hide( context );
+ $.suggestions.cancel( context );
+ } );
+ }
+
+ // Store the context for next time
+ $( this ).data( 'suggestions-context', context );
+ } );
+ return returnValue !== undefined ? returnValue : $( this );
+};
+
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.tabIndex.js b/resources/src/jquery/jquery.tabIndex.js
new file mode 100644
index 00000000..46cc8f2c
--- /dev/null
+++ b/resources/src/jquery/jquery.tabIndex.js
@@ -0,0 +1,57 @@
+/**
+ * @class jQuery.plugin.tabIndex
+ */
+( function ( $ ) {
+
+ /**
+ * Find the lowest tabindex in use within a selection.
+ *
+ * @return {number} Lowest tabindex on the page
+ */
+ $.fn.firstTabIndex = function () {
+ var minTabIndex = null;
+ $(this).find( '[tabindex]' ).each( function () {
+ var tabIndex = parseInt( $(this).prop( 'tabindex' ), 10 );
+ // In IE6/IE7 the above jQuery selector returns all elements,
+ // becuase it has a default value for tabIndex in IE6/IE7 of 0
+ // (rather than null/undefined). Therefore check "> 0" as well.
+ // Under IE7 under Windows NT 5.2 is also capable of returning NaN.
+ if ( tabIndex > 0 && !isNaN( tabIndex ) ) {
+ // Initial value
+ if ( minTabIndex === null ) {
+ minTabIndex = tabIndex;
+ } else if ( tabIndex < minTabIndex ) {
+ minTabIndex = tabIndex;
+ }
+ }
+ } );
+ return minTabIndex;
+ };
+
+ /**
+ * Find the highest tabindex in use within a selection.
+ *
+ * @return {number} Highest tabindex on the page
+ */
+ $.fn.lastTabIndex = function () {
+ var maxTabIndex = null;
+ $(this).find( '[tabindex]' ).each( function () {
+ var tabIndex = parseInt( $(this).prop( 'tabindex' ), 10 );
+ if ( tabIndex > 0 && !isNaN( tabIndex ) ) {
+ // Initial value
+ if ( maxTabIndex === null ) {
+ maxTabIndex = tabIndex;
+ } else if ( tabIndex > maxTabIndex ) {
+ maxTabIndex = tabIndex;
+ }
+ }
+ } );
+ return maxTabIndex;
+ };
+
+ /**
+ * @class jQuery
+ * @mixins jQuery.plugin.tabIndex
+ */
+
+}( jQuery ) );
diff --git a/resources/src/jquery/jquery.tablesorter.css b/resources/src/jquery/jquery.tablesorter.css
new file mode 100644
index 00000000..a88acc09
--- /dev/null
+++ b/resources/src/jquery/jquery.tablesorter.css
@@ -0,0 +1,17 @@
+/* Table Sorting */
+table.jquery-tablesorter th.headerSort {
+ /* @embed */
+ background-image: url(images/sort_both.gif);
+ cursor: pointer;
+ background-repeat: no-repeat;
+ background-position: center right;
+ padding-right: 21px;
+}
+table.jquery-tablesorter th.headerSortUp {
+ /* @embed */
+ background-image: url(images/sort_up.gif);
+}
+table.jquery-tablesorter th.headerSortDown {
+ /* @embed */
+ background-image: url(images/sort_down.gif);
+}
diff --git a/resources/src/jquery/jquery.tablesorter.js b/resources/src/jquery/jquery.tablesorter.js
new file mode 100644
index 00000000..ea2c5f92
--- /dev/null
+++ b/resources/src/jquery/jquery.tablesorter.js
@@ -0,0 +1,1161 @@
+/**
+ * TableSorter for MediaWiki
+ *
+ * Written 2011 Leo Koppelkamm
+ * Based on tablesorter.com plugin, written (c) 2007 Christian Bach.
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Depends on mw.config (wgDigitTransformTable, wgDefaultDateFormat, wgContentLanguage)
+ * and mw.language.months.
+ *
+ * Uses 'tableSorterCollation' in mw.config (if available)
+ */
+/**
+ *
+ * @description Create a sortable table with multi-column sorting capabilitys
+ *
+ * @example $( 'table' ).tablesorter();
+ * @desc Create a simple tablesorter interface.
+ *
+ * @example $( 'table' ).tablesorter( { sortList: [ { 0: 'desc' }, { 1: 'asc' } ] } );
+ * @desc Create a tablesorter interface initially sorting on the first and second column.
+ *
+ * @option String cssHeader ( optional ) A string of the class name to be appended
+ * to sortable tr elements in the thead of the table. Default value:
+ * "header"
+ *
+ * @option String cssAsc ( optional ) A string of the class name to be appended to
+ * sortable tr elements in the thead on a ascending sort. Default value:
+ * "headerSortUp"
+ *
+ * @option String cssDesc ( optional ) A string of the class name to be appended
+ * to sortable tr elements in the thead on a descending sort. Default
+ * value: "headerSortDown"
+ *
+ * @option String sortInitialOrder ( optional ) A string of the inital sorting
+ * order can be asc or desc. Default value: "asc"
+ *
+ * @option String sortMultisortKey ( optional ) A string of the multi-column sort
+ * key. Default value: "shiftKey"
+ *
+ * @option Boolean sortLocaleCompare ( optional ) Boolean flag indicating whatever
+ * to use String.localeCampare method or not. Set to false.
+ *
+ * @option Boolean cancelSelection ( optional ) Boolean flag indicating if
+ * tablesorter should cancel selection of the table headers text.
+ * Default value: true
+ *
+ * @option Array sortList ( optional ) An array containing objects specifying sorting.
+ * By passing more than one object, multi-sorting will be applied. Object structure:
+ * { <Integer column index>: <String 'asc' or 'desc'> }
+ * Default value: []
+ *
+ * @option Boolean debug ( optional ) Boolean flag indicating if tablesorter
+ * should display debuging information usefull for development.
+ *
+ * @event sortEnd.tablesorter: Triggered as soon as any sorting has been applied.
+ *
+ * @type jQuery
+ *
+ * @name tablesorter
+ *
+ * @cat Plugins/Tablesorter
+ *
+ * @author Christian Bach/christian.bach@polyester.se
+ */
+
+( function ( $, mw ) {
+ /* Local scope */
+
+ var ts,
+ parsers = [];
+
+ /* Parser utility functions */
+
+ function getParserById( name ) {
+ var i,
+ len = parsers.length;
+ for ( i = 0; i < len; i++ ) {
+ if ( parsers[i].id.toLowerCase() === name.toLowerCase() ) {
+ return parsers[i];
+ }
+ }
+ return false;
+ }
+
+ function getElementSortKey( node ) {
+ var $node = $( node ),
+ // Use data-sort-value attribute.
+ // Use data() instead of attr() so that live value changes
+ // are processed as well (bug 38152).
+ data = $node.data( 'sortValue' );
+
+ if ( data !== null && data !== undefined ) {
+ // Cast any numbers or other stuff to a string, methods
+ // like charAt, toLowerCase and split are expected.
+ return String( data );
+ } else {
+ if ( !node ) {
+ return $node.text();
+ } else if ( node.tagName.toLowerCase() === 'img' ) {
+ return $node.attr( 'alt' ) || ''; // handle undefined alt
+ } else {
+ return $.map( $.makeArray( node.childNodes ), function ( elem ) {
+ // 1 is for document.ELEMENT_NODE (the constant is undefined on old browsers)
+ if ( elem.nodeType === 1 ) {
+ return getElementSortKey( elem );
+ } else {
+ return $.text( elem );
+ }
+ } ).join( '' );
+ }
+ }
+ }
+
+ function detectParserForColumn( table, rows, cellIndex ) {
+ var l = parsers.length,
+ nodeValue,
+ // Start with 1 because 0 is the fallback parser
+ i = 1,
+ rowIndex = 0,
+ concurrent = 0,
+ needed = ( rows.length > 4 ) ? 5 : rows.length;
+
+ while ( i < l ) {
+ if ( rows[rowIndex] && rows[rowIndex].cells[cellIndex] ) {
+ nodeValue = $.trim( getElementSortKey( rows[rowIndex].cells[cellIndex] ) );
+ } else {
+ nodeValue = '';
+ }
+
+ if ( nodeValue !== '' ) {
+ if ( parsers[i].is( nodeValue, table ) ) {
+ concurrent++;
+ rowIndex++;
+ if ( concurrent >= needed ) {
+ // Confirmed the parser for multiple cells, let's return it
+ return parsers[i];
+ }
+ } else {
+ // Check next parser, reset rows
+ i++;
+ rowIndex = 0;
+ concurrent = 0;
+ }
+ } else {
+ // Empty cell
+ rowIndex++;
+ if ( rowIndex > rows.length ) {
+ rowIndex = 0;
+ i++;
+ }
+ }
+ }
+
+ // 0 is always the generic parser (text)
+ return parsers[0];
+ }
+
+ function buildParserCache( table, $headers ) {
+ var sortType, cells, len, i, parser,
+ rows = table.tBodies[0].rows,
+ parsers = [];
+
+ if ( rows[0] ) {
+
+ cells = rows[0].cells;
+ len = cells.length;
+
+ for ( i = 0; i < len; i++ ) {
+ parser = false;
+ sortType = $headers.eq( i ).data( 'sortType' );
+ if ( sortType !== undefined ) {
+ parser = getParserById( sortType );
+ }
+
+ if ( parser === false ) {
+ parser = detectParserForColumn( table, rows, i );
+ }
+
+ parsers.push( parser );
+ }
+ }
+ return parsers;
+ }
+
+ /* Other utility functions */
+
+ function buildCache( table ) {
+ var i, j, $row, cols,
+ totalRows = ( table.tBodies[0] && table.tBodies[0].rows.length ) || 0,
+ totalCells = ( table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length ) || 0,
+ parsers = table.config.parsers,
+ cache = {
+ row: [],
+ normalized: []
+ };
+
+ for ( i = 0; i < totalRows; ++i ) {
+
+ // Add the table data to main data array
+ $row = $( table.tBodies[0].rows[i] );
+ cols = [];
+
+ // if this is a child row, add it to the last row's children and
+ // continue to the next row
+ if ( $row.hasClass( table.config.cssChildRow ) ) {
+ cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add( $row );
+ // go to the next for loop
+ continue;
+ }
+
+ cache.row.push( $row );
+
+ for ( j = 0; j < totalCells; ++j ) {
+ cols.push( parsers[j].format( getElementSortKey( $row[0].cells[j] ), table, $row[0].cells[j] ) );
+ }
+
+ cols.push( cache.normalized.length ); // add position for rowCache
+ cache.normalized.push( cols );
+ cols = null;
+ }
+
+ return cache;
+ }
+
+ function appendToTable( table, cache ) {
+ var i, pos, l, j,
+ row = cache.row,
+ normalized = cache.normalized,
+ totalRows = normalized.length,
+ checkCell = ( normalized[0].length - 1 ),
+ fragment = document.createDocumentFragment();
+
+ for ( i = 0; i < totalRows; i++ ) {
+ pos = normalized[i][checkCell];
+
+ l = row[pos].length;
+
+ for ( j = 0; j < l; j++ ) {
+ fragment.appendChild( row[pos][j] );
+ }
+
+ }
+ table.tBodies[0].appendChild( fragment );
+
+ $( table ).trigger( 'sortEnd.tablesorter' );
+ }
+
+ /**
+ * Find all header rows in a thead-less table and put them in a <thead> tag.
+ * This only treats a row as a header row if it contains only <th>s (no <td>s)
+ * and if it is preceded entirely by header rows. The algorithm stops when
+ * it encounters the first non-header row.
+ *
+ * After this, it will look at all rows at the bottom for footer rows
+ * And place these in a tfoot using similar rules.
+ * @param $table jQuery object for a <table>
+ */
+ function emulateTHeadAndFoot( $table ) {
+ var $thead, $tfoot, i, len,
+ $rows = $table.find( '> tbody > tr' );
+ if ( !$table.get( 0 ).tHead ) {
+ $thead = $( '<thead>' );
+ $rows.each( function () {
+ if ( $( this ).children( 'td' ).length ) {
+ // This row contains a <td>, so it's not a header row
+ // Stop here
+ return false;
+ }
+ $thead.append( this );
+ } );
+ $table.find( ' > tbody:first' ).before( $thead );
+ }
+ if ( !$table.get( 0 ).tFoot ) {
+ $tfoot = $( '<tfoot>' );
+ len = $rows.length;
+ for ( i = len - 1; i >= 0; i-- ) {
+ if ( $( $rows[i] ).children( 'td' ).length ) {
+ break;
+ }
+ $tfoot.prepend( $( $rows[i] ) );
+ }
+ $table.append( $tfoot );
+ }
+ }
+
+ function buildHeaders( table, msg ) {
+ var maxSeen = 0,
+ colspanOffset = 0,
+ columns,
+ i,
+ rowspan,
+ colspan,
+ headerCount,
+ longestTR,
+ exploded,
+ $tableHeaders = $( [] ),
+ $tableRows = $( 'thead:eq(0) > tr', table );
+ if ( $tableRows.length <= 1 ) {
+ $tableHeaders = $tableRows.children( 'th' );
+ } else {
+ exploded = [];
+
+ // Loop through all the dom cells of the thead
+ $tableRows.each( function ( rowIndex, row ) {
+ $.each( row.cells, function ( columnIndex, cell ) {
+ var matrixRowIndex,
+ matrixColumnIndex;
+
+ rowspan = Number( cell.rowSpan );
+ colspan = Number( cell.colSpan );
+
+ // Skip the spots in the exploded matrix that are already filled
+ while ( exploded[rowIndex] && exploded[rowIndex][columnIndex] !== undefined ) {
+ ++columnIndex;
+ }
+
+ // Find the actual dimensions of the thead, by placing each cell
+ // in the exploded matrix rowspan times colspan times, with the proper offsets
+ for ( matrixColumnIndex = columnIndex; matrixColumnIndex < columnIndex + colspan; ++matrixColumnIndex ) {
+ for ( matrixRowIndex = rowIndex; matrixRowIndex < rowIndex + rowspan; ++matrixRowIndex ) {
+ if ( !exploded[matrixRowIndex] ) {
+ exploded[matrixRowIndex] = [];
+ }
+ exploded[matrixRowIndex][matrixColumnIndex] = cell;
+ }
+ }
+ } );
+ } );
+ // We want to find the row that has the most columns (ignoring colspan)
+ $.each( exploded, function ( index, cellArray ) {
+ headerCount = $( uniqueElements( cellArray ) ).filter( 'th' ).length;
+ if ( headerCount >= maxSeen ) {
+ maxSeen = headerCount;
+ longestTR = index;
+ }
+ } );
+ // We cannot use $.unique() here because it sorts into dom order, which is undesirable
+ $tableHeaders = $( uniqueElements( exploded[longestTR] ) ).filter( 'th' );
+ }
+
+ // as each header can span over multiple columns (using colspan=N),
+ // we have to bidirectionally map headers to their columns and columns to their headers
+ table.headerToColumns = [];
+ table.columnToHeader = [];
+
+ $tableHeaders.each( function ( headerIndex ) {
+ columns = [];
+ for ( i = 0; i < this.colSpan; i++ ) {
+ table.columnToHeader[ colspanOffset + i ] = headerIndex;
+ columns.push( colspanOffset + i );
+ }
+
+ table.headerToColumns[ headerIndex ] = columns;
+ colspanOffset += this.colSpan;
+
+ this.headerIndex = headerIndex;
+ this.order = 0;
+ this.count = 0;
+
+ if ( $( this ).hasClass( table.config.unsortableClass ) ) {
+ this.sortDisabled = true;
+ }
+
+ if ( !this.sortDisabled ) {
+ $( this )
+ .addClass( table.config.cssHeader )
+ .prop( 'tabIndex', 0 )
+ .attr( {
+ role: 'columnheader button',
+ title: msg[1]
+ } );
+ }
+
+ // add cell to headerList
+ table.config.headerList[headerIndex] = this;
+ } );
+
+ return $tableHeaders;
+
+ }
+
+ /**
+ * Sets the sort count of the columns that are not affected by the sorting to have them sorted
+ * in default (ascending) order when their header cell is clicked the next time.
+ *
+ * @param {jQuery} $headers
+ * @param {Number[][]} sortList
+ * @param {Number[][]} headerToColumns
+ */
+ function setHeadersOrder( $headers, sortList, headerToColumns ) {
+ // Loop through all headers to retrieve the indices of the columns the header spans across:
+ $.each( headerToColumns, function ( headerIndex, columns ) {
+
+ $.each( columns, function ( i, columnIndex ) {
+ var header = $headers[headerIndex];
+
+ if ( !isValueInArray( columnIndex, sortList ) ) {
+ // Column shall not be sorted: Reset header count and order.
+ header.order = 0;
+ header.count = 0;
+ } else {
+ // Column shall be sorted: Apply designated count and order.
+ $.each( sortList, function ( j, sortColumn ) {
+ if ( sortColumn[0] === i ) {
+ header.order = sortColumn[1];
+ header.count = sortColumn[1] + 1;
+ return false;
+ }
+ } );
+ }
+ } );
+
+ } );
+ }
+
+ function isValueInArray( v, a ) {
+ var i,
+ len = a.length;
+ for ( i = 0; i < len; i++ ) {
+ if ( a[i][0] === v ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function uniqueElements( array ) {
+ var uniques = [];
+ $.each( array, function ( index, elem ) {
+ if ( elem !== undefined && $.inArray( elem, uniques ) === -1 ) {
+ uniques.push( elem );
+ }
+ } );
+ return uniques;
+ }
+
+ function setHeadersCss( table, $headers, list, css, msg, columnToHeader ) {
+ // Remove all header information and reset titles to default message
+ $headers.removeClass( css[0] ).removeClass( css[1] ).attr( 'title', msg[1] );
+
+ for ( var i = 0; i < list.length; i++ ) {
+ $headers.eq( columnToHeader[ list[i][0] ] )
+ .addClass( css[ list[i][1] ] )
+ .attr( 'title', msg[ list[i][1] ] );
+ }
+ }
+
+ function sortText( a, b ) {
+ return ( ( a < b ) ? -1 : ( ( a > b ) ? 1 : 0 ) );
+ }
+
+ function sortTextDesc( a, b ) {
+ return ( ( b < a ) ? -1 : ( ( b > a ) ? 1 : 0 ) );
+ }
+
+ function multisort( table, sortList, cache ) {
+ var i,
+ sortFn = [],
+ len = sortList.length;
+ for ( i = 0; i < len; i++ ) {
+ sortFn[i] = ( sortList[i][1] ) ? sortTextDesc : sortText;
+ }
+ cache.normalized.sort( function ( array1, array2 ) {
+ var i, col, ret;
+ for ( i = 0; i < len; i++ ) {
+ col = sortList[i][0];
+ ret = sortFn[i].call( this, array1[col], array2[col] );
+ if ( ret !== 0 ) {
+ return ret;
+ }
+ }
+ // Fall back to index number column to ensure stable sort
+ return sortText.call( this, array1[array1.length - 1], array2[array2.length - 1] );
+ } );
+ return cache;
+ }
+
+ function buildTransformTable() {
+ var ascii, localised, i, digitClass,
+ digits = '0123456789,.'.split( '' ),
+ separatorTransformTable = mw.config.get( 'wgSeparatorTransformTable' ),
+ digitTransformTable = mw.config.get( 'wgDigitTransformTable' );
+
+ if ( separatorTransformTable === null || ( separatorTransformTable[0] === '' && digitTransformTable[2] === '' ) ) {
+ ts.transformTable = false;
+ } else {
+ ts.transformTable = {};
+
+ // Unpack the transform table
+ ascii = separatorTransformTable[0].split( '\t' ).concat( digitTransformTable[0].split( '\t' ) );
+ localised = separatorTransformTable[1].split( '\t' ).concat( digitTransformTable[1].split( '\t' ) );
+
+ // Construct regex for number identification
+ for ( i = 0; i < ascii.length; i++ ) {
+ ts.transformTable[localised[i]] = ascii[i];
+ digits.push( $.escapeRE( localised[i] ) );
+ }
+ }
+ digitClass = '[' + digits.join( '', digits ) + ']';
+
+ // We allow a trailing percent sign, which we just strip. This works fine
+ // if percents and regular numbers aren't being mixed.
+ ts.numberRegex = new RegExp( '^(' + '[-+\u2212]?[0-9][0-9,]*(\\.[0-9,]*)?(E[-+\u2212]?[0-9][0-9,]*)?' + // Fortran-style scientific
+ '|' + '[-+\u2212]?' + digitClass + '+[\\s\\xa0]*%?' + // Generic localised
+ ')$', 'i' );
+ }
+
+ function buildDateTable() {
+ var i, name,
+ regex = [];
+
+ ts.monthNames = {};
+
+ for ( i = 0; i < 12; i++ ) {
+ name = mw.language.months.names[i].toLowerCase();
+ ts.monthNames[name] = i + 1;
+ regex.push( $.escapeRE( name ) );
+ name = mw.language.months.genitive[i].toLowerCase();
+ ts.monthNames[name] = i + 1;
+ regex.push( $.escapeRE( name ) );
+ name = mw.language.months.abbrev[i].toLowerCase().replace( '.', '' );
+ ts.monthNames[name] = i + 1;
+ regex.push( $.escapeRE( name ) );
+ }
+
+ // Build piped string
+ regex = regex.join( '|' );
+
+ // Build RegEx
+ // Any date formated with . , ' - or /
+ ts.dateRegex[0] = new RegExp( /^\s*(\d{1,2})[\,\.\-\/'\s]{1,2}(\d{1,2})[\,\.\-\/'\s]{1,2}(\d{2,4})\s*?/i );
+
+ // Written Month name, dmy
+ ts.dateRegex[1] = new RegExp( '^\\s*(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(' + regex + ')' + '[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$', 'i' );
+
+ // Written Month name, mdy
+ ts.dateRegex[2] = new RegExp( '^\\s*(' + regex + ')' + '[\\,\\.\\-\\/\'\\s]+(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$', 'i' );
+
+ }
+
+ /**
+ * Replace all rowspanned cells in the body with clones in each row, so sorting
+ * need not worry about them.
+ *
+ * @param $table jQuery object for a <table>
+ */
+ function explodeRowspans( $table ) {
+ var spanningRealCellIndex, rowSpan, colSpan,
+ cell, i, $tds, $clone, $nextRows,
+ rowspanCells = $table.find( '> tbody > tr > [rowspan]' ).get();
+
+ // Short circuit
+ if ( !rowspanCells.length ) {
+ return;
+ }
+
+ // First, we need to make a property like cellIndex but taking into
+ // account colspans. We also cache the rowIndex to avoid having to take
+ // cell.parentNode.rowIndex in the sorting function below.
+ $table.find( '> tbody > tr' ).each( function () {
+ var i,
+ col = 0,
+ l = this.cells.length;
+ for ( i = 0; i < l; i++ ) {
+ this.cells[i].realCellIndex = col;
+ this.cells[i].realRowIndex = this.rowIndex;
+ col += this.cells[i].colSpan;
+ }
+ } );
+
+ // Split multi row cells into multiple cells with the same content.
+ // Sort by column then row index to avoid problems with odd table structures.
+ // Re-sort whenever a rowspanned cell's realCellIndex is changed, because it
+ // might change the sort order.
+ function resortCells() {
+ rowspanCells = rowspanCells.sort( function ( a, b ) {
+ var ret = a.realCellIndex - b.realCellIndex;
+ if ( !ret ) {
+ ret = a.realRowIndex - b.realRowIndex;
+ }
+ return ret;
+ } );
+ $.each( rowspanCells, function () {
+ this.needResort = false;
+ } );
+ }
+ resortCells();
+
+ function filterfunc() {
+ return this.realCellIndex >= spanningRealCellIndex;
+ }
+
+ function fixTdCellIndex() {
+ this.realCellIndex += colSpan;
+ if ( this.rowSpan > 1 ) {
+ this.needResort = true;
+ }
+ }
+
+ while ( rowspanCells.length ) {
+ if ( rowspanCells[0].needResort ) {
+ resortCells();
+ }
+
+ cell = rowspanCells.shift();
+ rowSpan = cell.rowSpan;
+ colSpan = cell.colSpan;
+ spanningRealCellIndex = cell.realCellIndex;
+ cell.rowSpan = 1;
+ $nextRows = $( cell ).parent().nextAll();
+ for ( i = 0; i < rowSpan - 1; i++ ) {
+ $tds = $( $nextRows[i].cells ).filter( filterfunc );
+ $clone = $( cell ).clone();
+ $clone[0].realCellIndex = spanningRealCellIndex;
+ if ( $tds.length ) {
+ $tds.each( fixTdCellIndex );
+ $tds.first().before( $clone );
+ } else {
+ $nextRows.eq( i ).append( $clone );
+ }
+ }
+ }
+ }
+
+ function buildCollationTable() {
+ ts.collationTable = mw.config.get( 'tableSorterCollation' );
+ ts.collationRegex = null;
+ if ( ts.collationTable ) {
+ var key,
+ keys = [];
+
+ // Build array of key names
+ for ( key in ts.collationTable ) {
+ // Check hasOwn to be safe
+ if ( ts.collationTable.hasOwnProperty( key ) ) {
+ keys.push( key );
+ }
+ }
+ if ( keys.length ) {
+ ts.collationRegex = new RegExp( '[' + keys.join( '' ) + ']', 'ig' );
+ }
+ }
+ }
+
+ function cacheRegexs() {
+ if ( ts.rgx ) {
+ return;
+ }
+ ts.rgx = {
+ IPAddress: [
+ new RegExp( /^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/ )
+ ],
+ currency: [
+ new RegExp( /(^[£$€¥]|[£$€¥]$)/ ),
+ new RegExp( /[£$€¥]/g )
+ ],
+ url: [
+ new RegExp( /^(https?|ftp|file):\/\/$/ ),
+ new RegExp( /(https?|ftp|file):\/\// )
+ ],
+ isoDate: [
+ new RegExp( /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/ )
+ ],
+ usLongDate: [
+ new RegExp( /^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/ )
+ ],
+ time: [
+ new RegExp( /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/ )
+ ]
+ };
+ }
+
+ /**
+ * Converts sort objects [ { Integer: String }, ... ] to the internally used nested array
+ * structure [ [ Integer , Integer ], ... ]
+ *
+ * @param sortObjects {Array} List of sort objects.
+ * @return {Array} List of internal sort definitions.
+ */
+
+ function convertSortList( sortObjects ) {
+ var sortList = [];
+ $.each( sortObjects, function ( i, sortObject ) {
+ $.each( sortObject, function ( columnIndex, order ) {
+ var orderIndex = ( order === 'desc' ) ? 1 : 0;
+ sortList.push( [parseInt( columnIndex, 10 ), orderIndex] );
+ } );
+ } );
+ return sortList;
+ }
+
+ /* Public scope */
+
+ $.tablesorter = {
+
+ defaultOptions: {
+ cssHeader: 'headerSort',
+ cssAsc: 'headerSortUp',
+ cssDesc: 'headerSortDown',
+ cssChildRow: 'expand-child',
+ sortInitialOrder: 'asc',
+ sortMultiSortKey: 'shiftKey',
+ sortLocaleCompare: false,
+ unsortableClass: 'unsortable',
+ parsers: {},
+ widgets: [],
+ headers: {},
+ cancelSelection: true,
+ sortList: [],
+ headerList: [],
+ selectorHeaders: 'thead tr:eq(0) th',
+ debug: false
+ },
+
+ dateRegex: [],
+ monthNames: {},
+
+ /**
+ * @param $tables {jQuery}
+ * @param settings {Object} (optional)
+ */
+ construct: function ( $tables, settings ) {
+ return $tables.each( function ( i, table ) {
+ // Declare and cache.
+ var $headers, cache, config, sortCSS, sortMsg,
+ $table = $( table ),
+ firstTime = true;
+
+ // Quit if no tbody
+ if ( !table.tBodies ) {
+ return;
+ }
+ if ( !table.tHead ) {
+ // No thead found. Look for rows with <th>s and
+ // move them into a <thead> tag or a <tfoot> tag
+ emulateTHeadAndFoot( $table );
+
+ // Still no thead? Then quit
+ if ( !table.tHead ) {
+ return;
+ }
+ }
+ $table.addClass( 'jquery-tablesorter' );
+
+ // FIXME config should probably not be stored in the plain table node
+ // New config object.
+ table.config = {};
+
+ // Merge and extend.
+ config = $.extend( table.config, $.tablesorter.defaultOptions, settings );
+
+ // Save the settings where they read
+ $.data( table, 'tablesorter', { config: config } );
+
+ // Get the CSS class names, could be done else where.
+ sortCSS = [ config.cssDesc, config.cssAsc ];
+ sortMsg = [ mw.msg( 'sort-descending' ), mw.msg( 'sort-ascending' ) ];
+
+ // Build headers
+ $headers = buildHeaders( table, sortMsg );
+
+ // Grab and process locale settings.
+ buildTransformTable();
+ buildDateTable();
+
+ // Precaching regexps can bring 10 fold
+ // performance improvements in some browsers.
+ cacheRegexs();
+
+ function setupForFirstSort() {
+ firstTime = false;
+
+ // Defer buildCollationTable to first sort. As user and site scripts
+ // may customize tableSorterCollation but load after $.ready(), other
+ // scripts may call .tablesorter() before they have done the
+ // tableSorterCollation customizations.
+ buildCollationTable();
+
+ // Legacy fix of .sortbottoms
+ // Wrap them inside inside a tfoot (because that's what they actually want to be) &
+ // and put the <tfoot> at the end of the <table>
+ var $tfoot,
+ $sortbottoms = $table.find( '> tbody > tr.sortbottom' );
+ if ( $sortbottoms.length ) {
+ $tfoot = $table.children( 'tfoot' );
+ if ( $tfoot.length ) {
+ $tfoot.eq( 0 ).prepend( $sortbottoms );
+ } else {
+ $table.append( $( '<tfoot>' ).append( $sortbottoms ) );
+ }
+ }
+
+ explodeRowspans( $table );
+
+ // try to auto detect column type, and store in tables config
+ table.config.parsers = buildParserCache( table, $headers );
+ }
+
+ // Apply event handling to headers
+ // this is too big, perhaps break it out?
+ $headers.not( '.' + table.config.unsortableClass ).on( 'keypress click', function ( e ) {
+ var cell, columns, newSortList, i,
+ totalRows,
+ j, s, o;
+
+ if ( e.type === 'click' && e.target.nodeName.toLowerCase() === 'a' ) {
+ // The user clicked on a link inside a table header.
+ // Do nothing and let the default link click action continue.
+ return true;
+ }
+
+ if ( e.type === 'keypress' && e.which !== 13 ) {
+ // Only handle keypresses on the "Enter" key.
+ return true;
+ }
+
+ if ( firstTime ) {
+ setupForFirstSort();
+ }
+
+ // Build the cache for the tbody cells
+ // to share between calculations for this sort action.
+ // Re-calculated each time a sort action is performed due to possiblity
+ // that sort values change. Shouldn't be too expensive, but if it becomes
+ // too slow an event based system should be implemented somehow where
+ // cells get event .change() and bubbles up to the <table> here
+ cache = buildCache( table );
+
+ totalRows = ( $table[0].tBodies[0] && $table[0].tBodies[0].rows.length ) || 0;
+ if ( !table.sortDisabled && totalRows > 0 ) {
+ // Get current column sort order
+ this.order = this.count % 2;
+ this.count++;
+
+ cell = this;
+ // Get current column index
+ columns = table.headerToColumns[ this.headerIndex ];
+ newSortList = $.map( columns, function ( c ) {
+ // jQuery "helpfully" flattens the arrays...
+ return [[c, cell.order]];
+ } );
+ // Index of first column belonging to this header
+ i = columns[0];
+
+ if ( !e[config.sortMultiSortKey] ) {
+ // User only wants to sort on one column set
+ // Flush the sort list and add new columns
+ config.sortList = newSortList;
+ } else {
+ // Multi column sorting
+ // It is not possible for one column to belong to multiple headers,
+ // so this is okay - we don't need to check for every value in the columns array
+ if ( isValueInArray( i, config.sortList ) ) {
+ // The user has clicked on an already sorted column.
+ // Reverse the sorting direction for all tables.
+ for ( j = 0; j < config.sortList.length; j++ ) {
+ s = config.sortList[j];
+ o = config.headerList[s[0]];
+ if ( isValueInArray( s[0], newSortList ) ) {
+ o.count = s[1];
+ o.count++;
+ s[1] = o.count % 2;
+ }
+ }
+ } else {
+ // Add columns to sort list array
+ config.sortList = config.sortList.concat( newSortList );
+ }
+ }
+
+ // Reset order/counts of cells not affected by sorting
+ setHeadersOrder( $headers, config.sortList, table.headerToColumns );
+
+ // Set CSS for headers
+ setHeadersCss( $table[0], $headers, config.sortList, sortCSS, sortMsg, table.columnToHeader );
+ appendToTable(
+ $table[0], multisort( $table[0], config.sortList, cache )
+ );
+
+ // Stop normal event by returning false
+ return false;
+ }
+
+ // Cancel selection
+ } ).mousedown( function () {
+ if ( config.cancelSelection ) {
+ this.onselectstart = function () {
+ return false;
+ };
+ return false;
+ }
+ } );
+
+ /**
+ * Sorts the table. If no sorting is specified by passing a list of sort
+ * objects, the table is sorted according to the initial sorting order.
+ * Passing an empty array will reset sorting (basically just reset the headers
+ * making the table appear unsorted).
+ *
+ * @param sortList {Array} (optional) List of sort objects.
+ */
+ $table.data( 'tablesorter' ).sort = function ( sortList ) {
+
+ if ( firstTime ) {
+ setupForFirstSort();
+ }
+
+ if ( sortList === undefined ) {
+ sortList = config.sortList;
+ } else if ( sortList.length > 0 ) {
+ sortList = convertSortList( sortList );
+ }
+
+ // Set each column's sort count to be able to determine the correct sort
+ // order when clicking on a header cell the next time
+ setHeadersOrder( $headers, sortList, table.headerToColumns );
+
+ // re-build the cache for the tbody cells
+ cache = buildCache( table );
+
+ // set css for headers
+ setHeadersCss( table, $headers, sortList, sortCSS, sortMsg, table.columnToHeader );
+
+ // sort the table and append it to the dom
+ appendToTable( table, multisort( table, sortList, cache ) );
+ };
+
+ // sort initially
+ if ( config.sortList.length > 0 ) {
+ setupForFirstSort();
+ config.sortList = convertSortList( config.sortList );
+ $table.data( 'tablesorter' ).sort();
+ }
+
+ } );
+ },
+
+ addParser: function ( parser ) {
+ var i,
+ len = parsers.length,
+ a = true;
+ for ( i = 0; i < len; i++ ) {
+ if ( parsers[i].id.toLowerCase() === parser.id.toLowerCase() ) {
+ a = false;
+ }
+ }
+ if ( a ) {
+ parsers.push( parser );
+ }
+ },
+
+ formatDigit: function ( s ) {
+ var out, c, p, i;
+ if ( ts.transformTable !== false ) {
+ out = '';
+ for ( p = 0; p < s.length; p++ ) {
+ c = s.charAt( p );
+ if ( c in ts.transformTable ) {
+ out += ts.transformTable[c];
+ } else {
+ out += c;
+ }
+ }
+ s = out;
+ }
+ i = parseFloat( s.replace( /[, ]/g, '' ).replace( '\u2212', '-' ) );
+ return isNaN( i ) ? 0 : i;
+ },
+
+ formatFloat: function ( s ) {
+ var i = parseFloat( s );
+ return isNaN( i ) ? 0 : i;
+ },
+
+ formatInt: function ( s ) {
+ var i = parseInt( s, 10 );
+ return isNaN( i ) ? 0 : i;
+ },
+
+ clearTableBody: function ( table ) {
+ $( table.tBodies[0] ).empty();
+ }
+ };
+
+ // Shortcut
+ ts = $.tablesorter;
+
+ // Register as jQuery prototype method
+ $.fn.tablesorter = function ( settings ) {
+ return ts.construct( this, settings );
+ };
+
+ // Add default parsers
+ ts.addParser( {
+ id: 'text',
+ is: function () {
+ return true;
+ },
+ format: function ( s ) {
+ s = $.trim( s.toLowerCase() );
+ if ( ts.collationRegex ) {
+ var tsc = ts.collationTable;
+ s = s.replace( ts.collationRegex, function ( match ) {
+ var r = tsc[match] ? tsc[match] : tsc[match.toUpperCase()];
+ return r.toLowerCase();
+ } );
+ }
+ return s;
+ },
+ type: 'text'
+ } );
+
+ ts.addParser( {
+ id: 'IPAddress',
+ is: function ( s ) {
+ return ts.rgx.IPAddress[0].test( s );
+ },
+ format: function ( s ) {
+ var i, item,
+ a = s.split( '.' ),
+ r = '',
+ len = a.length;
+ for ( i = 0; i < len; i++ ) {
+ item = a[i];
+ if ( item.length === 1 ) {
+ r += '00' + item;
+ } else if ( item.length === 2 ) {
+ r += '0' + item;
+ } else {
+ r += item;
+ }
+ }
+ return $.tablesorter.formatFloat( r );
+ },
+ type: 'numeric'
+ } );
+
+ ts.addParser( {
+ id: 'currency',
+ is: function ( s ) {
+ return ts.rgx.currency[0].test( s );
+ },
+ format: function ( s ) {
+ return $.tablesorter.formatDigit( s.replace( ts.rgx.currency[1], '' ) );
+ },
+ type: 'numeric'
+ } );
+
+ ts.addParser( {
+ id: 'url',
+ is: function ( s ) {
+ return ts.rgx.url[0].test( s );
+ },
+ format: function ( s ) {
+ return $.trim( s.replace( ts.rgx.url[1], '' ) );
+ },
+ type: 'text'
+ } );
+
+ ts.addParser( {
+ id: 'isoDate',
+ is: function ( s ) {
+ return ts.rgx.isoDate[0].test( s );
+ },
+ format: function ( s ) {
+ return $.tablesorter.formatFloat( ( s !== '' ) ? new Date( s.replace(
+ new RegExp( /-/g ), '/' ) ).getTime() : '0' );
+ },
+ type: 'numeric'
+ } );
+
+ ts.addParser( {
+ id: 'usLongDate',
+ is: function ( s ) {
+ return ts.rgx.usLongDate[0].test( s );
+ },
+ format: function ( s ) {
+ return $.tablesorter.formatFloat( new Date( s ).getTime() );
+ },
+ type: 'numeric'
+ } );
+
+ ts.addParser( {
+ id: 'date',
+ is: function ( s ) {
+ return ( ts.dateRegex[0].test( s ) || ts.dateRegex[1].test( s ) || ts.dateRegex[2].test( s ) );
+ },
+ format: function ( s ) {
+ var match, y;
+ s = $.trim( s.toLowerCase() );
+
+ if ( ( match = s.match( ts.dateRegex[0] ) ) !== null ) {
+ if ( mw.config.get( 'wgDefaultDateFormat' ) === 'mdy' || mw.config.get( 'wgContentLanguage' ) === 'en' ) {
+ s = [ match[3], match[1], match[2] ];
+ } else if ( mw.config.get( 'wgDefaultDateFormat' ) === 'dmy' ) {
+ s = [ match[3], match[2], match[1] ];
+ } else {
+ // If we get here, we don't know which order the dd-dd-dddd
+ // date is in. So return something not entirely invalid.
+ return '99999999';
+ }
+ } else if ( ( match = s.match( ts.dateRegex[1] ) ) !== null ) {
+ s = [ match[3], '' + ts.monthNames[match[2]], match[1] ];
+ } else if ( ( match = s.match( ts.dateRegex[2] ) ) !== null ) {
+ s = [ match[3], '' + ts.monthNames[match[1]], match[2] ];
+ } else {
+ // Should never get here
+ return '99999999';
+ }
+
+ // Pad Month and Day
+ if ( s[1].length === 1 ) {
+ s[1] = '0' + s[1];
+ }
+ if ( s[2].length === 1 ) {
+ s[2] = '0' + s[2];
+ }
+
+ if ( ( y = parseInt( s[0], 10 ) ) < 100 ) {
+ // Guestimate years without centuries
+ if ( y < 30 ) {
+ s[0] = 2000 + y;
+ } else {
+ s[0] = 1900 + y;
+ }
+ }
+ while ( s[0].length < 4 ) {
+ s[0] = '0' + s[0];
+ }
+ return parseInt( s.join( '' ), 10 );
+ },
+ type: 'numeric'
+ } );
+
+ ts.addParser( {
+ id: 'time',
+ is: function ( s ) {
+ return ts.rgx.time[0].test( s );
+ },
+ format: function ( s ) {
+ return $.tablesorter.formatFloat( new Date( '2000/01/01 ' + s ).getTime() );
+ },
+ type: 'numeric'
+ } );
+
+ ts.addParser( {
+ id: 'number',
+ is: function ( s ) {
+ return $.tablesorter.numberRegex.test( $.trim( s ) );
+ },
+ format: function ( s ) {
+ return $.tablesorter.formatDigit( s );
+ },
+ type: 'numeric'
+ } );
+
+}( jQuery, mediaWiki ) );
diff --git a/resources/src/jquery/jquery.textSelection.js b/resources/src/jquery/jquery.textSelection.js
new file mode 100644
index 00000000..8d440fdc
--- /dev/null
+++ b/resources/src/jquery/jquery.textSelection.js
@@ -0,0 +1,572 @@
+/**
+ * These plugins provide extra functionality for interaction with textareas.
+ */
+( function ( $ ) {
+ if ( document.selection && document.selection.createRange ) {
+ // On IE, patch the focus() method to restore the windows' scroll position
+ // (bug 32241)
+ $.fn.extend( {
+ focus: ( function ( jqFocus ) {
+ return function () {
+ var $w, state, result;
+ if ( arguments.length === 0 ) {
+ $w = $( window );
+ state = { top: $w.scrollTop(), left: $w.scrollLeft() };
+ result = jqFocus.apply( this, arguments );
+ window.scrollTo( state.top, state.left );
+ return result;
+ }
+ return jqFocus.apply( this, arguments );
+ };
+ }( $.fn.focus ) )
+ } );
+ }
+
+ $.fn.textSelection = function ( command, options ) {
+ var fn,
+ context,
+ hasWikiEditorSurface, // The alt edit surface needs to implement the WikiEditor API
+ needSave,
+ retval;
+
+ /**
+ * Helper function to get an IE TextRange object for an element
+ */
+ function rangeForElementIE( e ) {
+ if ( e.nodeName.toLowerCase() === 'input' ) {
+ return e.createTextRange();
+ } else {
+ var sel = document.body.createTextRange();
+ sel.moveToElementText( e );
+ return sel;
+ }
+ }
+
+ /**
+ * Helper function for IE for activating the textarea. Called only in the
+ * IE-specific code paths below; makes use of IE-specific non-standard
+ * function setActive() if possible to avoid screen flicker.
+ */
+ function activateElementOnIE( element ) {
+ if ( element.setActive ) {
+ element.setActive(); // bug 32241: doesn't scroll
+ } else {
+ $( element ).focus(); // may scroll (but we patched it above)
+ }
+ }
+
+ fn = {
+ /**
+ * Get the contents of the textarea
+ */
+ getContents: function () {
+ return this.val();
+ },
+ /**
+ * Set the contents of the textarea, replacing anything that was there before
+ */
+ setContents: function ( content ) {
+ this.val( content );
+ },
+ /**
+ * Get the currently selected text in this textarea. Will focus the textarea
+ * in some browsers (IE/Opera)
+ */
+ getSelection: function () {
+ var retval, range,
+ el = this.get( 0 );
+
+ if ( !el || $( el ).is( ':hidden' ) ) {
+ retval = '';
+ } else if ( document.selection && document.selection.createRange ) {
+ activateElementOnIE( el );
+ range = document.selection.createRange();
+ retval = range.text;
+ } else if ( el.selectionStart || el.selectionStart === 0 ) {
+ retval = el.value.substring( el.selectionStart, el.selectionEnd );
+ }
+
+ return retval;
+ },
+ /**
+ * Ported from skins/common/edit.js by Trevor Parscal
+ * (c) 2009 Wikimedia Foundation (GPLv2) - http://www.wikimedia.org
+ *
+ * Inserts text at the beginning and end of a text selection, optionally
+ * inserting text at the caret when selection is empty.
+ *
+ * @fixme document the options parameters
+ */
+ encapsulateSelection: function ( options ) {
+ return this.each( function () {
+ var selText, scrollTop, insertText,
+ isSample, range, range2, range3, startPos, endPos,
+ pre = options.pre,
+ post = options.post;
+
+ /**
+ * Check if the selected text is the same as the insert text
+ */
+ function checkSelectedText() {
+ if ( !selText ) {
+ selText = options.peri;
+ isSample = true;
+ } else if ( options.replace ) {
+ selText = options.peri;
+ } else {
+ while ( selText.charAt( selText.length - 1 ) === ' ' ) {
+ // Exclude ending space char
+ selText = selText.slice( 0, -1 );
+ post += ' ';
+ }
+ while ( selText.charAt( 0 ) === ' ' ) {
+ // Exclude prepending space char
+ selText = selText.slice( 1 );
+ pre = ' ' + pre;
+ }
+ }
+ }
+
+ /**
+ * Do the splitlines stuff.
+ *
+ * Wrap each line of the selected text with pre and post
+ */
+ function doSplitLines( selText, pre, post ) {
+ var i,
+ insertText = '',
+ selTextArr = selText.split( '\n' );
+ for ( i = 0; i < selTextArr.length; i++ ) {
+ insertText += pre + selTextArr[i] + post;
+ if ( i !== selTextArr.length - 1 ) {
+ insertText += '\n';
+ }
+ }
+ return insertText;
+ }
+
+ isSample = false;
+ // Do nothing if display none
+ if ( this.style.display !== 'none' ) {
+ if ( document.selection && document.selection.createRange ) {
+ // IE
+
+ // Note that IE9 will trigger the next section unless we check this first.
+ // See bug 35201.
+
+ activateElementOnIE( this );
+ if ( context ) {
+ context.fn.restoreCursorAndScrollTop();
+ }
+ if ( options.selectionStart !== undefined ) {
+ $( this ).textSelection( 'setSelection', { 'start': options.selectionStart, 'end': options.selectionEnd } );
+ }
+
+ selText = $( this ).textSelection( 'getSelection' );
+ scrollTop = this.scrollTop;
+ range = document.selection.createRange();
+
+ checkSelectedText();
+ insertText = pre + selText + post;
+ if ( options.splitlines ) {
+ insertText = doSplitLines( selText, pre, post );
+ }
+ if ( options.ownline && range.moveStart ) {
+ range2 = document.selection.createRange();
+ range2.collapse();
+ range2.moveStart( 'character', -1 );
+ // FIXME: Which check is correct?
+ if ( range2.text !== '\r' && range2.text !== '\n' && range2.text !== '' ) {
+ insertText = '\n' + insertText;
+ pre += '\n';
+ }
+ range3 = document.selection.createRange();
+ range3.collapse( false );
+ range3.moveEnd( 'character', 1 );
+ if ( range3.text !== '\r' && range3.text !== '\n' && range3.text !== '' ) {
+ insertText += '\n';
+ post += '\n';
+ }
+ }
+
+ range.text = insertText;
+ if ( isSample && options.selectPeri && range.moveStart ) {
+ range.moveStart( 'character', -post.length - selText.length );
+ range.moveEnd( 'character', -post.length );
+ }
+ range.select();
+ // Restore the scroll position
+ this.scrollTop = scrollTop;
+ } else if ( this.selectionStart || this.selectionStart === 0 ) {
+ // Mozilla/Opera
+
+ $( this ).focus();
+ if ( options.selectionStart !== undefined ) {
+ $( this ).textSelection( 'setSelection', { 'start': options.selectionStart, 'end': options.selectionEnd } );
+ }
+
+ selText = $( this ).textSelection( 'getSelection' );
+ startPos = this.selectionStart;
+ endPos = this.selectionEnd;
+ scrollTop = this.scrollTop;
+ checkSelectedText();
+ if ( options.selectionStart !== undefined
+ && endPos - startPos !== options.selectionEnd - options.selectionStart )
+ {
+ // This means there is a difference in the selection range returned by browser and what we passed.
+ // This happens for Chrome in the case of composite characters. Ref bug #30130
+ // Set the startPos to the correct position.
+ startPos = options.selectionStart;
+ }
+
+ insertText = pre + selText + post;
+ if ( options.splitlines ) {
+ insertText = doSplitLines( selText, pre, post );
+ }
+ if ( options.ownline ) {
+ if ( startPos !== 0 && this.value.charAt( startPos - 1 ) !== '\n' && this.value.charAt( startPos - 1 ) !== '\r' ) {
+ insertText = '\n' + insertText;
+ pre += '\n';
+ }
+ if ( this.value.charAt( endPos ) !== '\n' && this.value.charAt( endPos ) !== '\r' ) {
+ insertText += '\n';
+ post += '\n';
+ }
+ }
+ this.value = this.value.slice( 0, startPos ) + insertText +
+ this.value.slice( endPos );
+ // Setting this.value scrolls the textarea to the top, restore the scroll position
+ this.scrollTop = scrollTop;
+ if ( window.opera ) {
+ pre = pre.replace( /\r?\n/g, '\r\n' );
+ selText = selText.replace( /\r?\n/g, '\r\n' );
+ post = post.replace( /\r?\n/g, '\r\n' );
+ }
+ if ( isSample && options.selectPeri && !options.splitlines ) {
+ this.selectionStart = startPos + pre.length;
+ this.selectionEnd = startPos + pre.length + selText.length;
+ } else {
+ this.selectionStart = startPos + insertText.length;
+ this.selectionEnd = this.selectionStart;
+ }
+ }
+ }
+ $( this ).trigger( 'encapsulateSelection', [ options.pre, options.peri, options.post, options.ownline,
+ options.replace, options.spitlines ] );
+ } );
+ },
+ /**
+ * Ported from Wikia's LinkSuggest extension
+ * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
+ * Some code copied from
+ * http://www.dedestruct.com/2008/03/22/howto-cross-browser-cursor-position-in-textareas/
+ *
+ * Get the position (in resolution of bytes not necessarily characters)
+ * in a textarea
+ *
+ * Will focus the textarea in some browsers (IE/Opera)
+ *
+ * @fixme document the options parameters
+ */
+ getCaretPosition: function ( options ) {
+ function getCaret( e ) {
+ var caretPos = 0,
+ endPos = 0,
+ preText, rawPreText, periText,
+ rawPeriText, postText, rawPostText,
+ // IE Support
+ preFinished,
+ periFinished,
+ postFinished,
+ // Range containing text in the selection
+ periRange,
+ // Range containing text before the selection
+ preRange,
+ // Range containing text after the selection
+ postRange;
+
+ if ( e && document.selection && document.selection.createRange ) {
+ // IE doesn't properly report non-selected caret position through
+ // the selection ranges when textarea isn't focused. This can
+ // lead to saving a bogus empty selection, which then screws up
+ // whatever we do later (bug 31847).
+ activateElementOnIE( e );
+
+ preFinished = false;
+ periFinished = false;
+ postFinished = false;
+ periRange = document.selection.createRange().duplicate();
+
+ preRange = rangeForElementIE( e );
+ // Move the end where we need it
+ preRange.setEndPoint( 'EndToStart', periRange );
+
+ postRange = rangeForElementIE( e );
+ // Move the start where we need it
+ postRange.setEndPoint( 'StartToEnd', periRange );
+
+ // Load the text values we need to compare
+ preText = rawPreText = preRange.text;
+ periText = rawPeriText = periRange.text;
+ postText = rawPostText = postRange.text;
+
+ /*
+ * Check each range for trimmed newlines by shrinking the range by 1
+ * character and seeing if the text property has changed. If it has
+ * not changed then we know that IE has trimmed a \r\n from the end.
+ */
+ do {
+ if ( !preFinished ) {
+ if ( preRange.compareEndPoints( 'StartToEnd', preRange ) === 0 ) {
+ preFinished = true;
+ } else {
+ preRange.moveEnd( 'character', -1 );
+ if ( preRange.text === preText ) {
+ rawPreText += '\r\n';
+ } else {
+ preFinished = true;
+ }
+ }
+ }
+ if ( !periFinished ) {
+ if ( periRange.compareEndPoints( 'StartToEnd', periRange ) === 0 ) {
+ periFinished = true;
+ } else {
+ periRange.moveEnd( 'character', -1 );
+ if ( periRange.text === periText ) {
+ rawPeriText += '\r\n';
+ } else {
+ periFinished = true;
+ }
+ }
+ }
+ if ( !postFinished ) {
+ if ( postRange.compareEndPoints( 'StartToEnd', postRange ) === 0 ) {
+ postFinished = true;
+ } else {
+ postRange.moveEnd( 'character', -1 );
+ if ( postRange.text === postText ) {
+ rawPostText += '\r\n';
+ } else {
+ postFinished = true;
+ }
+ }
+ }
+ } while ( ( !preFinished || !periFinished || !postFinished ) );
+ caretPos = rawPreText.replace( /\r\n/g, '\n' ).length;
+ endPos = caretPos + rawPeriText.replace( /\r\n/g, '\n' ).length;
+ } else if ( e && ( e.selectionStart || e.selectionStart === 0 ) ) {
+ // Firefox support
+ caretPos = e.selectionStart;
+ endPos = e.selectionEnd;
+ }
+ return options.startAndEnd ? [ caretPos, endPos ] : caretPos;
+ }
+ return getCaret( this.get( 0 ) );
+ },
+ /**
+ * @fixme document the options parameters
+ */
+ setSelection: function ( options ) {
+ return this.each( function () {
+ var selection, length, newLines;
+ // Do nothing if hidden
+ if ( !$( this ).is( ':hidden' ) ) {
+ if ( this.selectionStart || this.selectionStart === 0 ) {
+ // Opera 9.0 doesn't allow setting selectionStart past
+ // selectionEnd; any attempts to do that will be ignored
+ // Make sure to set them in the right order
+ if ( options.start > this.selectionEnd ) {
+ this.selectionEnd = options.end;
+ this.selectionStart = options.start;
+ } else {
+ this.selectionStart = options.start;
+ this.selectionEnd = options.end;
+ }
+ } else if ( document.body.createTextRange ) {
+ selection = rangeForElementIE( this );
+ length = this.value.length;
+ // IE doesn't count \n when computing the offset, so we won't either
+ newLines = this.value.match( /\n/g );
+ if ( newLines ) {
+ length = length - newLines.length;
+ }
+ selection.moveStart( 'character', options.start );
+ selection.moveEnd( 'character', -length + options.end );
+
+ // This line can cause an error under certain circumstances (textarea empty, no selection)
+ // Silence that error
+ try {
+ selection.select();
+ } catch ( e ) { }
+ }
+ }
+ } );
+ },
+ /**
+ * Ported from Wikia's LinkSuggest extension
+ * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
+ *
+ * Scroll a textarea to the current cursor position. You can set the cursor
+ * position with setSelection()
+ * @param options boolean Whether to force a scroll even if the caret position
+ * is already visible. Defaults to false
+ *
+ * @fixme document the options parameters (function body suggests options.force is a boolean, not options itself)
+ */
+ scrollToCaretPosition: function ( options ) {
+ function getLineLength( e ) {
+ return Math.floor( e.scrollWidth / ( $.client.profile().platform === 'linux' ? 7 : 8 ) );
+ }
+ function getCaretScrollPosition( e ) {
+ // FIXME: This functions sucks and is off by a few lines most
+ // of the time. It should be replaced by something decent.
+ var i, j,
+ nextSpace,
+ text = e.value.replace( /\r/g, '' ),
+ caret = $( e ).textSelection( 'getCaretPosition' ),
+ lineLength = getLineLength( e ),
+ row = 0,
+ charInLine = 0,
+ lastSpaceInLine = 0;
+
+ for ( i = 0; i < caret; i++ ) {
+ charInLine++;
+ if ( text.charAt( i ) === ' ' ) {
+ lastSpaceInLine = charInLine;
+ } else if ( text.charAt( i ) === '\n' ) {
+ lastSpaceInLine = 0;
+ charInLine = 0;
+ row++;
+ }
+ if ( charInLine > lineLength ) {
+ if ( lastSpaceInLine > 0 ) {
+ charInLine = charInLine - lastSpaceInLine;
+ lastSpaceInLine = 0;
+ row++;
+ }
+ }
+ }
+ nextSpace = 0;
+ for ( j = caret; j < caret + lineLength; j++ ) {
+ if (
+ text.charAt( j ) === ' ' ||
+ text.charAt( j ) === '\n' ||
+ caret === text.length
+ ) {
+ nextSpace = j;
+ break;
+ }
+ }
+ if ( nextSpace > lineLength && caret <= lineLength ) {
+ charInLine = caret - lastSpaceInLine;
+ row++;
+ }
+ return ( $.client.profile().platform === 'mac' ? 13 : ( $.client.profile().platform === 'linux' ? 15 : 16 ) ) * row;
+ }
+ return this.each( function () {
+ var scroll, range, savedRange, pos, oldScrollTop;
+ // Do nothing if hidden
+ if ( !$( this ).is( ':hidden' ) ) {
+ if ( this.selectionStart || this.selectionStart === 0 ) {
+ // Mozilla
+ scroll = getCaretScrollPosition( this );
+ if ( options.force || scroll < $( this ).scrollTop() ||
+ scroll > $( this ).scrollTop() + $( this ).height() ) {
+ $( this ).scrollTop( scroll );
+ }
+ } else if ( document.selection && document.selection.createRange ) {
+ // IE / Opera
+ /*
+ * IE automatically scrolls the selected text to the
+ * bottom of the textarea at range.select() time, except
+ * if it was already in view and the cursor position
+ * wasn't changed, in which case it does nothing. To
+ * cover that case, we'll force it to act by moving one
+ * character back and forth.
+ */
+ range = document.body.createTextRange();
+ savedRange = document.selection.createRange();
+ pos = $( this ).textSelection( 'getCaretPosition' );
+ oldScrollTop = this.scrollTop;
+ range.moveToElementText( this );
+ range.collapse();
+ range.move( 'character', pos + 1 );
+ range.select();
+ if ( this.scrollTop !== oldScrollTop ) {
+ this.scrollTop += range.offsetTop;
+ } else if ( options.force ) {
+ range.move( 'character', -1 );
+ range.select();
+ }
+ savedRange.select();
+ }
+ }
+ $( this ).trigger( 'scrollToPosition' );
+ } );
+ }
+ };
+
+ // Apply defaults
+ switch ( command ) {
+ //case 'getContents': // no params
+ //case 'setContents': // no params with defaults
+ //case 'getSelection': // no params
+ case 'encapsulateSelection':
+ options = $.extend( {
+ pre: '', // Text to insert before the cursor/selection
+ peri: '', // Text to insert between pre and post and select afterwards
+ post: '', // Text to insert after the cursor/selection
+ ownline: false, // Put the inserted text on a line of its own
+ replace: false, // If there is a selection, replace it with peri instead of leaving it alone
+ selectPeri: true, // Select the peri text if it was inserted (but not if there was a selection and replace==false, or if splitlines==true)
+ splitlines: false, // If multiple lines are selected, encapsulate each line individually
+ selectionStart: undefined, // Position to start selection at
+ selectionEnd: undefined // Position to end selection at. Defaults to start
+ }, options );
+ break;
+ case 'getCaretPosition':
+ options = $.extend( {
+ // Return [start, end] instead of just start
+ startAndEnd: false
+ }, options );
+ // FIXME: We may not need character position-based functions if we insert markers in the right places
+ break;
+ case 'setSelection':
+ options = $.extend( {
+ // Position to start selection at
+ start: undefined,
+ // Position to end selection at. Defaults to start
+ end: undefined
+ }, options );
+
+ if ( options.end === undefined ) {
+ options.end = options.start;
+ }
+ // FIXME: We may not need character position-based functions if we insert markers in the right places
+ break;
+ case 'scrollToCaretPosition':
+ options = $.extend( {
+ force: false // Force a scroll even if the caret position is already visible
+ }, options );
+ break;
+ }
+
+ context = $( this ).data( 'wikiEditor-context' );
+ hasWikiEditorSurface = ( context !== undefined && context.$iframe !== undefined );
+
+ // IE selection restore voodoo
+ needSave = false;
+ if ( hasWikiEditorSurface && context.savedSelection !== null ) {
+ context.fn.restoreSelection();
+ needSave = true;
+ }
+ retval = ( hasWikiEditorSurface && context.fn[command] !== undefined ? context.fn : fn )[command].call( this, options );
+ if ( hasWikiEditorSurface && needSave ) {
+ context.fn.saveSelection();
+ }
+
+ return retval;
+ };
+
+}( jQuery ) );
diff --git a/resources/src/json-skip.js b/resources/src/json-skip.js
new file mode 100644
index 00000000..0a1e1c26
--- /dev/null
+++ b/resources/src/json-skip.js
@@ -0,0 +1,4 @@
+/*!
+ * Skip function for json2.js.
+ */
+return !!( window.JSON && JSON.stringify && JSON.parse );
diff --git a/resources/src/mediawiki.action/images/green-checkmark.png b/resources/src/mediawiki.action/images/green-checkmark.png
new file mode 100644
index 00000000..8ec604ea
--- /dev/null
+++ b/resources/src/mediawiki.action/images/green-checkmark.png
Binary files differ
diff --git a/resources/src/mediawiki.action/images/nextredirect-ltr.png b/resources/src/mediawiki.action/images/nextredirect-ltr.png
new file mode 100644
index 00000000..cd657c33
--- /dev/null
+++ b/resources/src/mediawiki.action/images/nextredirect-ltr.png
Binary files differ
diff --git a/resources/src/mediawiki.action/images/nextredirect-rtl.png b/resources/src/mediawiki.action/images/nextredirect-rtl.png
new file mode 100644
index 00000000..b788f334
--- /dev/null
+++ b/resources/src/mediawiki.action/images/nextredirect-rtl.png
Binary files differ
diff --git a/resources/src/mediawiki.action/images/redirect-ltr.png b/resources/src/mediawiki.action/images/redirect-ltr.png
new file mode 100644
index 00000000..695f2a13
--- /dev/null
+++ b/resources/src/mediawiki.action/images/redirect-ltr.png
Binary files differ
diff --git a/resources/src/mediawiki.action/images/redirect-rtl.png b/resources/src/mediawiki.action/images/redirect-rtl.png
new file mode 100644
index 00000000..c954a2ad
--- /dev/null
+++ b/resources/src/mediawiki.action/images/redirect-rtl.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css b/resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css
new file mode 100644
index 00000000..1af4a7a0
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css
@@ -0,0 +1,17 @@
+/* Styles for collapsible lists of templates used and hidden categories */
+.mw-editfooter-toggler {
+ cursor: pointer;
+ background-position: left center;
+ padding-left: 16px;
+}
+
+.mw-editfooter-list {
+ margin-bottom: 1em;
+ margin-left: 2.5em;
+}
+
+/* Show/hide animation is incorrect if the table has a margin set. Extra
+ * "table.wikitable" is needed in the selector for CSS specificity. */
+table.wikitable.preview-limit-report {
+ margin: 0;
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js b/resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js
new file mode 100644
index 00000000..7ae51aba
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js
@@ -0,0 +1,54 @@
+jQuery( document ).ready( function ( $ ) {
+ var collapsibleLists, i, handleOne;
+
+ // Collapsible lists of categories and templates
+ collapsibleLists = [
+ {
+ $list: $( '.templatesUsed ul' ),
+ $toggler: $( '.mw-templatesUsedExplanation' ),
+ cookieName: 'templates-used-list'
+ },
+ {
+ $list: $( '.hiddencats ul' ),
+ $toggler: $( '.mw-hiddenCategoriesExplanation' ),
+ cookieName: 'hidden-categories-list'
+ },
+ {
+ $list: $( '.preview-limit-report-wrapper' ),
+ $toggler: $( '.mw-limitReportExplanation' ),
+ cookieName: 'preview-limit-report'
+ }
+ ];
+
+ handleOne = function ( $list, $toggler, cookieName ) {
+ var isCollapsed = $.cookie( cookieName ) !== 'expanded';
+
+ // Style the toggler with an arrow icon and add a tabIndex and a role for accessibility
+ $toggler.addClass( 'mw-editfooter-toggler' ).prop( 'tabIndex', 0 ).attr( 'role', 'button' );
+ $list.addClass( 'mw-editfooter-list' );
+
+ $list.makeCollapsible( {
+ $customTogglers: $toggler,
+ linksPassthru: true,
+ plainMode: true,
+ collapsed: isCollapsed
+ } );
+
+ $toggler.addClass( isCollapsed ? 'mw-icon-arrow-collapsed' : 'mw-icon-arrow-expanded' );
+
+ $list.on( 'beforeExpand.mw-collapsible', function () {
+ $toggler.removeClass( 'mw-icon-arrow-collapsed' ).addClass( 'mw-icon-arrow-expanded' );
+ $.cookie( cookieName, 'expanded' );
+ } );
+
+ $list.on( 'beforeCollapse.mw-collapsible', function () {
+ $toggler.removeClass( 'mw-icon-arrow-expanded' ).addClass( 'mw-icon-arrow-collapsed' );
+ $.cookie( cookieName, 'collapsed' );
+ } );
+ };
+
+ for ( i = 0; i < collapsibleLists.length; i++ ) {
+ // Pass to a function for iteration-local variables
+ handleOne( collapsibleLists[i].$list, collapsibleLists[i].$toggler, collapsibleLists[i].cookieName );
+ }
+} );
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.css b/resources/src/mediawiki.action/mediawiki.action.edit.css
new file mode 100644
index 00000000..45ba5437
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.css
@@ -0,0 +1,18 @@
+/*!
+ * Styles for elements of the editing form, loaded only when JavaScript is enabled.
+ */
+
+.mw-toolbar-editbutton {
+ width: 23px;
+ height: 22px;
+ cursor: pointer;
+ vertical-align: middle;
+ /* Cross-browser inline-block */
+ /* Firefox 2 */
+ display: -moz-inline-block;
+ /* Modern browsers */
+ display: inline-block;
+ /* IE7 */
+ zoom: 1;
+ *display: inline;
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js b/resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js
new file mode 100644
index 00000000..b5654400
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js
@@ -0,0 +1,59 @@
+/*
+ * Javascript for module editWarning
+ */
+( function ( mw, $ ) {
+ 'use strict';
+
+ $( function () {
+ var savedWindowOnBeforeUnload,
+ $wpTextbox1 = $( '#wpTextbox1' ),
+ $wpSummary = $( '#wpSummary' );
+ // Check if EditWarning is enabled and if we need it
+ if ( $wpTextbox1.length === 0 ) {
+ return true;
+ }
+ // Get the original values of some form elements
+ $wpTextbox1.add( $wpSummary ).each( function () {
+ $( this ).data( 'origtext', $( this ).val() );
+ } );
+ $( window )
+ .on( 'beforeunload.editwarning', function () {
+ var retval;
+
+ // Check if the current values of some form elements are the same as
+ // the original values
+ if (
+ mw.config.get( 'wgAction' ) === 'submit' ||
+ $wpTextbox1.data( 'origtext' ) !== $wpTextbox1.textSelection( 'getContents' ) ||
+ $wpSummary.data( 'origtext' ) !== $wpSummary.textSelection( 'getContents' )
+ ) {
+ // Return our message
+ retval = mw.msg( 'editwarning-warning' );
+ }
+
+ // Unset the onbeforeunload handler so we don't break page caching in Firefox
+ savedWindowOnBeforeUnload = window.onbeforeunload;
+ window.onbeforeunload = null;
+ if ( retval !== undefined ) {
+ // ...but if the user chooses not to leave the page, we need to rebind it
+ setTimeout( function () {
+ window.onbeforeunload = savedWindowOnBeforeUnload;
+ }, 1 );
+ return retval;
+ }
+ } )
+ .on( 'pageshow.editwarning', function () {
+ // Re-add onbeforeunload handler
+ if ( !window.onbeforeunload ) {
+ window.onbeforeunload = savedWindowOnBeforeUnload;
+ }
+ } );
+
+ // Add form submission handler
+ $( '#editform' ).submit( function () {
+ // Unbind our handlers
+ $( window ).off( '.editwarning' );
+ } );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.js b/resources/src/mediawiki.action/mediawiki.action.edit.js
new file mode 100644
index 00000000..4519b049
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.js
@@ -0,0 +1,217 @@
+/**
+ * Interface for the classic edit toolbar.
+ *
+ * @class mw.toolbar
+ * @singleton
+ */
+( function ( mw, $ ) {
+ var toolbar, isReady, $toolbar, queue, slice, $currentFocused;
+
+ /**
+ * Internal helper that does the actual insertion of the button into the toolbar.
+ *
+ * See #addButton for parameter documentation.
+ *
+ * @private
+ */
+ function insertButton( b, speedTip, tagOpen, tagClose, sampleText, imageId ) {
+ var $button;
+
+ // Backwards compatibility
+ if ( typeof b !== 'object' ) {
+ b = {
+ imageFile: b,
+ speedTip: speedTip,
+ tagOpen: tagOpen,
+ tagClose: tagClose,
+ sampleText: sampleText,
+ imageId: imageId
+ };
+ }
+
+ if ( b.imageFile ) {
+ $button = $( '<img>' ).attr( {
+ src: b.imageFile,
+ alt: b.speedTip,
+ title: b.speedTip,
+ id: b.imageId || undefined,
+ 'class': 'mw-toolbar-editbutton'
+ } );
+ } else {
+ $button = $( '<div>' ).attr( {
+ title: b.speedTip,
+ id: b.imageId || undefined,
+ 'class': 'mw-toolbar-editbutton'
+ } );
+ }
+
+ $button.click( function ( e ) {
+ if ( b.onClick !== undefined ) {
+ b.onClick( e );
+ } else {
+ toolbar.insertTags( b.tagOpen, b.tagClose, b.sampleText );
+ }
+
+ return false;
+ } );
+
+ $toolbar.append( $button );
+ }
+
+ isReady = false;
+ $toolbar = false;
+
+ /**
+ * @private
+ * @property {Array}
+ * Contains button objects (and for backwards compatibilty, it can
+ * also contains an arguments array for insertButton).
+ */
+ queue = [];
+ slice = queue.slice;
+
+ toolbar = {
+
+ /**
+ * Add buttons to the toolbar.
+ *
+ * Takes care of race conditions and time-based dependencies
+ * by placing buttons in a queue if this method is called before
+ * the toolbar is created.
+ *
+ * For backwards-compatibility, passing `imageFile`, `speedTip`, `tagOpen`, `tagClose`,
+ * `sampleText` and `imageId` as separate arguments (in this order) is also supported.
+ *
+ * @param {Object} button Object with the following properties.
+ * You are required to provide *either* the `onClick` parameter, or the three parameters
+ * `tagOpen`, `tagClose` and `sampleText`, but not both (they're mutually exclusive).
+ * @param {string} [button.imageFile] Image to use for the button.
+ * @param {string} button.speedTip Tooltip displayed when user mouses over the button.
+ * @param {Function} [button.onClick] Function to be executed when the button is clicked.
+ * @param {string} [button.tagOpen]
+ * @param {string} [button.tagClose]
+ * @param {string} [button.sampleText] Alternative to `onClick`. `tagOpen`, `tagClose` and
+ * `sampleText` together provide the markup that should be inserted into page text at
+ * current cursor position.
+ * @param {string} [button.imageId] `id` attribute of the button HTML element. Can be
+ * used to define the image with CSS if it's not provided as `imageFile`.
+ */
+ addButton: function () {
+ if ( isReady ) {
+ insertButton.apply( toolbar, arguments );
+ } else {
+ // Convert arguments list to array
+ queue.push( slice.call( arguments ) );
+ }
+ },
+ /**
+ * Add multiple buttons to the toolbar (see also #addButton).
+ *
+ * Example usage:
+ *
+ * addButtons( [ { .. }, { .. }, { .. } ] );
+ * addButtons( { .. }, { .. } );
+ *
+ * @param {Object|Array...} [buttons] An array of button objects or the first
+ * button object in a list of variadic arguments.
+ */
+ addButtons: function ( buttons ) {
+ if ( !$.isArray( buttons ) ) {
+ buttons = slice.call( arguments );
+ }
+ if ( isReady ) {
+ $.each( buttons, function () {
+ insertButton( this );
+ } );
+ } else {
+ // Push each button into the queue
+ queue.push.apply( queue, buttons );
+ }
+ },
+
+ /**
+ * Apply tagOpen/tagClose to selection in currently focused textarea.
+ *
+ * Uses `sampleText` if selection is empty.
+ *
+ * @param {string} tagOpen
+ * @param {string} tagClose
+ * @param {string} sampleText
+ */
+ insertTags: function ( tagOpen, tagClose, sampleText ) {
+ if ( $currentFocused && $currentFocused.length ) {
+ $currentFocused.textSelection(
+ 'encapsulateSelection', {
+ pre: tagOpen,
+ peri: sampleText,
+ post: tagClose
+ }
+ );
+ }
+ },
+
+ // For backwards compatibility,
+ // Called from EditPage.php, maybe in other places as well.
+ init: function () {}
+ };
+
+ // Legacy (for compatibility with the code previously in skins/common.edit.js)
+ mw.log.deprecate( window, 'addButton', toolbar.addButton, 'Use mw.toolbar.addButton instead.' );
+ mw.log.deprecate( window, 'insertTags', toolbar.insertTags, 'Use mw.toolbar.insertTags instead.' );
+
+ // Expose API publicly
+ mw.toolbar = toolbar;
+
+ $( function () {
+ var i, b, editBox, scrollTop, $editForm;
+
+ // Used to determine where to insert tags
+ $currentFocused = $( '#wpTextbox1' );
+
+ // Populate the selector cache for $toolbar
+ $toolbar = $( '#toolbar' );
+
+ for ( i = 0; i < queue.length; i++ ) {
+ b = queue[i];
+ if ( $.isArray( b ) ) {
+ // Forwarded arguments array from mw.toolbar.addButton
+ insertButton.apply( toolbar, b );
+ } else {
+ // Raw object from mw.toolbar.addButtons
+ insertButton( b );
+ }
+ }
+
+ // Clear queue
+ queue.length = 0;
+
+ // This causes further calls to addButton to go to insertion directly
+ // instead of to the queue.
+ // It is important that this is after the one and only loop through
+ // the the queue
+ isReady = true;
+
+ // Make sure edit summary does not exceed byte limit
+ $( '#wpSummary' ).byteLimit( 255 );
+
+ // Restore the edit box scroll state following a preview operation,
+ // and set up a form submission handler to remember this state.
+ editBox = document.getElementById( 'wpTextbox1' );
+ scrollTop = document.getElementById( 'wpScrolltop' );
+ $editForm = $( '#editform' );
+ if ( $editForm.length && editBox && scrollTop ) {
+ if ( scrollTop.value ) {
+ editBox.scrollTop = scrollTop.value;
+ }
+ $editForm.submit( function () {
+ scrollTop.value = editBox.scrollTop;
+ } );
+ }
+
+ // Apply to dynamically created textboxes as well as normal ones
+ $( document ).on( 'focus', 'textarea, input:text', function () {
+ $currentFocused = $( this );
+ } );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.preview.js b/resources/src/mediawiki.action/mediawiki.action.edit.preview.js
new file mode 100644
index 00000000..6b212c28
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.preview.js
@@ -0,0 +1,165 @@
+/*!
+ * Live edit preview.
+ */
+( function ( mw, $ ) {
+
+ /**
+ * @ignore
+ * @param {jQuery.Event} e
+ */
+ function doLivePreview( e ) {
+ var $wikiPreview, $editform, copySelectors, $copyElements, $spinner,
+ targetUrl, postData, $previewDataHolder;
+
+ e.preventDefault();
+
+ // Deprecated: Use mw.hook instead
+ $( mw ).trigger( 'LivePreviewPrepare' );
+
+ $wikiPreview = $( '#wikiPreview' );
+ $editform = $( '#editform' );
+
+ // Show #wikiPreview if it's hidden to be able to scroll to it
+ // (if it is hidden, it's also empty, so nothing changes in the rendering)
+ $wikiPreview.show();
+
+ // Jump to where the preview will appear
+ $wikiPreview[0].scrollIntoView();
+
+ // List of selectors matching elements that we will
+ // update from from the ajax-loaded preview page.
+ copySelectors = [
+ // Main
+ '#firstHeading',
+ '#wikiPreview',
+ '#wikiDiff',
+ '#catlinks',
+ '.hiddencats',
+ '#p-lang',
+ // Editing-related
+ '.templatesUsed',
+ '.limitreport',
+ '.mw-summary-preview'
+ ];
+ $copyElements = $( copySelectors.join( ',' ) );
+
+ // Not shown during normal preview, to be removed if present
+ $( '.mw-newarticletext' ).remove();
+
+ $spinner = $.createSpinner( {
+ size: 'large',
+ type: 'block'
+ } );
+ $wikiPreview.before( $spinner );
+ $spinner.css( {
+ marginTop: $spinner.height()
+ } );
+
+ // Can't use fadeTo because it calls show(), and we might want to keep some elements hidden
+ // (e.g. empty #catlinks)
+ $copyElements.animate( { opacity: 0.4 }, 'fast' );
+
+ $previewDataHolder = $( '<div>' );
+ targetUrl = $editform.attr( 'action' );
+ targetUrl += targetUrl.indexOf( '?' ) !== -1 ? '&' : '?';
+ targetUrl += $.param( {
+ debug: mw.config.get( 'debug' ),
+ uselang: mw.config.get( 'wgUserLanguage' ),
+ useskin: mw.config.get( 'skin' )
+ } );
+
+ // Gather all the data from the form
+ postData = $editform.formToArray();
+ postData.push( {
+ name: e.target.name,
+ value: ''
+ } );
+
+ // Load new preview data.
+ // TODO: This should use the action=parse API instead of loading the entire page,
+ // although that requires figuring out how to convert that raw data into proper HTML.
+ $previewDataHolder.load( targetUrl + ' ' + copySelectors.join( ',' ), postData, function () {
+ var i, $from, $next, $parent;
+
+ // Copy the contents of the specified elements from the loaded page to the real page.
+ // Also copy their class attributes.
+ for ( i = 0; i < copySelectors.length; i++ ) {
+ $from = $previewDataHolder.find( copySelectors[i] );
+
+ if ( copySelectors[i] === '#wikiPreview' ) {
+ $next = $wikiPreview.next();
+ // If there is no next node, use parent instead.
+ // Only query parent if needed, false otherwise.
+ $parent = !$next.length && $wikiPreview.parent();
+
+ $wikiPreview
+ .detach()
+ .empty()
+ .append( $from.contents() )
+ .attr( 'class', $from.attr( 'class' ) );
+
+ mw.hook( 'wikipage.content' ).fire( $wikiPreview );
+
+ // Reattach
+ if ( $parent ) {
+ $parent.append( $wikiPreview );
+ } else {
+ $next.before( $wikiPreview );
+ }
+
+ } else {
+ $( copySelectors[i] )
+ .empty()
+ .append( $from.contents() )
+ .attr( 'class', $from.attr( 'class' ) );
+ }
+ }
+
+ // Deprecated: Use mw.hook instead
+ $( mw ).trigger( 'LivePreviewDone', [copySelectors] );
+
+ $spinner.remove();
+ $copyElements.animate( {
+ opacity: 1
+ }, 'fast' );
+ } );
+ }
+
+ $( function () {
+ // Do not enable on user .js/.css pages, as there's no sane way of "previewing"
+ // the scripts or styles without reloading the page.
+ if ( $( '#mw-userjsyoucanpreview' ).length || $( '#mw-usercssyoucanpreview' ).length ) {
+ return;
+ }
+
+ // The following elements can change in a preview but are not output
+ // by the server when they're empty until the preview response.
+ // TODO: Make the server output these always (in a hidden state), so we don't
+ // have to fish and (hopefully) put them in the right place (since skins
+ // can change where they are output).
+
+ if ( !document.getElementById( 'p-lang' ) && document.getElementById( 'p-tb' ) ) {
+ $( '#p-tb' ).after(
+ $( '<div>' ).attr( 'id', 'p-lang' )
+ );
+ }
+
+ if ( !$( '.mw-summary-preview' ).length ) {
+ $( '.editCheckboxes' ).before(
+ $( '<div>' ).addClass( 'mw-summary-preview' )
+ );
+ }
+
+ if ( !document.getElementById( 'wikiDiff' ) && document.getElementById( 'wikiPreview' ) ) {
+ $( '#wikiPreview' ).after(
+ $( '<div>' ).attr( 'id', 'wikiDiff' )
+ );
+ }
+
+ // This should be moved down to '#editform', but is kept on the body for now
+ // because the LiquidThreads extension is re-using this module with only half
+ // the EditPage (doesn't include #editform presumably, bug 55463).
+ $( document.body ).on( 'click', '#wpPreview, #wpDiff', doLivePreview );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.styles.css b/resources/src/mediawiki.action/mediawiki.action.edit.styles.css
new file mode 100644
index 00000000..7148b964
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.styles.css
@@ -0,0 +1,44 @@
+/*!
+ * Styles for elements of the editing form.
+ */
+
+/* General layout */
+#wpTextbox1 {
+ margin: 0;
+ display: block;
+}
+
+.editOptions {
+ background-color: #F0F0F0;
+ border: 1px solid silver;
+ border-top: none;
+ padding: 1em 1em 1.5em 1em;
+ margin-bottom: 2em;
+}
+
+/* Adjustments to edit form elements */
+.editCheckboxes {
+ margin-bottom: 1em;
+}
+
+.editCheckboxes input:first-child {
+ margin-left: 0;
+}
+
+.cancelLink {
+ margin-left: 0.5em;
+}
+
+#editpage-copywarn {
+ font-size: 0.9em;
+}
+
+#wpSummary {
+ display: block;
+ margin-top: 0;
+ margin-bottom: 0.5em;
+}
+
+.editButtons input:first-child {
+ margin-left: .1em;
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_bold.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_bold.png
new file mode 100644
index 00000000..e524f6cb
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_bold.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_headline.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_headline.png
new file mode 100644
index 00000000..398e5614
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_headline.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_italic.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_italic.png
new file mode 100644
index 00000000..6ec73e9e
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_link.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_link.png
new file mode 100644
index 00000000..c9c63f6c
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_link.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_nowiki.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_nowiki.png
new file mode 100644
index 00000000..743ea61b
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ar/button_nowiki.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_bold.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_bold.png
new file mode 100644
index 00000000..5c10cfe2
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_bold.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_italic.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_italic.png
new file mode 100644
index 00000000..72209d74
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_link.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_link.png
new file mode 100644
index 00000000..09c86fb1
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/be-tarask/button_link.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_bold.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_bold.png
new file mode 100644
index 00000000..367d5bc1
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_bold.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_italic.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_italic.png
new file mode 100644
index 00000000..fdd8c9f9
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/de/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_bold.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_bold.png
new file mode 100644
index 00000000..75c3f109
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_bold.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_extlink.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_extlink.png
new file mode 100644
index 00000000..458943c1
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_extlink.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_headline.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_headline.png
new file mode 100644
index 00000000..9cf751d9
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_headline.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_hr.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_hr.png
new file mode 100644
index 00000000..47e1ca40
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_hr.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_image.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_image.png
new file mode 100644
index 00000000..69192965
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_image.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_italic.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_italic.png
new file mode 100644
index 00000000..527fbd14
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_link.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_link.png
new file mode 100644
index 00000000..eb5634b9
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_link.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_media.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_media.png
new file mode 100644
index 00000000..4194ec18
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_media.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_nowiki.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_nowiki.png
new file mode 100644
index 00000000..2ba818de
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_nowiki.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_sig.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_sig.png
new file mode 100644
index 00000000..fe34b3fb
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/en/button_sig.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_bold.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_bold.png
new file mode 100644
index 00000000..c54d094c
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_bold.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_headline.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_headline.png
new file mode 100644
index 00000000..9890d155
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_headline.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_italic.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_italic.png
new file mode 100644
index 00000000..33f91ed6
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_link.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_link.png
new file mode 100644
index 00000000..76b939e6
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_link.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_nowiki.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_nowiki.png
new file mode 100644
index 00000000..743ea61b
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/fa/button_nowiki.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/LICENSE b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/LICENSE
new file mode 100644
index 00000000..47ecfe4e
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/LICENSE
@@ -0,0 +1,7 @@
+
+button_italic.png
+-------------------
+Source : http://commons.wikimedia.org/wiki/Image:Button_S_italic.png
+License: Public domain
+Author : Purodha Blissenbach, http://ksh.wikipedia.org/wiki/User:Purodha
+
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/button_italic.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/button_italic.png
new file mode 100644
index 00000000..15496c08
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ksh/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/LICENSE b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/LICENSE
new file mode 100644
index 00000000..bedcec66
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/LICENSE
@@ -0,0 +1,17 @@
+button_bold.png
+---------------
+Source : http://commons.wikimedia.org/wiki/Image:Button_bold_ukr.png
+License: Public domain
+Author : Alexey Belomoev
+
+button_italic.png
+------------------------
+Source : http://commons.wikimedia.org/wiki/Image:Button_italic_ukr.png
+License: Public domain
+Author : Alexey Belomoev
+
+button_link.png
+-----------------
+Source : http://commons.wikimedia.org/wiki/Image:Button_internal_link_ukr.png
+License: GPL
+Author : Saproj, Erik Möller
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_bold.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_bold.png
new file mode 100644
index 00000000..eae30d98
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_bold.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_italic.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_italic.png
new file mode 100644
index 00000000..b958d220
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_italic.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_link.png b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_link.png
new file mode 100644
index 00000000..12ad3731
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/images/ru/button_link.png
Binary files differ
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/mediawiki.action.edit.toolbar.less b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/mediawiki.action.edit.toolbar.less
new file mode 100644
index 00000000..d65b2842
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.edit.toolbar/mediawiki.action.edit.toolbar.less
@@ -0,0 +1,42 @@
+@import "mediawiki.mixins";
+
+#mw-editbutton-bold {
+ .background-image("images/@{button-bold}");
+}
+
+#mw-editbutton-italic {
+ .background-image("images/@{button-italic}");
+}
+
+#mw-editbutton-link {
+ .background-image("images/@{button-link}");
+}
+
+#mw-editbutton-extlink {
+ .background-image("images/@{button-extlink}");
+}
+
+#mw-editbutton-headline {
+ .background-image("images/@{button-headline}");
+}
+
+#mw-editbutton-image {
+ .background-image("images/@{button-image}");
+}
+
+#mw-editbutton-media {
+ .background-image("images/@{button-media}");
+}
+
+#mw-editbutton-nowiki {
+ .background-image("images/@{button-nowiki}");
+}
+
+// Who decided to make only this single one different than the name of the data item?
+#mw-editbutton-signature {
+ .background-image("images/@{button-sig}");
+}
+
+#mw-editbutton-hr {
+ .background-image("images/@{button-hr}");
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.history.css b/resources/src/mediawiki.action/mediawiki.action.history.css
new file mode 100644
index 00000000..603a9657
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.history.css
@@ -0,0 +1,4 @@
+#pagehistory li.before input[name="oldid"],
+#pagehistory li.after input[name="diff"] {
+ visibility: hidden;
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.history.diff.css b/resources/src/mediawiki.action/mediawiki.action.history.diff.css
new file mode 100644
index 00000000..afe92468
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.history.diff.css
@@ -0,0 +1,96 @@
+/*
+** Diff rendering
+*/
+table.diff {
+ background-color: white;
+ border: none;
+ border-spacing: 4px;
+ margin: 0;
+ width: 100%;
+ /* Ensure that colums are of equal width */
+ table-layout: fixed;
+}
+
+table.diff td {
+ padding: 0.33em 0.5em;
+}
+
+table.diff td.diff-marker {
+ /* Compensate padding for increased font-size */
+ padding: 0.25em;
+}
+
+table.diff col.diff-marker {
+ width: 2%;
+}
+
+table.diff col.diff-content {
+ width: 48%;
+}
+
+table.diff td div {
+ /* Force-wrap very long lines such as URLs or page-widening char strings */
+ word-wrap: break-word;
+}
+
+td.diff-otitle,
+td.diff-ntitle {
+ text-align: center;
+}
+
+td.diff-lineno {
+ font-weight: bold;
+}
+
+td.diff-marker {
+ text-align: right;
+ font-weight: bold;
+ font-size: 1.25em;
+ line-height: 1.2;
+}
+
+td.diff-addedline,
+td.diff-deletedline,
+td.diff-context {
+ font-size: 88%;
+ line-height: 1.6;
+ vertical-align: top;
+ white-space: -moz-pre-wrap;
+ white-space: pre-wrap;
+ border-style: solid;
+ border-width: 1px 1px 1px 4px;
+ border-radius: 0.33em;
+}
+
+td.diff-addedline {
+ border-color: #a3d3ff;
+}
+
+td.diff-deletedline {
+ border-color: #ffe49c;
+}
+
+td.diff-context {
+ background: #f9f9f9;
+ border-color: #e6e6e6;
+ color: #333333;
+}
+
+.diffchange {
+ font-weight: bold;
+ text-decoration: none;
+}
+
+td.diff-addedline .diffchange,
+td.diff-deletedline .diffchange {
+ border-radius: 0.33em;
+ padding: 0.25em 0;
+}
+
+td.diff-addedline .diffchange {
+ background: #d8ecff;
+}
+
+td.diff-deletedline .diffchange {
+ background: #feeec8;
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.history.js b/resources/src/mediawiki.action/mediawiki.action.history.js
new file mode 100644
index 00000000..ac48c596
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.history.js
@@ -0,0 +1,111 @@
+/*!
+ * JavaScript for History action
+ */
+jQuery( function ( $ ) {
+ var $historyCompareForm = $( '#mw-history-compare' ),
+ $historySubmitter,
+ $lis = $( '#pagehistory > li' );
+
+ /**
+ * @ignore
+ * @context {Element} input
+ * @param e {jQuery.Event}
+ */
+ function updateDiffRadios() {
+ var nextState = 'before',
+ $li,
+ $inputs,
+ $oldidRadio,
+ $diffRadio;
+
+ if ( !$lis.length ) {
+ return true;
+ }
+
+ $lis
+ .each( function () {
+ $li = $( this );
+ $inputs = $li.find( 'input[type="radio"]' );
+ $oldidRadio = $inputs.filter( '[name="oldid"]' ).eq( 0 );
+ $diffRadio = $inputs.filter( '[name="diff"]' ).eq( 0 );
+
+ $li.removeClass( 'selected between before after' );
+
+ if ( !$oldidRadio.length || !$diffRadio.length ) {
+ return true;
+ }
+
+ if ( $oldidRadio.prop( 'checked' ) ) {
+ $li.addClass( 'selected after' );
+ nextState = 'after';
+ } else if ( $diffRadio.prop( 'checked' ) ) {
+ $li.addClass( 'selected ' + nextState );
+ nextState = 'between';
+ } else {
+ // This list item has neither checked
+ // apply the appropriate class following the previous item.
+ $li.addClass( nextState );
+ }
+ } );
+
+ return true;
+ }
+
+ $lis.find( 'input[name="diff"], input[name="oldid"]' ).click( updateDiffRadios );
+
+ // Set initial state
+ updateDiffRadios();
+
+ // Prettify url output for HistoryAction submissions,
+ // to cover up action=historysubmit construction.
+
+ // Ideally we'd use e.target instead of $historySubmitter, but e.target points
+ // to the form element for submit actions, so.
+ $historyCompareForm.find( '.historysubmit' ).click( function () {
+ $historySubmitter = $( this );
+ } );
+
+ // On submit we clone the form element, remove unneeded fields in the clone
+ // that pollute the query parameter with stuff from the other "use case",
+ // and then submit the clone.
+ // Without the cloning we'd be changing the real form, which is slower, could make
+ // the page look broken for a second in slow browsers and might show the form broken
+ // again when coming back from a "next" page.
+ $historyCompareForm.submit( function ( e ) {
+ var $copyForm, $copyRadios, $copyAction;
+
+ if ( $historySubmitter ) {
+ $copyForm = $historyCompareForm.clone();
+ $copyRadios = $copyForm.find( '#pagehistory > li' ).find( 'input[name="diff"], input[name="oldid"]' );
+ $copyAction = $copyForm.find( '> [name="action"]' );
+
+ // Remove action=historysubmit and ids[..]=..
+ if ( $historySubmitter.hasClass( 'mw-history-compareselectedversions-button' ) ) {
+ $copyAction.remove();
+ $copyForm.find( 'input[name^="ids["]:checked' ).prop( 'checked', false );
+
+ // Remove diff=&oldid=, change action=historysubmit to revisiondelete, remove revisiondelete
+ } else if ( $historySubmitter.hasClass( 'mw-history-revisiondelete-button' ) ) {
+ $copyRadios.remove();
+ $copyAction.val( $historySubmitter.attr( 'name' ) );
+ $copyForm.find( ':submit' ).remove();
+ }
+
+ // IE7 doesn't do submission from an off-DOM clone, so insert hidden into document first
+ // Also remove potentially conflicting id attributes that we don't need anyway
+ $copyForm
+ .css( 'display', 'none' )
+ .find( '[id]' )
+ .removeAttr( 'id' )
+ .end()
+ .insertAfter( $historyCompareForm )
+ .submit();
+
+ e.preventDefault();
+ return false; // Because the submit is special, return false as well.
+ }
+
+ // Continue natural browser handling other wise
+ return true;
+ } );
+} );
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.dblClickEdit.js b/resources/src/mediawiki.action/mediawiki.action.view.dblClickEdit.js
new file mode 100644
index 00000000..2ded40cf
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.view.dblClickEdit.js
@@ -0,0 +1,12 @@
+/*!
+ * Enables double-click-to-edit functionality.
+ */
+( function ( mw, $ ) {
+ $( function () {
+ mw.util.$content.dblclick( function ( e ) {
+ e.preventDefault();
+ // Trigger native HTMLElement click instead of opening URL (bug 43052)
+ $( '#ca-edit a' ).get( 0 ).click();
+ } );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.metadata.css b/resources/src/mediawiki.action/mediawiki.action.view.metadata.css
new file mode 100644
index 00000000..2c8d2e65
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.view.metadata.css
@@ -0,0 +1,6 @@
+/*!
+ * Hide collapsable rows in a collapsed table.
+ */
+table.collapsed tr.collapsable {
+ display: none;
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.metadata.js b/resources/src/mediawiki.action/mediawiki.action.view.metadata.js
new file mode 100644
index 00000000..25b5acc5
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.view.metadata.js
@@ -0,0 +1,45 @@
+/*!
+ * Exif metadata display for MediaWiki file uploads
+ *
+ * Add an expand/collapse link and collapse by default if set to
+ * (with JS disabled, user will see all items)
+ *
+ * See also ImagePage.php#makeMetadataTable (creates the HTML)
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var $row, $col, $link,
+ showText = mw.msg( 'metadata-expand' ),
+ hideText = mw.msg( 'metadata-collapse' ),
+ $table = $( '#mw_metadata' ),
+ $tbody = $table.find( 'tbody' );
+
+ if ( !$tbody.length || !$tbody.find( '.collapsable' ).length ) {
+ return;
+ }
+
+ $row = $( '<tr class="mw-metadata-show-hide-extended"></tr>' );
+ $col = $( '<td colspan="2"></td>' );
+
+ $link = $( '<a>', {
+ text: showText,
+ href: '#'
+ } ).click( function () {
+ if ( $table.hasClass( 'collapsed' ) ) {
+ $( this ).text( hideText );
+ } else {
+ $( this ).text( showText );
+ }
+ $table.toggleClass( 'expanded collapsed' );
+ return false;
+ } );
+
+ $col.append( $link );
+ $row.append( $col );
+ $tbody.append( $row );
+
+ // And collapse!
+ $table.addClass( 'collapsed' );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.postEdit.css b/resources/src/mediawiki.action/mediawiki.action.view.postEdit.css
new file mode 100644
index 00000000..db89990d
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.view.postEdit.css
@@ -0,0 +1,76 @@
+.postedit-container {
+ margin: 0 auto;
+ position: fixed;
+ top: 0;
+ height: 0;
+ left: 50%;
+ z-index: 1000;
+ font-size: 13px;
+}
+
+.postedit-container:hover {
+ cursor: pointer;
+}
+
+.postedit {
+ position: relative;
+ top: 0.6em;
+ left: -50%;
+ padding: .6em 3.6em .6em 1.1em;
+ line-height: 1.5625em;
+ color: #626465;
+ background-color: #f4f4f4;
+ border: 1px solid #dcd9d9;
+ text-shadow: 0 0.0625em 0 rgba(255, 255, 255, 0.5);
+ border-radius: 5px;
+ box-shadow: 0 2px 5px 0 #ccc;
+ -webkit-transition: all 0.25s ease-in-out;
+ -moz-transition: all 0.25s ease-in-out;
+ -ms-transition: all 0.25s ease-in-out;
+ -o-transition: all 0.25s ease-in-out;
+ transition: all 0.25s ease-in-out;
+}
+
+.skin-monobook .postedit {
+ top: 6em !important;
+}
+
+.postedit-faded {
+ opacity: 0;
+}
+
+.postedit-icon {
+ padding-left: 41px; /* 25 + 8 + 8 */
+ /* like min-height, but old IE compatible and keeps text vertically aligned, too */
+ line-height: 25px;
+ background-repeat: no-repeat;
+ background-position: 8px 50%;
+}
+
+.postedit-icon-checkmark {
+ /* @embed */
+ background-image: url(images/green-checkmark.png);
+ background-position: left;
+}
+
+.postedit-close {
+ position: absolute;
+ padding: 0 .8em;
+ right: 0;
+ top: 0;
+ font-size: 1.25em;
+ font-weight: bold;
+ line-height: 2.3em;
+ color: black;
+ text-shadow: 0 0.0625em 0 white;
+ text-decoration: none;
+ opacity: 0.2;
+ filter: alpha(opacity=20);
+}
+
+.postedit-close:hover {
+ color: black;
+ text-decoration: none;
+ opacity: 0.4;
+ filter: alpha(opacity=40);
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.postEdit.js b/resources/src/mediawiki.action/mediawiki.action.view.postEdit.js
new file mode 100644
index 00000000..4d2c47a5
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.view.postEdit.js
@@ -0,0 +1,86 @@
+( function ( mw, $ ) {
+ 'use strict';
+
+ /**
+ * @event postEdit
+ * @member mw.hook
+ * @param {Object} [data] Optional data
+ * @param {string|jQuery|Array} [data.message] Message that listeners
+ * should use when displaying notifications. String for plain text,
+ * use array or jQuery object to pass actual nodes.
+ * @param {string|mw.user} [data.user=mw.user] User that made the edit.
+ */
+
+ /**
+ * After the listener for #postEdit removes the notification.
+ *
+ * @event postEdit_afterRemoval
+ * @member mw.hook
+ */
+
+ var config = mw.config.get( [ 'wgAction', 'wgCurRevisionId' ] ),
+ // This should match EditPage::POST_EDIT_COOKIE_KEY_PREFIX:
+ cookieKey = 'PostEditRevision' + config.wgCurRevisionId,
+ cookieVal = mw.cookie.get( cookieKey ),
+ $div, id;
+
+ function showConfirmation( data ) {
+ data = data || {};
+ if ( data.message === undefined ) {
+ data.message = $.parseHTML( mw.message( 'postedit-confirmation-saved', data.user || mw.user ).escaped() );
+ }
+
+ $div = $(
+ '<div class="postedit-container">' +
+ '<div class="postedit">' +
+ '<div class="postedit-icon postedit-icon-checkmark postedit-content"></div>' +
+ '<a href="#" class="postedit-close">&times;</a>' +
+ '</div>' +
+ '</div>'
+ );
+
+ if ( typeof data.message === 'string' ) {
+ $div.find( '.postedit-content' ).text( data.message );
+ } else if ( typeof data.message === 'object' ) {
+ $div.find( '.postedit-content' ).append( data.message );
+ }
+
+ $div
+ .click( fadeOutConfirmation )
+ .prependTo( 'body' );
+
+ id = setTimeout( fadeOutConfirmation, 3000 );
+ }
+
+ function fadeOutConfirmation() {
+ clearTimeout( id );
+ $div.find( '.postedit' ).addClass( 'postedit postedit-faded' );
+ setTimeout( removeConfirmation, 500 );
+
+ return false;
+ }
+
+ function removeConfirmation() {
+ $div.remove();
+ mw.hook( 'postEdit.afterRemoval' ).fire();
+ }
+
+ mw.hook( 'postEdit' ).add( showConfirmation );
+
+ if ( config.wgAction === 'view' && cookieVal ) {
+ mw.config.set( 'wgPostEdit', true );
+
+ mw.hook( 'postEdit' ).fire( {
+ // The following messages can be used here:
+ // postedit-confirmation-saved
+ // postedit-confirmation-created
+ // postedit-confirmation-restored
+ 'message': mw.msg(
+ 'postedit-confirmation-' + cookieVal,
+ mw.user
+ )
+ } );
+ mw.cookie.set( cookieKey, null );
+ }
+
+} ( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.redirect.js b/resources/src/mediawiki.action/mediawiki.action.view.redirect.js
new file mode 100644
index 00000000..52e0d4e3
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.view.redirect.js
@@ -0,0 +1,65 @@
+/*!
+ * JavaScript to update page URL when a redirect is viewed, ensuring that the
+ * page is scrolled to the id when it's a redirect with fragment.
+ *
+ * This is loaded in the top queue, so avoid unnecessary dependencies
+ * like mediawiki.Title or mediawiki.Uri.
+ */
+( function ( mw, $ ) {
+ var profile = $.client.profile(),
+ canonical = mw.config.get( 'wgInternalRedirectTargetUrl' ),
+ fragment = null,
+ shouldChangeFragment, index;
+
+ // Clear internal mw.config entries, so that no one tries to depend on them
+ mw.config.set( 'wgInternalRedirectTargetUrl', null );
+
+ index = canonical.indexOf( '#' );
+ if ( index !== -1 ) {
+ fragment = canonical.slice( index );
+ }
+
+ // Never override the fragment if the user intended to look at a different section
+ shouldChangeFragment = fragment && !location.hash;
+
+ // Replace the whole URL if possible, otherwise just change the fragment
+ if ( canonical && history.replaceState ) {
+ if ( !shouldChangeFragment ) {
+ // If the current page view has a fragment already, don't override it
+ canonical = canonical.replace( /#.*$/, '' );
+ canonical += location.hash;
+ }
+
+ // This will also cause the browser to scroll to given fragment
+ history.replaceState( /*data=*/ history.state, /*title=*/ document.title, /*url=*/ canonical );
+
+ // …except for IE 10 and 11. Prod it with a location.hash change.
+ if ( shouldChangeFragment && profile.name === 'msie' && profile.versionNumber >= 10 ) {
+ location.hash = fragment;
+ }
+
+ } else if ( shouldChangeFragment ) {
+ if ( profile.layout === 'webkit' && profile.layoutVersion < 420 ) {
+ // Released Safari w/ WebKit 418.9.1 messes up horribly
+ // Nightlies of 420+ are ok
+ return;
+ }
+
+ location.hash = fragment;
+ }
+
+ if ( shouldChangeFragment && profile.layout === 'gecko' ) {
+ // Mozilla needs to wait until after load, otherwise the window doesn't
+ // scroll. See <https://bugzilla.mozilla.org/show_bug.cgi?id=516293>.
+ // There's no obvious way to detect this programmatically, so we use
+ // version-testing. If Firefox fixes the bug, they'll jump twice, but
+ // better twice than not at all, so make the fix hit future versions as
+ // well.
+ $( function () {
+ if ( location.hash === fragment ) {
+ location.hash = fragment;
+ }
+ } );
+ }
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.redirectPage.css b/resources/src/mediawiki.action/mediawiki.action.view.redirectPage.css
new file mode 100644
index 00000000..fdbb655f
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.view.redirectPage.css
@@ -0,0 +1,53 @@
+/*!
+ * Display neat icons on redirect pages.
+ */
+
+/* Hide, but keep accessible for screen-readers. */
+.redirectMsg p {
+ overflow: hidden;
+ height: 0;
+ zoom: 1;
+}
+
+.redirectText {
+ list-style: none none;
+ display: inline;
+ /* shared.css has some very weird directionality-specific rules for 'ul' we need to override,
+ search for "Correct directionality when page dir is different from site/user dir" */
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+/* @noflip */
+.mw-content-ltr .redirectText li {
+ display: inline;
+ margin: 0;
+ padding: 0;
+ padding-left: 42px;
+ /* @embed */
+ background: url(images/nextredirect-ltr.png) bottom left no-repeat;
+}
+
+/* @noflip */
+.mw-content-ltr .redirectText li:first-child {
+ padding-left: 47px;
+ /* @embed */
+ background: url(images/redirect-ltr.png) bottom left no-repeat;
+}
+
+/* @noflip */
+.mw-content-rtl .redirectText li {
+ display: inline;
+ margin: 0;
+ padding: 0;
+ padding-right: 42px;
+ /* @embed */
+ background: url(images/nextredirect-rtl.png) bottom right no-repeat;
+}
+
+/* @noflip */
+.mw-content-rtl .redirectText li:first-child {
+ padding-right: 47px;
+ /* @embed */
+ background: url(images/redirect-rtl.png) bottom right no-repeat;
+}
diff --git a/resources/src/mediawiki.action/mediawiki.action.view.rightClickEdit.js b/resources/src/mediawiki.action/mediawiki.action.view.rightClickEdit.js
new file mode 100644
index 00000000..ada101eb
--- /dev/null
+++ b/resources/src/mediawiki.action/mediawiki.action.view.rightClickEdit.js
@@ -0,0 +1,26 @@
+/*!
+ * JavaScript to enable right click edit functionality.
+ * When the user right-clicks in a heading, it will open the
+ * edit screen.
+ */
+jQuery( function ( $ ) {
+ // Select all h1-h6 elements that contain editsection links
+ // Don't use the ":has:(.mw-editsection a)" selector because it performs very bad.
+ // http://jsperf.com/jq-1-7-2-vs-jq-1-8-1-performance-of-mw-has/2
+ $( document ).on( 'contextmenu', 'h1, h2, h3, h4, h5, h6', function ( e ) {
+ var $edit = $( this ).find( '.mw-editsection a' );
+ if ( !$edit.length ) {
+ return;
+ }
+
+ // Headings can contain rich text.
+ // Make sure to not block contextmenu events on (other) anchor tags
+ // inside the heading (e.g. to do things like copy URL, open in new tab, ..).
+ // e.target can be the heading, but it can also be anything inside the heading.
+ if ( e.target.nodeName.toLowerCase() !== 'a' ) {
+ // Trigger native HTMLElement click instead of opening URL (bug 43052)
+ e.preventDefault();
+ $edit.get( 0 ).click();
+ }
+ } );
+} );
diff --git a/resources/src/mediawiki.api/mediawiki.api.category.js b/resources/src/mediawiki.api/mediawiki.api.category.js
new file mode 100644
index 00000000..7dd9730f
--- /dev/null
+++ b/resources/src/mediawiki.api/mediawiki.api.category.js
@@ -0,0 +1,146 @@
+/**
+ * @class mw.Api.plugin.category
+ */
+( function ( mw, $ ) {
+
+ var msg = 'Use of mediawiki.api callback params is deprecated. Use the Promise instead.';
+ $.extend( mw.Api.prototype, {
+ /**
+ * Determine if a category exists.
+ *
+ * @param {mw.Title|string} title
+ * @param {Function} [ok] Success callback (deprecated)
+ * @param {Function} [err] Error callback (deprecated)
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {boolean} return.done.isCategory Whether the category exists.
+ */
+ isCategory: function ( title, ok, err ) {
+ var apiPromise = this.get( {
+ prop: 'categoryinfo',
+ titles: String( title )
+ } );
+
+ if ( ok || err ) {
+ mw.track( 'mw.deprecate', 'api.cbParam' );
+ mw.log.warn( msg );
+ }
+
+ return apiPromise
+ .then( function ( data ) {
+ var exists = false;
+ if ( data.query && data.query.pages ) {
+ $.each( data.query.pages, function ( id, page ) {
+ if ( page.categoryinfo ) {
+ exists = true;
+ }
+ } );
+ }
+ return exists;
+ } )
+ .done( ok )
+ .fail( err )
+ .promise( { abort: apiPromise.abort } );
+ },
+
+ /**
+ * Get a list of categories that match a certain prefix.
+ *
+ * E.g. given "Foo", return "Food", "Foolish people", "Foosball tables"...
+ *
+ * @param {string} prefix Prefix to match.
+ * @param {Function} [ok] Success callback (deprecated)
+ * @param {Function} [err] Error callback (deprecated)
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {string[]} return.done.categories Matched categories
+ */
+ getCategoriesByPrefix: function ( prefix, ok, err ) {
+ // Fetch with allpages to only get categories that have a corresponding description page.
+ var apiPromise = this.get( {
+ list: 'allpages',
+ apprefix: prefix,
+ apnamespace: mw.config.get( 'wgNamespaceIds' ).category
+ } );
+
+ if ( ok || err ) {
+ mw.track( 'mw.deprecate', 'api.cbParam' );
+ mw.log.warn( msg );
+ }
+
+ return apiPromise
+ .then( function ( data ) {
+ var texts = [];
+ if ( data.query && data.query.allpages ) {
+ $.each( data.query.allpages, function ( i, category ) {
+ texts.push( new mw.Title( category.title ).getMainText() );
+ } );
+ }
+ return texts;
+ } )
+ .done( ok )
+ .fail( err )
+ .promise( { abort: apiPromise.abort } );
+ },
+
+ /**
+ * Get the categories that a particular page on the wiki belongs to.
+ *
+ * @param {mw.Title|string} title
+ * @param {Function} [ok] Success callback (deprecated)
+ * @param {Function} [err] Error callback (deprecated)
+ * @param {boolean} [async=true] Asynchronousness (deprecated)
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {boolean|mw.Title[]} return.done.categories List of category titles or false
+ * if title was not found.
+ */
+ getCategories: function ( title, ok, err, async ) {
+ var apiPromise = this.get( {
+ prop: 'categories',
+ titles: String( title )
+ }, {
+ async: async === undefined ? true : async
+ } );
+
+ if ( ok || err ) {
+ mw.track( 'mw.deprecate', 'api.cbParam' );
+ mw.log.warn( msg );
+ }
+ if ( async !== undefined ) {
+ mw.track( 'mw.deprecate', 'api.async' );
+ mw.log.warn(
+ 'Use of mediawiki.api async=false param is deprecated. ' +
+ 'The sychronous mode will be removed in the future.'
+ );
+ }
+
+ return apiPromise
+ .then( function ( data ) {
+ var titles = false;
+ if ( data.query && data.query.pages ) {
+ $.each( data.query.pages, function ( id, page ) {
+ if ( page.categories ) {
+ if ( titles === false ) {
+ titles = [];
+ }
+ $.each( page.categories, function ( i, cat ) {
+ titles.push( new mw.Title( cat.title ) );
+ } );
+ }
+ } );
+ }
+ return titles;
+ } )
+ .done( ok )
+ .fail( err )
+ .promise( { abort: apiPromise.abort } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.category
+ */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api/mediawiki.api.edit.js b/resources/src/mediawiki.api/mediawiki.api.edit.js
new file mode 100644
index 00000000..e88ae5e2
--- /dev/null
+++ b/resources/src/mediawiki.api/mediawiki.api.edit.js
@@ -0,0 +1,87 @@
+/**
+ * @class mw.Api.plugin.edit
+ */
+( function ( mw, $ ) {
+
+ var msg = 'Use of mediawiki.api callback params is deprecated. Use the Promise instead.';
+ $.extend( mw.Api.prototype, {
+
+ /**
+ * Post to API with edit token. If we have no token, get one and try to post.
+ * If we have a cached token try using that, and if it fails, blank out the
+ * cached token and start over.
+ *
+ * @param {Object} params API parameters
+ * @param {Function} [ok] Success callback (deprecated)
+ * @param {Function} [err] Error callback (deprecated)
+ * @return {jQuery.Promise} See #post
+ */
+ postWithEditToken: function ( params, ok, err ) {
+ if ( ok || err ) {
+ mw.track( 'mw.deprecate', 'api.cbParam' );
+ mw.log.warn( msg );
+ }
+
+ return this.postWithToken( 'edit', params ).done( ok ).fail( err );
+ },
+
+ /**
+ * API helper to grab an edit token.
+ *
+ * @param {Function} [ok] Success callback (deprecated)
+ * @param {Function} [err] Error callback (deprecated)
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {string} return.done.token Received token.
+ */
+ getEditToken: function ( ok, err ) {
+ if ( ok || err ) {
+ mw.track( 'mw.deprecate', 'api.cbParam' );
+ mw.log.warn( msg );
+ }
+
+ return this.getToken( 'edit' ).done( ok ).fail( err );
+ },
+
+ /**
+ * Post a new section to the page.
+ * @see #postWithEditToken
+ * @param {mw.Title|String} title Target page
+ * @param {string} header
+ * @param {string} message wikitext message
+ * @param {Object} [additionalParams] Additional API parameters, e.g. `{ redirect: true }`
+ * @param {Function} [ok] Success handler (deprecated)
+ * @param {Function} [err] Error handler (deprecated)
+ * @return {jQuery.Promise}
+ */
+ newSection: function ( title, header, message, additionalParams, ok, err ) {
+ // Until we remove 'ok' and 'err' parameters, we have to support code that passes them,
+ // but not additionalParams...
+ if ( $.isFunction( additionalParams ) ) {
+ err = ok;
+ ok = additionalParams;
+ additionalParams = undefined;
+ }
+
+ if ( ok || err ) {
+ mw.track( 'mw.deprecate', 'api.cbParam' );
+ mw.log.warn( msg );
+ }
+
+ return this.postWithEditToken( $.extend( {
+ action: 'edit',
+ section: 'new',
+ format: 'json',
+ title: String( title ),
+ summary: header,
+ text: message
+ }, additionalParams ) ).done( ok ).fail( err );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.edit
+ */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api/mediawiki.api.js b/resources/src/mediawiki.api/mediawiki.api.js
new file mode 100644
index 00000000..51b3238c
--- /dev/null
+++ b/resources/src/mediawiki.api/mediawiki.api.js
@@ -0,0 +1,397 @@
+( function ( mw, $ ) {
+
+ // We allow people to omit these default parameters from API requests
+ // there is very customizable error handling here, on a per-call basis
+ // wondering, would it be simpler to make it easy to clone the api object,
+ // change error handling, and use that instead?
+ var defaultOptions = {
+
+ // Query parameters for API requests
+ parameters: {
+ action: 'query',
+ format: 'json'
+ },
+
+ // Ajax options for jQuery.ajax()
+ ajax: {
+ url: mw.util.wikiScript( 'api' ),
+
+ timeout: 30 * 1000, // 30 seconds
+
+ dataType: 'json'
+ }
+ },
+ // Keyed by ajax url and symbolic name for the individual request
+ promises = {};
+
+ // Pre-populate with fake ajax promises to save http requests for tokens
+ // we already have on the page via the user.tokens module (bug 34733).
+ promises[ defaultOptions.ajax.url ] = {};
+ $.each( mw.user.tokens.get(), function ( key, value ) {
+ // This requires #getToken to use the same key as user.tokens.
+ // Format: token-type + "Token" (eg. editToken, patrolToken, watchToken).
+ promises[ defaultOptions.ajax.url ][ key ] = $.Deferred()
+ .resolve( value )
+ .promise( { abort: function () {} } );
+ } );
+
+ /**
+ * Constructor to create an object to interact with the API of a particular MediaWiki server.
+ * mw.Api objects represent the API of a particular MediaWiki server.
+ *
+ * TODO: Share API objects with exact same config.
+ *
+ * var api = new mw.Api();
+ * api.get( {
+ * action: 'query',
+ * meta: 'userinfo'
+ * } ).done ( function ( data ) {
+ * console.log( data );
+ * } );
+ *
+ * @class
+ *
+ * @constructor
+ * @param {Object} options See defaultOptions documentation above. Ajax options can also be
+ * overridden for each individual request to {@link jQuery#ajax} later on.
+ */
+ mw.Api = function ( options ) {
+
+ if ( options === undefined ) {
+ options = {};
+ }
+
+ // Force a string if we got a mw.Uri object
+ if ( options.ajax && options.ajax.url !== undefined ) {
+ options.ajax.url = String( options.ajax.url );
+ }
+
+ options.parameters = $.extend( {}, defaultOptions.parameters, options.parameters );
+ options.ajax = $.extend( {}, defaultOptions.ajax, options.ajax );
+
+ this.defaults = options;
+ };
+
+ mw.Api.prototype = {
+
+ /**
+ * Normalize the ajax options for compatibility and/or convenience methods.
+ *
+ * @param {Object} [arg] An object contaning one or more of options.ajax.
+ * @return {Object} Normalized ajax options.
+ */
+ normalizeAjaxOptions: function ( arg ) {
+ // Arg argument is usually empty
+ // (before MW 1.20 it was used to pass ok callbacks)
+ var opts = arg || {};
+ // Options can also be a success callback handler
+ if ( typeof arg === 'function' ) {
+ opts = { ok: arg };
+ }
+ return opts;
+ },
+
+ /**
+ * Perform API get request
+ *
+ * @param {Object} parameters
+ * @param {Object|Function} [ajaxOptions]
+ * @return {jQuery.Promise}
+ */
+ get: function ( parameters, ajaxOptions ) {
+ ajaxOptions = this.normalizeAjaxOptions( ajaxOptions );
+ ajaxOptions.type = 'GET';
+ return this.ajax( parameters, ajaxOptions );
+ },
+
+ /**
+ * Perform API post request
+ *
+ * TODO: Post actions for non-local hostnames will need proxy.
+ *
+ * @param {Object} parameters
+ * @param {Object|Function} [ajaxOptions]
+ * @return {jQuery.Promise}
+ */
+ post: function ( parameters, ajaxOptions ) {
+ ajaxOptions = this.normalizeAjaxOptions( ajaxOptions );
+ ajaxOptions.type = 'POST';
+ return this.ajax( parameters, ajaxOptions );
+ },
+
+ /**
+ * Perform the API call.
+ *
+ * @param {Object} parameters
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise} Done: API response data and the jqXHR object.
+ * Fail: Error code
+ */
+ ajax: function ( parameters, ajaxOptions ) {
+ var token,
+ apiDeferred = $.Deferred(),
+ msg = 'Use of mediawiki.api callback params is deprecated. Use the Promise instead.',
+ xhr, key, formData;
+
+ parameters = $.extend( {}, this.defaults.parameters, parameters );
+ ajaxOptions = $.extend( {}, this.defaults.ajax, ajaxOptions );
+
+ // Ensure that token parameter is last (per [[mw:API:Edit#Token]]).
+ if ( parameters.token ) {
+ token = parameters.token;
+ delete parameters.token;
+ }
+
+ // If multipart/form-data has been requested and emulation is possible, emulate it
+ if (
+ ajaxOptions.type === 'POST' &&
+ window.FormData &&
+ ajaxOptions.contentType === 'multipart/form-data'
+ ) {
+
+ formData = new FormData();
+
+ for ( key in parameters ) {
+ formData.append( key, parameters[key] );
+ }
+ // If we extracted a token parameter, add it back in.
+ if ( token ) {
+ formData.append( 'token', token );
+ }
+
+ ajaxOptions.data = formData;
+
+ // Prevent jQuery from mangling our FormData object
+ ajaxOptions.processData = false;
+ // Prevent jQuery from overriding the Content-Type header
+ ajaxOptions.contentType = false;
+ } else {
+ // Some deployed MediaWiki >= 1.17 forbid periods in URLs, due to an IE XSS bug
+ // So let's escape them here. See bug #28235
+ // This works because jQuery accepts data as a query string or as an Object
+ ajaxOptions.data = $.param( parameters ).replace( /\./g, '%2E' );
+
+ // If we extracted a token parameter, add it back in.
+ if ( token ) {
+ ajaxOptions.data += '&token=' + encodeURIComponent( token );
+ }
+
+ if ( ajaxOptions.contentType === 'multipart/form-data' ) {
+ // We were asked to emulate but can't, so drop the Content-Type header, otherwise
+ // it'll be wrong and the server will fail to decode the POST body
+ delete ajaxOptions.contentType;
+ }
+ }
+
+ // Backwards compatibility: Before MediaWiki 1.20,
+ // callbacks were done with the 'ok' and 'err' property in ajaxOptions.
+ if ( ajaxOptions.ok ) {
+ mw.track( 'mw.deprecate', 'api.cbParam' );
+ mw.log.warn( msg );
+ apiDeferred.done( ajaxOptions.ok );
+ delete ajaxOptions.ok;
+ }
+ if ( ajaxOptions.err ) {
+ mw.track( 'mw.deprecate', 'api.cbParam' );
+ mw.log.warn( msg );
+ apiDeferred.fail( ajaxOptions.err );
+ delete ajaxOptions.err;
+ }
+
+ // Make the AJAX request
+ xhr = $.ajax( ajaxOptions )
+ // If AJAX fails, reject API call with error code 'http'
+ // and details in second argument.
+ .fail( function ( xhr, textStatus, exception ) {
+ apiDeferred.reject( 'http', {
+ xhr: xhr,
+ textStatus: textStatus,
+ exception: exception
+ } );
+ } )
+ // AJAX success just means "200 OK" response, also check API error codes
+ .done( function ( result, textStatus, jqXHR ) {
+ if ( result === undefined || result === null || result === '' ) {
+ apiDeferred.reject( 'ok-but-empty',
+ 'OK response but empty result (check HTTP headers?)'
+ );
+ } else if ( result.error ) {
+ var code = result.error.code === undefined ? 'unknown' : result.error.code;
+ apiDeferred.reject( code, result );
+ } else {
+ apiDeferred.resolve( result, jqXHR );
+ }
+ } );
+
+ // Return the Promise
+ return apiDeferred.promise( { abort: xhr.abort } ).fail( function ( code, details ) {
+ if ( !( code === 'http' && details && details.textStatus === 'abort' ) ) {
+ mw.log( 'mw.Api error: ', code, details );
+ }
+ } );
+ },
+
+ /**
+ * Post to API with specified type of token. If we have no token, get one and try to post.
+ * If we have a cached token try using that, and if it fails, blank out the
+ * cached token and start over. For example to change an user option you could do:
+ *
+ * new mw.Api().postWithToken( 'options', {
+ * action: 'options',
+ * optionname: 'gender',
+ * optionvalue: 'female'
+ * } );
+ *
+ * @param {string} tokenType The name of the token, like options or edit.
+ * @param {Object} params API parameters
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise} See #post
+ * @since 1.22
+ */
+ postWithToken: function ( tokenType, params, ajaxOptions ) {
+ var api = this;
+
+ // Do not allow deprecated ok-callback
+ // FIXME: Remove this check when the deprecated ok-callback is removed in #post
+ if ( $.isFunction( ajaxOptions ) ) {
+ ajaxOptions = undefined;
+ }
+
+ return api.getToken( tokenType ).then( function ( token ) {
+ params.token = token;
+ return api.post( params, ajaxOptions ).then(
+ // If no error, return to caller as-is
+ null,
+ // Error handler
+ function ( code ) {
+ if ( code === 'badtoken' ) {
+ // Clear from cache
+ promises[ api.defaults.ajax.url ][ tokenType + 'Token' ] =
+ params.token = undefined;
+
+ // Try again, once
+ return api.getToken( tokenType ).then( function ( token ) {
+ params.token = token;
+ return api.post( params, ajaxOptions );
+ } );
+ }
+
+ // Different error, pass on to let caller handle the error code
+ return this;
+ }
+ );
+ } );
+ },
+
+ /**
+ * Get a token for a certain action from the API.
+ *
+ * @param {string} type Token type
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {string} return.done.token Received token.
+ * @since 1.22
+ */
+ getToken: function ( type ) {
+ var apiPromise,
+ promiseGroup = promises[ this.defaults.ajax.url ],
+ d = promiseGroup && promiseGroup[ type + 'Token' ];
+
+ if ( !d ) {
+ apiPromise = this.get( { action: 'tokens', type: type } );
+
+ d = apiPromise
+ .then( function ( data ) {
+ // If token type is not available for this user,
+ // key '...token' is either missing or set to boolean false
+ if ( data.tokens && data.tokens[type + 'token'] ) {
+ return data.tokens[type + 'token'];
+ }
+
+ return $.Deferred().reject( 'token-missing', data );
+ }, function () {
+ // Clear promise. Do not cache errors.
+ delete promiseGroup[ type + 'Token' ];
+
+ // Pass on to allow the caller to handle the error
+ return this;
+ } )
+ // Attach abort handler
+ .promise( { abort: apiPromise.abort } );
+
+ // Store deferred now so that we can use it again even if it isn't ready yet
+ if ( !promiseGroup ) {
+ promiseGroup = promises[ this.defaults.ajax.url ] = {};
+ }
+ promiseGroup[ type + 'Token' ] = d;
+ }
+
+ return d;
+ }
+ };
+
+ /**
+ * @static
+ * @property {Array}
+ * List of errors we might receive from the API.
+ * For now, this just documents our expectation that there should be similar messages
+ * available.
+ */
+ mw.Api.errors = [
+ // occurs when POST aborted
+ // jQuery 1.4 can't distinguish abort or lost connection from 200 OK + empty result
+ 'ok-but-empty',
+
+ // timeout
+ 'timeout',
+
+ // really a warning, but we treat it like an error
+ 'duplicate',
+ 'duplicate-archive',
+
+ // upload succeeded, but no image info.
+ // this is probably impossible, but might as well check for it
+ 'noimageinfo',
+ // remote errors, defined in API
+ 'uploaddisabled',
+ 'nomodule',
+ 'mustbeposted',
+ 'badaccess-groups',
+ 'stashfailed',
+ 'missingresult',
+ 'missingparam',
+ 'invalid-file-key',
+ 'copyuploaddisabled',
+ 'mustbeloggedin',
+ 'empty-file',
+ 'file-too-large',
+ 'filetype-missing',
+ 'filetype-banned',
+ 'filetype-banned-type',
+ 'filename-tooshort',
+ 'illegal-filename',
+ 'verification-error',
+ 'hookaborted',
+ 'unknown-error',
+ 'internal-error',
+ 'overwrite',
+ 'badtoken',
+ 'fetchfileerror',
+ 'fileexists-shared-forbidden',
+ 'invalidtitle',
+ 'notloggedin'
+ ];
+
+ /**
+ * @static
+ * @property {Array}
+ * List of warnings we might receive from the API.
+ * For now, this just documents our expectation that there should be similar messages
+ * available.
+ */
+ mw.Api.warnings = [
+ 'duplicate',
+ 'exists'
+ ];
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api/mediawiki.api.login.js b/resources/src/mediawiki.api/mediawiki.api.login.js
new file mode 100644
index 00000000..ccbae06c
--- /dev/null
+++ b/resources/src/mediawiki.api/mediawiki.api.login.js
@@ -0,0 +1,54 @@
+/**
+ * Make the two-step login easier.
+ * @author Niklas Laxström
+ * @class mw.Api.plugin.login
+ * @since 1.22
+ */
+( function ( mw, $ ) {
+ 'use strict';
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * @param {string} username
+ * @param {string} password
+ * @return {jQuery.Promise} See mw.Api#post
+ */
+ login: function ( username, password ) {
+ var params, request,
+ deferred = $.Deferred(),
+ api = this;
+
+ params = {
+ action: 'login',
+ lgname: username,
+ lgpassword: password
+ };
+
+ request = api.post( params );
+ request.fail( deferred.reject );
+ request.done( function ( data ) {
+ params.lgtoken = data.login.token;
+ api.post( params )
+ .fail( deferred.reject )
+ .done( function ( data ) {
+ var code;
+ if ( data.login && data.login.result === 'Success' ) {
+ deferred.resolve( data );
+ } else {
+ // Set proper error code whenever possible
+ code = data.error && data.error.code || 'unknown';
+ deferred.reject( code, data );
+ }
+ } );
+ } );
+
+ return deferred.promise( { abort: request.abort } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.login
+ */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api/mediawiki.api.parse.js b/resources/src/mediawiki.api/mediawiki.api.parse.js
new file mode 100644
index 00000000..b1f1d2b0
--- /dev/null
+++ b/resources/src/mediawiki.api/mediawiki.api.parse.js
@@ -0,0 +1,45 @@
+/**
+ * @class mw.Api.plugin.parse
+ */
+( function ( mw, $ ) {
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * Convenience method for 'action=parse'.
+ *
+ * @param {string} wikitext
+ * @param {Function} [ok] Success callback (deprecated)
+ * @param {Function} [err] Error callback (deprecated)
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {string} return.done.data Parsed HTML of `wikitext`.
+ */
+ parse: function ( wikitext, ok, err ) {
+ var apiPromise = this.get( {
+ action: 'parse',
+ contentmodel: 'wikitext',
+ text: wikitext
+ } );
+
+ // Backwards compatibility (< MW 1.20)
+ if ( ok || err ) {
+ mw.track( 'mw.deprecate', 'api.cbParam' );
+ mw.log.warn( 'Use of mediawiki.api callback params is deprecated. Use the Promise instead.' );
+ }
+
+ return apiPromise
+ .then( function ( data ) {
+ return data.parse.text['*'];
+ } )
+ .done( ok )
+ .fail( err )
+ .promise( { abort: apiPromise.abort } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.parse
+ */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.api/mediawiki.api.watch.js b/resources/src/mediawiki.api/mediawiki.api.watch.js
new file mode 100644
index 00000000..af2dee10
--- /dev/null
+++ b/resources/src/mediawiki.api/mediawiki.api.watch.js
@@ -0,0 +1,79 @@
+/**
+ * @class mw.Api.plugin.watch
+ * @since 1.19
+ */
+( function ( mw, $ ) {
+
+ /**
+ * @private
+ * @static
+ * @context mw.Api
+ *
+ * @param {string|mw.Title|string[]|mw.Title[]} pages Full page name or instance of mw.Title, or an
+ * array thereof. If an array is passed, the return value passed to the promise will also be an
+ * array of appropriate objects.
+ * @param {Function} [ok] Success callback (deprecated)
+ * @param {Function} [err] Error callback (deprecated)
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {Object|Object[]} return.done.watch Object or list of objects (depends on the `pages`
+ * parameter)
+ * @return {string} return.done.watch.title Full pagename
+ * @return {boolean} return.done.watch.watched Whether the page is now watched or unwatched
+ * @return {string} return.done.watch.message Parsed HTML of the confirmational interface message
+ */
+ function doWatchInternal( pages, ok, err, addParams ) {
+ // XXX: Parameter addParams is undocumented because we inherit this
+ // documentation in the public method...
+ var apiPromise = this.postWithToken( 'watch',
+ $.extend(
+ {
+ action: 'watch',
+ titles: $.isArray( pages ) ? pages.join( '|' ) : String( pages ),
+ uselang: mw.config.get( 'wgUserLanguage' )
+ },
+ addParams
+ )
+ );
+
+ // Backwards compatibility (< MW 1.20)
+ if ( ok || err ) {
+ mw.track( 'mw.deprecate', 'api.cbParam' );
+ mw.log.warn( 'Use of mediawiki.api callback params is deprecated. Use the Promise instead.' );
+ }
+
+ return apiPromise
+ .then( function ( data ) {
+ // If a single page was given (not an array) respond with a single item as well.
+ return $.isArray( pages ) ? data.watch : data.watch[0];
+ } )
+ .done( ok )
+ .fail( err )
+ .promise( { abort: apiPromise.abort } );
+ }
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * Convenience method for `action=watch`.
+ *
+ * @inheritdoc #doWatchInternal
+ */
+ watch: function ( pages, ok, err ) {
+ return doWatchInternal.call( this, pages, ok, err );
+ },
+ /**
+ * Convenience method for `action=watch&unwatch=1`.
+ *
+ * @inheritdoc #doWatchInternal
+ */
+ unwatch: function ( pages, ok, err ) {
+ return doWatchInternal.call( this, pages, ok, err, { unwatch: 1 } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.watch
+ */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.hidpi-skip.js b/resources/src/mediawiki.hidpi-skip.js
new file mode 100644
index 00000000..26b63c7b
--- /dev/null
+++ b/resources/src/mediawiki.hidpi-skip.js
@@ -0,0 +1,4 @@
+/*!
+ * Skip function for mediawiki.hdpi.js.
+ */
+return 'srcset' in new Image();
diff --git a/resources/src/mediawiki.language/languages/bs.js b/resources/src/mediawiki.language/languages/bs.js
new file mode 100644
index 00000000..b56e4b29
--- /dev/null
+++ b/resources/src/mediawiki.language/languages/bs.js
@@ -0,0 +1,19 @@
+/*!
+ * Bosnian (bosanski) language functions
+ */
+
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'bs', 'grammarForms' );
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word];
+ }
+ switch ( form ) {
+ case 'instrumental': // instrumental
+ word = 's ' + word;
+ break;
+ case 'lokativ': // locative
+ word = 'o ' + word;
+ break;
+ }
+ return word;
+};
diff --git a/resources/src/mediawiki.language/languages/dsb.js b/resources/src/mediawiki.language/languages/dsb.js
new file mode 100644
index 00000000..69c36cc0
--- /dev/null
+++ b/resources/src/mediawiki.language/languages/dsb.js
@@ -0,0 +1,19 @@
+/*!
+ * Lower Sorbian (Dolnoserbski) language functions
+ */
+
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'dsb', 'grammarForms' );
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word];
+ }
+ switch ( form ) {
+ case 'instrumental': // instrumental
+ word = 'z ' + word;
+ break;
+ case 'lokatiw': // lokatiw
+ word = 'wo ' + word;
+ break;
+ }
+ return word;
+};
diff --git a/resources/src/mediawiki.language/languages/fi.js b/resources/src/mediawiki.language/languages/fi.js
new file mode 100644
index 00000000..453a675d
--- /dev/null
+++ b/resources/src/mediawiki.language/languages/fi.js
@@ -0,0 +1,47 @@
+/*!
+ * Finnish (Suomi) language functions
+ * @author Santhosh Thottingal
+ */
+
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms, aou, origWord;
+
+ grammarForms = mediaWiki.language.getData( 'fi', 'grammarForms' );
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word];
+ }
+
+ // vowel harmony flag
+ aou = word.match( /[aou][^äöy]*$/i );
+ origWord = word;
+ if ( word.match( /wiki$/i ) ) {
+ aou = false;
+ }
+ //append i after final consonant
+ if ( word.match( /[bcdfghjklmnpqrstvwxz]$/i ) ) {
+ word += 'i';
+ }
+
+ switch ( form ) {
+ case 'genitive':
+ word += 'n';
+ break;
+ case 'elative':
+ word += ( aou ? 'sta' : 'stä' );
+ break;
+ case 'partitive':
+ word += ( aou ? 'a' : 'ä' );
+ break;
+ case 'illative':
+ // Double the last letter and add 'n'
+ word += word.slice( -1 ) + 'n';
+ break;
+ case 'inessive':
+ word += ( aou ? 'ssa' : 'ssä' );
+ break;
+ default:
+ word = origWord;
+ break;
+ }
+ return word;
+};
diff --git a/resources/src/mediawiki.language/languages/ga.js b/resources/src/mediawiki.language/languages/ga.js
new file mode 100644
index 00000000..fb4e9396
--- /dev/null
+++ b/resources/src/mediawiki.language/languages/ga.js
@@ -0,0 +1,38 @@
+/*!
+ * Irish (Gaeilge) language functions
+ */
+
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ /*jshint onecase:true */
+ var grammarForms = mediaWiki.language.getData( 'ga', 'grammarForms' );
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word];
+ }
+ switch ( form ) {
+ case 'ainmlae':
+ switch ( word ) {
+ case 'an Domhnach':
+ word = 'Dé Domhnaigh';
+ break;
+ case 'an Luan':
+ word = 'Dé Luain';
+ break;
+ case 'an Mháirt':
+ word = 'Dé Mháirt';
+ break;
+ case 'an Chéadaoin':
+ word = 'Dé Chéadaoin';
+ break;
+ case 'an Déardaoin':
+ word = 'Déardaoin';
+ break;
+ case 'an Aoine':
+ word = 'Dé hAoine';
+ break;
+ case 'an Satharn':
+ word = 'Dé Sathairn';
+ break;
+ }
+ }
+ return word;
+};
diff --git a/resources/src/mediawiki.language/languages/he.js b/resources/src/mediawiki.language/languages/he.js
new file mode 100644
index 00000000..d1eba43b
--- /dev/null
+++ b/resources/src/mediawiki.language/languages/he.js
@@ -0,0 +1,29 @@
+/*!
+ * Hebrew (עברית) language functions
+ */
+
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'he', 'grammarForms' );
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word];
+ }
+ switch ( form ) {
+ case 'prefixed':
+ case 'תחילית': // the same word in Hebrew
+ // Duplicate prefixed "Waw", but only if it's not already double
+ if ( word.slice( 0, 1 ) === 'ו' && word.slice( 0, 2 ) !== 'וו' ) {
+ word = 'ו' + word;
+ }
+
+ // Remove the "He" if prefixed
+ if ( word.slice( 0, 1 ) === 'ה' ) {
+ word = word.slice( 1 );
+ }
+
+ // Add a hyphen (maqaf) before numbers and non-Hebrew letters
+ if ( word.slice( 0, 1 ) < 'א' || word.slice( 0, 1 ) > 'ת' ) {
+ word = '־' + word;
+ }
+ }
+ return word;
+};
diff --git a/resources/src/mediawiki.language/languages/hsb.js b/resources/src/mediawiki.language/languages/hsb.js
new file mode 100644
index 00000000..2d6b733e
--- /dev/null
+++ b/resources/src/mediawiki.language/languages/hsb.js
@@ -0,0 +1,19 @@
+/*!
+ * Upper Sorbian (Hornjoserbsce) language functions
+ */
+
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'hsb', 'grammarForms' );
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word];
+ }
+ switch ( form ) {
+ case 'instrumental': // instrumental
+ word = 'z ' + word;
+ break;
+ case 'lokatiw': // lokatiw
+ word = 'wo ' + word;
+ break;
+ }
+ return word;
+};
diff --git a/resources/src/mediawiki.language/languages/hu.js b/resources/src/mediawiki.language/languages/hu.js
new file mode 100644
index 00000000..d72a1c05
--- /dev/null
+++ b/resources/src/mediawiki.language/languages/hu.js
@@ -0,0 +1,23 @@
+/*!
+ * Hungarian language functions
+ * @author Santhosh Thottingal
+ */
+
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'hu', 'grammarForms' );
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word];
+ }
+ switch ( form ) {
+ case 'rol':
+ word += 'ról';
+ break;
+ case 'ba':
+ word += 'ba';
+ break;
+ case 'k':
+ word += 'k';
+ break;
+ }
+ return word;
+};
diff --git a/resources/src/mediawiki.language/languages/hy.js b/resources/src/mediawiki.language/languages/hy.js
new file mode 100644
index 00000000..9cae360b
--- /dev/null
+++ b/resources/src/mediawiki.language/languages/hy.js
@@ -0,0 +1,29 @@
+/*!
+ * Armenian (Հայերեն) language functions
+ */
+
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ /*jshint onecase:true */
+ var grammarForms = mediaWiki.language.getData( 'hy', 'grammarForms' );
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word];
+ }
+
+ // These rules are not perfect, but they are currently only used for site names so it doesn't
+ // matter if they are wrong sometimes. Just add a special case for your site name if necessary.
+
+ switch ( form ) {
+ case 'genitive': // սեռական հոլով
+ if ( word.slice( -1 ) === 'ա' ) {
+ word = word.slice( 0, -1 ) + 'այի';
+ } else if ( word.slice( -1 ) === 'ո' ) {
+ word = word.slice( 0, -1 ) + 'ոյի';
+ } else if ( word.slice( -4 ) === 'գիրք' ) {
+ word = word.slice( 0, -4 ) + 'գրքի';
+ } else {
+ word = word + 'ի';
+ }
+ break;
+ }
+ return word;
+};
diff --git a/resources/src/mediawiki.language/languages/la.js b/resources/src/mediawiki.language/languages/la.js
new file mode 100644
index 00000000..52e8dd44
--- /dev/null
+++ b/resources/src/mediawiki.language/languages/la.js
@@ -0,0 +1,50 @@
+/*!
+ * Latin (lingua Latina) language functions
+ * @author Santhosh Thottingal
+ */
+
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'la', 'grammarForms' );
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word];
+ }
+ switch ( form ) {
+ case 'genitive':
+ // only a few declensions, and even for those mostly the singular only
+ word = word.replace( /u[ms]$/i, 'i' ); // 2nd declension singular
+ word = word.replace( /ommunia$/i, 'ommunium' ); // 3rd declension neuter plural (partly)
+ word = word.replace( /a$/i, 'ae' ); // 1st declension singular
+ word = word.replace( /libri$/i, 'librorum' ); // 2nd declension plural (partly)
+ word = word.replace( /nuntii$/i, 'nuntiorum' ); // 2nd declension plural (partly)
+ word = word.replace( /tio$/i, 'tionis' ); // 3rd declension singular (partly)
+ word = word.replace( /ns$/i, 'ntis' );
+ word = word.replace( /as$/i, 'atis' );
+ word = word.replace( /es$/i, 'ei' ); // 5th declension singular
+ break;
+ case 'accusative':
+ // only a few declensions, and even for those mostly the singular only
+ word = word.replace( /u[ms]$/i, 'um' ); // 2nd declension singular
+ word = word.replace( /ommunia$/i, 'am' ); // 3rd declension neuter plural (partly)
+ word = word.replace( /a$/i, 'ommunia' ); // 1st declension singular
+ word = word.replace( /libri$/i, 'libros' ); // 2nd declension plural (partly)
+ word = word.replace( /nuntii$/i, 'nuntios' );// 2nd declension plural (partly)
+ word = word.replace( /tio$/i, 'tionem' ); // 3rd declension singular (partly)
+ word = word.replace( /ns$/i, 'ntem' );
+ word = word.replace( /as$/i, 'atem');
+ word = word.replace( /es$/i, 'em' ); // 5th declension singular
+ break;
+ case 'ablative':
+ // only a few declensions, and even for those mostly the singular only
+ word = word.replace( /u[ms]$/i, 'o' ); // 2nd declension singular
+ word = word.replace( /ommunia$/i, 'ommunibus' ); // 3rd declension neuter plural (partly)
+ word = word.replace( /a$/i, 'a' ); // 1st declension singular
+ word = word.replace( /libri$/i, 'libris' ); // 2nd declension plural (partly)
+ word = word.replace( /nuntii$/i, 'nuntiis' ); // 2nd declension plural (partly)
+ word = word.replace( /tio$/i, 'tione' ); // 3rd declension singular (partly)
+ word = word.replace( /ns$/i, 'nte' );
+ word = word.replace( /as$/i, 'ate');
+ word = word.replace( /es$/i, 'e' ); // 5th declension singular
+ break;
+ }
+ return word;
+};
diff --git a/resources/src/mediawiki.language/languages/os.js b/resources/src/mediawiki.language/languages/os.js
new file mode 100644
index 00000000..787be36d
--- /dev/null
+++ b/resources/src/mediawiki.language/languages/os.js
@@ -0,0 +1,69 @@
+/*!
+ * Ossetian (Ирон) language functions
+ * @author Santhosh Thottingal
+ */
+
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'os', 'grammarForms' ),
+ // Ending for allative case
+ endAllative = 'мæ',
+ // Variable for 'j' beetwen vowels
+ jot = '',
+ // Variable for "-" for not Ossetic words
+ hyphen = '',
+ // Variable for ending
+ ending = '';
+
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word];
+ }
+ // Checking if the $word is in plural form
+ if ( word.match( /тæ$/i ) ) {
+ word = word.slice( 0, -1 );
+ endAllative = 'æм';
+ }
+ // Works if word is in singular form.
+ // Checking if word ends on one of the vowels: е, ё, и, о, ы, э, ю, я.
+ else if ( word.match( /[аæеёиоыэюя]$/i ) ) {
+ jot = 'й';
+ }
+ // Checking if word ends on 'у'. 'У' can be either consonant 'W' or vowel 'U' in cyrillic Ossetic.
+ // Examples: {{grammar:genitive|аунеу}} = аунеуы, {{grammar:genitive|лæппу}} = лæппуйы.
+ else if ( word.match( /у$/i ) ) {
+ if ( !word.slice( -2, -1 ).match( /[аæеёиоыэюя]$/i ) ) {
+ jot = 'й';
+ }
+ } else if ( !word.match( /[бвгджзйклмнопрстфхцчшщьъ]$/i ) ) {
+ hyphen = '-';
+ }
+
+ switch ( form ) {
+ case 'genitive':
+ ending = hyphen + jot + 'ы';
+ break;
+ case 'dative':
+ ending = hyphen + jot + 'æн';
+ break;
+ case 'allative':
+ ending = hyphen + endAllative;
+ break;
+ case 'ablative':
+ if ( jot === 'й' ) {
+ ending = hyphen + jot + 'æ';
+ }
+ else {
+ ending = hyphen + jot + 'æй';
+ }
+ break;
+ case 'superessive':
+ ending = hyphen + jot + 'ыл';
+ break;
+ case 'equative':
+ ending = hyphen + jot + 'ау';
+ break;
+ case 'comitative':
+ ending = hyphen + 'имæ';
+ break;
+ }
+ return word + ending;
+};
diff --git a/resources/src/mediawiki.language/languages/ru.js b/resources/src/mediawiki.language/languages/ru.js
new file mode 100644
index 00000000..2077b6be
--- /dev/null
+++ b/resources/src/mediawiki.language/languages/ru.js
@@ -0,0 +1,57 @@
+/*!
+ * Russian (Русский) language functions
+ */
+
+// These tests were originally made for names of Wikimedia
+// websites, so they don't currently cover all the possible
+// cases.
+
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ 'use strict';
+
+ var grammarForms = mediaWiki.language.getData( 'ru', 'grammarForms' );
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word];
+ }
+ switch ( form ) {
+ case 'genitive': // родительный падеж
+ if ( word.slice( -1 ) === 'ь' ) {
+ word = word.slice( 0, -1 ) + 'я';
+ } else if ( word.slice( -2 ) === 'ия' ) {
+ word = word.slice( 0, -2 ) + 'ии';
+ } else if ( word.slice( -2 ) === 'ка' ) {
+ word = word.slice( 0, -2 ) + 'ки';
+ } else if ( word.slice( -2 ) === 'ти' ) {
+ word = word.slice( 0, -2 ) + 'тей';
+ } else if ( word.slice( -2 ) === 'ды' ) {
+ word = word.slice( 0, -2 ) + 'дов';
+ } else if ( word.slice( -1 ) === 'д' ) {
+ word = word.slice( 0, -1 ) + 'да';
+ } else if ( word.slice( -3 ) === 'ные' ) {
+ word = word.slice( 0, -3 ) + 'ных';
+ } else if ( word.slice( -3 ) === 'ник' ) {
+ word = word.slice( 0, -3 ) + 'ника';
+ }
+ break;
+ case 'prepositional': // предложный падеж
+ if ( word.slice( -1 ) === 'ь' ) {
+ word = word.slice( 0, -1 ) + 'е';
+ } else if ( word.slice( -2 ) === 'ия' ) {
+ word = word.slice( 0, -2 ) + 'ии';
+ } else if ( word.slice( -2 ) === 'ка' ) {
+ word = word.slice( 0, -2 ) + 'ке';
+ } else if ( word.slice( -2 ) === 'ти' ) {
+ word = word.slice( 0, -2 ) + 'тях';
+ } else if ( word.slice( -2 ) === 'ды' ) {
+ word = word.slice( 0, -2 ) + 'дах';
+ } else if ( word.slice( -1 ) === 'д' ) {
+ word = word.slice( 0, -1 ) + 'де';
+ } else if ( word.slice( -3 ) === 'ные' ) {
+ word = word.slice( 0, -3 ) + 'ных';
+ } else if ( word.slice( -3 ) === 'ник' ) {
+ word = word.slice( 0, -3 ) + 'нике';
+ }
+ break;
+ }
+ return word;
+};
diff --git a/resources/src/mediawiki.language/languages/sl.js b/resources/src/mediawiki.language/languages/sl.js
new file mode 100644
index 00000000..d20d0b34
--- /dev/null
+++ b/resources/src/mediawiki.language/languages/sl.js
@@ -0,0 +1,19 @@
+/*!
+ * Slovenian (Slovenščina) language functions
+ */
+
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'sl', 'grammarForms' );
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word];
+ }
+ switch ( form ) {
+ case 'mestnik': // locative
+ word = 'o ' + word;
+ break;
+ case 'orodnik': // instrumental
+ word = 'z ' + word;
+ break;
+ }
+ return word;
+};
diff --git a/resources/src/mediawiki.language/languages/uk.js b/resources/src/mediawiki.language/languages/uk.js
new file mode 100644
index 00000000..550a388c
--- /dev/null
+++ b/resources/src/mediawiki.language/languages/uk.js
@@ -0,0 +1,37 @@
+/*!
+ * Ukrainian (Українська) language functions
+ */
+
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'uk', 'grammarForms' );
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word];
+ }
+ switch ( form ) {
+ case 'genitive': // родовий відмінок
+ if ( word.slice( -4 ) !== 'вікі' && word.slice( -4 ) !== 'Вікі' ) {
+ if ( word.slice( -1 ) === 'ь' ) {
+ word = word.slice(0, -1 ) + 'я';
+ } else if ( word.slice( -2 ) === 'ія' ) {
+ word = word.slice(0, -2 ) + 'ії';
+ } else if ( word.slice( -2 ) === 'ка' ) {
+ word = word.slice(0, -2 ) + 'ки';
+ } else if ( word.slice( -2 ) === 'ти' ) {
+ word = word.slice(0, -2 ) + 'тей';
+ } else if ( word.slice( -2 ) === 'ды' ) {
+ word = word.slice(0, -2 ) + 'дов';
+ } else if ( word.slice( -3 ) === 'ник' ) {
+ word = word.slice(0, -3 ) + 'ника';
+ }
+ }
+ break;
+ case 'accusative': // знахідний відмінок
+ if ( word.slice( -4 ) !== 'вікі' && word.slice( -4 ) !== 'Вікі' ) {
+ if ( word.slice( -2 ) === 'ія' ) {
+ word = word.slice(0, -2 ) + 'ію';
+ }
+ }
+ break;
+ }
+ return word;
+};
diff --git a/resources/src/mediawiki.language/mediawiki.cldr.js b/resources/src/mediawiki.language/mediawiki.cldr.js
new file mode 100644
index 00000000..f6fb8f10
--- /dev/null
+++ b/resources/src/mediawiki.language/mediawiki.cldr.js
@@ -0,0 +1,32 @@
+( function ( mw ) {
+ 'use strict';
+
+ /**
+ * Namespace for CLDR-related utility methods.
+ *
+ * @class
+ * @singleton
+ */
+ mw.cldr = {
+ /**
+ * Get the plural form index for the number.
+ *
+ * In case none of the rules passed, we return `pluralRules.length` -
+ * that means it is the "other" form.
+ *
+ * @param {number} number
+ * @param {Array} pluralRules
+ * @return {number} plural form index
+ */
+ getPluralForm: function ( number, pluralRules ) {
+ var i;
+ for ( i = 0; i < pluralRules.length; i++ ) {
+ if ( mw.libs.pluralRuleParser( pluralRules[i], number ) ) {
+ break;
+ }
+ }
+ return i;
+ }
+ };
+
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki.language/mediawiki.language.fallback.js b/resources/src/mediawiki.language/mediawiki.language.fallback.js
new file mode 100644
index 00000000..b1bab02a
--- /dev/null
+++ b/resources/src/mediawiki.language/mediawiki.language.fallback.js
@@ -0,0 +1,35 @@
+/*
+ * Language-fallback-chain-related utilities for mediawiki.language.
+ */
+( function ( mw, $ ) {
+ /**
+ * @class mw.language
+ */
+
+ $.extend( mw.language, {
+
+ /**
+ * Get the language fallback chain for current UI language (not including the language itself).
+ *
+ * @return {string[]} List of language keys, e.g. `['de', 'en']`
+ */
+ getFallbackLanguages: function () {
+ return mw.language.getData(
+ mw.config.get( 'wgUserLanguage' ),
+ 'fallbackLanguages'
+ ) || [];
+ },
+
+ /**
+ * Get the language fallback chain for current UI language, including the language itself.
+ *
+ * @return {string[]} List of language keys, e.g. `['pfl', de', 'en']`
+ */
+ getFallbackLanguageChain: function () {
+ return [ mw.config.get( 'wgUserLanguage' ) ]
+ .concat( mw.language.getFallbackLanguages() );
+ }
+
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.language/mediawiki.language.init.js b/resources/src/mediawiki.language/mediawiki.language.init.js
new file mode 100644
index 00000000..df95d751
--- /dev/null
+++ b/resources/src/mediawiki.language/mediawiki.language.init.js
@@ -0,0 +1,81 @@
+( function ( mw ) {
+ /**
+ * Base language object with methods related to language support, attempting to mirror some of the
+ * functionality of the Language class in MediaWiki:
+ *
+ * - storing and retrieving language data
+ * - transforming message syntax (`{{PLURAL:}}`, `{{GRAMMAR:}}`, `{{GENDER:}}`)
+ * - formatting numbers
+ *
+ * @class
+ * @singleton
+ */
+ mw.language = {
+ /**
+ * Language-related data (keyed by language, contains instances of mw.Map).
+ * Loaded dynamically (see ResourceLoaderLanguageDataModule class in PHP, registered
+ * as mediawiki.language.data on the client).
+ *
+ * To set data:
+ *
+ * // Override, extend or create the language data object of 'nl'
+ * mw.language.setData( 'nl', 'myKey', 'My value' );
+ *
+ * // Set multiple key/values pairs at once
+ * mw.language.setData( 'nl', { foo: 'X', bar: 'Y' } );
+ *
+ * To get GrammarForms data for language 'nl':
+ *
+ * var grammarForms = mw.language.getData( 'nl', 'grammarForms' );
+ *
+ * Possible data keys:
+ *
+ * - `digitTransformTable`
+ * - `separatorTransformTable`
+ * - `grammarForms`
+ * - `pluralRules`
+ * - `digitGroupingPattern`
+ * - `fallbackLanguages`
+ *
+ * @property
+ */
+ data: {},
+
+ /**
+ * Convenience method for retrieving language data.
+ *
+ * Structured by language code and data key, covering for the potential inexistence of a
+ * data object for this language.
+ *
+ * @param {string} langCode
+ * @param {string} dataKey
+ * @return {Mixed} Value stored in the mw.Map (or `undefined` if there is no map for the
+ * specified langCode)
+ */
+ getData: function ( langCode, dataKey ) {
+ var langData = mw.language.data;
+ if ( langData && langData[langCode] instanceof mw.Map ) {
+ return langData[langCode].get( dataKey );
+ }
+ return undefined;
+ },
+
+ /**
+ * Convenience method for setting language data.
+ *
+ * Creates the data mw.Map if there isn't one for the specified language already.
+ *
+ * @param {string} langCode
+ * @param {string|Object} dataKey Key or object of key/values
+ * @param {Mixed} [value] Value for dataKey, omit if dataKey is an object
+ */
+ setData: function ( langCode, dataKey, value ) {
+ var langData = mw.language.data;
+ if ( !( langData[langCode] instanceof mw.Map ) ) {
+ langData[langCode] = new mw.Map();
+ }
+ langData[langCode].set( dataKey, value );
+ }
+ };
+
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki.language/mediawiki.language.js b/resources/src/mediawiki.language/mediawiki.language.js
new file mode 100644
index 00000000..d4f3c69e
--- /dev/null
+++ b/resources/src/mediawiki.language/mediawiki.language.js
@@ -0,0 +1,171 @@
+/*
+ * Methods for transforming message syntax.
+ */
+( function ( mw, $ ) {
+
+/**
+ * @class mw.language
+ */
+$.extend( mw.language, {
+
+ /**
+ * Process the PLURAL template substitution
+ *
+ * @private
+ * @param {Object} template Template object
+ * @param {string} template.title
+ * @param {Array} template.parameters
+ * @return {string}
+ */
+ procPLURAL: function ( template ) {
+ if ( template.title && template.parameters && mw.language.convertPlural ) {
+ // Check if we have forms to replace
+ if ( template.parameters.length === 0 ) {
+ return '';
+ }
+ // Restore the count into a Number ( if it got converted earlier )
+ var count = mw.language.convertNumber( template.title, true );
+ // Do convertPlural call
+ return mw.language.convertPlural( parseInt( count, 10 ), template.parameters );
+ }
+ // Could not process plural return first form or nothing
+ if ( template.parameters[0] ) {
+ return template.parameters[0];
+ }
+ return '';
+ },
+
+ /**
+ * Plural form transformations, needed for some languages.
+ *
+ * @param {number} count Non-localized quantifier
+ * @param {Array} forms List of plural forms
+ * @return {string} Correct form for quantifier in this language
+ */
+ convertPlural: function ( count, forms ) {
+ var pluralRules,
+ formCount,
+ form,
+ index,
+ equalsPosition,
+ pluralFormIndex = 0;
+
+ if ( !forms || forms.length === 0 ) {
+ return '';
+ }
+
+ // Handle for explicit n= forms
+ for ( index = 0; index < forms.length; index++ ) {
+ form = forms[index];
+ if ( /^\d+=/.test( form ) ) {
+ equalsPosition = form.indexOf( '=' );
+ formCount = parseInt( form.slice( 0, equalsPosition ), 10 );
+ if ( formCount === count ) {
+ return form.slice( equalsPosition + 1 );
+ }
+ forms[index] = undefined;
+ }
+ }
+
+ // Remove explicit plural forms from the forms.
+ forms = $.map( forms, function ( form ) {
+ return form;
+ } );
+
+ if ( forms.length === 0 ) {
+ return '';
+ }
+
+ pluralRules = mw.language.getData( mw.config.get( 'wgUserLanguage' ), 'pluralRules' );
+ if ( !pluralRules ) {
+ // default fallback.
+ return ( count === 1 ) ? forms[0] : forms[1];
+ }
+ pluralFormIndex = mw.cldr.getPluralForm( count, pluralRules );
+ pluralFormIndex = Math.min( pluralFormIndex, forms.length - 1 );
+ return forms[pluralFormIndex];
+ },
+
+ /**
+ * Pads an array to a specific length by copying the last one element.
+ *
+ * @private
+ * @param {Array} forms Number of forms given to convertPlural
+ * @param {number} count Number of forms required
+ * @return {Array} Padded array of forms
+ */
+ preConvertPlural: function ( forms, count ) {
+ while ( forms.length < count ) {
+ forms.push( forms[ forms.length - 1 ] );
+ }
+ return forms;
+ },
+
+ /**
+ * Provides an alternative text depending on specified gender.
+ *
+ * Usage in message text: `{{gender:[gender|user object]|masculine|feminine|neutral}}`.
+ * If second or third parameter are not specified, masculine is used.
+ *
+ * These details may be overriden per language.
+ *
+ * @param {string} gender 'male', 'female', or anything else for neutral.
+ * @param {Array} forms List of gender forms
+ * @return string
+ */
+ gender: function ( gender, forms ) {
+ if ( !forms || forms.length === 0 ) {
+ return '';
+ }
+ forms = mw.language.preConvertPlural( forms, 2 );
+ if ( gender === 'male' ) {
+ return forms[0];
+ }
+ if ( gender === 'female' ) {
+ return forms[1];
+ }
+ return ( forms.length === 3 ) ? forms[2] : forms[0];
+ },
+
+ /**
+ * Grammatical transformations, needed for inflected languages.
+ * Invoked by putting `{{grammar:form|word}}` in a message.
+ *
+ * The rules can be defined in $wgGrammarForms global or computed
+ * dynamically by overriding this method per language.
+ *
+ * @param {string} word
+ * @param {string} form
+ * @return {string}
+ */
+ convertGrammar: function ( word, form ) {
+ var grammarForms = mw.language.getData( mw.config.get( 'wgUserLanguage' ), 'grammarForms' );
+ if ( grammarForms && grammarForms[form] ) {
+ return grammarForms[form][word] || word;
+ }
+ return word;
+ },
+
+ /**
+ * Turn a list of string into a simple list using commas and 'and'.
+ *
+ * See Language::listToText in languages/Language.php
+ *
+ * @param {string[]} list
+ * @return {string}
+ */
+ listToText: function ( list ) {
+ var text = '', i = 0;
+ for ( ; i < list.length; i++ ) {
+ text += list[i];
+ if ( list.length - 2 === i ) {
+ text += mw.msg( 'and' ) + mw.msg( 'word-separator' );
+ } else if ( list.length - 1 !== i ) {
+ text += mw.msg( 'comma-separator' );
+ }
+ }
+ return text;
+ }
+} );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.language/mediawiki.language.months.js b/resources/src/mediawiki.language/mediawiki.language.months.js
new file mode 100644
index 00000000..5a1a5cb6
--- /dev/null
+++ b/resources/src/mediawiki.language/mediawiki.language.months.js
@@ -0,0 +1,56 @@
+/*
+ * Transfer of month names from messages into mw.language.
+ *
+ * Loading this module also ensures the availability of appropriate messages via mw.msg.
+ */
+( function ( mw, $ ) {
+ var
+ monthMessages = [
+ 'january', 'february', 'march', 'april',
+ 'may_long', 'june', 'july', 'august',
+ 'september', 'october', 'november', 'december'
+ ],
+ monthGenMessages = [
+ 'january-gen', 'february-gen', 'march-gen', 'april-gen',
+ 'may-gen', 'june-gen', 'july-gen', 'august-gen',
+ 'september-gen', 'october-gen', 'november-gen', 'december-gen'
+ ],
+ monthAbbrevMessages = [
+ 'jan', 'feb', 'mar', 'apr',
+ 'may', 'jun', 'jul', 'aug',
+ 'sep', 'oct', 'nov', 'dec'
+ ];
+
+ // Function suitable for passing to jQuery.map
+ // Can't use mw.msg directly because jQuery.map passes element index as second argument
+ function mwMsgMapper( key ) {
+ return mw.msg( key );
+ }
+
+ /**
+ * Information about month names in current UI language.
+ *
+ * Object keys:
+ *
+ * - `names`: array of month names (in nominative case in languages which have the distinction),
+ * zero-indexed
+ * - `genitive`: array of month names in genitive case, zero-indexed
+ * - `abbrev`: array of three-letter-long abbreviated month names, zero-indexed
+ * - `keys`: object with three keys like the above, containing zero-indexed arrays of message keys
+ * for appropriate messages which can be passed to mw.msg.
+ *
+ * @property
+ * @member mw.language
+ */
+ mw.language.months = {
+ keys: {
+ names: monthMessages,
+ genitive: monthGenMessages,
+ abbrev: monthAbbrevMessages
+ },
+ names: $.map( monthMessages, mwMsgMapper ),
+ genitive: $.map( monthGenMessages, mwMsgMapper ),
+ abbrev: $.map( monthAbbrevMessages, mwMsgMapper )
+ };
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.language/mediawiki.language.numbers.js b/resources/src/mediawiki.language/mediawiki.language.numbers.js
new file mode 100644
index 00000000..a0b81410
--- /dev/null
+++ b/resources/src/mediawiki.language/mediawiki.language.numbers.js
@@ -0,0 +1,258 @@
+/*
+ * Number-related utilities for mediawiki.language.
+ */
+( function ( mw, $ ) {
+ /**
+ * @class mw.language
+ */
+
+ /**
+ * Pad a string to guarantee that it is at least `size` length by
+ * filling with the character `ch` at either the start or end of the
+ * string. Pads at the start, by default.
+ *
+ * Example: Fill the string to length 10 with '+' characters on the right.
+ *
+ * pad( 'blah', 10, '+', true ); // => 'blah++++++'
+ *
+ * @private
+ * @param {string} text The string to pad
+ * @param {number} size The length to pad to
+ * @param {string} [ch='0'] Character to pad with
+ * @param {boolean} [end=false] Adds padding at the end if true, otherwise pads at start
+ * @return {string}
+ */
+ function pad( text, size, ch, end ) {
+ if ( !ch ) {
+ ch = '0';
+ }
+
+ var out = String( text ),
+ padStr = replicate( ch, Math.ceil( ( size - out.length ) / ch.length ) );
+
+ return end ? out + padStr : padStr + out;
+ }
+
+ /**
+ * Replicate a string 'n' times.
+ *
+ * @private
+ * @param {string} str The string to replicate
+ * @param {number} num Number of times to replicate the string
+ * @return {string}
+ */
+ function replicate( str, num ) {
+ if ( num <= 0 || !str ) {
+ return '';
+ }
+
+ var buf = [];
+ while ( num-- ) {
+ buf.push( str );
+ }
+ return buf.join( '' );
+ }
+
+ /**
+ * Apply numeric pattern to absolute value using options. Gives no
+ * consideration to local customs.
+ *
+ * Adapted from dojo/number library with thanks
+ * <http://dojotoolkit.org/reference-guide/1.8/dojo/number.html>
+ *
+ * @private
+ * @param {number} value the number to be formatted, ignores sign
+ * @param {string} pattern the number portion of a pattern (e.g. `#,##0.00`)
+ * @param {Object} [options] If provided, both option keys must be present:
+ * @param {string} options.decimal The decimal separator. Defaults to: `'.'`.
+ * @param {string} options.group The group separator. Defaults to: `','`.
+ * @return {string}
+ */
+ function commafyNumber( value, pattern, options ) {
+ options = options || {
+ group: ',',
+ decimal: '.'
+ };
+
+ if ( isNaN( value) ) {
+ return value;
+ }
+
+ var padLength,
+ patternDigits,
+ index,
+ whole,
+ off,
+ remainder,
+ patternParts = pattern.split( '.' ),
+ maxPlaces = ( patternParts[1] || [] ).length,
+ valueParts = String( Math.abs( value ) ).split( '.' ),
+ fractional = valueParts[1] || '',
+ groupSize = 0,
+ groupSize2 = 0,
+ pieces = [];
+
+ if ( patternParts[1] ) {
+ // Pad fractional with trailing zeros
+ padLength = ( patternParts[1] && patternParts[1].lastIndexOf( '0' ) + 1 );
+
+ if ( padLength > fractional.length ) {
+ valueParts[1] = pad( fractional, padLength, '0', true );
+ }
+
+ // Truncate fractional
+ if ( maxPlaces < fractional.length ) {
+ valueParts[1] = fractional.slice( 0, maxPlaces );
+ }
+ } else {
+ if ( valueParts[1] ) {
+ valueParts.pop();
+ }
+ }
+
+ // Pad whole with leading zeros
+ patternDigits = patternParts[0].replace( ',', '' );
+
+ padLength = patternDigits.indexOf( '0' );
+
+ if ( padLength !== -1 ) {
+ padLength = patternDigits.length - padLength;
+
+ if ( padLength > valueParts[0].length ) {
+ valueParts[0] = pad( valueParts[0], padLength );
+ }
+
+ // Truncate whole
+ if ( patternDigits.indexOf( '#' ) === -1 ) {
+ valueParts[0] = valueParts[0].slice( valueParts[0].length - padLength );
+ }
+ }
+
+ // Add group separators
+ index = patternParts[0].lastIndexOf( ',' );
+
+ if ( index !== -1 ) {
+ groupSize = patternParts[0].length - index - 1;
+ remainder = patternParts[0].slice( 0, index );
+ index = remainder.lastIndexOf( ',' );
+ if ( index !== -1 ) {
+ groupSize2 = remainder.length - index - 1;
+ }
+ }
+
+ for ( whole = valueParts[0]; whole; ) {
+ off = groupSize ? whole.length - groupSize : 0;
+ pieces.push( ( off > 0 ) ? whole.slice( off ) : whole );
+ whole = ( off > 0 ) ? whole.slice( 0, off ) : '';
+
+ if ( groupSize2 ) {
+ groupSize = groupSize2;
+ groupSize2 = null;
+ }
+ }
+ valueParts[0] = pieces.reverse().join( options.group );
+
+ return valueParts.join( options.decimal );
+ }
+
+ $.extend( mw.language, {
+
+ /**
+ * Converts a number using #getDigitTransformTable.
+ *
+ * @param {number} num Value to be converted
+ * @param {boolean} [integer=false] Whether to convert the return value to an integer
+ * @return {number|string} Formatted number
+ */
+ convertNumber: function ( num, integer ) {
+ var i, tmp, transformTable, numberString, convertedNumber, pattern;
+
+ pattern = mw.language.getData( mw.config.get( 'wgUserLanguage' ),
+ 'digitGroupingPattern' ) || '#,##0.###';
+
+ // Set the target transform table:
+ transformTable = mw.language.getDigitTransformTable();
+
+ if ( !transformTable ) {
+ return num;
+ }
+
+ // Check if the 'restore' to Latin number flag is set:
+ if ( integer ) {
+ if ( parseInt( num, 10 ) === num ) {
+ return num;
+ }
+ tmp = [];
+ for ( i in transformTable ) {
+ tmp[ transformTable[ i ] ] = i;
+ }
+ transformTable = tmp;
+ numberString = num + '';
+ } else {
+ numberString = mw.language.commafy( num, pattern );
+ }
+
+ convertedNumber = '';
+ for ( i = 0; i < numberString.length; i++ ) {
+ if ( transformTable[ numberString[i] ] ) {
+ convertedNumber += transformTable[numberString[i]];
+ } else {
+ convertedNumber += numberString[i];
+ }
+ }
+ return integer ? parseInt( convertedNumber, 10 ) : convertedNumber;
+ },
+
+ /**
+ * Get the digit transform table for current UI language.
+ * @return {Object|Array}
+ */
+ getDigitTransformTable: function () {
+ return mw.language.getData( mw.config.get( 'wgUserLanguage' ),
+ 'digitTransformTable' ) || [];
+ },
+
+ /**
+ * Get the separator transform table for current UI language.
+ * @return {Object|Array}
+ */
+ getSeparatorTransformTable: function () {
+ return mw.language.getData( mw.config.get( 'wgUserLanguage' ),
+ 'separatorTransformTable' ) || [];
+ },
+
+ /**
+ * Apply pattern to format value as a string.
+ *
+ * Using patterns from [Unicode TR35](http://www.unicode.org/reports/tr35/#Number_Format_Patterns).
+ *
+ * @param {number} value
+ * @param {string} pattern Pattern string as described by Unicode TR35
+ * @throws {Error} If unable to find a number expression in `pattern`.
+ * @return {string}
+ */
+ commafy: function ( value, pattern ) {
+ var numberPattern,
+ transformTable = mw.language.getSeparatorTransformTable(),
+ group = transformTable[','] || ',',
+ numberPatternRE = /[#0,]*[#0](?:\.0*#*)?/, // not precise, but good enough
+ decimal = transformTable['.'] || '.',
+ patternList = pattern.split( ';' ),
+ positivePattern = patternList[0];
+
+ pattern = patternList[ ( value < 0 ) ? 1 : 0] || ( '-' + positivePattern );
+ numberPattern = positivePattern.match( numberPatternRE );
+
+ if ( !numberPattern ) {
+ throw new Error( 'unable to find a number expression in pattern: ' + pattern );
+ }
+
+ return pattern.replace( numberPatternRE, commafyNumber( value, numberPattern[0], {
+ decimal: decimal,
+ group: group
+ } ) );
+ }
+
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.legacy/ajax.js b/resources/src/mediawiki.legacy/ajax.js
new file mode 100644
index 00000000..6b9464a9
--- /dev/null
+++ b/resources/src/mediawiki.legacy/ajax.js
@@ -0,0 +1,194 @@
+/**
+ * Remote Scripting Library
+ * Copyright 2005 modernmethod, inc
+ * Under the open source BSD license
+ * http://www.modernmethod.com/sajax/
+ */
+
+/*jshint camelcase:false */
+/*global alert */
+( function ( mw ) {
+
+/**
+ * if sajax_debug_mode is true, this function outputs given the message into
+ * the element with id = sajax_debug; if no such element exists in the document,
+ * it is injected.
+ */
+function debug( text ) {
+ if ( !window.sajax_debug_mode ) {
+ return false;
+ }
+
+ var b, m,
+ e = document.getElementById( 'sajax_debug' );
+
+ if ( !e ) {
+ e = document.createElement( 'p' );
+ e.className = 'sajax_debug';
+ e.id = 'sajax_debug';
+
+ b = document.getElementsByTagName( 'body' )[0];
+
+ if ( b.firstChild ) {
+ b.insertBefore( e, b.firstChild );
+ } else {
+ b.appendChild( e );
+ }
+ }
+
+ m = document.createElement( 'div' );
+ m.appendChild( document.createTextNode( text ) );
+
+ e.appendChild( m );
+
+ return true;
+}
+
+/**
+ * Compatibility wrapper for creating a new XMLHttpRequest object.
+ */
+function createXhr() {
+ debug( 'sajax_init_object() called..' );
+ var a;
+ try {
+ // Try the new style before ActiveX so we don't
+ // unnecessarily trigger warnings in IE 7 when
+ // set to prompt about ActiveX usage
+ a = new XMLHttpRequest();
+ } catch ( xhrE ) {
+ try {
+ a = new window.ActiveXObject( 'Msxml2.XMLHTTP' );
+ } catch ( msXmlE ) {
+ try {
+ a = new window.ActiveXObject( 'Microsoft.XMLHTTP' );
+ } catch ( msXhrE ) {
+ a = null;
+ }
+ }
+ }
+ if ( !a ) {
+ debug( 'Could not create connection object.' );
+ }
+
+ return a;
+}
+
+/**
+ * Perform an AJAX call to MediaWiki. Calls are handled by AjaxDispatcher.php
+ * func_name - the name of the function to call. Must be registered in $wgAjaxExportList
+ * args - an array of arguments to that function
+ * target - the target that will handle the result of the call. If this is a function,
+ * if will be called with the XMLHttpRequest as a parameter; if it's an input
+ * element, its value will be set to the resultText; if it's another type of
+ * element, its innerHTML will be set to the resultText.
+ *
+ * Example:
+ * sajax_do_call( 'doFoo', [1, 2, 3], document.getElementById( 'showFoo' ) );
+ *
+ * This will call the doFoo function via MediaWiki's AjaxDispatcher, with
+ * (1, 2, 3) as the parameter list, and will show the result in the element
+ * with id = showFoo
+ */
+function doAjaxRequest( func_name, args, target ) {
+ var i, x, uri, post_data;
+ uri = mw.util.wikiScript() + '?action=ajax';
+ if ( window.sajax_request_type === 'GET' ) {
+ if ( uri.indexOf( '?' ) === -1 ) {
+ uri = uri + '?rs=' + encodeURIComponent( func_name );
+ } else {
+ uri = uri + '&rs=' + encodeURIComponent( func_name );
+ }
+ for ( i = 0; i < args.length; i++ ) {
+ uri = uri + '&rsargs[]=' + encodeURIComponent( args[i] );
+ }
+ //uri = uri + '&rsrnd=' + new Date().getTime();
+ post_data = null;
+ } else {
+ post_data = 'rs=' + encodeURIComponent( func_name );
+ for ( i = 0; i < args.length; i++ ) {
+ post_data = post_data + '&rsargs[]=' + encodeURIComponent( args[i] );
+ }
+ }
+ x = createXhr();
+ if ( !x ) {
+ alert( 'AJAX not supported' );
+ return false;
+ }
+
+ try {
+ x.open( window.sajax_request_type, uri, true );
+ } catch ( e ) {
+ if ( location.hostname === 'localhost' ) {
+ alert( 'Your browser blocks XMLHttpRequest to "localhost", try using a real hostname for development/testing.' );
+ }
+ throw e;
+ }
+ if ( window.sajax_request_type === 'POST' ) {
+ x.setRequestHeader( 'Method', 'POST ' + uri + ' HTTP/1.1' );
+ x.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
+ }
+ x.setRequestHeader( 'Pragma', 'cache=yes' );
+ x.setRequestHeader( 'Cache-Control', 'no-transform' );
+ x.onreadystatechange = function () {
+ if ( x.readyState !== 4 ) {
+ return;
+ }
+
+ debug( 'received (' + x.status + ' ' + x.statusText + ') ' + x.responseText );
+
+ //if ( x.status != 200 )
+ // alert( 'Error: ' + x.status + ' ' + x.statusText + ': ' + x.responseText );
+ //else
+
+ if ( typeof target === 'function' ) {
+ target( x );
+ } else if ( typeof target === 'object' ) {
+ if ( target.tagName === 'INPUT' ) {
+ if ( x.status === 200 ) {
+ target.value = x.responseText;
+ }
+ //else alert( 'Error: ' + x.status + ' ' + x.statusText + ' (' + x.responseText + ')' );
+ } else {
+ if ( x.status === 200 ) {
+ target.innerHTML = x.responseText;
+ } else {
+ target.innerHTML = '<div class="error">Error: ' + x.status +
+ ' ' + x.statusText + ' (' + x.responseText + ')</div>';
+ }
+ }
+ } else {
+ alert( 'Bad target for sajax_do_call: not a function or object: ' + target );
+ }
+ };
+
+ debug( func_name + ' uri = ' + uri + ' / post = ' + post_data );
+ x.send( post_data );
+ debug( func_name + ' waiting..' );
+
+ return true;
+}
+
+/**
+ * @return {boolean} Whether the browser supports AJAX
+ */
+function wfSupportsAjax() {
+ var request = createXhr(),
+ supportsAjax = request ? true : false;
+
+ request = undefined;
+ return supportsAjax;
+}
+
+// Expose + Mark as deprecated
+var deprecationNotice = 'Sajax is deprecated, use jQuery.ajax or mediawiki.api instead.';
+
+// Variables
+mw.log.deprecate( window, 'sajax_debug_mode', false, deprecationNotice );
+mw.log.deprecate( window, 'sajax_request_type', 'GET', deprecationNotice );
+// Methods
+mw.log.deprecate( window, 'sajax_debug', debug, deprecationNotice );
+mw.log.deprecate( window, 'sajax_init_object', createXhr, deprecationNotice );
+mw.log.deprecate( window, 'sajax_do_call', doAjaxRequest, deprecationNotice );
+mw.log.deprecate( window, 'wfSupportsAjax', wfSupportsAjax, deprecationNotice );
+
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki.legacy/commonPrint.css b/resources/src/mediawiki.legacy/commonPrint.css
new file mode 100644
index 00000000..830b02fa
--- /dev/null
+++ b/resources/src/mediawiki.legacy/commonPrint.css
@@ -0,0 +1,435 @@
+/**
+ * MediaWiki Print style sheet for CSS2-capable browsers.
+ * Copyright Gabriel Wicke, http://www.aulinx.de/
+ *
+ * Derived from the plone (http://plone.org/) styles
+ * Copyright Alexander Limi
+ */
+
+/* Thanks to A List Apart (http://alistapart.com/) for useful extras */
+
+/**
+ * Hide all the elements irrelevant for printing
+ */
+.noprint,
+div#jump-to-nav,
+.mw-jump,
+div.top,
+div#column-one,
+#colophon,
+.mw-editsection,
+.mw-editsection-like,
+.toctoggle,
+#toc.tochidden,
+div#f-poweredbyico,
+div#f-copyrightico,
+li#viewcount,
+li#about,
+li#disclaimer,
+li#mobileview,
+li#privacy,
+#footer-places,
+.mw-hidden-catlinks,
+tr.mw-metadata-show-hide-extended,
+span.mw-filepage-other-resolutions,
+#filetoc,
+.usermessage,
+.patrollink,
+#mw-navigation,
+#siteNotice {
+ display: none;
+}
+
+/**
+ * Pagination
+ */
+.wikitable, .thumb, img {
+ page-break-inside: avoid;
+}
+
+h2, h3, h4, h5, h6 {
+ page-break-after: avoid;
+}
+
+p {
+ widows: 3;
+ orphans: 3;
+}
+
+/**
+ * Generic HTML elements
+ */
+body {
+ background: white;
+ color: black;
+ margin: 0;
+ padding: 0;
+}
+
+ul {
+ list-style-type: square;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: bold;
+}
+
+dt {
+ font-weight: bold;
+}
+
+p {
+ margin: 1em 0;
+ line-height: 1.2em;
+}
+
+pre, .mw-code {
+ border: 1pt dashed black;
+ white-space: pre;
+ font-size: 8pt;
+ overflow: auto;
+ padding: 1em 0;
+ background: white;
+ color: black;
+}
+
+/**
+ * MediaWiki-specific elements
+ */
+#globalWrapper {
+ width: 100% !important;
+ min-width: 0 !important;
+}
+
+.mw-body {
+ background: white;
+ border: none !important;
+ padding: 0 !important;
+ margin: 0 !important;
+ direction: ltr;
+ color: black;
+}
+
+#column-content {
+ margin: 0 !important;
+}
+
+#column-content .mw-body {
+ padding: 1em;
+ margin: 0 !important;
+}
+
+#toc {
+ border: 1px solid #aaaaaa;
+ background-color: #f9f9f9;
+ padding: 5px;
+ display: -moz-inline-block;
+ display: inline-block;
+ display: table;
+ /* IE7 and earlier */
+ zoom: 1;
+ *display: inline;
+}
+
+#footer {
+ background: white;
+ color: black;
+ margin-top: 1em;
+ border-top: 1px solid #AAA;
+ direction: ltr;
+}
+
+img {
+ border: none;
+ vertical-align: middle;
+}
+
+/* math */
+span.texhtml {
+ font-family: serif;
+}
+
+/**
+ * Links
+ */
+a.stub,
+a.new {
+ color: #ba0000;
+ text-decoration: none;
+}
+
+a {
+ color: black !important;
+ background: none !important;
+ padding: 0 !important;
+}
+
+a:link, a:visited {
+ color: #520;
+ background: transparent;
+ text-decoration: underline;
+}
+
+/* Expand URLs for printing */
+.mw-body a.external.text:after,
+.mw-body a.external.autonumber:after {
+ content: " (" attr(href) ")";
+}
+
+/* Expand protocol-relative URLs for printing */
+.mw-body a.external.text[href^='//']:after,
+.mw-body a.external.autonumber[href^='//']:after {
+ content: " (https:" attr(href) ")";
+}
+
+/* MSIE/Win doesn't understand 'inherit' */
+a,
+a.external,
+a.new,
+a.stub {
+ color: black !important;
+ text-decoration: none !important;
+}
+
+/* Continue ... */
+a,
+a.external,
+a.new,
+a.stub {
+ color: inherit !important;
+ text-decoration: inherit !important;
+}
+
+/**
+ * Floating divs
+ */
+div.floatright {
+ float: right;
+ clear: right;
+ position: relative;
+ margin: 0.5em 0 0.8em 1.4em;
+}
+
+div.floatright p {
+ font-style: italic;
+}
+
+div.floatleft {
+ float: left;
+ clear: left;
+ position: relative;
+ margin: 0.5em 1.4em 0.8em 0;
+}
+
+div.floatleft p {
+ font-style: italic;
+}
+
+div.center {
+ text-align: center;
+}
+
+/**
+ * Thumbnails
+ */
+div.thumb {
+ border: none;
+ width: auto;
+ margin-top: 0.5em;
+ margin-bottom: 0.8em;
+ background-color: transparent;
+}
+
+div.thumbinner {
+ border: 1px solid #cccccc;
+ padding: 3px !important;
+ background-color: White;
+ font-size: 94%;
+ text-align: center;
+ overflow: hidden;
+}
+
+html .thumbimage {
+ border: 1px solid #cccccc;
+}
+
+html .thumbcaption {
+ border: none;
+ text-align: left;
+ line-height: 1.4em;
+ padding: 3px !important;
+ font-size: 94%;
+}
+
+div.magnify {
+ display: none;
+}
+
+/* @noflip */
+div.tright {
+ float: right;
+ clear: right;
+ margin: 0.5em 0 0.8em 1.4em;
+}
+
+/* @noflip */
+div.tleft {
+ float: left;
+ clear: left;
+ margin: 0.5em 1.4em 0.8em 0;
+}
+
+img.thumbborder {
+ border: 1px solid #dddddd;
+}
+
+/**
+ * Galleries (see shared.css for more info)
+ */
+li.gallerybox {
+ vertical-align: top;
+ display: inline-block;
+}
+
+ul.gallery, li.gallerybox {
+ zoom: 1;
+ *display: inline;
+}
+
+ul.gallery {
+ margin: 2px;
+ padding: 2px;
+ display: block;
+}
+
+li.gallerycaption {
+ font-weight: bold;
+ text-align: center;
+ display: block;
+ word-wrap: break-word;
+}
+
+li.gallerybox div.thumb {
+ text-align: center;
+ border: 1px solid #ccc;
+ margin: 2px;
+}
+
+div.gallerytext {
+ overflow: hidden;
+ font-size: 94%;
+ padding: 2px 4px;
+ word-wrap: break-word;
+}
+
+/**
+ * Diff rendering
+ */
+table.diff {
+ background: white;
+}
+
+td.diff-otitle {
+ background: #ffffff;
+}
+
+td.diff-ntitle {
+ background: #ffffff;
+}
+
+td.diff-addedline {
+ background: #ccffcc;
+ font-size: smaller;
+ border: solid 2px black;
+}
+
+td.diff-deletedline {
+ background: #ffffaa;
+ font-size: smaller;
+ border: dotted 2px black;
+}
+
+td.diff-context {
+ background: #eeeeee;
+ font-size: smaller;
+}
+
+.diffchange {
+ color: silver;
+ font-weight: bold;
+ text-decoration: underline;
+}
+
+/**
+ * Table rendering
+ * As on shared.css but with white background.
+ */
+table.wikitable,
+table.mw_metadata {
+ margin: 1em 0;
+ border: 1px #aaa solid;
+ background: white;
+ border-collapse: collapse;
+}
+
+table.wikitable > tr > th, table.wikitable > tr > td,
+table.wikitable > * > tr > th, table.wikitable > * > tr > td,
+.mw_metadata th, .mw_metadata td {
+ border: 1px #aaa solid;
+ padding: 0.2em;
+}
+
+table.wikitable > tr > th,
+table.wikitable > * > tr > th,
+.mw_metadata th {
+ text-align: center;
+ background: white;
+ font-weight: bold;
+}
+
+table.wikitable > caption,
+.mw_metadata caption {
+ font-weight: bold;
+}
+
+table.listing,
+table.listing td {
+ border: 1pt solid black;
+ border-collapse: collapse;
+}
+
+a.sortheader {
+ margin: 0 0.3em;
+}
+
+/**
+ * Categories
+ */
+.catlinks ul {
+ display: inline;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ list-style-type: none;
+ list-style-image: none;
+ vertical-align: middle !ie;
+}
+
+.catlinks li {
+ display: inline-block;
+ line-height: 1.15em;
+ padding: 0 .4em;
+ border-left: 1px solid #AAA;
+ margin: 0.1em 0;
+ zoom: 1;
+ display: inline !ie;
+}
+
+.catlinks li:first-child {
+ padding-left: .2em;
+ border-left: none;
+}
+
+.printfooter {
+ padding: 1em 0 1em 0;
+}
diff --git a/resources/src/mediawiki.legacy/images/ajax-loader.gif b/resources/src/mediawiki.legacy/images/ajax-loader.gif
new file mode 100644
index 00000000..72203fdd
--- /dev/null
+++ b/resources/src/mediawiki.legacy/images/ajax-loader.gif
Binary files differ
diff --git a/resources/src/mediawiki.legacy/images/checker.png b/resources/src/mediawiki.legacy/images/checker.png
new file mode 100644
index 00000000..3e9e3d09
--- /dev/null
+++ b/resources/src/mediawiki.legacy/images/checker.png
Binary files differ
diff --git a/resources/src/mediawiki.legacy/images/feed-icon.png b/resources/src/mediawiki.legacy/images/feed-icon.png
new file mode 100644
index 00000000..00f49f6c
--- /dev/null
+++ b/resources/src/mediawiki.legacy/images/feed-icon.png
Binary files differ
diff --git a/resources/src/mediawiki.legacy/images/feed-icon.svg b/resources/src/mediawiki.legacy/images/feed-icon.svg
new file mode 100644
index 00000000..6e5f570a
--- /dev/null
+++ b/resources/src/mediawiki.legacy/images/feed-icon.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 256 256"><defs><linearGradient x1=".085" y1=".085" x2=".915" y2=".915" id="a"><stop offset="0" stop-color="#E3702D"/><stop offset=".107" stop-color="#EA7D31"/><stop offset=".35" stop-color="#F69537"/><stop offset=".5" stop-color="#FB9E3A"/><stop offset=".702" stop-color="#EA7C31"/><stop offset=".887" stop-color="#DE642B"/><stop offset="1" stop-color="#D95B29"/></linearGradient></defs><rect width="256" height="256" rx="55" ry="55" fill="#CC5D15"/><rect width="246" height="246" rx="50" ry="50" x="5" y="5" fill="#F49C52"/><rect width="236" height="236" rx="47" ry="47" x="10" y="10" fill="url(#a)"/><circle cx="68" cy="189" r="24" fill="#FFF"/><path d="M160 213h-34a82 82 0 0 0-82-82v-34a116 116 0 0 1 116 116zM184 213a140 140 0 0 0-140-140v-35a175 175 0 0 1 175 175z" fill="#FFF"/></svg> \ No newline at end of file
diff --git a/resources/src/mediawiki.legacy/images/help-question-hover.gif b/resources/src/mediawiki.legacy/images/help-question-hover.gif
new file mode 100644
index 00000000..515138db
--- /dev/null
+++ b/resources/src/mediawiki.legacy/images/help-question-hover.gif
Binary files differ
diff --git a/resources/src/mediawiki.legacy/images/help-question.gif b/resources/src/mediawiki.legacy/images/help-question.gif
new file mode 100644
index 00000000..b4fc9c5b
--- /dev/null
+++ b/resources/src/mediawiki.legacy/images/help-question.gif
Binary files differ
diff --git a/resources/src/mediawiki.legacy/images/question.png b/resources/src/mediawiki.legacy/images/question.png
new file mode 100644
index 00000000..f7405d26
--- /dev/null
+++ b/resources/src/mediawiki.legacy/images/question.png
Binary files differ
diff --git a/resources/src/mediawiki.legacy/images/question.svg b/resources/src/mediawiki.legacy/images/question.svg
new file mode 100644
index 00000000..98fbe8dd
--- /dev/null
+++ b/resources/src/mediawiki.legacy/images/question.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="21.059" height="21.06"><path fill="#575757" d="M10.529 0c-5.814 0-10.529 4.714-10.529 10.529s4.715 10.53 10.529 10.53c5.816 0 10.529-4.715 10.529-10.53s-4.712-10.529-10.529-10.529zm-.002 16.767c-.861 0-1.498-.688-1.498-1.516 0-.862.637-1.534 1.498-1.534.828 0 1.5.672 1.5 1.534 0 .827-.672 1.516-1.5 1.516zm2.137-6.512c-.723.568-1 .931-1 1.739v.5h-2.205v-.603c0-1.517.449-2.136 1.154-2.688.707-.552 1.139-.845 1.139-1.637 0-.672-.414-1.051-1.24-1.051-.707 0-1.328.189-1.982.638l-1.051-1.807c.861-.604 1.93-1.034 3.342-1.034 1.912 0 3.516 1.051 3.516 3.066-.001 1.43-.794 2.188-1.673 2.877z"/></svg> \ No newline at end of file
diff --git a/resources/src/mediawiki.legacy/images/spinner.gif b/resources/src/mediawiki.legacy/images/spinner.gif
new file mode 100644
index 00000000..6146be4e
--- /dev/null
+++ b/resources/src/mediawiki.legacy/images/spinner.gif
Binary files differ
diff --git a/resources/src/mediawiki.legacy/oldshared.css b/resources/src/mediawiki.legacy/oldshared.css
new file mode 100644
index 00000000..d92d3bb9
--- /dev/null
+++ b/resources/src/mediawiki.legacy/oldshared.css
@@ -0,0 +1,489 @@
+/**
+ * oldshared.css
+ * This file contains CSS settings common to Wikistandard, Nostalgia and
+ * CologneBlue, the old pre-Monobook skins
+ */
+
+/* For clarity, explicitly state some recommendations from
+ * http://www.w3.org/TR/CSS21/sample.html to make sure the editsection links scale right
+ */
+
+h1 {
+ font-size: 2em;
+}
+
+h2 {
+ font-size: 1.5em;
+}
+
+h3 {
+ font-size: 1.17em;
+}
+
+h4 {
+ font-size: 1.11em;
+}
+
+h5 {
+ font-size: 1.05em;
+}
+
+h6 {
+ font-size: 1em;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: bolder;
+}
+
+/* Now the custom parts */
+
+#footer {
+ clear: both;
+}
+
+/* images */
+/* @noflip */
+div.floatright {
+ float: right;
+ clear: right;
+ margin: 0 0 1em 1em;
+}
+
+/* @noflip */
+div.floatright p {
+ font-style: italic;
+}
+
+/* @noflip */
+div.floatleft {
+ float: left;
+ clear: left;
+ margin: 0.3em 0.5em 0.5em 0;
+}
+
+/* @noflip */
+div.floatleft p {
+ font-style: italic;
+}
+
+/* table standards */
+table.rimage {
+ float: right;
+ margin-left: 1em;
+ margin-bottom: 1em;
+ text-align: center;
+ font-size: smaller;
+}
+
+/* thumbnails */
+div.thumb {
+ margin-bottom: .5em;
+ border-style: solid;
+ border-color: white;
+ width: auto;
+}
+
+div.thumbinner {
+ border: 1px solid #ccc;
+ padding: 3px;
+ background-color: #f9f9f9;
+ font-size: 94%;
+ text-align: center;
+ overflow: hidden;
+}
+
+html .thumbimage {
+ border: 1px solid #ccc;
+}
+
+html .thumbcaption {
+ border: none;
+ line-height: 1.4em;
+ padding: 3px;
+ font-size: 94%;
+ text-align: left;
+}
+
+div.magnify {
+ float: right;
+ margin-left: 3px;
+}
+
+div.magnify a {
+ display: block;
+ /* Hide the text… */
+ text-indent: 15px;
+ white-space: nowrap;
+ overflow: hidden;
+ /* …and replace it with the image */
+ width: 15px;
+ height: 11px;
+ /* @embed */
+ background: url(images/magnify-clip-ltr.png) center center no-repeat;
+ /* Don't annoy people who copy-paste everything too much */
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+/* @noflip */
+div.tright {
+ clear: right;
+ float: right;
+ border-width: .5em 0 .8em 1.4em;
+}
+
+/* @noflip */
+div.tleft {
+ float: left;
+ clear: left;
+ margin-right: .5em;
+ border-width: .5em 1.4em .8em 0;
+}
+
+img.thumbborder {
+ border: 1px solid #dddddd;
+}
+
+/* Page history styling */
+/* the auto-generated edit comments */
+.autocomment {
+ color: #4b4b4b;
+}
+
+img {
+ border: none;
+}
+
+#toc,
+.toc {
+ border: 1px solid #bba;
+ background-color: #f7f8ff;
+ padding: 5px;
+ font-size: 95%;
+ text-align: center;
+ display: -moz-inline-block;
+ display: inline-block;
+ display: table;
+
+ /* IE7 and earlier */
+ zoom: 1;
+ *display: inline;
+
+ padding: 7px;
+}
+
+/* CSS for backwards-compatibility with cached page renders and creative uses in wikitext */
+table#toc,
+table.toc {
+ border-collapse: collapse;
+}
+
+/* Remove additional paddings inside table-cells that are not present in <div>s */
+table#toc td,
+table.toc td {
+ padding: 0;
+}
+
+#toc h2,
+.toc h2 {
+ display: inline;
+ border: none;
+ padding: 0;
+ font-size: 100%;
+ font-weight: bold;
+}
+
+#toc ul,
+.toc ul {
+ list-style-type: none;
+ list-style-image: none;
+ padding: 0;
+ text-align: left;
+}
+
+#toc ul ul,
+.toc ul ul {
+ margin: 0 0 0 2em;
+}
+
+#toc .toctoggle,
+.toc .toctoggle {
+ font-size: 94%;
+}
+
+.error {
+ color: red;
+ font-size: larger;
+}
+
+/* preference page with js-genrated toc */
+#preftoc {
+ float: left;
+ margin: 1em 1em 1em 1em;
+ width: 13em;
+}
+
+#preftoc li {
+ border: 1px solid White;
+}
+
+#preftoc li.selected {
+ background-color: #f9f9f9;
+ border: 1px dashed #aaaaaa;
+}
+
+#preftoc a,
+#preftoc a:active {
+ display: block;
+ color: #005189;
+}
+
+.mw-prefs-buttons {
+ clear: left;
+ float: left;
+ margin-top: 1em;
+}
+
+div.htmlform-tip {
+ font-size: 94%;
+ margin-top: 0.4em;
+ color: #666;
+}
+
+fieldset.prefsection {
+ margin-top: 1em;
+}
+
+fieldset.operaprefsection {
+ margin-left: 15em;
+}
+
+/* emulate center */
+.center {
+ width: 100%;
+ text-align: center;
+}
+
+*.center * {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* small for tables and similar */
+.small {
+ font-size: 94%;
+}
+
+table.small {
+ font-size: 100%;
+}
+
+/* use this instead of #toc for page content */
+.toccolours {
+ border: 1px solid #aaaaaa;
+ background-color: #f9f9f9;
+ padding: 5px;
+ font-size: 95%;
+}
+
+#siteNotice {
+ border: 1px solid #aaaaaa;
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+
+.sharedUploadNotice {
+ font-style: italic;
+}
+
+span.unpatrolled {
+ font-weight: bold;
+ color: red;
+}
+
+span.updatedmarker {
+ color: black;
+ background-color: #00FF00;
+}
+
+div.gallerybox {
+ width: 150px;
+}
+
+span.comment {
+ font-style: italic;
+}
+
+span.changedby {
+ font-size: 95%;
+}
+
+.previewnote {
+ text-align: center;
+ color: #cc0000;
+}
+
+.editExternally {
+ border-style: solid;
+ border-width: 1px;
+ border-color: gray;
+ background: #ffffff;
+ padding: 3px;
+ margin-top: 0.5em;
+ float: left;
+ font-size: small;
+ text-align: center;
+}
+
+.editExternallyHelp {
+ font-style: italic;
+ color: gray;
+}
+
+li span.deleted {
+ text-decoration: line-through;
+ color: #888;
+ font-style: italic;
+}
+
+/* Classes for Exif data display */
+table.mw_metadata {
+ margin-left: 0.5em;
+}
+
+table.mw_metadata caption {
+ font-weight: bold;
+}
+
+table.mw_metadata th {
+ font-weight: normal;
+}
+
+table.mw_metadata td {
+ padding: 0.1em;
+}
+
+table.mw_metadata {
+ border: none;
+ border-collapse: collapse;
+}
+
+table.mw_metadata td,
+table.mw_metadata th {
+ border: 1px solid #aaaaaa;
+ padding-left: 4px;
+ padding-right: 4px;
+}
+
+table.mw_metadata th {
+ background-color: #f9f9f9;
+}
+
+table.mw_metadata td {
+ background-color: #fcfcfc;
+}
+
+table.mw_metadata td.spacer {
+ background: inherit;
+ border-top: none;
+ border-bottom: none;
+}
+
+.visualClear {
+ clear: both;
+}
+
+/* Allmessages table */
+#allmessagestable th {
+ background-color: #b2b2ff;
+}
+
+#allmessagestable tr.orig {
+ background-color: #ffe2e2;
+}
+
+#allmessagestable tr.new {
+ background-color: #e2ffe2;
+}
+
+#allmessagestable tr.def {
+ background-color: #f0f0ff;
+}
+
+#jump-to-nav {
+ display: none;
+}
+
+div.multipageimagenavbox {
+ border: solid 1px silver;
+ padding: 4px;
+ margin: 1em;
+ background: #f0f0f0;
+}
+
+div.multipageimagenavbox div.thumb {
+ border: none;
+ margin-left: 2em;
+ margin-right: 2em;
+}
+
+div.multipageimagenavbox hr {
+ margin: 6px;
+}
+
+table.multipageimage td {
+ text-align: center;
+}
+
+.templatesUsed {
+ margin-top: 1em;
+}
+
+.MediaTransformError {
+ border: thin solid #777;
+ background-color: #ccc;
+ padding: 0.1em;
+}
+
+.MediaTransformError td {
+ text-align: center;
+ vertical-align: middle;
+ font-size: 90%;
+}
+
+form#specialpages {
+ display: inline;
+}
+
+body {
+ direction: ltr;
+ unicode-bidi: embed;
+ background-color: #ffffec;
+}
+
+body.ns-0 {
+ background-color: white;
+}
+
+/** RTL specific CSS starts here **/
+
+/**
+ * Lists:
+ * The following lines don't have a visible effect on non-Gecko browsers
+ * They fix a problem with Gecko browsers rendering lists to the right of
+ * left-floated objects in an RTL layout.
+ */
+/* @noflip */
+html > body.rtl div#article ul {
+ display: table;
+}
+
+/* @noflip */
+html > body.rtl .mw-body ul#filetoc {
+ display: block;
+}
+
+/* RTL specific CSS ends here **/
diff --git a/resources/src/mediawiki.legacy/protect.js b/resources/src/mediawiki.legacy/protect.js
new file mode 100644
index 00000000..f9069b6f
--- /dev/null
+++ b/resources/src/mediawiki.legacy/protect.js
@@ -0,0 +1,240 @@
+( function ( mw, $ ) {
+
+var ProtectionForm = window.ProtectionForm = {
+ /**
+ * Set up the protection chaining interface (i.e. "unlock move permissions" checkbox)
+ * on the protection form
+ */
+ init: function () {
+ var $cell = $( '<td>' ), $row = $( '<tr>' ).append( $cell );
+
+ if ( !$( '#mwProtectSet' ).length ) {
+ return false;
+ }
+
+ if ( mw.config.get( 'wgCascadeableLevels' ) !== undefined ) {
+ $( 'form#mw-Protect-Form' ).submit( this.toggleUnchainedInputs.bind( ProtectionForm, true ) );
+ }
+ this.getExpirySelectors().each( function () {
+ $( this ).change( ProtectionForm.updateExpiryList.bind( ProtectionForm, this ) );
+ } );
+ this.getExpiryInputs().each( function () {
+ $( this ).on( 'keyup change', ProtectionForm.updateExpiry.bind( ProtectionForm, this ) );
+ } );
+ this.getLevelSelectors().each( function () {
+ $( this ).change( ProtectionForm.updateLevels.bind( ProtectionForm, this ) );
+ } );
+
+ $( '#mwProtectSet > tbody > tr:first' ).after( $row );
+
+ // If there is only one protection type, there is nothing to chain
+ if ( $( '[id ^= mw-protect-table-]' ).length > 1 ) {
+ $cell.append(
+ $( '<input>' )
+ .attr( { id: 'mwProtectUnchained', type: 'checkbox' } )
+ .click( this.onChainClick.bind( this ) )
+ .prop( 'checked', !this.areAllTypesMatching() ),
+ document.createTextNode( ' ' ),
+ $( '<label>' )
+ .attr( 'for', 'mwProtectUnchained' )
+ .text( mw.msg( 'protect-unchain-permissions' ) )
+ );
+
+ this.toggleUnchainedInputs( !this.areAllTypesMatching() );
+ }
+
+ $( '#mwProtect-reason' ).byteLimit( 180 );
+
+ this.updateCascadeCheckbox();
+ },
+
+ /**
+ * Sets the disabled attribute on the cascade checkbox depending on the current selected levels
+ */
+ updateCascadeCheckbox: function () {
+ this.getLevelSelectors().each( function () {
+ if ( !ProtectionForm.isCascadeableLevel( $( this ).val() ) ) {
+ $( '#mwProtect-cascade' ).prop( { checked: false, disabled: true } );
+ return false;
+ } else {
+ $( '#mwProtect-cascade' ).prop( 'disabled', false );
+ }
+ } );
+ },
+
+ /**
+ * Checks if a cerain protection level is cascadeable.
+ *
+ * @param {string} level
+ * @return {boolean}
+ */
+ isCascadeableLevel: function ( level ) {
+ return $.inArray( level, mw.config.get( 'wgCascadeableLevels' ) ) !== -1;
+ },
+
+ /**
+ * When protection levels are locked together, update the rest
+ * when one action's level changes
+ *
+ * @param {Element} source Level selector that changed
+ */
+ updateLevels: function ( source ) {
+ if ( !this.isUnchained() ) {
+ this.setAllSelectors( source.selectedIndex );
+ }
+ this.updateCascadeCheckbox();
+ },
+
+ /**
+ * When protection levels are locked together, update the
+ * expiries when one changes
+ *
+ * @param {Element} source expiry input that changed
+ */
+
+ updateExpiry: function ( source ) {
+ if ( !this.isUnchained() ) {
+ this.getExpiryInputs().each( function () {
+ this.value = source.value;
+ } );
+ }
+ if ( this.isUnchained() ) {
+ $( '#' + source.id.replace( /^mwProtect-(\w+)-expires$/, 'mwProtectExpirySelection-$1' ) ).val( 'othertime' );
+ } else {
+ this.getExpirySelectors().each( function () {
+ this.value = 'othertime';
+ } );
+ }
+ },
+
+ /**
+ * When protection levels are locked together, update the
+ * expiry lists when one changes and clear the custom inputs
+ *
+ * @param {Element} source Expiry selector that changed
+ */
+ updateExpiryList: function ( source ) {
+ if ( !this.isUnchained() ) {
+ this.getExpirySelectors().each( function () {
+ this.value = source.value;
+ } );
+ this.getExpiryInputs().each( function () {
+ this.value = '';
+ } );
+ }
+ },
+
+ /**
+ * Update chain status and enable/disable various bits of the UI
+ * when the user changes the "unlock move permissions" checkbox
+ */
+ onChainClick: function () {
+ this.toggleUnchainedInputs( this.isUnchained() );
+ if ( !this.isUnchained() ) {
+ this.setAllSelectors( this.getMaxLevel() );
+ }
+ this.updateCascadeCheckbox();
+ },
+
+ /**
+ * Returns true if the named attribute in all objects in the given array are matching
+ *
+ * @param {Object[]} objects
+ * @param {string} attrName
+ * @return {boolean}
+ */
+ matchAttribute: function ( objects, attrName ) {
+ return $.map( objects, function ( object ) {
+ return object[attrName];
+ } ).filter( function ( item, index, a ) {
+ return index === a.indexOf( item );
+ } ).length === 1;
+ },
+
+ /**
+ * Are all actions protected at the same level, with the same expiry time?
+ *
+ * @return {boolean}
+ */
+ areAllTypesMatching: function () {
+ return this.matchAttribute( this.getLevelSelectors(), 'selectedIndex' )
+ && this.matchAttribute( this.getExpirySelectors(), 'selectedIndex' )
+ && this.matchAttribute( this.getExpiryInputs(), 'value' );
+ },
+
+ /**
+ * Is protection chaining off?
+ *
+ * @return {boolean}
+ */
+ isUnchained: function () {
+ var element = document.getElementById( 'mwProtectUnchained' );
+ return element
+ ? element.checked
+ : true; // No control, so we need to let the user set both levels
+ },
+
+ /**
+ * Find the highest protection level in any selector
+ * @return {number}
+ */
+ getMaxLevel: function () {
+ return Math.max.apply( Math, this.getLevelSelectors().map( function () {
+ return this.selectedIndex;
+ } ) );
+ },
+
+ /**
+ * Protect all actions at the specified level
+ *
+ * @param {number} index Protection level
+ */
+ setAllSelectors: function ( index ) {
+ this.getLevelSelectors().each( function () {
+ this.selectedIndex = index;
+ } );
+ },
+
+ /**
+ * Get a list of all protection selectors on the page
+ *
+ * @return {jQuery}
+ */
+ getLevelSelectors: function () {
+ return $( 'select[id ^= mwProtect-level-]' );
+ },
+
+ /**
+ * Get a list of all expiry inputs on the page
+ *
+ * @return {jQuery}
+ */
+ getExpiryInputs: function () {
+ return $( 'input[id ^= mwProtect-][id $= -expires]' );
+ },
+
+ /**
+ * Get a list of all expiry selector lists on the page
+ *
+ * @return {jQuery}
+ */
+ getExpirySelectors: function () {
+ return $( 'select[id ^= mwProtectExpirySelection-]' );
+ },
+
+ /**
+ * Enable/disable protection selectors and expiry inputs
+ *
+ * @param {boolean} val Enable?
+ */
+ toggleUnchainedInputs: function ( val ) {
+ var setDisabled = function () { this.disabled = !val; };
+ this.getLevelSelectors().slice( 1 ).each( setDisabled );
+ this.getExpiryInputs().slice( 1 ).each( setDisabled );
+ this.getExpirySelectors().slice( 1 ).each( setDisabled );
+ }
+};
+
+$( ProtectionForm.init.bind( ProtectionForm ) );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.legacy/shared.css b/resources/src/mediawiki.legacy/shared.css
new file mode 100644
index 00000000..0604773e
--- /dev/null
+++ b/resources/src/mediawiki.legacy/shared.css
@@ -0,0 +1,1167 @@
+/**
+ * CSS in this file is used by *all* skins (that have any CSS at all). Be
+ * careful what you put in here, since what looks good in one skin may not in
+ * another, but don't ignore the poor pre-Monobook users either.
+ */
+
+/* GENERAL CLASSES FOR DIRECTIONALITY SUPPORT */
+
+/**
+ * These classes should be used for text depending on the content direction.
+ * Content stuff like editsection, ul/ol and TOC depend on this.
+ */
+.mw-content-ltr {
+ /* @noflip */
+ direction: ltr;
+}
+
+.mw-content-rtl {
+ /* @noflip */
+ direction: rtl;
+}
+
+/* Most input fields should be in site direction */
+.sitedir-ltr textarea,
+.sitedir-ltr input {
+ /* @noflip */
+ direction: ltr;
+}
+
+.sitedir-rtl textarea,
+.sitedir-rtl input {
+ /* @noflip */
+ direction: rtl;
+}
+
+.mw-userlink {
+ unicode-bidi: embed;
+}
+
+/* User-Agent styles for new HTML5 elements */
+mark {
+ background-color: yellow;
+ color: black;
+}
+
+/* Helper for wbr element on IE 8+; in HTML5, but not supported by default as of IE 11. */
+/* Note canonical HTML5 styles recommend "content: \u200B", but this doesn't work as of IE 11. */
+wbr {
+ display: inline-block;
+}
+
+/* Input types that should follow user direction, like buttons */
+/* TODO: What about buttons in wikipage content ? */
+input[type="submit"],
+input[type="button"],
+input[type="reset"],
+input[type="file"] {
+ direction: ltr;
+}
+
+/* Override default values */
+textarea[dir="ltr"],
+input[dir="ltr"] {
+ /* @noflip */
+ direction: ltr;
+}
+
+textarea[dir="rtl"],
+input[dir="rtl"] {
+ /* @noflip */
+ direction: rtl;
+}
+
+/* Default style for semantic tags */
+abbr[title],
+.explain[title] {
+ border-bottom: 1px dotted;
+ cursor: help;
+}
+
+/* Colored watchlist and recent changes numbers */
+.mw-plusminus-pos {
+ color: #006400; /* dark green */
+}
+
+.mw-plusminus-neg {
+ color: #8b0000; /* dark red */
+}
+
+.mw-plusminus-null {
+ color: #aaa; /* gray */
+}
+
+/**
+ * Links to redirects appear italicized on [[Special:AllPages]], [[Special:PrefixIndex]],
+ * [[Special:Watchlist/edit]] and in category listings.
+ */
+.allpagesredirect,
+.redirect-in-category,
+.watchlistredir {
+ font-style: italic;
+}
+
+/* Comment and username portions of RC entries */
+span.comment {
+ font-style: italic;
+}
+
+span.changedby {
+ font-size: 95%;
+}
+
+/* Math */
+.texvc {
+ direction: ltr;
+ unicode-bidi: embed;
+}
+
+img.tex {
+ vertical-align: middle;
+}
+
+span.texhtml {
+ font-family: serif;
+}
+
+/**
+ * Add a bit of margin space between the preview and the toolbar.
+ * This replaces the ugly <p><br /></p> we used to insert into the page source
+ */
+#wikiPreview.ontop {
+ margin-bottom: 1em;
+}
+
+/* Stop floats from intruding into edit area in previews */
+#editform,
+#toolbar,
+#wpTextbox1 {
+ clear: both;
+}
+
+/**
+ * File description page
+ */
+
+div.mw-filepage-resolutioninfo {
+ font-size: smaller;
+}
+
+/**
+ * File histories
+ */
+h2#filehistory {
+ clear: both;
+}
+
+table.filehistory th,
+table.filehistory td {
+ vertical-align: top;
+}
+
+table.filehistory th {
+ text-align: left;
+}
+
+table.filehistory td.mw-imagepage-filesize,
+table.filehistory th.mw-imagepage-filesize {
+ white-space: nowrap;
+}
+
+table.filehistory td.filehistory-selected {
+ font-weight: bold;
+}
+
+/**
+ * Add a checkered background image on hover for file
+ * description pages. (bug 26470)
+ */
+.filehistory a img,
+#file img:hover {
+ /* @embed */
+ background: white url(images/checker.png) repeat;
+}
+
+/**
+ * rev_deleted stuff
+ */
+li span.deleted,
+span.history-deleted {
+ text-decoration: line-through;
+ color: #888;
+ font-style: italic;
+}
+
+/**
+ * Patrol stuff
+ */
+.not-patrolled {
+ background-color: #ffa;
+}
+
+.unpatrolled {
+ font-weight: bold;
+ color: red;
+}
+
+div.patrollink {
+ font-size: 75%;
+ text-align: right;
+}
+
+/**
+ * Forms
+ */
+td.mw-label {
+ text-align: right;
+}
+
+td.mw-input {
+ text-align: left;
+}
+
+td.mw-submit {
+ text-align: left;
+}
+
+td.mw-label {
+ vertical-align: top;
+}
+
+.prefsection td.mw-label {
+ width: 20%;
+}
+
+.prefsection table {
+ width: 100%;
+}
+
+.prefsection table.mw-htmlform-matrix {
+ width: auto;
+}
+
+.mw-icon-question {
+ /* SVG support using a transparent gradient to guarantee cross-browser
+ * compatibility (browsers able to understand gradient syntax support also SVG).
+ * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique */
+ background-image: url(images/question.png);
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/question.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/question.svg);
+ background-repeat: no-repeat;
+ background-size: 13px 13px;
+ display: inline-block;
+ height: 13px;
+ width: 13px;
+ margin-left: 4px;
+}
+
+.mw-icon-question:lang(ar),
+.mw-icon-question:lang(fa),
+.mw-icon-question:lang(ur) {
+ -webkit-transform: scaleX(-1);
+ -ms-transform: scaleX(-1);
+ transform: scaleX(-1);
+}
+
+td.mw-submit {
+ white-space: nowrap;
+}
+
+table.mw-htmlform-nolabel td.mw-label {
+ width: 1px;
+}
+
+tr.mw-htmlform-vertical-label td.mw-label {
+ text-align: left !important;
+}
+
+.mw-htmlform-invalid-input td.mw-input input {
+ border-color: red;
+}
+
+.mw-htmlform-flatlist div.mw-htmlform-flatlist-item {
+ display: inline;
+ margin-right: 1em;
+ white-space: nowrap;
+}
+
+.mw-htmlform-matrix td {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+
+input#wpSummary {
+ width: 80%;
+ margin-bottom: 1em;
+}
+
+/**
+ * Image captions.
+ *
+ * This is only meant to provide the most basic of styles, visual settings shouldn't be added here.
+ */
+
+/* @noflip */
+.mw-content-ltr .thumbcaption {
+ text-align: left;
+}
+
+/* @noflip */
+.mw-content-ltr .magnify {
+ float: right;
+}
+
+/* @noflip */
+.mw-content-rtl .thumbcaption {
+ text-align: right;
+}
+
+/* @noflip */
+.mw-content-rtl .magnify {
+ float: left;
+}
+
+/**
+ * Categories
+ */
+#catlinks {
+ /**
+ * Overrides text justification (user preference)
+ * See bug 31990
+ */
+ text-align: left;
+}
+
+.catlinks ul {
+ display: inline;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ list-style-type: none;
+ list-style-image: none;
+ vertical-align: middle !ie;
+}
+
+.catlinks li {
+ display: inline-block;
+ line-height: 1.25em;
+ border-left: 1px solid #AAA;
+ margin: 0.125em 0;
+ padding: 0 0.5em;
+ zoom: 1;
+ display: inline !ie;
+}
+
+.catlinks li:first-child {
+ padding-left: 0.25em;
+ border-left: none;
+}
+
+/* (bug 5346) make category redirects italic */
+.catlinks li a.mw-redirect {
+ font-style: italic;
+}
+
+/**
+ * Hidden categories
+ */
+.mw-hidden-cats-hidden {
+ display: none;
+}
+
+.catlinks-allhidden {
+ display: none;
+}
+
+/**
+ * Convenience links to edit block, delete and protect reasons
+ * and upload licenses
+ */
+p.mw-ipb-conveniencelinks,
+p.mw-protect-editreasons,
+p.mw-filedelete-editreasons,
+p.mw-delete-editreasons,
+p.mw-revdel-editreasons,
+p.mw-upload-editlicenses {
+ font-size: 90%;
+ text-align: right;
+}
+
+/* Page history styling */
+
+/* The auto-generated edit comments */
+.autocomment {
+ color: gray;
+}
+
+#pagehistory .history-user {
+ margin-left: 0.4em;
+ margin-right: 0.2em;
+}
+
+#pagehistory span.minor {
+ font-weight: bold;
+}
+
+#pagehistory li {
+ border: 1px solid white;
+}
+
+#pagehistory li.selected {
+ background-color: #f9f9f9;
+ border: 1px dashed #aaa;
+}
+
+.mw-history-revisiondelete-button, #mw-fileduplicatesearch-icon {
+ float: right;
+}
+
+/** Generic minor/bot/newpage styling (recent changes) */
+.newpage,
+.minoredit,
+.botedit {
+ font-weight: bold;
+}
+
+#shared-image-dup,
+#shared-image-conflict {
+ font-style: italic;
+}
+
+/**
+ * Recreating deleted page warning
+ * Reupload file warning
+ * Page protection warning
+ * incl. log entries for these warnings
+ */
+div.mw-warning-with-logexcerpt {
+ padding: 3px;
+ margin-bottom: 3px;
+ border: 2px solid #2F6FAB;
+ clear: both;
+}
+
+div.mw-warning-with-logexcerpt ul li {
+ font-size: 90%;
+}
+
+/* (show/hide) revision deletion links */
+span.mw-revdelundel-link,
+strong.mw-revdelundel-link {
+ font-size: 90%;
+}
+
+span.mw-revdelundel-hidden,
+input.mw-revdelundel-hidden {
+ visibility: hidden;
+}
+
+td.mw-revdel-checkbox,
+th.mw-revdel-checkbox {
+ padding-right: 10px;
+ text-align: center;
+}
+
+/* red links; see bug 36276 */
+a.new {
+ color: #BA0000;
+}
+
+/* feed links */
+a.feedlink {
+ /* SVG support using a transparent gradient to guarantee cross-browser
+ * compatibility (browsers able to understand gradient syntax support also SVG).
+ * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique */
+ background-image: url(images/feed-icon.png);
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/feed-icon.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/feed-icon.svg);
+ background-position: center left;
+ background-repeat: no-repeat;
+ background-size: 12px 12px;
+ padding-left: 16px;
+}
+
+/* Plainlinks - this can be used to switch
+ * off special external link styling */
+.plainlinks a.external {
+ background: none !important;
+ padding: 0 !important;
+}
+
+/* External URLs should always be treated as LTR (bug 4330) */
+/* @noflip */ .rtl a.external.free,
+.rtl a.external.autonumber {
+ direction: ltr;
+ unicode-bidi: embed;
+}
+
+/**
+ * wikitable class for skinning normal tables
+ * keep in sync with commonPrint.css
+ */
+table.wikitable {
+ margin: 1em 0;
+ background-color: #f9f9f9;
+ border: 1px #aaa solid;
+ border-collapse: collapse;
+ color: black;
+}
+
+table.wikitable > tr > th,
+table.wikitable > tr > td,
+table.wikitable > * > tr > th,
+table.wikitable > * > tr > td {
+ border: 1px #aaa solid;
+ padding: 0.2em;
+}
+
+table.wikitable > tr > th,
+table.wikitable > * > tr > th {
+ background-color: #f2f2f2;
+ text-align: center;
+}
+
+table.wikitable > caption {
+ font-weight: bold;
+}
+
+/* success and error messages */
+.error,
+.warning,
+.success {
+ font-size: larger;
+}
+
+.error {
+ color: #cc0000;
+}
+
+.warning {
+ color: #705000;
+}
+
+.success {
+ color: #009000;
+}
+
+.errorbox,
+.warningbox,
+.successbox {
+ border: 1px solid;
+ padding: .5em 1em;
+ margin-bottom: 1em;
+ display: -moz-inline-block;
+ display: inline-block;
+ zoom: 1;
+ *display: inline;
+}
+
+.errorbox h2,
+.warningbox h2,
+.successbox h2 {
+ font-size: 1em;
+ color: inherit;
+ font-weight: bold;
+ display: inline;
+ margin: 0 .5em 0 0;
+ border: none;
+}
+
+.errorbox {
+ color: #cc0000;
+ border-color: #fac5c5;
+ background-color: #fae3e3;
+}
+
+.warningbox {
+ color: #705000;
+ border-color: #fde29b;
+ background-color: #fdf1d1;
+}
+
+.successbox {
+ color: #009000;
+ border-color: #b7fdb5;
+ background-color: #e1fddf;
+}
+
+/* general info/warning box for SP */
+.mw-infobox {
+ border: 2px solid #ff7f00;
+ margin: 0.5em;
+ clear: left;
+ overflow: hidden;
+}
+
+.mw-infobox-left {
+ margin: 7px;
+ float: left;
+ width: 35px;
+}
+
+.mw-infobox-right {
+ margin: 0.5em 0.5em 0.5em 49px;
+}
+
+/* Note on preview page */
+.previewnote {
+ color: #c00;
+ margin-bottom: 1em;
+}
+
+.previewnote p {
+ text-indent: 3em;
+ margin: 0.8em 0;
+}
+
+.visualClear {
+ clear: both;
+}
+
+/**
+ * Data table style
+ *
+ * Transparent table with suddle borders
+ * and blue row-highlighting.
+ */
+.mw-datatable {
+ border-collapse: collapse;
+}
+
+.mw-datatable,
+.mw-datatable td,
+.mw-datatable th {
+ border: 1px solid #aaaaaa;
+ padding: 0 0.15em 0 0.15em;
+}
+
+.mw-datatable th {
+ background-color: #ddddff;
+}
+
+.mw-datatable td {
+ background-color: #ffffff;
+}
+
+.mw-datatable tr:hover td {
+ background-color: #eeeeff;
+}
+
+/* filetoc */
+ul#filetoc {
+ text-align: center;
+ border: 1px solid #aaaaaa;
+ background-color: #f9f9f9;
+ padding: 5px;
+ font-size: 95%;
+ margin-bottom: 0.5em;
+ margin-left: 0;
+ margin-right: 0;
+}
+
+#filetoc li {
+ display: inline;
+ list-style-type: none;
+ padding-right: 2em;
+}
+
+/* Classes for Exif data display */
+table.mw_metadata {
+ font-size: 0.8em;
+ margin-left: 0.5em;
+ margin-bottom: 0.5em;
+ width: 400px;
+}
+
+table.mw_metadata caption {
+ font-weight: bold;
+}
+
+table.mw_metadata th {
+ font-weight: normal;
+}
+
+table.mw_metadata td {
+ padding: 0.1em;
+}
+
+table.mw_metadata {
+ border: none;
+ border-collapse: collapse;
+}
+
+table.mw_metadata td,
+table.mw_metadata th {
+ text-align: center;
+ border: 1px solid #aaaaaa;
+ padding-left: 5px;
+ padding-right: 5px;
+}
+
+table.mw_metadata th {
+ background-color: #f9f9f9;
+}
+
+table.mw_metadata td {
+ background-color: #fcfcfc;
+}
+
+table.mw_metadata ul.metadata-langlist {
+ list-style-type: none;
+ list-style-image: none;
+ padding-right: 5px;
+ padding-left: 5px;
+ margin: 0;
+}
+
+/* Correct directionality when page dir is different from site/user dir */
+.mw-content-ltr ul,
+.mw-content-rtl .mw-content-ltr ul {
+ /* @noflip */
+ margin: 0.3em 0 0 1.6em;
+ padding: 0;
+}
+
+.mw-content-rtl ul,
+.mw-content-ltr .mw-content-rtl ul {
+ /* @noflip */
+ margin: 0.3em 1.6em 0 0;
+ padding: 0;
+}
+
+.mw-content-ltr ol,
+.mw-content-rtl .mw-content-ltr ol {
+ /* @noflip */
+ margin: 0.3em 0 0 3.2em;
+ padding: 0;
+}
+
+.mw-content-rtl ol,
+.mw-content-ltr .mw-content-rtl ol {
+ /* @noflip */
+ margin: 0.3em 3.2em 0 0;
+ padding: 0;
+}
+
+/* @noflip */
+.mw-content-ltr dd,
+.mw-content-rtl .mw-content-ltr dd {
+ margin-left: 1.6em;
+ margin-right: 0;
+}
+
+/* @noflip */
+.mw-content-rtl dd,
+.mw-content-ltr .mw-content-rtl dd {
+ margin-right: 1.6em;
+ margin-left: 0;
+}
+
+/* Galleries */
+/* These display attributes look nonsensical, but are needed to support IE and FF2 */
+/* Don't forget to update commonPrint.css */
+li.gallerybox {
+ vertical-align: top;
+ display: -moz-inline-box;
+ display: inline-block;
+}
+
+ul.gallery,
+li.gallerybox {
+ zoom: 1;
+ *display: inline;
+}
+
+ul.gallery {
+ margin: 2px;
+ padding: 2px;
+ display: block;
+}
+
+li.gallerycaption {
+ font-weight: bold;
+ text-align: center;
+ display: block;
+ word-wrap: break-word;
+}
+
+li.gallerybox div.thumb {
+ text-align: center;
+ border: 1px solid #ccc;
+ background-color: #f9f9f9;
+ margin: 2px;
+}
+
+li.gallerybox div.thumb img {
+ display: block;
+ margin: 0 auto;
+}
+
+div.gallerytext {
+ overflow: hidden;
+ font-size: 94%;
+ padding: 2px 4px;
+ word-wrap: break-word;
+}
+
+/* new gallery stuff */
+ul.mw-gallery-nolines li.gallerybox div.thumb {
+ background-color: transparent;
+ border: none;
+}
+
+ul.mw-gallery-nolines li.gallerybox div.gallerytext {
+ text-align: center;
+}
+
+/* height constrained gallery */
+
+ul.mw-gallery-packed li.gallerybox div.thumb,
+ul.mw-gallery-packed-overlay li.gallerybox div.thumb,
+ul.mw-gallery-packed-hover li.gallerybox div.thumb {
+ background-color: transparent;
+ border: none;
+}
+
+ul.mw-gallery-packed li.gallerybox div.thumb img,
+ul.mw-gallery-packed-overlay li.gallerybox div.thumb img,
+ul.mw-gallery-packed-hover li.gallerybox div.thumb img {
+ margin: 0 auto;
+}
+
+ul.mw-gallery-packed-hover li.gallerybox,
+ul.mw-gallery-packed-overlay li.gallerybox {
+ position: relative;
+}
+
+ul.mw-gallery-packed-hover div.gallerytextwrapper {
+ overflow: hidden;
+ height: 0;
+}
+
+ul.mw-gallery-packed-hover li.gallerybox:hover div.gallerytextwrapper,
+ul.mw-gallery-packed-overlay li.gallerybox div.gallerytextwrapper,
+ul.mw-gallery-packed-hover li.gallerybox.mw-gallery-focused div.gallerytextwrapper {
+ position: absolute;
+ background: white;
+ background: rgba(255, 255, 255, 0.8);
+ padding: 5px 10px;
+ bottom: 0;
+ left: 0; /* Needed for IE */
+ height: auto;
+ font-weight: bold;
+ margin: 2px; /* correspond to style on div.thumb */
+}
+
+ul.mw-gallery-packed-hover,
+ul.mw-gallery-packed-overlay,
+ul.mw-gallery-packed {
+ text-align: center;
+}
+
+.mw-ajax-loader {
+ /* @embed */
+ background-image: url(images/ajax-loader.gif);
+ background-position: center center;
+ background-repeat: no-repeat;
+ padding: 16px;
+ position: relative;
+ top: -16px;
+}
+
+.mw-small-spinner {
+ padding: 10px !important;
+ margin-right: 0.6em;
+ /* @embed */
+ background-image: url(images/spinner.gif);
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+
+/* Language specific height correction for titles. Ref Bug 29405 and Bug 30809 */
+/* Languages like hi or ml require slightly more vertical space to show diacritics properly */
+h1:lang(anp),
+h1:lang(as),
+h1:lang(bh), /* Macrolanguage, used on bh.wikipedia.org, should be removed one day */
+h1:lang(bho),
+h1:lang(bn),
+h1:lang(gu),
+h1:lang(hi),
+h1:lang(kn),
+h1:lang(ks),
+h1:lang(ml),
+h1:lang(mr),
+h1:lang(my),
+h1:lang(mai),
+h1:lang(ne),
+h1:lang(new),
+h1:lang(or),
+h1:lang(pa),
+h1:lang(pi),
+h1:lang(sa),
+h1:lang(ta),
+h1:lang(te) {
+ line-height: 1.6em !important;
+}
+
+h2:lang(anp), h3:lang(anp), h4:lang(anp), h5:lang(anp), h6:lang(anp),
+h2:lang(as), h3:lang(as), h4:lang(as), h5:lang(as), h6:lang(as),
+h2:lang(bho), h3:lang(bho), h4:lang(bho), h5:lang(bho), h6:lang(bho),
+h2:lang(bh), h3:lang(bh), h4:lang(bh), h5:lang(bh), h6:lang(bh),
+h2:lang(bn), h3:lang(bn), h4:lang(bn), h5:lang(bn), h6:lang(bn),
+h2:lang(gu), h3:lang(gu), h4:lang(gu), h5:lang(gu), h6:lang(gu),
+h2:lang(hi), h3:lang(hi), h4:lang(hi), h5:lang(hi), h6:lang(hi),
+h2:lang(kn), h3:lang(kn), h4:lang(kn), h5:lang(kn), h6:lang(kn),
+h2:lang(ks), h3:lang(ks), h4:lang(ks), h5:lang(ks), h6:lang(ks),
+h2:lang(ml), h3:lang(ml), h4:lang(ml), h5:lang(ml), h6:lang(ml),
+h2:lang(mr), h3:lang(mr), h4:lang(mr), h5:lang(mr), h6:lang(mr),
+h2:lang(my), h3:lang(my), h4:lang(my), h5:lang(my), h6:lang(my),
+h2:lang(mai), h3:lang(mai), h4:lang(mai), h5:lang(mai), h6:lang(mai),
+h2:lang(ne), h3:lang(ne), h4:lang(ne), h5:lang(ne), h6:lang(ne),
+h2:lang(new), h3:lang(new), h4:lang(new), h5:lang(new), h6:lang(new),
+h2:lang(or), h3:lang(or), h4:lang(or), h5:lang(or), h6:lang(or),
+h2:lang(pa), h3:lang(pa), h4:lang(pa), h5:lang(pa), h6:lang(pa),
+h2:lang(pi), h3:lang(pi), h4:lang(pi), h5:lang(pi), h6:lang(pi),
+h2:lang(sa), h3:lang(sa), h4:lang(sa), h5:lang(sa), h6:lang(sa),
+h2:lang(ta), h3:lang(ta), h4:lang(ta), h5:lang(ta), h6:lang(ta),
+h2:lang(te), h3:lang(te), h4:lang(te), h5:lang(te), h6:lang(te) {
+ line-height: 1.2em;
+}
+
+/* Localised ordered list numbering for some languages */
+ol:lang(bcc) li,
+ol:lang(bqi) li,
+ol:lang(fa) li,
+ol:lang(glk) li,
+ol:lang(kk-arab) li,
+ol:lang(mzn) li {
+ list-style-type: -moz-persian;
+ list-style-type: persian;
+}
+
+ol:lang(ckb) li {
+ list-style-type: -moz-arabic-indic;
+ list-style-type: arabic-indic;
+}
+
+ol:lang(hi) li,
+ol:lang(mr) li {
+ list-style-type: -moz-devanagari;
+ list-style-type: devanagari;
+}
+
+ol:lang(as) li,
+ol:lang(bn) li {
+ list-style-type: -moz-bengali;
+ list-style-type: bengali;
+}
+
+ol:lang(or) li {
+ list-style-type: -moz-oriya;
+ list-style-type: oriya;
+}
+
+#toc ul, .toc ul {
+ margin: .3em 0;
+}
+
+/* Correct directionality when page dir is different from site/user dir */
+/* @noflip */ .mw-content-ltr .toc ul,
+.mw-content-ltr #toc ul,
+.mw-content-rtl .mw-content-ltr .toc ul,
+.mw-content-rtl .mw-content-ltr #toc ul {
+ text-align: left;
+}
+
+/* @noflip */ .mw-content-rtl .toc ul,
+.mw-content-rtl #toc ul,
+.mw-content-ltr .mw-content-rtl .toc ul,
+.mw-content-ltr .mw-content-rtl #toc ul {
+ text-align: right;
+}
+
+/* @noflip */ .mw-content-ltr .toc ul ul,
+.mw-content-ltr #toc ul ul,
+.mw-content-rtl .mw-content-ltr .toc ul ul,
+.mw-content-rtl .mw-content-ltr #toc ul ul {
+ margin: 0 0 0 2em;
+}
+
+/* @noflip */ .mw-content-rtl .toc ul ul,
+.mw-content-rtl #toc ul ul,
+.mw-content-ltr .mw-content-rtl .toc ul ul,
+.mw-content-ltr .mw-content-rtl #toc ul ul {
+ margin: 0 2em 0 0;
+}
+
+#toc #toctitle,
+.toc #toctitle,
+#toc .toctitle,
+.toc .toctitle {
+ direction: ltr;
+}
+
+/* tooltip styles */
+.mw-help-field-hint {
+ display: none;
+ margin-left: 2px;
+ margin-bottom: -8px;
+ padding: 0 0 0 15px;
+ /* @embed */
+ background-image: url(images/help-question.gif);
+ background-position: left center;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ font-size: .8em;
+ text-decoration: underline;
+ color: #0645ad;
+}
+
+.mw-help-field-hint:hover {
+ /* @embed */
+ background-image: url(images/help-question-hover.gif);
+}
+
+.mw-help-field-data {
+ display: block;
+ background-color: #d6f3ff;
+ padding: 5px 8px 4px 8px;
+ border: 1px solid #5dc9f4;
+ margin-left: 20px;
+}
+
+#mw-clearyourcache,
+#mw-sitecsspreview,
+#mw-sitejspreview,
+#mw-usercsspreview,
+#mw-userjspreview {
+ direction: ltr;
+ unicode-bidi: embed;
+}
+
+/* Correct user & content directionality when viewing a diff */
+.diff-currentversion-title,
+.diff {
+ direction: ltr;
+ unicode-bidi: embed;
+}
+
+/* @noflip */ .diff-contentalign-right td {
+ direction: rtl;
+ unicode-bidi: embed;
+}
+
+/* @noflip */ .diff-contentalign-left td {
+ direction: ltr;
+ unicode-bidi: embed;
+}
+
+.diff-multi,
+.diff-otitle,
+.diff-ntitle,
+.diff-lineno {
+ direction: ltr !important;
+ unicode-bidi: embed;
+}
+
+#mw-revision-info,
+#mw-revision-info-current,
+#mw-revision-nav {
+ direction: ltr;
+ display: inline;
+}
+
+/* Images */
+
+/* @noflip */ div.tright,
+div.floatright,
+table.floatright {
+ clear: right;
+ float: right;
+}
+
+/* @noflip */ div.tleft,
+div.floatleft,
+table.floatleft {
+ float: left;
+ clear: left;
+}
+
+div.floatright,
+table.floatright,
+div.floatleft,
+table.floatleft {
+ position: relative;
+}
+
+/* bug 12205 */
+#mw-credits a {
+ unicode-bidi: embed;
+}
+
+/* Accessibility */
+.mw-jump,
+#jump-to-nav {
+ overflow: hidden;
+ height: 0;
+ zoom: 1; /* http://webaim.org/techniques/skipnav/#iequirk */
+}
+
+/* Print footer should be hidden by default in screen. */
+.printfooter {
+ display: none;
+}
+
+/* For developers */
+.xdebug-error {
+ position: absolute;
+ z-index: 99;
+}
+
+.mw-editsection,
+.toctoggle,
+#jump-to-nav {
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+/* Display editsection links smaller and next to headings */
+.mw-editsection,
+.mw-editsection-like {
+ font-size: small;
+ font-weight: normal;
+ margin-left: 1em;
+ vertical-align: baseline;
+ /* Reset line-height; headings tend to have it set to larger values */
+ line-height: 1em;
+ /* As .mw-editsection is a <span> (inline element), it is treated as part */
+ /* of the heading content when selecting text by multiple clicks and thus */
+ /* selected together with heading content, despite the user-select: none; */
+ /* rule set above. This enforces non-selection without changing the look. */
+ display: inline-block;
+}
+
+/* Correct directionality when page dir is different from site/user dir */
+/* @noflip */
+.mw-content-ltr .mw-editsection,
+.mw-content-rtl .mw-content-ltr .mw-editsection {
+ margin-left: 1em;
+}
+
+/* @noflip */
+.mw-content-rtl .mw-editsection,
+.mw-content-ltr .mw-content-rtl .mw-editsection {
+ margin-right: 1em;
+}
+
+/* Prevent citations and subscripts from interfering with the line-height */
+sup,
+sub {
+ line-height: 1;
+}
diff --git a/resources/src/mediawiki.legacy/wikibits.js b/resources/src/mediawiki.legacy/wikibits.js
new file mode 100644
index 00000000..a4039966
--- /dev/null
+++ b/resources/src/mediawiki.legacy/wikibits.js
@@ -0,0 +1,204 @@
+/**
+ * MediaWiki legacy wikibits
+ */
+( function ( mw, $ ) {
+ var msg,
+ win = window,
+ ua = navigator.userAgent.toLowerCase(),
+ onloadFuncts = [];
+
+/**
+ * User-agent sniffing.
+ *
+ * @deprecated since 1.17 Use jquery.client instead
+ */
+
+msg = 'Use feature detection or module jquery.client instead.';
+
+mw.log.deprecate( win, 'clientPC', ua, msg );
+
+// Ignored dummy values
+mw.log.deprecate( win, 'is_gecko', false, msg );
+mw.log.deprecate( win, 'is_chrome_mac', false, msg );
+mw.log.deprecate( win, 'is_chrome', false, msg );
+mw.log.deprecate( win, 'webkit_version', false, msg );
+mw.log.deprecate( win, 'is_safari_win', false, msg );
+mw.log.deprecate( win, 'is_safari', false, msg );
+mw.log.deprecate( win, 'webkit_match', false, msg );
+mw.log.deprecate( win, 'is_ff2', false, msg );
+mw.log.deprecate( win, 'ff2_bugs', false, msg );
+mw.log.deprecate( win, 'is_ff2_win', false, msg );
+mw.log.deprecate( win, 'is_ff2_x11', false, msg );
+mw.log.deprecate( win, 'opera95_bugs', false, msg );
+mw.log.deprecate( win, 'opera7_bugs', false, msg );
+mw.log.deprecate( win, 'opera6_bugs', false, msg );
+mw.log.deprecate( win, 'is_opera_95', false, msg );
+mw.log.deprecate( win, 'is_opera_preseven', false, msg );
+mw.log.deprecate( win, 'is_opera', false, msg );
+mw.log.deprecate( win, 'ie6_bugs', false, msg );
+
+/**
+ * DOM utilities for handling of events, text nodes and selecting elements
+ *
+ * @deprecated since 1.17 Use jQuery instead
+ */
+msg = 'Use jQuery instead.';
+
+// Ignored dummy values
+mw.log.deprecate( win, 'doneOnloadHook', undefined, msg );
+mw.log.deprecate( win, 'onloadFuncts', [], msg );
+mw.log.deprecate( win, 'runOnloadHook', $.noop, msg );
+mw.log.deprecate( win, 'changeText', $.noop, msg );
+mw.log.deprecate( win, 'killEvt', $.noop, msg );
+mw.log.deprecate( win, 'addHandler', $.noop, msg );
+mw.log.deprecate( win, 'hookEvent', $.noop, msg );
+mw.log.deprecate( win, 'addClickHandler', $.noop, msg );
+mw.log.deprecate( win, 'removeHandler', $.noop, msg );
+mw.log.deprecate( win, 'getElementsByClassName', function () { return []; }, msg );
+mw.log.deprecate( win, 'getInnerText', function () { return ''; }, msg );
+
+// Run a function after the window onload event is fired
+mw.log.deprecate( win, 'addOnloadHook', function ( hookFunct ) {
+ if ( onloadFuncts ) {
+ onloadFuncts.push(hookFunct);
+ } else {
+ // If func queue is gone the event has happened already,
+ // run immediately instead of queueing.
+ hookFunct();
+ }
+}, msg );
+
+$( win ).on( 'load', function () {
+ var i, functs;
+
+ // Don't run twice
+ if ( !onloadFuncts ) {
+ return;
+ }
+
+ // Deference and clear onloadFuncts before running any
+ // hooks to make sure we don't miss any addOnloadHook
+ // calls.
+ functs = onloadFuncts.slice();
+ onloadFuncts = undefined;
+
+ // Execute the queued functions
+ for ( i = 0; i < functs.length; i++ ) {
+ functs[i]();
+ }
+} );
+
+/**
+ * Toggle checkboxes with shift selection
+ *
+ * @deprecated since 1.17 Use jquery.checkboxShiftClick instead
+ */
+msg = 'Use jquery.checkboxShiftClick instead.';
+mw.log.deprecate( win, 'checkboxes', [], msg );
+mw.log.deprecate( win, 'lastCheckbox', null, msg );
+mw.log.deprecate( win, 'setupCheckboxShiftClick', $.noop, msg );
+mw.log.deprecate( win, 'addCheckboxClickHandlers', $.noop, msg );
+mw.log.deprecate( win, 'checkboxClickHandler', $.noop, msg );
+
+/**
+ * Add a button to the default editor toolbar
+ *
+ * @deprecated since 1.17 Use mw.toolbar instead
+ */
+mw.log.deprecate( win, 'mwEditButtons', [], 'Use mw.toolbar instead.' );
+mw.log.deprecate( win, 'mwCustomEditButtons', [], 'Use mw.toolbar instead.' );
+
+/**
+ * Spinner creation, injection and removal
+ *
+ * @deprecated since 1.18 Use jquery.spinner instead
+ */
+mw.log.deprecate( win, 'injectSpinner', $.noop, 'Use jquery.spinner instead.' );
+mw.log.deprecate( win, 'removeSpinner', $.noop, 'Use jquery.spinner instead.' );
+
+/**
+ * Escape utilities
+ *
+ * @deprecated since 1.18 Use mw.html instead
+ */
+mw.log.deprecate( win, 'escapeQuotes', $.noop, 'Use mw.html instead.' );
+mw.log.deprecate( win, 'escapeQuotesHTML', $.noop, 'Use mw.html instead.' );
+
+/**
+ * Display a message to the user
+ *
+ * @deprecated since 1.17 Use mediawiki.notify instead
+ * @param {string|HTMLElement} message To be put inside the message box
+ */
+mw.log.deprecate( win, 'jsMsg', function ( message ) {
+ if ( !arguments.length || message === '' || message === null ) {
+ return true;
+ }
+ if ( typeof message !== 'object' ) {
+ message = $.parseHTML( message );
+ }
+ mw.notify( message, { autoHide: true, tag: 'legacy' } );
+ return true;
+}, 'Use mediawiki.notify instead.' );
+
+/**
+ * Misc. utilities
+ *
+ * @deprecated since 1.17 Use mediawiki.util or jquery.accessKeyLabel instead
+ */
+msg = 'Use mediawiki.util instead.';
+mw.log.deprecate( win, 'addPortletLink', mw.util.addPortletLink, msg );
+mw.log.deprecate( win, 'appendCSS', mw.util.addCSS, msg );
+msg = 'Use jquery.accessKeyLabel instead.';
+mw.log.deprecate( win, 'tooltipAccessKeyPrefix', 'alt-', msg );
+mw.log.deprecate( win, 'tooltipAccessKeyRegexp', /\[(alt-)?(.)\]$/, msg );
+// mw.util.updateTooltipAccessKeys already generates a deprecation message.
+win.updateTooltipAccessKeys = function () {
+ return mw.util.updateTooltipAccessKeys.apply( null, arguments );
+};
+
+/**
+ * Wikipage import methods
+ */
+
+// included-scripts tracker
+win.loadedScripts = {};
+
+win.importScript = function ( page ) {
+ var uri = mw.config.get( 'wgScript' ) + '?title=' +
+ mw.util.wikiUrlencode( page ) +
+ '&action=raw&ctype=text/javascript';
+ return win.importScriptURI( uri );
+};
+
+win.importScriptURI = function ( url ) {
+ if ( win.loadedScripts[url] ) {
+ return null;
+ }
+ win.loadedScripts[url] = true;
+ var s = document.createElement( 'script' );
+ s.setAttribute( 'src', url );
+ s.setAttribute( 'type', 'text/javascript' );
+ document.getElementsByTagName( 'head' )[0].appendChild( s );
+ return s;
+};
+
+win.importStylesheet = function ( page ) {
+ var uri = mw.config.get( 'wgScript' ) + '?title=' +
+ mw.util.wikiUrlencode( page ) +
+ '&action=raw&ctype=text/css';
+ return win.importStylesheetURI( uri );
+};
+
+win.importStylesheetURI = function ( url, media ) {
+ var l = document.createElement( 'link' );
+ l.rel = 'stylesheet';
+ l.href = url;
+ if ( media ) {
+ l.media = media;
+ }
+ document.getElementsByTagName('head')[0].appendChild( l );
+ return l;
+};
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.less/mediawiki.mixins.animation.less b/resources/src/mediawiki.less/mediawiki.mixins.animation.less
new file mode 100644
index 00000000..ec3cddc6
--- /dev/null
+++ b/resources/src/mediawiki.less/mediawiki.mixins.animation.less
@@ -0,0 +1,12 @@
+.animation (...) {
+ -webkit-animation: @arguments;
+ -moz-animation: @arguments;
+ -o-animation: @arguments;
+ animation: @arguments;
+}
+
+.transform-rotate (@deg) {
+ -webkit-transform: rotate(@deg);
+ -moz-transform: rotate(@deg);
+ transform: rotate(@deg);
+} \ No newline at end of file
diff --git a/resources/src/mediawiki.less/mediawiki.mixins.less b/resources/src/mediawiki.less/mediawiki.mixins.less
new file mode 100644
index 00000000..c360e1f4
--- /dev/null
+++ b/resources/src/mediawiki.less/mediawiki.mixins.less
@@ -0,0 +1,61 @@
+/**
+ * Common LESS mixin library for MediaWiki
+ *
+ * By default the folder containing this file is included in $wgResourceLoaderLESSImportPaths,
+ * which makes this file importable by all less files via '@import "mediawiki.mixins";'.
+ *
+ * The mixins included below are considered a public interface for MediaWiki extensions.
+ * The signatures of parametrized mixins should be kept as stable as possible.
+ *
+ * See <http://lesscss.org/#-mixins> for more information about how to write mixins.
+ */
+
+.background-image(@url) {
+ background-image: e('/* @embed */') url(@url);
+}
+
+.vertical-gradient(@startColor: gray, @endColor: white, @startPos: 0, @endPos: 100%) {
+ background-color: @endColor;
+ background-image: -moz-linear-gradient( top, @startColor @startPos, @endColor @endPos ); // Firefox 3.6+
+ background-image: -webkit-gradient( linear, left top, left bottom, color-stop( @startPos, @startColor ), color-stop( @endPos, @endColor ) ); // Safari 4+, Chrome 2+
+ background-image: -webkit-linear-gradient( top, @startColor @startPos, @endColor @endPos ); // Safari 5.1+, Chrome 10+
+ background-image: linear-gradient( @startColor @startPos, @endColor @endPos ); // Standard
+}
+
+/*
+ * SVG support using a transparent gradient to guarantee cross-browser
+ * compatibility (browsers able to understand gradient syntax support also SVG).
+ * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique
+ *
+ * We use gzip compression, which means that it is okay to embed twice.
+ *
+ * We do not embed the fallback image on the assumption that the gain for old browsers
+ * is not worth the harm done to modern ones.
+ */
+.background-image-svg(@svg, @fallback) {
+ background-image: url(@fallback);
+ background-image: -webkit-linear-gradient(transparent, transparent), e('/* @embed */') url(@svg);
+ background-image: linear-gradient(transparent, transparent), e('/* @embed */') url(@svg);
+}
+
+.list-style-image(@url) {
+ list-style-image: e('/* @embed */') url(@url);
+}
+
+.transition(@value) {
+ -webkit-transition: @value; // Safari 3.1-6.0, iOS 3.2-6.1, Android 2.1-4.3
+ -moz-transition: @value; // Firefox 4-15
+ -o-transition: @value; // Opera 10.5-12.0
+ transition: @value; // Chrome 26+, Firefox 16+, IE 10+, Safari 6.1+, Opera 12.1+, iOS 7+, Android 4.4+
+}
+
+.box-sizing(@value) {
+ -webkit-box-sizing: @value; // Safari 3.1-5.0, iOS 3.2-4.3, Android 2.1-3.0
+ -moz-box-sizing: @value; // Firefox 4-28,
+ box-sizing: @value; // Chrome 10+, Firefox 29+, IE 8+, Safari 5.1+, Opera 10+, iOS 5+, Android 4+
+}
+
+.box-shadow(@value) {
+ -webkit-box-shadow: @value; // Safari 3.1-5.0, iOS 3.2-4.3, Android 2.1-3.0
+ box-shadow: @value; // Chrome 10+, Firefox 4+, IE 9+, Safari 5.1+, Opera 11+, iOS 5+, Android 4+
+}
diff --git a/resources/src/mediawiki.less/mediawiki.mixins.rotation.less b/resources/src/mediawiki.less/mediawiki.mixins.rotation.less
new file mode 100644
index 00000000..e28b333f
--- /dev/null
+++ b/resources/src/mediawiki.less/mediawiki.mixins.rotation.less
@@ -0,0 +1,33 @@
+// This is a separate file because importing the mixin causes
+// the keyframes blocks to be included in the output, regardless
+// of whether .rotation is used.
+@import "mediawiki.mixins.animation";
+
+.rotate-frames () {
+ from {
+ .transform-rotate(0deg);
+ }
+ to {
+ .transform-rotate(360deg);
+ }
+}
+
+@-webkit-keyframes rotate {
+ .rotate-frames;
+}
+
+@-moz-keyframes rotate {
+ .rotate-frames;
+}
+
+@-o-keyframes rotate {
+ .rotate-frames;
+}
+
+@keyframes rotate {
+ .rotate-frames;
+}
+
+.rotation( @time ) {
+ .animation(rotate, @time, infinite, linear);
+}
diff --git a/resources/src/mediawiki.less/mediawiki.ui/mixins.less b/resources/src/mediawiki.less/mediawiki.ui/mixins.less
new file mode 100644
index 00000000..ec9888f2
--- /dev/null
+++ b/resources/src/mediawiki.less/mediawiki.ui/mixins.less
@@ -0,0 +1,122 @@
+// ----------------------------------------------------------------------------
+// Form styling mixins
+// ----------------------------------------------------------------------------
+.agora-label-styling() {
+ font-size: 0.9em;
+ color: @colorText;
+
+ * {
+ font-weight: normal;
+ }
+}
+
+.agora-inline-label-styling() {
+ margin-bottom: 0.5em;
+ cursor: pointer;
+ vertical-align: bottom;
+ line-height: normal;
+
+ font-weight: normal;
+
+ & > input[type="checkbox"],
+ & > input[type="radio"] {
+ width: auto;
+ height: auto;
+ margin: 0 0.1em 0 0;
+ padding: 0;
+ border: 1px solid @colorFieldBorder;
+ cursor: pointer;
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Button styling
+// ----------------------------------------------------------------------------
+
+.button-colors(@bgColor) {
+ background: @bgColor;
+
+ &:hover,
+ &:focus {
+ // The inner bottom bevel should match the active background color.
+ box-shadow: 0 1px rgba(0, 0, 0, 10%), inset 0 -3px rgba(0, 0, 0, 20%);
+ border-bottom-color: mix(#000, @bgColor, 20%);
+ outline: none;
+ // remove outline in Firefox
+ &::-moz-focus-inner {
+ border-color: transparent;
+ }
+ }
+
+ &:active,
+ &.mw-ui-checked {
+ // lessphp doesn't implement shade (https://github.com/leafo/lessphp/issues/528);
+ // it passes it through, then ResourceLoader drops it.
+ // background: shade(@bgColor, 20%);
+ background: mix(#000, @bgColor, 20%);
+ box-shadow: none;
+ }
+}
+
+.button-colors(@bgColor) when (lightness(@bgColor) >= 70%) {
+ color: @colorButtonText;
+ border: 1px solid @colorGray12;
+
+ &:disabled {
+ color: @colorDisabledText;
+
+ // make sure disabled buttons don't have hover and active states
+ &:hover,
+ &:active {
+ background: @bgColor;
+ box-shadow: none;
+ }
+ }
+}
+
+.button-colors(@bgColor) when (lightness(@bgColor) < 70%) {
+ color: #fff;
+ // border of the same color as background so that light background and
+ // dark background buttons are the same height (only top and bottom to
+ // make box shadow on hover cover the corners too)
+ border: 1px solid @bgColor;
+ border-left: none;
+ border-right: none;
+ text-shadow: 0 1px rgba(0, 0, 0, .1);
+
+ &:disabled {
+ background: @colorGray12;
+ border-color: @colorGray12;
+
+ // make sure disabled buttons don't have hover and active states
+ &:hover,
+ &:active,
+ &.mw-ui-checked {
+ box-shadow: none;
+ }
+ }
+}
+
+.button-colors-quiet(@textColor) {
+ // Quiet buttons all start gray, and reveal
+ // constructive/progressive/destructive color on hover and active.
+ color: @colorButtonText;
+
+ &:hover,
+ &:focus {
+ // lessphp doesn't implement tint, see above
+ // color: tint(@textColor, 20%);
+ color: mix(#fff, @textColor, 20%);
+ }
+
+ &:active,
+ &.mw-ui-checked {
+ // lessphp doesn't implement shade, see above
+ // color: shade(@textColor, 20%);
+ color: mix(#000, @textColor, 20%);
+ }
+
+ &:disabled {
+ color: @colorDisabledText;
+ }
+}
diff --git a/resources/src/mediawiki.less/mediawiki.ui/variables.less b/resources/src/mediawiki.less/mediawiki.ui/variables.less
new file mode 100644
index 00000000..e91302be
--- /dev/null
+++ b/resources/src/mediawiki.less/mediawiki.ui/variables.less
@@ -0,0 +1,62 @@
+// Colors for use in mediawiki.ui and elsewhere
+
+// Although this defines many shades, be parsimonious in your own use of grays. Prefer
+// colors already in use in MediaWiki. Prefer semantic color names such as "@colorText".
+@colorGray1: #111; // darkest
+@colorGray2: #222;
+@colorGray3: #333;
+@colorGray4: #444;
+@colorGray5: #555;
+@colorGray6: #666;
+@colorGray7: #777;
+@colorGray8: #888;
+@colorGray9: #999;
+@colorGray10: #AAA;
+@colorGray11: #BBB;
+@colorGray12: #CCC;
+@colorGray13: #DDD;
+@colorGray14: #EEE;
+@colorGray15: #F9F9F9; // lightest
+
+// Semantic background colors
+// Blue; for contextual use of a continuing action
+@colorProgressive: #347bff;
+// Green; for contextual use of a positive finalizing action
+@colorConstructive: #00af89;
+// Orange; for contextual use of returning to a past action
+@colorRegressive: #FF5D00;
+// Red; for contextual use of a negative action of high severity
+@colorDestructive: #d11d13;
+// Orange; for contextual use of a potentially negative action of medium severity
+@colorMediumSevere: #FF5D00;
+// Yellow; for contextual use of a potentially negative action of low severity
+@colorLowSevere: #FFB50D;
+
+// Used in mixins to darken contextual colors by the same amount (eg. focus)
+@colorDarkenPercentage: 13.5%;
+// Used in mixins to lighten contextual colors by the same amount (eg. hover)
+@colorLightenPercentage: 13.5%;
+
+// Text colors
+@colorText: @colorGray2;
+@colorTextLight: @colorGray6;
+@colorButtonText: @colorGray5;
+@colorDisabledText: @colorGray12;
+@colorErrorText: #CC0000;
+
+// UI colors
+@colorFieldBorder: @colorGray12;
+@colorShadow: @colorGray14;
+@colorPlaceholder: @colorGray10;
+@colorNeutral: @colorGray7;
+
+// The following rules are deprecated
+@colorWhite: #fff;
+@colorOffWhite: #fafafa;
+@colorGrayDark: #898989;
+@colorGrayLight: #ccc;
+@colorGrayLighter: #ddd;
+@colorGrayLightest: #eee;
+
+// Global border radius to be used to buttons and inputs
+@borderRadius: 2px;
diff --git a/resources/src/mediawiki.libs/CLDRPluralRuleParser.js b/resources/src/mediawiki.libs/CLDRPluralRuleParser.js
new file mode 100644
index 00000000..83c25245
--- /dev/null
+++ b/resources/src/mediawiki.libs/CLDRPluralRuleParser.js
@@ -0,0 +1,475 @@
+/* This is CLDRPluralRuleParser v1.1, ported to MediaWiki ResourceLoader */
+
+/**
+* CLDRPluralRuleParser.js
+* A parser engine for CLDR plural rules.
+*
+* Copyright 2012 GPLV3+, Santhosh Thottingal
+*
+* @version 0.1.0-alpha
+* @source https://github.com/santhoshtr/CLDRPluralRuleParser
+* @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
+* @author Timo Tijhof
+* @author Amir Aharoni
+*/
+
+( function ( mw ) {
+/**
+ * Evaluates a plural rule in CLDR syntax for a number
+ * @param {string} rule
+ * @param {integer} number
+ * @return {boolean} true if evaluation passed, false if evaluation failed.
+ */
+
+function pluralRuleParser(rule, number) {
+ /*
+ Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
+ -----------------------------------------------------------------
+ condition = and_condition ('or' and_condition)*
+ ('@integer' samples)?
+ ('@decimal' samples)?
+ and_condition = relation ('and' relation)*
+ relation = is_relation | in_relation | within_relation
+ is_relation = expr 'is' ('not')? value
+ in_relation = expr (('not')? 'in' | '=' | '!=') range_list
+ within_relation = expr ('not')? 'within' range_list
+ expr = operand (('mod' | '%') value)?
+ operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
+ range_list = (range | value) (',' range_list)*
+ value = digit+
+ digit = 0|1|2|3|4|5|6|7|8|9
+ range = value'..'value
+ samples = sampleRange (',' sampleRange)* (',' ('…'|'...'))?
+ sampleRange = decimalValue '~' decimalValue
+ decimalValue = value ('.' value)?
+ */
+
+ // we don't evaluate the samples section of the rule. Ignore it.
+ rule = rule.split('@')[0].replace(/^\s*/, '').replace(/\s*$/, '');
+
+ if (!rule.length) {
+ // empty rule or 'other' rule.
+ return true;
+ }
+ // Indicates current position in the rule as we parse through it.
+ // Shared among all parsing functions below.
+ var pos = 0,
+ operand,
+ expression,
+ relation,
+ result,
+ whitespace = makeRegexParser(/^\s+/),
+ value = makeRegexParser(/^\d+/),
+ _n_ = makeStringParser('n'),
+ _i_ = makeStringParser('i'),
+ _f_ = makeStringParser('f'),
+ _t_ = makeStringParser('t'),
+ _v_ = makeStringParser('v'),
+ _w_ = makeStringParser('w'),
+ _is_ = makeStringParser('is'),
+ _isnot_ = makeStringParser('is not'),
+ _isnot_sign_ = makeStringParser('!='),
+ _equal_ = makeStringParser('='),
+ _mod_ = makeStringParser('mod'),
+ _percent_ = makeStringParser('%'),
+ _not_ = makeStringParser('not'),
+ _in_ = makeStringParser('in'),
+ _within_ = makeStringParser('within'),
+ _range_ = makeStringParser('..'),
+ _comma_ = makeStringParser(','),
+ _or_ = makeStringParser('or'),
+ _and_ = makeStringParser('and');
+
+ function debug() {
+ // console.log.apply(console, arguments);
+ }
+
+ debug('pluralRuleParser', rule, number);
+
+ // Try parsers until one works, if none work return null
+
+ function choice(parserSyntax) {
+ return function() {
+ for (var i = 0; i < parserSyntax.length; i++) {
+ var result = parserSyntax[i]();
+ if (result !== null) {
+ return result;
+ }
+ }
+ return null;
+ };
+ }
+
+ // Try several parserSyntax-es in a row.
+ // All must succeed; otherwise, return null.
+ // This is the only eager one.
+
+ function sequence(parserSyntax) {
+ var originalPos = pos;
+ var result = [];
+ for (var i = 0; i < parserSyntax.length; i++) {
+ var res = parserSyntax[i]();
+ if (res === null) {
+ pos = originalPos;
+ return null;
+ }
+ result.push(res);
+ }
+ return result;
+ }
+
+ // Run the same parser over and over until it fails.
+ // Must succeed a minimum of n times; otherwise, return null.
+
+ function nOrMore(n, p) {
+ return function() {
+ var originalPos = pos;
+ var result = [];
+ var parsed = p();
+ while (parsed !== null) {
+ result.push(parsed);
+ parsed = p();
+ }
+ if (result.length < n) {
+ pos = originalPos;
+ return null;
+ }
+ return result;
+ };
+ }
+
+ // Helpers -- just make parserSyntax out of simpler JS builtin types
+ function makeStringParser(s) {
+ var len = s.length;
+ return function() {
+ var result = null;
+ if (rule.substr(pos, len) === s) {
+ result = s;
+ pos += len;
+ }
+
+ return result;
+ };
+ }
+
+ function makeRegexParser(regex) {
+ return function() {
+ var matches = rule.substr(pos).match(regex);
+ if (matches === null) {
+ return null;
+ }
+ pos += matches[0].length;
+ return matches[0];
+ };
+ }
+
+ /*
+ * integer digits of n.
+ */
+ function i() {
+ var result = _i_();
+ if (result === null) {
+ debug(' -- failed i', parseInt(number, 10));
+ return result;
+ }
+ result = parseInt(number, 10);
+ debug(' -- passed i ', result);
+ return result;
+ }
+
+ /*
+ * absolute value of the source number (integer and decimals).
+ */
+ function n() {
+ var result = _n_();
+ if (result === null) {
+ debug(' -- failed n ', number);
+ return result;
+ }
+ result = parseFloat(number, 10);
+ debug(' -- passed n ', result);
+ return result;
+ }
+
+ /*
+ * visible fractional digits in n, with trailing zeros.
+ */
+ function f() {
+ var result = _f_();
+ if (result === null) {
+ debug(' -- failed f ', number);
+ return result;
+ }
+ result = (number + '.').split('.')[1] || 0;
+ debug(' -- passed f ', result);
+ return result;
+ }
+
+ /*
+ * visible fractional digits in n, without trailing zeros.
+ */
+ function t() {
+ var result = _t_();
+ if (result === null) {
+ debug(' -- failed t ', number);
+ return result;
+ }
+ result = (number + '.').split('.')[1].replace(/0$/, '') || 0;
+ debug(' -- passed t ', result);
+ return result;
+ }
+
+ /*
+ * number of visible fraction digits in n, with trailing zeros.
+ */
+ function v() {
+ var result = _v_();
+ if (result === null) {
+ debug(' -- failed v ', number);
+ return result;
+ }
+ result = (number + '.').split('.')[1].length || 0;
+ debug(' -- passed v ', result);
+ return result;
+ }
+
+ /*
+ * number of visible fraction digits in n, without trailing zeros.
+ */
+ function w() {
+ var result = _w_();
+ if (result === null) {
+ debug(' -- failed w ', number);
+ return result;
+ }
+ result = (number + '.').split('.')[1].replace(/0$/, '').length || 0;
+ debug(' -- passed w ', result);
+ return result;
+ }
+
+ // operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
+ operand = choice([n, i, f, t, v, w]);
+
+ // expr = operand (('mod' | '%') value)?
+ expression = choice([mod, operand]);
+
+ function mod() {
+ var result = sequence([operand, whitespace, choice([_mod_, _percent_]), whitespace, value]);
+ if (result === null) {
+ debug(' -- failed mod');
+ return null;
+ }
+ debug(' -- passed ' + parseInt(result[0], 10) + ' ' + result[2] + ' ' + parseInt(result[4], 10));
+ return parseInt(result[0], 10) % parseInt(result[4], 10);
+ }
+
+ function not() {
+ var result = sequence([whitespace, _not_]);
+ if (result === null) {
+ debug(' -- failed not');
+ return null;
+ }
+
+ return result[1];
+ }
+
+ // is_relation = expr 'is' ('not')? value
+ function is() {
+ var result = sequence([expression, whitespace, choice([_is_]), whitespace, value]);
+ if (result !== null) {
+ debug(' -- passed is : ' + result[0] + ' == ' + parseInt(result[4], 10));
+ return result[0] === parseInt(result[4], 10);
+ }
+ debug(' -- failed is');
+ return null;
+ }
+
+ // is_relation = expr 'is' ('not')? value
+ function isnot() {
+ var result = sequence([expression, whitespace, choice([_isnot_, _isnot_sign_]), whitespace, value]);
+ if (result !== null) {
+ debug(' -- passed isnot: ' + result[0] + ' != ' + parseInt(result[4], 10));
+ return result[0] !== parseInt(result[4], 10);
+ }
+ debug(' -- failed isnot');
+ return null;
+ }
+
+ function not_in() {
+ var result = sequence([expression, whitespace, _isnot_sign_, whitespace, rangeList]);
+ if (result !== null) {
+ debug(' -- passed not_in: ' + result[0] + ' != ' + result[4]);
+ var range_list = result[4];
+ for (var i = 0; i < range_list.length; i++) {
+ if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ debug(' -- failed not_in');
+ return null;
+ }
+
+ // range_list = (range | value) (',' range_list)*
+ function rangeList() {
+ var result = sequence([choice([range, value]), nOrMore(0, rangeTail)]);
+ var resultList = [];
+ if (result !== null) {
+ resultList = resultList.concat(result[0]);
+ if (result[1][0]) {
+ resultList = resultList.concat(result[1][0]);
+ }
+ return resultList;
+ }
+ debug(' -- failed rangeList');
+ return null;
+ }
+
+ function rangeTail() {
+ // ',' range_list
+ var result = sequence([_comma_, rangeList]);
+ if (result !== null) {
+ return result[1];
+ }
+ debug(' -- failed rangeTail');
+ return null;
+ }
+
+ // range = value'..'value
+
+ function range() {
+ var i;
+ var result = sequence([value, _range_, value]);
+ if (result !== null) {
+ debug(' -- passed range');
+ var array = [];
+ var left = parseInt(result[0], 10);
+ var right = parseInt(result[2], 10);
+ for (i = left; i <= right; i++) {
+ array.push(i);
+ }
+ return array;
+ }
+ debug(' -- failed range');
+ return null;
+ }
+
+ function _in() {
+ // in_relation = expr ('not')? 'in' range_list
+ var result = sequence([expression, nOrMore(0, not), whitespace, choice([_in_, _equal_]), whitespace, rangeList]);
+ if (result !== null) {
+ debug(' -- passed _in:' + result);
+ var range_list = result[5];
+ for (var i = 0; i < range_list.length; i++) {
+ if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
+ return (result[1][0] !== 'not');
+ }
+ }
+ return (result[1][0] === 'not');
+ }
+ debug(' -- failed _in ');
+ return null;
+ }
+
+ /*
+ * The difference between in and within is that in only includes integers in the specified range,
+ * while within includes all values.
+ */
+
+ function within() {
+ // within_relation = expr ('not')? 'within' range_list
+ var result = sequence([expression, nOrMore(0, not), whitespace, _within_, whitespace, rangeList]);
+ if (result !== null) {
+ debug(' -- passed within');
+ var range_list = result[5];
+ if ((result[0] >= parseInt(range_list[0], 10)) &&
+ (result[0] < parseInt(range_list[range_list.length - 1], 10))) {
+ return (result[1][0] !== 'not');
+ }
+ return (result[1][0] === 'not');
+ }
+ debug(' -- failed within ');
+ return null;
+ }
+
+ // relation = is_relation | in_relation | within_relation
+ relation = choice([is, not_in, isnot, _in, within]);
+
+ // and_condition = relation ('and' relation)*
+ function and() {
+ var result = sequence([relation, nOrMore(0, andTail)]);
+ if (result) {
+ if (!result[0]) {
+ return false;
+ }
+ for (var i = 0; i < result[1].length; i++) {
+ if (!result[1][i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ debug(' -- failed and');
+ return null;
+ }
+
+ // ('and' relation)*
+ function andTail() {
+ var result = sequence([whitespace, _and_, whitespace, relation]);
+ if (result !== null) {
+ debug(' -- passed andTail' + result);
+ return result[3];
+ }
+ debug(' -- failed andTail');
+ return null;
+
+ }
+ // ('or' and_condition)*
+ function orTail() {
+ var result = sequence([whitespace, _or_, whitespace, and]);
+ if (result !== null) {
+ debug(' -- passed orTail: ' + result[3]);
+ return result[3];
+ }
+ debug(' -- failed orTail');
+ return null;
+
+ }
+
+ // condition = and_condition ('or' and_condition)*
+ function condition() {
+ var result = sequence([and, nOrMore(0, orTail)]);
+ if (result) {
+ for (var i = 0; i < result[1].length; i++) {
+ if (result[1][i]) {
+ return true;
+ }
+ }
+ return result[0];
+
+ }
+ return false;
+ }
+
+ result = condition();
+ /*
+ * For success, the pos must have gotten to the end of the rule
+ * and returned a non-null.
+ * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
+ */
+ if (result === null) {
+ throw new Error('Parse error at position ' + pos.toString() + ' for rule: ' + rule);
+ }
+
+ if (pos !== rule.length) {
+ debug('Warning: Rule not parsed completely. Parser stopped at ' + rule.substr(0, pos) + ' for rule: ' + rule);
+ }
+
+ return result;
+}
+
+/* pluralRuleParser ends here */
+mw.libs.pluralRuleParser = pluralRuleParser;
+
+} )( mediaWiki );
diff --git a/resources/src/mediawiki.libs/mediawiki.libs.jpegmeta.js b/resources/src/mediawiki.libs/mediawiki.libs.jpegmeta.js
new file mode 100644
index 00000000..b3ed88c8
--- /dev/null
+++ b/resources/src/mediawiki.libs/mediawiki.libs.jpegmeta.js
@@ -0,0 +1,737 @@
+/**
+ * This is JsJpegMeta v1.0
+ * From: https://code.google.com/p/jsjpegmeta/downloads/list
+ * From: https://github.com/bennoleslie/jsjpegmeta/blob/v1.0.0/jpegmeta.js
+ *
+ * Ported to MediaWiki ResourceLoader by Bryan Tong Minh
+ * Changes:
+ * - Add closure.
+ * - Add this.JpegMeta assignment to expose it as global.
+ * - Add mw.libs.jpegmeta wrapper.
+ */
+
+( function () {
+ /*
+ Copyright (c) 2009 Ben Leslie
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ */
+
+ /*
+ This JavaScript library is used to parse meta-data from files
+ with mime-type image/jpeg.
+
+ Include it with something like:
+
+ <script type="text/javascript" src="jpegmeta.js"></script>
+
+ This adds a single 'module' object called 'JpegMeta' to the global
+ namespace.
+
+ Public Functions
+ ----------------
+ JpegMeta.parseNum - parse unsigned integers from binary data
+ JpegMeta.parseSnum - parse signed integers from binary data
+
+ Public Classes
+ --------------
+ JpegMeta.Rational - A rational number class
+ JpegMeta.JfifSegment
+ JpegMeta.ExifSegment
+ JpegMeta.JpegFile - Primary class for Javascript parsing
+ */
+
+ var JpegMeta = {};
+ // MediaWiki: Expose as global
+ this.JpegMeta = JpegMeta;
+
+ /*
+ parse an unsigned number of size bytes at offset in some binary string data.
+ If endian
+ is "<" parse the data as little endian, if endian
+ is ">" parse as big-endian.
+ */
+ JpegMeta.parseNum = function parseNum(endian, data, offset, size) {
+ var i;
+ var ret;
+ var big_endian = (endian === ">");
+ if (offset === undefined) offset = 0;
+ if (size === undefined) size = data.length - offset;
+ for (big_endian ? i = offset : i = offset + size - 1;
+ big_endian ? i < offset + size : i >= offset;
+ big_endian ? i++ : i--) {
+ ret <<= 8;
+ ret += data.charCodeAt(i);
+ }
+ return ret;
+ };
+
+ /*
+ parse an signed number of size bytes at offset in some binary string data.
+ If endian
+ is "<" parse the data as little endian, if endian
+ is ">" parse as big-endian.
+ */
+ JpegMeta.parseSnum = function parseSnum(endian, data, offset, size) {
+ var i;
+ var ret;
+ var neg;
+ var big_endian = (endian === ">");
+ if (offset === undefined) offset = 0;
+ if (size === undefined) size = data.length - offset;
+ for (big_endian ? i = offset : i = offset + size - 1;
+ big_endian ? i < offset + size : i >= offset;
+ big_endian ? i++ : i--) {
+ if (neg === undefined) {
+ /* Negative if top bit is set */
+ neg = (data.charCodeAt(i) & 0x80) === 0x80;
+ }
+ ret <<= 8;
+ /* If it is negative we invert the bits */
+ ret += neg ? ~data.charCodeAt(i) & 0xff: data.charCodeAt(i);
+ }
+ if (neg) {
+ /* If it is negative we do two's complement */
+ ret += 1;
+ ret *= -1;
+ }
+ return ret;
+ };
+
+ /* Rational number class */
+ JpegMeta.Rational = function Rational(num, den)
+ {
+ this.num = num;
+ this.den = den || 1;
+ return this;
+ };
+
+ /* Rational number methods */
+ JpegMeta.Rational.prototype.toString = function toString() {
+ if (this.num === 0) {
+ return "" + this.num;
+ }
+ if (this.den === 1) {
+ return "" + this.num;
+ }
+ if (this.num === 1) {
+ return this.num + " / " + this.den;
+ }
+ return this.num / this.den; // + "/" + this.den;
+ };
+
+ JpegMeta.Rational.prototype.asFloat = function asFloat() {
+ return this.num / this.den;
+ };
+
+ /* MetaGroup class */
+ JpegMeta.MetaGroup = function MetaGroup(fieldName, description) {
+ this.fieldName = fieldName;
+ this.description = description;
+ this.metaProps = {};
+ return this;
+ };
+
+ JpegMeta.MetaGroup.prototype._addProperty = function _addProperty(fieldName, description, value) {
+ var property = new JpegMeta.MetaProp(fieldName, description, value);
+ this[property.fieldName] = property;
+ this.metaProps[property.fieldName] = property;
+ };
+
+ JpegMeta.MetaGroup.prototype.toString = function toString() {
+ return "[MetaGroup " + this.description + "]";
+ };
+
+ /* MetaProp class */
+ JpegMeta.MetaProp = function MetaProp(fieldName, description, value) {
+ this.fieldName = fieldName;
+ this.description = description;
+ this.value = value;
+ return this;
+ };
+
+ JpegMeta.MetaProp.prototype.toString = function toString() {
+ return "" + this.value;
+ };
+
+ /* JpegFile class */
+ JpegMeta.JpegFile = function JpegFile(binary_data, filename) {
+ /* Change this to EOI if we want to parse. */
+ var break_segment = this._SOS;
+
+ this.metaGroups = {};
+ this._binary_data = binary_data;
+ this.filename = filename;
+
+ /* Go through and parse. */
+ var pos = 0;
+ var pos_start_of_segment = 0;
+ var delim;
+ var mark;
+ var _mark;
+ var segsize;
+ var headersize;
+ var mark_code;
+ var mark_fn;
+
+ /* Check to see if this looks like a JPEG file */
+ if (this._binary_data.slice(0, 2) !== this._SOI_MARKER) {
+ throw new Error("Doesn't look like a JPEG file. First two bytes are " +
+ this._binary_data.charCodeAt(0) + "," +
+ this._binary_data.charCodeAt(1) + ".");
+ }
+
+ pos += 2;
+
+ while (pos < this._binary_data.length) {
+ delim = this._binary_data.charCodeAt(pos++);
+ mark = this._binary_data.charCodeAt(pos++);
+
+ pos_start_of_segment = pos;
+
+ if (delim != this._DELIM) {
+ break;
+ }
+
+ if (mark === break_segment) {
+ break;
+ }
+
+ headersize = JpegMeta.parseNum(">", this._binary_data, pos, 2);
+
+ /* Find the end */
+ pos += headersize;
+ while (pos < this._binary_data.length) {
+ delim = this._binary_data.charCodeAt(pos++);
+ if (delim == this._DELIM) {
+ _mark = this._binary_data.charCodeAt(pos++);
+ if (_mark != 0x0) {
+ pos -= 2;
+ break;
+ }
+ }
+ }
+
+ segsize = pos - pos_start_of_segment;
+
+ if (this._markers[mark]) {
+ mark_code = this._markers[mark][0];
+ mark_fn = this._markers[mark][1];
+ } else {
+ mark_code = "UNKN";
+ mark_fn = undefined;
+ }
+
+ if (mark_fn) {
+ this[mark_fn](mark, pos_start_of_segment + 2);
+ }
+
+ }
+
+ if (this.general === undefined) {
+ throw Error("Invalid JPEG file.");
+ }
+
+ return this;
+ };
+
+ this.JpegMeta.JpegFile.prototype.toString = function () {
+ return "[JpegFile " + this.filename + " " +
+ this.general.type + " " +
+ this.general.pixelWidth + "x" +
+ this.general.pixelHeight +
+ " Depth: " + this.general.depth + "]";
+ };
+
+ /* Some useful constants */
+ this.JpegMeta.JpegFile.prototype._SOI_MARKER = '\xff\xd8';
+ this.JpegMeta.JpegFile.prototype._DELIM = 0xff;
+ this.JpegMeta.JpegFile.prototype._EOI = 0xd9;
+ this.JpegMeta.JpegFile.prototype._SOS = 0xda;
+
+ this.JpegMeta.JpegFile.prototype._sofHandler = function _sofHandler (mark, pos) {
+ if (this.general !== undefined) {
+ throw Error("Unexpected multiple-frame image");
+ }
+
+ this._addMetaGroup("general", "General");
+ this.general._addProperty("depth", "Depth", JpegMeta.parseNum(">", this._binary_data, pos, 1));
+ this.general._addProperty("pixelHeight", "Pixel Height", JpegMeta.parseNum(">", this._binary_data, pos + 1, 2));
+ this.general._addProperty("pixelWidth", "Pixel Width",JpegMeta.parseNum(">", this._binary_data, pos + 3, 2));
+ this.general._addProperty("type", "Type", this._markers[mark][2]);
+ };
+
+ /* JFIF idents */
+ this.JpegMeta.JpegFile.prototype._JFIF_IDENT = "JFIF\x00";
+ this.JpegMeta.JpegFile.prototype._JFXX_IDENT = "JFXX\x00";
+
+ /* Exif idents */
+ this.JpegMeta.JpegFile.prototype._EXIF_IDENT = "Exif\x00";
+
+ /* TIFF types */
+ this.JpegMeta.JpegFile.prototype._types = {
+ /* The format is identifier : ["type name", type_size_in_bytes ] */
+ 1 : ["BYTE", 1],
+ 2 : ["ASCII", 1],
+ 3 : ["SHORT", 2],
+ 4 : ["LONG", 4],
+ 5 : ["RATIONAL", 8],
+ 6 : ["SBYTE", 1],
+ 7 : ["UNDEFINED", 1],
+ 8 : ["SSHORT", 2],
+ 9 : ["SLONG", 4],
+ 10 : ["SRATIONAL", 8],
+ 11 : ["FLOAT", 4],
+ 12 : ["DOUBLE", 8]
+ };
+
+ this.JpegMeta.JpegFile.prototype._tifftags = {
+ /* A. Tags relating to image data structure */
+ 256 : ["Image width", "ImageWidth"],
+ 257 : ["Image height", "ImageLength"],
+ 258 : ["Number of bits per component", "BitsPerSample"],
+ 259 : ["Compression scheme", "Compression",
+ {1 : "uncompressed", 6 : "JPEG compression" }],
+ 262 : ["Pixel composition", "PhotmetricInerpretation",
+ {2 : "RGB", 6 : "YCbCr"}],
+ 274 : ["Orientation of image", "Orientation",
+ /* FIXME: Check the mirror-image / reverse encoding and rotation */
+ {1 : "Normal", 2 : "Reverse?",
+ 3 : "Upside-down", 4 : "Upside-down Reverse",
+ 5 : "90 degree CW", 6 : "90 degree CW reverse",
+ 7 : "90 degree CCW", 8 : "90 degree CCW reverse"}],
+ 277 : ["Number of components", "SamplesPerPixel"],
+ 284 : ["Image data arrangement", "PlanarConfiguration",
+ {1 : "chunky format", 2 : "planar format"}],
+ 530 : ["Subsampling ratio of Y to C", "YCbCrSubSampling"],
+ 531 : ["Y and C positioning", "YCbCrPositioning",
+ {1 : "centered", 2 : "co-sited"}],
+ 282 : ["X Resolution", "XResolution"],
+ 283 : ["Y Resolution", "YResolution"],
+ 296 : ["Resolution Unit", "ResolutionUnit",
+ {2 : "inches", 3 : "centimeters"}],
+ /* B. Tags realting to recording offset */
+ 273 : ["Image data location", "StripOffsets"],
+ 278 : ["Number of rows per strip", "RowsPerStrip"],
+ 279 : ["Bytes per compressed strip", "StripByteCounts"],
+ 513 : ["Offset to JPEG SOI", "JPEGInterchangeFormat"],
+ 514 : ["Bytes of JPEG Data", "JPEGInterchangeFormatLength"],
+ /* C. Tags relating to image data characteristics */
+ 301 : ["Transfer function", "TransferFunction"],
+ 318 : ["White point chromaticity", "WhitePoint"],
+ 319 : ["Chromaticities of primaries", "PrimaryChromaticities"],
+ 529 : ["Color space transformation matrix coefficients", "YCbCrCoefficients"],
+ 532 : ["Pair of black and white reference values", "ReferenceBlackWhite"],
+ /* D. Other tags */
+ 306 : ["Date and time", "DateTime"],
+ 270 : ["Image title", "ImageDescription"],
+ 271 : ["Make", "Make"],
+ 272 : ["Model", "Model"],
+ 305 : ["Software", "Software"],
+ 315 : ["Person who created the image", "Artist"],
+ 316 : ["Host Computer", "HostComputer"],
+ 33432 : ["Copyright holder", "Copyright"],
+
+ 34665 : ["Exif tag", "ExifIfdPointer"],
+ 34853 : ["GPS tag", "GPSInfoIfdPointer"]
+ };
+
+ this.JpegMeta.JpegFile.prototype._exiftags = {
+ /* Tag Support Levels (2) - 0th IFX Exif Private Tags */
+ /* A. Tags Relating to Version */
+ 36864 : ["Exif Version", "ExifVersion"],
+ 40960 : ["FlashPix Version", "FlashpixVersion"],
+
+ /* B. Tag Relating to Image Data Characteristics */
+ 40961 : ["Color Space", "ColorSpace"],
+
+ /* C. Tags Relating to Image Configuration */
+ 37121 : ["Meaning of each component", "ComponentsConfiguration"],
+ 37122 : ["Compressed Bits Per Pixel", "CompressedBitsPerPixel"],
+ 40962 : ["Pixel X Dimension", "PixelXDimension"],
+ 40963 : ["Pixel Y Dimension", "PixelYDimension"],
+
+ /* D. Tags Relating to User Information */
+ 37500 : ["Manufacturer notes", "MakerNote"],
+ 37510 : ["User comments", "UserComment"],
+
+ /* E. Tag Relating to Related File Information */
+ 40964 : ["Related audio file", "RelatedSoundFile"],
+
+ /* F. Tags Relating to Date and Time */
+ 36867 : ["Date Time Original", "DateTimeOriginal"],
+ 36868 : ["Date Time Digitized", "DateTimeDigitized"],
+ 37520 : ["DateTime subseconds", "SubSecTime"],
+ 37521 : ["DateTimeOriginal subseconds", "SubSecTimeOriginal"],
+ 37522 : ["DateTimeDigitized subseconds", "SubSecTimeDigitized"],
+
+ /* G. Tags Relating to Picture-Taking Conditions */
+ 33434 : ["Exposure time", "ExposureTime"],
+ 33437 : ["FNumber", "FNumber"],
+ 34850 : ["Exposure program", "ExposureProgram"],
+ 34852 : ["Spectral sensitivity", "SpectralSensitivity"],
+ 34855 : ["ISO Speed Ratings", "ISOSpeedRatings"],
+ 34856 : ["Optoelectric coefficient", "OECF"],
+ 37377 : ["Shutter Speed", "ShutterSpeedValue"],
+ 37378 : ["Aperture Value", "ApertureValue"],
+ 37379 : ["Brightness", "BrightnessValue"],
+ 37380 : ["Exposure Bias Value", "ExposureBiasValue"],
+ 37381 : ["Max Aperture Value", "MaxApertureValue"],
+ 37382 : ["Subject Distance", "SubjectDistance"],
+ 37383 : ["Metering Mode", "MeteringMode"],
+ 37384 : ["Light Source", "LightSource"],
+ 37385 : ["Flash", "Flash"],
+ 37386 : ["Focal Length", "FocalLength"],
+ 37396 : ["Subject Area", "SubjectArea"],
+ 41483 : ["Flash Energy", "FlashEnergy"],
+ 41484 : ["Spatial Frequency Response", "SpatialFrequencyResponse"],
+ 41486 : ["Focal Plane X Resolution", "FocalPlaneXResolution"],
+ 41487 : ["Focal Plane Y Resolution", "FocalPlaneYResolution"],
+ 41488 : ["Focal Plane Resolution Unit", "FocalPlaneResolutionUnit"],
+ 41492 : ["Subject Location", "SubjectLocation"],
+ 41493 : ["Exposure Index", "ExposureIndex"],
+ 41495 : ["Sensing Method", "SensingMethod"],
+ 41728 : ["File Source", "FileSource"],
+ 41729 : ["Scene Type", "SceneType"],
+ 41730 : ["CFA Pattern", "CFAPattern"],
+ 41985 : ["Custom Rendered", "CustomRendered"],
+ 41986 : ["Exposure Mode", "Exposure Mode"],
+ 41987 : ["White Balance", "WhiteBalance"],
+ 41988 : ["Digital Zoom Ratio", "DigitalZoomRatio"],
+ 41990 : ["Scene Capture Type", "SceneCaptureType"],
+ 41991 : ["Gain Control", "GainControl"],
+ 41992 : ["Contrast", "Contrast"],
+ 41993 : ["Saturation", "Saturation"],
+ 41994 : ["Sharpness", "Sharpness"],
+ 41995 : ["Device settings description", "DeviceSettingDescription"],
+ 41996 : ["Subject distance range", "SubjectDistanceRange"],
+
+ /* H. Other Tags */
+ 42016 : ["Unique image ID", "ImageUniqueID"],
+
+ 40965 : ["Interoperability tag", "InteroperabilityIFDPointer"]
+ };
+
+ this.JpegMeta.JpegFile.prototype._gpstags = {
+ /* A. Tags Relating to GPS */
+ 0 : ["GPS tag version", "GPSVersionID"],
+ 1 : ["North or South Latitude", "GPSLatitudeRef"],
+ 2 : ["Latitude", "GPSLatitude"],
+ 3 : ["East or West Longitude", "GPSLongitudeRef"],
+ 4 : ["Longitude", "GPSLongitude"],
+ 5 : ["Altitude reference", "GPSAltitudeRef"],
+ 6 : ["Altitude", "GPSAltitude"],
+ 7 : ["GPS time (atomic clock)", "GPSTimeStamp"],
+ 8 : ["GPS satellites usedd for measurement", "GPSSatellites"],
+ 9 : ["GPS receiver status", "GPSStatus"],
+ 10 : ["GPS mesaurement mode", "GPSMeasureMode"],
+ 11 : ["Measurement precision", "GPSDOP"],
+ 12 : ["Speed unit", "GPSSpeedRef"],
+ 13 : ["Speed of GPS receiver", "GPSSpeed"],
+ 14 : ["Reference for direction of movement", "GPSTrackRef"],
+ 15 : ["Direction of movement", "GPSTrack"],
+ 16 : ["Reference for direction of image", "GPSImgDirectionRef"],
+ 17 : ["Direction of image", "GPSImgDirection"],
+ 18 : ["Geodetic survey data used", "GPSMapDatum"],
+ 19 : ["Reference for latitude of destination", "GPSDestLatitudeRef"],
+ 20 : ["Latitude of destination", "GPSDestLatitude"],
+ 21 : ["Reference for longitude of destination", "GPSDestLongitudeRef"],
+ 22 : ["Longitude of destination", "GPSDestLongitude"],
+ 23 : ["Reference for bearing of destination", "GPSDestBearingRef"],
+ 24 : ["Bearing of destination", "GPSDestBearing"],
+ 25 : ["Reference for distance to destination", "GPSDestDistanceRef"],
+ 26 : ["Distance to destination", "GPSDestDistance"],
+ 27 : ["Name of GPS processing method", "GPSProcessingMethod"],
+ 28 : ["Name of GPS area", "GPSAreaInformation"],
+ 29 : ["GPS Date", "GPSDateStamp"],
+ 30 : ["GPS differential correction", "GPSDifferential"]
+ };
+
+ this.JpegMeta.JpegFile.prototype._markers = {
+ /* Start Of Frame markers, non-differential, Huffman coding */
+ 0xc0: ["SOF0", "_sofHandler", "Baseline DCT"],
+ 0xc1: ["SOF1", "_sofHandler", "Extended sequential DCT"],
+ 0xc2: ["SOF2", "_sofHandler", "Progressive DCT"],
+ 0xc3: ["SOF3", "_sofHandler", "Lossless (sequential)"],
+
+ /* Start Of Frame markers, differential, Huffman coding */
+ 0xc5: ["SOF5", "_sofHandler", "Differential sequential DCT"],
+ 0xc6: ["SOF6", "_sofHandler", "Differential progressive DCT"],
+ 0xc7: ["SOF7", "_sofHandler", "Differential lossless (sequential)"],
+
+ /* Start Of Frame markers, non-differential, arithmetic coding */
+ 0xc8: ["JPG", null, "Reserved for JPEG extensions"],
+ 0xc9: ["SOF9", "_sofHandler", "Extended sequential DCT"],
+ 0xca: ["SOF10", "_sofHandler", "Progressive DCT"],
+ 0xcb: ["SOF11", "_sofHandler", "Lossless (sequential)"],
+
+ /* Start Of Frame markers, differential, arithmetic coding */
+ 0xcd: ["SOF13", "_sofHandler", "Differential sequential DCT"],
+ 0xce: ["SOF14", "_sofHandler", "Differential progressive DCT"],
+ 0xcf: ["SOF15", "_sofHandler", "Differential lossless (sequential)"],
+
+ /* Huffman table specification */
+ 0xc4: ["DHT", null, "Define Huffman table(s)"],
+ 0xcc: ["DAC", null, "Define arithmetic coding conditioning(s)"],
+
+ /* Restart interval termination" */
+ 0xd0: ["RST0", null, "Restart with modulo 8 count “0”"],
+ 0xd1: ["RST1", null, "Restart with modulo 8 count “1”"],
+ 0xd2: ["RST2", null, "Restart with modulo 8 count “2”"],
+ 0xd3: ["RST3", null, "Restart with modulo 8 count “3”"],
+ 0xd4: ["RST4", null, "Restart with modulo 8 count “4”"],
+ 0xd5: ["RST5", null, "Restart with modulo 8 count “5”"],
+ 0xd6: ["RST6", null, "Restart with modulo 8 count “6”"],
+ 0xd7: ["RST7", null, "Restart with modulo 8 count “7”"],
+
+ /* Other markers */
+ 0xd8: ["SOI", null, "Start of image"],
+ 0xd9: ["EOI", null, "End of image"],
+ 0xda: ["SOS", null, "Start of scan"],
+ 0xdb: ["DQT", null, "Define quantization table(s)"],
+ 0xdc: ["DNL", null, "Define number of lines"],
+ 0xdd: ["DRI", null, "Define restart interval"],
+ 0xde: ["DHP", null, "Define hierarchical progression"],
+ 0xdf: ["EXP", null, "Expand reference component(s)"],
+ 0xe0: ["APP0", "_app0Handler", "Reserved for application segments"],
+ 0xe1: ["APP1", "_app1Handler"],
+ 0xe2: ["APP2", null],
+ 0xe3: ["APP3", null],
+ 0xe4: ["APP4", null],
+ 0xe5: ["APP5", null],
+ 0xe6: ["APP6", null],
+ 0xe7: ["APP7", null],
+ 0xe8: ["APP8", null],
+ 0xe9: ["APP9", null],
+ 0xea: ["APP10", null],
+ 0xeb: ["APP11", null],
+ 0xec: ["APP12", null],
+ 0xed: ["APP13", null],
+ 0xee: ["APP14", null],
+ 0xef: ["APP15", null],
+ 0xf0: ["JPG0", null], /* Reserved for JPEG extensions */
+ 0xf1: ["JPG1", null],
+ 0xf2: ["JPG2", null],
+ 0xf3: ["JPG3", null],
+ 0xf4: ["JPG4", null],
+ 0xf5: ["JPG5", null],
+ 0xf6: ["JPG6", null],
+ 0xf7: ["JPG7", null],
+ 0xf8: ["JPG8", null],
+ 0xf9: ["JPG9", null],
+ 0xfa: ["JPG10", null],
+ 0xfb: ["JPG11", null],
+ 0xfc: ["JPG12", null],
+ 0xfd: ["JPG13", null],
+ 0xfe: ["COM", null], /* Comment */
+
+ /* Reserved markers */
+ 0x01: ["JPG13", null] /* For temporary private use in arithmetic coding */
+ /* 02 -> bf are reserverd */
+ };
+
+ /* Private methods */
+ this.JpegMeta.JpegFile.prototype._addMetaGroup = function _addMetaGroup(name, description) {
+ var group = new JpegMeta.MetaGroup(name, description);
+ this[group.fieldName] = group;
+ this.metaGroups[group.fieldName] = group;
+ return group;
+ };
+
+ this.JpegMeta.JpegFile.prototype._parseIfd = function _parseIfd(endian, _binary_data, base, ifd_offset, tags, name, description) {
+ var num_fields = JpegMeta.parseNum(endian, _binary_data, base + ifd_offset, 2);
+ /* Per tag variables */
+ var i, j;
+ var tag_base;
+ var tag_field;
+ var type, type_field, type_size;
+ var num_values;
+ var value_offset;
+ var value;
+ var _val;
+ var num;
+ var den;
+
+ var group;
+
+ group = this._addMetaGroup(name, description);
+
+ for (var i = 0; i < num_fields; i++) {
+ /* parse the field */
+ tag_base = base + ifd_offset + 2 + (i * 12);
+ tag_field = JpegMeta.parseNum(endian, _binary_data, tag_base, 2);
+ type_field = JpegMeta.parseNum(endian, _binary_data, tag_base + 2, 2);
+ num_values = JpegMeta.parseNum(endian, _binary_data, tag_base + 4, 4);
+ value_offset = JpegMeta.parseNum(endian, _binary_data, tag_base + 8, 4);
+ if (this._types[type_field] === undefined) {
+ continue;
+ }
+ type = this._types[type_field][0];
+ type_size = this._types[type_field][1];
+
+ if (type_size * num_values <= 4) {
+ /* Data is in-line */
+ value_offset = tag_base + 8;
+ } else {
+ value_offset = base + value_offset;
+ }
+
+ /* Read the value */
+ if (type == "UNDEFINED") {
+ value = _binary_data.slice(value_offset, value_offset + num_values);
+ } else if (type == "ASCII") {
+ value = _binary_data.slice(value_offset, value_offset + num_values);
+ value = value.split('\x00')[0];
+ /* strip trail nul */
+ } else {
+ value = new Array();
+ for (j = 0; j < num_values; j++, value_offset += type_size) {
+ if (type == "BYTE" || type == "SHORT" || type == "LONG") {
+ value.push(JpegMeta.parseNum(endian, _binary_data, value_offset, type_size));
+ }
+ if (type == "SBYTE" || type == "SSHORT" || type == "SLONG") {
+ value.push(JpegMeta.parseSnum(endian, _binary_data, value_offset, type_size));
+ }
+ if (type == "RATIONAL") {
+ num = JpegMeta.parseNum(endian, _binary_data, value_offset, 4);
+ den = JpegMeta.parseNum(endian, _binary_data, value_offset + 4, 4);
+ value.push(new JpegMeta.Rational(num, den));
+ }
+ if (type == "SRATIONAL") {
+ num = JpegMeta.parseSnum(endian, _binary_data, value_offset, 4);
+ den = JpegMeta.parseSnum(endian, _binary_data, value_offset + 4, 4);
+ value.push(new JpegMeta.Rational(num, den));
+ }
+ value.push();
+ }
+ if (num_values === 1) {
+ value = value[0];
+ }
+ }
+ if (tags[tag_field] !== undefined) {
+ group._addProperty(tags[tag_field][1], tags[tag_field][0], value);
+ }
+ }
+ };
+
+ this.JpegMeta.JpegFile.prototype._jfifHandler = function _jfifHandler(mark, pos) {
+ if (this.jfif !== undefined) {
+ throw Error("Multiple JFIF segments found");
+ }
+ this._addMetaGroup("jfif", "JFIF");
+ this.jfif._addProperty("version_major", "Version Major", this._binary_data.charCodeAt(pos + 5));
+ this.jfif._addProperty("version_minor", "Version Minor", this._binary_data.charCodeAt(pos + 6));
+ this.jfif._addProperty("version", "JFIF Version", this.jfif.version_major.value + "." + this.jfif.version_minor.value);
+ this.jfif._addProperty("units", "Density Unit", this._binary_data.charCodeAt(pos + 7));
+ this.jfif._addProperty("Xdensity", "X density", JpegMeta.parseNum(">", this._binary_data, pos + 8, 2));
+ this.jfif._addProperty("Ydensity", "Y Density", JpegMeta.parseNum(">", this._binary_data, pos + 10, 2));
+ this.jfif._addProperty("Xthumbnail", "X Thumbnail", JpegMeta.parseNum(">", this._binary_data, pos + 12, 1));
+ this.jfif._addProperty("Ythumbnail", "Y Thumbnail", JpegMeta.parseNum(">", this._binary_data, pos + 13, 1));
+ };
+
+ /* Handle app0 segments */
+ this.JpegMeta.JpegFile.prototype._app0Handler = function app0Handler(mark, pos) {
+ var ident = this._binary_data.slice(pos, pos + 5);
+ if (ident == this._JFIF_IDENT) {
+ this._jfifHandler(mark, pos);
+ } else if (ident == this._JFXX_IDENT) {
+ /* Don't handle JFXX Ident yet */
+ } else {
+ /* Don't know about other idents */
+ }
+ };
+
+ /* Handle app1 segments */
+ this.JpegMeta.JpegFile.prototype._app1Handler = function _app1Handler(mark, pos) {
+ var ident = this._binary_data.slice(pos, pos + 5);
+ if (ident == this._EXIF_IDENT) {
+ this._exifHandler(mark, pos + 6);
+ } else {
+ /* Don't know about other idents */
+ }
+ };
+
+ /* Handle exif segments */
+ JpegMeta.JpegFile.prototype._exifHandler = function _exifHandler(mark, pos) {
+ if (this.exif !== undefined) {
+ throw new Error("Multiple JFIF segments found");
+ }
+
+ /* Parse this TIFF header */
+ var endian;
+ var magic_field;
+ var ifd_offset;
+ var primary_ifd, exif_ifd, gps_ifd;
+ var endian_field = this._binary_data.slice(pos, pos + 2);
+
+ /* Trivia: This 'I' is for Intel, the 'M' is for Motorola */
+ if (endian_field === "II") {
+ endian = "<";
+ } else if (endian_field === "MM") {
+ endian = ">";
+ } else {
+ throw new Error("Malformed TIFF meta-data. Unknown endianess: " + endian_field);
+ }
+
+ magic_field = JpegMeta.parseNum(endian, this._binary_data, pos + 2, 2);
+
+ if (magic_field !== 42) {
+ throw new Error("Malformed TIFF meta-data. Bad magic: " + magic_field);
+ }
+
+ ifd_offset = JpegMeta.parseNum(endian, this._binary_data, pos + 4, 4);
+
+ /* Parse 0th IFD */
+ this._parseIfd(endian, this._binary_data, pos, ifd_offset, this._tifftags, "tiff", "TIFF");
+
+ if (this.tiff.ExifIfdPointer) {
+ this._parseIfd(endian, this._binary_data, pos, this.tiff.ExifIfdPointer.value, this._exiftags, "exif", "Exif");
+ }
+
+ if (this.tiff.GPSInfoIfdPointer) {
+ this._parseIfd(endian, this._binary_data, pos, this.tiff.GPSInfoIfdPointer.value, this._gpstags, "gps", "GPS");
+ if (this.gps.GPSLatitude) {
+ var latitude;
+ latitude = this.gps.GPSLatitude.value[0].asFloat() +
+ (1 / 60) * this.gps.GPSLatitude.value[1].asFloat() +
+ (1 / 3600) * this.gps.GPSLatitude.value[2].asFloat();
+ if (this.gps.GPSLatitudeRef.value === "S") {
+ latitude = -latitude;
+ }
+ this.gps._addProperty("latitude", "Dec. Latitude", latitude);
+ }
+ if (this.gps.GPSLongitude) {
+ var longitude;
+ longitude = this.gps.GPSLongitude.value[0].asFloat() +
+ (1 / 60) * this.gps.GPSLongitude.value[1].asFloat() +
+ (1 / 3600) * this.gps.GPSLongitude.value[2].asFloat();
+ if (this.gps.GPSLongitudeRef.value === "W") {
+ longitude = -longitude;
+ }
+ this.gps._addProperty("longitude", "Dec. Longitude", longitude);
+ }
+ }
+ };
+
+ // MediaWiki: Add mw.libs wrapper
+ mw.libs.jpegmeta = function( fileReaderResult, fileName ) {
+ return new JpegMeta.JpegFile( fileReaderResult, fileName );
+ };
+
+}() );
diff --git a/resources/src/mediawiki.page/mediawiki.page.gallery.js b/resources/src/mediawiki.page/mediawiki.page.gallery.js
new file mode 100644
index 00000000..1892967a
--- /dev/null
+++ b/resources/src/mediawiki.page/mediawiki.page.gallery.js
@@ -0,0 +1,212 @@
+/*!
+ * Show gallery captions when focused. Copied directly from jquery.mw-jump.js.
+ * Also Dynamically resize images to justify them.
+ */
+( function ( $ ) {
+ $( function () {
+ var isTouchScreen,
+ gettingFocus,
+ galleries = 'ul.mw-gallery-packed-overlay, ul.mw-gallery-packed-hover, ul.mw-gallery-packed';
+
+ // Is there a better way to detect a touchscreen? Current check taken from stack overflow.
+ isTouchScreen = !!( window.ontouchstart !== undefined || window.DocumentTouch !== undefined && document instanceof window.DocumentTouch );
+
+ if ( isTouchScreen ) {
+ // Always show the caption for a touch screen.
+ $( 'ul.mw-gallery-packed-hover' )
+ .addClass( 'mw-gallery-packed-overlay' )
+ .removeClass( 'mw-gallery-packed-hover' );
+ } else {
+ // Note use of just "a", not a.image, since we want this to trigger if a link in
+ // the caption receives focus
+ $( 'ul.mw-gallery-packed-hover li.gallerybox' ).on( 'focus blur', 'a', function ( e ) {
+ // Confusingly jQuery leaves e.type as focusout for delegated blur events
+ gettingFocus = e.type !== 'blur' && e.type !== 'focusout';
+ $( this ).closest( 'li.gallerybox' ).toggleClass( 'mw-gallery-focused', gettingFocus );
+ } );
+ }
+
+ // Now on to justification.
+ // We may still get ragged edges if someone resizes their window. Could bind to
+ // that event, otoh do we really want to constantly be resizing galleries?
+ $( galleries ).each( function () {
+ var lastTop,
+ $img,
+ imgWidth,
+ imgHeight,
+ rows = [],
+ $gallery = $( this );
+
+ $gallery.children( 'li' ).each( function () {
+ // Math.floor to be paranoid if things are off by 0.00000000001
+ var top = Math.floor( $( this ).position().top ),
+ $this = $( this );
+
+ if ( top !== lastTop ) {
+ rows[rows.length] = [];
+ lastTop = top;
+ }
+
+ $img = $this.find( 'div.thumb a.image img' );
+ if ( $img.length && $img[0].height ) {
+ imgHeight = $img[0].height;
+ imgWidth = $img[0].width;
+ } else {
+ // If we don't have a real image, get the containing divs width/height.
+ // Note that if we do have a real image, using this method will generally
+ // give the same answer, but can be different in the case of a very
+ // narrow image where extra padding is added.
+ imgHeight = $this.children().children( 'div:first' ).height();
+ imgWidth = $this.children().children( 'div:first' ).width();
+ }
+
+ // Hack to make an edge case work ok
+ if ( imgHeight < 30 ) {
+ // Don't try and resize this item.
+ imgHeight = 0;
+ }
+
+ rows[rows.length - 1][rows[rows.length - 1].length] = {
+ $elm: $this,
+ width: $this.outerWidth(),
+ imgWidth: imgWidth,
+ // XXX: can divide by 0 ever happen?
+ aspect: imgWidth / imgHeight,
+ captionWidth: $this.children().children( 'div.gallerytextwrapper' ).width(),
+ height: imgHeight
+ };
+ } );
+
+ ( function () {
+ var maxWidth,
+ combinedAspect,
+ combinedPadding,
+ curRow,
+ curRowHeight,
+ wantedWidth,
+ preferredHeight,
+ newWidth,
+ padding,
+ $outerDiv,
+ $innerDiv,
+ $imageDiv,
+ $imageElm,
+ imageElm,
+ $caption,
+ i,
+ j,
+ avgZoom,
+ totalZoom = 0;
+
+ for ( i = 0; i < rows.length; i++ ) {
+ maxWidth = $gallery.width();
+ combinedAspect = 0;
+ combinedPadding = 0;
+ curRow = rows[i];
+ curRowHeight = 0;
+
+ for ( j = 0; j < curRow.length; j++ ) {
+ if ( curRowHeight === 0 ) {
+ if ( isFinite( curRow[j].height ) ) {
+ // Get the height of this row, by taking the first
+ // non-out of bounds height
+ curRowHeight = curRow[j].height;
+ }
+ }
+
+ if ( curRow[j].aspect === 0 || !isFinite( curRow[j].aspect ) ) {
+ // One of the dimensions are 0. Probably should
+ // not try to resize.
+ combinedPadding += curRow[j].width;
+ } else {
+ combinedAspect += curRow[j].aspect;
+ combinedPadding += curRow[j].width - curRow[j].imgWidth;
+ }
+ }
+
+ // Add some padding for inter-element spacing.
+ combinedPadding += 5 * curRow.length;
+ wantedWidth = maxWidth - combinedPadding;
+ preferredHeight = wantedWidth / combinedAspect;
+
+ if ( preferredHeight > curRowHeight * 1.5 ) {
+ // Only expand at most 1.5 times current size
+ // As that's as high a resolution as we have.
+ // Also on the off chance there is a bug in this
+ // code, would prevent accidentally expanding to
+ // be 10 billion pixels wide.
+ if ( i === rows.length - 1 ) {
+ // If its the last row, and we can't fit it,
+ // don't make the entire row huge.
+ avgZoom = ( totalZoom / ( rows.length - 1 ) ) * curRowHeight;
+ if ( isFinite( avgZoom ) && avgZoom >= 1 && avgZoom <= 1.5 ) {
+ preferredHeight = avgZoom;
+ } else {
+ // Probably a single row gallery
+ preferredHeight = curRowHeight;
+ }
+ } else {
+ preferredHeight = 1.5 * curRowHeight;
+ }
+ }
+ if ( !isFinite( preferredHeight ) ) {
+ // This *definitely* should not happen.
+ // Skip this row.
+ continue;
+ }
+ if ( preferredHeight < 5 ) {
+ // Well something clearly went wrong...
+ // Skip this row.
+ continue;
+ }
+
+ if ( preferredHeight / curRowHeight > 1 ) {
+ totalZoom += preferredHeight / curRowHeight;
+ } else {
+ // If we shrink, still consider that a zoom of 1
+ totalZoom += 1;
+ }
+
+ for ( j = 0; j < curRow.length; j++ ) {
+ newWidth = preferredHeight * curRow[j].aspect;
+ padding = curRow[j].width - curRow[j].imgWidth;
+ $outerDiv = curRow[j].$elm;
+ $innerDiv = $outerDiv.children( 'div' ).first();
+ $imageDiv = $innerDiv.children( 'div.thumb' );
+ $imageElm = $imageDiv.find( 'img' ).first();
+ imageElm = $imageElm.length ? $imageElm[0] : null;
+ $caption = $outerDiv.find( 'div.gallerytextwrapper' );
+
+ // Since we are going to re-adjust the height, the vertical
+ // centering margins need to be reset.
+ $imageDiv.children( 'div' ).css( 'margin', '0px auto' );
+
+ if ( newWidth < 60 || !isFinite( newWidth ) ) {
+ // Making something skinnier than this will mess up captions,
+ if ( newWidth < 1 || !isFinite( newWidth ) ) {
+ $innerDiv.height( preferredHeight );
+ // Don't even try and touch the image size if it could mean
+ // making it disappear.
+ continue;
+ }
+ } else {
+ $outerDiv.width( newWidth + padding );
+ $innerDiv.width( newWidth + padding );
+ $imageDiv.width( newWidth );
+ $caption.width( curRow[j].captionWidth + ( newWidth - curRow[j].imgWidth ) );
+ }
+
+ if ( imageElm ) {
+ // We don't always have an img, e.g. in the case of an invalid file.
+ imageElm.width = newWidth;
+ imageElm.height = preferredHeight;
+ } else {
+ // Not a file box.
+ $imageDiv.height( preferredHeight );
+ }
+ }
+ }
+ }() );
+ } );
+ } );
+}( jQuery ) );
diff --git a/resources/src/mediawiki.page/mediawiki.page.image.pagination.js b/resources/src/mediawiki.page/mediawiki.page.image.pagination.js
new file mode 100644
index 00000000..622e818d
--- /dev/null
+++ b/resources/src/mediawiki.page/mediawiki.page.image.pagination.js
@@ -0,0 +1,101 @@
+/*!
+ * Implement AJAX navigation for multi-page images so the user may browse without a full page reload.
+ */
+( function ( mw, $ ) {
+ var jqXhr, $multipageimage, $spinner;
+
+ /* Fetch the next page and use jQuery to swap the table.multipageimage contents.
+ * @param {string} url
+ * @param {boolean} [hist=false] Whether this is a load triggered by history navigation (if
+ * true, this function won't push a new history state, for the browser did so already).
+ */
+ function loadPage( url, hist ) {
+ var $tr;
+ if ( jqXhr ) {
+ // Prevent race conditions and piling up pending requests
+ jqXhr.abort();
+ jqXhr = undefined;
+ }
+
+ // Add a new spinner if one doesn't already exist
+ if ( !$spinner ) {
+ $tr = $multipageimage.find( 'tr' );
+ $spinner = $.createSpinner( {
+ size: 'large',
+ type: 'block'
+ } )
+ // Copy the old content dimensions equal so that the current scroll position is not
+ // lost between emptying the table is and receiving the new contents.
+ .css( {
+ height: $tr.outerHeight(),
+ width: $tr.outerWidth()
+ } );
+
+ $multipageimage.empty().append( $spinner );
+ }
+
+ // @todo Don't fetch the entire page. Ideally we'd only fetch the content portion or the data
+ // (thumbnail urls) and update the interface manually.
+ jqXhr = $.ajax( url ).done( function ( data ) {
+ jqXhr = $spinner = undefined;
+
+ // Replace table contents
+ $multipageimage.empty().append( $( data ).find( 'table.multipageimage' ).contents() );
+
+ bindPageNavigation( $multipageimage );
+
+ // Fire hook because the page's content has changed
+ mw.hook( 'wikipage.content' ).fire( $multipageimage );
+
+ // Update browser history and address bar. But not if we came here from a history
+ // event, in which case the url is already updated by the browser.
+ if ( history.pushState && !hist ) {
+ history.pushState( { tag: 'mw-pagination' }, document.title, url );
+ }
+ } );
+ }
+
+ function bindPageNavigation( $container ) {
+ $container.find( '.multipageimagenavbox' ).one( 'click', 'a', function ( e ) {
+ var page, uri;
+
+ // Generate the same URL on client side as the one generated in ImagePage::openShowImage.
+ // We avoid using the URL in the link directly since it could have been manipulated (bug 66608)
+ page = Number( mw.util.getParamValue( 'page', this.href ) );
+ uri = new mw.Uri( mw.util.wikiScript() )
+ .extend( { title: mw.config.get( 'wgPageName' ), page: page } )
+ .toString();
+
+ loadPage( uri );
+ e.preventDefault();
+ } );
+
+ $container.find( 'form[name="pageselector"]' ).one( 'change submit', function ( e ) {
+ loadPage( this.action + '?' + $( this ).serialize() );
+ e.preventDefault();
+ } );
+ }
+
+ $( function () {
+ if ( mw.config.get( 'wgNamespaceNumber' ) !== 6 ) {
+ return;
+ }
+ $multipageimage = $( 'table.multipageimage' );
+ if ( !$multipageimage.length ) {
+ return;
+ }
+
+ bindPageNavigation( $multipageimage );
+
+ // Update the url using the History API (if available)
+ if ( history.pushState && history.replaceState ) {
+ history.replaceState( { tag: 'mw-pagination' }, '' );
+ $( window ).on( 'popstate', function ( e ) {
+ var state = e.originalEvent.state;
+ if ( state && state.tag === 'mw-pagination' ) {
+ loadPage( location.href, true );
+ }
+ } );
+ }
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js b/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js
new file mode 100644
index 00000000..cc72e168
--- /dev/null
+++ b/resources/src/mediawiki.page/mediawiki.page.patrol.ajax.js
@@ -0,0 +1,65 @@
+/*!
+ * Animate patrol links to use asynchronous API requests to
+ * patrol pages, rather than navigating to a different URI.
+ *
+ * @since 1.21
+ * @author Marius Hoch <hoo@online.de>
+ */
+( function ( mw, $ ) {
+ if ( !mw.user.tokens.exists( 'patrolToken' ) ) {
+ // Current user has no patrol right, or an old cached version of user.tokens
+ // that didn't have patrolToken yet.
+ return;
+ }
+ $( function () {
+ var $patrolLinks = $( '.patrollink a' );
+ $patrolLinks.on( 'click', function ( e ) {
+ var $spinner, href, rcid, apiRequest;
+
+ // Start preloading the notification module (normally loaded by mw.notify())
+ mw.loader.load( ['mediawiki.notification'], null, true );
+
+ // Hide the link and create a spinner to show it inside the brackets.
+ $spinner = $.createSpinner( {
+ size: 'small',
+ type: 'inline'
+ } );
+ $( this ).hide().after( $spinner );
+
+ href = $( this ).attr( 'href' );
+ rcid = mw.util.getParamValue( 'rcid', href );
+ apiRequest = new mw.Api();
+
+ apiRequest.postWithToken( 'patrol', {
+ action: 'patrol',
+ rcid: rcid
+ } )
+ .done( function ( data ) {
+ // Remove all patrollinks from the page (including any spinners inside).
+ $patrolLinks.closest( '.patrollink' ).remove();
+ if ( data.patrol !== undefined ) {
+ // Success
+ var title = new mw.Title( data.patrol.title );
+ mw.notify( mw.msg( 'markedaspatrollednotify', title.toText() ) );
+ } else {
+ // This should never happen as errors should trigger fail
+ mw.notify( mw.msg( 'markedaspatrollederrornotify' ) );
+ }
+ } )
+ .fail( function ( error ) {
+ $spinner.remove();
+ // Restore the patrol link. This allows the user to try again
+ // (or open it in a new window, bypassing this ajax module).
+ $patrolLinks.show();
+ if ( error === 'noautopatrol' ) {
+ // Can't patrol own
+ mw.notify( mw.msg( 'markedaspatrollederror-noautopatrol' ) );
+ } else {
+ mw.notify( mw.msg( 'markedaspatrollederrornotify' ) );
+ }
+ } );
+
+ e.preventDefault();
+ } );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.page/mediawiki.page.ready.js b/resources/src/mediawiki.page/mediawiki.page.ready.js
new file mode 100644
index 00000000..246cc817
--- /dev/null
+++ b/resources/src/mediawiki.page/mediawiki.page.ready.js
@@ -0,0 +1,64 @@
+( function ( mw, $ ) {
+ var supportsPlaceholder = 'placeholder' in document.createElement( 'input' );
+
+ // Break out of framesets
+ if ( mw.config.get( 'wgBreakFrames' ) ) {
+ // Note: In IE < 9 strict comparison to window is non-standard (the standard didn't exist yet)
+ // it works only comparing to window.self or window.window (http://stackoverflow.com/q/4850978/319266)
+ if ( window.top !== window.self ) {
+ // Un-trap us from framesets
+ window.top.location = window.location;
+ }
+ }
+
+ mw.hook( 'wikipage.content' ).add( function ( $content ) {
+ var $sortableTables;
+
+ // Run jquery.placeholder polyfill if placeholder is not supported
+ if ( !supportsPlaceholder ) {
+ $content.find( 'input[placeholder]' ).placeholder();
+ }
+
+ // Run jquery.makeCollapsible
+ $content.find( '.mw-collapsible' ).makeCollapsible();
+
+ // Lazy load jquery.tablesorter
+ $sortableTables = $content.find( 'table.sortable' );
+ if ( $sortableTables.length ) {
+ mw.loader.using( 'jquery.tablesorter', function () {
+ $sortableTables.tablesorter();
+ } );
+ }
+
+ // Run jquery.checkboxShiftClick
+ $content.find( 'input[type="checkbox"]:not(.noshiftselect)' ).checkboxShiftClick();
+ } );
+
+ // Things outside the wikipage content
+ $( function () {
+ var $nodes;
+
+ if ( !supportsPlaceholder ) {
+ // Exclude content to avoid hitting it twice for the (first) wikipage content
+ $( 'input[placeholder]' ).not( '#mw-content-text input' ).placeholder();
+ }
+
+ // Add accesskey hints to the tooltips
+ if ( document.querySelectorAll ) {
+ // If we're running on a browser where we can do this efficiently,
+ // just find all elements that have accesskeys. We can't use jQuery's
+ // polyfill for the selector since looping over all elements on page
+ // load might be too slow.
+ $nodes = $( document.querySelectorAll( '[accesskey]' ) );
+ } else {
+ // Otherwise go through some elements likely to have accesskeys rather
+ // than looping over all of them. Unfortunately this will not fully
+ // work for custom skins with different HTML structures. Input, label
+ // and button should be rare enough that no optimizations are needed.
+ $nodes = $( '#column-one a, #mw-head a, #mw-panel a, #p-logo a, input, label, button' );
+ }
+ $nodes.updateTooltipAccessKeys();
+
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.page/mediawiki.page.startup.js b/resources/src/mediawiki.page/mediawiki.page.startup.js
new file mode 100644
index 00000000..4aae6069
--- /dev/null
+++ b/resources/src/mediawiki.page/mediawiki.page.startup.js
@@ -0,0 +1,33 @@
+( function ( mw, $ ) {
+
+ mw.page = {};
+
+ // Client profile classes for <html>
+ // Allows for easy hiding/showing of JS or no-JS-specific UI elements
+ $( 'html' )
+ .addClass( 'client-js' )
+ .removeClass( 'client-nojs' );
+
+ $( function () {
+ mw.util.init();
+
+ /**
+ * Fired when wiki content is being added to the DOM
+ *
+ * It is encouraged to fire it before the main DOM is changed (when $content
+ * is still detatched). However, this order is not defined either way, so you
+ * should only rely on $content itself.
+ *
+ * This includes the ready event on a page load (including post-edit loads)
+ * and when content has been previewed with LivePreview.
+ *
+ * @event wikipage_content
+ * @member mw.hook
+ * @param {jQuery} $content The most appropriate element containing the content,
+ * such as #mw-content-text (regular content root) or #wikiPreview (live preview
+ * root)
+ */
+ mw.hook( 'wikipage.content' ).fire( $( '#mw-content-text' ) );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.page/mediawiki.page.watch.ajax.js b/resources/src/mediawiki.page/mediawiki.page.watch.ajax.js
new file mode 100644
index 00000000..d252f0e4
--- /dev/null
+++ b/resources/src/mediawiki.page/mediawiki.page.watch.ajax.js
@@ -0,0 +1,178 @@
+/**
+ * Animate watch/unwatch links to use asynchronous API requests to
+ * watch pages, rather than navigating to a different URI.
+ *
+ * @class mw.page.watch.ajax
+ */
+( function ( mw, $ ) {
+ // The name of the page to watch or unwatch
+ var title = mw.config.get( 'wgRelevantPageName' );
+
+ /**
+ * Update the link text, link href attribute and (if applicable)
+ * "loading" class.
+ *
+ * @param {jQuery} $link Anchor tag of (un)watch link
+ * @param {string} action One of 'watch', 'unwatch'
+ * @param {string} [state="idle"] 'idle' or 'loading'. Default is 'idle'
+ */
+ function updateWatchLink( $link, action, state ) {
+ var msgKey, $li, otherAction;
+
+ // A valid but empty jQuery object shouldn't throw a TypeError
+ if ( !$link.length ) {
+ return;
+ }
+
+ // Invalid actions shouldn't silently turn the page in an unrecoverable state
+ if ( action !== 'watch' && action !== 'unwatch' ) {
+ throw new Error( 'Invalid action' );
+ }
+
+ // message keys 'watch', 'watching', 'unwatch' or 'unwatching'.
+ msgKey = state === 'loading' ? action + 'ing' : action;
+ otherAction = action === 'watch' ? 'unwatch' : 'watch';
+ $li = $link.closest( 'li' );
+
+ // Trigger a 'watchpage' event for this List item.
+ // Announce the otherAction value as the first param.
+ // Used to monitor the state of watch link.
+ // TODO: Revise when system wide hooks are implemented
+ if ( state === undefined ) {
+ $li.trigger( 'watchpage.mw', otherAction );
+ }
+
+ $link
+ .text( mw.msg( msgKey ) )
+ .attr( 'title', mw.msg( 'tooltip-ca-' + action ) )
+ .updateTooltipAccessKeys()
+ .attr( 'href', mw.util.wikiScript() + '?' + $.param( {
+ title: title,
+ action: action
+ } )
+ );
+
+ // Most common ID style
+ if ( $li.prop( 'id' ) === 'ca-' + otherAction ) {
+ $li.prop( 'id', 'ca-' + action );
+ }
+
+ if ( state === 'loading' ) {
+ $link.addClass( 'loading' );
+ } else {
+ $link.removeClass( 'loading' );
+ }
+ }
+
+ /**
+ * TODO: This should be moved somewhere more accessible.
+ *
+ * @private
+ * @param {string} url
+ * @return {string} The extracted action, defaults to 'view'
+ */
+ function mwUriGetAction( url ) {
+ var action, actionPaths, key, i, m, parts;
+
+ // TODO: Does MediaWiki give action path or query param
+ // precedence? If the former, move this to the bottom
+ action = mw.util.getParamValue( 'action', url );
+ if ( action !== null ) {
+ return action;
+ }
+
+ actionPaths = mw.config.get( 'wgActionPaths' );
+ for ( key in actionPaths ) {
+ if ( actionPaths.hasOwnProperty( key ) ) {
+ parts = actionPaths[key].split( '$1' );
+ for ( i = 0; i < parts.length; i++ ) {
+ parts[i] = $.escapeRE( parts[i] );
+ }
+ m = new RegExp( parts.join( '(.+)' ) ).exec( url );
+ if ( m && m[1] ) {
+ return key;
+ }
+
+ }
+ }
+
+ return 'view';
+ }
+
+ // Expose public methods
+ mw.page.watch = {
+ updateWatchLink: updateWatchLink
+ };
+
+ $( function () {
+ var $links = $( '.mw-watchlink a, a.mw-watchlink, ' +
+ '#ca-watch a, #ca-unwatch a, #mw-unwatch-link1, ' +
+ '#mw-unwatch-link2, #mw-watch-link2, #mw-watch-link1' );
+
+ // Allowing people to add inline animated links is a little scary
+ $links = $links.filter( ':not( #bodyContent *, #content * )' );
+
+ $links.click( function ( e ) {
+ var action, api, $link;
+
+ // Start preloading the notification module (normally loaded by mw.notify())
+ mw.loader.load( ['mediawiki.notification'], null, true );
+
+ action = mwUriGetAction( this.href );
+
+ if ( action !== 'watch' && action !== 'unwatch' ) {
+ // Could not extract target action from link url,
+ // let native browsing handle it further
+ return true;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+
+ $link = $( this );
+
+ if ( $link.hasClass( 'loading' ) ) {
+ return;
+ }
+
+ updateWatchLink( $link, action, 'loading' );
+
+ api = new mw.Api();
+
+ api[action]( title )
+ .done( function ( watchResponse ) {
+ var otherAction = action === 'watch' ? 'unwatch' : 'watch';
+
+ mw.notify( $.parseHTML( watchResponse.message ), {
+ tag: 'watch-self'
+ } );
+
+ // Set link to opposite
+ updateWatchLink( $link, otherAction );
+
+ // Update the "Watch this page" checkbox on action=edit when the
+ // page is watched or unwatched via the tab (bug 12395).
+ $( '#wpWatchthis' ).prop( 'checked', watchResponse.watched !== undefined );
+ } )
+ .fail( function () {
+ var cleanTitle, msg, link;
+
+ // Reset link to non-loading mode
+ updateWatchLink( $link, action );
+
+ // Format error message
+ cleanTitle = title.replace( /_/g, ' ' );
+ link = mw.html.element(
+ 'a', {
+ href: mw.util.getUrl( title ),
+ title: cleanTitle
+ }, cleanTitle
+ );
+ msg = mw.message( 'watcherrortext', link );
+
+ // Report to user about the error
+ mw.notify( msg, { tag: 'watch-self' } );
+ } );
+ } );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.skinning/content.css b/resources/src/mediawiki.skinning/content.css
new file mode 100644
index 00000000..7a417081
--- /dev/null
+++ b/resources/src/mediawiki.skinning/content.css
@@ -0,0 +1,227 @@
+/**
+ * MediaWiki style sheet for general styles on complex content
+ *
+ * Styles for complex things which are a standard part of page content
+ * (ie: the CSS classing built into the system), like the TOC.
+ */
+
+/* Table of Contents */
+#toc,
+.toc,
+.mw-warning {
+ border: 1px solid #aaa;
+ background-color: #f9f9f9;
+ padding: 5px;
+ font-size: 95%;
+}
+
+/**
+ * We want to display the ToC element with intrinsic width in block mode. The fit-content
+ * value for width is however not supported by large groups of browsers.
+ *
+ * We use display:table. Even though it should only contain other table-* display
+ * elements, there are no known problems with using this.
+ *
+ * Because IE < 8, FF 2 and other older browsers don't support display:table, we fallback to
+ * using inline-block mode, which features at least intrinsic width, but won't clear preceding
+ * inline elements. In practice inline elements surrounding the TOC are uncommon enough that
+ * this is an acceptable sacrifice.
+ */
+#toc,
+.toc {
+ display: -moz-inline-block;
+ display: inline-block;
+ display: table;
+
+ /* IE7 and earlier */
+ zoom: 1;
+ *display: inline;
+
+ padding: 7px;
+}
+
+/* CSS for backwards-compatibility with cached page renders and creative uses in wikitext */
+table#toc,
+table.toc {
+ border-collapse: collapse;
+}
+
+/* Remove additional paddings inside table-cells that are not present in <div>s */
+table#toc td,
+table.toc td {
+ padding: 0;
+}
+
+#toc h2,
+.toc h2 {
+ display: inline;
+ border: none;
+ padding: 0;
+ font-size: 100%;
+ font-weight: bold;
+}
+
+#toc #toctitle,
+.toc #toctitle,
+#toc .toctitle,
+.toc .toctitle {
+ text-align: center;
+}
+
+#toc ul,
+.toc ul {
+ list-style-type: none;
+ list-style-image: none;
+ margin-left: 0;
+ padding: 0;
+ text-align: left;
+}
+
+#toc ul ul,
+.toc ul ul {
+ margin: 0 0 0 2em;
+}
+
+#toc .toctoggle,
+.toc .toctoggle {
+ font-size: 94%;
+}
+
+.toccolours {
+ border: 1px solid #aaa;
+ background-color: #f9f9f9;
+ padding: 5px;
+ font-size: 95%;
+}
+
+/* Warning */
+.mw-warning {
+ margin-left: 50px;
+ margin-right: 50px;
+ text-align: center;
+}
+
+/* Images */
+/* @noflip */div.floatright, table.floatright {
+ margin: 0 0 .5em .5em;
+ border: 0;
+}
+
+div.floatright p {
+ font-style: italic;
+}
+
+/* @noflip */div.floatleft, table.floatleft {
+ margin: 0 .5em .5em 0;
+ border: 0;
+}
+
+div.floatleft p {
+ font-style: italic;
+}
+
+/* Thumbnails */
+div.thumb {
+ margin-bottom: .5em;
+ width: auto;
+ background-color: transparent;
+}
+
+div.thumbinner {
+ border: 1px solid #ccc;
+ padding: 3px;
+ background-color: #f9f9f9;
+ font-size: 94%;
+ text-align: center;
+ overflow: hidden;
+}
+
+html .thumbimage {
+ border: 1px solid #ccc;
+}
+
+html .thumbcaption {
+ border: none;
+ line-height: 1.4em;
+ padding: 3px;
+ font-size: 94%;
+ /* Default styles when there's no .mw-content-ltr or .mw-content-rtl, overridden below */
+ text-align: left;
+}
+
+div.magnify {
+ /* Default styles when there's no .mw-content-ltr or .mw-content-rtl, overridden below */
+ float: right;
+ margin-left: 3px;
+}
+
+div.magnify a {
+ display: block;
+ /* Hide the text… */
+ text-indent: 15px;
+ white-space: nowrap;
+ overflow: hidden;
+ /* …and replace it with the image */
+ width: 15px;
+ height: 11px;
+ /* Default styles when there's no .mw-content-ltr or .mw-content-rtl, overridden below */
+ /* @embed */
+ background: url(images/magnify-clip-ltr.png) center center no-repeat;
+ /* Don't annoy people who copy-paste everything too much */
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+img.thumbborder {
+ border: 1px solid #dddddd;
+}
+
+/* Directionality-specific styles for thumbnails - their positioning depends on content language */
+
+/* @noflip */
+.mw-content-ltr .thumbcaption {
+ text-align: left;
+}
+
+/* @noflip */
+.mw-content-ltr .magnify {
+ float: right;
+ margin-left: 3px;
+ margin-right: 0;
+}
+
+/* @noflip */
+.mw-content-ltr div.magnify a {
+ /* @embed */
+ background-image: url(images/magnify-clip-ltr.png);
+}
+
+/* @noflip */
+.mw-content-rtl .thumbcaption {
+ text-align: right;
+}
+
+/* @noflip */
+.mw-content-rtl .magnify {
+ float: left;
+ margin-left: 0;
+ margin-right: 3px;
+}
+
+/* @noflip */
+.mw-content-rtl div.magnify a {
+ /* @embed */
+ background-image: url(images/magnify-clip-rtl.png);
+}
+
+/* @noflip */
+div.tright {
+ margin: .5em 0 1.3em 1.4em;
+}
+
+/* @noflip */
+div.tleft {
+ margin: .5em 1.4em 1.3em 0;
+}
diff --git a/resources/src/mediawiki.skinning/content.externallinks.css b/resources/src/mediawiki.skinning/content.externallinks.css
new file mode 100644
index 00000000..d23540da
--- /dev/null
+++ b/resources/src/mediawiki.skinning/content.externallinks.css
@@ -0,0 +1,102 @@
+/*!
+ * Icons and colors for external links.
+ */
+
+/* Bug 66091 is blocking is from converting this file to LESS
+ * and using the .background-image-svg mixin. */
+
+/* SVG support using a transparent gradient to guarantee cross-browser
+ * compatibility (browsers able to understand gradient syntax support also SVG).
+ * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique */
+
+.mw-body a.external,
+.link-https {
+ background: url(images/external-ltr.png) center right no-repeat;
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/external-ltr.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/external-ltr.svg);
+ padding-right: 15px;
+}
+
+.mw-body a.external[href^="mailto:"],
+.link-mailto {
+ background: url(images/mail.png) center right no-repeat;
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/mail.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/mail.svg);
+ padding-right: 15px;
+}
+
+.mw-body a.external[href^="ftp://"],
+.link-ftp {
+ background: url(images/ftp-ltr.png) center right no-repeat;
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/ftp-ltr.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/ftp-ltr.svg);
+ padding-right: 15px;
+}
+
+.mw-body a.external[href^="irc://"],
+.mw-body a.external[href^="ircs://"],
+.link-irc {
+ background: url(images/chat-ltr.png) center right no-repeat;
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/chat-ltr.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/chat-ltr.svg);
+ padding-right: 15px;
+}
+
+.mw-body a.external[href$=".ogg"], .mw-body a.external[href$=".OGG"],
+.mw-body a.external[href$=".mid"], .mw-body a.external[href$=".MID"],
+.mw-body a.external[href$=".midi"], .mw-body a.external[href$=".MIDI"],
+.mw-body a.external[href$=".mp3"], .mw-body a.external[href$=".MP3"],
+.mw-body a.external[href$=".wav"], .mw-body a.external[href$=".WAV"],
+.mw-body a.external[href$=".wma"], .mw-body a.external[href$=".WMA"],
+.link-audio {
+ background: url(images/audio-ltr.png) center right no-repeat;
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/audio-ltr.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/audio-ltr.svg);
+ padding-right: 15px;
+}
+
+.mw-body a.external[href$=".ogm"], .mw-body a.external[href$=".OGM"],
+.mw-body a.external[href$=".avi"], .mw-body a.external[href$=".AVI"],
+.mw-body a.external[href$=".mpeg"], .mw-body a.external[href$=".MPEG"],
+.mw-body a.external[href$=".mpg"], .mw-body a.external[href$=".MPG"],
+.link-video {
+ background: url(images/video.png) center right no-repeat;
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/video.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/video.svg);
+ padding-right: 15px;
+}
+
+.mw-body a.external[href$=".pdf"], .mw-body a.external[href$=".PDF"],
+.mw-body a.external[href*=".pdf#"], .mw-body a.external[href*=".PDF#"],
+.mw-body a.external[href*=".pdf?"], .mw-body a.external[href*=".PDF?"],
+.link-document {
+ background: url(images/document-ltr.png) center right no-repeat;
+ /* @embed */
+ background-image: -webkit-linear-gradient(transparent, transparent), url(images/document-ltr.svg);
+ /* @embed */
+ background-image: linear-gradient(transparent, transparent), url(images/document-ltr.svg);
+ padding-right: 15px;
+}
+
+/* Interwiki styling */
+.mw-body a.extiw,
+.mw-body a.extiw:active {
+ color: #36b;
+}
+
+/* External link color */
+.mw-body a.external {
+ color: #36b;
+}
diff --git a/resources/src/mediawiki.skinning/content.parsoid.less b/resources/src/mediawiki.skinning/content.parsoid.less
new file mode 100644
index 00000000..a6515d2e
--- /dev/null
+++ b/resources/src/mediawiki.skinning/content.parsoid.less
@@ -0,0 +1,131 @@
+/**
+ * Style Parsoid HTML+RDFa output consistent with wikitext from PHP parser.
+ */
+
+/*csslint regex-selectors:false */
+
+/*
+ * Auto-numbered external links
+ * Parsoid renders those as link without content, and lets CSS do the
+ * counting. This way the counting style can be customized, and counts update
+ * automatically when content is modified.
+ */
+.mw-body-content {
+ counter-reset: mw-NumberedExtLink;
+}
+
+.mw-body-content a[rel~="mw:ExtLink"]:empty:after {
+ content: "[" counter(mw-NumberedExtLink) "]";
+ counter-increment: mw-NumberedExtLink;
+}
+
+/**
+ * References
+ *
+ * Parser and Extension:Cite output reference numbers for <sup>[1]</sup> for <ref> tags.
+ *
+ * Markup:
+ * Cake is good<sup>[2]</sup>
+ * The cake is a lie<span class="reference">[1]</span>
+ *
+ * Styleguide 1.1.
+ */
+span.reference {
+ font-size: 80%;
+ line-height: 1;
+ vertical-align: super;
+ unicode-bidi: -moz-isolate;
+ unicode-bidi: -webkit-isolate;
+ unicode-bidi: isolate;
+}
+
+sup, sub {
+ line-height: 1;
+}
+
+/**
+ * Block media items
+ */
+figure[typeof*='mw:Image'] {
+ margin: 0;
+
+ a {
+ border: 0;
+ }
+
+ &.mw-halign-right {
+ /* @noflip */
+ margin: .5em 0 1.3em 1.4em;
+ /* @noflip */
+ clear: right;
+ /* @noflip */
+ float: right;
+ }
+
+ &.mw-halign-left {
+ /* @noflip */
+ margin: .5em 1.4em 1.3em 0;
+ /* @noflip */
+ clear: left;
+ /* @noflip */
+ float: left;
+ }
+
+ &.mw-halign-none {
+ margin: 0;
+ clear: none;
+ float: none;
+ }
+
+ &.mw-halign-center {
+ margin: 0 auto .5em auto;
+ display: table;
+ clear: none;
+ float: none;
+ }
+
+ > figcaption {
+ display: table-caption;
+ caption-side: bottom;
+ /* In mw-core the font-size is duplicated, 94% in thumbiner
+ and again 94% in thumbcaption. 88% for font size of the
+ caption results in the same behavior. */
+ font-size: 88%;
+ line-height: 1.4em;
+ text-align: left;
+
+ border: 1px solid #ccc;
+ border-top: 0;
+
+ /* taken from .thumbcaption, plus .thumbinner */
+ padding: 1px 5px 5px;
+ background-color: #f9f9f9;
+ }
+}
+
+figure[typeof~='mw:Image/Thumb'],
+figure[typeof~='mw:Image/Frame'] {
+ display: table;
+ overflow: auto;
+ text-align: center;
+ border: 1px solid #ccc;
+ border-bottom: 0; // No border to caption
+ border-collapse: collapse;
+ background-color: #f9f9f9;
+ // Default to right alignment. This is needed since Parsoid only specifies the
+ // alignment class when the alignment is explicitly set.
+ margin: .5em 0 1.3em 1.4em;
+ clear: right;
+ float: right;
+}
+
+figure[typeof~='mw:Image/Thumb'] > *:first-child > img,
+figure[typeof~='mw:Image/Frame'] > *:first-child > img,
+.mw-image-border > *:first-child > img {
+ border: 1px solid #cccccc;
+ margin: 3px;
+}
+
+/* Hide the caption for frameless and plain floated images */
+figure[typeof~="mw:Image/Frameless"] > figcaption,
+figure[typeof~="mw:Image"] > figcaption { display: none }
diff --git a/resources/src/mediawiki.skinning/elements.css b/resources/src/mediawiki.skinning/elements.css
new file mode 100644
index 00000000..392a2a66
--- /dev/null
+++ b/resources/src/mediawiki.skinning/elements.css
@@ -0,0 +1,273 @@
+/**
+ * MediaWiki style sheet for general styles on basic content elements
+ *
+ * Styles for basic elements: links, lists, etc...
+ *
+ * This style sheet is used by the Monobook and Vector skins.
+ */
+
+/* Links */
+a {
+ text-decoration: none;
+ color: #0645ad;
+ background: none;
+}
+
+a:visited {
+ color: #0b0080;
+}
+
+a:active {
+ color: #faa700;
+}
+
+a:hover, a:focus {
+ text-decoration: underline;
+}
+
+a.stub {
+ color: #772233;
+}
+
+a.new, #p-personal a.new {
+ color: #ba0000;
+}
+
+a.new:visited, #p-personal a.new:visited {
+ color: #a55858;
+}
+
+/* Interwiki Styling */
+.mw-body a.extiw,
+.mw-body a.extiw:active {
+ color: #36b;
+}
+
+.mw-body a.extiw:visited {
+ color: #636;
+}
+
+.mw-body a.extiw:active {
+ color: #b63;
+}
+
+/* External links */
+.mw-body a.external {
+ color: #36b;
+}
+
+.mw-body a.external:visited {
+ color: #636; /* bug 3112 */
+}
+
+.mw-body a.external:active {
+ color: #b63;
+}
+
+/* Inline Elements */
+img {
+ border: none;
+ vertical-align: middle;
+}
+
+hr {
+ height: 1px;
+ color: #aaa;
+ background-color: #aaa;
+ border: 0;
+ margin: .2em 0;
+}
+
+/* Structural Elements */
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ color: black;
+ background: none;
+ font-weight: normal;
+ margin: 0;
+ overflow: hidden;
+ padding-top: .5em;
+ padding-bottom: .17em;
+ border-bottom: 1px solid #aaa;
+}
+
+h1 {
+ font-size: 188%;
+}
+
+h2 {
+ font-size: 150%;
+}
+
+h3,
+h4,
+h5,
+h6 {
+ border-bottom: none;
+ font-weight: bold;
+}
+
+h3 {
+ font-size: 132%;
+}
+
+h4 {
+ font-size: 116%;
+}
+
+h5 {
+ font-size: 108%;
+}
+
+h6 {
+ font-size: 100%;
+}
+
+/* Some space under the headers in the content area */
+h1,
+h2 {
+ margin-bottom: .6em;
+}
+
+h3,
+h4,
+h5 {
+ margin-bottom: .3em;
+}
+
+p {
+ margin: .4em 0 .5em 0;
+ line-height: 1.5em;
+}
+
+p img {
+ margin: 0;
+}
+
+ul {
+ line-height: 1.5em;
+ list-style-type: square;
+ margin: .3em 0 0 1.6em;
+ padding: 0;
+}
+
+ol {
+ line-height: 1.5em;
+ margin: .3em 0 0 3.2em;
+ padding: 0;
+ list-style-image: none;
+}
+
+li {
+ margin-bottom: .1em;
+}
+
+dt {
+ font-weight: bold;
+ margin-bottom: .1em;
+}
+
+dl {
+ margin-top: .2em;
+ margin-bottom: .5em;
+}
+
+dd {
+ line-height: 1.5em;
+ margin-left: 1.6em;
+ margin-bottom: .1em;
+}
+
+/* IE 6 and 7 lack support for quotes aroud the <q> element ('::before' and '::after'
+ pseudoelements, 'quotes' property). Let's italicize it instead (using the star hack). */
+q {
+ *font-style: italic;
+}
+
+pre, code, tt, kbd, samp, .mw-code {
+ /*
+ * Some browsers will render the monospace text too small, namely Firefox, Chrome and Safari.
+ * Specifying any valid, second value will trigger correct behavior without forcing a different font.
+ */
+ font-family: monospace, Courier;
+}
+
+code {
+ color: black;
+ background-color: #f9f9f9;
+ border: 1px solid #ddd;
+ border-radius: 2px;
+ padding: 1px 4px;
+}
+
+pre, .mw-code {
+ color: black;
+ background-color: #f9f9f9;
+ border: 1px solid #ddd;
+ padding: 1em;
+}
+
+/* Tables */
+table {
+ font-size: 100%;
+}
+
+/* Forms */
+fieldset {
+ border: 1px solid #2f6fab;
+ margin: 1em 0 1em 0;
+ padding: 0 1em 1em;
+ line-height: 1.5em;
+}
+
+fieldset.nested {
+ margin: 0 0 0.5em 0;
+ padding: 0 0.5em 0.5em;
+}
+
+legend {
+ padding: .5em;
+ font-size: 95%;
+}
+
+form {
+ border: none;
+ margin: 0;
+}
+
+textarea {
+ width: 100%;
+ padding: .1em;
+ display: block;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+select {
+ vertical-align: top;
+}
+
+/* Emulate Center */
+.center {
+ width: 100%;
+ text-align: center;
+}
+
+*.center * {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* Small for tables and similar */
+.small {
+ font-size: 94%;
+}
+
+table.small {
+ font-size: 100%;
+}
diff --git a/resources/src/mediawiki.skinning/images/audio-ltr.png b/resources/src/mediawiki.skinning/images/audio-ltr.png
new file mode 100644
index 00000000..8efc4f2d
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/audio-ltr.png
Binary files differ
diff --git a/resources/src/mediawiki.skinning/images/audio-ltr.svg b/resources/src/mediawiki.skinning/images/audio-ltr.svg
new file mode 100644
index 00000000..e27a5f53
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/audio-ltr.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g transform="translate(-36.467808,-258.39005)">
+<path style="opacity:1;fill:#3366bb;" d="m43.47,259.4-3,3-3,0,0,4,3,0,3,3zm-1,2.5,0,5-1.5-1.5-2.5,0,0-2,2.5,0z"/>
+<path style="opacity:1;fill:#15a5ea;" d="m43.9,262.5c0-0.6213,0.6213-1.243,1.243-0.6213,0,0,0.6213,0.6213,0.6213,2.485s-0.6213,2.485-0.6213,2.485c-0.6213,0.6213-1.243,0-1.243-0.6213,0,0,0.6213-0.6213,0.6213-1.864s-0.6213-1.864-0.6213-1.864z"/>
+<path style="opacity:1;fill:#15a5ea;" d="m45.76,261.2c0-0.6213,0.6213-1.243,1.243-0.6213,0,0,1.243,1.243,1.243,3.728s-1.243,3.728-1.243,3.728c-0.6213,0.6213-1.243,0-1.243-0.6213,0,0,1.243-1.243,1.243-3.107s-1.243-3.107-1.243-3.107z"/>
+</g>
+</svg>
diff --git a/resources/src/mediawiki.skinning/images/audio-rtl.png b/resources/src/mediawiki.skinning/images/audio-rtl.png
new file mode 100644
index 00000000..1afdf404
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/audio-rtl.png
Binary files differ
diff --git a/resources/src/mediawiki.skinning/images/audio-rtl.svg b/resources/src/mediawiki.skinning/images/audio-rtl.svg
new file mode 100644
index 00000000..683bbcd7
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/audio-rtl.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g transform="translate(-36.467808,-258.39005)">
+<path style="opacity:1;fill:#3366bb;" d="m42.47,259.4,3,3,3,0,0,4-3,0-3,3zm1,2.5,0,5,1.5-1.5,2.5,0,0-2-2.5,0z"/>
+<path style="opacity:1;fill:#15a5ea;" d="m42.04,262.5c0-0.6213-0.6213-1.243-1.243-0.6213,0,0-0.6213,0.6213-0.6213,2.485s0.6213,2.485,0.6213,2.485c0.6213,0.6213,1.243,0,1.243-0.6213,0,0-0.6213-0.6213-0.6213-1.864s0.6213-1.864,0.6213-1.864z"/>
+<path style="opacity:1;fill:#15a5ea;" d="m40.17,261.2c0-0.6213-0.6213-1.243-1.243-0.6213,0,0-1.243,1.243-1.243,3.728s1.243,3.728,1.243,3.728c0.6213,0.6213,1.243,0,1.243-0.6213,0,0-1.243-1.243-1.243-3.107s1.243-3.107,1.243-3.107z"/>
+</g>
+</svg>
diff --git a/resources/src/mediawiki.skinning/images/chat-ltr.png b/resources/src/mediawiki.skinning/images/chat-ltr.png
new file mode 100644
index 00000000..624ecec3
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/chat-ltr.png
Binary files differ
diff --git a/resources/src/mediawiki.skinning/images/chat-ltr.svg b/resources/src/mediawiki.skinning/images/chat-ltr.svg
new file mode 100644
index 00000000..bd5329e0
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/chat-ltr.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g transform="translate(-36.467808,-258.39005)">
+<path style="opacity:1;fill:#3366bb;" d="m38.09,260.4-0.6213,0.6213,0,5.757,0.6213,0.6213,1.689,0-0.6213,2.728,4.311-2.728,4.379,0,0.6213-0.6213,0-5.757-0.6213-0.6213zm0.3787,1,9,0,0,5-4,0-2.902,1.897,0.9021-1.897-3,0z"/>
+</g>
+</svg>
diff --git a/resources/src/mediawiki.skinning/images/chat-rtl.png b/resources/src/mediawiki.skinning/images/chat-rtl.png
new file mode 100644
index 00000000..f90fa33a
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/chat-rtl.png
Binary files differ
diff --git a/resources/src/mediawiki.skinning/images/chat-rtl.svg b/resources/src/mediawiki.skinning/images/chat-rtl.svg
new file mode 100644
index 00000000..b86218f3
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/chat-rtl.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g transform="translate(-36.467808,-258.39005)">
+<path style="opacity:1;fill:#3366bb;" d="m47.85,260.4,0.6213,0.6213,0,5.757-0.6213,0.6213-1.689,0,0.6213,2.728-4.311-2.728-4.379,0-0.6213-0.6213,0-5.757,0.6213-0.6213zm-0.3787,1-9,0,0,5,4,0,2.902,1.897-0.9021-1.897,3,0z"/>
+</g>
+</svg>
diff --git a/resources/src/mediawiki.skinning/images/document-ltr.png b/resources/src/mediawiki.skinning/images/document-ltr.png
new file mode 100644
index 00000000..4ea9373f
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/document-ltr.png
Binary files differ
diff --git a/resources/src/mediawiki.skinning/images/document-ltr.svg b/resources/src/mediawiki.skinning/images/document-ltr.svg
new file mode 100644
index 00000000..43960980
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/document-ltr.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path style="opacity:1;fill:#15a5ea;" d="m7.5,2,0,3,2.5,0,1-1-2.5,0,0-3z"/>
+<path style="opacity:1;fill:#3366bb;" d="m3,1,0,10,8,0,0-7-2.5-3zm1,1,4,0,2,2.5,0,5.5-6,0z"/>
+</svg>
diff --git a/resources/src/mediawiki.skinning/images/document-rtl.png b/resources/src/mediawiki.skinning/images/document-rtl.png
new file mode 100644
index 00000000..c281677a
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/document-rtl.png
Binary files differ
diff --git a/resources/src/mediawiki.skinning/images/document-rtl.svg b/resources/src/mediawiki.skinning/images/document-rtl.svg
new file mode 100644
index 00000000..c37dadca
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/document-rtl.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path style="opacity:1;fill:#15a5ea;" d="m5.5,2,0,3-2.5,0-1-1,2.5,0,0-3z"/>
+<path style="opacity:1;fill:#3366bb;" d="m10,1,0,10-8,0,0-7,2.5-3zm-1,1-4,0-2,2.5,0,5.5,6,0z"/>
+</svg>
diff --git a/resources/src/mediawiki.skinning/images/external link icons.svg b/resources/src/mediawiki.skinning/images/external link icons.svg
new file mode 100644
index 00000000..6a67993d
--- /dev/null
+++ b/resources/src/mediawiki.skinning/images/external link icons.svg
@@ -0,0 +1,697 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="13"
+ height="110"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.5 r10040"
+ sodipodi:docname="external link icons.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="15.999999"
+ inkscape:cx="10.40536"
+ inkscape:cy="65.686256"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer5"
+ showgrid="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1283"
+ inkscape:window-height="711"
+ inkscape:window-x="1790"
+ inkscape:window-y="-6"
+ inkscape:window-maximized="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid3246"
+ empspacing="4"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="0px"
+ originy="-27.999997px" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:groupmode="layer"
+ id="layer2"
+ inkscape:label="base"
+ style="display:none"
+ transform="translate(-505,-869.36218)">
+ <rect
+ style="fill:#b3b3b3;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect4646"
+ width="13"
+ height="12.999996"
+ x="505"
+ y="885.36218" />
+ <rect
+ style="fill:#b3b3b3;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect4646-4"
+ width="13"
+ height="12.999998"
+ x="505"
+ y="901.36218" />
+ <rect
+ style="fill:#b3b3b3;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect4646-4-6"
+ width="13"
+ height="12.999996"
+ x="505"
+ y="917.36218"
+ inkscape:export-filename="/home/rahah/elvidishu/steak/unreal/dev/skins/external link icons/mail.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90" />
+ <rect
+ style="fill:#b3b3b3;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect4646-4-6-9"
+ width="13"
+ height="12.999996"
+ x="505"
+ y="933.36218" />
+ <rect
+ style="fill:#b3b3b3;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect4646-4-6-6"
+ width="13"
+ height="12.999996"
+ x="505"
+ y="950.36218" />
+ <rect
+ style="fill:#b3b3b3;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect4646-4-6-2"
+ width="13"
+ height="12.999998"
+ x="505"
+ y="966.36218" />
+ <rect
+ style="fill:#b3b3b3;fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline"
+ id="rect4646-44"
+ width="13"
+ height="12.999996"
+ x="505"
+ y="869.36218" />
+ </g>
+ <g
+ inkscape:label="sketch 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-505,-869.36218)"
+ style="display:none"
+ sodipodi:insensitive="true">
+ <path
+ style="fill:none;stroke:#0066ff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 507,870.36218 0,5 3,0 4,4 0,-13 -4,4 z"
+ id="path3194"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <path
+ style="fill:none;stroke:#5b9dff;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 517,869.36218 c 1,2 1,5 0,7"
+ id="path3196"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#5b9dff;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 520,867.36218 c 2,2 2,9 0,11"
+ id="path3198"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#0066ff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 507.93861,989.90562 0,15.99988 13,0 0,-10.99988 -5,-5 z"
+ id="path3200"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ style="fill:none;stroke:#0066ff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 506.93861,918.90546 0,11.5 15,0 0,-11.5 z"
+ id="path3202"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:none;stroke:#0066ff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 506.93861,918.90546 7.5,6 7.5,-6"
+ id="path3204"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccc" />
+ <path
+ style="fill:none;stroke:#5b9dff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 507.93861,890.90546 3,0"
+ id="path3212"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#5b9dff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 507.93861,893.90546 3,0"
+ id="path3214"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#5b9dff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 507.93861,899.90546 3,0"
+ id="path3218"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#5b9dff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 507.93861,902.90546 3,0"
+ id="path3220"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#5b9dff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 517.93861,890.90546 3,0"
+ id="path3222"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#5b9dff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 507.93861,896.90546 13,0"
+ id="path3224"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#5b9dff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 517.93861,893.90546 3,0"
+ id="path3226"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#5b9dff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 517.93861,899.90546 3,0"
+ id="path3230"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#5b9dff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 517.93861,902.90546 3,0"
+ id="path3232"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#0066ff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 510.93861,890.90546 0,12 7,0 0,-12 z"
+ id="path3206"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:none;stroke:#0066ff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 507.93861,888.90546 0,16"
+ id="path3208"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#0066ff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 520.93861,888.90546 0,16"
+ id="path3210"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#0066ff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 515.93861,989.90562 0,5 5,0"
+ id="path3234"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccc" />
+ <path
+ style="fill:none;stroke:#0066ff;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 513.93861,969.40546 c -2,0 -5,0