summaryrefslogtreecommitdiff
path: root/resources
diff options
context:
space:
mode:
Diffstat (limited to 'resources')
-rw-r--r--resources/Resources.php326
-rw-r--r--resources/jquery.chosen/LICENSE24
-rw-r--r--resources/jquery.chosen/chosen-sprite.pngbin0 -> 646 bytes
-rw-r--r--resources/jquery.chosen/chosen-sprite@2x.pngbin0 -> 871 bytes
-rw-r--r--resources/jquery.chosen/chosen.css440
-rw-r--r--resources/jquery.chosen/chosen.jquery.js1103
-rw-r--r--resources/jquery.effects/jquery.effects.blind.js2
-rw-r--r--resources/jquery.effects/jquery.effects.bounce.js2
-rw-r--r--resources/jquery.effects/jquery.effects.clip.js2
-rw-r--r--resources/jquery.effects/jquery.effects.core.js4
-rw-r--r--resources/jquery.effects/jquery.effects.drop.js2
-rw-r--r--resources/jquery.effects/jquery.effects.explode.js2
-rw-r--r--resources/jquery.effects/jquery.effects.fade.js2
-rw-r--r--resources/jquery.effects/jquery.effects.fold.js2
-rw-r--r--resources/jquery.effects/jquery.effects.highlight.js2
-rw-r--r--resources/jquery.effects/jquery.effects.pulsate.js2
-rw-r--r--resources/jquery.effects/jquery.effects.scale.js2
-rw-r--r--resources/jquery.effects/jquery.effects.shake.js2
-rw-r--r--resources/jquery.effects/jquery.effects.slide.js2
-rw-r--r--resources/jquery.effects/jquery.effects.transfer.js2
-rw-r--r--resources/jquery.tipsy/images/tipsy.pngbin175 -> 133 bytes
-rw-r--r--resources/jquery.ui/i18n/jquery.ui.datepicker-zh-CN.js4
-rw-r--r--resources/jquery.ui/i18n/jquery.ui.datepicker-zh-HK.js4
-rw-r--r--resources/jquery.ui/i18n/jquery.ui.datepicker-zh-TW.js4
-rw-r--r--resources/jquery.ui/jquery.ui.accordion.js4
-rw-r--r--resources/jquery.ui/jquery.ui.autocomplete.js2
-rw-r--r--resources/jquery.ui/jquery.ui.button.js2
-rw-r--r--resources/jquery.ui/jquery.ui.core.js4
-rw-r--r--resources/jquery.ui/jquery.ui.datepicker.js6
-rw-r--r--resources/jquery.ui/jquery.ui.dialog.js4
-rw-r--r--resources/jquery.ui/jquery.ui.draggable.js13
-rw-r--r--resources/jquery.ui/jquery.ui.droppable.js11
-rw-r--r--resources/jquery.ui/jquery.ui.mouse.js2
-rw-r--r--resources/jquery.ui/jquery.ui.position.js2
-rw-r--r--resources/jquery.ui/jquery.ui.progressbar.js4
-rw-r--r--resources/jquery.ui/jquery.ui.resizable.js4
-rw-r--r--resources/jquery.ui/jquery.ui.selectable.js4
-rw-r--r--resources/jquery.ui/jquery.ui.slider.js4
-rw-r--r--resources/jquery.ui/jquery.ui.sortable.js34
-rw-r--r--resources/jquery.ui/jquery.ui.tabs.js4
-rw-r--r--resources/jquery.ui/jquery.ui.widget.js2
-rw-r--r--resources/jquery.ui/themes/default/images/ui-bg_flat_0_aaaaaa_40x100.pngbin180 -> 87 bytes
-rw-r--r--resources/jquery.ui/themes/default/images/ui-bg_flat_75_ffffff_40x100.pngbin178 -> 87 bytes
-rw-r--r--resources/jquery.ui/themes/default/images/ui-bg_glass_55_fbf9ee_1x400.pngbin120 -> 115 bytes
-rw-r--r--resources/jquery.ui/themes/default/images/ui-bg_glass_65_ffffff_1x400.pngbin105 -> 99 bytes
-rw-r--r--resources/jquery.ui/themes/default/images/ui-bg_glass_75_dadada_1x400.pngbin111 -> 111 bytes
-rw-r--r--resources/jquery.ui/themes/default/images/ui-bg_highlight-soft_75_cccccc_1x100.pngbin101 -> 86 bytes
-rw-r--r--resources/jquery.ui/themes/default/images/ui-icons_222222_256x240.pngbin4369 -> 3702 bytes
-rw-r--r--resources/jquery.ui/themes/default/images/ui-icons_2e83ff_256x240.pngbin4369 -> 3702 bytes
-rw-r--r--resources/jquery.ui/themes/default/images/ui-icons_454545_256x240.pngbin4369 -> 3702 bytes
-rw-r--r--resources/jquery.ui/themes/default/images/ui-icons_888888_256x240.pngbin4369 -> 3702 bytes
-rw-r--r--resources/jquery.ui/themes/default/images/ui-icons_cd0a0a_256x240.pngbin4369 -> 3702 bytes
-rw-r--r--resources/jquery.ui/themes/default/jquery.ui.accordion.css2
-rw-r--r--resources/jquery.ui/themes/default/jquery.ui.autocomplete.css4
-rw-r--r--resources/jquery.ui/themes/default/jquery.ui.button.css114
-rw-r--r--resources/jquery.ui/themes/default/jquery.ui.core.css2
-rw-r--r--resources/jquery.ui/themes/default/jquery.ui.datepicker.css2
-rw-r--r--resources/jquery.ui/themes/default/jquery.ui.dialog.css2
-rw-r--r--resources/jquery.ui/themes/default/jquery.ui.progressbar.css2
-rw-r--r--resources/jquery.ui/themes/default/jquery.ui.resizable.css2
-rw-r--r--resources/jquery.ui/themes/default/jquery.ui.selectable.css2
-rw-r--r--resources/jquery.ui/themes/default/jquery.ui.slider.css2
-rw-r--r--resources/jquery.ui/themes/default/jquery.ui.tabs.css2
-rw-r--r--resources/jquery.ui/themes/default/jquery.ui.theme.css2
-rw-r--r--resources/jquery.ui/themes/vector/images/button-blue-hover-large.pngbin260 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-blue-hover.pngbin175 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-blue-large.pngbin265 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-blue.pngbin175 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-disabled-blue.pngbin84 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-disabled-green.pngbin149 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-disabled-red.pngbin84 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-disabled.pngbin84 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-down-blue.pngbin130 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-down-green.pngbin141 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-down-red.pngbin130 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-down.pngbin130 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-green-hover-large.pngbin265 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-green-hover.pngbin175 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-green-large.pngbin265 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-green.pngbin175 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-large-disabled-green.pngbin277 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-large-off-green.pngbin282 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-off-blue.pngbin175 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-off-green.pngbin149 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-off-red.pngbin175 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-off.pngbin152 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-orange-hover-large.pngbin265 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-orange-hover.pngbin175 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-orange-large.pngbin265 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-orange.pngbin175 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-over-blue.pngbin175 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-over-green.pngbin149 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-over-red.pngbin174 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-over.pngbin155 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-red-hover-large.pngbin260 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-red-hover.pngbin175 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-red-large.pngbin265 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/button-red.pngbin175 -> 0 bytes
-rw-r--r--resources/jquery.ui/themes/vector/images/titlebar-fade.pngbin188 -> 81 bytes
-rw-r--r--resources/jquery.ui/themes/vector/jquery.ui.button.css460
-rw-r--r--resources/jquery/images/jquery.arrowSteps.divider-ltr.pngbin135 -> 126 bytes
-rw-r--r--resources/jquery/images/jquery.arrowSteps.divider-rtl.pngbin139 -> 127 bytes
-rw-r--r--resources/jquery/images/jquery.arrowSteps.head-ltr.pngbin390 -> 303 bytes
-rw-r--r--resources/jquery/images/jquery.arrowSteps.head-rtl.pngbin365 -> 311 bytes
-rw-r--r--resources/jquery/images/jquery.arrowSteps.tail-ltr.pngbin223 -> 222 bytes
-rw-r--r--resources/jquery/images/marker.pngbin652 -> 472 bytes
-rw-r--r--resources/jquery/images/mask.pngbin2020 -> 1795 bytes
-rw-r--r--resources/jquery/jquery.arrowSteps.js11
-rw-r--r--resources/jquery/jquery.badge.css42
-rw-r--r--resources/jquery/jquery.badge.js121
-rw-r--r--resources/jquery/jquery.byteLength.js12
-rw-r--r--resources/jquery/jquery.byteLimit.js8
-rw-r--r--resources/jquery/jquery.checkboxShiftClick.js26
-rw-r--r--resources/jquery/jquery.client.js222
-rw-r--r--resources/jquery/jquery.collapsibleTabs.js126
-rw-r--r--resources/jquery/jquery.colorUtil.js21
-rw-r--r--resources/jquery/jquery.delayedBind.js6
-rw-r--r--resources/jquery/jquery.expandableField.js8
-rw-r--r--resources/jquery/jquery.hidpi.js117
-rw-r--r--resources/jquery/jquery.highlightText.js2
-rw-r--r--resources/jquery/jquery.jStorage.js853
-rw-r--r--resources/jquery/jquery.js206
-rw-r--r--resources/jquery/jquery.json.js250
-rw-r--r--resources/jquery/jquery.localize.js59
-rw-r--r--resources/jquery/jquery.makeCollapsible.js604
-rw-r--r--resources/jquery/jquery.mw-jump.js10
-rw-r--r--resources/jquery/jquery.mwExtension.js5
-rw-r--r--resources/jquery/jquery.placeholder.js9
-rw-r--r--resources/jquery/jquery.qunit.completenessTest.js4
-rw-r--r--resources/jquery/jquery.qunit.css15
-rw-r--r--resources/jquery/jquery.qunit.js493
-rw-r--r--resources/jquery/jquery.spinner.css2
-rw-r--r--resources/jquery/jquery.spinner.js47
-rw-r--r--resources/jquery/jquery.suggestions.js211
-rw-r--r--resources/jquery/jquery.tablesorter.js505
-rw-r--r--resources/jquery/jquery.textSelection.js119
-rw-r--r--resources/mediawiki.action/images/green-checkmark.pngbin0 -> 681 bytes
-rw-r--r--resources/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css17
-rw-r--r--resources/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js54
-rw-r--r--resources/mediawiki.action/mediawiki.action.edit.editWarning.js56
-rw-r--r--resources/mediawiki.action/mediawiki.action.edit.js169
-rw-r--r--resources/mediawiki.action/mediawiki.action.edit.preview.js59
-rw-r--r--resources/mediawiki.action/mediawiki.action.edit.styles.css44
-rw-r--r--resources/mediawiki.action/mediawiki.action.history.diff.css80
-rw-r--r--resources/mediawiki.action/mediawiki.action.history.js4
-rw-r--r--resources/mediawiki.action/mediawiki.action.view.dblClickEdit.js14
-rw-r--r--resources/mediawiki.action/mediawiki.action.view.postEdit.css77
-rw-r--r--resources/mediawiki.action/mediawiki.action.view.postEdit.js76
-rw-r--r--resources/mediawiki.action/mediawiki.action.view.rightClickEdit.js16
-rw-r--r--resources/mediawiki.api/mediawiki.api.category.js183
-rw-r--r--resources/mediawiki.api/mediawiki.api.edit.js108
-rw-r--r--resources/mediawiki.api/mediawiki.api.js161
-rw-r--r--resources/mediawiki.api/mediawiki.api.login.js54
-rw-r--r--resources/mediawiki.api/mediawiki.api.parse.js41
-rw-r--r--resources/mediawiki.api/mediawiki.api.titleblacklist.js52
-rw-r--r--resources/mediawiki.api/mediawiki.api.watch.js70
-rw-r--r--resources/mediawiki.language/languages/bs.js6
-rw-r--r--resources/mediawiki.language/languages/dsb.js6
-rw-r--r--resources/mediawiki.language/languages/fi.js15
-rw-r--r--resources/mediawiki.language/languages/ga.js7
-rw-r--r--resources/mediawiki.language/languages/he.js14
-rw-r--r--resources/mediawiki.language/languages/hsb.js6
-rw-r--r--resources/mediawiki.language/languages/hu.js6
-rw-r--r--resources/mediawiki.language/languages/hy.js16
-rw-r--r--resources/mediawiki.language/languages/la.js6
-rw-r--r--resources/mediawiki.language/languages/os.js29
-rw-r--r--resources/mediawiki.language/languages/ru.js46
-rw-r--r--resources/mediawiki.language/languages/sl.js6
-rw-r--r--resources/mediawiki.language/languages/uk.js28
-rw-r--r--resources/mediawiki.language/mediawiki.cldr.js23
-rw-r--r--resources/mediawiki.language/mediawiki.language.init.js4
-rw-r--r--resources/mediawiki.language/mediawiki.language.js74
-rw-r--r--resources/mediawiki.language/mediawiki.language.months.js54
-rw-r--r--resources/mediawiki.language/mediawiki.language.numbers.js243
-rw-r--r--resources/mediawiki.less/mediawiki.mixins.less46
-rw-r--r--resources/mediawiki.libs/CLDRPluralRuleParser.js357
-rw-r--r--resources/mediawiki.libs/mediawiki.libs.jpegmeta.js2
-rw-r--r--resources/mediawiki.page/mediawiki.page.gallery.js248
-rw-r--r--resources/mediawiki.page/mediawiki.page.image.pagination.js51
-rw-r--r--resources/mediawiki.page/mediawiki.page.patrol.ajax.js63
-rw-r--r--resources/mediawiki.page/mediawiki.page.ready.js52
-rw-r--r--resources/mediawiki.page/mediawiki.page.startup.js23
-rw-r--r--resources/mediawiki.page/mediawiki.page.watch.ajax.js23
-rw-r--r--resources/mediawiki.special/images/arrow-collapsed-ltr.pngbin206 -> 0 bytes
-rw-r--r--resources/mediawiki.special/images/arrow-collapsed-rtl.pngbin205 -> 0 bytes
-rw-r--r--resources/mediawiki.special/images/arrow-expanded.pngbin205 -> 0 bytes
-rw-r--r--resources/mediawiki.special/images/glyph-people-large.pngbin0 -> 1663 bytes
-rw-r--r--resources/mediawiki.special/images/icon-contributors.pngbin0 -> 1169 bytes
-rw-r--r--resources/mediawiki.special/images/icon-edits.pngbin0 -> 780 bytes
-rw-r--r--resources/mediawiki.special/images/icon-lock.pngbin0 -> 172 bytes
-rw-r--r--resources/mediawiki.special/images/icon-pages.pngbin0 -> 528 bytes
-rw-r--r--resources/mediawiki.special/mediawiki.special.block.js78
-rw-r--r--resources/mediawiki.special/mediawiki.special.changeemail.js62
-rw-r--r--resources/mediawiki.special/mediawiki.special.changeslist.css56
-rw-r--r--resources/mediawiki.special/mediawiki.special.changeslist.enhanced.css66
-rw-r--r--resources/mediawiki.special/mediawiki.special.createAccount.css89
-rw-r--r--resources/mediawiki.special/mediawiki.special.createAccount.js112
-rw-r--r--resources/mediawiki.special/mediawiki.special.javaScriptTest.js2
-rw-r--r--resources/mediawiki.special/mediawiki.special.js6
-rw-r--r--resources/mediawiki.special/mediawiki.special.movePage.js9
-rw-r--r--resources/mediawiki.special/mediawiki.special.pagesWithProp.css4
-rw-r--r--resources/mediawiki.special/mediawiki.special.preferences.js338
-rw-r--r--resources/mediawiki.special/mediawiki.special.recentchanges.js29
-rw-r--r--resources/mediawiki.special/mediawiki.special.search.js90
-rw-r--r--resources/mediawiki.special/mediawiki.special.undelete.js13
-rw-r--r--resources/mediawiki.special/mediawiki.special.upload.js91
-rw-r--r--resources/mediawiki.special/mediawiki.special.userLogin.css39
-rw-r--r--resources/mediawiki.special/mediawiki.special.vforms.css46
-rw-r--r--resources/mediawiki.ui/mediawiki.ui.default.css272
-rw-r--r--resources/mediawiki.ui/mediawiki.ui.vector.css414
-rw-r--r--resources/mediawiki.ui/sourcefiles/Makefile24
-rw-r--r--resources/mediawiki.ui/sourcefiles/config.rb27
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/components/_default.scss3
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/components/_utilities.scss17
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/components/_vector.scss4
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/components/default/_buttons.scss69
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/components/default/_forms.scss114
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/components/vector/_buttons.scss19
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/components/vector/_containers.scss5
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/components/vector/_forms.scss7
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/mediawiki.ui.default.scss16
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/mediawiki.ui.vector.scss15
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/mixins/_all.scss4
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/mixins/_effects.scss62
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/mixins/_forms.scss66
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/mixins/_type.scss6
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/mixins/_utilities.scss19
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/settings/_all.scss2
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/settings/_colors.scss17
-rw-r--r--resources/mediawiki.ui/sourcefiles/scss/settings/_typography.scss5
-rw-r--r--resources/mediawiki/images/arrow-collapsed-ltr.pngbin0 -> 133 bytes
-rw-r--r--resources/mediawiki/images/arrow-collapsed-rtl.pngbin0 -> 136 bytes
-rw-r--r--resources/mediawiki/images/arrow-expanded.pngbin0 -> 134 bytes
-rw-r--r--resources/mediawiki/mediawiki.Title.js609
-rw-r--r--resources/mediawiki/mediawiki.Uri.js68
-rw-r--r--resources/mediawiki/mediawiki.debug.css1
-rw-r--r--resources/mediawiki/mediawiki.debug.js8
-rw-r--r--resources/mediawiki/mediawiki.feedback.js45
-rw-r--r--resources/mediawiki/mediawiki.hidpi.js5
-rw-r--r--resources/mediawiki/mediawiki.htmlform.js172
-rw-r--r--resources/mediawiki/mediawiki.icon.css15
-rw-r--r--resources/mediawiki/mediawiki.inspect.js204
-rw-r--r--resources/mediawiki/mediawiki.jqueryMsg.js730
-rw-r--r--resources/mediawiki/mediawiki.jqueryMsg.peg1
-rw-r--r--resources/mediawiki/mediawiki.js1083
-rw-r--r--resources/mediawiki/mediawiki.log.js67
-rw-r--r--resources/mediawiki/mediawiki.notification.css16
-rw-r--r--resources/mediawiki/mediawiki.notification.js164
-rw-r--r--resources/mediawiki/mediawiki.notify.js24
-rw-r--r--resources/mediawiki/mediawiki.searchSuggest.css16
-rw-r--r--resources/mediawiki/mediawiki.searchSuggest.js158
-rw-r--r--resources/mediawiki/mediawiki.user.js264
-rw-r--r--resources/mediawiki/mediawiki.util.js434
-rw-r--r--resources/startup.js53
254 files changed, 11806 insertions, 4190 deletions
diff --git a/resources/Resources.php b/resources/Resources.php
index 0a70c5a2..06120008 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -1,8 +1,34 @@
<?php
+/**
+ * Definition of core ResourceLoader modules.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+if ( !defined( 'MEDIAWIKI' ) ) {
+ die( 'Not an entry point.' );
+}
return array(
- /* Special modules who have their own classes */
+ /**
+ * Special modules who have their own classes
+ */
// Scripts managed by the local wiki (stored in the MediaWiki namespace)
'site' => array( 'class' => 'ResourceLoaderSiteModule' ),
@@ -24,15 +50,21 @@ return array(
// Scripts for the dynamic language specific data, like grammar forms.
'mediawiki.language.data' => array( 'class' => 'ResourceLoaderLanguageDataModule' ),
- /* Skins */
-
- 'skins.chick' => array(
- 'styles' => array( 'chick/main.css' => array( 'media' => 'screen, handheld' ) ),
- 'remoteBasePath' => $GLOBALS['wgStylePath'],
- 'localBasePath' => $GLOBALS['wgStyleDirectory'],
- ),
+ /**
+ * Skins
+ * Be careful not to add 'scripts' to these modules,
+ * since they are loaded with OutputPage::addModuleStyles so that the skin styles
+ * apply without javascript.
+ * If a skin needs custom js in the interface, register a separate module
+ * and add it to the load queue with OutputPage::addModules.
+ *
+ * See Vector for an example.
+ */
'skins.cologneblue' => array(
- 'styles' => array( 'cologneblue/screen.css' => array( 'media' => 'screen' ) ),
+ 'styles' => array(
+ 'cologneblue/screen.css' => array( 'media' => 'screen' ),
+ 'cologneblue/print.css' => array( 'media' => 'print' ),
+ ),
'remoteBasePath' => $GLOBALS['wgStylePath'],
'localBasePath' => $GLOBALS['wgStyleDirectory'],
),
@@ -67,33 +99,53 @@ return array(
'remoteBasePath' => $GLOBALS['wgStylePath'],
'localBasePath' => $GLOBALS['wgStyleDirectory'],
),
- 'skins.nostalgia' => array(
- 'styles' => array( 'nostalgia/screen.css' => array( 'media' => 'screen' ) ),
+ 'skins.vector' => array(
+ // Used in the web installer. Test it after modifying this definition!
+ 'styles' => array(
+ 'common/commonElements.css' => array( 'media' => 'screen' ),
+ 'common/commonContent.css' => array( 'media' => 'screen' ),
+ 'common/commonInterface.css' => array( 'media' => 'screen' ),
+ 'vector/styles.less',
+ ),
'remoteBasePath' => $GLOBALS['wgStylePath'],
'localBasePath' => $GLOBALS['wgStyleDirectory'],
),
- 'skins.simple' => array(
- 'styles' => array( 'simple/main.css' => array( 'media' => 'screen' ) ),
+ 'skins.vector.beta' => array(
+ // Keep in sync with skins.vector
+ 'styles' => array(
+ 'common/commonElements.css' => array( 'media' => 'screen' ),
+ 'common/commonContent.css' => array( 'media' => 'screen' ),
+ 'common/commonInterface.css' => array( 'media' => 'screen' ),
+ 'vector/styles-beta.less',
+ ),
'remoteBasePath' => $GLOBALS['wgStylePath'],
'localBasePath' => $GLOBALS['wgStyleDirectory'],
),
- 'skins.standard' => array(
- 'styles' => array( 'standard/main.css' => array( 'media' => 'screen' ) ),
+ 'skins.vector.js' => array(
+ 'scripts' => array(
+ 'vector/collapsibleTabs.js',
+ 'vector/vector.js',
+ ),
+ 'position' => 'top',
+ 'dependencies' => 'jquery.delayedBind',
'remoteBasePath' => $GLOBALS['wgStylePath'],
'localBasePath' => $GLOBALS['wgStyleDirectory'],
),
- 'skins.vector' => array(
- // Keep in sync with WebInstallerOutput::getCSS()
- 'styles' => array(
- 'common/commonElements.css' => array( 'media' => 'screen' ),
- 'common/commonContent.css' => array( 'media' => 'screen' ),
- 'common/commonInterface.css' => array( 'media' => 'screen' ),
- 'vector/screen.css' => array( 'media' => 'screen' ),
- 'vector/screen-hd.css' => array( 'media' => 'screen and (min-width: 982px)' ),
+ 'skins.vector.collapsibleNav' => array(
+ 'scripts' => array(
+ 'vector/collapsibleNav.js',
+ ),
+ 'messages' => array(
+ 'vector-collapsiblenav-more',
+ ),
+ 'dependencies' => array(
+ 'jquery.client',
+ 'jquery.cookie',
+ 'jquery.tabIndex',
),
- 'scripts' => 'vector/vector.js',
'remoteBasePath' => $GLOBALS['wgStylePath'],
'localBasePath' => $GLOBALS['wgStyleDirectory'],
+ 'position' => 'bottom',
),
/* jQuery */
@@ -101,6 +153,7 @@ return array(
'jquery' => array(
'scripts' => 'resources/jquery/jquery.js',
'debugRaw' => false,
+ 'targets' => array( 'desktop', 'mobile' ),
),
/* jQuery Plugins */
@@ -122,9 +175,11 @@ return array(
'jquery.badge' => array(
'scripts' => 'resources/jquery/jquery.badge.js',
'styles' => 'resources/jquery/jquery.badge.css',
+ 'dependencies' => 'mediawiki.language',
),
'jquery.byteLength' => array(
'scripts' => 'resources/jquery/jquery.byteLength.js',
+ 'targets' => array( 'desktop', 'mobile' ),
),
'jquery.byteLimit' => array(
'scripts' => 'resources/jquery/jquery.byteLimit.js',
@@ -132,12 +187,15 @@ return array(
),
'jquery.checkboxShiftClick' => array(
'scripts' => 'resources/jquery/jquery.checkboxShiftClick.js',
+ 'targets' => array( 'desktop', 'mobile' ),
+ ),
+ 'jquery.chosen' => array(
+ 'scripts' => 'resources/jquery.chosen/chosen.jquery.js',
+ 'styles' => 'resources/jquery.chosen/chosen.css',
),
'jquery.client' => array(
'scripts' => 'resources/jquery/jquery.client.js',
- ),
- 'jquery.collapsibleTabs' => array(
- 'scripts' => 'resources/jquery/jquery.collapsibleTabs.js',
+ 'targets' => array( 'desktop', 'mobile' ),
),
'jquery.color' => array(
'scripts' => 'resources/jquery/jquery.color.js',
@@ -148,6 +206,7 @@ return array(
),
'jquery.cookie' => array(
'scripts' => 'resources/jquery/jquery.cookie.js',
+ 'targets' => array( 'desktop', 'mobile' ),
),
'jquery.delayedBind' => array(
'scripts' => 'resources/jquery/jquery.delayedBind.js',
@@ -170,6 +229,11 @@ return array(
),
'jquery.getAttrs' => array(
'scripts' => 'resources/jquery/jquery.getAttrs.js',
+ 'targets' => array( 'desktop', 'mobile' ),
+ ),
+ 'jquery.hidpi' => array(
+ 'scripts' => 'resources/jquery/jquery.hidpi.js',
+ 'targets' => array( 'desktop', 'mobile' ),
),
'jquery.highlightText' => array(
'scripts' => 'resources/jquery/jquery.highlightText.js',
@@ -180,6 +244,7 @@ return array(
),
'jquery.json' => array(
'scripts' => 'resources/jquery/jquery.json.js',
+ 'targets' => array( 'mobile', 'desktop' ),
),
'jquery.localize' => array(
'scripts' => 'resources/jquery/jquery.localize.js',
@@ -188,27 +253,33 @@ return array(
'scripts' => 'resources/jquery/jquery.makeCollapsible.js',
'styles' => 'resources/jquery/jquery.makeCollapsible.css',
'messages' => array( 'collapsible-expand', 'collapsible-collapse' ),
+ 'targets' => array( 'desktop', 'mobile' ),
),
'jquery.mockjax' => array(
'scripts' => 'resources/jquery/jquery.mockjax.js',
),
'jquery.mw-jump' => array(
'scripts' => 'resources/jquery/jquery.mw-jump.js',
+ 'targets' => array( 'desktop', 'mobile' ),
),
'jquery.mwExtension' => array(
'scripts' => 'resources/jquery/jquery.mwExtension.js',
+ 'targets' => array( 'desktop', 'mobile' ),
),
'jquery.placeholder' => array(
'scripts' => 'resources/jquery/jquery.placeholder.js',
+ 'targets' => array( 'desktop', 'mobile' ),
),
'jquery.qunit' => array(
'scripts' => 'resources/jquery/jquery.qunit.js',
'styles' => 'resources/jquery/jquery.qunit.css',
'position' => 'top',
+ 'targets' => array( 'desktop', 'mobile' ),
),
'jquery.qunit.completenessTest' => array(
'scripts' => 'resources/jquery/jquery.qunit.completenessTest.js',
'dependencies' => 'jquery.qunit',
+ 'targets' => array( 'desktop', 'mobile' ),
),
'jquery.spinner' => array(
'scripts' => 'resources/jquery/jquery.spinner.js',
@@ -230,7 +301,10 @@ return array(
'scripts' => 'resources/jquery/jquery.tablesorter.js',
'styles' => 'resources/jquery/jquery.tablesorter.css',
'messages' => array( 'sort-descending', 'sort-ascending' ),
- 'dependencies' => 'jquery.mwExtension',
+ 'dependencies' => array(
+ 'jquery.mwExtension',
+ 'mediawiki.language.months',
+ ),
),
'jquery.textSelection' => array(
'scripts' => 'resources/jquery/jquery.textSelection.js',
@@ -539,6 +613,7 @@ return array(
'scripts' => 'resources/mediawiki/mediawiki.js',
'debugScripts' => 'resources/mediawiki/mediawiki.log.js',
'debugRaw' => false,
+ 'targets' => array( 'desktop', 'mobile' ),
),
'mediawiki.api' => array(
'scripts' => 'resources/mediawiki.api/mediawiki.api.js',
@@ -558,17 +633,16 @@ return array(
'mediawiki.Title',
),
),
- 'mediawiki.api.parse' => array(
- 'scripts' => 'resources/mediawiki.api/mediawiki.api.parse.js',
- 'dependencies' => 'mediawiki.api',
- ),
- 'mediawiki.api.titleblacklist' => array(
- 'scripts' => 'resources/mediawiki.api/mediawiki.api.titleblacklist.js',
+ 'mediawiki.api.login' => array(
+ 'scripts' => 'resources/mediawiki.api/mediawiki.api.login.js',
'dependencies' => array(
'mediawiki.api',
- 'mediawiki.Title',
),
),
+ 'mediawiki.api.parse' => array(
+ 'scripts' => 'resources/mediawiki.api/mediawiki.api.parse.js',
+ 'dependencies' => 'mediawiki.api',
+ ),
'mediawiki.api.watch' => array(
'scripts' => 'resources/mediawiki.api/mediawiki.api.watch.js',
'dependencies' => array(
@@ -576,6 +650,9 @@ return array(
'user.tokens',
),
),
+ 'mediawiki.icon' => array(
+ 'styles' => 'resources/mediawiki/mediawiki.icon.css',
+ ),
'mediawiki.debug' => array(
'scripts' => 'resources/mediawiki/mediawiki.debug.js',
'styles' => 'resources/mediawiki/mediawiki.debug.css',
@@ -589,6 +666,14 @@ return array(
// must be loaded on the bottom
'position' => 'bottom',
),
+ 'mediawiki.inspect' => array(
+ 'scripts' => 'resources/mediawiki/mediawiki.inspect.js',
+ 'dependencies' => array(
+ 'jquery.byteLength',
+ 'jquery.json',
+ ),
+ 'targets' => array( 'desktop', 'mobile' ),
+ ),
'mediawiki.feedback' => array(
'scripts' => 'resources/mediawiki/mediawiki.feedback.js',
'styles' => 'resources/mediawiki/mediawiki.feedback.css',
@@ -614,8 +699,16 @@ return array(
'feedback-bugnew',
),
),
+ 'mediawiki.hidpi' => array(
+ 'scripts' => 'resources/mediawiki/mediawiki.hidpi.js',
+ 'dependencies' => array(
+ 'jquery.hidpi',
+ ),
+ 'targets' => array( 'desktop', 'mobile' ),
+ ),
'mediawiki.htmlform' => array(
'scripts' => 'resources/mediawiki/mediawiki.htmlform.js',
+ 'messages' => array( 'htmlform-chosen-placeholder' ),
),
'mediawiki.notification' => array(
'styles' => 'resources/mediawiki/mediawiki.notification.css',
@@ -626,9 +719,11 @@ return array(
),
'mediawiki.notify' => array(
'scripts' => 'resources/mediawiki/mediawiki.notify.js',
+ 'targets' => array( 'desktop', 'mobile' ),
),
'mediawiki.searchSuggest' => array(
'scripts' => 'resources/mediawiki/mediawiki.searchSuggest.js',
+ 'styles' => 'resources/mediawiki/mediawiki.searchSuggest.css',
'messages' => array(
'searchsuggest-search',
'searchsuggest-containing',
@@ -638,11 +733,15 @@ return array(
'jquery.client',
'jquery.placeholder',
'jquery.suggestions',
+ 'mediawiki.api',
),
),
'mediawiki.Title' => array(
'scripts' => 'resources/mediawiki/mediawiki.Title.js',
- 'dependencies' => 'mediawiki.util',
+ 'dependencies' => array(
+ 'jquery.byteLength',
+ 'mediawiki.util',
+ ),
),
'mediawiki.Uri' => array(
'scripts' => 'resources/mediawiki/mediawiki.Uri.js',
@@ -652,6 +751,8 @@ return array(
'dependencies' => array(
'jquery.cookie',
'mediawiki.api',
+ 'user.options',
+ 'user.tokens',
),
),
'mediawiki.util' => array(
@@ -664,6 +765,7 @@ return array(
),
'messages' => array( 'showtoc', 'hidetoc' ),
'position' => 'top', // For $wgPreloadJavaScriptMwUtil
+ 'targets' => array( 'desktop', 'mobile' ),
),
/* MediaWiki Action */
@@ -671,16 +773,30 @@ return array(
'mediawiki.action.edit' => array(
'scripts' => 'resources/mediawiki.action/mediawiki.action.edit.js',
'dependencies' => array(
+ 'mediawiki.action.edit.styles',
'jquery.textSelection',
'jquery.byteLimit',
),
'position' => 'top',
),
+ 'mediawiki.action.edit.styles' => array(
+ 'styles' => 'resources/mediawiki.action/mediawiki.action.edit.styles.css',
+ 'position' => 'top',
+ ),
+ 'mediawiki.action.edit.collapsibleFooter' => array(
+ 'scripts' => 'resources/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js',
+ 'styles' => 'resources/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css',
+ 'dependencies' => array(
+ 'jquery.makeCollapsible',
+ 'mediawiki.icon',
+ ),
+ ),
'mediawiki.action.edit.preview' => array(
'scripts' => 'resources/mediawiki.action/mediawiki.action.edit.preview.js',
'dependencies' => array(
'jquery.form',
'jquery.spinner',
+ 'mediawiki.action.history.diff',
),
),
'mediawiki.action.history' => array(
@@ -693,7 +809,10 @@ return array(
),
'mediawiki.action.view.dblClickEdit' => array(
'scripts' => 'resources/mediawiki.action/mediawiki.action.view.dblClickEdit.js',
- 'dependencies' => 'mediawiki.util',
+ 'dependencies' => array(
+ 'mediawiki.util',
+ 'mediawiki.page.startup',
+ ),
),
'mediawiki.action.view.metadata' => array(
'scripts' => 'resources/mediawiki.action/mediawiki.action.view.metadata.js',
@@ -702,9 +821,26 @@ return array(
'metadata-collapse',
),
),
+ 'mediawiki.action.view.postEdit' => array(
+ 'scripts' => 'resources/mediawiki.action/mediawiki.action.view.postEdit.js',
+ 'styles' => 'resources/mediawiki.action/mediawiki.action.view.postEdit.css',
+ 'dependencies' => array(
+ 'jquery.cookie',
+ 'mediawiki.jqueryMsg'
+ ),
+ 'messages' => array(
+ 'postedit-confirmation',
+ ),
+ ),
'mediawiki.action.view.rightClickEdit' => array(
'scripts' => 'resources/mediawiki.action/mediawiki.action.view.rightClickEdit.js',
),
+ 'mediawiki.action.edit.editWarning' => array(
+ 'scripts' => 'resources/mediawiki.action/mediawiki.action.edit.editWarning.js',
+ 'messages' => array(
+ 'editwarning-warning',
+ ),
+ ),
// Alias for backwards compatibility
'mediawiki.action.watch.ajax' => array(
'dependencies' => 'mediawiki.page.watch.ajax'
@@ -713,7 +849,10 @@ return array(
/* MediaWiki Language */
'mediawiki.language' => array(
- 'scripts' => 'resources/mediawiki.language/mediawiki.language.js',
+ 'scripts' => array(
+ 'resources/mediawiki.language/mediawiki.language.js',
+ 'resources/mediawiki.language/mediawiki.language.numbers.js'
+ ),
'languageScripts' => array(
'bs' => 'resources/mediawiki.language/languages/bs.js',
'dsb' => 'resources/mediawiki.language/languages/dsb.js',
@@ -731,8 +870,9 @@ return array(
),
'dependencies' => array(
'mediawiki.language.data',
- 'mediawiki.cldr'
+ 'mediawiki.cldr',
),
+ 'targets' => array( 'desktop', 'mobile' ),
),
'mediawiki.cldr' => array(
@@ -740,14 +880,17 @@ return array(
'dependencies' => array(
'mediawiki.libs.pluralruleparser',
),
+ 'targets' => array( 'desktop', 'mobile' ),
),
'mediawiki.libs.pluralruleparser' => array(
'scripts' => 'resources/mediawiki.libs/CLDRPluralRuleParser.js',
+ 'targets' => array( 'desktop', 'mobile' ),
),
'mediawiki.language.init' => array(
'scripts' => 'resources/mediawiki.language/mediawiki.language.init.js',
+ 'targets' => array( 'desktop', 'mobile' ),
),
'mediawiki.jqueryMsg' => array(
@@ -756,6 +899,17 @@ return array(
'mediawiki.util',
'mediawiki.language',
),
+ 'targets' => array( 'desktop', 'mobile' ),
+ ),
+
+ 'mediawiki.language.months' => array(
+ 'scripts' => 'resources/mediawiki.language/mediawiki.language.months.js',
+ 'dependencies' => 'mediawiki.language',
+ 'messages' => array_merge(
+ Language::$mMonthMsgs,
+ Language::$mMonthGenMsgs,
+ Language::$mMonthAbbrevMsgs
+ )
),
/* MediaWiki Libs */
@@ -766,6 +920,9 @@ return array(
/* MediaWiki Page */
+ 'mediawiki.page.gallery' => array(
+ 'scripts' => 'resources/mediawiki.page/mediawiki.page.gallery.js',
+ ),
'mediawiki.page.ready' => array(
'scripts' => 'resources/mediawiki.page/mediawiki.page.ready.js',
'dependencies' => array(
@@ -775,6 +932,7 @@ return array(
'jquery.mw-jump',
'mediawiki.util',
),
+ 'targets' => array( 'desktop', 'mobile' ),
),
'mediawiki.page.startup' => array(
'scripts' => 'resources/mediawiki.page/mediawiki.page.startup.js',
@@ -783,6 +941,24 @@ return array(
'mediawiki.util',
),
'position' => 'top',
+ 'targets' => array( 'desktop', 'mobile' ),
+ ),
+ 'mediawiki.page.patrol.ajax' => array(
+ 'scripts' => 'resources/mediawiki.page/mediawiki.page.patrol.ajax.js',
+ 'dependencies' => array(
+ 'mediawiki.page.startup',
+ 'mediawiki.api',
+ 'mediawiki.util',
+ 'mediawiki.Title',
+ 'mediawiki.notify',
+ 'jquery.spinner',
+ 'user.tokens'
+ ),
+ 'messages' => array(
+ 'markedaspatrollednotify',
+ 'markedaspatrollederrornotify',
+ 'markedaspatrollederror-noautopatrol'
+ ),
),
'mediawiki.page.watch.ajax' => array(
'scripts' => 'resources/mediawiki.page/mediawiki.page.watch.ajax.js',
@@ -803,6 +979,10 @@ return array(
'watcherrortext',
),
),
+ 'mediawiki.page.image.pagination' => array(
+ 'scripts' => 'resources/mediawiki.page/mediawiki.page.image.pagination.js',
+ 'dependencies' => array( 'jquery.spinner' )
+ ),
/* MediaWiki Special pages */
@@ -829,15 +1009,21 @@ return array(
),
'mediawiki.special.changeslist' => array(
'styles' => 'resources/mediawiki.special/mediawiki.special.changeslist.css',
- 'dependencies' => array( 'jquery.makeCollapsible' ),
+ ),
+ 'mediawiki.special.changeslist.enhanced' => array(
+ 'styles' => 'resources/mediawiki.special/mediawiki.special.changeslist.enhanced.css',
),
'mediawiki.special.movePage' => array(
'scripts' => 'resources/mediawiki.special/mediawiki.special.movePage.js',
'dependencies' => 'jquery.byteLimit',
),
+ 'mediawiki.special.pagesWithProp' => array(
+ 'styles' => 'resources/mediawiki.special/mediawiki.special.pagesWithProp.css',
+ ),
'mediawiki.special.preferences' => array(
'scripts' => 'resources/mediawiki.special/mediawiki.special.preferences.js',
- 'styles' => 'resources/mediawiki.special/mediawiki.special.preferences.css',
+ 'styles' => 'resources/mediawiki.special/mediawiki.special.preferences.css',
+ 'position' => 'top',
),
'mediawiki.special.recentchanges' => array(
'scripts' => 'resources/mediawiki.special/mediawiki.special.recentchanges.js',
@@ -857,7 +1043,7 @@ return array(
'scripts' => 'resources/mediawiki.special/mediawiki.special.undelete.js',
),
'mediawiki.special.upload' => array(
- // @TODO: merge in remainder of mediawiki.legacy.upload
+ // @todo merge in remainder of mediawiki.legacy.upload
'scripts' => 'resources/mediawiki.special/mediawiki.special.upload.js',
'messages' => array(
'widthheight',
@@ -867,7 +1053,33 @@ return array(
'size-gigabytes',
'largefileserver',
),
- 'dependencies' => array( 'mediawiki.libs.jpegmeta', 'mediawiki.util' ),
+ 'dependencies' => array(
+ 'mediawiki.libs.jpegmeta',
+ 'mediawiki.util',
+ ),
+ ),
+ 'mediawiki.special.userlogin' => array(
+ 'styles' => array(
+ 'resources/mediawiki.special/mediawiki.special.vforms.css',
+ 'resources/mediawiki.special/mediawiki.special.userLogin.css',
+ ),
+ 'position' => 'top',
+ ),
+ 'mediawiki.special.createaccount' => array(
+ 'styles' => array(
+ 'resources/mediawiki.special/mediawiki.special.vforms.css',
+ 'resources/mediawiki.special/mediawiki.special.createAccount.css',
+ ),
+ ),
+ 'mediawiki.special.createaccount.js' => array(
+ 'scripts' => 'resources/mediawiki.special/mediawiki.special.createAccount.js',
+ 'messages' => array(
+ 'createacct-captcha',
+ 'createacct-emailrequired',
+ 'createacct-imgcaptcha-ph'
+ ),
+ 'dependencies' => 'mediawiki.jqueryMsg',
+ 'position' => 'top',
),
'mediawiki.special.javaScriptTest' => array(
'scripts' => 'resources/mediawiki.special/mediawiki.special.javaScriptTest.js',
@@ -877,6 +1089,7 @@ return array(
) ),
'dependencies' => array( 'jquery.qunit' ),
'position' => 'top',
+ 'targets' => array( 'desktop', 'mobile' ),
),
/* MediaWiki Tests */
@@ -884,12 +1097,14 @@ return array(
'mediawiki.tests.qunit.testrunner' => array(
'scripts' => 'tests/qunit/data/testrunner.js',
'dependencies' => array(
+ 'jquery.getAttrs',
'jquery.qunit',
'jquery.qunit.completenessTest',
'mediawiki.page.startup',
'mediawiki.page.ready',
),
'position' => 'top',
+ 'targets' => array( 'desktop', 'mobile' ),
),
/* MediaWiki Legacy */
@@ -910,14 +1125,9 @@ return array(
'localBasePath' => $GLOBALS['wgStyleDirectory'],
),
'mediawiki.legacy.config' => array(
+ // Used in the web installer. Test it after modifying this definition!
'scripts' => 'common/config.js',
- 'styles' => array( 'common/config.css', 'common/config-cc.css' ),
- 'remoteBasePath' => $GLOBALS['wgStylePath'],
- 'localBasePath' => $GLOBALS['wgStyleDirectory'],
- 'dependencies' => 'mediawiki.legacy.wikibits',
- ),
- 'mediawiki.legacy.IEFixes' => array(
- 'scripts' => 'common/IEFixes.js',
+ 'styles' => array( 'common/config.css' ),
'remoteBasePath' => $GLOBALS['wgStylePath'],
'localBasePath' => $GLOBALS['wgStyleDirectory'],
'dependencies' => 'mediawiki.legacy.wikibits',
@@ -927,12 +1137,12 @@ return array(
'remoteBasePath' => $GLOBALS['wgStylePath'],
'localBasePath' => $GLOBALS['wgStyleDirectory'],
'dependencies' => array(
- 'mediawiki.legacy.wikibits',
'jquery.byteLimit',
),
'position' => 'top',
),
'mediawiki.legacy.shared' => array(
+ // Used in the web installer. Test it after modifying this definition!
'styles' => array( 'common/shared.css' => array( 'media' => 'screen' ) ),
'remoteBasePath' => $GLOBALS['wgStylePath'],
'localBasePath' => $GLOBALS['wgStyleDirectory'],
@@ -947,7 +1157,9 @@ return array(
'remoteBasePath' => $GLOBALS['wgStylePath'],
'localBasePath' => $GLOBALS['wgStyleDirectory'],
'dependencies' => array(
- 'mediawiki.legacy.wikibits',
+ 'jquery.spinner',
+ 'mediawiki.api',
+ 'mediawiki.Title',
'mediawiki.util',
),
),
@@ -960,9 +1172,11 @@ return array(
),
'position' => 'top',
),
- 'mediawiki.legacy.wikiprintable' => array(
- 'styles' => array( 'common/wikiprintable.css' => array( 'media' => 'print' ) ),
- 'remoteBasePath' => $GLOBALS['wgStylePath'],
- 'localBasePath' => $GLOBALS['wgStyleDirectory'],
+ 'mediawiki.ui' => array(
+ 'skinStyles' => array(
+ 'default' => 'resources/mediawiki.ui/mediawiki.ui.default.css',
+ 'vector' => 'resources/mediawiki.ui/mediawiki.ui.vector.css',
+ ),
+ 'position' => 'top',
),
);
diff --git a/resources/jquery.chosen/LICENSE b/resources/jquery.chosen/LICENSE
new file mode 100644
index 00000000..0675dc52
--- /dev/null
+++ b/resources/jquery.chosen/LICENSE
@@ -0,0 +1,24 @@
+# Chosen, a Select Box Enhancer for jQuery and Protoype
+## by Patrick Filler for [Harvest](http://getharvest.com)
+
+Available for use under the [MIT License](http://en.wikipedia.org/wiki/MIT_License)
+
+Copyright (c) 2011-2013 by Harvest
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/resources/jquery.chosen/chosen-sprite.png b/resources/jquery.chosen/chosen-sprite.png
new file mode 100644
index 00000000..3611ae4a
--- /dev/null
+++ b/resources/jquery.chosen/chosen-sprite.png
Binary files differ
diff --git a/resources/jquery.chosen/chosen-sprite@2x.png b/resources/jquery.chosen/chosen-sprite@2x.png
new file mode 100644
index 00000000..bd61d963
--- /dev/null
+++ b/resources/jquery.chosen/chosen-sprite@2x.png
Binary files differ
diff --git a/resources/jquery.chosen/chosen.css b/resources/jquery.chosen/chosen.css
new file mode 100644
index 00000000..17793ed7
--- /dev/null
+++ b/resources/jquery.chosen/chosen.css
@@ -0,0 +1,440 @@
+/* @group Base */
+.chzn-container {
+ font-size: 13px;
+ position: relative;
+ display: inline-block;
+ vertical-align: middle;
+ zoom: 1;
+ *display: inline;
+}
+.chzn-container .chzn-drop {
+ background: #fff;
+ border: 1px solid #aaa;
+ border-top: 0;
+ position: absolute;
+ top: 100%;
+ left: -9999px;
+ -webkit-box-shadow: 0 4px 5px rgba(0,0,0,.15);
+ -moz-box-shadow : 0 4px 5px rgba(0,0,0,.15);
+ box-shadow : 0 4px 5px rgba(0,0,0,.15);
+ z-index: 1010;
+ width: 100%;
+ -moz-box-sizing : border-box;
+ -ms-box-sizing : border-box;
+ -webkit-box-sizing: border-box;
+ -khtml-box-sizing : border-box;
+ box-sizing : border-box;
+}
+
+.chzn-container.chzn-with-drop .chzn-drop {
+ left: 0;
+}
+
+/* @end */
+
+/* @group Single Chosen */
+.chzn-container-single .chzn-single {
+ background-color: #ffffff;
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0 );
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(20%, #ffffff), color-stop(50%, #f6f6f6), color-stop(52%, #eeeeee), color-stop(100%, #f4f4f4));
+ background-image: -webkit-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
+ background-image: -moz-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
+ background-image: -o-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
+ background-image: linear-gradient(#ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
+ -webkit-border-radius: 5px;
+ -moz-border-radius : 5px;
+ border-radius : 5px;
+ -moz-background-clip : padding;
+ -webkit-background-clip: padding-box;
+ background-clip : padding-box;
+ border: 1px solid #aaaaaa;
+ -webkit-box-shadow: 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1);
+ -moz-box-shadow : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1);
+ box-shadow : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1);
+ display: block;
+ overflow: hidden;
+ white-space: nowrap;
+ position: relative;
+ height: 23px;
+ line-height: 24px;
+ padding: 0 0 0 8px;
+ color: #444444;
+ text-decoration: none;
+}
+.chzn-container-single .chzn-default {
+ color: #999;
+}
+.chzn-container-single .chzn-single span {
+ margin-right: 26px;
+ display: block;
+ overflow: hidden;
+ white-space: nowrap;
+ -o-text-overflow: ellipsis;
+ -ms-text-overflow: ellipsis;
+ text-overflow: ellipsis;
+}
+.chzn-container-single .chzn-single abbr {
+ display: block;
+ position: absolute;
+ right: 26px;
+ top: 6px;
+ width: 12px;
+ height: 12px;
+ font-size: 1px;
+ background: url('chosen-sprite.png') -42px 1px no-repeat;
+}
+.chzn-container-single .chzn-single abbr:hover {
+ background-position: -42px -10px;
+}
+.chzn-container-single.chzn-disabled .chzn-single abbr:hover {
+ background-position: -42px -10px;
+}
+.chzn-container-single .chzn-single div {
+ position: absolute;
+ right: 0;
+ top: 0;
+ display: block;
+ height: 100%;
+ width: 18px;
+}
+.chzn-container-single .chzn-single div b {
+ background: url('chosen-sprite.png') no-repeat 0px 2px;
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+.chzn-container-single .chzn-search {
+ padding: 3px 4px;
+ position: relative;
+ margin: 0;
+ white-space: nowrap;
+ z-index: 1010;
+}
+.chzn-container-single .chzn-search input {
+ background: #fff url('chosen-sprite.png') no-repeat 100% -20px;
+ background: url('chosen-sprite.png') no-repeat 100% -20px, -webkit-gradient(linear, 0 0, 0 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
+ background: url('chosen-sprite.png') no-repeat 100% -20px, -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background: url('chosen-sprite.png') no-repeat 100% -20px, -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background: url('chosen-sprite.png') no-repeat 100% -20px, -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background: url('chosen-sprite.png') no-repeat 100% -20px, linear-gradient(#eeeeee 1%, #ffffff 15%);
+ margin: 1px 0;
+ padding: 4px 20px 4px 5px;
+ outline: 0;
+ border: 1px solid #aaa;
+ font-family: sans-serif;
+ font-size: 1em;
+ width: 100%;
+ -moz-box-sizing : border-box;
+ -ms-box-sizing : border-box;
+ -webkit-box-sizing: border-box;
+ -khtml-box-sizing : border-box;
+ box-sizing : border-box;
+}
+.chzn-container-single .chzn-drop {
+ margin-top: -1px;
+ -webkit-border-radius: 0 0 4px 4px;
+ -moz-border-radius : 0 0 4px 4px;
+ border-radius : 0 0 4px 4px;
+ -moz-background-clip : padding;
+ -webkit-background-clip: padding-box;
+ background-clip : padding-box;
+}
+.chzn-container-single-nosearch .chzn-search {
+ position: absolute;
+ left: -9999px;
+}
+/* @end */
+
+/* @group Multi Chosen */
+.chzn-container-multi .chzn-choices {
+ background-color: #fff;
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
+ background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background-image: linear-gradient(#eeeeee 1%, #ffffff 15%);
+ border: 1px solid #aaa;
+ margin: 0;
+ padding: 0;
+ cursor: text;
+ overflow: hidden;
+ height: auto !important;
+ height: 1%;
+ position: relative;
+ width: 100%;
+ -moz-box-sizing : border-box;
+ -ms-box-sizing : border-box;
+ -webkit-box-sizing: border-box;
+ -khtml-box-sizing : border-box;
+ box-sizing : border-box;
+}
+.chzn-container-multi .chzn-choices li {
+ float: left;
+ list-style: none;
+}
+.chzn-container-multi .chzn-choices .search-field {
+ white-space: nowrap;
+ margin: 0;
+ padding: 0;
+}
+.chzn-container-multi .chzn-choices .search-field input {
+ color: #666;
+ background: transparent !important;
+ border: 0 !important;
+ font-family: sans-serif;
+ font-size: 100%;
+ height: 15px;
+ padding: 5px;
+ margin: 1px 0;
+ outline: 0;
+ -webkit-box-shadow: none;
+ -moz-box-shadow : none;
+ box-shadow : none;
+}
+.chzn-container-multi .chzn-choices .search-field .default {
+ color: #999;
+}
+.chzn-container-multi .chzn-choices .search-choice {
+ -webkit-border-radius: 3px;
+ -moz-border-radius : 3px;
+ border-radius : 3px;
+ -moz-background-clip : padding;
+ -webkit-background-clip: padding-box;
+ background-clip : padding-box;
+ background-color: #e4e4e4;
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f4f4f4', endColorstr='#eeeeee', GradientType=0 );
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
+ background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ -webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+ -moz-box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+ box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+ color: #333;
+ border: 1px solid #aaaaaa;
+ line-height: 13px;
+ padding: 3px 20px 3px 5px;
+ margin: 3px 0 3px 5px;
+ position: relative;
+ cursor: default;
+}
+.chzn-container-multi .chzn-choices .search-choice.search-choice-disabled {
+ background-color: #e4e4e4;
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f4f4f4', endColorstr='#eeeeee', GradientType=0 );
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
+ background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ color: #666;
+ border: 1px solid #cccccc;
+ padding-right: 5px;
+}
+.chzn-container-multi .chzn-choices .search-choice-focus {
+ background: #d4d4d4;
+}
+.chzn-container-multi .chzn-choices .search-choice .search-choice-close {
+ display: block;
+ position: absolute;
+ right: 3px;
+ top: 4px;
+ width: 12px;
+ height: 12px;
+ font-size: 1px;
+ background: url('chosen-sprite.png') -42px 1px no-repeat;
+}
+.chzn-container-multi .chzn-choices .search-choice .search-choice-close:hover {
+ background-position: -42px -10px;
+}
+.chzn-container-multi .chzn-choices .search-choice-focus .search-choice-close {
+ background-position: -42px -10px;
+}
+/* @end */
+
+/* @group Results */
+.chzn-container .chzn-results {
+ margin: 0 4px 4px 0;
+ max-height: 240px;
+ padding: 0 0 0 4px;
+ position: relative;
+ overflow-x: hidden;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+}
+.chzn-container-multi .chzn-results {
+ margin: 0;
+ padding: 0;
+}
+.chzn-container .chzn-results li {
+ display: none;
+ line-height: 15px;
+ padding: 5px 6px;
+ margin: 0;
+ list-style: none;
+}
+.chzn-container .chzn-results .active-result {
+ cursor: pointer;
+ display: list-item;
+}
+.chzn-container .chzn-results .highlighted {
+ background-color: #3875d7;
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#3875d7', endColorstr='#2a62bc', GradientType=0 );
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(20%, #3875d7), color-stop(90%, #2a62bc));
+ background-image: -webkit-linear-gradient(top, #3875d7 20%, #2a62bc 90%);
+ background-image: -moz-linear-gradient(top, #3875d7 20%, #2a62bc 90%);
+ background-image: -o-linear-gradient(top, #3875d7 20%, #2a62bc 90%);
+ background-image: linear-gradient(#3875d7 20%, #2a62bc 90%);
+ color: #fff;
+}
+.chzn-container .chzn-results li em {
+ background: #feffde;
+ font-style: normal;
+}
+.chzn-container .chzn-results .highlighted em {
+ background: transparent;
+}
+.chzn-container .chzn-results .no-results {
+ background: #f4f4f4;
+ display: list-item;
+}
+.chzn-container .chzn-results .group-result {
+ cursor: default;
+ color: #999;
+ font-weight: bold;
+}
+.chzn-container .chzn-results .group-option {
+ padding-left: 15px;
+}
+.chzn-container-multi .chzn-drop .result-selected {
+ display: none;
+}
+.chzn-container .chzn-results-scroll {
+ background: white;
+ margin: 0 4px;
+ position: absolute;
+ text-align: center;
+ width: 321px; /* This should by dynamic with js */
+ z-index: 1;
+}
+.chzn-container .chzn-results-scroll span {
+ display: inline-block;
+ height: 17px;
+ text-indent: -5000px;
+ width: 9px;
+}
+.chzn-container .chzn-results-scroll-down {
+ bottom: 0;
+}
+.chzn-container .chzn-results-scroll-down span {
+ background: url('chosen-sprite.png') no-repeat -4px -3px;
+}
+.chzn-container .chzn-results-scroll-up span {
+ background: url('chosen-sprite.png') no-repeat -22px -3px;
+}
+/* @end */
+
+/* @group Active */
+.chzn-container-active .chzn-single {
+ -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
+ -moz-box-shadow : 0 0 5px rgba(0,0,0,.3);
+ box-shadow : 0 0 5px rgba(0,0,0,.3);
+ border: 1px solid #5897fb;
+}
+.chzn-container-active.chzn-with-drop .chzn-single {
+ border: 1px solid #aaa;
+ -webkit-box-shadow: 0 1px 0 #fff inset;
+ -moz-box-shadow : 0 1px 0 #fff inset;
+ box-shadow : 0 1px 0 #fff inset;
+ background-color: #eee;
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0 );
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(20%, #eeeeee), color-stop(80%, #ffffff));
+ background-image: -webkit-linear-gradient(top, #eeeeee 20%, #ffffff 80%);
+ background-image: -moz-linear-gradient(top, #eeeeee 20%, #ffffff 80%);
+ background-image: -o-linear-gradient(top, #eeeeee 20%, #ffffff 80%);
+ background-image: linear-gradient(#eeeeee 20%, #ffffff 80%);
+ -webkit-border-bottom-left-radius : 0;
+ -webkit-border-bottom-right-radius: 0;
+ -moz-border-radius-bottomleft : 0;
+ -moz-border-radius-bottomright: 0;
+ border-bottom-left-radius : 0;
+ border-bottom-right-radius: 0;
+}
+.chzn-container-active.chzn-with-drop .chzn-single div {
+ background: transparent;
+ border-left: none;
+}
+.chzn-container-active.chzn-with-drop .chzn-single div b {
+ background-position: -18px 2px;
+}
+.chzn-container-active .chzn-choices {
+ -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
+ -moz-box-shadow : 0 0 5px rgba(0,0,0,.3);
+ box-shadow : 0 0 5px rgba(0,0,0,.3);
+ border: 1px solid #5897fb;
+}
+.chzn-container-active .chzn-choices .search-field input {
+ color: #111 !important;
+}
+/* @end */
+
+/* @group Disabled Support */
+.chzn-disabled {
+ cursor: default;
+ opacity:0.5 !important;
+}
+.chzn-disabled .chzn-single {
+ cursor: default;
+}
+.chzn-disabled .chzn-choices .search-choice .search-choice-close {
+ cursor: default;
+}
+
+/* @group Right to Left */
+.chzn-rtl { text-align: right; }
+.chzn-rtl .chzn-single { padding: 0 8px 0 0; overflow: visible; }
+.chzn-rtl .chzn-single span { margin-left: 26px; margin-right: 0; direction: rtl; }
+
+.chzn-rtl .chzn-single div { left: 3px; right: auto; }
+.chzn-rtl .chzn-single abbr {
+ left: 26px;
+ right: auto;
+}
+.chzn-rtl .chzn-choices .search-field input { direction: rtl; }
+.chzn-rtl .chzn-choices li { float: right; }
+.chzn-rtl .chzn-choices .search-choice { padding: 3px 5px 3px 19px; margin: 3px 5px 3px 0; }
+.chzn-rtl .chzn-choices .search-choice .search-choice-close { left: 4px; right: auto; }
+.chzn-rtl .chzn-search { left: 9999px; }
+.chzn-rtl.chzn-with-drop .chzn-search { left: 0px; }
+.chzn-rtl .chzn-drop { left: 9999px; }
+.chzn-rtl.chzn-container-single .chzn-results { margin: 0 0 4px 4px; padding: 0 4px 0 0; }
+.chzn-rtl .chzn-results .group-option { padding-left: 0; padding-right: 15px; }
+.chzn-rtl.chzn-container-active.chzn-with-drop .chzn-single div { border-right: none; }
+.chzn-rtl .chzn-search input {
+ background: #fff url('chosen-sprite.png') no-repeat -30px -20px;
+ background: url('chosen-sprite.png') no-repeat -30px -20px, -webkit-gradient(linear, 0 0, 0 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
+ background: url('chosen-sprite.png') no-repeat -30px -20px, -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background: url('chosen-sprite.png') no-repeat -30px -20px, -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background: url('chosen-sprite.png') no-repeat -30px -20px, -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background: url('chosen-sprite.png') no-repeat -30px -20px, linear-gradient(#eeeeee 1%, #ffffff 15%);
+ padding: 4px 5px 4px 20px;
+ direction: rtl;
+}
+.chzn-container-single.chzn-rtl .chzn-single div b {
+ background-position: 6px 2px;
+}
+.chzn-container-single.chzn-rtl.chzn-with-drop .chzn-single div b {
+ background-position: -12px 2px;
+}
+/* @end */
+
+/* @group Retina compatibility */
+@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-resolution: 144dpi) {
+ .chzn-rtl .chzn-search input, .chzn-container-single .chzn-single abbr, .chzn-container-single .chzn-single div b, .chzn-container-single .chzn-search input, .chzn-container-multi .chzn-choices .search-choice .search-choice-close, .chzn-container .chzn-results-scroll-down span, .chzn-container .chzn-results-scroll-up span {
+ background-image: url('chosen-sprite@2x.png') !important;
+ background-repeat: no-repeat !important;
+ background-size: 52px 37px !important;
+ }
+}
+/* @end */
diff --git a/resources/jquery.chosen/chosen.jquery.js b/resources/jquery.chosen/chosen.jquery.js
new file mode 100644
index 00000000..745174f7
--- /dev/null
+++ b/resources/jquery.chosen/chosen.jquery.js
@@ -0,0 +1,1103 @@
+// Chosen, a Select Box Enhancer for jQuery and Protoype
+// by Patrick Filler for Harvest, http://getharvest.com
+//
+// Version 0.9.14
+// Full source at https://github.com/harvesthq/chosen
+// Copyright (c) 2011 Harvest http://getharvest.com
+
+// MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md
+// This file is generated by `cake build`, do not edit it by hand.
+(function() {
+ var SelectParser;
+
+ SelectParser = (function() {
+
+ function SelectParser() {
+ this.options_index = 0;
+ this.parsed = [];
+ }
+
+ SelectParser.prototype.add_node = function(child) {
+ if (child.nodeName.toUpperCase() === "OPTGROUP") {
+ return this.add_group(child);
+ } else {
+ return this.add_option(child);
+ }
+ };
+
+ SelectParser.prototype.add_group = function(group) {
+ var group_position, option, _i, _len, _ref, _results;
+ group_position = this.parsed.length;
+ this.parsed.push({
+ array_index: group_position,
+ group: true,
+ label: group.label,
+ children: 0,
+ disabled: group.disabled
+ });
+ _ref = group.childNodes;
+ _results = [];
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ option = _ref[_i];
+ _results.push(this.add_option(option, group_position, group.disabled));
+ }
+ return _results;
+ };
+
+ SelectParser.prototype.add_option = function(option, group_position, group_disabled) {
+ if (option.nodeName.toUpperCase() === "OPTION") {
+ if (option.text !== "") {
+ if (group_position != null) {
+ this.parsed[group_position].children += 1;
+ }
+ this.parsed.push({
+ array_index: this.parsed.length,
+ options_index: this.options_index,
+ value: option.value,
+ text: option.text,
+ html: option.innerHTML,
+ selected: option.selected,
+ disabled: group_disabled === true ? group_disabled : option.disabled,
+ group_array_index: group_position,
+ classes: option.className,
+ style: option.style.cssText
+ });
+ } else {
+ this.parsed.push({
+ array_index: this.parsed.length,
+ options_index: this.options_index,
+ empty: true
+ });
+ }
+ return this.options_index += 1;
+ }
+ };
+
+ return SelectParser;
+
+ })();
+
+ SelectParser.select_to_array = function(select) {
+ var child, parser, _i, _len, _ref;
+ parser = new SelectParser();
+ _ref = select.childNodes;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ child = _ref[_i];
+ parser.add_node(child);
+ }
+ return parser.parsed;
+ };
+
+ this.SelectParser = SelectParser;
+
+}).call(this);
+
+/*
+Chosen source: generate output using 'cake build'
+Copyright (c) 2011 by Harvest
+*/
+
+
+(function() {
+ var AbstractChosen, root;
+
+ root = this;
+
+ AbstractChosen = (function() {
+
+ function AbstractChosen(form_field, options) {
+ this.form_field = form_field;
+ this.options = options != null ? options : {};
+ if (!AbstractChosen.browser_is_supported()) {
+ return;
+ }
+ this.is_multiple = this.form_field.multiple;
+ this.set_default_text();
+ this.set_default_values();
+ this.setup();
+ this.set_up_html();
+ this.register_observers();
+ this.finish_setup();
+ }
+
+ AbstractChosen.prototype.set_default_values = function() {
+ var _this = this;
+ this.click_test_action = function(evt) {
+ return _this.test_active_click(evt);
+ };
+ this.activate_action = function(evt) {
+ return _this.activate_field(evt);
+ };
+ this.active_field = false;
+ this.mouse_on_container = false;
+ this.results_showing = false;
+ this.result_highlighted = null;
+ this.result_single_selected = null;
+ this.allow_single_deselect = (this.options.allow_single_deselect != null) && (this.form_field.options[0] != null) && this.form_field.options[0].text === "" ? this.options.allow_single_deselect : false;
+ this.disable_search_threshold = this.options.disable_search_threshold || 0;
+ this.disable_search = this.options.disable_search || false;
+ this.enable_split_word_search = this.options.enable_split_word_search != null ? this.options.enable_split_word_search : true;
+ this.search_contains = this.options.search_contains || false;
+ this.choices = 0;
+ this.single_backstroke_delete = this.options.single_backstroke_delete || false;
+ this.max_selected_options = this.options.max_selected_options || Infinity;
+ return this.inherit_select_classes = this.options.inherit_select_classes || false;
+ };
+
+ AbstractChosen.prototype.set_default_text = function() {
+ if (this.form_field.getAttribute("data-placeholder")) {
+ this.default_text = this.form_field.getAttribute("data-placeholder");
+ } else if (this.is_multiple) {
+ this.default_text = this.options.placeholder_text_multiple || this.options.placeholder_text || AbstractChosen.default_multiple_text;
+ } else {
+ this.default_text = this.options.placeholder_text_single || this.options.placeholder_text || AbstractChosen.default_single_text;
+ }
+ return this.results_none_found = this.form_field.getAttribute("data-no_results_text") || this.options.no_results_text || AbstractChosen.default_no_result_text;
+ };
+
+ AbstractChosen.prototype.mouse_enter = function() {
+ return this.mouse_on_container = true;
+ };
+
+ AbstractChosen.prototype.mouse_leave = function() {
+ return this.mouse_on_container = false;
+ };
+
+ AbstractChosen.prototype.input_focus = function(evt) {
+ var _this = this;
+ if (this.is_multiple) {
+ if (!this.active_field) {
+ return setTimeout((function() {
+ return _this.container_mousedown();
+ }), 50);
+ }
+ } else {
+ if (!this.active_field) {
+ return this.activate_field();
+ }
+ }
+ };
+
+ AbstractChosen.prototype.input_blur = function(evt) {
+ var _this = this;
+ if (!this.mouse_on_container) {
+ this.active_field = false;
+ return setTimeout((function() {
+ return _this.blur_test();
+ }), 100);
+ }
+ };
+
+ AbstractChosen.prototype.result_add_option = function(option) {
+ var classes, style;
+ if (!option.disabled) {
+ option.dom_id = this.container_id + "_o_" + option.array_index;
+ classes = option.selected && this.is_multiple ? [] : ["active-result"];
+ if (option.selected) {
+ classes.push("result-selected");
+ }
+ if (option.group_array_index != null) {
+ classes.push("group-option");
+ }
+ if (option.classes !== "") {
+ classes.push(option.classes);
+ }
+ style = option.style.cssText !== "" ? " style=\"" + option.style + "\"" : "";
+ return '<li id="' + option.dom_id + '" class="' + classes.join(' ') + '"' + style + '>' + option.html + '</li>';
+ } else {
+ return "";
+ }
+ };
+
+ AbstractChosen.prototype.results_update_field = function() {
+ this.set_default_text();
+ if (!this.is_multiple) {
+ this.results_reset_cleanup();
+ }
+ this.result_clear_highlight();
+ this.result_single_selected = null;
+ return this.results_build();
+ };
+
+ AbstractChosen.prototype.results_toggle = function() {
+ if (this.results_showing) {
+ return this.results_hide();
+ } else {
+ return this.results_show();
+ }
+ };
+
+ AbstractChosen.prototype.results_search = function(evt) {
+ if (this.results_showing) {
+ return this.winnow_results();
+ } else {
+ return this.results_show();
+ }
+ };
+
+ AbstractChosen.prototype.choices_click = function(evt) {
+ evt.preventDefault();
+ if (!this.results_showing) {
+ return this.results_show();
+ }
+ };
+
+ AbstractChosen.prototype.keyup_checker = function(evt) {
+ var stroke, _ref;
+ stroke = (_ref = evt.which) != null ? _ref : evt.keyCode;
+ this.search_field_scale();
+ switch (stroke) {
+ case 8:
+ if (this.is_multiple && this.backstroke_length < 1 && this.choices > 0) {
+ return this.keydown_backstroke();
+ } else if (!this.pending_backstroke) {
+ this.result_clear_highlight();
+ return this.results_search();
+ }
+ break;
+ case 13:
+ evt.preventDefault();
+ if (this.results_showing) {
+ return this.result_select(evt);
+ }
+ break;
+ case 27:
+ if (this.results_showing) {
+ this.results_hide();
+ }
+ return true;
+ case 9:
+ case 38:
+ case 40:
+ case 16:
+ case 91:
+ case 17:
+ break;
+ default:
+ return this.results_search();
+ }
+ };
+
+ AbstractChosen.prototype.generate_field_id = function() {
+ var new_id;
+ new_id = this.generate_random_id();
+ this.form_field.id = new_id;
+ return new_id;
+ };
+
+ AbstractChosen.prototype.generate_random_char = function() {
+ var chars, newchar, rand;
+ chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ rand = Math.floor(Math.random() * chars.length);
+ return newchar = chars.substring(rand, rand + 1);
+ };
+
+ AbstractChosen.prototype.container_width = function() {
+ var width;
+ if (this.options.width != null) {
+ return this.options.width;
+ }
+ width = window.getComputedStyle != null ? parseFloat(window.getComputedStyle(this.form_field).getPropertyValue('width')) : (typeof jQuery !== "undefined" && jQuery !== null) && (this.form_field_jq != null) ? this.form_field_jq.outerWidth() : this.form_field.getWidth();
+ return width + "px";
+ };
+
+ AbstractChosen.browser_is_supported = function() {
+ var _ref;
+ if (window.navigator.appName === "Microsoft Internet Explorer") {
+ return (null !== (_ref = document.documentMode) && _ref >= 8);
+ }
+ return true;
+ };
+
+ AbstractChosen.default_multiple_text = "Select Some Options";
+
+ AbstractChosen.default_single_text = "Select an Option";
+
+ AbstractChosen.default_no_result_text = "No results match";
+
+ return AbstractChosen;
+
+ })();
+
+ root.AbstractChosen = AbstractChosen;
+
+}).call(this);
+
+/*
+Chosen source: generate output using 'cake build'
+Copyright (c) 2011 by Harvest
+*/
+
+
+(function() {
+ var $, Chosen, root,
+ __hasProp = {}.hasOwnProperty,
+ __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
+
+ root = this;
+
+ $ = jQuery;
+
+ $.fn.extend({
+ chosen: function(options) {
+ if (!AbstractChosen.browser_is_supported()) {
+ return this;
+ }
+ return this.each(function(input_field) {
+ var $this;
+ $this = $(this);
+ if (!$this.hasClass("chzn-done")) {
+ return $this.data('chosen', new Chosen(this, options));
+ }
+ });
+ }
+ });
+
+ Chosen = (function(_super) {
+
+ __extends(Chosen, _super);
+
+ function Chosen() {
+ return Chosen.__super__.constructor.apply(this, arguments);
+ }
+
+ Chosen.prototype.setup = function() {
+ this.form_field_jq = $(this.form_field);
+ this.current_selectedIndex = this.form_field.selectedIndex;
+ return this.is_rtl = this.form_field_jq.hasClass("chzn-rtl");
+ };
+
+ Chosen.prototype.finish_setup = function() {
+ return this.form_field_jq.addClass("chzn-done");
+ };
+
+ Chosen.prototype.set_up_html = function() {
+ var container_classes, container_props;
+ this.container_id = this.form_field.id.length ? this.form_field.id.replace(/[^\w]/g, '_') : this.generate_field_id();
+ this.container_id += "_chzn";
+ container_classes = ["chzn-container"];
+ container_classes.push("chzn-container-" + (this.is_multiple ? "multi" : "single"));
+ if (this.inherit_select_classes && this.form_field.className) {
+ container_classes.push(this.form_field.className);
+ }
+ if (this.is_rtl) {
+ container_classes.push("chzn-rtl");
+ }
+ container_props = {
+ 'id': this.container_id,
+ 'class': container_classes.join(' '),
+ 'style': "width: " + (this.container_width()) + ";",
+ 'title': this.form_field.title
+ };
+ this.container = $("<div />", container_props);
+ if (this.is_multiple) {
+ this.container.html('<ul class="chzn-choices"><li class="search-field"><input type="text" value="' + this.default_text + '" class="default" autocomplete="off" style="width:auto;" /></li></ul><div class="chzn-drop"><ul class="chzn-results"></ul></div>');
+ } else {
+ this.container.html('<a href="javascript:void(0)" class="chzn-single chzn-default" tabindex="-1"><span>' + this.default_text + '</span><div><b></b></div></a><div class="chzn-drop"><div class="chzn-search"><input type="text" autocomplete="off" /></div><ul class="chzn-results"></ul></div>');
+ }
+ this.form_field_jq.hide().after(this.container);
+ this.dropdown = this.container.find('div.chzn-drop').first();
+ this.search_field = this.container.find('input').first();
+ this.search_results = this.container.find('ul.chzn-results').first();
+ this.search_field_scale();
+ this.search_no_results = this.container.find('li.no-results').first();
+ if (this.is_multiple) {
+ this.search_choices = this.container.find('ul.chzn-choices').first();
+ this.search_container = this.container.find('li.search-field').first();
+ } else {
+ this.search_container = this.container.find('div.chzn-search').first();
+ this.selected_item = this.container.find('.chzn-single').first();
+ }
+ this.results_build();
+ this.set_tab_index();
+ this.set_label_behavior();
+ return this.form_field_jq.trigger("liszt:ready", {
+ chosen: this
+ });
+ };
+
+ Chosen.prototype.register_observers = function() {
+ var _this = this;
+ this.container.mousedown(function(evt) {
+ _this.container_mousedown(evt);
+ });
+ this.container.mouseup(function(evt) {
+ _this.container_mouseup(evt);
+ });
+ this.container.mouseenter(function(evt) {
+ _this.mouse_enter(evt);
+ });
+ this.container.mouseleave(function(evt) {
+ _this.mouse_leave(evt);
+ });
+ this.search_results.mouseup(function(evt) {
+ _this.search_results_mouseup(evt);
+ });
+ this.search_results.mouseover(function(evt) {
+ _this.search_results_mouseover(evt);
+ });
+ this.search_results.mouseout(function(evt) {
+ _this.search_results_mouseout(evt);
+ });
+ this.search_results.bind('mousewheel DOMMouseScroll', function(evt) {
+ _this.search_results_mousewheel(evt);
+ });
+ this.form_field_jq.bind("liszt:updated", function(evt) {
+ _this.results_update_field(evt);
+ });
+ this.form_field_jq.bind("liszt:activate", function(evt) {
+ _this.activate_field(evt);
+ });
+ this.form_field_jq.bind("liszt:open", function(evt) {
+ _this.container_mousedown(evt);
+ });
+ this.search_field.blur(function(evt) {
+ _this.input_blur(evt);
+ });
+ this.search_field.keyup(function(evt) {
+ _this.keyup_checker(evt);
+ });
+ this.search_field.keydown(function(evt) {
+ _this.keydown_checker(evt);
+ });
+ this.search_field.focus(function(evt) {
+ _this.input_focus(evt);
+ });
+ if (this.is_multiple) {
+ return this.search_choices.click(function(evt) {
+ _this.choices_click(evt);
+ });
+ } else {
+ return this.container.click(function(evt) {
+ evt.preventDefault();
+ });
+ }
+ };
+
+ Chosen.prototype.search_field_disabled = function() {
+ this.is_disabled = this.form_field_jq[0].disabled;
+ if (this.is_disabled) {
+ this.container.addClass('chzn-disabled');
+ this.search_field[0].disabled = true;
+ if (!this.is_multiple) {
+ this.selected_item.unbind("focus", this.activate_action);
+ }
+ return this.close_field();
+ } else {
+ this.container.removeClass('chzn-disabled');
+ this.search_field[0].disabled = false;
+ if (!this.is_multiple) {
+ return this.selected_item.bind("focus", this.activate_action);
+ }
+ }
+ };
+
+ Chosen.prototype.container_mousedown = function(evt) {
+ if (!this.is_disabled) {
+ if (evt && evt.type === "mousedown" && !this.results_showing) {
+ evt.preventDefault();
+ }
+ if (!((evt != null) && ($(evt.target)).hasClass("search-choice-close"))) {
+ if (!this.active_field) {
+ if (this.is_multiple) {
+ this.search_field.val("");
+ }
+ $(document).click(this.click_test_action);
+ this.results_show();
+ } else if (!this.is_multiple && evt && (($(evt.target)[0] === this.selected_item[0]) || $(evt.target).parents("a.chzn-single").length)) {
+ evt.preventDefault();
+ this.results_toggle();
+ }
+ return this.activate_field();
+ }
+ }
+ };
+
+ Chosen.prototype.container_mouseup = function(evt) {
+ if (evt.target.nodeName === "ABBR" && !this.is_disabled) {
+ return this.results_reset(evt);
+ }
+ };
+
+ Chosen.prototype.search_results_mousewheel = function(evt) {
+ var delta, _ref, _ref1;
+ delta = -((_ref = evt.originalEvent) != null ? _ref.wheelDelta : void 0) || ((_ref1 = evt.originialEvent) != null ? _ref1.detail : void 0);
+ if (delta != null) {
+ evt.preventDefault();
+ if (evt.type === 'DOMMouseScroll') {
+ delta = delta * 40;
+ }
+ return this.search_results.scrollTop(delta + this.search_results.scrollTop());
+ }
+ };
+
+ Chosen.prototype.blur_test = function(evt) {
+ if (!this.active_field && this.container.hasClass("chzn-container-active")) {
+ return this.close_field();
+ }
+ };
+
+ Chosen.prototype.close_field = function() {
+ $(document).unbind("click", this.click_test_action);
+ this.active_field = false;
+ this.results_hide();
+ this.container.removeClass("chzn-container-active");
+ this.winnow_results_clear();
+ this.clear_backstroke();
+ this.show_search_field_default();
+ return this.search_field_scale();
+ };
+
+ Chosen.prototype.activate_field = function() {
+ this.container.addClass("chzn-container-active");
+ this.active_field = true;
+ this.search_field.val(this.search_field.val());
+ return this.search_field.focus();
+ };
+
+ Chosen.prototype.test_active_click = function(evt) {
+ if ($(evt.target).parents('#' + this.container_id).length) {
+ return this.active_field = true;
+ } else {
+ return this.close_field();
+ }
+ };
+
+ Chosen.prototype.results_build = function() {
+ var content, data, _i, _len, _ref;
+ this.parsing = true;
+ this.results_data = root.SelectParser.select_to_array(this.form_field);
+ if (this.is_multiple && this.choices > 0) {
+ this.search_choices.find("li.search-choice").remove();
+ this.choices = 0;
+ } else if (!this.is_multiple) {
+ this.selected_item.addClass("chzn-default").find("span").text(this.default_text);
+ if (this.disable_search || this.form_field.options.length <= this.disable_search_threshold) {
+ this.container.addClass("chzn-container-single-nosearch");
+ } else {
+ this.container.removeClass("chzn-container-single-nosearch");
+ }
+ }
+ content = '';
+ _ref = this.results_data;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ data = _ref[_i];
+ if (data.group) {
+ content += this.result_add_group(data);
+ } else if (!data.empty) {
+ content += this.result_add_option(data);
+ if (data.selected && this.is_multiple) {
+ this.choice_build(data);
+ } else if (data.selected && !this.is_multiple) {
+ this.selected_item.removeClass("chzn-default").find("span").text(data.text);
+ if (this.allow_single_deselect) {
+ this.single_deselect_control_build();
+ }
+ }
+ }
+ }
+ this.search_field_disabled();
+ this.show_search_field_default();
+ this.search_field_scale();
+ this.search_results.html(content);
+ return this.parsing = false;
+ };
+
+ Chosen.prototype.result_add_group = function(group) {
+ if (!group.disabled) {
+ group.dom_id = this.container_id + "_g_" + group.array_index;
+ return '<li id="' + group.dom_id + '" class="group-result">' + $("<div />").text(group.label).html() + '</li>';
+ } else {
+ return "";
+ }
+ };
+
+ Chosen.prototype.result_do_highlight = function(el) {
+ var high_bottom, high_top, maxHeight, visible_bottom, visible_top;
+ if (el.length) {
+ this.result_clear_highlight();
+ this.result_highlight = el;
+ this.result_highlight.addClass("highlighted");
+ maxHeight = parseInt(this.search_results.css("maxHeight"), 10);
+ visible_top = this.search_results.scrollTop();
+ visible_bottom = maxHeight + visible_top;
+ high_top = this.result_highlight.position().top + this.search_results.scrollTop();
+ high_bottom = high_top + this.result_highlight.outerHeight();
+ if (high_bottom >= visible_bottom) {
+ return this.search_results.scrollTop((high_bottom - maxHeight) > 0 ? high_bottom - maxHeight : 0);
+ } else if (high_top < visible_top) {
+ return this.search_results.scrollTop(high_top);
+ }
+ }
+ };
+
+ Chosen.prototype.result_clear_highlight = function() {
+ if (this.result_highlight) {
+ this.result_highlight.removeClass("highlighted");
+ }
+ return this.result_highlight = null;
+ };
+
+ Chosen.prototype.results_show = function() {
+ if (this.result_single_selected != null) {
+ this.result_do_highlight(this.result_single_selected);
+ } else if (this.is_multiple && this.max_selected_options <= this.choices) {
+ this.form_field_jq.trigger("liszt:maxselected", {
+ chosen: this
+ });
+ return false;
+ }
+ this.container.addClass("chzn-with-drop");
+ this.form_field_jq.trigger("liszt:showing_dropdown", {
+ chosen: this
+ });
+ this.results_showing = true;
+ this.search_field.focus();
+ this.search_field.val(this.search_field.val());
+ return this.winnow_results();
+ };
+
+ Chosen.prototype.results_hide = function() {
+ this.result_clear_highlight();
+ this.container.removeClass("chzn-with-drop");
+ this.form_field_jq.trigger("liszt:hiding_dropdown", {
+ chosen: this
+ });
+ return this.results_showing = false;
+ };
+
+ Chosen.prototype.set_tab_index = function(el) {
+ var ti;
+ if (this.form_field_jq.attr("tabindex")) {
+ ti = this.form_field_jq.attr("tabindex");
+ this.form_field_jq.attr("tabindex", -1);
+ return this.search_field.attr("tabindex", ti);
+ }
+ };
+
+ Chosen.prototype.set_label_behavior = function() {
+ var _this = this;
+ this.form_field_label = this.form_field_jq.parents("label");
+ if (!this.form_field_label.length && this.form_field.id.length) {
+ this.form_field_label = $("label[for=" + this.form_field.id + "]");
+ }
+ if (this.form_field_label.length > 0) {
+ return this.form_field_label.click(function(evt) {
+ if (_this.is_multiple) {
+ return _this.container_mousedown(evt);
+ } else {
+ return _this.activate_field();
+ }
+ });
+ }
+ };
+
+ Chosen.prototype.show_search_field_default = function() {
+ if (this.is_multiple && this.choices < 1 && !this.active_field) {
+ this.search_field.val(this.default_text);
+ return this.search_field.addClass("default");
+ } else {
+ this.search_field.val("");
+ return this.search_field.removeClass("default");
+ }
+ };
+
+ Chosen.prototype.search_results_mouseup = function(evt) {
+ var target;
+ target = $(evt.target).hasClass("active-result") ? $(evt.target) : $(evt.target).parents(".active-result").first();
+ if (target.length) {
+ this.result_highlight = target;
+ this.result_select(evt);
+ return this.search_field.focus();
+ }
+ };
+
+ Chosen.prototype.search_results_mouseover = function(evt) {
+ var target;
+ target = $(evt.target).hasClass("active-result") ? $(evt.target) : $(evt.target).parents(".active-result").first();
+ if (target) {
+ return this.result_do_highlight(target);
+ }
+ };
+
+ Chosen.prototype.search_results_mouseout = function(evt) {
+ if ($(evt.target).hasClass("active-result" || $(evt.target).parents('.active-result').first())) {
+ return this.result_clear_highlight();
+ }
+ };
+
+ Chosen.prototype.choice_build = function(item) {
+ var choice_id, html, link,
+ _this = this;
+ if (this.is_multiple && this.max_selected_options <= this.choices) {
+ this.form_field_jq.trigger("liszt:maxselected", {
+ chosen: this
+ });
+ return false;
+ }
+ choice_id = this.container_id + "_c_" + item.array_index;
+ this.choices += 1;
+ if (item.disabled) {
+ html = '<li class="search-choice search-choice-disabled" id="' + choice_id + '"><span>' + item.html + '</span></li>';
+ } else {
+ html = '<li class="search-choice" id="' + choice_id + '"><span>' + item.html + '</span><a href="javascript:void(0)" class="search-choice-close" rel="' + item.array_index + '"></a></li>';
+ }
+ this.search_container.before(html);
+ link = $('#' + choice_id).find("a").first();
+ return link.click(function(evt) {
+ return _this.choice_destroy_link_click(evt);
+ });
+ };
+
+ Chosen.prototype.choice_destroy_link_click = function(evt) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ if (!this.is_disabled) {
+ return this.choice_destroy($(evt.target));
+ }
+ };
+
+ Chosen.prototype.choice_destroy = function(link) {
+ if (this.result_deselect(link.attr("rel"))) {
+ this.choices -= 1;
+ this.show_search_field_default();
+ if (this.is_multiple && this.choices > 0 && this.search_field.val().length < 1) {
+ this.results_hide();
+ }
+ link.parents('li').first().remove();
+ return this.search_field_scale();
+ }
+ };
+
+ Chosen.prototype.results_reset = function() {
+ this.form_field.options[0].selected = true;
+ this.selected_item.find("span").text(this.default_text);
+ if (!this.is_multiple) {
+ this.selected_item.addClass("chzn-default");
+ }
+ this.show_search_field_default();
+ this.results_reset_cleanup();
+ this.form_field_jq.trigger("change");
+ if (this.active_field) {
+ return this.results_hide();
+ }
+ };
+
+ Chosen.prototype.results_reset_cleanup = function() {
+ this.current_selectedIndex = this.form_field.selectedIndex;
+ return this.selected_item.find("abbr").remove();
+ };
+
+ Chosen.prototype.result_select = function(evt) {
+ var high, high_id, item, position;
+ if (this.result_highlight) {
+ high = this.result_highlight;
+ high_id = high.attr("id");
+ this.result_clear_highlight();
+ if (this.is_multiple) {
+ this.result_deactivate(high);
+ } else {
+ this.search_results.find(".result-selected").removeClass("result-selected");
+ this.result_single_selected = high;
+ this.selected_item.removeClass("chzn-default");
+ }
+ high.addClass("result-selected");
+ position = high_id.substr(high_id.lastIndexOf("_") + 1);
+ item = this.results_data[position];
+ item.selected = true;
+ this.form_field.options[item.options_index].selected = true;
+ if (this.is_multiple) {
+ this.choice_build(item);
+ } else {
+ this.selected_item.find("span").first().text(item.text);
+ if (this.allow_single_deselect) {
+ this.single_deselect_control_build();
+ }
+ }
+ if (!((evt.metaKey || evt.ctrlKey) && this.is_multiple)) {
+ this.results_hide();
+ }
+ this.search_field.val("");
+ if (this.is_multiple || this.form_field.selectedIndex !== this.current_selectedIndex) {
+ this.form_field_jq.trigger("change", {
+ 'selected': this.form_field.options[item.options_index].value
+ });
+ }
+ this.current_selectedIndex = this.form_field.selectedIndex;
+ return this.search_field_scale();
+ }
+ };
+
+ Chosen.prototype.result_activate = function(el) {
+ return el.addClass("active-result");
+ };
+
+ Chosen.prototype.result_deactivate = function(el) {
+ return el.removeClass("active-result");
+ };
+
+ Chosen.prototype.result_deselect = function(pos) {
+ var result, result_data;
+ result_data = this.results_data[pos];
+ if (!this.form_field.options[result_data.options_index].disabled) {
+ result_data.selected = false;
+ this.form_field.options[result_data.options_index].selected = false;
+ result = $("#" + this.container_id + "_o_" + pos);
+ result.removeClass("result-selected").addClass("active-result").show();
+ this.result_clear_highlight();
+ this.winnow_results();
+ this.form_field_jq.trigger("change", {
+ deselected: this.form_field.options[result_data.options_index].value
+ });
+ this.search_field_scale();
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ Chosen.prototype.single_deselect_control_build = function() {
+ if (this.allow_single_deselect && this.selected_item.find("abbr").length < 1) {
+ return this.selected_item.find("span").first().after("<abbr class=\"search-choice-close\"></abbr>");
+ }
+ };
+
+ Chosen.prototype.winnow_results = function() {
+ var found, option, part, parts, regex, regexAnchor, result, result_id, results, searchText, startpos, text, zregex, _i, _j, _len, _len1, _ref;
+ this.no_results_clear();
+ results = 0;
+ searchText = this.search_field.val() === this.default_text ? "" : $('<div/>').text($.trim(this.search_field.val())).html();
+ regexAnchor = this.search_contains ? "" : "^";
+ regex = new RegExp(regexAnchor + searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), 'i');
+ zregex = new RegExp(searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), 'i');
+ _ref = this.results_data;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ option = _ref[_i];
+ if (!option.disabled && !option.empty) {
+ if (option.group) {
+ $('#' + option.dom_id).css('display', 'none');
+ } else if (!(this.is_multiple && option.selected)) {
+ found = false;
+ result_id = option.dom_id;
+ result = $("#" + result_id);
+ if (regex.test(option.html)) {
+ found = true;
+ results += 1;
+ } else if (this.enable_split_word_search && (option.html.indexOf(" ") >= 0 || option.html.indexOf("[") === 0)) {
+ parts = option.html.replace(/\[|\]/g, "").split(" ");
+ if (parts.length) {
+ for (_j = 0, _len1 = parts.length; _j < _len1; _j++) {
+ part = parts[_j];
+ if (regex.test(part)) {
+ found = true;
+ results += 1;
+ }
+ }
+ }
+ }
+ if (found) {
+ if (searchText.length) {
+ startpos = option.html.search(zregex);
+ text = option.html.substr(0, startpos + searchText.length) + '</em>' + option.html.substr(startpos + searchText.length);
+ text = text.substr(0, startpos) + '<em>' + text.substr(startpos);
+ } else {
+ text = option.html;
+ }
+ result.html(text);
+ this.result_activate(result);
+ if (option.group_array_index != null) {
+ $("#" + this.results_data[option.group_array_index].dom_id).css('display', 'list-item');
+ }
+ } else {
+ if (this.result_highlight && result_id === this.result_highlight.attr('id')) {
+ this.result_clear_highlight();
+ }
+ this.result_deactivate(result);
+ }
+ }
+ }
+ }
+ if (results < 1 && searchText.length) {
+ return this.no_results(searchText);
+ } else {
+ return this.winnow_results_set_highlight();
+ }
+ };
+
+ Chosen.prototype.winnow_results_clear = function() {
+ var li, lis, _i, _len, _results;
+ this.search_field.val("");
+ lis = this.search_results.find("li");
+ _results = [];
+ for (_i = 0, _len = lis.length; _i < _len; _i++) {
+ li = lis[_i];
+ li = $(li);
+ if (li.hasClass("group-result")) {
+ _results.push(li.css('display', 'auto'));
+ } else if (!this.is_multiple || !li.hasClass("result-selected")) {
+ _results.push(this.result_activate(li));
+ } else {
+ _results.push(void 0);
+ }
+ }
+ return _results;
+ };
+
+ Chosen.prototype.winnow_results_set_highlight = function() {
+ var do_high, selected_results;
+ if (!this.result_highlight) {
+ selected_results = !this.is_multiple ? this.search_results.find(".result-selected.active-result") : [];
+ do_high = selected_results.length ? selected_results.first() : this.search_results.find(".active-result").first();
+ if (do_high != null) {
+ return this.result_do_highlight(do_high);
+ }
+ }
+ };
+
+ Chosen.prototype.no_results = function(terms) {
+ var no_results_html;
+ no_results_html = $('<li class="no-results">' + this.results_none_found + ' "<span></span>"</li>');
+ no_results_html.find("span").first().html(terms);
+ return this.search_results.append(no_results_html);
+ };
+
+ Chosen.prototype.no_results_clear = function() {
+ return this.search_results.find(".no-results").remove();
+ };
+
+ Chosen.prototype.keydown_arrow = function() {
+ var first_active, next_sib;
+ if (!this.result_highlight) {
+ first_active = this.search_results.find("li.active-result").first();
+ if (first_active) {
+ this.result_do_highlight($(first_active));
+ }
+ } else if (this.results_showing) {
+ next_sib = this.result_highlight.nextAll("li.active-result").first();
+ if (next_sib) {
+ this.result_do_highlight(next_sib);
+ }
+ }
+ if (!this.results_showing) {
+ return this.results_show();
+ }
+ };
+
+ Chosen.prototype.keyup_arrow = function() {
+ var prev_sibs;
+ if (!this.results_showing && !this.is_multiple) {
+ return this.results_show();
+ } else if (this.result_highlight) {
+ prev_sibs = this.result_highlight.prevAll("li.active-result");
+ if (prev_sibs.length) {
+ return this.result_do_highlight(prev_sibs.first());
+ } else {
+ if (this.choices > 0) {
+ this.results_hide();
+ }
+ return this.result_clear_highlight();
+ }
+ }
+ };
+
+ Chosen.prototype.keydown_backstroke = function() {
+ var next_available_destroy;
+ if (this.pending_backstroke) {
+ this.choice_destroy(this.pending_backstroke.find("a").first());
+ return this.clear_backstroke();
+ } else {
+ next_available_destroy = this.search_container.siblings("li.search-choice").last();
+ if (next_available_destroy.length && !next_available_destroy.hasClass("search-choice-disabled")) {
+ this.pending_backstroke = next_available_destroy;
+ if (this.single_backstroke_delete) {
+ return this.keydown_backstroke();
+ } else {
+ return this.pending_backstroke.addClass("search-choice-focus");
+ }
+ }
+ }
+ };
+
+ Chosen.prototype.clear_backstroke = function() {
+ if (this.pending_backstroke) {
+ this.pending_backstroke.removeClass("search-choice-focus");
+ }
+ return this.pending_backstroke = null;
+ };
+
+ Chosen.prototype.keydown_checker = function(evt) {
+ var stroke, _ref;
+ stroke = (_ref = evt.which) != null ? _ref : evt.keyCode;
+ this.search_field_scale();
+ if (stroke !== 8 && this.pending_backstroke) {
+ this.clear_backstroke();
+ }
+ switch (stroke) {
+ case 8:
+ this.backstroke_length = this.search_field.val().length;
+ break;
+ case 9:
+ if (this.results_showing && !this.is_multiple) {
+ this.result_select(evt);
+ }
+ this.mouse_on_container = false;
+ break;
+ case 13:
+ evt.preventDefault();
+ break;
+ case 38:
+ evt.preventDefault();
+ this.keyup_arrow();
+ break;
+ case 40:
+ this.keydown_arrow();
+ break;
+ }
+ };
+
+ Chosen.prototype.search_field_scale = function() {
+ var div, h, style, style_block, styles, w, _i, _len;
+ if (this.is_multiple) {
+ h = 0;
+ w = 0;
+ style_block = "position:absolute; left: -1000px; top: -1000px; display:none;";
+ styles = ['font-size', 'font-style', 'font-weight', 'font-family', 'line-height', 'text-transform', 'letter-spacing'];
+ for (_i = 0, _len = styles.length; _i < _len; _i++) {
+ style = styles[_i];
+ style_block += style + ":" + this.search_field.css(style) + ";";
+ }
+ div = $('<div />', {
+ 'style': style_block
+ });
+ div.text(this.search_field.val());
+ $('body').append(div);
+ w = div.width() + 25;
+ div.remove();
+ if (!this.f_width) {
+ this.f_width = this.container.outerWidth();
+ }
+ if (w > this.f_width - 10) {
+ w = this.f_width - 10;
+ }
+ return this.search_field.css({
+ 'width': w + 'px'
+ });
+ }
+ };
+
+ Chosen.prototype.generate_random_id = function() {
+ var string;
+ string = "sel" + this.generate_random_char() + this.generate_random_char() + this.generate_random_char();
+ while ($("#" + string).length > 0) {
+ string += this.generate_random_char();
+ }
+ return string;
+ };
+
+ return Chosen;
+
+ })(AbstractChosen);
+
+ root.Chosen = Chosen;
+
+}).call(this);
diff --git a/resources/jquery.effects/jquery.effects.blind.js b/resources/jquery.effects/jquery.effects.blind.js
index 1e997690..ac25bbd8 100644
--- a/resources/jquery.effects/jquery.effects.blind.js
+++ b/resources/jquery.effects/jquery.effects.blind.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects Blind 1.8.23
+ * jQuery UI Effects Blind 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.effects/jquery.effects.bounce.js b/resources/jquery.effects/jquery.effects.bounce.js
index 7927a4a9..1169d770 100644
--- a/resources/jquery.effects/jquery.effects.bounce.js
+++ b/resources/jquery.effects/jquery.effects.bounce.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects Bounce 1.8.23
+ * jQuery UI Effects Bounce 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.effects/jquery.effects.clip.js b/resources/jquery.effects/jquery.effects.clip.js
index d8b8218f..edd51a6a 100644
--- a/resources/jquery.effects/jquery.effects.clip.js
+++ b/resources/jquery.effects/jquery.effects.clip.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects Clip 1.8.23
+ * jQuery UI Effects Clip 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.effects/jquery.effects.core.js b/resources/jquery.effects/jquery.effects.core.js
index 91ac5755..7fd946fd 100644
--- a/resources/jquery.effects/jquery.effects.core.js
+++ b/resources/jquery.effects/jquery.effects.core.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects 1.8.23
+ * jQuery UI Effects 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -307,7 +307,7 @@ $.fn.extend({
/******************************************************************************/
$.extend($.effects, {
- version: "1.8.23",
+ version: "1.8.24",
// Saves a set of properties in a data storage
save: function(element, set) {
diff --git a/resources/jquery.effects/jquery.effects.drop.js b/resources/jquery.effects/jquery.effects.drop.js
index 6d25bd30..97e5abd4 100644
--- a/resources/jquery.effects/jquery.effects.drop.js
+++ b/resources/jquery.effects/jquery.effects.drop.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects Drop 1.8.23
+ * jQuery UI Effects Drop 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.effects/jquery.effects.explode.js b/resources/jquery.effects/jquery.effects.explode.js
index 1caeca86..f63e47a2 100644
--- a/resources/jquery.effects/jquery.effects.explode.js
+++ b/resources/jquery.effects/jquery.effects.explode.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects Explode 1.8.23
+ * jQuery UI Effects Explode 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.effects/jquery.effects.fade.js b/resources/jquery.effects/jquery.effects.fade.js
index 61249798..7aa37b1a 100644
--- a/resources/jquery.effects/jquery.effects.fade.js
+++ b/resources/jquery.effects/jquery.effects.fade.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects Fade 1.8.23
+ * jQuery UI Effects Fade 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.effects/jquery.effects.fold.js b/resources/jquery.effects/jquery.effects.fold.js
index 81b15b83..06cc5533 100644
--- a/resources/jquery.effects/jquery.effects.fold.js
+++ b/resources/jquery.effects/jquery.effects.fold.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects Fold 1.8.23
+ * jQuery UI Effects Fold 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.effects/jquery.effects.highlight.js b/resources/jquery.effects/jquery.effects.highlight.js
index dee0639f..ad9e7bd4 100644
--- a/resources/jquery.effects/jquery.effects.highlight.js
+++ b/resources/jquery.effects/jquery.effects.highlight.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects Highlight 1.8.23
+ * jQuery UI Effects Highlight 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.effects/jquery.effects.pulsate.js b/resources/jquery.effects/jquery.effects.pulsate.js
index 45cdc884..d730beed 100644
--- a/resources/jquery.effects/jquery.effects.pulsate.js
+++ b/resources/jquery.effects/jquery.effects.pulsate.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects Pulsate 1.8.23
+ * jQuery UI Effects Pulsate 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.effects/jquery.effects.scale.js b/resources/jquery.effects/jquery.effects.scale.js
index 44ecee18..52d18710 100644
--- a/resources/jquery.effects/jquery.effects.scale.js
+++ b/resources/jquery.effects/jquery.effects.scale.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects Scale 1.8.23
+ * jQuery UI Effects Scale 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.effects/jquery.effects.shake.js b/resources/jquery.effects/jquery.effects.shake.js
index bc1fd191..44b8ea44 100644
--- a/resources/jquery.effects/jquery.effects.shake.js
+++ b/resources/jquery.effects/jquery.effects.shake.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects Shake 1.8.23
+ * jQuery UI Effects Shake 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.effects/jquery.effects.slide.js b/resources/jquery.effects/jquery.effects.slide.js
index 0a430278..502e6c9d 100644
--- a/resources/jquery.effects/jquery.effects.slide.js
+++ b/resources/jquery.effects/jquery.effects.slide.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects Slide 1.8.23
+ * jQuery UI Effects Slide 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.effects/jquery.effects.transfer.js b/resources/jquery.effects/jquery.effects.transfer.js
index 64f2a17b..4ee4ae88 100644
--- a/resources/jquery.effects/jquery.effects.transfer.js
+++ b/resources/jquery.effects/jquery.effects.transfer.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Effects Transfer 1.8.23
+ * jQuery UI Effects Transfer 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.tipsy/images/tipsy.png b/resources/jquery.tipsy/images/tipsy.png
index fef8c4b5..ef17cc32 100644
--- a/resources/jquery.tipsy/images/tipsy.png
+++ b/resources/jquery.tipsy/images/tipsy.png
Binary files differ
diff --git a/resources/jquery.ui/i18n/jquery.ui.datepicker-zh-CN.js b/resources/jquery.ui/i18n/jquery.ui.datepicker-zh-CN.js
index 6c4883f5..83f2825c 100644
--- a/resources/jquery.ui/i18n/jquery.ui.datepicker-zh-CN.js
+++ b/resources/jquery.ui/i18n/jquery.ui.datepicker-zh-CN.js
@@ -8,8 +8,8 @@ jQuery(function($){
currentText: '今天',
monthNames: ['一月','二月','三月','四月','五月','六月',
'七月','八月','九月','十月','十一月','十二月'],
- monthNamesShort: ['一','二','三','四','五','六',
- '七','八','九','十','十一','十二'],
+ monthNamesShort: ['一月','二月','三月','四月','五月','六月',
+ '七月','八月','九月','十月','十一月','十二月'],
dayNames: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'],
dayNamesShort: ['周日','周一','周二','周三','周四','周五','周六'],
dayNamesMin: ['日','一','二','三','四','五','六'],
diff --git a/resources/jquery.ui/i18n/jquery.ui.datepicker-zh-HK.js b/resources/jquery.ui/i18n/jquery.ui.datepicker-zh-HK.js
index 06c4c628..11189d3a 100644
--- a/resources/jquery.ui/i18n/jquery.ui.datepicker-zh-HK.js
+++ b/resources/jquery.ui/i18n/jquery.ui.datepicker-zh-HK.js
@@ -8,8 +8,8 @@ jQuery(function($){
currentText: '今天',
monthNames: ['一月','二月','三月','四月','五月','六月',
'七月','八月','九月','十月','十一月','十二月'],
- monthNamesShort: ['一','二','三','四','五','六',
- '七','八','九','十','十一','十二'],
+ monthNamesShort: ['一月','二月','三月','四月','五月','六月',
+ '七月','八月','九月','十月','十一月','十二月'],
dayNames: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'],
dayNamesShort: ['周日','周一','周二','周三','周四','周五','周六'],
dayNamesMin: ['日','一','二','三','四','五','六'],
diff --git a/resources/jquery.ui/i18n/jquery.ui.datepicker-zh-TW.js b/resources/jquery.ui/i18n/jquery.ui.datepicker-zh-TW.js
index dd51e359..089498b4 100644
--- a/resources/jquery.ui/i18n/jquery.ui.datepicker-zh-TW.js
+++ b/resources/jquery.ui/i18n/jquery.ui.datepicker-zh-TW.js
@@ -8,8 +8,8 @@ jQuery(function($){
currentText: '今天',
monthNames: ['一月','二月','三月','四月','五月','六月',
'七月','八月','九月','十月','十一月','十二月'],
- monthNamesShort: ['一','二','三','四','五','六',
- '七','八','九','十','十一','十二'],
+ monthNamesShort: ['一月','二月','三月','四月','五月','六月',
+ '七月','八月','九月','十月','十一月','十二月'],
dayNames: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'],
dayNamesShort: ['周日','周一','周二','周三','周四','周五','周六'],
dayNamesMin: ['日','一','二','三','四','五','六'],
diff --git a/resources/jquery.ui/jquery.ui.accordion.js b/resources/jquery.ui/jquery.ui.accordion.js
index b3340e09..dc1ba60a 100644
--- a/resources/jquery.ui/jquery.ui.accordion.js
+++ b/resources/jquery.ui/jquery.ui.accordion.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Accordion 1.8.23
+ * jQuery UI Accordion 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -518,7 +518,7 @@ $.widget( "ui.accordion", {
});
$.extend( $.ui.accordion, {
- version: "1.8.23",
+ version: "1.8.24",
animations: {
slide: function( options, additions ) {
options = $.extend({
diff --git a/resources/jquery.ui/jquery.ui.autocomplete.js b/resources/jquery.ui/jquery.ui.autocomplete.js
index b634cce5..8d69be28 100644
--- a/resources/jquery.ui/jquery.ui.autocomplete.js
+++ b/resources/jquery.ui/jquery.ui.autocomplete.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Autocomplete 1.8.23
+ * jQuery UI Autocomplete 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/jquery.ui.button.js b/resources/jquery.ui/jquery.ui.button.js
index db2637e8..8326262c 100644
--- a/resources/jquery.ui/jquery.ui.button.js
+++ b/resources/jquery.ui/jquery.ui.button.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Button 1.8.23
+ * jQuery UI Button 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/jquery.ui.core.js b/resources/jquery.ui/jquery.ui.core.js
index 1285a6dd..b36c1ac4 100644
--- a/resources/jquery.ui/jquery.ui.core.js
+++ b/resources/jquery.ui/jquery.ui.core.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI 1.8.23
+ * jQuery UI 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -18,7 +18,7 @@ if ( $.ui.version ) {
}
$.extend( $.ui, {
- version: "1.8.23",
+ version: "1.8.24",
keyCode: {
ALT: 18,
diff --git a/resources/jquery.ui/jquery.ui.datepicker.js b/resources/jquery.ui/jquery.ui.datepicker.js
index 7ea5b079..1fcea12a 100644
--- a/resources/jquery.ui/jquery.ui.datepicker.js
+++ b/resources/jquery.ui/jquery.ui.datepicker.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Datepicker 1.8.23
+ * jQuery UI Datepicker 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -12,7 +12,7 @@
*/
(function( $, undefined ) {
-$.extend($.ui, { datepicker: { version: "1.8.23" } });
+$.extend($.ui, { datepicker: { version: "1.8.24" } });
var PROP_NAME = 'datepicker';
var dpuuid = new Date().getTime();
@@ -1845,7 +1845,7 @@ $.fn.datepicker = function(options){
$.datepicker = new Datepicker(); // singleton instance
$.datepicker.initialized = false;
$.datepicker.uuid = new Date().getTime();
-$.datepicker.version = "1.8.23";
+$.datepicker.version = "1.8.24";
// Workaround for #4055
// Add another global to avoid noConflict issues with inline event handlers
diff --git a/resources/jquery.ui/jquery.ui.dialog.js b/resources/jquery.ui/jquery.ui.dialog.js
index 082bf2c0..06b85f23 100644
--- a/resources/jquery.ui/jquery.ui.dialog.js
+++ b/resources/jquery.ui/jquery.ui.dialog.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Dialog 1.8.23
+ * jQuery UI Dialog 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -690,7 +690,7 @@ $.widget("ui.dialog", {
});
$.extend($.ui.dialog, {
- version: "1.8.23",
+ version: "1.8.24",
uuid: 0,
maxZ: 0,
diff --git a/resources/jquery.ui/jquery.ui.draggable.js b/resources/jquery.ui/jquery.ui.draggable.js
index 6da1aafe..149035c2 100644
--- a/resources/jquery.ui/jquery.ui.draggable.js
+++ b/resources/jquery.ui/jquery.ui.draggable.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Draggable 1.8.23
+ * jQuery UI Draggable 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -237,11 +237,10 @@ $.widget("ui.draggable", $.ui.mouse, {
},
_mouseUp: function(event) {
- if (this.options.iframeFix === true) {
- $("div.ui-draggable-iframeFix").each(function() {
- this.parentNode.removeChild(this);
- }); //Remove frame helpers
- }
+ //Remove frame helpers
+ $("div.ui-draggable-iframeFix").each(function() {
+ this.parentNode.removeChild(this);
+ });
//If the ddmanager is used for droppables, inform the manager that dragging has stopped (see #5003)
if( $.ui.ddmanager ) $.ui.ddmanager.dragStop(this, event);
@@ -513,7 +512,7 @@ $.widget("ui.draggable", $.ui.mouse, {
});
$.extend($.ui.draggable, {
- version: "1.8.23"
+ version: "1.8.24"
});
$.ui.plugin.add("draggable", "connectToSortable", {
diff --git a/resources/jquery.ui/jquery.ui.droppable.js b/resources/jquery.ui/jquery.ui.droppable.js
index 4b98b3ad..f17c2223 100644
--- a/resources/jquery.ui/jquery.ui.droppable.js
+++ b/resources/jquery.ui/jquery.ui.droppable.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Droppable 1.8.23
+ * jQuery UI Droppable 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -147,7 +147,7 @@ $.widget("ui.droppable", {
});
$.extend($.ui.droppable, {
- version: "1.8.23"
+ version: "1.8.24"
});
$.ui.intersect = function(draggable, droppable, toleranceMode) {
@@ -260,7 +260,12 @@ $.ui.ddmanager = {
var parentInstance;
if (this.options.greedy) {
- var parent = this.element.parents(':data(droppable):eq(0)');
+ // find droppable parents with same scope
+ var scope = this.options.scope;
+ var parent = this.element.parents(':data(droppable)').filter(function () {
+ return $.data(this, 'droppable').options.scope === scope;
+ });
+
if (parent.length) {
parentInstance = $.data(parent[0], 'droppable');
parentInstance.greedyChild = (c == 'isover' ? 1 : 0);
diff --git a/resources/jquery.ui/jquery.ui.mouse.js b/resources/jquery.ui/jquery.ui.mouse.js
index e051055d..52a1786c 100644
--- a/resources/jquery.ui/jquery.ui.mouse.js
+++ b/resources/jquery.ui/jquery.ui.mouse.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Mouse 1.8.23
+ * jQuery UI Mouse 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/jquery.ui.position.js b/resources/jquery.ui/jquery.ui.position.js
index 03f2606c..8b20179b 100644
--- a/resources/jquery.ui/jquery.ui.position.js
+++ b/resources/jquery.ui/jquery.ui.position.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Position 1.8.23
+ * jQuery UI Position 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/jquery.ui.progressbar.js b/resources/jquery.ui/jquery.ui.progressbar.js
index c1d9f3c2..7cea1baa 100644
--- a/resources/jquery.ui/jquery.ui.progressbar.js
+++ b/resources/jquery.ui/jquery.ui.progressbar.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Progressbar 1.8.23
+ * jQuery UI Progressbar 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -103,7 +103,7 @@ $.widget( "ui.progressbar", {
});
$.extend( $.ui.progressbar, {
- version: "1.8.23"
+ version: "1.8.24"
});
})( jQuery );
diff --git a/resources/jquery.ui/jquery.ui.resizable.js b/resources/jquery.ui/jquery.ui.resizable.js
index f6ce6949..6cc6f41f 100644
--- a/resources/jquery.ui/jquery.ui.resizable.js
+++ b/resources/jquery.ui/jquery.ui.resizable.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Resizable 1.8.23
+ * jQuery UI Resizable 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -540,7 +540,7 @@ $.widget("ui.resizable", $.ui.mouse, {
});
$.extend($.ui.resizable, {
- version: "1.8.23"
+ version: "1.8.24"
});
/*
diff --git a/resources/jquery.ui/jquery.ui.selectable.js b/resources/jquery.ui/jquery.ui.selectable.js
index ac5bf046..44c6e8c5 100644
--- a/resources/jquery.ui/jquery.ui.selectable.js
+++ b/resources/jquery.ui/jquery.ui.selectable.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Selectable 1.8.23
+ * jQuery UI Selectable 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -261,7 +261,7 @@ $.widget("ui.selectable", $.ui.mouse, {
});
$.extend($.ui.selectable, {
- version: "1.8.23"
+ version: "1.8.24"
});
})(jQuery);
diff --git a/resources/jquery.ui/jquery.ui.slider.js b/resources/jquery.ui/jquery.ui.slider.js
index 5ea589e6..c554e783 100644
--- a/resources/jquery.ui/jquery.ui.slider.js
+++ b/resources/jquery.ui/jquery.ui.slider.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Slider 1.8.23
+ * jQuery UI Slider 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -656,7 +656,7 @@ $.widget( "ui.slider", $.ui.mouse, {
});
$.extend( $.ui.slider, {
- version: "1.8.23"
+ version: "1.8.24"
});
}(jQuery));
diff --git a/resources/jquery.ui/jquery.ui.sortable.js b/resources/jquery.ui/jquery.ui.sortable.js
index 1d87f653..9e0cac68 100644
--- a/resources/jquery.ui/jquery.ui.sortable.js
+++ b/resources/jquery.ui/jquery.ui.sortable.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Sortable 1.8.23
+ * jQuery UI Sortable 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -296,7 +296,16 @@ $.widget("ui.sortable", $.ui.mouse, {
var item = this.items[i], itemElement = item.item[0], intersection = this._intersectsWithPointer(item);
if (!intersection) continue;
- if(itemElement != this.currentItem[0] //cannot intersect with itself
+ // Only put the placeholder inside the current Container, skip all
+ // items form other containers. This works because when moving
+ // an item from one container to another the
+ // currentContainer is switched before the placeholder is moved.
+ //
+ // Without this moving items in "sub-sortables" can cause the placeholder to jitter
+ // beetween the outer and inner container.
+ if (item.instance !== this.currentContainer) continue;
+
+ if (itemElement != this.currentItem[0] //cannot intersect with itself
&& this.placeholder[intersection == 1 ? "next" : "prev"]()[0] != itemElement //no useless actions that have been done before
&& !$.ui.contains(this.placeholder[0], itemElement) //no action if the item moved is the parent of the item checked
&& (this.options.type == 'semi-dynamic' ? !$.ui.contains(this.element[0], itemElement) : true)
@@ -1003,15 +1012,16 @@ $.widget("ui.sortable", $.ui.mouse, {
if(this.fromOutside && !noPropagation) delayedTriggers.push(function(event) { this._trigger("receive", event, this._uiHash(this.fromOutside)); });
if((this.fromOutside || this.domPosition.prev != this.currentItem.prev().not(".ui-sortable-helper")[0] || this.domPosition.parent != this.currentItem.parent()[0]) && !noPropagation) delayedTriggers.push(function(event) { this._trigger("update", event, this._uiHash()); }); //Trigger update callback if the DOM position has changed
- if(!$.ui.contains(this.element[0], this.currentItem[0])) { //Node was moved out of the current element
- if(!noPropagation) delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); });
- for (var i = this.containers.length - 1; i >= 0; i--){
- if($.ui.contains(this.containers[i].element[0], this.currentItem[0]) && !noPropagation) {
- delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); }; }).call(this, this.containers[i]));
- delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this)); }; }).call(this, this.containers[i]));
- }
- };
- };
+
+ // Check if the items Container has Changed and trigger appropriate
+ // events.
+ if (this !== this.currentContainer) {
+ if(!noPropagation) {
+ delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); });
+ delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); }; }).call(this, this.currentContainer));
+ delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this)); }; }).call(this, this.currentContainer));
+ }
+ }
//Post events to containers
for (var i = this.containers.length - 1; i >= 0; i--){
@@ -1078,7 +1088,7 @@ $.widget("ui.sortable", $.ui.mouse, {
});
$.extend($.ui.sortable, {
- version: "1.8.23"
+ version: "1.8.24"
});
})(jQuery);
diff --git a/resources/jquery.ui/jquery.ui.tabs.js b/resources/jquery.ui/jquery.ui.tabs.js
index de453cc3..0c47f0e1 100644
--- a/resources/jquery.ui/jquery.ui.tabs.js
+++ b/resources/jquery.ui/jquery.ui.tabs.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Tabs 1.8.23
+ * jQuery UI Tabs 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -698,7 +698,7 @@ $.widget( "ui.tabs", {
});
$.extend( $.ui.tabs, {
- version: "1.8.23"
+ version: "1.8.24"
});
/*
diff --git a/resources/jquery.ui/jquery.ui.widget.js b/resources/jquery.ui/jquery.ui.widget.js
index befdcc25..66ef0133 100644
--- a/resources/jquery.ui/jquery.ui.widget.js
+++ b/resources/jquery.ui/jquery.ui.widget.js
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Widget 1.8.23
+ * jQuery UI Widget 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/themes/default/images/ui-bg_flat_0_aaaaaa_40x100.png b/resources/jquery.ui/themes/default/images/ui-bg_flat_0_aaaaaa_40x100.png
index 5b5dab2a..e425e6e4 100644
--- a/resources/jquery.ui/themes/default/images/ui-bg_flat_0_aaaaaa_40x100.png
+++ b/resources/jquery.ui/themes/default/images/ui-bg_flat_0_aaaaaa_40x100.png
Binary files differ
diff --git a/resources/jquery.ui/themes/default/images/ui-bg_flat_75_ffffff_40x100.png b/resources/jquery.ui/themes/default/images/ui-bg_flat_75_ffffff_40x100.png
index ac8b229a..72d47573 100644
--- a/resources/jquery.ui/themes/default/images/ui-bg_flat_75_ffffff_40x100.png
+++ b/resources/jquery.ui/themes/default/images/ui-bg_flat_75_ffffff_40x100.png
Binary files differ
diff --git a/resources/jquery.ui/themes/default/images/ui-bg_glass_55_fbf9ee_1x400.png b/resources/jquery.ui/themes/default/images/ui-bg_glass_55_fbf9ee_1x400.png
index ad3d6346..3b2914a2 100644
--- a/resources/jquery.ui/themes/default/images/ui-bg_glass_55_fbf9ee_1x400.png
+++ b/resources/jquery.ui/themes/default/images/ui-bg_glass_55_fbf9ee_1x400.png
Binary files differ
diff --git a/resources/jquery.ui/themes/default/images/ui-bg_glass_65_ffffff_1x400.png b/resources/jquery.ui/themes/default/images/ui-bg_glass_65_ffffff_1x400.png
index 42ccba26..8569c1bc 100644
--- a/resources/jquery.ui/themes/default/images/ui-bg_glass_65_ffffff_1x400.png
+++ b/resources/jquery.ui/themes/default/images/ui-bg_glass_65_ffffff_1x400.png
Binary files differ
diff --git a/resources/jquery.ui/themes/default/images/ui-bg_glass_75_dadada_1x400.png b/resources/jquery.ui/themes/default/images/ui-bg_glass_75_dadada_1x400.png
index 5a46b47c..d6cc3c58 100644
--- a/resources/jquery.ui/themes/default/images/ui-bg_glass_75_dadada_1x400.png
+++ b/resources/jquery.ui/themes/default/images/ui-bg_glass_75_dadada_1x400.png
Binary files differ
diff --git a/resources/jquery.ui/themes/default/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/resources/jquery.ui/themes/default/images/ui-bg_highlight-soft_75_cccccc_1x100.png
index 7c9fa6c6..3cd467e1 100644
--- a/resources/jquery.ui/themes/default/images/ui-bg_highlight-soft_75_cccccc_1x100.png
+++ b/resources/jquery.ui/themes/default/images/ui-bg_highlight-soft_75_cccccc_1x100.png
Binary files differ
diff --git a/resources/jquery.ui/themes/default/images/ui-icons_222222_256x240.png b/resources/jquery.ui/themes/default/images/ui-icons_222222_256x240.png
index ee039dc0..9a9606f7 100644
--- a/resources/jquery.ui/themes/default/images/ui-icons_222222_256x240.png
+++ b/resources/jquery.ui/themes/default/images/ui-icons_222222_256x240.png
Binary files differ
diff --git a/resources/jquery.ui/themes/default/images/ui-icons_2e83ff_256x240.png b/resources/jquery.ui/themes/default/images/ui-icons_2e83ff_256x240.png
index 45e8928e..08d26179 100644
--- a/resources/jquery.ui/themes/default/images/ui-icons_2e83ff_256x240.png
+++ b/resources/jquery.ui/themes/default/images/ui-icons_2e83ff_256x240.png
Binary files differ
diff --git a/resources/jquery.ui/themes/default/images/ui-icons_454545_256x240.png b/resources/jquery.ui/themes/default/images/ui-icons_454545_256x240.png
index 7ec70d11..80cb644a 100644
--- a/resources/jquery.ui/themes/default/images/ui-icons_454545_256x240.png
+++ b/resources/jquery.ui/themes/default/images/ui-icons_454545_256x240.png
Binary files differ
diff --git a/resources/jquery.ui/themes/default/images/ui-icons_888888_256x240.png b/resources/jquery.ui/themes/default/images/ui-icons_888888_256x240.png
index 5ba708c3..8373712d 100644
--- a/resources/jquery.ui/themes/default/images/ui-icons_888888_256x240.png
+++ b/resources/jquery.ui/themes/default/images/ui-icons_888888_256x240.png
Binary files differ
diff --git a/resources/jquery.ui/themes/default/images/ui-icons_cd0a0a_256x240.png b/resources/jquery.ui/themes/default/images/ui-icons_cd0a0a_256x240.png
index 7930a558..34fc8937 100644
--- a/resources/jquery.ui/themes/default/images/ui-icons_cd0a0a_256x240.png
+++ b/resources/jquery.ui/themes/default/images/ui-icons_cd0a0a_256x240.png
Binary files differ
diff --git a/resources/jquery.ui/themes/default/jquery.ui.accordion.css b/resources/jquery.ui/themes/default/jquery.ui.accordion.css
index 1ce7d5ea..cd8f971c 100644
--- a/resources/jquery.ui/themes/default/jquery.ui.accordion.css
+++ b/resources/jquery.ui/themes/default/jquery.ui.accordion.css
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Accordion 1.8.23
+ * jQuery UI Accordion 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/themes/default/jquery.ui.autocomplete.css b/resources/jquery.ui/themes/default/jquery.ui.autocomplete.css
index a9817ceb..c7eaff22 100644
--- a/resources/jquery.ui/themes/default/jquery.ui.autocomplete.css
+++ b/resources/jquery.ui/themes/default/jquery.ui.autocomplete.css
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Autocomplete 1.8.23
+ * jQuery UI Autocomplete 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -13,7 +13,7 @@
* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
/*
- * jQuery UI Menu 1.8.23
+ * jQuery UI Menu 1.8.24
*
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/themes/default/jquery.ui.button.css b/resources/jquery.ui/themes/default/jquery.ui.button.css
index c1f26009..cd2dbb6e 100644
--- a/resources/jquery.ui/themes/default/jquery.ui.button.css
+++ b/resources/jquery.ui/themes/default/jquery.ui.button.css
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Button 1.8.23
+ * jQuery UI Button 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
@@ -7,32 +7,104 @@
*
* http://docs.jquery.com/UI/Button#theming
*/
-.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
-.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
-button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
-.ui-button-icons-only { width: 3.4em; }
-button.ui-button-icons-only { width: 3.7em; }
+
+.ui-button {
+ display: inline-block;
+ position: relative;
+ padding: 0;
+ margin-right: .1em;
+ text-decoration: none !important;
+ cursor: pointer;
+ text-align: center;
+ zoom: 1;
+ overflow: visible; /* the overflow property removes extra width in IE */
+}
+/* to make room for the icon, a width needs to be set here */
+.ui-button-icon-only {
+ width: 2.2em;
+}
+
+/* button elements seem to need a little more width */
+button.ui-button-icon-only {
+ width: 2.4em;
+}
+.ui-button-icons-only {
+ width: 3.4em;
+}
+button.ui-button-icons-only {
+ width: 3.7em;
+}
/*button text element */
-.ui-button .ui-button-text { display: block; line-height: 1.4; }
-.ui-button-text-only .ui-button-text { padding: .4em 1em; }
-.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
-.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; }
-.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; }
-.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
+.ui-button .ui-button-text {
+ display: block;
+ line-height: 1.4;
+}
+.ui-button-text-only .ui-button-text {
+ padding: .4em 1em;
+}
+.ui-button-icon-only .ui-button-text,
+.ui-button-icons-only .ui-button-text {
+ padding: .4em;
+ text-indent: -9999999px;
+}
+.ui-button-text-icon-primary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+ padding: .4em 1em .4em 2.1em;
+}
+.ui-button-text-icon-secondary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+ padding: .4em 2.1em .4em 1em;
+}
+.ui-button-text-icons .ui-button-text {
+ padding-left: 2.1em;
+ padding-right: 2.1em;
+}
/* no icon support for input elements, provide padding by default */
-input.ui-button { padding: .4em 1em; }
+ input.ui-button {
+ padding: .4em 1em;
+}
/*button icon element(s) */
-.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; }
-.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
-.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; }
-.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
-.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+.ui-button-icon-only .ui-icon,
+.ui-button-text-icon-primary .ui-icon,
+.ui-button-text-icon-secondary .ui-icon,
+.ui-button-text-icons .ui-icon,
+.ui-button-icons-only .ui-icon {
+ position: absolute;
+ top: 50%;
+ margin-top: -8px;
+}
+.ui-button-icon-only .ui-icon {
+ left: 50%;
+ margin-left: -8px;
+}
+.ui-button-text-icon-primary .ui-button-icon-primary,
+.ui-button-text-icons .ui-button-icon-primary,
+.ui-button-icons-only .ui-button-icon-primary {
+ left: .5em;
+}
+.ui-button-text-icon-secondary .ui-button-icon-secondary,
+.ui-button-text-icons .ui-button-icon-secondary,
+.ui-button-icons-only .ui-button-icon-secondary {
+ right: .5em;
+}
+.ui-button-text-icons .ui-button-icon-secondary,
+.ui-button-icons-only .ui-button-icon-secondary {
+ right: .5em;
+}
/*button sets*/
-.ui-buttonset { margin-right: 7px; }
-.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
+.ui-buttonset {
+ margin-right: 7px;
+}
+.ui-buttonset .ui-button {
+ margin-left: 0;
+ margin-right: -.3em;
+}
/* workarounds */
-button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
+button.ui-button::-moz-focus-inner {
+ border: 0;
+ padding: 0; /* reset extra padding in Firefox */
+}
diff --git a/resources/jquery.ui/themes/default/jquery.ui.core.css b/resources/jquery.ui/themes/default/jquery.ui.core.css
index c24627e7..8b953a2b 100644
--- a/resources/jquery.ui/themes/default/jquery.ui.core.css
+++ b/resources/jquery.ui/themes/default/jquery.ui.core.css
@@ -1,5 +1,5 @@
/*!
- * jQuery UI CSS Framework 1.8.23
+ * jQuery UI CSS Framework 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/themes/default/jquery.ui.datepicker.css b/resources/jquery.ui/themes/default/jquery.ui.datepicker.css
index 0282eeee..37d3a98e 100644
--- a/resources/jquery.ui/themes/default/jquery.ui.datepicker.css
+++ b/resources/jquery.ui/themes/default/jquery.ui.datepicker.css
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Datepicker 1.8.23
+ * jQuery UI Datepicker 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/themes/default/jquery.ui.dialog.css b/resources/jquery.ui/themes/default/jquery.ui.dialog.css
index ba50ba5a..04515f48 100644
--- a/resources/jquery.ui/themes/default/jquery.ui.dialog.css
+++ b/resources/jquery.ui/themes/default/jquery.ui.dialog.css
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Dialog 1.8.23
+ * jQuery UI Dialog 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/themes/default/jquery.ui.progressbar.css b/resources/jquery.ui/themes/default/jquery.ui.progressbar.css
index c775a33a..90bf3081 100644
--- a/resources/jquery.ui/themes/default/jquery.ui.progressbar.css
+++ b/resources/jquery.ui/themes/default/jquery.ui.progressbar.css
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Progressbar 1.8.23
+ * jQuery UI Progressbar 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/themes/default/jquery.ui.resizable.css b/resources/jquery.ui/themes/default/jquery.ui.resizable.css
index 420c4afa..d17873e7 100644
--- a/resources/jquery.ui/themes/default/jquery.ui.resizable.css
+++ b/resources/jquery.ui/themes/default/jquery.ui.resizable.css
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Resizable 1.8.23
+ * jQuery UI Resizable 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/themes/default/jquery.ui.selectable.css b/resources/jquery.ui/themes/default/jquery.ui.selectable.css
index 33202748..9850ee7e 100644
--- a/resources/jquery.ui/themes/default/jquery.ui.selectable.css
+++ b/resources/jquery.ui/themes/default/jquery.ui.selectable.css
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Selectable 1.8.23
+ * jQuery UI Selectable 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/themes/default/jquery.ui.slider.css b/resources/jquery.ui/themes/default/jquery.ui.slider.css
index 650ad7e2..fbfe6658 100644
--- a/resources/jquery.ui/themes/default/jquery.ui.slider.css
+++ b/resources/jquery.ui/themes/default/jquery.ui.slider.css
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Slider 1.8.23
+ * jQuery UI Slider 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/themes/default/jquery.ui.tabs.css b/resources/jquery.ui/themes/default/jquery.ui.tabs.css
index 64ac9bf5..f0bee7a2 100644
--- a/resources/jquery.ui/themes/default/jquery.ui.tabs.css
+++ b/resources/jquery.ui/themes/default/jquery.ui.tabs.css
@@ -1,5 +1,5 @@
/*!
- * jQuery UI Tabs 1.8.23
+ * jQuery UI Tabs 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/themes/default/jquery.ui.theme.css b/resources/jquery.ui/themes/default/jquery.ui.theme.css
index 536c8e0e..b7d2f617 100644
--- a/resources/jquery.ui/themes/default/jquery.ui.theme.css
+++ b/resources/jquery.ui/themes/default/jquery.ui.theme.css
@@ -1,5 +1,5 @@
/*!
- * jQuery UI CSS Framework 1.8.23
+ * jQuery UI CSS Framework 1.8.24
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
diff --git a/resources/jquery.ui/themes/vector/images/button-blue-hover-large.png b/resources/jquery.ui/themes/vector/images/button-blue-hover-large.png
deleted file mode 100644
index 8f7cf74e..00000000
--- a/resources/jquery.ui/themes/vector/images/button-blue-hover-large.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-blue-hover.png b/resources/jquery.ui/themes/vector/images/button-blue-hover.png
deleted file mode 100644
index 1fc28163..00000000
--- a/resources/jquery.ui/themes/vector/images/button-blue-hover.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-blue-large.png b/resources/jquery.ui/themes/vector/images/button-blue-large.png
deleted file mode 100644
index 22ee5e59..00000000
--- a/resources/jquery.ui/themes/vector/images/button-blue-large.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-blue.png b/resources/jquery.ui/themes/vector/images/button-blue.png
deleted file mode 100644
index 2e6d121a..00000000
--- a/resources/jquery.ui/themes/vector/images/button-blue.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-disabled-blue.png b/resources/jquery.ui/themes/vector/images/button-disabled-blue.png
deleted file mode 100644
index 28eb1fcd..00000000
--- a/resources/jquery.ui/themes/vector/images/button-disabled-blue.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-disabled-green.png b/resources/jquery.ui/themes/vector/images/button-disabled-green.png
deleted file mode 100644
index 0e29d85b..00000000
--- a/resources/jquery.ui/themes/vector/images/button-disabled-green.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-disabled-red.png b/resources/jquery.ui/themes/vector/images/button-disabled-red.png
deleted file mode 100644
index ede69988..00000000
--- a/resources/jquery.ui/themes/vector/images/button-disabled-red.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-disabled.png b/resources/jquery.ui/themes/vector/images/button-disabled.png
deleted file mode 100644
index e4e4ec1c..00000000
--- a/resources/jquery.ui/themes/vector/images/button-disabled.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-down-blue.png b/resources/jquery.ui/themes/vector/images/button-down-blue.png
deleted file mode 100644
index 766e5746..00000000
--- a/resources/jquery.ui/themes/vector/images/button-down-blue.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-down-green.png b/resources/jquery.ui/themes/vector/images/button-down-green.png
deleted file mode 100644
index 90969c39..00000000
--- a/resources/jquery.ui/themes/vector/images/button-down-green.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-down-red.png b/resources/jquery.ui/themes/vector/images/button-down-red.png
deleted file mode 100644
index f6257298..00000000
--- a/resources/jquery.ui/themes/vector/images/button-down-red.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-down.png b/resources/jquery.ui/themes/vector/images/button-down.png
deleted file mode 100644
index c6467571..00000000
--- a/resources/jquery.ui/themes/vector/images/button-down.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-green-hover-large.png b/resources/jquery.ui/themes/vector/images/button-green-hover-large.png
deleted file mode 100644
index dd8851e1..00000000
--- a/resources/jquery.ui/themes/vector/images/button-green-hover-large.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-green-hover.png b/resources/jquery.ui/themes/vector/images/button-green-hover.png
deleted file mode 100644
index 19c39911..00000000
--- a/resources/jquery.ui/themes/vector/images/button-green-hover.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-green-large.png b/resources/jquery.ui/themes/vector/images/button-green-large.png
deleted file mode 100644
index a8e830eb..00000000
--- a/resources/jquery.ui/themes/vector/images/button-green-large.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-green.png b/resources/jquery.ui/themes/vector/images/button-green.png
deleted file mode 100644
index 54c418e9..00000000
--- a/resources/jquery.ui/themes/vector/images/button-green.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-large-disabled-green.png b/resources/jquery.ui/themes/vector/images/button-large-disabled-green.png
deleted file mode 100644
index f76f7b05..00000000
--- a/resources/jquery.ui/themes/vector/images/button-large-disabled-green.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-large-off-green.png b/resources/jquery.ui/themes/vector/images/button-large-off-green.png
deleted file mode 100644
index f997431b..00000000
--- a/resources/jquery.ui/themes/vector/images/button-large-off-green.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-off-blue.png b/resources/jquery.ui/themes/vector/images/button-off-blue.png
deleted file mode 100644
index 82dedb53..00000000
--- a/resources/jquery.ui/themes/vector/images/button-off-blue.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-off-green.png b/resources/jquery.ui/themes/vector/images/button-off-green.png
deleted file mode 100644
index 109907f0..00000000
--- a/resources/jquery.ui/themes/vector/images/button-off-green.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-off-red.png b/resources/jquery.ui/themes/vector/images/button-off-red.png
deleted file mode 100644
index 5a85b8aa..00000000
--- a/resources/jquery.ui/themes/vector/images/button-off-red.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-off.png b/resources/jquery.ui/themes/vector/images/button-off.png
deleted file mode 100644
index cc5eb119..00000000
--- a/resources/jquery.ui/themes/vector/images/button-off.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-orange-hover-large.png b/resources/jquery.ui/themes/vector/images/button-orange-hover-large.png
deleted file mode 100644
index 6f0dbd5d..00000000
--- a/resources/jquery.ui/themes/vector/images/button-orange-hover-large.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-orange-hover.png b/resources/jquery.ui/themes/vector/images/button-orange-hover.png
deleted file mode 100644
index a1077c58..00000000
--- a/resources/jquery.ui/themes/vector/images/button-orange-hover.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-orange-large.png b/resources/jquery.ui/themes/vector/images/button-orange-large.png
deleted file mode 100644
index 3d7f37f6..00000000
--- a/resources/jquery.ui/themes/vector/images/button-orange-large.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-orange.png b/resources/jquery.ui/themes/vector/images/button-orange.png
deleted file mode 100644
index 02347cf9..00000000
--- a/resources/jquery.ui/themes/vector/images/button-orange.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-over-blue.png b/resources/jquery.ui/themes/vector/images/button-over-blue.png
deleted file mode 100644
index 9edb7aa2..00000000
--- a/resources/jquery.ui/themes/vector/images/button-over-blue.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-over-green.png b/resources/jquery.ui/themes/vector/images/button-over-green.png
deleted file mode 100644
index 47a0b1b8..00000000
--- a/resources/jquery.ui/themes/vector/images/button-over-green.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-over-red.png b/resources/jquery.ui/themes/vector/images/button-over-red.png
deleted file mode 100644
index a2445365..00000000
--- a/resources/jquery.ui/themes/vector/images/button-over-red.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-over.png b/resources/jquery.ui/themes/vector/images/button-over.png
deleted file mode 100644
index 558f1f80..00000000
--- a/resources/jquery.ui/themes/vector/images/button-over.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-red-hover-large.png b/resources/jquery.ui/themes/vector/images/button-red-hover-large.png
deleted file mode 100644
index 11ccef06..00000000
--- a/resources/jquery.ui/themes/vector/images/button-red-hover-large.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-red-hover.png b/resources/jquery.ui/themes/vector/images/button-red-hover.png
deleted file mode 100644
index 55a61743..00000000
--- a/resources/jquery.ui/themes/vector/images/button-red-hover.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-red-large.png b/resources/jquery.ui/themes/vector/images/button-red-large.png
deleted file mode 100644
index 8d089efd..00000000
--- a/resources/jquery.ui/themes/vector/images/button-red-large.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/button-red.png b/resources/jquery.ui/themes/vector/images/button-red.png
deleted file mode 100644
index 7a292fc1..00000000
--- a/resources/jquery.ui/themes/vector/images/button-red.png
+++ /dev/null
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/images/titlebar-fade.png b/resources/jquery.ui/themes/vector/images/titlebar-fade.png
index f9fde8b3..12a80c88 100644
--- a/resources/jquery.ui/themes/vector/images/titlebar-fade.png
+++ b/resources/jquery.ui/themes/vector/images/titlebar-fade.png
Binary files differ
diff --git a/resources/jquery.ui/themes/vector/jquery.ui.button.css b/resources/jquery.ui/themes/vector/jquery.ui.button.css
index 006bbeac..ea14723f 100644
--- a/resources/jquery.ui/themes/vector/jquery.ui.button.css
+++ b/resources/jquery.ui/themes/vector/jquery.ui.button.css
@@ -1,195 +1,379 @@
/* Button
----------------------------------*/
-.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
-.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
-button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
-.ui-button-icons-only { width: 3.4em; }
-button.ui-button-icons-only { width: 3.7em; }
+.ui-button {
+ display: inline-block;
+ position: relative;
+ padding: 0;
+ margin-right: .1em;
+ text-decoration: none !important;
+ cursor: pointer;
+ text-align: center;
+ zoom: 1;
+ overflow: visible; /* the overflow property removes extra width in IE */
+}
/*button text element */
-.ui-button .ui-button-text { display: block; line-height: 1.4; }
-.ui-button-text-only .ui-button-text { padding: 0.3em 1em 0.25em 1em; }
-.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: 0.3em; text-indent: -9999999px; }
-.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: 0.3em 1em 0.25em 2.1em; }
-.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: 0.3em 2.1em 0.25em 1em; }
-.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
+.ui-button .ui-button-text {
+ display: block;
+ line-height: 1.4;
+ text-shadow: 0 1px 1px #fff;
+}
+.ui-button-text-only .ui-button-text {
+ padding: 0.3em 1em 0.25em 1em;
+}
+.ui-button-icon-only .ui-button-text,
+.ui-button-icons-only .ui-button-text {
+ padding: 0.3em;
+ text-indent: -9999999px;
+}
+.ui-button-text-icon-primary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+ padding: 0.3em 1em 0.25em 2.1em;
+}
+.ui-button-text-icon-secondary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+ padding: 0.3em 2.1em 0.25em 1em;
+}
+.ui-button-text-icons .ui-button-text {
+ padding-left: 2.1em;
+ padding-right: 2.1em;
+}
/* no icon support for input elements, provide padding by default */
-input.ui-button { padding: 0.3em 1em; }
+input.ui-button {
+ padding: 0.3em 1em;
+}
/*button icon element(s) */
-.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-text-icon .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -9px; }
-.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
-.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icon .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: 0.5em; }
-.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icon .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: 0.5em; }
+.ui-button-icon-only .ui-icon,
+.ui-button-text-icon-primary .ui-icon,
+.ui-button-text-icon-secondary .ui-icon,
+.ui-button-text-icons .ui-icon,
+.ui-button-text-icon .ui-icon,
+.ui-button-icons-only .ui-icon {
+ position: absolute;
+ top: 50%;
+ margin-top: -9px;
+}
+.ui-button-icon-only .ui-icon {
+ left: 50%;
+ margin-left: -8px;
+}
+.ui-button-text-icon-primary .ui-button-icon-primary,
+.ui-button-text-icon .ui-button-icon-primary,
+.ui-button-text-icons .ui-button-icon-primary,
+.ui-button-icons-only .ui-button-icon-primary {
+ left: 0.5em;
+}
+.ui-button-text-icon-secondary .ui-button-icon-secondary,
+.ui-button-text-icon .ui-button-icon-secondary,
+.ui-button-text-icons .ui-button-icon-secondary,
+.ui-button-icons-only .ui-button-icon-secondary {
+ right: 0.5em;
+}
/*button sets*/
-.ui-buttonset { margin-right: 7px; }
-.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
+.ui-buttonset {
+ margin-right: 7px;
+}
+.ui-buttonset .ui-button {
+ margin-left: 0;
+ margin-right: -.4em;
+}
/* workarounds */
-button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
+button.ui-button::-moz-focus-inner {
+ border: 0;
+ padding: 0; /* reset extra padding in Firefox */
+}
+/* Disables the annoying dashed border Firefox puts on active buttons */
+body button.ui-button::-moz-focus-inner {
+ border: 0;
+}
+/* Give large buttons some extra padding */
+body .ui-button-large {
+ padding: 5px;
+}
+/* Use white icons for colored buttons */
+.ui-button-green .ui-icon,
+.ui-button-blue .ui-icon,
+.ui-button-red .ui-icon,
+.ui-button-orange .ui-icon {
+ /* @embed */
+ background-image: url(images/ui-icons_ffffff_256x240.png) !important;
+}
+
+/* Corner radius */
+/* This is normally handled in jquery.ui.theme.css, but in our case, the corner
+ styling of our buttons doesn't match our default widget corner styling */
+.ui-button.ui-corner-all,
+.ui-button.ui-corner-top,
+.ui-button.ui-corner-left,
+.ui-button.ui-corner-tl {
+ -moz-border-radius-topleft: 4px;
+ -webkit-border-top-left-radius: 4px;
+ border-top-left-radius: 4px;
+}
+.ui-button.ui-corner-all,
+.ui-button.ui-corner-top,
+
+.ui-button.ui-corner-right,
+.ui-button.ui-corner-tr {
+ -moz-border-radius-topright: 4px;
+ -webkit-border-top-right-radius: 4px;
+ border-top-right-radius: 4px;
+}
+.ui-button.ui-corner-all,
+.ui-button.ui-corner-bottom,
+.ui-button.ui-corner-left,
+.ui-button.ui-corner-bl {
+ -moz-border-radius-bottomleft: 4px;
+ -webkit-border-bottom-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+.ui-button.ui-corner-all,
+.ui-button.ui-corner-bottom,
+.ui-button.ui-corner-right,
+.ui-button.ui-corner-br {
+ -moz-border-radius-bottomright: 4px;
+ -webkit-border-bottom-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
body .ui-button {
+ color: #2779aa;
margin: 0.5em 0 0.5em 0.4em;
- border: 1px solid #a6a6a6 !important;
- /* @embed */
- background: #f2f2f2 url(images/button-off.png) repeat-x scroll 50% 100% !important;
+ border: 1px solid #aaa !important;
+ background: #f0f0f0 !important;
+ background: -moz-linear-gradient(top, #fff 0%, #ddd 90%) !important; /* FF3.6+ */
+ background: -webkit-linear-gradient(top, #fff 0%, #ddd 90%) !important; /* Chrome10+, Safari5.1+ */
+ background: -o-linear-gradient(top, #fff 0%, #ddd 90%) !important; /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #fff 0%, #ddd 90%) !important; /* IE10+ */
+ background: linear-gradient(to bottom, #fff 0%, #ddd 90%) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#dddddd', GradientType=0); /* IE6-8 */
cursor: pointer;
font-size: 1em;
line-height: 1.4em;
width: auto;
overflow: visible;
+ -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.2);
+ -moz-box-shadow: 0 1px 3px rgba(0,0,0,.2);
+ box-shadow: 0 1px 3px rgba(0,0,0,.2);
}
-/* Corner radius */
-/* This is normally handled in jquery.ui.theme.css, but in our case, the corner
- styling of our buttons doesn't match our default widget corner styling */
-.ui-button.ui-corner-all, .ui-button.ui-corner-top, .ui-button.ui-corner-left, .ui-button.ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; }
-.ui-button.ui-corner-all, .ui-button.ui-corner-top, .ui-button.ui-corner-right, .ui-button.ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; }
-.ui-button.ui-corner-all, .ui-button.ui-corner-bottom, .ui-button.ui-corner-left, .ui-button.ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; }
-.ui-button.ui-corner-all, .ui-button.ui-corner-bottom, .ui-button.ui-corner-right, .ui-button.ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
+body .ui-button-icon-only {
+ width: 2.2em;
+}
+
+body .ui-button-icons-only {
+ width: 3.4em;
+}
body .ui-button:hover {
- border-color: #6e7273;
- /* @embed */
- background: #e1e1e1 url(images/button-over.png) repeat-x scroll 50% 100% !important;
+ color: #2779aa;
+ border-color: #bbb !important;
+ background: #fff !important;
+ background: -moz-linear-gradient(top, #fff 0%, #eee 90%) !important; /* FF3.6+ */
+ background: -webkit-linear-gradient(top, #fff 0%, #eee 90%) !important; /* Chrome10+, Safari5.1+ */
+ background: -o-linear-gradient(top, #fff 0%, #eee 90%) !important; /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #fff 0%, #eee 90%) !important; /* IE10+ */
+ background: linear-gradient(to bottom, #fff 0%, #eee 90%) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0); /* IE6-8 */
+ -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.1);
+ -moz-box-shadow: 0 1px 3px rgba(0,0,0,.1);
+ box-shadow: 0 1px 3px rgba(0,0,0,.1);
}
body .ui-button:active,
body .ui-button:focus {
- border-color: #707271;
- /* @embed */
- background: #bfbfbf url(images/button-down.png) repeat-x scroll 50% 100% !important;
-}
-body .ui-button.disabled {
- color: #7f7f7f;
- border-color: #cccccc;
- /* @embed */
- background: #f2f2f2 url(images/button-disabled.png) repeat-x scroll 50% 100% !important;
+ border-color: #8ad !important;
+ -webkit-box-shadow: 0 0 1px 1px rgba(167,215,249,.5);
+ -moz-box-shadow: 0 0 1px 1px rgba(167,215,249,.5);
+ box-shadow: 0 0 1px 1px rgba(167,215,249,.5);
}
-/* Disables the annoying dashed border Firefox puts on active buttons */
-body button.ui-button::-moz-focus-inner {
- border: 0;
-}
-/* Give large buttons some extra padding */
-body .ui-button-large {
- padding: 5px;
-}
-/* Use white icons for colored buttons */
-.ui-button-green .ui-icon, .ui-button-blue .ui-icon, .ui-button-red .ui-icon, .ui-button-orange .ui-icon {
- /* @embed */
- background-image: url(images/ui-icons_ffffff_256x240.png) !important;
+body .ui-button:active {
+ background: #e0e0e0 !important;
+ background: -moz-linear-gradient(top, #f0f0f0 0%, #d0d0d0 90%) !important; /* FF3.6+ */
+ background: -webkit-linear-gradient(top, #f0f0f0 0%, #d0d0d0 90%) !important; /* Chrome10+, Safari5.1+ */
+ background: -o-linear-gradient(top, #f0f0f0 0%, #d0d0d0 90%) !important; /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #f0f0f0 0%, #d0d0d0 90%) !important; /* IE10+ */
+ background: linear-gradient(to bottom, #f0f0f0 0%, #d0d0d0 90%) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f0f0f0', endColorstr='#d0d0d0', GradientType=0); /* IE6-8 */
}
/* Green buttons */
-
+body .ui-button-green,
+body .ui-button-green .ui-button-text {
+ color: white;
+ text-shadow: 0 -1px 1px #072;
+}
body .ui-button.ui-button-green {
- color: white !important;
- border-color: #97af7e !important;
- /* @embed */
- background: #3cb677 url(images/button-green.png) repeat-x scroll 50% 100% !important;
+ border-color: #294 !important;
+ background: #295 !important;
+ background: -moz-linear-gradient(top, #3c8 0%, #295 90%) !important; /* FF3.6+ */
+ background: -webkit-linear-gradient(top, #3c8 0%, #295 90%) !important; /* Chrome10+, Safari5.1+ */
+ background: -o-linear-gradient(top, #3c8 0%, #295 90%) !important; /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #3c8 0%, #295 90%) !important; /* IE10+ */
+ background: linear-gradient(to bottom, #3c8 0%, #295 90%) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#33cc88', endColorstr='#229955', GradientType=0); /* IE6-8 */
+ -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.3);
+ -moz-box-shadow: 0 1px 3px rgba(0,0,0,.3);
+ box-shadow: 0 1px 3px rgba(0,0,0,.3);
}
body .ui-button.ui-button-green:hover {
- border-color: #778e61 !important;
- /* @embed */
- background: #339b65 url(images/button-green-hover.png) repeat-x scroll 50% 100% !important;
+ background: #33a055 !important;
+ background: -moz-linear-gradient(top, #44d388 0%, #33a055 90%) !important; /* FF3.6+ */
+ background: -webkit-linear-gradient(top, #44d388 0%, #33a055 90%) !important; /* Chrome10+, Safari5.1+ */
+ background: -o-linear-gradient(top, #44d388 0%, #33a055 90%) !important; /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #44d388 0%, #33a055 90%) !important; /* IE10+ */
+ background: linear-gradient(to bottom, #44d388 0%, #33a055 90%) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#44d388', endColorstr='#33a055', GradientType=0); /* IE6-8 */
+ -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.25);
+ -moz-box-shadow: 0 1px 3px rgba(0,0,0,.25);
+ box-shadow: 0 1px 3px rgba(0,0,0,.25);
}
-body .ui-button.ui-button-green.ui-button-large {
- /* @embed */
- background: #3cb677 url(images/button-green-large.png) repeat-x scroll 50% 100% !important;
+body .ui-button.ui-button-green:active,
+body .ui-button.ui-button-green:focus {
+ border-color: #172 !important;
+ -webkit-box-shadow: 0 0 2px 2px rgba(167,215,249,.75);
+ -moz-box-shadow: 0 0 2px 2px rgba(167,215,249,.75);
+ box-shadow: 0 0 2px 2px rgba(167,215,249,.75);
}
-body .ui-button.ui-button-green.ui-button-large:hover {
- /* @embed */
- background: #339b65 url(images/button-green-hover-large.png) repeat-x scroll 50% 100% !important;
-}
-body .ui-button.ui-button-green.disabled {
- filter:alpha(opacity=50);
- -moz-opacity:0.50;
- -khtml-opacity: 0.50;
- opacity: 0.50;
+body .ui-button.ui-button-green:active {
+ background: #338855 !important;
+ background: -moz-linear-gradient(top, #30c080 0%, #338855 90%) !important; /* FF3.6+ */
+ background: -webkit-linear-gradient(top, #30c080 0%, #338855 90%) !important; /* Chrome10+, Safari5.1+ */
+ background: -o-linear-gradient(top, #30c080 0%, #338855 90%) !important; /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #30c080 0%, #338855 90%) !important; /* IE10+ */
+ background: linear-gradient(to bottom, #30c080 0%, #338855 90%) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#30c080', endColorstr='#338855', GradientType=0); /* IE6-8 */
}
/* Blue buttons */
-
+body .ui-button-blue,
+body .ui-button-blue .ui-button-text {
+ color: white;
+ text-shadow: 0 -1px 1px #037;
+}
body .ui-button.ui-button-blue {
- color: white !important;
- border-color: #628acb !important;
- /* @embed */
- background: #3365ba url(images/button-blue.png) repeat-x scroll 50% 100% !important;
+ border-color: #468 !important;
+ background: #36b !important;
+ background: -moz-linear-gradient(top, #48e 0%, #36b 90%) !important; /* FF3.6+ */
+ background: -webkit-linear-gradient(top, #48e 0%, #36b 90%) !important; /* Chrome10+, Safari5.1+ */
+ background: -o-linear-gradient(top, #48e 0%, #36b 90%) !important; /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #48e 0%, #36b 90%) !important; /* IE10+ */
+ background: linear-gradient(to bottom, #48e 0%, #36b 90%) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#4488ee', endColorstr='#3366bb', GradientType=0); /* IE6-8 */
+ -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.35);
+ -moz-box-shadow: 0 1px 3px rgba(0,0,0,.35);
+ box-shadow: 0 1px 3px rgba(0,0,0,.35);
}
body .ui-button.ui-button-blue:hover {
- border-color: #5375ad !important;
- /* @embed */
- background: #2b569e url(images/button-blue-hover.png) repeat-x scroll 50% 100% !important;
+ background: #36c !important;
+ background: -moz-linear-gradient(top, #59e 0%, #36c 90%) !important; /* FF3.6+ */
+ background: -webkit-linear-gradient(top, #59e 0%, #36c 90%) !important; /* Chrome10+, Safari5.1+ */
+ background: -o-linear-gradient(top, #59e 0%, #36c 90%) !important; /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #59e 0%, #36c 90%) !important; /* IE10+ */
+ background: linear-gradient(to bottom, #59e 0%, #36c 90%) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5599ee', endColorstr='#3366cc', GradientType=0); /* IE6-8 */
}
-body .ui-button.ui-button-blue.ui-button-large {
- /* @embed */
- background: #3365ba url(images/button-blue-large.png) repeat-x scroll 50% 100% !important;
-}
-body .ui-button.ui-button-blue.ui-button-large:hover {
- /* @embed */
- background: #2b569e url(images/button-blue-hover-large.png) repeat-x scroll 50% 100% !important;
+body .ui-button.ui-button-blue:active,
+body .ui-button.ui-button-blue:focus {
+ border-color: #357 !important;
+ -webkit-box-shadow: 0 0 2px 2px rgba(167,215,249,.75);
+ -moz-box-shadow: 0 0 2px 2px rgba(167,215,249,.75);
+ box-shadow: 0 0 2px 2px rgba(167,215,249,.75);
}
-body .ui-button.ui-button-blue.disabled {
- filter:alpha(opacity=50);
- -moz-opacity:0.50;
- -khtml-opacity: 0.50;
- opacity: 0.50;
+body .ui-button.ui-button-blue:active {
+ background: #3060a0 !important;
+ background: -moz-linear-gradient(top, #4080e0 0%, #3060a0 90%) !important; /* FF3.6+ */
+ background: -webkit-linear-gradient(top, #4080e0 0%, #3060a0 90%) !important; /* Chrome10+, Safari5.1+ */
+ background: -o-linear-gradient(top, #4080e0 0%, #3060a0 90%) !important; /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #4080e0 0%, #3060a0 90%) !important; /* IE10+ */
+ background: linear-gradient(to bottom, #4080e0 0%, #3060a0 90%) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#4080e0', endColorstr='#3060a0', GradientType=0); /* IE6-8 */
}
/* Red buttons */
-
+body .ui-button-red,
+body .ui-button-red .ui-button-text {
+ color: white;
+ text-shadow: 0 -1px 1px #700;
+}
body .ui-button.ui-button-red {
- color: white !important;
- border-color: #af977e !important;
- /* @embed */
- background: #cb0000 url(images/button-red.png) repeat-x scroll 50% 100% !important;
+ border-color: #944 !important;
+ background: #a22 !important;
+ background: -moz-linear-gradient(top, #d44 0%, #a22 90%) !important; /* FF3.6+ */
+ background: -webkit-linear-gradient(top, #d44 0%, #a22 90%) !important; /* Chrome10+, Safari5.1+ */
+ background: -o-linear-gradient(top, #d44 0%, #a22 90%) !important; /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #d44 0%, #a22 90%) !important; /* IE10+ */
+ background: linear-gradient(to bottom, #d44 0%, #a22 90%) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#dd4444', endColorstr='#aa2222', GradientType=0); /* IE6-8 */
+ -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.35);
+ -moz-box-shadow: 0 1px 3px rgba(0,0,0,.35);
+ box-shadow: 0 1px 3px rgba(0,0,0,.35);
}
body .ui-button.ui-button-red:hover {
- border-color: #8e7761 !important;
- /* @embed */
- background: #ad0000 url(images/button-red-hover.png) repeat-x scroll 50% 100% !important;
+ border-color: #a44 !important;
+ background: #b03333 !important;
+ background: -moz-linear-gradient(top, #ee4646 0%, #b03333 90%) !important; /* FF3.6+ */
+ background: -webkit-linear-gradient(top, #ee4646 0%, #b03333 90%) !important; /* Chrome10+, Safari5.1+ */
+ background: -o-linear-gradient(top, #ee4646 0%, #b03333 90%) !important; /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #ee4646 0%, #b03333 90%) !important; /* IE10+ */
+ background: linear-gradient(to bottom, #ee4646 0%, #b03333 90%) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee4646', endColorstr='#b03333', GradientType=0); /* IE6-8 */
+ -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.3);
+ -moz-box-shadow: 0 1px 3px rgba(0,0,0,.3);
+ box-shadow: 0 1px 3px rgba(0,0,0,.3);
}
-body .ui-button.ui-button-red.ui-button-large {
- /* @embed */
- background: #cb0000 url(images/button-red.png) repeat-x scroll 50% 100% !important;
+body .ui-button.ui-button-red:active,
+body .ui-button.ui-button-red:focus {
+ border-color: #747 !important;
+ -webkit-box-shadow: 0 0 2px 2px rgba(167,215,249,.7);
+ -moz-box-shadow: 0 0 2px 2px rgba(167,215,249,.7);
+ box-shadow: 0 0 2px 2px rgba(167,215,249,.7);
}
-body .ui-button.ui-button-red.ui-button-large:hover {
- /* @embed */
- background: #ad0000 url(images/button-red-hover.png) repeat-x scroll 50% 100% !important;
-}
-body .ui-button.ui-button-red.disabled {
- filter:alpha(opacity=50);
- -moz-opacity:0.50;
- -khtml-opacity: 0.50;
- opacity: 0.50;
+body .ui-button.ui-button-red:active {
+ background: #952020 !important;
+ background: -moz-linear-gradient(top, #d04545 0%, #952020 90%) !important; /* FF3.6+ */
+ background: -webkit-linear-gradient(top, #d04545 0%, #952020 90%) !important; /* Chrome10+, Safari5.1+ */
+ background: -o-linear-gradient(top, #d04545 0%, #952020 90%) !important; /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #d04545 0%, #952020 90%) !important; /* IE10+ */
+ background: linear-gradient(to bottom, #d04545 0%, #952020 90%) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#d04545', endColorstr='#952020', GradientType=0); /* IE6-8 */
}
-/* Orange buttons */
-
-body .ui-button.ui-button-orange {
- color: white !important;
- border-color: #f3a863 !important;
- /* @embed */
- background: #f07f14 url(images/button-orange.png) repeat-x scroll 50% 100% !important;
-}
-body .ui-button.ui-button-orange:hover {
- border-color: #ce9055 !important;
- /* @embed */
- background: #cc6c11 url(images/button-orange-hover.png) repeat-x scroll 50% 100% !important;
-}
-body .ui-button.ui-button-orange.ui-button-large {
- /* @embed */
- background: #f07f14 url(images/button-orange-large.png) repeat-x scroll 50% 100% !important;
-}
-body .ui-button.ui-button-orange.ui-button-large:hover {
- /* @embed */
- background: #cc6c11 url(images/button-orange-hover-large.png) repeat-x scroll 50% 100% !important;
+/* Disabled buttons */
+body .ui-button-green.disabled,
+body .ui-button-green.disabled:hover,
+body .ui-button-green.disabled:active,
+body .ui-button-green.disabled:focus,
+body .ui-button-blue.disabled,
+body .ui-button-blue.disabled:hover,
+body .ui-button-blue.disabled:active,
+body .ui-button-blue.disabled:focus,
+body .ui-button-red.disabled,
+body .ui-button-red.disabled:hover,
+body .ui-button-red.disabled:active,
+body .ui-button-red.disabled:focus,
+body .ui-button.disabled,
+body .ui-button.disabled:hover {
+ color: #aaa;
+ border-color: #ccc !important;
+ background: #eee !important;
+ background: -moz-linear-gradient(top, #f6f6f6 0%, #eee 90%) !important; /* FF3.6+ */
+ background: -webkit-linear-gradient(top, #f6f6f6 0%, #eee 90%) !important; /* Chrome10+, Safari5.1+ */
+ background: -o-linear-gradient(top, #f6f6f6 0%, #eee 90%) !important; /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, #f6f6f6 0%, #eee 90%) !important; /* IE10+ */
+ background: linear-gradient(to bottom, #f6f6f6 0%, #eee 90%) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f6f6f6', endColorstr='#eeeeee', GradientType=0); /* IE6-8 */
+ -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0);
+ -moz-box-shadow: 0 1px 3px rgba(0,0,0,0);
+ box-shadow: 0 1px 3px rgba(0,0,0,0);
}
-body .ui-button.ui-button-orange.disabled {
- filter:alpha(opacity=50);
- -moz-opacity:0.50;
- -khtml-opacity: 0.50;
- opacity: 0.50;
+body .ui-button-green.disabled .ui-button-text,
+body .ui-button-blue.disabled .ui-button-text,
+body .ui-button-red.disabled .ui-button-text {
+ color: #aaa;
+ text-shadow: 0 1px 1px #fff;
}
diff --git a/resources/jquery/images/jquery.arrowSteps.divider-ltr.png b/resources/jquery/images/jquery.arrowSteps.divider-ltr.png
index 83d6ff84..84ed2a2d 100644
--- a/resources/jquery/images/jquery.arrowSteps.divider-ltr.png
+++ b/resources/jquery/images/jquery.arrowSteps.divider-ltr.png
Binary files differ
diff --git a/resources/jquery/images/jquery.arrowSteps.divider-rtl.png b/resources/jquery/images/jquery.arrowSteps.divider-rtl.png
index 529d7b84..7cfbfeba 100644
--- a/resources/jquery/images/jquery.arrowSteps.divider-rtl.png
+++ b/resources/jquery/images/jquery.arrowSteps.divider-rtl.png
Binary files differ
diff --git a/resources/jquery/images/jquery.arrowSteps.head-ltr.png b/resources/jquery/images/jquery.arrowSteps.head-ltr.png
index 3289617d..eb070280 100644
--- a/resources/jquery/images/jquery.arrowSteps.head-ltr.png
+++ b/resources/jquery/images/jquery.arrowSteps.head-ltr.png
Binary files differ
diff --git a/resources/jquery/images/jquery.arrowSteps.head-rtl.png b/resources/jquery/images/jquery.arrowSteps.head-rtl.png
index 3d9f70cb..7ea2fdb5 100644
--- a/resources/jquery/images/jquery.arrowSteps.head-rtl.png
+++ b/resources/jquery/images/jquery.arrowSteps.head-rtl.png
Binary files differ
diff --git a/resources/jquery/images/jquery.arrowSteps.tail-ltr.png b/resources/jquery/images/jquery.arrowSteps.tail-ltr.png
index 92b872b2..3ad990b6 100644
--- a/resources/jquery/images/jquery.arrowSteps.tail-ltr.png
+++ b/resources/jquery/images/jquery.arrowSteps.tail-ltr.png
Binary files differ
diff --git a/resources/jquery/images/marker.png b/resources/jquery/images/marker.png
index 3929bbb5..19efb6ce 100644
--- a/resources/jquery/images/marker.png
+++ b/resources/jquery/images/marker.png
Binary files differ
diff --git a/resources/jquery/images/mask.png b/resources/jquery/images/mask.png
index b0a4d406..fe08de0e 100644
--- a/resources/jquery/images/mask.png
+++ b/resources/jquery/images/mask.png
Binary files differ
diff --git a/resources/jquery/jquery.arrowSteps.js b/resources/jquery/jquery.arrowSteps.js
index 488d1065..a1fd679d 100644
--- a/resources/jquery/jquery.arrowSteps.js
+++ b/resources/jquery/jquery.arrowSteps.js
@@ -42,18 +42,21 @@
*/
( function ( $ ) {
$.fn.arrowSteps = function () {
- var $steps, width, arrowWidth;
+ var $steps, width, arrowWidth,
+ paddingSide = $( 'body' ).hasClass( 'rtl' ) ? 'padding-left' : 'padding-right';
+
this.addClass( 'arrowSteps' );
$steps = this.find( 'li' );
width = parseInt( 100 / $steps.length, 10 );
$steps.css( 'width', width + '%' );
- // every step except the last one has an arrow at the right hand side. Also add in the padding
- // for the calculated arrow 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.
arrowWidth = parseInt( this.outerHeight(), 10 );
$steps.filter( ':not(:last-child)' ).addClass( 'arrow' )
- .find( 'div' ).css( 'padding-right', arrowWidth.toString() + 'px' );
+ .find( 'div' ).css( paddingSide, arrowWidth.toString() + 'px' );
this.data( 'arrowSteps', $steps );
return this;
diff --git a/resources/jquery/jquery.badge.css b/resources/jquery/jquery.badge.css
index 92e72555..f313663e 100644
--- a/resources/jquery/jquery.badge.css
+++ b/resources/jquery/jquery.badge.css
@@ -1,39 +1,37 @@
.mw-badge {
- min-width: 8px;
- height: 14px;
- border: 1px solid white;
- -moz-border-radius: 8px;
- -webkit-border-radius: 8px;
- border-radius: 8px;
- -moz-box-shadow: 0px 1px 4px #ccc;
- -webkit-box-shadow: 0px 1px 4px #ccc;
- box-shadow: 0px 1px 4px #ccc;
- background-color: #b60a00;
- background-image: -o-linear-gradient(bottom, #a70802 0%, #cf0e00 100%);
- background-image: -moz-linear-gradient(bottom, #a70802 0%, #cf0e00 100%);
- background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #a70802), color-stop(1, #cf0e00));
- background-image: -webkit-linear-gradient(bottom, #a70802 0%, #cf0e00 100%);
- background-image: -ms-linear-gradient(bottom, #a70802 0%, #cf0e00 100%);
- background-image: linear-gradient(bottom, #a70802 0%, #cf0e00 100%);
- padding: 0 3px;
+ 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-size: 12px;
- line-height: 14px;
+ font-weight: bold;
color: white;
- vertical-align: top;
+ vertical-align: baseline;
+ text-shadow: 0 1px rgba(0, 0, 0, 0.4);
}
.mw-badge-inline {
- display: inline-block;
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/jquery/jquery.badge.js b/resources/jquery/jquery.badge.js
index 04495b71..9404e818 100644
--- a/resources/jquery/jquery.badge.js
+++ b/resources/jquery/jquery.badge.js
@@ -1,8 +1,6 @@
/**
* jQuery Badge plugin
*
- * Based on Badger plugin by Daniel Raftery (http://thrivingkings.com/badger).
- *
* @license MIT
*/
@@ -23,95 +21,56 @@
*
* This program is distributed WITHOUT ANY WARRANTY.
*/
-( function ( $ ) {
-
+( function ( $, mw ) {
/**
- * Allows you to put a numeric "badge" on an item on the page.
+ * Allows you to put a "badge" on an item on the page. The badge container
+ * will be appended to the selected element(s).
* See mediawiki.org/wiki/ResourceLoader/Default_modules#jQuery.badge
*
- * @param {string|number} badgeCount An explicit number, or "+n"/ "-n"
- * to modify the existing value. If the new value is equal or lower than 0,
- * any existing badge will be removed. The badge container will be appended
- * to the selected element(s).
- * @param {Object} options Optional parameters specified below
- * type: 'inline' or 'overlay' (default)
- * callback: will be called with the number now shown on the badge as a parameter
+ * @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 if the badge should be displayed inline, false
+ * if the badge should overlay the parent element (default is inline)
+ * @param {boolean} displayZero True if the number zero should be displayed,
+ * false if the number zero should result in the badge being hidden
+ * (default is zero will result in the badge being hidden)
*/
- $.fn.badge = function ( badgeCount, options ) {
- var $badge,
- oldBadgeCount,
- newBadgeCount,
- $existingBadge = this.find( '.mw-badge' );
-
- options = $.extend( { type : 'overlay' }, options );
-
- // If there is no existing badge, this will give an empty string
- oldBadgeCount = Number( $existingBadge.text() );
- if ( isNaN( oldBadgeCount ) ) {
- oldBadgeCount = 0;
- }
+ $.fn.badge = function ( text, inline, displayZero ) {
+ var $badge = this.find( '.mw-badge' ),
+ badgeStyleClass = 'mw-badge-' + ( inline ? 'inline' : 'overlay' ),
+ isImportant = true, displayBadge = true;
- // If badgeCount is a number, use that as the new badge
- if ( typeof badgeCount === 'number' ) {
- newBadgeCount = badgeCount;
- } else if ( typeof badgeCount === 'string' ) {
- // If badgeCount is "+x", add x to the old badge
- if ( badgeCount.charAt(0) === '+' ) {
- newBadgeCount = oldBadgeCount + Number( badgeCount.substr(1) );
- // If badgeCount is "-x", subtract x from the old badge
- } else if ( badgeCount.charAt(0) === '-' ) {
- newBadgeCount = oldBadgeCount - Number( badgeCount.substr(1) );
- // If badgeCount can be converted into a number, convert it
- } else if ( !isNaN( Number( badgeCount ) ) ) {
- newBadgeCount = Number( badgeCount );
- } else {
- newBadgeCount = 0;
+ // If we're displaying zero, ensure style to be non-important
+ if ( mw.language.convertNumber( text, true ) === 0 ) {
+ isImportant = false;
+ if ( !displayZero ) {
+ displayBadge = false;
}
- // Other types are not supported, fall back to 0.
- } else {
- newBadgeCount = 0;
+ // If text is falsey (besides 0), hide the badge
+ } else if ( !text ) {
+ displayBadge = false;
}
- // Badge count must be a whole number
- newBadgeCount = Math.round( newBadgeCount );
-
- if ( newBadgeCount <= 0 ) {
- // Badges should only exist for values > 0.
- $existingBadge.remove();
- } else {
- // Don't add duplicates
- if ( $existingBadge.length ) {
- $badge = $existingBadge;
- // Insert the new count into the badge
- this.find( '.mw-badge-content' ).text( newBadgeCount );
- } else {
- // Contruct a new badge with the count
- $badge = $( '<div>' )
- .addClass( 'mw-badge' )
- .append(
- $( '<span>' )
- .addClass( 'mw-badge-content' )
- .text( newBadgeCount )
- );
- this.append( $badge );
- }
-
- if ( options.type === 'inline' ) {
+ if ( displayBadge ) {
+ // If a badge already exists, reuse it
+ if ( $badge.length ) {
$badge
- .removeClass( 'mw-badge-overlay' )
- .addClass( 'mw-badge-inline' );
- // Default: overlay
+ .toggleClass( 'mw-badge-important', isImportant )
+ .find( '.mw-badge-content' )
+ .text( text );
} else {
- $badge
- .removeClass( 'mw-badge-inline' )
- .addClass( 'mw-badge-overlay' );
-
- }
-
- // If a callback was specified, call it with the badge count
- if ( $.isFunction( options.callback ) ) {
- options.callback( newBadgeCount );
+ // 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;
};
-}( jQuery ) );
+}( jQuery, mediaWiki ) );
diff --git a/resources/jquery/jquery.byteLength.js b/resources/jquery/jquery.byteLength.js
index 3d5b7206..398937e6 100644
--- a/resources/jquery/jquery.byteLength.js
+++ b/resources/jquery/jquery.byteLength.js
@@ -4,6 +4,8 @@
* Calculate the byte length of a string (accounting for UTF-8).
*
* @author Jan Paul Posma, 2011
+ * @author Timo Tijhof, 2012
+ * @author David Chan, 2013
*/
jQuery.byteLength = function ( str ) {
@@ -12,8 +14,18 @@ jQuery.byteLength = function ( str ) {
// 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;
+
};
diff --git a/resources/jquery/jquery.byteLimit.js b/resources/jquery/jquery.byteLimit.js
index 75dc2b90..a8c0b065 100644
--- a/resources/jquery/jquery.byteLimit.js
+++ b/resources/jquery/jquery.byteLimit.js
@@ -78,7 +78,8 @@
// Chop off characters from the end of the "inserted content" string
// until the limit is statisfied.
if ( fn ) {
- while ( $.byteLength( fn( inpParts.join( '' ) ) ) > byteLimit ) {
+ // 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 {
@@ -221,8 +222,11 @@
// This is a side-effect of limiting after the fact.
if ( res.trimmed === true ) {
this.value = res.newVal;
- prevSafeVal = 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;
} );
} );
};
diff --git a/resources/jquery/jquery.checkboxShiftClick.js b/resources/jquery/jquery.checkboxShiftClick.js
index 1990dc0d..b2065665 100644
--- a/resources/jquery/jquery.checkboxShiftClick.js
+++ b/resources/jquery/jquery.checkboxShiftClick.js
@@ -1,23 +1,31 @@
/**
* jQuery checkboxShiftClick
*
- * This will enable checkboxes to be checked or unchecked in a row by clicking one, holding shift and clicking another one
+ * This will enable checkboxes to be checked or unchecked in a row by clicking one,
+ * holding shift and clicking another one.
*
- * @author Krinkle <krinklemail@gmail.com>
+ * @author Timo Tijhof, 2011 - 2012
* @license GPL v2
*/
( function ( $ ) {
- $.fn.checkboxShiftClick = function ( text ) {
- var prevCheckbox = null, $box = this;
+ $.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
- $box.slice(
- Math.min( $box.index( prevCheckbox ), $box.index( e.target ) ),
- Math.max( $box.index( prevCheckbox ), $box.index( e.target ) ) + 1
- ).prop( 'checked', !!e.target.checked );
+ // 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;
diff --git a/resources/jquery/jquery.client.js b/resources/jquery/jquery.client.js
index 24f8959e..5a95dc5b 100644
--- a/resources/jquery/jquery.client.js
+++ b/resources/jquery/jquery.client.js
@@ -6,7 +6,7 @@
/* Private Members */
/**
- * @var profileCache {Object} Keyed by userAgent string,
+ * @var {Object} profileCache Keyed by userAgent string,
* value is the parsed $.client.profile object for that user agent.
*/
var profileCache = {};
@@ -18,9 +18,9 @@
/**
* Get an object containing information about the client.
*
- * @param nav {Object} An object with atleast a 'userAgent' and 'platform' key.
+ * @param {Object} nav An object with atleast a 'userAgent' and 'platform' key.
* Defaults to the global Navigator object.
- * @return {Object} The resulting client object will be in the following format:
+ * @returns {Object} The resulting client object will be in the following format:
* {
* 'name': 'firefox',
* 'layout': 'gecko',
@@ -40,71 +40,74 @@
// Use the cached version if possible
if ( profileCache[nav.userAgent] === undefined ) {
- /* Configuration */
-
- // Name of browsers or layout engines we don't recognize
- var uk = 'unknown';
- // Generic version digit
- var x = 'x';
- // Strings found in user agent strings that need to be conformed
- var wildUserAgents = ['Opera', 'Navigator', 'Minefield', 'KHTML', 'Chrome', 'PLAYSTATION 3'];
- // Translations for conforming user agent strings
- var 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 differnt 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 detectection
- var versionPrefixes = [
- 'camino', 'chrome', 'firefox', 'netscape', 'netscape6', 'opera', 'version', 'konqueror',
- 'lynx', 'msie', 'safari', 'ps3'
- ];
- // Used as matches 2, 3 and 4 in version extraction - 3 is used as actual version number
- var versionSuffix = '(\\/|\\;?\\s|)([a-z0-9\\.\\+]*?)(\\;|dev|rel|\\)|\\s|$)';
- // Names of known browsers
- var names = [
- 'camino', 'chrome', 'firefox', 'netscape', 'konqueror', 'lynx', 'msie', 'opera',
- 'safari', 'ipod', 'iphone', 'blackberry', 'ps3', 'rekonq'
- ];
- // Tanslations for conforming browser names
- var nameTranslations = [];
- // Names of known layout engines
- var layouts = ['gecko', 'konqueror', 'msie', 'opera', 'webkit'];
- // Translations for conforming layout names
- var layoutTranslations = [['konqueror', 'khtml'], ['msie', 'trident'], ['opera', 'presto']];
- // Names of supported layout engines for version number
- var layoutVersions = ['applewebkit', 'gecko'];
- // Names of known operating systems
- var platforms = ['win', 'mac', 'linux', 'sunos', 'solaris', 'iphone'];
- // Translations for conforming operating system names
- var platformTranslations = [['sunos', 'solaris']];
-
- /* Methods */
-
- /**
- * Performs multiple replacements on a string
- */
- var translate = function ( source, translations ) {
- var i;
- for ( i = 0; i < translations.length; i++ ) {
- source = source.replace( translations[i][0], translations[i][1] );
- }
- return source;
- };
+ var
+ versionNumber,
+
+ /* 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'] ],
+
+ /* Methods */
+
+ /**
+ * Performs multiple replacements on a string
+ */
+ 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 */
+ /* Pre-processing */
- var ua = nav.userAgent,
+ ua = nav.userAgent,
match,
name = uk,
layout = uk,
@@ -140,14 +143,34 @@
/* Edge Cases -- did I mention about how user agent string lie? */
// Decode Safari's crazy 400+ version numbers
- if ( name.match( /safari/ ) && version > 400 ) {
+ if ( name === 'safari' && version > 400 ) {
version = '2.0';
}
// Expose Opera 10's lies about being Opera 9.8
- if ( name === 'opera' && version >= 9.8) {
- version = ua.match( /version\/([0-9\.]*)/i )[1] || 10;
+ 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];
+ }
}
- var versionNumber = parseFloat( version, 10 ) || 0.0;
+
+ versionNumber = parseFloat( version, 10 ) || 0.0;
/* Caching */
@@ -165,50 +188,64 @@
},
/**
- * Checks the current browser against a support map object to determine if the browser has been black-listed or
- * not. If the browser was not configured specifically it is assumed to work. It is assumed that the body
- * element is classified as either "ltr" or "rtl". If neither is set, "ltr" is assumed.
+ * Checks the current browser against a support map object.
*
* 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': {
- * // Multiple rules with configurable operators
- * 'msie': [['>=', 7], ['!=', 9]],
- * // Blocked entirely
+ * 'android': null,
* 'iphone': false
* },
* 'rtl': {
- * // Test against a string
- * 'msie': [['!==', '8.1.2.3']],
- * // RTL rules do not fall through to LTR rules, you must explicity set each of them
+ * 'android': false,
+ * // rules are not inherited from ltr
* 'iphone': false
* }
* }
*
- * @param map {Object} Browser support map
- * @param profile {Object} (optional) a client-profile object.
+ * @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 true if browser known or assumed to be supported, false if blacklisted
+ * @returns {boolean} The current browser is in the support map
*/
- test: function ( map, profile ) {
- /*jshint evil:true */
+ test: function ( map, profile, exactMatchOnly ) {
+ /*jshint evil: true */
var conditions, dir, i, op, val;
profile = $.isPlainObject( profile ) ? profile : $.client.profile();
-
- dir = $( 'body' ).is( '.rtl' ) ? 'rtl' : 'ltr';
+ 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[dir] !== 'object' || map[dir][profile.name] === undefined ) {
- // Unknown, so we assume it's working
+ 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;
}
- conditions = map[dir][profile.name];
for ( i = 0; i < conditions.length; i++ ) {
op = conditions[i][0];
val = conditions[i][1];
- if ( val === false ) {
- return false;
- }
if ( typeof val === 'string' ) {
if ( !( eval( 'profile.version' + op + '"' + val + '"' ) ) ) {
return false;
@@ -219,6 +256,7 @@
}
}
}
+
return true;
}
};
diff --git a/resources/jquery/jquery.collapsibleTabs.js b/resources/jquery/jquery.collapsibleTabs.js
deleted file mode 100644
index cb25796f..00000000
--- a/resources/jquery/jquery.collapsibleTabs.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * Collapsible tabs jQuery Plugin
- */
-( function ( $ ) {
- $.fn.collapsibleTabs = function ( options ) {
- // return if the function is called on an empty jquery object
- if ( !this.length ) {
- return this;
- }
- // Merge options into the defaults
- var $settings = $.extend( {}, $.collapsibleTabs.defaults, options );
-
- this.each( function () {
- var $el = $( this );
- // add the element to our array of collapsible managers
- $.collapsibleTabs.instances = ( $.collapsibleTabs.instances.length === 0 ?
- $el : $.collapsibleTabs.instances.add( $el ) );
- // attach the settings to the elements
- $el.data( 'collapsibleTabsSettings', $settings );
- // attach data to our collapsible elements
- $el.children( $settings.collapsible ).each( function () {
- $.collapsibleTabs.addData( $( this ) );
- } );
- } );
-
- // if we haven't already bound our resize hanlder, bind it now
- if ( !$.collapsibleTabs.boundEvent ) {
- $( window )
- .delayedBind( '500', 'resize', function ( ) {
- $.collapsibleTabs.handleResize();
- } );
- }
- // call our resize handler to setup the page
- $.collapsibleTabs.handleResize();
- return this;
- };
- $.collapsibleTabs = {
- instances: [],
- boundEvent: null,
- defaults: {
- expandedContainer: '#p-views ul',
- collapsedContainer: '#p-cactions ul',
- collapsible: 'li.collapsible',
- shifting: false,
- expandCondition: function ( eleWidth ) {
- return ( $( '#left-navigation' ).position().left + $( '#left-navigation' ).width() )
- < ( $( '#right-navigation' ).position().left - eleWidth );
- },
- collapseCondition: function () {
- return ( $( '#left-navigation' ).position().left + $( '#left-navigation' ).width() )
- > $( '#right-navigation' ).position().left;
- }
- },
- addData: function ( $collapsible ) {
- var $settings = $collapsible.parent().data( 'collapsibleTabsSettings' );
- if ( $settings !== null ) {
- $collapsible.data( 'collapsibleTabsSettings', {
- expandedContainer: $settings.expandedContainer,
- collapsedContainer: $settings.collapsedContainer,
- expandedWidth: $collapsible.width(),
- prevElement: $collapsible.prev()
- } );
- }
- },
- getSettings: function ( $collapsible ) {
- var $settings = $collapsible.data( 'collapsibleTabsSettings' );
- if ( $settings === undefined ) {
- $.collapsibleTabs.addData( $collapsible );
- $settings = $collapsible.data( 'collapsibleTabsSettings' );
- }
- return $settings;
- },
- handleResize: function ( e ) {
- $.collapsibleTabs.instances.each( function () {
- var $el = $( this ),
- data = $.collapsibleTabs.getSettings( $el );
-
- if ( data.shifting ) {
- return;
- }
-
- // if the two navigations are colliding
- if ( $el.children( data.collapsible ).length > 0 && data.collapseCondition() ) {
-
- $el.trigger( 'beforeTabCollapse' );
- // move the element to the dropdown menu
- $.collapsibleTabs.moveToCollapsed( $el.children( data.collapsible + ':last' ) );
- }
-
- // if there are still moveable items in the dropdown menu,
- // and there is sufficient space to place them in the tab container
- if ( $( data.collapsedContainer + ' ' + data.collapsible ).length > 0
- && data.expandCondition( $.collapsibleTabs.getSettings( $( data.collapsedContainer ).children(
- data.collapsible + ':first' ) ).expandedWidth ) ) {
- //move the element from the dropdown to the tab
- $el.trigger( 'beforeTabExpand' );
- $.collapsibleTabs
- .moveToExpanded( data.collapsedContainer + ' ' + data.collapsible + ':first' );
- }
- });
- },
- moveToCollapsed: function ( ele ) {
- var $moving = $( ele ),
- data = $.collapsibleTabs.getSettings( $moving ),
- dataExp = $.collapsibleTabs.getSettings( data.expandedContainer );
- dataExp.shifting = true;
- $moving
- .detach()
- .prependTo( data.collapsedContainer )
- .data( 'collapsibleTabsSettings', data );
- dataExp.shifting = false;
- $.collapsibleTabs.handleResize();
- },
- moveToExpanded: function ( ele ) {
- var $moving = $( ele ),
- data = $.collapsibleTabs.getSettings( $moving ),
- dataExp = $.collapsibleTabs.getSettings( data.expandedContainer );
- dataExp.shifting = true;
- // remove this element from where it's at and put it in the dropdown menu
- $moving.detach().insertAfter( data.prevElement ).data( 'collapsibleTabsSettings', data );
- dataExp.shifting = false;
- $.collapsibleTabs.handleResize();
- }
- };
-
-}( jQuery ) );
diff --git a/resources/jquery/jquery.colorUtil.js b/resources/jquery/jquery.colorUtil.js
index c1fe7fe3..9c6f9ecb 100644
--- a/resources/jquery/jquery.colorUtil.js
+++ b/resources/jquery/jquery.colorUtil.js
@@ -113,17 +113,20 @@
* @return Array The HSL representation
*/
rgbToHsl: function ( R, G, B ) {
- var r = R / 255,
+ var d,
+ r = R / 255,
g = G / 255,
- b = B / 255;
- var max = Math.max( r, g, b ), min = Math.min( r, g, b );
- var h, s, l = (max + min) / 2;
+ b = B / 255,
+ 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 {
- var d = max - min;
+ d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch ( max ) {
case r:
@@ -155,12 +158,12 @@
* @return Array The RGB representation
*/
hslToRgb: function ( h, s, l ) {
- var r, g, b;
+ var r, g, b, hue2rgb, q, p;
if ( s === 0 ) {
r = g = b = l; // achromatic
} else {
- var hue2rgb = function ( p, q, t ) {
+ hue2rgb = function ( p, q, t ) {
if ( t < 0 ) {
t += 1;
}
@@ -179,8 +182,8 @@
return p;
};
- var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
- var p = 2 * l - q;
+ 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 );
diff --git a/resources/jquery/jquery.delayedBind.js b/resources/jquery/jquery.delayedBind.js
index 5d32b6b0..40f3d44e 100644
--- a/resources/jquery/jquery.delayedBind.js
+++ b/resources/jquery/jquery.delayedBind.js
@@ -43,12 +43,12 @@ $.fn.extend( {
$(this).data( '_delayedBindTimerID-' + encEvent + '-' + timeout, timerID );
} );
}
-
+
// Bottom half
$(this).bind( '_delayedBind-' + encEvent + '-' + timeout, data, callback );
} );
},
-
+
/**
* Cancel the timers for delayed events on the selected elements.
*/
@@ -61,7 +61,7 @@ $.fn.extend( {
}
} );
},
-
+
/**
* Unbind an event bound with delayedBind()
*/
diff --git a/resources/jquery/jquery.expandableField.js b/resources/jquery/jquery.expandableField.js
index 063f2609..9e532e52 100644
--- a/resources/jquery/jquery.expandableField.js
+++ b/resources/jquery/jquery.expandableField.js
@@ -67,16 +67,16 @@
context = {
config: {
// callback function for before collapse
- beforeCondense: function ( context ) {},
+ beforeCondense: function () {},
// callback function for before expand
- beforeExpand: function ( context ) {},
+ beforeExpand: function () {},
// callback function for after collapse
- afterCondense: function ( context ) {},
+ afterCondense: function () {},
// callback function for after expand
- afterExpand: function ( context ) {},
+ afterExpand: function () {},
// Whether the field should expand to the left or the right -- defaults to left
expandToLeft: true
diff --git a/resources/jquery/jquery.hidpi.js b/resources/jquery/jquery.hidpi.js
new file mode 100644
index 00000000..70bfc4ea
--- /dev/null
+++ b/resources/jquery/jquery.hidpi.js
@@ -0,0 +1,117 @@
+/**
+ * Responsive images based on 'srcset' and 'window.devicePixelRatio' emulation where needed.
+ *
+ * Call $().hidpi() on a document or part of a document to replace image srcs in that section.
+ *
+ * $.devicePixelRatio() can be used to supplement window.devicePixelRatio with support on
+ * some additional browsers.
+ */
+( function ( $ ) {
+
+/**
+ * Detect 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.
+ *
+ * @method
+ * @returns {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.
+ *
+ * @method
+ * @returns {jQuery} This selection
+ */
+$.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
+ *
+ * @param {number} devicePixelRatio
+ * @param {string} srcset
+ * @return {mixed} null or the matching src string
+ *
+ * Exposed for testing.
+ */
+$.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].substr( 0, bits[1].length - 1 );
+ ratio = parseFloat( ratioStr );
+ if ( ratio <= devicePixelRatio && ratio > selectedRatio ) {
+ selectedRatio = ratio;
+ selectedSrc = src;
+ }
+ }
+ }
+ return selectedSrc;
+};
+
+}( jQuery ) );
diff --git a/resources/jquery/jquery.highlightText.js b/resources/jquery/jquery.highlightText.js
index f720e07f..cda2765b 100644
--- a/resources/jquery/jquery.highlightText.js
+++ b/resources/jquery/jquery.highlightText.js
@@ -29,7 +29,7 @@
// non latin characters can make regex think a new word has begun: do not use \b
// http://stackoverflow.com/questions/3787072/regex-wordwrap-with-utf8-characters-in-js
// look for an occurrence of our pattern and store the starting position
- match = node.data.match( new RegExp( "(^|\\s)" + $.escapeRE( pat ), "i" ) );
+ match = node.data.match( new RegExp( '(^|\\s)' + $.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
diff --git a/resources/jquery/jquery.jStorage.js b/resources/jquery/jquery.jStorage.js
index 95959cf7..6ca21b5c 100644
--- a/resources/jquery/jquery.jStorage.js
+++ b/resources/jquery/jquery.jStorage.js
@@ -3,12 +3,9 @@
* Simple local storage wrapper to save data on the browser side, supporting
* all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
*
- * Copyright (c) 2010 Andris Reinman, andris.reinman@gmail.com
+ * Copyright (c) 2010 - 2012 Andris Reinman, andris.reinman@gmail.com
* Project homepage: www.jstorage.info
*
- * Taken from Github with slight modifications by Hoo man
- * https://raw.github.com/andris9/jStorage/master/jstorage.js
- *
* Licensed under MIT-style license:
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -27,52 +24,30 @@
* SOFTWARE.
*/
-/**
- * $.jStorage
- *
- * USAGE:
- *
- * jStorage requires Prototype, MooTools or jQuery! If jQuery is used, then
- * jQuery-JSON (http://code.google.com/p/jquery-json/) is also needed.
- * (jQuery-JSON needs to be loaded BEFORE jStorage!)
- *
- * Methods:
- *
- * -set(key, value[, options])
- * $.jStorage.set(key, value) -> saves a value
- *
- * -get(key[, default])
- * value = $.jStorage.get(key [, default]) ->
- * retrieves value if key exists, or default if it doesn't
- *
- * -deleteKey(key)
- * $.jStorage.deleteKey(key) -> removes a key from the storage
- *
- * -flush()
- * $.jStorage.flush() -> clears the cache
- *
- * -storageObj()
- * $.jStorage.storageObj() -> returns a read-ony copy of the actual storage
- *
- * -storageSize()
- * $.jStorage.storageSize() -> returns the size of the storage in bytes
- *
- * -index()
- * $.jStorage.index() -> returns the used keys as an array
- *
- * -storageAvailable()
- * $.jStorage.storageAvailable() -> returns true if storage is available
- *
- * -reInit()
- * $.jStorage.reInit() -> reloads the data from browser storage
- *
- * <value> can be any JSON-able value, including objects and arrays.
- *
- **/
+ (function(){
+ var
+ /* jStorage version */
+ JSTORAGE_VERSION = "0.3.0",
+
+ /* detect a dollar object or create one if not found */
+ $ = window.jQuery || window.$ || (window.$ = {}),
-(function($){
- if(!$ || !($.toJSON || Object.toJSON || window.JSON)){
- throw new Error("jQuery, MooTools or Prototype needs to be loaded before jStorage!");
+ /* check for a JSON handling support */
+ JSON = {
+ parse:
+ window.JSON && (window.JSON.parse || window.JSON.decode) ||
+ String.prototype.evalJSON && function(str){return String(str).evalJSON();} ||
+ $.parseJSON ||
+ $.evalJSON,
+ stringify:
+ Object.toJSON ||
+ window.JSON && (window.JSON.stringify || window.JSON.encode) ||
+ $.toJSON
+ };
+
+ // Break if no JSON support was found
+ if(!JSON.parse || !JSON.stringify){
+ throw new Error("No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page");
}
var
@@ -88,20 +63,58 @@
/* How much space does the storage take */
_storage_size = 0,
- /* function to encode objects to JSON strings */
- json_encode = $.toJSON || Object.toJSON || (window.JSON && (JSON.encode || JSON.stringify)),
-
- /* function to decode objects from JSON strings */
- json_decode = $.evalJSON || (window.JSON && (JSON.decode || JSON.parse)) || function(str){
- return String(str).evalJSON();
- },
-
/* which backend is currently used */
_backend = false,
+ /* onchange observers */
+ _observers = {},
+
+ /* timeout to wait after onchange event */
+ _observer_timeout = false,
+
+ /* last update time */
+ _observer_update = 0,
+
+ /* pubsub observers */
+ _pubsub_observers = {},
+
+ /* skip published items older than current timestamp */
+ _pubsub_last = +new Date(),
+
/* Next check for TTL */
_ttl_timeout,
+ /* crc32 table */
+ _crc32Table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 "+
+ "0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 "+
+ "6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 "+
+ "FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 "+
+ "A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 "+
+ "32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 "+
+ "56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 "+
+ "C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 "+
+ "E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 "+
+ "6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 "+
+ "12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE "+
+ "A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 "+
+ "DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 "+
+ "5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 "+
+ "2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF "+
+ "04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 "+
+ "7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 "+
+ "FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 "+
+ "A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C "+
+ "36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 "+
+ "5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 "+
+ "C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 "+
+ "EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D "+
+ "7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 "+
+ "18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 "+
+ "A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A "+
+ "D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A "+
+ "53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 "+
+ "2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D",
+
/**
* XML encoding and decoding as XML nodes can't be JSON'ized
* XML nodes are encoded and decoded if the node is the value to be saved
@@ -158,14 +171,16 @@
resultXML = dom_parser.call("DOMParser" in window && (new DOMParser()) || window, xmlString, 'text/xml');
return this.isXML(resultXML)?resultXML:false;
}
- };
+ },
+
+ _localStoragePolyfillSetKey = function(){};
+
////////////////////////// PRIVATE METHODS ////////////////////////
/**
* Initialization function. Detects if the browser supports DOM Storage
* or userData behavior and behaves accordingly.
- * @returns undefined
*/
function _init(){
/* Check if browser supports localStorage */
@@ -180,11 +195,13 @@
// QUOTA_EXCEEDED_ERRROR DOM Exception 22.
}
}
+
if(localStorageReallyWorks){
try {
if(window.localStorage) {
_storage_service = window.localStorage;
_backend = "localStorage";
+ _observer_update = _storage_service.jStorage_update;
}
} catch(E3) {/* Firefox fails when touching localStorage and cookies are disabled */}
}
@@ -194,6 +211,7 @@
if(window.globalStorage) {
_storage_service = window.globalStorage[window.location.hostname];
_backend = "globalStorage";
+ _observer_update = _storage_service.jStorage_update;
}
} catch(E4) {/* Firefox fails when touching localStorage and cookies are disabled */}
}
@@ -208,11 +226,24 @@
/* userData element needs to be inserted into the DOM! */
document.getElementsByTagName('head')[0].appendChild(_storage_elm);
- _storage_elm.load("jStorage");
+ try{
+ _storage_elm.load("jStorage");
+ }catch(E){
+ // try to reset cache
+ _storage_elm.setAttribute("jStorage", "{}");
+ _storage_elm.save("jStorage");
+ _storage_elm.load("jStorage");
+ }
+
var data = "{}";
try{
data = _storage_elm.getAttribute("jStorage");
}catch(E5){}
+
+ try{
+ _observer_update = _storage_elm.getAttribute("jStorage_update");
+ }catch(E6){}
+
_storage_service.jStorage = data;
_backend = "userDataBehavior";
}else{
@@ -221,35 +252,427 @@
}
}
+ // Load data from storage
+ _load_storage();
+
+ // remove dead keys
+ _handleTTL();
+
+ // create localStorage and sessionStorage polyfills if needed
+ _createPolyfillStorage("local");
+ _createPolyfillStorage("session");
+
+ // start listening for changes
+ _setupObserver();
+
+ // initialize publish-subscribe service
+ _handlePubSub();
+
+ // handle cached navigation
+ if("addEventListener" in window){
+ window.addEventListener("pageshow", function(event){
+ if(event.persisted){
+ _storageObserver();
+ }
+ }, false);
+ }
+ }
+
+ /**
+ * Create a polyfill for localStorage (type="local") or sessionStorage (type="session")
+ *
+ * @param {String} type Either "local" or "session"
+ * @param {Boolean} forceCreate If set to true, recreate the polyfill (needed with flush)
+ */
+ function _createPolyfillStorage(type, forceCreate){
+ var _skipSave = false,
+ _length = 0,
+ i,
+ storage,
+ storage_source = {};
+
+ var rand = Math.random();
+
+ if(!forceCreate && typeof window[type+"Storage"] != "undefined"){
+ return;
+ }
+
+ // Use globalStorage for localStorage if available
+ if(type == "local" && window.globalStorage){
+ localStorage = window.globalStorage[window.location.hostname];
+ return;
+ }
+
+ // only IE6/7 from this point on
+ if(_backend != "userDataBehavior"){
+ return;
+ }
+
+ // Remove existing storage element if available
+ if(forceCreate && window[type+"Storage"] && window[type+"Storage"].parentNode){
+ window[type+"Storage"].parentNode.removeChild(window[type+"Storage"]);
+ }
+
+ storage = document.createElement("button");
+ document.getElementsByTagName('head')[0].appendChild(storage);
+
+ if(type == "local"){
+ storage_source = _storage;
+ }else if(type == "session"){
+ _sessionStoragePolyfillUpdate();
+ }
+
+ for(i in storage_source){
+
+ if(storage_source.hasOwnProperty(i) && i != "__jstorage_meta" && i != "length" && typeof storage_source[i] != "undefined"){
+ if(!(i in storage)){
+ _length++;
+ }
+ storage[i] = storage_source[i];
+ }
+ }
+
+ // Polyfill API
+
+ /**
+ * Indicates how many keys are stored in the storage
+ */
+ storage.length = _length;
+
+ /**
+ * Returns the key of the nth stored value
+ *
+ * @param {Number} n Index position
+ * @return {String} Key name of the nth stored value
+ */
+ storage.key = function(n){
+ var count = 0, i;
+ _sessionStoragePolyfillUpdate();
+ for(i in storage_source){
+ if(storage_source.hasOwnProperty(i) && i != "__jstorage_meta" && i!="length" && typeof storage_source[i] != "undefined"){
+ if(count == n){
+ return i;
+ }
+ count++;
+ }
+ }
+ }
+
+ /**
+ * Returns the current value associated with the given key
+ *
+ * @param {String} key key name
+ * @return {Mixed} Stored value
+ */
+ storage.getItem = function(key){
+ _sessionStoragePolyfillUpdate();
+ if(type == "session"){
+ return storage_source[key];
+ }
+ return $.jStorage.get(key);
+ }
+
+ /**
+ * Sets or updates value for a give key
+ *
+ * @param {String} key Key name to be updated
+ * @param {String} value String value to be stored
+ */
+ storage.setItem = function(key, value){
+ if(typeof value == "undefined"){
+ return;
+ }
+ storage[key] = (value || "").toString();
+ }
+
+ /**
+ * Removes key from the storage
+ *
+ * @param {String} key Key name to be removed
+ */
+ storage.removeItem = function(key){
+ if(type == "local"){
+ return $.jStorage.deleteKey(key);
+ }
+
+ storage[key] = undefined;
+
+ _skipSave = true;
+ if(key in storage){
+ storage.removeAttribute(key);
+ }
+ _skipSave = false;
+ }
+
+ /**
+ * Clear storage
+ */
+ storage.clear = function(){
+ if(type == "session"){
+ window.name = "";
+ _createPolyfillStorage("session", true);
+ return;
+ }
+ $.jStorage.flush();
+ }
+
+ if(type == "local"){
+
+ _localStoragePolyfillSetKey = function(key, value){
+ if(key == "length"){
+ return;
+ }
+ _skipSave = true;
+ if(typeof value == "undefined"){
+ if(key in storage){
+ _length--;
+ storage.removeAttribute(key);
+ }
+ }else{
+ if(!(key in storage)){
+ _length++;
+ }
+ storage[key] = (value || "").toString();
+ }
+ storage.length = _length;
+ _skipSave = false;
+ }
+ }
+
+ function _sessionStoragePolyfillUpdate(){
+ if(type != "session"){
+ return;
+ }
+ try{
+ storage_source = JSON.parse(window.name || "{}");
+ }catch(E){
+ storage_source = {};
+ }
+ }
+
+ function _sessionStoragePolyfillSave(){
+ if(type != "session"){
+ return;
+ }
+ window.name = JSON.stringify(storage_source);
+ };
+
+ storage.attachEvent("onpropertychange", function(e){
+ if(e.propertyName == "length"){
+ return;
+ }
+
+ if(_skipSave || e.propertyName == "length"){
+ return;
+ }
+
+ if(type == "local"){
+ if(!(e.propertyName in storage_source) && typeof storage[e.propertyName] != "undefined"){
+ _length ++;
+ }
+ }else if(type == "session"){
+ _sessionStoragePolyfillUpdate();
+ if(typeof storage[e.propertyName] != "undefined" && !(e.propertyName in storage_source)){
+ storage_source[e.propertyName] = storage[e.propertyName];
+ _length++;
+ }else if(typeof storage[e.propertyName] == "undefined" && e.propertyName in storage_source){
+ delete storage_source[e.propertyName];
+ _length--;
+ }else{
+ storage_source[e.propertyName] = storage[e.propertyName];
+ }
+
+ _sessionStoragePolyfillSave();
+ storage.length = _length;
+ return;
+ }
+
+ $.jStorage.set(e.propertyName, storage[e.propertyName]);
+ storage.length = _length;
+ });
+
+ window[type+"Storage"] = storage;
+ }
+
+ /**
+ * Reload data from storage when needed
+ */
+ function _reloadData(){
+ var data = "{}";
+
+ if(_backend == "userDataBehavior"){
+ _storage_elm.load("jStorage");
+
+ try{
+ data = _storage_elm.getAttribute("jStorage");
+ }catch(E5){}
+
+ try{
+ _observer_update = _storage_elm.getAttribute("jStorage_update");
+ }catch(E6){}
+
+ _storage_service.jStorage = data;
+ }
+
_load_storage();
// remove dead keys
_handleTTL();
+
+ _handlePubSub();
+ }
+
+ /**
+ * Sets up a storage change observer
+ */
+ function _setupObserver(){
+ if(_backend == "localStorage" || _backend == "globalStorage"){
+ if("addEventListener" in window){
+ window.addEventListener("storage", _storageObserver, false);
+ }else{
+ document.attachEvent("onstorage", _storageObserver);
+ }
+ }else if(_backend == "userDataBehavior"){
+ setInterval(_storageObserver, 1000);
+ }
+ }
+
+ /**
+ * Fired on any kind of data change, needs to check if anything has
+ * really been changed
+ */
+ function _storageObserver(){
+ var updateTime;
+ // cumulate change notifications with timeout
+ clearTimeout(_observer_timeout);
+ _observer_timeout = setTimeout(function(){
+
+ if(_backend == "localStorage" || _backend == "globalStorage"){
+ updateTime = _storage_service.jStorage_update;
+ }else if(_backend == "userDataBehavior"){
+ _storage_elm.load("jStorage");
+ try{
+ updateTime = _storage_elm.getAttribute("jStorage_update");
+ }catch(E5){}
+ }
+
+ if(updateTime && updateTime != _observer_update){
+ _observer_update = updateTime;
+ _checkUpdatedKeys();
+ }
+
+ }, 25);
+ }
+
+ /**
+ * Reloads the data and checks if any keys are changed
+ */
+ function _checkUpdatedKeys(){
+ var oldCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)),
+ newCrc32List;
+
+ _reloadData();
+ newCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32));
+
+ var key,
+ updated = [],
+ removed = [];
+
+ for(key in oldCrc32List){
+ if(oldCrc32List.hasOwnProperty(key)){
+ if(!newCrc32List[key]){
+ removed.push(key);
+ continue;
+ }
+ if(oldCrc32List[key] != newCrc32List[key]){
+ updated.push(key);
+ }
+ }
+ }
+
+ for(key in newCrc32List){
+ if(newCrc32List.hasOwnProperty(key)){
+ if(!oldCrc32List[key]){
+ updated.push(key);
+ }
+ }
+ }
+
+ _fireObservers(updated, "updated");
+ _fireObservers(removed, "deleted");
+ }
+
+ /**
+ * Fires observers for updated keys
+ *
+ * @param {Array|String} keys Array of key names or a key
+ * @param {String} action What happened with the value (updated, deleted, flushed)
+ */
+ function _fireObservers(keys, action){
+ keys = [].concat(keys || []);
+ if(action == "flushed"){
+ keys = [];
+ for(var key in _observers){
+ if(_observers.hasOwnProperty(key)){
+ keys.push(key);
+ }
+ }
+ action = "deleted";
+ }
+ for(var i=0, len = keys.length; i<len; i++){
+ if(_observers[keys[i]]){
+ for(var j=0, jlen = _observers[keys[i]].length; j<jlen; j++){
+ _observers[keys[i]][j](keys[i], action);
+ }
+ }
+ }
+ }
+
+ /**
+ * Publishes key change to listeners
+ */
+ function _publishChange(){
+ var updateTime = (+new Date()).toString();
+
+ if(_backend == "localStorage" || _backend == "globalStorage"){
+ _storage_service.jStorage_update = updateTime;
+ }else if(_backend == "userDataBehavior"){
+ _storage_elm.setAttribute("jStorage_update", updateTime);
+ _storage_elm.save("jStorage");
+ }
+
+ _storageObserver();
}
/**
* Loads the data from the storage based on the supported mechanism
- * @returns undefined
*/
function _load_storage(){
/* if jStorage string is retrieved, then decode it */
if(_storage_service.jStorage){
try{
- _storage = json_decode(String(_storage_service.jStorage));
+ _storage = JSON.parse(String(_storage_service.jStorage));
}catch(E6){_storage_service.jStorage = "{}";}
}else{
_storage_service.jStorage = "{}";
}
_storage_size = _storage_service.jStorage?String(_storage_service.jStorage).length:0;
+
+ if(!_storage.__jstorage_meta){
+ _storage.__jstorage_meta = {};
+ }
+ if(!_storage.__jstorage_meta.CRC32){
+ _storage.__jstorage_meta.CRC32 = {};
+ }
}
/**
* This functions provides the "save" mechanism to store the jStorage object
- * @returns undefined
*/
function _save(){
+ _dropOldEvents(); // remove expired events
try{
- _storage_service.jStorage = json_encode(_storage);
+ _storage_service.jStorage = JSON.stringify(_storage);
// If userData is used as the storage engine, additional
if(_storage_elm) {
_storage_elm.setAttribute("jStorage",_storage_service.jStorage);
@@ -261,12 +684,14 @@
/**
* Function checks if a key is set and is string or numberic
+ *
+ * @param {String} key Key name
*/
function _checkKey(key){
- if(!key || (typeof key !== "string" && typeof key !== "number")){
+ if(!key || (typeof key != "string" && typeof key != "number")){
throw new TypeError('Key name must be string or numeric');
}
- if(key === "__jstorage_meta"){
+ if(key == "__jstorage_meta"){
throw new TypeError('Reserved key name');
}
return true;
@@ -276,23 +701,27 @@
* Removes expired keys
*/
function _handleTTL(){
- var curtime, i, TTL, nextExpire = Infinity, changed = false;
+ var curtime, i, TTL, CRC32, nextExpire = Infinity, changed = false, deleted = [];
clearTimeout(_ttl_timeout);
- if(!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL !== "object"){
+ if(!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != "object"){
// nothing to do here
return;
}
curtime = +new Date();
TTL = _storage.__jstorage_meta.TTL;
+
+ CRC32 = _storage.__jstorage_meta.CRC32;
for(i in TTL){
if(TTL.hasOwnProperty(i)){
if(TTL[i] <= curtime){
delete TTL[i];
+ delete CRC32[i];
delete _storage[i];
changed = true;
+ deleted.push(i);
}else if(TTL[i] < nextExpire){
nextExpire = TTL[i];
}
@@ -307,47 +736,158 @@
// save changes
if(changed){
_save();
+ _publishChange();
+ _fireObservers(deleted, "deleted");
}
}
+ /**
+ * Checks if there's any events on hold to be fired to listeners
+ */
+ function _handlePubSub(){
+ if(!_storage.__jstorage_meta.PubSub){
+ return;
+ }
+ var pubelm,
+ _pubsubCurrent = _pubsub_last;
+
+ for(var i=len=_storage.__jstorage_meta.PubSub.length-1; i>=0; i--){
+ pubelm = _storage.__jstorage_meta.PubSub[i];
+ if(pubelm[0] > _pubsub_last){
+ _pubsubCurrent = pubelm[0];
+ _fireSubscribers(pubelm[1], pubelm[2]);
+ }
+ }
+
+ _pubsub_last = _pubsubCurrent;
+ }
+
+ /**
+ * Fires all subscriber listeners for a pubsub channel
+ *
+ * @param {String} channel Channel name
+ * @param {Mixed} payload Payload data to deliver
+ */
+ function _fireSubscribers(channel, payload){
+ if(_pubsub_observers[channel]){
+ for(var i=0, len = _pubsub_observers[channel].length; i<len; i++){
+ // send immutable data that can't be modified by listeners
+ _pubsub_observers[channel][i](channel, JSON.parse(JSON.stringify(payload)));
+ }
+ }
+ }
+
+ /**
+ * Remove old events from the publish stream (at least 2sec old)
+ */
+ function _dropOldEvents(){
+ if(!_storage.__jstorage_meta.PubSub){
+ return;
+ }
+
+ var retire = +new Date() - 2000;
+
+ for(var i=0, len = _storage.__jstorage_meta.PubSub.length; i<len; i++){
+ if(_storage.__jstorage_meta.PubSub[i][0] <= retire){
+ // deleteCount is needed for IE6
+ _storage.__jstorage_meta.PubSub.splice(i, _storage.__jstorage_meta.PubSub.length - i);
+ break;
+ }
+ }
+
+ if(!_storage.__jstorage_meta.PubSub.length){
+ delete _storage.__jstorage_meta.PubSub;
+ }
+
+ }
+
+ /**
+ * Publish payload to a channel
+ *
+ * @param {String} channel Channel name
+ * @param {Mixed} payload Payload to send to the subscribers
+ */
+ function _publish(channel, payload){
+ if(!_storage.__jstorage_meta){
+ _storage.__jstorage_meta = {};
+ }
+ if(!_storage.__jstorage_meta.PubSub){
+ _storage.__jstorage_meta.PubSub = [];
+ }
+
+ _storage.__jstorage_meta.PubSub.unshift([+new Date, channel, payload]);
+
+ _save();
+ _publishChange();
+ }
+
+ /**
+ * CRC32 calculation based on http://noteslog.com/post/crc32-for-javascript/
+ *
+ * @param {String} str String to be hashed
+ * @param {Number} [crc] Last crc value in case of streams
+ */
+ function _crc32(str, crc){
+ crc = crc || 0;
+
+ var n = 0, //a number between 0 and 255
+ x = 0; //an hex number
+
+ crc = crc ^ (-1);
+ for(var i = 0, len = str.length; i < len; i++){
+ n = (crc ^ str.charCodeAt(i)) & 0xFF;
+ x = "0x" + _crc32Table.substr(n * 9, 8);
+ crc = (crc >>> 8)^x;
+ }
+ return crc^(-1);
+ }
+
////////////////////////// PUBLIC INTERFACE /////////////////////////
$.jStorage = {
/* Version number */
- version: "0.1.7.0",
+ version: JSTORAGE_VERSION,
/**
* Sets a key's value.
*
- * @param {String} key - Key to set. If this value is not set or not
+ * @param {String} key Key to set. If this value is not set or not
* a string an exception is raised.
- * @param {Mixed} value - Value to set. This can be any value that is JSON
+ * @param {Mixed} value Value to set. This can be any value that is JSON
* compatible (Numbers, Strings, Objects etc.).
* @param {Object} [options] - possible options to use
* @param {Number} [options.TTL] - optional TTL value
- * @returns the used value
+ * @return {Mixed} the used value
*/
set: function(key, value, options){
_checkKey(key);
options = options || {};
+ // undefined values are deleted automatically
+ if(typeof value == "undefined"){
+ this.deleteKey(key);
+ return value;
+ }
+
if(_XMLService.isXML(value)){
value = {_is_xml:true,xml:_XMLService.encode(value)};
- }else if(typeof value === "function"){
- value = null; // functions can't be saved!
- }else if(value && typeof value === "object"){
+ }else if(typeof value == "function"){
+ return undefined; // functions can't be saved!
+ }else if(value && typeof value == "object"){
// clone the object before saving to _storage tree
- value = json_decode(json_encode(value));
+ value = JSON.parse(JSON.stringify(value));
}
+
_storage[key] = value;
- if(!isNaN(options.TTL)){
- this.setTTL(key, options.TTL);
- // also handles saving
- }else{
- _save();
- }
+ _storage.__jstorage_meta.CRC32[key] = _crc32(JSON.stringify(value));
+
+ this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange
+
+ _localStoragePolyfillSetKey(key, value);
+
+ _fireObservers(key, "updated");
return value;
},
@@ -356,12 +896,12 @@
*
* @param {String} key - Key to look up.
* @param {mixed} def - Default value to return, if key didn't exist.
- * @returns the key value, default value or <null>
+ * @return {Mixed} the key value, default value or null
*/
get: function(key, def){
_checkKey(key);
if(key in _storage){
- if(_storage[key] && typeof _storage[key] === "object" &&
+ if(_storage[key] && typeof _storage[key] == "object" &&
_storage[key]._is_xml &&
_storage[key]._is_xml){
return _XMLService.decode(_storage[key].xml);
@@ -369,26 +909,31 @@
return _storage[key];
}
}
- return typeof(def) === 'undefined' ? null : def;
+ return typeof(def) == 'undefined' ? null : def;
},
/**
* Deletes a key from cache.
*
* @param {String} key - Key to delete.
- * @returns true if key existed or false if it didn't
+ * @return {Boolean} true if key existed or false if it didn't
*/
deleteKey: function(key){
_checkKey(key);
if(key in _storage){
delete _storage[key];
// remove from TTL list
- if(_storage.__jstorage_meta &&
- typeof _storage.__jstorage_meta.TTL === "object" &&
+ if(typeof _storage.__jstorage_meta.TTL == "object" &&
key in _storage.__jstorage_meta.TTL){
delete _storage.__jstorage_meta.TTL[key];
}
+
+ delete _storage.__jstorage_meta.CRC32[key];
+ _localStoragePolyfillSetKey(key, undefined);
+
_save();
+ _publishChange();
+ _fireObservers(key, "deleted");
return true;
}
return false;
@@ -399,7 +944,7 @@
*
* @param {String} key - key to set the TTL for
* @param {Number} ttl - TTL timeout in milliseconds
- * @returns true if key existed or false if it didn't
+ * @return {Boolean} true if key existed or false if it didn't
*/
setTTL: function(key, ttl){
var curtime = +new Date();
@@ -407,9 +952,6 @@
ttl = Number(ttl) || 0;
if(key in _storage){
- if(!_storage.__jstorage_meta){
- _storage.__jstorage_meta = {};
- }
if(!_storage.__jstorage_meta.TTL){
_storage.__jstorage_meta.TTL = {};
}
@@ -424,26 +966,47 @@
_save();
_handleTTL();
+
+ _publishChange();
return true;
}
return false;
},
/**
+ * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set
+ *
+ * @param {String} key Key to check
+ * @return {Number} Remaining TTL in milliseconds
+ */
+ getTTL: function(key){
+ var curtime = +new Date(), ttl;
+ _checkKey(key);
+ if(key in _storage && _storage.__jstorage_meta.TTL && _storage.__jstorage_meta.TTL[key]){
+ ttl = _storage.__jstorage_meta.TTL[key] - curtime;
+ return ttl || 0;
+ }
+ return 0;
+ },
+
+ /**
* Deletes everything in cache.
*
- * @return true
+ * @return {Boolean} Always true
*/
flush: function(){
- _storage = {};
+ _storage = {__jstorage_meta:{CRC32:{}}};
+ _createPolyfillStorage("local", true);
_save();
+ _publishChange();
+ _fireObservers(null, "flushed");
return true;
},
/**
* Returns a read-only copy of _storage
*
- * @returns Object
+ * @return {Object} Read-only copy of _storage
*/
storageObj: function(){
function F() {}
@@ -455,12 +1018,12 @@
* Returns an index of all used keys as an array
* ['key1', 'key2',..'keyN']
*
- * @returns Array
+ * @return {Array} Used keys
*/
index: function(){
var index = [], i;
for(i in _storage){
- if(_storage.hasOwnProperty(i) && i !== "__jstorage_meta"){
+ if(_storage.hasOwnProperty(i) && i != "__jstorage_meta"){
index.push(i);
}
}
@@ -470,7 +1033,8 @@
/**
* How much space in bytes does the storage take?
*
- * @returns Number
+ * @return {Number} Storage size in chars (not the same as in bytes,
+ * since some chars may take several bytes)
*/
storageSize: function(){
return _storage_size;
@@ -479,7 +1043,7 @@
/**
* Which backend is currently in use?
*
- * @returns String
+ * @return {String} Backend name
*/
currentBackend: function(){
return _backend;
@@ -488,45 +1052,92 @@
/**
* Test if storage is available
*
- * @returns Boolean
+ * @return {Boolean} True if storage can be used
*/
storageAvailable: function(){
return !!_backend;
},
/**
- * Reloads the data from browser storage
+ * Register change listeners
*
- * @returns undefined
+ * @param {String} key Key name
+ * @param {Function} callback Function to run when the key changes
*/
- reInit: function(){
- var new_storage_elm, data;
- if(_storage_elm && _storage_elm.addBehavior){
- new_storage_elm = document.createElement('link');
+ listenKeyChange: function(key, callback){
+ _checkKey(key);
+ if(!_observers[key]){
+ _observers[key] = [];
+ }
+ _observers[key].push(callback);
+ },
- _storage_elm.parentNode.replaceChild(new_storage_elm, _storage_elm);
- _storage_elm = new_storage_elm;
+ /**
+ * Remove change listeners
+ *
+ * @param {String} key Key name to unregister listeners against
+ * @param {Function} [callback] If set, unregister the callback, if not - unregister all
+ */
+ stopListening: function(key, callback){
+ _checkKey(key);
- /* Use a DOM element to act as userData storage */
- _storage_elm.style.behavior = 'url(#default#userData)';
+ if(!_observers[key]){
+ return;
+ }
- /* userData element needs to be inserted into the DOM! */
- document.getElementsByTagName('head')[0].appendChild(_storage_elm);
+ if(!callback){
+ delete _observers[key];
+ return;
+ }
- _storage_elm.load("jStorage");
- data = "{}";
- try{
- data = _storage_elm.getAttribute("jStorage");
- }catch(E5){}
- _storage_service.jStorage = data;
- _backend = "userDataBehavior";
+ for(var i = _observers[key].length - 1; i>=0; i--){
+ if(_observers[key][i] == callback){
+ _observers[key].splice(i,1);
+ }
}
+ },
+
+ /**
+ * Subscribe to a Publish/Subscribe event stream
+ *
+ * @param {String} channel Channel name
+ * @param {Function} callback Function to run when the something is published to the channel
+ */
+ subscribe: function(channel, callback){
+ channel = (channel || "").toString();
+ if(!channel){
+ throw new TypeError('Channel not defined');
+ }
+ if(!_pubsub_observers[channel]){
+ _pubsub_observers[channel] = [];
+ }
+ _pubsub_observers[channel].push(callback);
+ },
- _load_storage();
+ /**
+ * Publish data to an event stream
+ *
+ * @param {String} channel Channel name
+ * @param {Mixed} payload Payload to deliver
+ */
+ publish: function(channel, payload){
+ channel = (channel || "").toString();
+ if(!channel){
+ throw new TypeError('Channel not defined');
+ }
+
+ _publish(channel, payload);
+ },
+
+ /**
+ * Reloads the data from browser storage
+ */
+ reInit: function(){
+ _reloadData();
}
};
// Initialize jStorage
_init();
-})(window.$ || window.jQuery);
+})();
diff --git a/resources/jquery/jquery.js b/resources/jquery/jquery.js
index d4f3bb38..a86bf797 100644
--- a/resources/jquery/jquery.js
+++ b/resources/jquery/jquery.js
@@ -1,5 +1,5 @@
/*!
- * jQuery JavaScript Library v1.8.2
+ * jQuery JavaScript Library v1.8.3
* http://jquery.com/
*
* Includes Sizzle.js
@@ -9,7 +9,7 @@
* Released under the MIT license
* http://jquery.org/license
*
- * Date: Thu Sep 20 2012 21:13:05 GMT-0400 (Eastern Daylight Time)
+ * Date: Tue Nov 13 2012 08:20:33 GMT-0500 (Eastern Standard Time)
*/
(function( window, undefined ) {
var
@@ -186,7 +186,7 @@ jQuery.fn = jQuery.prototype = {
selector: "",
// The current version of jQuery being used
- jquery: "1.8.2",
+ jquery: "1.8.3",
// The default length of a jQuery object is 0
length: 0,
@@ -999,8 +999,10 @@ jQuery.Callbacks = function( options ) {
(function add( args ) {
jQuery.each( args, function( _, arg ) {
var type = jQuery.type( arg );
- if ( type === "function" && ( !options.unique || !self.has( arg ) ) ) {
- list.push( arg );
+ if ( type === "function" ) {
+ if ( !options.unique || !self.has( arg ) ) {
+ list.push( arg );
+ }
} else if ( arg && arg.length && type !== "string" ) {
// Inspect recursively
add( arg );
@@ -1253,24 +1255,23 @@ jQuery.support = (function() {
clickFn,
div = document.createElement("div");
- // Preliminary tests
+ // Setup
div.setAttribute( "className", "t" );
div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>";
+ // Support tests won't run in some limited or non-browser environments
all = div.getElementsByTagName("*");
a = div.getElementsByTagName("a")[ 0 ];
- a.style.cssText = "top:1px;float:left;opacity:.5";
-
- // Can't get basic test support
- if ( !all || !all.length ) {
+ if ( !all || !a || !all.length ) {
return {};
}
- // First batch of supports tests
+ // First batch of tests
select = document.createElement("select");
opt = select.appendChild( document.createElement("option") );
input = div.getElementsByTagName("input")[ 0 ];
+ a.style.cssText = "top:1px;float:left;opacity:.5";
support = {
// IE strips leading whitespace when .innerHTML is used
leadingWhitespace: ( div.firstChild.nodeType === 3 ),
@@ -1312,7 +1313,7 @@ jQuery.support = (function() {
// Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
getSetAttribute: div.className !== "t",
- // Tests for enctype support on a form(#6743)
+ // Tests for enctype support on a form (#6743)
enctype: !!document.createElement("form").enctype,
// Makes sure cloning an html5 element does not cause problems
@@ -2217,26 +2218,25 @@ jQuery.extend({
},
select: {
get: function( elem ) {
- var value, i, max, option,
- index = elem.selectedIndex,
- values = [],
+ var value, option,
options = elem.options,
- one = elem.type === "select-one";
-
- // Nothing was selected
- if ( index < 0 ) {
- return null;
- }
+ index = elem.selectedIndex,
+ one = elem.type === "select-one" || index < 0,
+ values = one ? null : [],
+ max = one ? index + 1 : options.length,
+ i = index < 0 ?
+ max :
+ one ? index : 0;
// Loop through all the selected options
- i = one ? index : 0;
- max = one ? index + 1 : options.length;
for ( ; i < max; i++ ) {
option = options[ i ];
- // Don't return options that are disabled or in a disabled optgroup
- if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) &&
- (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) {
+ // oldIE doesn't update selected after form reset (#2551)
+ if ( ( option.selected || i === index ) &&
+ // Don't return options that are disabled or in a disabled optgroup
+ ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) &&
+ ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {
// Get the specific value for the option
value = jQuery( option ).val();
@@ -2251,11 +2251,6 @@ jQuery.extend({
}
}
- // Fixes Bug #2551 -- select.val() broken in IE after form.reset()
- if ( one && !values.length && options.length ) {
- return jQuery( options[ index ] ).val();
- }
-
return values;
},
@@ -3233,7 +3228,7 @@ jQuery.removeEvent = document.removeEventListener ?
if ( elem.detachEvent ) {
- // #8545, #7054, preventing memory leaks for custom events in IE6-8 –
+ // #8545, #7054, preventing memory leaks for custom events in IE6-8
// detachEvent needed property on element, by name of that event, to properly expose it to GC
if ( typeof elem[ name ] === "undefined" ) {
elem[ name ] = null;
@@ -3725,7 +3720,8 @@ var cachedruns,
delete cache[ keys.shift() ];
}
- return (cache[ key ] = value);
+ // Retrieve with (key + " ") to avoid collision with native Object.prototype properties (see Issue #157)
+ return (cache[ key + " " ] = value);
}, cache );
},
@@ -4259,13 +4255,13 @@ Expr = Sizzle.selectors = {
},
"CLASS": function( className ) {
- var pattern = classCache[ expando ][ className ];
- if ( !pattern ) {
- pattern = classCache( className, new RegExp("(^|" + whitespace + ")" + className + "(" + whitespace + "|$)") );
- }
- return function( elem ) {
- return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" );
- };
+ var pattern = classCache[ expando ][ className + " " ];
+
+ return pattern ||
+ (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
+ classCache( className, function( elem ) {
+ return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" );
+ });
},
"ATTR": function( name, operator, check ) {
@@ -4511,7 +4507,7 @@ Expr = Sizzle.selectors = {
"focus": function( elem ) {
var doc = elem.ownerDocument;
- return elem === doc.activeElement && (!doc.hasFocus || doc.hasFocus()) && !!(elem.type || elem.href);
+ return elem === doc.activeElement && (!doc.hasFocus || doc.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
},
"active": function( elem ) {
@@ -4519,11 +4515,11 @@ Expr = Sizzle.selectors = {
},
// Positional types
- "first": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ "first": createPositionalPseudo(function() {
return [ 0 ];
}),
- "last": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ "last": createPositionalPseudo(function( matchIndexes, length ) {
return [ length - 1 ];
}),
@@ -4531,14 +4527,14 @@ Expr = Sizzle.selectors = {
return [ argument < 0 ? argument + length : argument ];
}),
- "even": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ "even": createPositionalPseudo(function( matchIndexes, length ) {
for ( var i = 0; i < length; i += 2 ) {
matchIndexes.push( i );
}
return matchIndexes;
}),
- "odd": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ "odd": createPositionalPseudo(function( matchIndexes, length ) {
for ( var i = 1; i < length; i += 2 ) {
matchIndexes.push( i );
}
@@ -4659,7 +4655,9 @@ baseHasDuplicate = !hasDuplicate;
// Document sorting and removing duplicates
Sizzle.uniqueSort = function( results ) {
var elem,
- i = 1;
+ duplicates = [],
+ i = 1,
+ j = 0;
hasDuplicate = baseHasDuplicate;
results.sort( sortOrder );
@@ -4667,9 +4665,12 @@ Sizzle.uniqueSort = function( results ) {
if ( hasDuplicate ) {
for ( ; (elem = results[i]); i++ ) {
if ( elem === results[ i - 1 ] ) {
- results.splice( i--, 1 );
+ j = duplicates.push( i );
}
}
+ while ( j-- ) {
+ results.splice( duplicates[ j ], 1 );
+ }
}
return results;
@@ -4680,8 +4681,9 @@ Sizzle.error = function( msg ) {
};
function tokenize( selector, parseOnly ) {
- var matched, match, tokens, type, soFar, groups, preFilters,
- cached = tokenCache[ expando ][ selector ];
+ var matched, match, tokens, type,
+ soFar, groups, preFilters,
+ cached = tokenCache[ expando ][ selector + " " ];
if ( cached ) {
return parseOnly ? 0 : cached.slice( 0 );
@@ -4696,7 +4698,8 @@ function tokenize( selector, parseOnly ) {
// Comma and first run
if ( !matched || (match = rcomma.exec( soFar )) ) {
if ( match ) {
- soFar = soFar.slice( match[0].length );
+ // Don't consume trailing commas as valid
+ soFar = soFar.slice( match[0].length ) || soFar;
}
groups.push( tokens = [] );
}
@@ -4715,8 +4718,7 @@ function tokenize( selector, parseOnly ) {
// Filters
for ( type in Expr.filter ) {
if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
- // The last two arguments here are (context, xml) for backCompat
- (match = preFilters[ type ]( match, document, true ))) ) {
+ (match = preFilters[ type ]( match ))) ) {
tokens.push( matched = new Token( match.shift() ) );
soFar = soFar.slice( matched.length );
@@ -4836,18 +4838,13 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS
postFinder = setMatcher( postFinder, postSelector );
}
return markFunction(function( seed, results, context, xml ) {
- // Positional selectors apply to seed elements, so it is invalid to follow them with relative ones
- if ( seed && postFinder ) {
- return;
- }
-
- var i, elem, postFilterIn,
+ var temp, i, elem,
preMap = [],
postMap = [],
preexisting = results.length,
// Get initial elements from seed or context
- elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [], seed ),
+ elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
// Prefilter to get matcher input, preserving a map for seed-results synchronization
matcherIn = preFilter && ( seed || !selector ) ?
@@ -4872,27 +4869,45 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS
// Apply postFilter
if ( postFilter ) {
- postFilterIn = condense( matcherOut, postMap );
- postFilter( postFilterIn, [], context, xml );
+ temp = condense( matcherOut, postMap );
+ postFilter( temp, [], context, xml );
// Un-match failing elements by moving them back to matcherIn
- i = postFilterIn.length;
+ i = temp.length;
while ( i-- ) {
- if ( (elem = postFilterIn[i]) ) {
+ if ( (elem = temp[i]) ) {
matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
}
}
}
- // Keep seed and results synchronized
if ( seed ) {
- // Ignore postFinder because it can't coexist with seed
- i = preFilter && matcherOut.length;
- while ( i-- ) {
- if ( (elem = matcherOut[i]) ) {
- seed[ preMap[i] ] = !(results[ preMap[i] ] = elem);
+ if ( postFinder || preFilter ) {
+ if ( postFinder ) {
+ // Get the final matcherOut by condensing this intermediate into postFinder contexts
+ temp = [];
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) ) {
+ // Restore matcherIn since elem is not yet a final match
+ temp.push( (matcherIn[i] = elem) );
+ }
+ }
+ postFinder( null, (matcherOut = []), temp, xml );
+ }
+
+ // Move matched elements from seed to results to keep them synchronized
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) &&
+ (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {
+
+ seed[temp] = !(results[temp] = elem);
+ }
}
}
+
+ // Add elements to results, through postFinder if defined
} else {
matcherOut = condense(
matcherOut === results ?
@@ -4933,7 +4948,6 @@ function matcherFromTokens( tokens ) {
if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ];
} else {
- // The concatenated values are (context, xml) for backCompat
matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
// Return special upon seeing a positional matcher
@@ -5062,7 +5076,7 @@ compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) {
var i,
setMatchers = [],
elementMatchers = [],
- cached = compilerCache[ expando ][ selector ];
+ cached = compilerCache[ expando ][ selector + " " ];
if ( !cached ) {
// Generate a function of recursive functions that can be used to check each element
@@ -5085,11 +5099,11 @@ compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) {
return cached;
};
-function multipleContexts( selector, contexts, results, seed ) {
+function multipleContexts( selector, contexts, results ) {
var i = 0,
len = contexts.length;
for ( ; i < len; i++ ) {
- Sizzle( selector, contexts[i], results, seed );
+ Sizzle( selector, contexts[i], results );
}
return results;
}
@@ -5167,15 +5181,14 @@ if ( document.querySelectorAll ) {
rescape = /'|\\/g,
rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,
- // qSa(:focus) reports false when true (Chrome 21),
+ // qSa(:focus) reports false when true (Chrome 21), no need to also add to buggyMatches since matches checks buggyQSA
// A support test would require too much code (would include document ready)
- rbuggyQSA = [":focus"],
+ rbuggyQSA = [ ":focus" ],
- // matchesSelector(:focus) reports false when true (Chrome 21),
// matchesSelector(:active) reports false when true (IE9/Opera 11.5)
// A support test would require too much code (would include document ready)
// just skip matchesSelector for :active
- rbuggyMatches = [ ":active", ":focus" ],
+ rbuggyMatches = [ ":active" ],
matches = docElem.matchesSelector ||
docElem.mozMatchesSelector ||
docElem.webkitMatchesSelector ||
@@ -5229,7 +5242,7 @@ if ( document.querySelectorAll ) {
// Only use querySelectorAll when not filtering,
// when this is not xml,
// and when no QSA bugs apply
- if ( !seed && !xml && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
+ if ( !seed && !xml && !rbuggyQSA.test( selector ) ) {
var groups, i,
old = true,
nid = expando,
@@ -5298,7 +5311,7 @@ if ( document.querySelectorAll ) {
expr = expr.replace( rattributeQuotes, "='$1']" );
// rbuggyMatches always contains :active, so no need for an existence check
- if ( !isXML( elem ) && !rbuggyMatches.test( expr ) && (!rbuggyQSA || !rbuggyQSA.test( expr )) ) {
+ if ( !isXML( elem ) && !rbuggyMatches.test( expr ) && !rbuggyQSA.test( expr ) ) {
try {
var ret = matches.call( elem, expr );
@@ -6533,7 +6546,7 @@ var curCSS, iframe, iframeDoc,
rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ),
rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ),
rrelNum = new RegExp( "^([-+])=(" + core_pnum + ")", "i" ),
- elemdisplay = {},
+ elemdisplay = { BODY: "block" },
cssShow = { position: "absolute", visibility: "hidden", display: "block" },
cssNormalTransform = {
@@ -6814,7 +6827,9 @@ if ( window.getComputedStyle ) {
if ( computed ) {
- ret = computed[ name ];
+ // getPropertyValue is only needed for .css('filter') in IE9, see #12537
+ ret = computed.getPropertyValue( name ) || computed[ name ];
+
if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
ret = jQuery.style( elem, name );
}
@@ -7843,9 +7858,12 @@ jQuery.extend({
// A cross-domain request is in order when we have a protocol:host:port mismatch
if ( s.crossDomain == null ) {
- parts = rurl.exec( s.url.toLowerCase() ) || false;
- s.crossDomain = parts && ( parts.join(":") + ( parts[ 3 ] ? "" : parts[ 1 ] === "http:" ? 80 : 443 ) ) !==
- ( ajaxLocParts.join(":") + ( ajaxLocParts[ 3 ] ? "" : ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) );
+ parts = rurl.exec( s.url.toLowerCase() );
+ s.crossDomain = !!( parts &&
+ ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||
+ ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) !=
+ ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) )
+ );
}
// Convert data if not already a string
@@ -8464,7 +8482,7 @@ if ( jQuery.support.ajax ) {
// on any attempt to access responseText (#11426)
try {
responses.text = xhr.responseText;
- } catch( _ ) {
+ } catch( e ) {
}
// Firefox throws an exception when accessing
@@ -8617,7 +8635,9 @@ function Animation( elem, properties, options ) {
tick = function() {
var currentTime = fxNow || createFxNow(),
remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
- percent = 1 - ( remaining / animation.duration || 0 ),
+ // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)
+ temp = remaining / animation.duration || 0,
+ percent = 1 - temp,
index = 0,
length = animation.tweens.length;
@@ -8769,7 +8789,7 @@ jQuery.Animation = jQuery.extend( Animation, {
});
function defaultPrefilter( elem, props, opts ) {
- var index, prop, value, length, dataShow, tween, hooks, oldfire,
+ var index, prop, value, length, dataShow, toggle, tween, hooks, oldfire,
anim = this,
style = elem.style,
orig = {},
@@ -8843,6 +8863,7 @@ function defaultPrefilter( elem, props, opts ) {
value = props[ index ];
if ( rfxtypes.exec( value ) ) {
delete props[ index ];
+ toggle = toggle || value === "toggle";
if ( value === ( hidden ? "hide" : "show" ) ) {
continue;
}
@@ -8853,6 +8874,14 @@ function defaultPrefilter( elem, props, opts ) {
length = handled.length;
if ( length ) {
dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} );
+ if ( "hidden" in dataShow ) {
+ hidden = dataShow.hidden;
+ }
+
+ // store state if its toggle - enables .stop().toggle() to "reverse"
+ if ( toggle ) {
+ dataShow.hidden = !hidden;
+ }
if ( hidden ) {
jQuery( elem ).show();
} else {
@@ -9149,6 +9178,8 @@ jQuery.fx.tick = function() {
timers = jQuery.timers,
i = 0;
+ fxNow = jQuery.now();
+
for ( ; i < timers.length; i++ ) {
timer = timers[ i ];
// Checks the timer has not already been removed
@@ -9160,6 +9191,7 @@ jQuery.fx.tick = function() {
if ( !timers.length ) {
jQuery.fx.stop();
}
+ fxNow = undefined;
};
jQuery.fx.timer = function( timer ) {
diff --git a/resources/jquery/jquery.json.js b/resources/jquery/jquery.json.js
index aac3428b..75953f4d 100644
--- a/resources/jquery/jquery.json.js
+++ b/resources/jquery/jquery.json.js
@@ -1,168 +1,174 @@
/**
- * jQuery JSON Plugin
- * version: 2.3 (2011-09-17)
+ * jQuery JSON plugin 2.4.0
*
- * This document is licensed as free software under the terms of the
- * MIT License: http://www.opensource.org/licenses/mit-license.php
- *
- * Brantley Harris wrote this plugin. It is based somewhat on the JSON.org
- * website's http://www.json.org/json2.js, which proclaims:
- * "NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.", a sentiment that
- * I uphold.
- *
- * It is also influenced heavily by MochiKit's serializeJSON, which is
- * copyrighted 2005 by Bob Ippolito.
+ * @author Brantley Harris, 2009-2011
+ * @author Timo Tijhof, 2011-2012
+ * @source This plugin is heavily influenced by MochiKit's serializeJSON, which is
+ * copyrighted 2005 by Bob Ippolito.
+ * @source Brantley Harris wrote this plugin. It is based somewhat on the JSON.org
+ * website's http://www.json.org/json2.js, which proclaims:
+ * "NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.", a sentiment that
+ * I uphold.
+ * @license MIT License <http://www.opensource.org/licenses/mit-license.php>
*/
+(function ($) {
+ 'use strict';
-(function( $ ) {
-
- var escapeable = /["\\\x00-\x1f\x7f-\x9f]/g,
- meta = {
- '\b': '\\b',
- '\t': '\\t',
- '\n': '\\n',
- '\f': '\\f',
- '\r': '\\r',
- '"' : '\\"',
- '\\': '\\\\'
- };
+ var escape = /["\\\x00-\x1f\x7f-\x9f]/g,
+ meta = {
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '"' : '\\"',
+ '\\': '\\\\'
+ },
+ hasOwn = Object.prototype.hasOwnProperty;
/**
* jQuery.toJSON
- * Converts the given argument into a JSON respresentation.
+ * Converts the given argument into a JSON representation.
*
- * @param o {Mixed} The json-serializble *thing* to be converted
+ * @param o {Mixed} The json-serializable *thing* to be converted
*
* If an object has a toJSON prototype, that will be used to get the representation.
* Non-integer/string keys are skipped in the object, as are keys that point to a
* function.
*
*/
- $.toJSON = typeof JSON === 'object' && JSON.stringify
- ? JSON.stringify
- : function( o ) {
-
- if ( o === null ) {
+ $.toJSON = typeof JSON === 'object' && JSON.stringify ? JSON.stringify : function (o) {
+ if (o === null) {
return 'null';
}
- var type = typeof o;
+ var pairs, k, name, val,
+ type = $.type(o);
- if ( type === 'undefined' ) {
+ if (type === 'undefined') {
return undefined;
}
- if ( type === 'number' || type === 'boolean' ) {
- return '' + o;
+
+ // Also covers instantiated Number and Boolean objects,
+ // which are typeof 'object' but thanks to $.type, we
+ // catch them here. I don't know whether it is right
+ // or wrong that instantiated primitives are not
+ // exported to JSON as an {"object":..}.
+ // We choose this path because that's what the browsers did.
+ if (type === 'number' || type === 'boolean') {
+ return String(o);
}
- if ( type === 'string') {
- return $.quoteString( o );
+ if (type === 'string') {
+ return $.quoteString(o);
}
- if ( type === 'object' ) {
- if ( typeof o.toJSON === 'function' ) {
- return $.toJSON( o.toJSON() );
- }
- if ( o.constructor === Date ) {
- var month = o.getUTCMonth() + 1,
- day = o.getUTCDate(),
- year = o.getUTCFullYear(),
- hours = o.getUTCHours(),
- minutes = o.getUTCMinutes(),
- seconds = o.getUTCSeconds(),
- milli = o.getUTCMilliseconds();
+ if (typeof o.toJSON === 'function') {
+ return $.toJSON(o.toJSON());
+ }
+ if (type === 'date') {
+ var month = o.getUTCMonth() + 1,
+ day = o.getUTCDate(),
+ year = o.getUTCFullYear(),
+ hours = o.getUTCHours(),
+ minutes = o.getUTCMinutes(),
+ seconds = o.getUTCSeconds(),
+ milli = o.getUTCMilliseconds();
- if ( month < 10 ) {
- month = '0' + month;
- }
- if ( day < 10 ) {
- day = '0' + day;
- }
- if ( hours < 10 ) {
- hours = '0' + hours;
- }
- if ( minutes < 10 ) {
- minutes = '0' + minutes;
- }
- if ( seconds < 10 ) {
- seconds = '0' + seconds;
- }
- if ( milli < 100 ) {
- milli = '0' + milli;
- }
- if ( milli < 10 ) {
- milli = '0' + milli;
- }
- return '"' + year + '-' + month + '-' + day + 'T' +
- hours + ':' + minutes + ':' + seconds +
- '.' + milli + 'Z"';
+ if (month < 10) {
+ month = '0' + month;
}
- if ( o.constructor === Array ) {
- var ret = [];
- for ( var i = 0; i < o.length; i++ ) {
- ret.push( $.toJSON( o[i] ) || 'null' );
- }
- return '[' + ret.join(',') + ']';
+ if (day < 10) {
+ day = '0' + day;
+ }
+ if (hours < 10) {
+ hours = '0' + hours;
+ }
+ if (minutes < 10) {
+ minutes = '0' + minutes;
+ }
+ if (seconds < 10) {
+ seconds = '0' + seconds;
+ }
+ if (milli < 100) {
+ milli = '0' + milli;
+ }
+ if (milli < 10) {
+ milli = '0' + milli;
+ }
+ return '"' + year + '-' + month + '-' + day + 'T' +
+ hours + ':' + minutes + ':' + seconds +
+ '.' + milli + 'Z"';
+ }
+
+ pairs = [];
+
+ if ($.isArray(o)) {
+ for (k = 0; k < o.length; k++) {
+ pairs.push($.toJSON(o[k]) || 'null');
}
- var name,
- val,
- pairs = [];
- for ( var k in o ) {
- type = typeof k;
- if ( type === 'number' ) {
- name = '"' + k + '"';
- } else if (type === 'string') {
- name = $.quoteString(k);
- } else {
+ return '[' + pairs.join(',') + ']';
+ }
+
+ // Any other object (plain object, RegExp, ..)
+ // Need to do typeof instead of $.type, because we also
+ // want to catch non-plain objects.
+ if (typeof o === 'object') {
+ for (k in o) {
+ // Only include own properties,
+ // Filter out inherited prototypes
+ if (hasOwn.call(o, k)) {
// Keys must be numerical or string. Skip others
- continue;
- }
- type = typeof o[k];
+ type = typeof k;
+ if (type === 'number') {
+ name = '"' + k + '"';
+ } else if (type === 'string') {
+ name = $.quoteString(k);
+ } else {
+ continue;
+ }
+ type = typeof o[k];
- if ( type === 'function' || type === 'undefined' ) {
// Invalid values like these return undefined
// from toJSON, however those object members
// shouldn't be included in the JSON string at all.
- continue;
+ if (type !== 'function' && type !== 'undefined') {
+ val = $.toJSON(o[k]);
+ pairs.push(name + ':' + val);
+ }
}
- val = $.toJSON( o[k] );
- pairs.push( name + ':' + val );
}
- return '{' + pairs.join( ',' ) + '}';
+ return '{' + pairs.join(',') + '}';
}
};
/**
* jQuery.evalJSON
- * Evaluates a given piece of json source.
+ * Evaluates a given json string.
*
- * @param src {String}
+ * @param str {String}
*/
- $.evalJSON = typeof JSON === 'object' && JSON.parse
- ? JSON.parse
- : function( src ) {
- return eval('(' + src + ')');
+ $.evalJSON = typeof JSON === 'object' && JSON.parse ? JSON.parse : function (str) {
+ /*jshint evil: true */
+ return eval('(' + str + ')');
};
/**
* jQuery.secureEvalJSON
* Evals JSON in a way that is *more* secure.
*
- * @param src {String}
+ * @param str {String}
*/
- $.secureEvalJSON = typeof JSON === 'object' && JSON.parse
- ? JSON.parse
- : function( src ) {
-
+ $.secureEvalJSON = typeof JSON === 'object' && JSON.parse ? JSON.parse : function (str) {
var filtered =
- src
- .replace( /\\["\\\/bfnrtu]/g, '@' )
- .replace( /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
- .replace( /(?:^|:|,)(?:\s*\[)+/g, '');
+ str
+ .replace(/\\["\\\/bfnrtu]/g, '@')
+ .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
+ .replace(/(?:^|:|,)(?:\s*\[)+/g, '');
- if ( /^[\],:{}\s]*$/.test( filtered ) ) {
- return eval( '(' + src + ')' );
- } else {
- throw new SyntaxError( 'Error parsing JSON, source is not valid.' );
+ if (/^[\],:{}\s]*$/.test(filtered)) {
+ /*jshint evil: true */
+ return eval('(' + str + ')');
}
+ throw new SyntaxError('Error parsing JSON, source is not valid.');
};
/**
@@ -176,18 +182,18 @@
* >>> jQuery.quoteString('"Where are we going?", she asked.')
* "\"Where are we going?\", she asked."
*/
- $.quoteString = function( string ) {
- if ( string.match( escapeable ) ) {
- return '"' + string.replace( escapeable, function( a ) {
+ $.quoteString = function (str) {
+ if (str.match(escape)) {
+ return '"' + str.replace(escape, function (a) {
var c = meta[a];
- if ( typeof c === 'string' ) {
+ if (typeof c === 'string') {
return c;
}
c = a.charCodeAt();
return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16);
}) + '"';
}
- return '"' + string + '"';
+ return '"' + str + '"';
};
-})( jQuery );
+}(jQuery));
diff --git a/resources/jquery/jquery.localize.js b/resources/jquery/jquery.localize.js
index 3e786ec2..d9a2b199 100644
--- a/resources/jquery/jquery.localize.js
+++ b/resources/jquery/jquery.localize.js
@@ -1,9 +1,31 @@
/**
- * Simple Placeholder-based Localization
+ * @class jQuery.plugin.localize
+ */
+( function ( $, mw ) {
+
+/**
+ * Gets a localized message, using parameters from options if present.
+ * @ignore
+ *
+ * @param {Object} options
+ * @param {string} key
+ * @returns {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
+ * 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
+ * `<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:
@@ -77,34 +99,12 @@
* Appends something like this to the body...
* <p>You may not get there all in one piece.</p>
*
- */
-( function ( $, mw ) {
-
-/**
- * Gets a localized message, using parameters from options if present.
- *
- * @function
- * @param {String} key Message key to get localized message for
- * @returns {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.
- *
* @method
* @param {Object} options Map of options to be used while localizing
- * @param {String} options.prefix String to prepend to all message keys
+ * @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
- * @returns {jQuery} This selection
+ * @return {jQuery}
*/
$.fn.localize = function ( options ) {
var $target = this,
@@ -162,4 +162,9 @@ $.fn.localize = function ( options ) {
// 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/jquery/jquery.makeCollapsible.js b/resources/jquery/jquery.makeCollapsible.js
index 0a4d3645..0cd6417c 100644
--- a/resources/jquery/jquery.makeCollapsible.js
+++ b/resources/jquery/jquery.makeCollapsible.js
@@ -2,340 +2,380 @@
* jQuery makeCollapsible
*
* This will enable collapsible-functionality on all passed elements.
- * 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 class "mw-made-collapsible".
- * Except for tables and lists, the inner content is wrapped in "mw-collapsible-content".
+ * - 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).
*
- * @author Krinkle <krinklemail@gmail.com>
+ * @author Krinkle, 2011-2012
*
* Dual license:
* @license CC-BY 3.0 <http://creativecommons.org/licenses/by/3.0>
* @license GPL2 <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
*/
( function ( $, mw ) {
+ var lpx = 'jquery.makeCollapsible> ';
+
+ /**
+ * Handler for a click on a collapsible toggler.
+ *
+ * @param {jQuery} $collapsible
+ * @param {string} action The action this function will take ('expand' or 'collapse').
+ * @param {jQuery|null} [optional] $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;
+ }
-$.fn.makeCollapsible = function () {
-
- return this.each(function () {
-
- // Define reused variables and functions
- var $toggle,
- lpx = 'jquery.makeCollapsible> ',
- $that = $(this).addClass( 'mw-collapsible' ), // case: $( '#myAJAXelement' ).makeCollapsible()
- that = this,
- collapsetext = $(this).attr( 'data-collapsetext' ),
- expandtext = $(this).attr( 'data-expandtext' ),
- toggleElement = function ( $collapsible, action, $defaultToggle, instantHide ) {
- var $collapsibleContent, $containers;
+ // 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
+ $containers = $collapsible.find( '> tbody > tr' );
+ if ( $defaultToggle ) {
+ // Exclude table row containing togglelink
+ $containers = $containers.not( $defaultToggle.closest( 'tr' ) );
+ }
- // Validate parameters
- if ( !$collapsible.jquery ) { // $collapsible must be an instance of 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 instanceof $) ) {
- // 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;
+ 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 );
+ }
- if ( action === 'collapse' ) {
-
- // Collapse the element
- if ( $collapsible.is( 'table' ) ) {
- // Hide all table rows of this table
- // Slide doens't work with tables, but fade does as of jQuery 1.1.3
- // http://stackoverflow.com/questions/467336#920480
- $containers = $collapsible.find( '>tbody>tr' );
- if ( $defaultToggle ) {
- // Exclude tablerow containing togglelink
- $containers.not( $defaultToggle.closest( 'tr' ) ).stop(true, true).fadeOut();
- } else {
- if ( instantHide ) {
- $containers.hide();
- } else {
- $containers.stop( true, true ).fadeOut();
- }
- }
-
- } else if ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) {
- $containers = $collapsible.find( '> li' );
- if ( $defaultToggle ) {
- // Exclude list-item containing togglelink
- $containers.not( $defaultToggle.parent() ).stop( true, true ).slideUp();
- } else {
- if ( instantHide ) {
- $containers.hide();
- } else {
- $containers.stop( true, true ).slideUp();
- }
- }
-
- } else { // <div>, <p> etc.
- $collapsibleContent = $collapsible.find( '> .mw-collapsible-content' );
-
- // If a collapsible-content is defined, collapse it
- if ( $collapsibleContent.length ) {
- if ( instantHide ) {
- $collapsibleContent.hide();
- } else {
- $collapsibleContent.slideUp();
- }
-
- // 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 ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) {
- $collapsible.fadeOut();
- } else {
- $collapsible.slideUp();
- }
- }
- }
+ } 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 );
+ }
- // Expand the element
- if ( $collapsible.is( 'table' ) ) {
- $containers = $collapsible.find( '>tbody>tr' );
- if ( $defaultToggle ) {
- // Exclude tablerow containing togglelink
- $containers.not( $defaultToggle.parent().parent() ).stop(true, true).fadeIn();
- } else {
- $containers.stop(true, true).fadeIn();
- }
-
- } else if ( $collapsible.is( 'ul' ) || $collapsible.is( 'ol' ) ) {
- $containers = $collapsible.find( '> li' );
- if ( $defaultToggle ) {
- // Exclude list-item containing togglelink
- $containers.not( $defaultToggle.parent() ).stop( true, true ).slideDown();
- } else {
- $containers.stop( true, true ).slideDown();
- }
-
- } else { // <div>, <p> etc.
- $collapsibleContent = $collapsible.find( '> .mw-collapsible-content' );
-
- // If a collapsible-content is defined, collapse it
- if ( $collapsibleContent.length ) {
- $collapsibleContent.slideDown();
+ } else {
+ // Everything else: <div>, <p> etc.
+ $collapsibleContent = $collapsible.find( '> .mw-collapsible-content' );
- // 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 ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) {
- $collapsible.fadeIn();
- } else {
- $collapsible.slideDown();
- }
- }
+ // 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 );
}
- },
- // Toggles collapsible and togglelink class and updates text label
- toggleLinkDefault = function ( that, e ) {
- var $that = $(that),
- $collapsible = $that.closest( '.mw-collapsible.mw-made-collapsible' ).toggleClass( 'mw-collapsed' );
- e.preventDefault();
- e.stopPropagation();
- // It's expanded right now
- if ( !$that.hasClass( 'mw-collapsible-toggle-collapsed' ) ) {
- // Change link to "Show"
- $that.removeClass( 'mw-collapsible-toggle-expanded' ).addClass( 'mw-collapsible-toggle-collapsed' );
- if ( $that.find( '> a' ).length ) {
- $that.find( '> a' ).text( expandtext );
+ // 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 {
- $that.text( expandtext );
+ if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) {
+ $collapsible.fadeOut().promise().done( hookCallback );
+ } else {
+ $collapsible.slideUp().promise().done( hookCallback );
+ }
}
- // Collapse element
- toggleElement( $collapsible, 'collapse', $that );
-
- // It's collapsed right now
} else {
- // Change link to "Hide"
- $that.removeClass( 'mw-collapsible-toggle-collapsed' ).addClass( 'mw-collapsible-toggle-expanded' );
- if ( $that.find( '> a' ).length ) {
- $that.find( '> a' ).text( collapsetext );
+ if ( $collapsible.is( 'tr' ) || $collapsible.is( 'td' ) || $collapsible.is( 'th' ) ) {
+ $collapsible.fadeIn().promise().done( hookCallback );
} else {
- $that.text( collapsetext );
+ $collapsible.slideDown().promise().done( hookCallback );
}
- // Expand element
- toggleElement( $collapsible, 'expand', $that );
}
+ }
+ }
+ }
+
+ /**
+ * Handles clicking/keypressing on the collapsible element toggle and other
+ * situations where a collapsible element is toggled (e.g. the initial
+ * toggle for collapsed ones).
+ *
+ * @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;
- },
- // Toggles collapsible and togglelink class
- toggleLinkPremade = function ( $that, e ) {
- var $collapsible = $that.eq(0).closest( '.mw-collapsible.mw-made-collapsible' ).toggleClass( 'mw-collapsed' );
- if ( $(e.target).is( 'a' ) ) {
- return true;
- }
+ } 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();
+ }
+ }
- // It's expanded right now
- if ( !$that.hasClass( 'mw-collapsible-toggle-collapsed' ) ) {
- // Change toggle to collapsed
- $that.removeClass( 'mw-collapsible-toggle-expanded' ).addClass( 'mw-collapsible-toggle-collapsed' );
- // Collapse element
- toggleElement( $collapsible, 'collapse', $that );
-
- // It's collapsed right now
- } else {
- // Change toggle to expanded
- $that.removeClass( 'mw-collapsible-toggle-collapsed' ).addClass( 'mw-collapsible-toggle-expanded' );
- // Expand element
- toggleElement( $collapsible, 'expand', $that );
- }
- return;
- },
- // Toggles customcollapsible
- toggleLinkCustom = function ( $that, e, $collapsible ) {
- // For the initial state call of customtogglers there is no event passed
- if (e) {
- e.preventDefault();
- e.stopPropagation();
- }
- // Get current state and toggle to the opposite
- var action = $collapsible.hasClass( 'mw-collapsed' ) ? 'expand' : 'collapse';
- $collapsible.toggleClass( 'mw-collapsed' );
- toggleElement( $collapsible, action, $that );
+ // 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 );
- // Use custom text or default ?
- if ( !collapsetext ) {
- collapsetext = mw.msg( 'collapsible-collapse' );
+ // 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 );
}
- if ( !expandtext ) {
- expandtext = mw.msg( 'collapsible-expand' );
+
+ // 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 );
}
- // Create toggle link with a space around the brackets (&nbsp;[text]&nbsp;)
- var $toggleLink =
- $( '<a href="#"></a>' )
- .text( collapsetext )
- .wrap( '<span class="mw-collapsible-toggle"></span>' )
- .parent()
- .prepend( '&nbsp;[' )
- .append( ']&nbsp;' )
- .on( 'click.mw-collapse', function ( e ) {
- toggleLinkDefault( this, e );
- } );
-
- // Return if it has been enabled already.
- if ( $that.hasClass( 'mw-made-collapsible' ) ) {
- return;
- } else {
- $that.addClass( 'mw-made-collapsible' );
+ // And finally toggle the element state itself
+ toggleElement( $collapsible, wasCollapsed ? 'expand' : 'collapse', $toggle, options );
+ }
+
+ /**
+ * Make any element collapsible.
+ *
+ * Supported options:
+ * - collapseText: text to be 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.
+ * - expandText: text to be 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.
+ * - collapsed: boolean, whether to collapse immediately. By default
+ * collapse only if the elements has the 'mw-collapsible' class.
+ * - $customTogglers: jQuerified list of 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.
+ * - plainMode: boolean, 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.
+ */
+ $.fn.makeCollapsible = function ( options ) {
+ if ( options === undefined ) {
+ options = {};
}
- // Check if this element has a custom position for the toggle link
- // (ie. outside the container or deeper inside the tree)
- // Then: Locate the custom toggle link(s) and bind them
- if ( ( $that.attr( 'id' ) || '' ).indexOf( 'mw-customcollapsible-' ) === 0 ) {
+ return this.each( function () {
+ var $collapsible, collapseText, expandText, $toggle, actionHandler, buildDefaultToggleLink,
+ premadeToggleHandler, $toggleLink, $firstItem, collapsibleId, $customTogglers, firstval;
- var thatId = $that.attr( 'id' ),
- $customTogglers = $( '.' + thatId.replace( 'mw-customcollapsible', 'mw-customtoggle' ) );
- mw.log( lpx + 'Found custom collapsible: #' + thatId );
+ // 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' );
- // Double check that there is actually a customtoggle link
- if ( $customTogglers.length ) {
- $customTogglers.on( 'click.mw-collapse', function ( e ) {
- toggleLinkCustom( $(this), e, $that );
- } );
+ // Return if it has been enabled already.
+ if ( $collapsible.data( 'mw-made-collapsible' ) ) {
+ return;
} else {
- mw.log( lpx + '#' + thatId + ': Missing toggler!' );
+ $collapsible.data( 'mw-made-collapsible', true );
}
- // Initial state
- if ( $that.hasClass( 'mw-collapsed' ) ) {
- $that.removeClass( 'mw-collapsed' );
- toggleLinkCustom( $customTogglers, null, $that );
+ // 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( '&nbsp;[' )
+ .append( ']&nbsp;' )
+ .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 ) {
+ mw.log( lpx + 'Found custom collapsible: #' + collapsibleId );
+ $customTogglers = $( '.' + collapsibleId.replace( 'mw-customcollapsible', 'mw-customtoggle' ) );
+
+ // Double check that there is actually a customtoggle link
+ if ( !$customTogglers.length ) {
+ mw.log( lpx + '#' + collapsibleId + ': Missing toggler!' );
+ }
+ }
}
- // If this is not a custom case, do the default:
- // Wrap the contents add the toggle link
- } else {
+ // Bind the togglers
+ if ( $customTogglers && $customTogglers.length ) {
+ actionHandler = function ( e, opts ) {
+ var defaultOpts = {};
+ opts = $.extend( defaultOpts, options, opts );
+ togglingHandler( $( this ), $collapsible, e, opts );
+ };
- // Elements are treated differently
- if ( $that.is( 'table' ) ) {
- // The toggle-link will be in one the the cells (td or th) of the first row
- var $firstRowCells = $that.find( 'tr:first th, tr:first td' );
- $toggle = $firstRowCells.find( '> .mw-collapsible-toggle' );
+ $toggleLink = $customTogglers;
+ $toggleLink.on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler );
- // If theres no toggle link, add it to the last cell
- if ( !$toggle.length ) {
- $firstRowCells.eq(-1).prepend( $toggleLink );
- } else {
- $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e ) {
- toggleLinkPremade( $toggle, e );
- } );
- }
+ } 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' ) ) {
+ // 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 );
+ }
- } else if ( $that.is( 'ul' ) || $that.is( 'ol' ) ) {
- // The toggle-link will be in the first list-item
- var $firstItem = $that.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 null or undefined.
- var firstval = $firstItem.attr( 'value' );
- if ( firstval === undefined || !firstval || firstval === '-1' || firstval === -1 ) {
- $firstItem.attr( 'value', '1' );
+ } 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.attr( 'value' );
+ if ( firstval === undefined || !firstval || firstval === '-1' || firstval === -1 ) {
+ $firstItem.attr( '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 );
}
- $that.prepend( $toggleLink.wrap( '<li class="mw-collapsible-toggle-li"></li>' ).parent() );
- } else {
- $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e ) {
- toggleLinkPremade( $toggle, e );
- } );
- }
- } else { // <div>, <p> etc.
+ } else { // <div>, <p> etc.
- // The toggle-link will be the first child of the element
- $toggle = $that.find( '> .mw-collapsible-toggle' );
+ // 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 ( !$that.find( '> .mw-collapsible-content' ).length ) {
- $that.wrapInner( '<div class="mw-collapsible-content"></div>' );
- }
+ // 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 ) {
- $that.prepend( $toggleLink );
- } else {
- $toggleLink = $toggle.off( 'click.mw-collapse' ).on( 'click.mw-collapse', function ( e ) {
- toggleLinkPremade( $toggle, e );
- } );
+ // 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 );
+ }
}
}
- }
- // Initial state (only for those that are not custom)
- if ( $that.hasClass( 'mw-collapsed' ) && ( $that.attr( 'id' ) || '').indexOf( 'mw-customcollapsible-' ) !== 0 ) {
- $that.removeClass( 'mw-collapsed' );
- // The collapsible element could have multiple togglers
- // To toggle the initial state only click one of them (ie. the first one, eq(0) )
- // Else it would go like: hide,show,hide,show for each toggle link.
- toggleElement( $that, 'collapse', $toggleLink.eq(0), /* instantHide = */ true );
- $toggleLink.eq(0).click();
- }
- } );
-};
+ // Attributes for accessibility. This isn't necessary when the toggler is already
+ // an <a> or a <button> etc., but it doesn't hurt either, and it's consistent.
+ $toggleLink.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 } );
+ }
+ } );
+ };
}( jQuery, mediaWiki ) );
diff --git a/resources/jquery/jquery.mw-jump.js b/resources/jquery/jquery.mw-jump.js
index 36b6690c..e2868341 100644
--- a/resources/jquery/jquery.mw-jump.js
+++ b/resources/jquery/jquery.mw-jump.js
@@ -1,12 +1,12 @@
/**
* JavaScript to show jump links to motor-impaired users when they are focused.
*/
-jQuery( function( $ ) {
+jQuery( function ( $ ) {
- $('.mw-jump').delegate( 'a', 'focus blur', 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' });
+ $( '.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/jquery/jquery.mwExtension.js b/resources/jquery/jquery.mwExtension.js
index bbffd7b7..de399788 100644
--- a/resources/jquery/jquery.mwExtension.js
+++ b/resources/jquery/jquery.mwExtension.js
@@ -15,12 +15,13 @@
return str.charAt( 0 ).toUpperCase() + str.substr( 1 );
},
escapeRE: function ( str ) {
- return str.replace ( /([\\{}()|.?*+\-\^$\[\]])/g, "\\$1" );
+ 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 )
{
@@ -32,7 +33,7 @@
return true;
}
if ( typeof v === 'object' ) {
- for ( var key in v ) {
+ for ( key in v ) {
return false;
}
return true;
diff --git a/resources/jquery/jquery.placeholder.js b/resources/jquery/jquery.placeholder.js
index 7badb11a..8044d880 100644
--- a/resources/jquery/jquery.placeholder.js
+++ b/resources/jquery/jquery.placeholder.js
@@ -10,17 +10,22 @@
*/
( function ( $ ) {
- $.fn.placeholder = function () {
+ $.fn.placeholder = function ( text ) {
+ var hasArg = arguments.length;
return this.each( function () {
var placeholder, $input;
+ if ( hasArg ) {
+ this.setAttribute( 'placeholder', text );
+ }
+
// If the HTML5 placeholder attribute is supported, use it
if ( this.placeholder && 'placeholder' in document.createElement( this.tagName ) ) {
return;
}
- placeholder = this.getAttribute( 'placeholder' );
+ placeholder = hasArg ? text : this.getAttribute( 'placeholder' );
$input = $(this);
// Show initially, if empty
diff --git a/resources/jquery/jquery.qunit.completenessTest.js b/resources/jquery/jquery.qunit.completenessTest.js
index 1475af2a..20e6678e 100644
--- a/resources/jquery/jquery.qunit.completenessTest.js
+++ b/resources/jquery/jquery.qunit.completenessTest.js
@@ -12,10 +12,8 @@
*
* @author Timo Tijhof, 2011-2012
*/
-/*global jQuery, QUnit */
-/*jshint eqeqeq:false, eqnull:false, forin:false */
( function ( $ ) {
- "use strict";
+ 'use strict';
var util,
hasOwn = Object.prototype.hasOwnProperty,
diff --git a/resources/jquery/jquery.qunit.css b/resources/jquery/jquery.qunit.css
index 55970e00..d7fc0c8e 100644
--- a/resources/jquery/jquery.qunit.css
+++ b/resources/jquery/jquery.qunit.css
@@ -1,5 +1,5 @@
/**
- * QUnit v1.10.0 - A JavaScript Unit Testing Framework
+ * QUnit v1.11.0 - A JavaScript Unit Testing Framework
*
* http://qunitjs.com
*
@@ -20,7 +20,7 @@
/** Resets */
-#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
margin: 0;
padding: 0;
}
@@ -111,7 +111,12 @@
color: #000;
}
-#qunit-tests ol {
+#qunit-tests li .runtime {
+ float: right;
+ font-size: smaller;
+}
+
+.qunit-assert-list {
margin-top: 0.5em;
padding: 0.5em;
@@ -122,6 +127,10 @@
-webkit-border-radius: 5px;
}
+.qunit-collapsed {
+ display: none;
+}
+
#qunit-tests table {
border-collapse: collapse;
margin-top: .2em;
diff --git a/resources/jquery/jquery.qunit.js b/resources/jquery/jquery.qunit.js
index d4f17b5a..302545f4 100644
--- a/resources/jquery/jquery.qunit.js
+++ b/resources/jquery/jquery.qunit.js
@@ -1,5 +1,5 @@
/**
- * QUnit v1.10.0 - A JavaScript Unit Testing Framework
+ * QUnit v1.11.0 - A JavaScript Unit Testing Framework
*
* http://qunitjs.com
*
@@ -11,6 +11,7 @@
(function( window ) {
var QUnit,
+ assert,
config,
onErrorFnPrev,
testId = 0,
@@ -20,18 +21,67 @@ var QUnit,
// Keep a local reference to Date (GH-283)
Date = window.Date,
defined = {
- setTimeout: typeof window.setTimeout !== "undefined",
- sessionStorage: (function() {
- var x = "qunit-test-string";
- try {
- sessionStorage.setItem( x, x );
- sessionStorage.removeItem( x );
- return true;
- } catch( e ) {
- return false;
+ setTimeout: typeof window.setTimeout !== "undefined",
+ sessionStorage: (function() {
+ var x = "qunit-test-string";
+ try {
+ sessionStorage.setItem( x, x );
+ sessionStorage.removeItem( x );
+ return true;
+ } catch( e ) {
+ return false;
+ }
+ }())
+ },
+ /**
+ * Provides a normalized error string, correcting an issue
+ * with IE 7 (and prior) where Error.prototype.toString is
+ * not properly implemented
+ *
+ * Based on http://es5.github.com/#x15.11.4.4
+ *
+ * @param {String|Error} error
+ * @return {String} error message
+ */
+ errorString = function( error ) {
+ var name, message,
+ errorString = error.toString();
+ if ( errorString.substring( 0, 7 ) === "[object" ) {
+ name = error.name ? error.name.toString() : "Error";
+ message = error.message ? error.message.toString() : "";
+ if ( name && message ) {
+ return name + ": " + message;
+ } else if ( name ) {
+ return name;
+ } else if ( message ) {
+ return message;
+ } else {
+ return "Error";
+ }
+ } else {
+ return errorString;
}
- }())
-};
+ },
+ /**
+ * Makes a clone of an object using only Array or Object as base,
+ * and copies over the own enumerable properties.
+ *
+ * @param {Object} obj
+ * @return {Object} New object with only the own properties (recursively).
+ */
+ objectValues = function( obj ) {
+ // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392.
+ /*jshint newcap: false */
+ var key, val,
+ vals = QUnit.is( "array", obj ) ? [] : {};
+ for ( key in obj ) {
+ if ( hasOwn.call( obj, key ) ) {
+ val = obj[key];
+ vals[key] = val === Object(val) ? objectValues(val) : val;
+ }
+ }
+ return vals;
+ };
function Test( settings ) {
extend( this, settings );
@@ -44,11 +94,11 @@ Test.count = 0;
Test.prototype = {
init: function() {
var a, b, li,
- tests = id( "qunit-tests" );
+ tests = id( "qunit-tests" );
if ( tests ) {
b = document.createElement( "strong" );
- b.innerHTML = this.name;
+ b.innerHTML = this.nameHtml;
// `a` initialized at top of scope
a = document.createElement( "a" );
@@ -92,6 +142,7 @@ Test.prototype = {
teardown: function() {}
}, this.moduleTestEnvironment );
+ this.started = +new Date();
runLoggingCallbacks( "testStart", QUnit, {
name: this.testName,
module: this.module
@@ -111,7 +162,7 @@ Test.prototype = {
try {
this.testEnvironment.setup.call( this.testEnvironment );
} catch( e ) {
- QUnit.pushFailure( "Setup failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) );
+ QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
}
},
run: function() {
@@ -120,22 +171,28 @@ Test.prototype = {
var running = id( "qunit-testresult" );
if ( running ) {
- running.innerHTML = "Running: <br/>" + this.name;
+ running.innerHTML = "Running: <br/>" + this.nameHtml;
}
if ( this.async ) {
QUnit.stop();
}
+ this.callbackStarted = +new Date();
+
if ( config.notrycatch ) {
this.callback.call( this.testEnvironment, QUnit.assert );
+ this.callbackRuntime = +new Date() - this.callbackStarted;
return;
}
try {
this.callback.call( this.testEnvironment, QUnit.assert );
+ this.callbackRuntime = +new Date() - this.callbackStarted;
} catch( e ) {
- QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + e.message, extractStacktrace( e, 0 ) );
+ this.callbackRuntime = +new Date() - this.callbackStarted;
+
+ QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
// else next test will carry the responsibility
saveGlobal();
@@ -148,38 +205,43 @@ Test.prototype = {
teardown: function() {
config.current = this;
if ( config.notrycatch ) {
+ if ( typeof this.callbackRuntime === "undefined" ) {
+ this.callbackRuntime = +new Date() - this.callbackStarted;
+ }
this.testEnvironment.teardown.call( this.testEnvironment );
return;
} else {
try {
this.testEnvironment.teardown.call( this.testEnvironment );
} catch( e ) {
- QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) );
+ QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
}
}
checkPollution();
},
finish: function() {
config.current = this;
- if ( config.requireExpects && this.expected == null ) {
+ if ( config.requireExpects && this.expected === null ) {
QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack );
- } else if ( this.expected != null && this.expected != this.assertions.length ) {
+ } else if ( this.expected !== null && this.expected !== this.assertions.length ) {
QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack );
- } else if ( this.expected == null && !this.assertions.length ) {
+ } else if ( this.expected === null && !this.assertions.length ) {
QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack );
}
- var assertion, a, b, i, li, ol,
+ var i, assertion, a, b, time, li, ol,
test = this,
good = 0,
bad = 0,
tests = id( "qunit-tests" );
+ this.runtime = +new Date() - this.started;
config.stats.all += this.assertions.length;
config.moduleStats.all += this.assertions.length;
if ( tests ) {
ol = document.createElement( "ol" );
+ ol.className = "qunit-assert-list";
for ( i = 0; i < this.assertions.length; i++ ) {
assertion = this.assertions[i];
@@ -208,22 +270,22 @@ Test.prototype = {
}
if ( bad === 0 ) {
- ol.style.display = "none";
+ addClass( ol, "qunit-collapsed" );
}
// `b` initialized at top of scope
b = document.createElement( "strong" );
- b.innerHTML = this.name + " <b class='counts'>(<b class='failed'>" + bad + "</b>, <b class='passed'>" + good + "</b>, " + this.assertions.length + ")</b>";
+ b.innerHTML = this.nameHtml + " <b class='counts'>(<b class='failed'>" + bad + "</b>, <b class='passed'>" + good + "</b>, " + this.assertions.length + ")</b>";
addEvent(b, "click", function() {
- var next = b.nextSibling.nextSibling,
- display = next.style.display;
- next.style.display = display === "none" ? "block" : "none";
+ var next = b.parentNode.lastChild,
+ collapsed = hasClass( next, "qunit-collapsed" );
+ ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" );
});
addEvent(b, "dblclick", function( e ) {
var target = e && e.target ? e.target : window.event.srcElement;
- if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) {
+ if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) {
target = target.parentNode;
}
if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
@@ -231,13 +293,19 @@ Test.prototype = {
}
});
+ // `time` initialized at top of scope
+ time = document.createElement( "span" );
+ time.className = "runtime";
+ time.innerHTML = this.runtime + " ms";
+
// `li` initialized at top of scope
li = id( this.id );
li.className = bad ? "fail" : "pass";
li.removeChild( li.firstChild );
a = li.firstChild;
li.appendChild( b );
- li.appendChild ( a );
+ li.appendChild( a );
+ li.appendChild( time );
li.appendChild( ol );
} else {
@@ -255,7 +323,8 @@ Test.prototype = {
module: this.module,
failed: bad,
passed: this.assertions.length - bad,
- total: this.assertions.length
+ total: this.assertions.length,
+ duration: this.runtime
});
QUnit.reset();
@@ -321,7 +390,7 @@ QUnit = {
test: function( testName, expected, callback, async ) {
var test,
- name = "<span class='test-name'>" + escapeInnerText( testName ) + "</span>";
+ nameHtml = "<span class='test-name'>" + escapeText( testName ) + "</span>";
if ( arguments.length === 2 ) {
callback = expected;
@@ -329,11 +398,11 @@ QUnit = {
}
if ( config.currentModule ) {
- name = "<span class='module-name'>" + config.currentModule + "</span>: " + name;
+ nameHtml = "<span class='module-name'>" + escapeText( config.currentModule ) + "</span>: " + nameHtml;
}
test = new Test({
- name: name,
+ nameHtml: nameHtml,
testName: testName,
expected: expected,
async: async,
@@ -360,6 +429,18 @@ QUnit = {
},
start: function( count ) {
+ // QUnit hasn't been initialized yet.
+ // Note: RequireJS (et al) may delay onLoad
+ if ( config.semaphore === undefined ) {
+ QUnit.begin(function() {
+ // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first
+ setTimeout(function() {
+ QUnit.start( count );
+ });
+ });
+ return;
+ }
+
config.semaphore -= count || 1;
// don't start until equal number of stop-calls
if ( config.semaphore > 0 ) {
@@ -368,6 +449,8 @@ QUnit = {
// ignore if start is called more often then stop
if ( config.semaphore < 0 ) {
config.semaphore = 0;
+ QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) );
+ return;
}
// A slight delay, to avoid any current callbacks
if ( defined.setTimeout ) {
@@ -403,11 +486,14 @@ QUnit = {
}
};
+// `assert` initialized at top of scope
// Asssert helpers
-// All of these must call either QUnit.push() or manually do:
+// All of these must either call QUnit.push() or manually do:
// - runLoggingCallbacks( "log", .. );
// - config.current.assertions.push({ .. });
-QUnit.assert = {
+// We attach it to the QUnit object *after* we expose the public API,
+// otherwise `assert` will become a global variable in browsers (#341).
+assert = {
/**
* Asserts rough true-ish result.
* @name ok
@@ -428,14 +514,14 @@ QUnit.assert = {
message: msg
};
- msg = escapeInnerText( msg || (result ? "okay" : "failed" ) );
+ msg = escapeText( msg || (result ? "okay" : "failed" ) );
msg = "<span class='test-message'>" + msg + "</span>";
if ( !result ) {
source = sourceFromStacktrace( 2 );
if ( source ) {
details.source = source;
- msg += "<table><tr class='test-source'><th>Source: </th><td><pre>" + escapeInnerText( source ) + "</pre></td></tr></table>";
+ msg += "<table><tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr></table>";
}
}
runLoggingCallbacks( "log", QUnit, details );
@@ -453,6 +539,7 @@ QUnit.assert = {
* @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" );
*/
equal: function( actual, expected, message ) {
+ /*jshint eqeqeq:false */
QUnit.push( expected == actual, actual, expected, message );
},
@@ -461,10 +548,31 @@ QUnit.assert = {
* @function
*/
notEqual: function( actual, expected, message ) {
+ /*jshint eqeqeq:false */
QUnit.push( expected != actual, actual, expected, message );
},
/**
+ * @name propEqual
+ * @function
+ */
+ propEqual: function( actual, expected, message ) {
+ actual = objectValues(actual);
+ expected = objectValues(expected);
+ QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
+ },
+
+ /**
+ * @name notPropEqual
+ * @function
+ */
+ notPropEqual: function( actual, expected, message ) {
+ actual = objectValues(actual);
+ expected = objectValues(expected);
+ QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
+ },
+
+ /**
* @name deepEqual
* @function
*/
@@ -496,8 +604,9 @@ QUnit.assert = {
QUnit.push( expected !== actual, actual, expected, message );
},
- throws: function( block, expected, message ) {
+ "throws": function( block, expected, message ) {
var actual,
+ expectedOutput = expected,
ok = false;
// 'expected' is optional
@@ -518,18 +627,20 @@ QUnit.assert = {
// we don't want to validate thrown error
if ( !expected ) {
ok = true;
+ expectedOutput = null;
// expected is a regexp
} else if ( QUnit.objectType( expected ) === "regexp" ) {
- ok = expected.test( actual );
+ ok = expected.test( errorString( actual ) );
// expected is a constructor
} else if ( actual instanceof expected ) {
ok = true;
// expected is a validation function which returns true is validation passed
} else if ( expected.call( {}, actual ) === true ) {
+ expectedOutput = null;
ok = true;
}
- QUnit.push( ok, actual, null, message );
+ QUnit.push( ok, actual, expectedOutput, message );
} else {
QUnit.pushFailure( message, null, 'No exception was thrown.' );
}
@@ -538,15 +649,16 @@ QUnit.assert = {
/**
* @deprecate since 1.8.0
- * Kept assertion helpers in root for backwards compatibility
+ * Kept assertion helpers in root for backwards compatibility.
*/
-extend( QUnit, QUnit.assert );
+extend( QUnit, assert );
/**
* @deprecated since 1.9.0
- * Kept global "raises()" for backwards compatibility
+ * Kept root "raises()" for backwards compatibility.
+ * (Note that we don't introduce assert.raises).
*/
-QUnit.raises = QUnit.assert.throws;
+QUnit.raises = assert[ "throws" ];
/**
* @deprecated since 1.0.0, replaced with error pushes since 1.3.0
@@ -622,6 +734,15 @@ config = {
moduleDone: []
};
+// Export global variables, unless an 'exports' object exists,
+// in that case we assume we're in CommonJS (dealt with on the bottom of the script)
+if ( typeof exports === "undefined" ) {
+ extend( window, QUnit );
+
+ // Expose QUnit object
+ window.QUnit = QUnit;
+}
+
// Initialize more QUnit.config and QUnit.urlParams
(function() {
var i,
@@ -655,18 +776,11 @@ config = {
QUnit.isLocal = location.protocol === "file:";
}());
-// Export global variables, unless an 'exports' object exists,
-// in that case we assume we're in CommonJS (dealt with on the bottom of the script)
-if ( typeof exports === "undefined" ) {
- extend( window, QUnit );
-
- // Expose QUnit object
- window.QUnit = QUnit;
-}
-
// Extend QUnit object,
// these after set here because they should not be exposed as global functions
extend( QUnit, {
+ assert: assert,
+
config: config,
// Initialize the configuration options
@@ -681,7 +795,7 @@ extend( QUnit, {
autorun: false,
filter: "",
queue: [],
- semaphore: 0
+ semaphore: 1
});
var tests, banner, result,
@@ -689,7 +803,7 @@ extend( QUnit, {
if ( qunit ) {
qunit.innerHTML =
- "<h1 id='qunit-header'>" + escapeInnerText( document.title ) + "</h1>" +
+ "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
"<h2 id='qunit-banner'></h2>" +
"<div id='qunit-testrunner-toolbar'></div>" +
"<h2 id='qunit-userAgent'></h2>" +
@@ -745,7 +859,7 @@ extend( QUnit, {
// Safe object type checking
is: function( type, obj ) {
- return QUnit.objectType( obj ) == type;
+ return QUnit.objectType( obj ) === type;
},
objectType: function( obj ) {
@@ -757,7 +871,8 @@ extend( QUnit, {
return "null";
}
- var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || "";
+ var match = toString.call( obj ).match(/^\[object\s(.*)\]$/),
+ type = match && match[1] || "";
switch ( type ) {
case "Number":
@@ -794,16 +909,16 @@ extend( QUnit, {
expected: expected
};
- message = escapeInnerText( message ) || ( result ? "okay" : "failed" );
+ message = escapeText( message ) || ( result ? "okay" : "failed" );
message = "<span class='test-message'>" + message + "</span>";
output = message;
if ( !result ) {
- expected = escapeInnerText( QUnit.jsDump.parse(expected) );
- actual = escapeInnerText( QUnit.jsDump.parse(actual) );
+ expected = escapeText( QUnit.jsDump.parse(expected) );
+ actual = escapeText( QUnit.jsDump.parse(actual) );
output += "<table><tr class='test-expected'><th>Expected: </th><td><pre>" + expected + "</pre></td></tr>";
- if ( actual != expected ) {
+ if ( actual !== expected ) {
output += "<tr class='test-actual'><th>Result: </th><td><pre>" + actual + "</pre></td></tr>";
output += "<tr class='test-diff'><th>Diff: </th><td><pre>" + QUnit.diff( expected, actual ) + "</pre></td></tr>";
}
@@ -812,7 +927,7 @@ extend( QUnit, {
if ( source ) {
details.source = source;
- output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeInnerText( source ) + "</pre></td></tr>";
+ output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr>";
}
output += "</table>";
@@ -839,19 +954,19 @@ extend( QUnit, {
message: message
};
- message = escapeInnerText( message ) || "error";
+ message = escapeText( message ) || "error";
message = "<span class='test-message'>" + message + "</span>";
output = message;
output += "<table>";
if ( actual ) {
- output += "<tr class='test-actual'><th>Result: </th><td><pre>" + escapeInnerText( actual ) + "</pre></td></tr>";
+ output += "<tr class='test-actual'><th>Result: </th><td><pre>" + escapeText( actual ) + "</pre></td></tr>";
}
if ( source ) {
details.source = source;
- output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeInnerText( source ) + "</pre></td></tr>";
+ output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr>";
}
output += "</table>";
@@ -876,7 +991,8 @@ extend( QUnit, {
querystring += encodeURIComponent( key ) + "=" +
encodeURIComponent( params[ key ] ) + "&";
}
- return window.location.pathname + querystring.slice( 0, -1 );
+ return window.location.protocol + "//" + window.location.host +
+ window.location.pathname + querystring.slice( 0, -1 );
},
extend: extend,
@@ -907,7 +1023,7 @@ extend( QUnit.constructor.prototype, {
// testStart: { name }
testStart: registerLoggingCallback( "testStart" ),
- // testDone: { name, failed, passed, total }
+ // testDone: { name, failed, passed, total, duration }
testDone: registerLoggingCallback( "testDone" ),
// moduleStart: { name }
@@ -925,9 +1041,10 @@ QUnit.load = function() {
runLoggingCallbacks( "begin", QUnit, {} );
// Initialize the config, saving the execution queue
- var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, urlConfigCheckboxes, moduleFilter,
- numModules = 0,
- moduleFilterHtml = "",
+ var banner, filter, i, label, len, main, ol, toolbar, userAgent, val,
+ urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter,
+ numModules = 0,
+ moduleFilterHtml = "",
urlConfigHtml = "",
oldconfig = extend( {}, config );
@@ -948,14 +1065,24 @@ QUnit.load = function() {
};
}
config[ val.id ] = QUnit.urlParams[ val.id ];
- urlConfigHtml += "<input id='qunit-urlconfig-" + val.id + "' name='" + val.id + "' type='checkbox'" + ( config[ val.id ] ? " checked='checked'" : "" ) + " title='" + val.tooltip + "'><label for='qunit-urlconfig-" + val.id + "' title='" + val.tooltip + "'>" + val.label + "</label>";
+ urlConfigHtml += "<input id='qunit-urlconfig-" + escapeText( val.id ) +
+ "' name='" + escapeText( val.id ) +
+ "' type='checkbox'" + ( config[ val.id ] ? " checked='checked'" : "" ) +
+ " title='" + escapeText( val.tooltip ) +
+ "'><label for='qunit-urlconfig-" + escapeText( val.id ) +
+ "' title='" + escapeText( val.tooltip ) + "'>" + val.label + "</label>";
}
- moduleFilterHtml += "<label for='qunit-modulefilter'>Module: </label><select id='qunit-modulefilter' name='modulefilter'><option value='' " + ( config.module === undefined ? "selected" : "" ) + ">< All Modules ></option>";
+ moduleFilterHtml += "<label for='qunit-modulefilter'>Module: </label><select id='qunit-modulefilter' name='modulefilter'><option value='' " +
+ ( config.module === undefined ? "selected='selected'" : "" ) +
+ ">< All Modules ></option>";
+
for ( i in config.modules ) {
if ( config.modules.hasOwnProperty( i ) ) {
numModules += 1;
- moduleFilterHtml += "<option value='" + encodeURIComponent(i) + "' " + ( config.module === i ? "selected" : "" ) + ">" + i + "</option>";
+ moduleFilterHtml += "<option value='" + escapeText( encodeURIComponent(i) ) + "' " +
+ ( config.module === i ? "selected='selected'" : "" ) +
+ ">" + escapeText(i) + "</option>";
}
}
moduleFilterHtml += "</select>";
@@ -1014,22 +1141,28 @@ QUnit.load = function() {
label.innerHTML = "Hide passed tests";
toolbar.appendChild( label );
- urlConfigCheckboxes = document.createElement( 'span' );
- urlConfigCheckboxes.innerHTML = urlConfigHtml;
- addEvent( urlConfigCheckboxes, "change", function( event ) {
- var params = {};
- params[ event.target.name ] = event.target.checked ? true : undefined;
+ urlConfigCheckboxesContainer = document.createElement("span");
+ urlConfigCheckboxesContainer.innerHTML = urlConfigHtml;
+ urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input");
+ // For oldIE support:
+ // * Add handlers to the individual elements instead of the container
+ // * Use "click" instead of "change"
+ // * Fallback from event.target to event.srcElement
+ addEvents( urlConfigCheckboxes, "click", function( event ) {
+ var params = {},
+ target = event.target || event.srcElement;
+ params[ target.name ] = target.checked ? true : undefined;
window.location = QUnit.url( params );
});
- toolbar.appendChild( urlConfigCheckboxes );
+ toolbar.appendChild( urlConfigCheckboxesContainer );
if (numModules > 1) {
moduleFilter = document.createElement( 'span' );
moduleFilter.setAttribute( 'id', 'qunit-modulefilter-container' );
moduleFilter.innerHTML = moduleFilterHtml;
- addEvent( moduleFilter, "change", function() {
+ addEvent( moduleFilter.lastChild, "change", function() {
var selectBox = moduleFilter.getElementsByTagName("select")[0],
- selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value);
+ selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value);
window.location = QUnit.url( { module: ( selectedModule === "" ) ? undefined : selectedModule } );
});
@@ -1106,7 +1239,7 @@ function done() {
" milliseconds.<br/>",
"<span class='passed'>",
passed,
- "</span> tests of <span class='total'>",
+ "</span> assertions of <span class='total'>",
config.stats.all,
"</span> passed, <span class='failed'>",
config.stats.bad,
@@ -1199,7 +1332,7 @@ function validTest( test ) {
function extractStacktrace( e, offset ) {
offset = offset === undefined ? 3 : offset;
- var stack, include, i, regex;
+ var stack, include, i;
if ( e.stacktrace ) {
// Opera
@@ -1213,7 +1346,7 @@ function extractStacktrace( e, offset ) {
if ( fileName ) {
include = [];
for ( i = offset; i < stack.length; i++ ) {
- if ( stack[ i ].indexOf( fileName ) != -1 ) {
+ if ( stack[ i ].indexOf( fileName ) !== -1 ) {
break;
}
include.push( stack[ i ] );
@@ -1242,17 +1375,27 @@ function sourceFromStacktrace( offset ) {
}
}
-function escapeInnerText( s ) {
+/**
+ * Escape text for attribute or text content.
+ */
+function escapeText( s ) {
if ( !s ) {
return "";
}
s = s + "";
- return s.replace( /[\&<>]/g, function( s ) {
+ // Both single quotes and double quotes (for attributes)
+ return s.replace( /['"<>&]/g, function( s ) {
switch( s ) {
- case "&": return "&amp;";
- case "<": return "&lt;";
- case ">": return "&gt;";
- default: return s;
+ case '\'':
+ return '&#039;';
+ case '"':
+ return '&quot;';
+ case '<':
+ return '&lt;';
+ case '>':
+ return '&gt;';
+ case '&':
+ return '&amp;';
}
});
}
@@ -1300,7 +1443,7 @@ function saveGlobal() {
}
}
-function checkPollution( name ) {
+function checkPollution() {
var newGlobals,
deletedGlobals,
old = config.pollution;
@@ -1349,16 +1492,53 @@ function extend( a, b ) {
return a;
}
+/**
+ * @param {HTMLElement} elem
+ * @param {string} type
+ * @param {Function} fn
+ */
function addEvent( elem, type, fn ) {
+ // Standards-based browsers
if ( elem.addEventListener ) {
elem.addEventListener( type, fn, false );
- } else if ( elem.attachEvent ) {
- elem.attachEvent( "on" + type, fn );
+ // IE
} else {
- fn();
+ elem.attachEvent( "on" + type, fn );
}
}
+/**
+ * @param {Array|NodeList} elems
+ * @param {string} type
+ * @param {Function} fn
+ */
+function addEvents( elems, type, fn ) {
+ var i = elems.length;
+ while ( i-- ) {
+ addEvent( elems[i], type, fn );
+ }
+}
+
+function hasClass( elem, name ) {
+ return (" " + elem.className + " ").indexOf(" " + name + " ") > -1;
+}
+
+function addClass( elem, name ) {
+ if ( !hasClass( elem, name ) ) {
+ elem.className += (elem.className ? " " : "") + name;
+ }
+}
+
+function removeClass( elem, name ) {
+ var set = " " + elem.className + " ";
+ // Class name may appear multiple times
+ while ( set.indexOf(" " + name + " ") > -1 ) {
+ set = set.replace(" " + name + " " , " ");
+ }
+ // If possible, trim it for prettiness, but not neccecarily
+ elem.className = window.jQuery ? jQuery.trim( set ) : ( set.trim ? set.trim() : set );
+}
+
function id( name ) {
return !!( typeof document !== "undefined" && document && document.getElementById ) &&
document.getElementById( name );
@@ -1372,7 +1552,6 @@ function registerLoggingCallback( key ) {
// Supports deprecated method of completely overwriting logging callbacks
function runLoggingCallbacks( key, scope, args ) {
- //debugger;
var i, callbacks;
if ( QUnit.hasOwnProperty( key ) ) {
QUnit[ key ].call(scope, args );
@@ -1414,6 +1593,7 @@ QUnit.equiv = (function() {
// for string, boolean, number and null
function useStrictEquality( b, a ) {
+ /*jshint eqeqeq:false */
if ( b instanceof a.constructor || a instanceof b.constructor ) {
// to catch short annotaion VS 'new' annotation of a
// declaration
@@ -1610,7 +1790,8 @@ QUnit.jsDump = (function() {
var reName = /^function (\w+)/,
jsDump = {
- parse: function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance
+ // type is used mostly internally, you can fix a (custom)type in advance
+ parse: function( obj, type, stack ) {
stack = stack || [ ];
var inStack, res,
parser = this.parsers[ type || this.typeOf(obj) ];
@@ -1618,18 +1799,16 @@ QUnit.jsDump = (function() {
type = typeof parser;
inStack = inArray( obj, stack );
- if ( inStack != -1 ) {
+ if ( inStack !== -1 ) {
return "recursion(" + (inStack - stack.length) + ")";
}
- //else
- if ( type == "function" ) {
+ if ( type === "function" ) {
stack.push( obj );
res = parser.call( this, obj, stack );
stack.pop();
return res;
}
- // else
- return ( type == "string" ) ? parser : this.parsers.error;
+ return ( type === "string" ) ? parser : this.parsers.error;
},
typeOf: function( obj ) {
var type;
@@ -1656,6 +1835,8 @@ QUnit.jsDump = (function() {
( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) )
) {
type = "array";
+ } else if ( obj.constructor === Error.prototype.constructor ) {
+ type = "error";
} else {
type = typeof obj;
}
@@ -1664,7 +1845,8 @@ QUnit.jsDump = (function() {
separator: function() {
return this.multiline ? this.HTML ? "<br />" : "\n" : this.HTML ? "&nbsp;" : " ";
},
- indent: function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing
+ // extra can be a number, shortcut for increasing-calling-decreasing
+ indent: function( extra ) {
if ( !this.multiline ) {
return "";
}
@@ -1693,13 +1875,16 @@ QUnit.jsDump = (function() {
parsers: {
window: "[Window]",
document: "[Document]",
- error: "[ERROR]", //when no parser is found, shouldn"t happen
+ error: function(error) {
+ return "Error(\"" + error.message + "\")";
+ },
unknown: "[Unknown]",
"null": "null",
"undefined": "undefined",
"function": function( fn ) {
var ret = "function",
- name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];//functions never have name in IE
+ // functions never have name in IE
+ name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];
if ( name ) {
ret += " " + name;
@@ -1715,13 +1900,9 @@ QUnit.jsDump = (function() {
object: function( map, stack ) {
var ret = [ ], keys, key, val, i;
QUnit.jsDump.up();
- if ( Object.keys ) {
- keys = Object.keys( map );
- } else {
- keys = [];
- for ( key in map ) {
- keys.push( key );
- }
+ keys = [];
+ for ( key in map ) {
+ keys.push( key );
}
keys.sort();
for ( i = 0; i < keys.length; i++ ) {
@@ -1733,21 +1914,34 @@ QUnit.jsDump = (function() {
return join( "{", ret, "}" );
},
node: function( node ) {
- var a, val,
+ var len, i, val,
open = QUnit.jsDump.HTML ? "&lt;" : "<",
close = QUnit.jsDump.HTML ? "&gt;" : ">",
tag = node.nodeName.toLowerCase(),
- ret = open + tag;
-
- for ( a in QUnit.jsDump.DOMAttrs ) {
- val = node[ QUnit.jsDump.DOMAttrs[a] ];
- if ( val ) {
- ret += " " + a + "=" + QUnit.jsDump.parse( val, "attribute" );
+ ret = open + tag,
+ attrs = node.attributes;
+
+ if ( attrs ) {
+ for ( i = 0, len = attrs.length; i < len; i++ ) {
+ val = attrs[i].nodeValue;
+ // IE6 includes all attributes in .attributes, even ones not explicitly set.
+ // Those have values like undefined, null, 0, false, "" or "inherit".
+ if ( val && val !== "inherit" ) {
+ ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" );
+ }
}
}
- return ret + close + open + "/" + tag + close;
+ ret += close;
+
+ // Show content of TextNode or CDATASection
+ if ( node.nodeType === 3 || node.nodeType === 4 ) {
+ ret += node.nodeValue;
+ }
+
+ return ret + open + "/" + tag + close;
},
- functionArgs: function( fn ) {//function calls it internally, it's the arguments part of the function
+ // function calls it internally, it's the arguments part of the function
+ functionArgs: function( fn ) {
var args,
l = fn.length;
@@ -1757,54 +1951,34 @@ QUnit.jsDump = (function() {
args = new Array(l);
while ( l-- ) {
- args[l] = String.fromCharCode(97+l);//97 is 'a'
+ // 97 is 'a'
+ args[l] = String.fromCharCode(97+l);
}
return " " + args.join( ", " ) + " ";
},
- key: quote, //object calls it internally, the key part of an item in a map
- functionCode: "[code]", //function calls it internally, it's the content of the function
- attribute: quote, //node calls it internally, it's an html attribute value
+ // object calls it internally, the key part of an item in a map
+ key: quote,
+ // function calls it internally, it's the content of the function
+ functionCode: "[code]",
+ // node calls it internally, it's an html attribute value
+ attribute: quote,
string: quote,
date: quote,
- regexp: literal, //regex
+ regexp: literal,
number: literal,
"boolean": literal
},
- DOMAttrs: {
- //attributes to dump from nodes, name=>realName
- id: "id",
- name: "name",
- "class": "className"
- },
- HTML: false,//if true, entities are escaped ( <, >, \t, space and \n )
- indentChar: " ",//indentation unit
- multiline: true //if true, items in a collection, are separated by a \n, else just a space.
+ // if true, entities are escaped ( <, >, \t, space and \n )
+ HTML: false,
+ // indentation unit
+ indentChar: " ",
+ // if true, items in a collection, are separated by a \n, else just a space.
+ multiline: true
};
return jsDump;
}());
-// from Sizzle.js
-function getText( elems ) {
- var i, elem,
- ret = "";
-
- for ( i = 0; elems[i]; i++ ) {
- elem = elems[i];
-
- // Get the text from text nodes and CDATA nodes
- if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
- ret += elem.nodeValue;
-
- // Traverse everything else, except comment nodes
- } else if ( elem.nodeType !== 8 ) {
- ret += getText( elem.childNodes );
- }
- }
-
- return ret;
-}
-
// from jquery.js
function inArray( elem, array ) {
if ( array.indexOf ) {
@@ -1835,13 +2009,14 @@ function inArray( elem, array ) {
* QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick <del>brown </del> fox <del>jumped </del><ins>jumps </ins> over"
*/
QUnit.diff = (function() {
+ /*jshint eqeqeq:false, eqnull:true */
function diff( o, n ) {
var i,
ns = {},
os = {};
for ( i = 0; i < n.length; i++ ) {
- if ( ns[ n[i] ] == null ) {
+ if ( !hasOwn.call( ns, n[i] ) ) {
ns[ n[i] ] = {
rows: [],
o: null
@@ -1851,7 +2026,7 @@ QUnit.diff = (function() {
}
for ( i = 0; i < o.length; i++ ) {
- if ( os[ o[i] ] == null ) {
+ if ( !hasOwn.call( os, o[i] ) ) {
os[ o[i] ] = {
rows: [],
n: null
@@ -1864,7 +2039,7 @@ QUnit.diff = (function() {
if ( !hasOwn.call( ns, i ) ) {
continue;
}
- if ( ns[i].rows.length == 1 && typeof os[i] != "undefined" && os[i].rows.length == 1 ) {
+ if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) {
n[ ns[i].rows[0] ] = {
text: n[ ns[i].rows[0] ],
row: os[i].rows[0]
@@ -1970,7 +2145,7 @@ QUnit.diff = (function() {
// for CommonJS enviroments, export everything
if ( typeof exports !== "undefined" ) {
- extend(exports, QUnit);
+ extend( exports, QUnit );
}
// get at whatever the global object is, like window in browsers
diff --git a/resources/jquery/jquery.spinner.css b/resources/jquery/jquery.spinner.css
index 4a775283..a9e06dbe 100644
--- a/resources/jquery/jquery.spinner.css
+++ b/resources/jquery/jquery.spinner.css
@@ -33,7 +33,7 @@
.mw-spinner-inline {
display: inline-block;
vertical-align: middle;
-
+
/* IE < 8 */
zoom: 1;
*display: inline;
diff --git a/resources/jquery/jquery.spinner.js b/resources/jquery/jquery.spinner.js
index 4a6ec3b4..27dabc6c 100644
--- a/resources/jquery/jquery.spinner.js
+++ b/resources/jquery/jquery.spinner.js
@@ -1,7 +1,9 @@
/**
- * jQuery spinner
+ * jQuery Spinner
*
* Simple jQuery plugin to create, inject and remove spinners.
+ *
+ * @class jQuery.plugin.spinner
*/
( function ( $ ) {
@@ -15,36 +17,37 @@
$.extend({
/**
- * Creates a spinner element.
+ * Create a spinner element
*
* The argument is an object with options used to construct the spinner. These can be:
*
* 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
+ * 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
+ * - .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 );
- * @example
+ *
* // 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 );
- * @example
+ *
* // The following two are equivalent:
* $.createSpinner( 'magic' );
* $.createSpinner({ id: 'magic' });
*
- * @param {Object|String} opts [optional] ID string or options:
- * - id: If given, spinner will be given an id of "mw-spinner-<id>"
+ * @static
+ * @inheritable
+ * @param {Object|string} [opts] ID string or options:
+ * - id: If given, spinner will be given an id of "mw-spinner-{id}"
* - size: 'small' (default) or 'large' for a 20-pixel or 32-pixel spinner
* - type: 'inline' (default) 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
@@ -72,10 +75,12 @@
},
/**
- * Removes a spinner element.
+ * Remove a spinner element
*
- * @param {String} id [optional] Id of the spinner, as passed to createSpinner.
- * @return {jQuery} The (now detached) spinner.
+ * @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();
@@ -83,13 +88,21 @@
});
/**
- * Injects a spinner after the elements in the jQuery collection
- * (as siblings, not children). Collection contents remain unchanged.
+ * 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} opts See createSpinner() for description.
+ * @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/jquery/jquery.suggestions.js b/resources/jquery/jquery.suggestions.js
index d80680fc..28e2afc4 100644
--- a/resources/jquery/jquery.suggestions.js
+++ b/resources/jquery/jquery.suggestions.js
@@ -13,11 +13,11 @@
*
* Options:
*
- * fetch(query): Callback that should fetch suggestions and set the suggestions property. Executed in the context of the
- * textbox
+ * 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
+ * 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'
@@ -33,12 +33,12 @@
* Type: Number, Range: 0 - 1200, Default: 120
* 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.
+ * 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' ).
+ * 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
@@ -49,8 +49,8 @@
$.suggestions = {
/**
- * Cancel any delayed updateSuggestions() call and inform the user so
- * they can cancel their result fetching if they use AJAX or something
+ * 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 ) {
@@ -60,28 +60,35 @@ $.suggestions = {
context.config.cancel.call( context.data.$textbox );
}
},
+
/**
- * 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
+ * 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.
+ * 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 ) {
- // Only fetch if the value in the textbox changed and is not empty
+ // 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
function maybeFetch() {
if ( context.data.$textbox.val().length === 0 ) {
context.data.$container.hide();
context.data.prevText = '';
- } else if ( context.data.$textbox.val() !== context.data.prevText ) {
+ } else if (
+ context.data.$textbox.val() !== context.data.prevText ||
+ !context.data.$container.is( ':visible' )
+ ) {
if ( typeof context.config.fetch === 'function' ) {
context.data.prevText = context.data.$textbox.val();
context.config.fetch.call( context.data.$textbox, context.data.$textbox.val() );
@@ -89,18 +96,19 @@ $.suggestions = {
}
}
- // Cancel previous call
- if ( context.data.timerID !== null ) {
- clearTimeout( context.data.timerID );
- }
+ // Cancels any delayed maybeFetch call, and invokes context.config.cancel.
+ $.suggestions.cancel( context );
+
if ( delayed ) {
- // Start a new asynchronous call
+ // 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();
}
$.suggestions.special( context );
},
+
special: function ( context ) {
// Allow custom rendering - but otherwise don't do any rendering
if ( typeof context.config.special.render === 'function' ) {
@@ -108,17 +116,21 @@ $.suggestions = {
setTimeout( function () {
// Render special
var $special = context.data.$container.find( '.suggestions-special' );
- context.config.special.render.call( $special, context.data.$textbox.val() );
+ 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;
+ var newCSS,
+ $autoEllipseMe, $result, $results, childrenWidth,
+ i, expWidth, matchedText, maxWidth, text;
+
// Validate creation using fallback values
switch( property ) {
case 'fetch':
@@ -208,59 +220,66 @@ $.suggestions = {
} else {
// Expand from right
newCSS.left = 'auto';
- newCSS.right = $( 'body' ).width() - ( context.config.$region.offset().left + context.config.$region.outerWidth() );
+ newCSS.right = $( document ).width() - ( context.config.$region.offset().left + context.config.$region.outerWidth() );
}
context.data.$container.css( newCSS );
- var $results = context.data.$container.children( '.suggestions-results' );
+ $results = context.data.$container.children( '.suggestions-results' );
$results.empty();
- var expWidth = -1;
- var $autoEllipseMe = $( [] );
- var matchedText = null;
- for ( var i = 0; i < context.config.suggestions.length; i++ ) {
+ expWidth = -1;
+ $autoEllipseMe = $( [] );
+ matchedText = null;
+ for ( i = 0; i < context.config.suggestions.length; i++ ) {
/*jshint loopfunc:true */
- var text = context.config.suggestions[i];
- var $result = $( '<div>' )
+ text = context.config.suggestions[i];
+ $result = $( '<div>' )
.addClass( 'suggestions-result' )
.attr( 'rel', i )
.data( 'text', context.config.suggestions[i] )
- .mousemove( function ( e ) {
+ .mousemove( function () {
context.data.selectedWithMouse = true;
$.suggestions.highlight(
- context, $(this).closest( '.suggestions-results div' ), false
+ 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.config.result.render.call( $result, context.config.suggestions[i], context );
} else {
// Add <span> with text
- if( context.config.highlightInput ) {
- matchedText = context.data.prevText;
- }
$result.append( $( '<span>' )
.css( 'whiteSpace', 'nowrap' )
.text( text )
);
+ }
- // Widen results box if needed
- // New width is only calculated here, applied later
- var $span = $result.children( 'span' );
- if ( $span.outerWidth() > $result.width() && $span.outerWidth() > expWidth ) {
- // factor in any padding, margin, or border space on the parent
- expWidth = $span.outerWidth() + ( context.data.$container.width() - $span.parent().width());
- }
- $autoEllipseMe = $autoEllipseMe.add( $result );
+ if ( context.config.highlightInput ) {
+ matchedText = context.data.prevText;
}
+
+ // Widen results box if needed
+ // New width is only calculated here, applied later
+ childrenWidth = $result.children().outerWidth();
+ 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() );
+ }
+ $autoEllipseMe = $autoEllipseMe.add( $result );
}
// Apply new width for results box, if any
if ( expWidth > context.data.$container.width() ) {
- var maxWidth = context.config.maxExpandFactor*context.data.$textbox.width();
+ maxWidth = context.config.maxExpandFactor*context.data.$textbox.width();
context.data.$container.width( Math.min( expWidth, maxWidth ) );
}
// autoEllipse the results. Has to be done after changing the width
- $autoEllipseMe.autoEllipsis( { hasSpan: true, tooltip: true, matchText: matchedText } );
+ $autoEllipseMe.autoEllipsis( {
+ hasSpan: true,
+ tooltip: true,
+ matchText: matchedText
+ } );
}
}
break;
@@ -280,6 +299,7 @@ $.suggestions = {
break;
}
},
+
/**
* Highlight a result in the results table
* @param result <tr> to highlight: jQuery object, or 'prev' or 'next'
@@ -289,30 +309,40 @@ $.suggestions = {
var selected = context.data.$container.find( '.suggestions-result-current' );
if ( !result.get || selected.get( 0 ) !== result.get( 0 ) ) {
if ( result === 'prev' ) {
- if( selected.is( '.suggestions-special' ) ) {
+ 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 div:last' );
+ 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 div:first' );
+ 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 ( selected.is( '.suggestions-special' ) ) {
+ 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 &&
@@ -338,13 +368,16 @@ $.suggestions = {
context.data.$textbox.trigger( 'change' );
}
},
+
/**
* Respond to keypress event
* @param key Integer Code of key pressed
*/
keypress: function ( e, context, key ) {
- var wasVisible = context.data.$container.is( ':visible' ),
+ var selected,
+ wasVisible = context.data.$container.is( ':visible' ),
preventDefault = false;
+
switch ( key ) {
// Arrow down
case 40:
@@ -376,7 +409,7 @@ $.suggestions = {
case 13:
context.data.$container.hide();
preventDefault = wasVisible;
- var selected = context.data.$container.find( '.suggestions-result-current' );
+ selected = context.data.$container.find( '.suggestions-result-current' );
if ( selected.length === 0 || context.data.selectedWithMouse ) {
// if nothing is selected OR if something was selected with the mouse,
// cancel any current requests and submit the form
@@ -420,18 +453,18 @@ $.fn.suggestions = function () {
if ( context === undefined || context === null ) {
context = {
config: {
- 'fetch' : function () {},
- 'cancel': function () {},
- 'special': {},
- 'result': {},
- '$region': $(this),
- 'suggestions': [],
- 'maxRows': 7,
- 'delay': 120,
- 'submitOnClick': false,
- 'maxExpandFactor': 3,
- 'expandFrom': 'auto',
- 'highlightInput': false
+ fetch: function () {},
+ cancel: function () {},
+ special: {},
+ result: {},
+ $region: $(this),
+ suggestions: [],
+ maxRows: 7,
+ delay: 120,
+ submitOnClick: false,
+ maxExpandFactor: 3,
+ expandFrom: 'auto',
+ highlightInput: false
}
};
}
@@ -480,44 +513,56 @@ $.fn.suggestions = function () {
.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
+ // 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 div' );
+ context.data.mouseDownOn = $( e.target ).closest( '.suggestions-results .suggestions-result' );
} )
.mouseup( function ( e ) {
- var $result = $( e.target ).closest( '.suggestions-results div' );
- var $other = context.data.mouseDownOn;
+ var $result = $( e.target ).closest( '.suggestions-results .suggestions-result' ),
+ $other = context.data.mouseDownOn;
+
context.data.mouseDownOn = $( [] );
if ( $result.get( 0 ) !== $other.get( 0 ) ) {
return;
}
- $.suggestions.highlight( context, $result, true );
- context.data.$container.hide();
- if ( typeof context.config.result.select === 'function' ) {
- context.config.result.select.call( $result, context.data.$textbox );
+ // 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 );
+ context.data.$container.hide();
+ if ( typeof context.config.result.select === 'function' ) {
+ context.config.result.select.call( $result, context.data.$textbox );
+ }
}
+ // but still restore focus to the textbox, so that the suggestions will be hidden properly
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
+ // 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' );
- var $other = context.data.mouseDownOn;
+ var $special = $( e.target ).closest( '.suggestions-special' ),
+ $other = context.data.mouseDownOn;
+
context.data.mouseDownOn = $( [] );
if ( $special.get( 0 ) !== $other.get( 0 ) ) {
return;
}
- context.data.$container.hide();
- if ( typeof context.config.special.select === 'function' ) {
- context.config.special.select.call( $special, context.data.$textbox );
+ // 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 ) ) {
+ context.data.$container.hide();
+ if ( typeof context.config.special.select === 'function' ) {
+ context.config.special.select.call( $special, context.data.$textbox );
+ }
}
+ // but still restore focus to the textbox, so that the suggestions will be hidden properly
context.data.$textbox.focus();
} )
.mousemove( function ( e ) {
@@ -540,10 +585,12 @@ $.fn.suggestions = function () {
switch ( context.data.keypressed ) {
// This preventDefault logic is duplicated from
// $.suggestions.keypress(), which sucks
+ // Arrow down
case 40:
e.preventDefault();
e.stopImmediatePropagation();
break;
+ // Arrow up, Escape and Enter
case 38:
case 27:
case 13:
diff --git a/resources/jquery/jquery.tablesorter.js b/resources/jquery/jquery.tablesorter.js
index 3ef71d57..b3d7bb3d 100644
--- a/resources/jquery/jquery.tablesorter.js
+++ b/resources/jquery/jquery.tablesorter.js
@@ -8,8 +8,9 @@
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
- * Depends on mw.config (wgDigitTransformTable, wgMonthNames, wgMonthNamesShort,
- * wgDefaultDateFormat, wgContentLanguage)
+ * Depends on mw.config (wgDigitTransformTable, wgDefaultDateFormat, wgContentLanguage)
+ * and mw.language.months.
+ *
* Uses 'tableSorterCollation' in mw.config (if available)
*/
/**
@@ -19,6 +20,9 @@
* @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"
@@ -44,9 +48,16 @@
* 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
@@ -57,6 +68,7 @@
*/
( function ( $, mw ) {
+ /*jshint onevar:false */
/* Local scope */
@@ -75,7 +87,7 @@
return false;
}
- function getElementText( node ) {
+ function getElementSortKey( node ) {
var $node = $( node ),
// Use data-sort-value attribute.
// Use data() instead of attr() so that live value changes
@@ -87,15 +99,20 @@
// like charAt, toLowerCase and split are expected.
return String( data );
} else {
- return $node.text();
- }
- }
-
- function getTextFromRowAndCellIndex( rows, rowIndex, cellIndex ) {
- if ( rows[rowIndex] && rows[rowIndex].cells[cellIndex] ) {
- return $.trim( getElementText( rows[rowIndex].cells[cellIndex] ) );
- } else {
- return '';
+ 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( '' );
+ }
}
}
@@ -108,8 +125,13 @@
concurrent = 0,
needed = ( rows.length > 4 ) ? 5 : rows.length;
- while( i < l ) {
- nodeValue = getTextFromRowAndCellIndex( rows, rowIndex, cellIndex );
+ 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++;
@@ -151,7 +173,7 @@
for ( i = 0; i < len; i++ ) {
parser = false;
- sortType = $headers.eq( i ).data( 'sort-type' );
+ sortType = $headers.eq( i ).data( 'sortType' );
if ( sortType !== undefined ) {
parser = getParserById( sortType );
}
@@ -194,7 +216,7 @@
cache.row.push( $row );
for ( var j = 0; j < totalCells; ++j ) {
- cols.push( parsers[j].format( getElementText( $row[0].cells[j] ), table, $row[0].cells[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
@@ -223,6 +245,8 @@
}
table.tBodies[0].appendChild( fragment );
+
+ $( table ).trigger( 'sortEnd.tablesorter' );
}
/**
@@ -264,44 +288,113 @@
function buildHeaders( table, msg ) {
var maxSeen = 0,
+ colspanOffset = 0,
longest,
- realCellIndex = 0,
- $tableHeaders = $( 'thead:eq(0) > tr', table );
- if ( $tableHeaders.length > 1 ) {
- $tableHeaders.each( function () {
- if ( this.cells.length > maxSeen ) {
- maxSeen = this.cells.length;
- longest = this;
+ columns,
+ i,
+ $tableHeaders = $( [] ),
+ $tableRows = $( 'thead:eq(0) > tr', table );
+ if ( $tableRows.length <= 1 ) {
+ $tableHeaders = $tableRows.children( 'th' );
+ } else {
+ // We need to find the cells of the row containing the most columns
+ var rowspan,
+ headersIndex = [];
+ $tableRows.each( function ( rowIndex ) {
+ $.each( this.cells, function( index2, cell ) {
+ rowspan = Number( cell.rowSpan );
+ for ( i = 0; i < rowspan; i++ ) {
+ if ( headersIndex[rowIndex+i] === undefined ) {
+ headersIndex[rowIndex+i] = $( [] );
+ }
+ headersIndex[rowIndex+i].push( cell );
+ }
+ } );
+ } );
+ $.each( headersIndex, function ( index, cellArray ) {
+ if ( cellArray.length >= maxSeen ) {
+ maxSeen = cellArray.length;
+ longest = index;
}
- });
- $tableHeaders = $( longest );
+ } );
+ $tableHeaders = headersIndex[longest];
}
- $tableHeaders = $tableHeaders.children( 'th' ).each( function ( index ) {
- this.column = realCellIndex;
- var colspan = this.colspan;
- colspan = colspan ? parseInt( colspan, 10 ) : 1;
- realCellIndex += colspan;
+ // 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 ).is( '.unsortable' ) ) {
+ if ( $( this ).hasClass( table.config.unsortableClass ) ) {
this.sortDisabled = true;
}
if ( !this.sortDisabled ) {
- var $th = $( this ).addClass( table.config.cssHeader ).attr( 'title', msg[1] );
+ $( this )
+ .addClass( table.config.cssHeader )
+ .prop( 'tabIndex', 0 )
+ .attr( {
+ role: 'columnheader button',
+ title: msg[1]
+ } );
}
// add cell to headerList
- table.config.headerList[index] = this;
+ 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 l = a.length;
for ( var i = 0; i < l; i++ ) {
@@ -312,20 +405,14 @@
return false;
}
- function setHeadersCss( table, $headers, list, css, msg ) {
- // Remove all header information
- $headers.removeClass( css[0] ).removeClass( css[1] );
-
- var h = [];
- $headers.each( function ( offset ) {
- if ( !this.sortDisabled ) {
- h[this.column] = $( this );
- }
- } );
+ 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] );
- var l = list.length;
- for ( var i = 0; i < l; i++ ) {
- h[ list[i][0] ].addClass( css[ list[i][1] ] ).attr( 'title', msg[ list[i][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] ] );
}
}
@@ -368,8 +455,8 @@
ts.transformTable = {};
// Unpack the transform table
- var ascii = separatorTransformTable[0].split( "\t" ).concat( digitTransformTable[0].split( "\t" ) );
- var localised = separatorTransformTable[1].split( "\t" ).concat( digitTransformTable[1].split( "\t" ) );
+ var ascii = separatorTransformTable[0].split( '\t' ).concat( digitTransformTable[0].split( '\t' ) );
+ var localised = separatorTransformTable[1].split( '\t' ).concat( digitTransformTable[1].split( '\t' ) );
// Construct regex for number identification
for ( var i = 0; i < ascii.length; i++ ) {
@@ -381,21 +468,24 @@
// 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");
+ 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 regex = [];
ts.monthNames = {};
- for ( var i = 1; i < 13; i++ ) {
- var name = mw.config.get( 'wgMonthNames' )[i].toLowerCase();
- ts.monthNames[name] = i;
+ for ( var i = 0; i < 12; i++ ) {
+ var name = mw.language.months.names[i].toLowerCase();
+ ts.monthNames[name] = i + 1;
regex.push( $.escapeRE( name ) );
- name = mw.config.get( 'wgMonthNamesShort' )[i].toLowerCase().replace( '.', '' );
- ts.monthNames[name] = i;
+ 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 ) );
}
@@ -407,31 +497,93 @@
ts.dateRegex[0] = new RegExp( /^\s*(\d{1,2})[\,\.\-\/'\s]{1,2}(\d{1,2})[\,\.\-\/'\s]{1,2}(\d{2,4})\s*?/i);
// Written Month name, dmy
- ts.dateRegex[1] = new RegExp( '^\\s*(\\d{1,2})[\\,\\.\\-\\/\'\\s]*(' + regex + ')' + '[\\,\\.\\-\\/\'\\s]*(\\d{2,4})\\s*$', 'i' );
+ ts.dateRegex[1] = new RegExp( '^\\s*(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(' + regex + ')' + '[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$', 'i' );
// Written Month name, mdy
- ts.dateRegex[2] = new RegExp( '^\\s*(' + regex + ')' + '[\\,\\.\\-\\/\'\\s]*(\\d{1,2})[\\,\\.\\-\\/\'\\s]*(\\d{2,4})\\s*$', 'i' );
+ ts.dateRegex[2] = new RegExp( '^\\s*(' + regex + ')' + '[\\,\\.\\-\\/\'\\s]+(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$', 'i' );
}
+ /**
+ * 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 ) {
- // Split multi row cells into multiple cells with the same content
- $table.find( '> tbody > tr > [rowspan]' ).each(function () {
- var rowSpan = this.rowSpan;
- this.rowSpan = 1;
- var cell = $( this );
- var next = cell.parent().nextAll();
+ var 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 col = 0;
+ var l = this.cells.length;
+ for ( var 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();
+
+ var spanningRealCellIndex, rowSpan, colSpan;
+ 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();
+ }
+
+ var cell = rowspanCells.shift();
+ rowSpan = cell.rowSpan;
+ colSpan = cell.colSpan;
+ spanningRealCellIndex = cell.realCellIndex;
+ cell.rowSpan = 1;
+ var $nextRows = $( cell ).parent().nextAll();
for ( var i = 0; i < rowSpan - 1; i++ ) {
- var td = next.eq( i ).children( 'td' );
- if ( !td.length ) {
- next.eq( i ).append( cell.clone() );
- } else if ( this.cellIndex === 0 ) {
- td.eq( this.cellIndex ).before( cell.clone() );
+ var $tds = $( $nextRows[i].cells ).filter( filterfunc );
+ var $clone = $( cell ).clone();
+ $clone[0].realCellIndex = spanningRealCellIndex;
+ if ( $tds.length ) {
+ $tds.each( fixTdCellIndex );
+ $tds.first().before( $clone );
} else {
- td.eq( this.cellIndex - 1 ).after( cell.clone() );
+ $nextRows.eq( i ).append( $clone );
}
}
- });
+ }
}
function buildCollationTable() {
@@ -480,6 +632,25 @@
};
}
+ /**
+ * 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 = {
@@ -492,6 +663,7 @@
sortInitialOrder: 'asc',
sortMultiSortKey: 'shiftKey',
sortLocaleCompare: false,
+ unsortableClass: 'unsortable',
parsers: {},
widgets: [],
headers: {},
@@ -512,9 +684,8 @@
construct: function ( $tables, settings ) {
return $tables.each( function ( i, table ) {
// Declare and cache.
- var $document, $headers, cache, config, sortOrder,
+ var $headers, cache, config,
$table = $( table ),
- shiftDown = 0,
firstTime = true;
// Quit if no tbody
@@ -531,8 +702,9 @@
return;
}
}
- $table.addClass( "jquery-tablesorter" );
+ $table.addClass( 'jquery-tablesorter' );
+ // FIXME config should probably not be stored in the plain table node
// New config object.
table.config = {};
@@ -540,7 +712,7 @@
config = $.extend( table.config, $.tablesorter.defaultOptions, settings );
// Save the settings where they read
- $.data( table, 'tablesorter', config );
+ $.data( table, 'tablesorter', { config: config } );
// Get the CSS class names, could be done else where.
var sortCSS = [ config.cssDesc, config.cssAsc ];
@@ -549,43 +721,58 @@
// Build headers
$headers = buildHeaders( table, sortMsg );
- // Grab and process locale settings
+ // Grab and process locale settings.
buildTransformTable();
buildDateTable();
- buildCollationTable();
// 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 $sortbottoms = $table.find( '> tbody > tr.sortbottom' );
+ if ( $sortbottoms.length ) {
+ var $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.click( function ( e ) {
- if ( 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
+ $headers.not( '.' + table.config.unsortableClass ).on( 'keypress click', function ( e ) {
+ 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 ( firstTime ) {
- firstTime = false;
-
- // 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 $sortbottoms = $table.find( '> tbody > tr.sortbottom' );
- if ( $sortbottoms.length ) {
- var $tfoot = $table.children( 'tfoot' );
- if ( $tfoot.length ) {
- $tfoot.eq(0).prepend( $sortbottoms );
- } else {
- $table.append( $( '<tfoot>' ).append( $sortbottoms ) );
- }
- }
+ if ( e.type === 'keypress' && e.which !== 13 ) {
+ // Only handle keypresses on the "Enter" key.
+ return true;
+ }
- explodeRowspans( $table );
- // try to auto detect column type, and store in tables config
- table.config.parsers = buildParserCache( table, $headers );
+ if ( firstTime ) {
+ setupForFirstSort();
}
// Build the cache for the tbody cells
@@ -598,46 +785,51 @@
var totalRows = ( $table[0].tBodies[0] && $table[0].tBodies[0].rows.length ) || 0;
if ( !table.sortDisabled && totalRows > 0 ) {
-
- // Cache jQuery object
- var $cell = $( this );
-
- // Get current column index
- var i = this.column;
-
// Get current column sort order
this.order = this.count % 2;
this.count++;
- // User only wants to sort on one column
- if ( !e[config.sortMultiSortKey] ) {
- // Flush the sort list
- config.sortList = [];
- // Add column to sort list
- config.sortList.push( [i, this.order] );
+ var cell = this;
+ // Get current column index
+ var columns = table.headerToColumns[ this.headerIndex ];
+ var newSortList = $.map( columns, function (c) {
+ // jQuery "helpfully" flattens the arrays...
+ return [[c, cell.order]];
+ });
+ // Index of first column belonging to this header
+ var i = columns[0];
- // Multi column sorting
+ 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 {
- // The user has clicked on an already sorted column.
+ // 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 ( var j = 0; j < config.sortList.length; j++ ) {
var s = config.sortList[j],
o = config.headerList[s[0]];
- if ( s[0] === i ) {
+ if ( isValueInArray( s[0], newSortList ) ) {
o.count = s[1];
o.count++;
s[1] = o.count % 2;
}
}
} else {
- // Add column to sort list array
- config.sortList.push( [i, this.order] );
+ // 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 );
+ setHeadersCss( $table[0], $headers, config.sortList, sortCSS, sortMsg, table.columnToHeader );
appendToTable(
$table[0], multisort( $table[0], config.sortList, cache )
);
@@ -655,6 +847,48 @@
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();
+ }
+
} );
},
@@ -672,10 +906,10 @@
},
formatDigit: function ( s ) {
+ var out, c, p, i;
if ( ts.transformTable !== false ) {
- var out = '',
- c;
- for ( var p = 0; p < s.length; p++ ) {
+ out = '';
+ for ( p = 0; p < s.length; p++ ) {
c = s.charAt(p);
if ( c in ts.transformTable ) {
out += ts.transformTable[c];
@@ -685,31 +919,22 @@
}
s = out;
}
- var i = parseFloat( s.replace( /[, ]/g, '' ).replace( "\u2212", '-' ) );
- return ( isNaN(i)) ? 0 : i;
+ i = parseFloat( s.replace( /[, ]/g, '' ).replace( '\u2212', '-' ) );
+ return isNaN( i ) ? 0 : i;
},
formatFloat: function ( s ) {
var i = parseFloat(s);
- return ( isNaN(i)) ? 0 : i;
+ return isNaN( i ) ? 0 : i;
},
formatInt: function ( s ) {
var i = parseInt( s, 10 );
- return ( isNaN(i)) ? 0 : i;
+ return isNaN( i ) ? 0 : i;
},
clearTableBody: function ( table ) {
- if ( $.browser.msie ) {
- var empty = function ( el ) {
- while ( el.firstChild ) {
- el.removeChild( el.firstChild );
- }
- };
- empty( table.tBodies[0] );
- } else {
- table.tBodies[0].innerHTML = '';
- }
+ $( table.tBodies[0] ).empty();
}
};
@@ -724,7 +949,7 @@
// Add default parsers
ts.addParser( {
id: 'text',
- is: function ( s ) {
+ is: function () {
return true;
},
format: function ( s ) {
@@ -815,7 +1040,7 @@
is: function ( s ) {
return ( ts.dateRegex[0].test(s) || ts.dateRegex[1].test(s) || ts.dateRegex[2].test(s ));
},
- format: function ( s, table ) {
+ format: function ( s ) {
var match;
s = $.trim( s.toLowerCase() );
@@ -824,6 +1049,10 @@
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] ];
@@ -872,7 +1101,7 @@
ts.addParser( {
id: 'number',
- is: function ( s, table ) {
+ is: function ( s ) {
return $.tablesorter.numberRegex.test( $.trim( s ));
},
format: function ( s ) {
diff --git a/resources/jquery/jquery.textSelection.js b/resources/jquery/jquery.textSelection.js
index abb0fa3f..2b5a4406 100644
--- a/resources/jquery/jquery.textSelection.js
+++ b/resources/jquery/jquery.textSelection.js
@@ -25,6 +25,11 @@
}
$.fn.textSelection = function ( command, options ) {
+ var fn,
+ context,
+ hasIframe,
+ needSave,
+ retval;
/**
* Helper function to get an IE TextRange object for an element
@@ -52,7 +57,7 @@
}
}
- var fn = {
+ fn = {
/**
* Get the contents of the textarea
*/
@@ -84,7 +89,7 @@
* Ported from skins/common/edit.js by Trevor Parscal
* (c) 2009 Wikimedia Foundation (GPLv2) - http://www.wikimedia.org
*
- * Inserts text at the begining and end of a text selection, optionally
+ * 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
@@ -168,16 +173,16 @@
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";
+ 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";
+ if ( range3.text !== '\r' && range3.text !== '\n' && range3.text !== '' ) {
+ insertText += '\n';
+ post += '\n';
}
}
@@ -216,13 +221,13 @@
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 ( 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";
+ if ( this.value.charAt( endPos ) !== '\n' && this.value.charAt( endPos ) !== '\r' ) {
+ insertText += '\n';
+ post += '\n';
}
}
this.value = this.value.substring( 0, startPos ) + insertText +
@@ -230,9 +235,9 @@
// 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" );
+ 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;
@@ -252,7 +257,7 @@
* 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 nessecarily characters)
+ * Get the position (in resolution of bytes not necessarily characters)
* in a textarea
*
* Will focus the textarea in some browsers (IE/Opera)
@@ -261,7 +266,21 @@
*/
getCaretPosition: function ( options ) {
function getCaret( e ) {
- var caretPos = 0, endPos = 0;
+ 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 ( 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
@@ -269,22 +288,12 @@
// whatever we do later (bug 31847).
activateElementOnIE( e );
- var
- preText, rawPreText, periText,
- rawPeriText, postText, rawPostText,
-
- // IE Support
- preFinished = false,
- periFinished = false,
- postFinished = false,
- // Range containing text in the selection
- periRange = document.selection.createRange().duplicate(),
- // Range containing text before the selection
- preRange,
- // Range containing text after the selection
- postRange;
-
- preRange = rangeForElementIE( 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 );
@@ -309,7 +318,7 @@
} else {
preRange.moveEnd( 'character', -1 );
if ( preRange.text === preText ) {
- rawPreText += "\r\n";
+ rawPreText += '\r\n';
} else {
preFinished = true;
}
@@ -321,7 +330,7 @@
} else {
periRange.moveEnd( 'character', -1 );
if ( periRange.text === periText ) {
- rawPeriText += "\r\n";
+ rawPeriText += '\r\n';
} else {
periFinished = true;
}
@@ -333,15 +342,15 @@
} else {
postRange.moveEnd( 'character', -1 );
if ( postRange.text === postText ) {
- rawPostText += "\r\n";
+ 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;
+ caretPos = rawPreText.replace( /\r\n/g, '\n' ).length;
+ endPos = caretPos + rawPeriText.replace( /\r\n/g, '\n' ).length;
} else if ( e.selectionStart || e.selectionStart === 0 ) {
// Firefox support
caretPos = e.selectionStart;
@@ -405,20 +414,22 @@
return Math.floor( e.scrollWidth / ( $.client.profile().platform === 'linux' ? 7 : 8 ) );
}
function getCaretScrollPosition( e ) {
- var i, j;
// FIXME: This functions sucks and is off by a few lines most
// of the time. It should be replaced by something decent.
- var text = e.value.replace( /\r/g, '' );
- var caret = $( e ).textSelection( 'getCaretPosition' );
- var lineLength = getLineLength( e );
- var row = 0;
- var charInLine = 0;
- var lastSpaceInLine = 0;
+ 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" ) {
+ } else if ( text.charAt( i ) === '\n' ) {
lastSpaceInLine = 0;
charInLine = 0;
row++;
@@ -431,11 +442,11 @@
}
}
}
- var nextSpace = 0;
+ nextSpace = 0;
for ( j = caret; j < caret + lineLength; j++ ) {
if (
text.charAt( j ) === ' ' ||
- text.charAt( j ) === "\n" ||
+ text.charAt( j ) === '\n' ||
caret === text.length
) {
nextSpace = j;
@@ -542,16 +553,16 @@
break;
}
- var context = $(this).data( 'wikiEditor-context' );
- var hasIframe = typeof context !== 'undefined' && context && typeof context.$iframe !== 'undefined';
+ context = $(this).data( 'wikiEditor-context' );
+ hasIframe = context !== undefined && context && context.$iframe !== undefined;
// IE selection restore voodoo
- var needSave = false;
+ needSave = false;
if ( hasIframe && context.savedSelection !== null ) {
context.fn.restoreSelection();
needSave = true;
}
- var retval = ( hasIframe ? context.fn : fn )[command].call( this, options );
+ retval = ( hasIframe ? context.fn : fn )[command].call( this, options );
if ( hasIframe && needSave ) {
context.fn.saveSelection();
}
diff --git a/resources/mediawiki.action/images/green-checkmark.png b/resources/mediawiki.action/images/green-checkmark.png
new file mode 100644
index 00000000..8ec604ea
--- /dev/null
+++ b/resources/mediawiki.action/images/green-checkmark.png
Binary files differ
diff --git a/resources/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css b/resources/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css
new file mode 100644
index 00000000..1af4a7a0
--- /dev/null
+++ b/resources/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/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js b/resources/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js
new file mode 100644
index 00000000..7ae51aba
--- /dev/null
+++ b/resources/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/mediawiki.action/mediawiki.action.edit.editWarning.js b/resources/mediawiki.action/mediawiki.action.edit.editWarning.js
new file mode 100644
index 00000000..89bb64df
--- /dev/null
+++ b/resources/mediawiki.action/mediawiki.action.edit.editWarning.js
@@ -0,0 +1,56 @@
+/*
+ * Javascript for module editWarning
+ */
+( function ( mw, $ ) {
+ $( function () {
+ // 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, #wpSummary' ).each( function () {
+ $( this ).data( 'origtext', $( this ).val() );
+ });
+ var savedWindowOnBeforeUnload;
+ $( 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' ).val() ||
+ $( '#wpSummary' ).data( 'origtext' ) !== $( '#wpSummary' ).val()
+ ) {
+ // 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/mediawiki.action/mediawiki.action.edit.js b/resources/mediawiki.action/mediawiki.action.edit.js
index 1c51c974..ba711aae 100644
--- a/resources/mediawiki.action/mediawiki.action.edit.js
+++ b/resources/mediawiki.action/mediawiki.action.edit.js
@@ -1,17 +1,20 @@
+/**
+ * Interface for the classic edit toolbar.
+ *
+ * @class mw.toolbar
+ * @singleton
+ */
( function ( mw, $ ) {
- var isReady, toolbar, currentFocused, queue, $toolbar, slice;
-
- isReady = false;
- queue = [];
- $toolbar = false;
- slice = Array.prototype.slice;
+ var toolbar, isReady, $toolbar, queue, slice, $currentFocused;
/**
- * Internal helper that does the actual insertion
- * of the button into the toolbar.
- * See mw.toolbar.addButton for parameter documentation.
+ * Internal helper that does the actual insertion of the button into the toolbar.
+ *
+ * See #addButton for parameter documentation.
+ *
+ * @private
*/
- function insertButton( b /* imageFile */, speedTip, tagOpen, tagClose, sampleText, imageId, selectText ) {
+ function insertButton( b, speedTip, tagOpen, tagClose, sampleText, imageId ) {
// Backwards compatibility
if ( typeof b !== 'object' ) {
b = {
@@ -20,11 +23,10 @@
tagOpen: tagOpen,
tagClose: tagClose,
sampleText: sampleText,
- imageId: imageId,
- selectText: selectText
+ imageId: imageId
};
}
- var $image = $( '<img>', {
+ var $image = $( '<img>' ).attr( {
width : 23,
height: 22,
src : b.imageFile,
@@ -33,30 +35,43 @@
id : b.imageId || undefined,
'class': 'mw-toolbar-editbutton'
} ).click( function () {
- toolbar.insertTags( b.tagOpen, b.tagClose, b.sampleText, b.selectText );
+ toolbar.insertTags( b.tagOpen, b.tagClose, b.sampleText );
return false;
} );
$toolbar.append( $image );
- return true;
}
+ 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.
- * @param {Object} button: Object with the following properties:
- * - imageFile
- * - speedTip
- * - tagOpen
- * - tagClose
- * - sampleText
- * - imageId
- * - selectText
- * For compatiblity, passing the above as separate arguments
+ *
+ * For compatiblity, passing the properties listed below as separate arguments
* (in the listed order) is also supported.
+ *
+ * @param {Object} button Object with the following properties:
+ * @param {string} button.imageFile
+ * @param {string} button.speedTip
+ * @param {string} button.tagOpen
+ * @param {string} button.tagClose
+ * @param {string} button.sampleText
+ * @param {string} [button.imageId]
*/
addButton: function () {
if ( isReady ) {
@@ -66,18 +81,44 @@
queue.push( slice.call( arguments ) );
}
},
+ /**
+ * 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 textarea,
- * use sampleText instead of selection if there is none.
+ * 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, selectText ) {
- if ( currentFocused && currentFocused.length ) {
- currentFocused.textSelection(
+ insertTags: function ( tagOpen, tagClose, sampleText ) {
+ if ( $currentFocused && $currentFocused.length ) {
+ $currentFocused.textSelection(
'encapsulateSelection', {
- 'pre': tagOpen,
- 'peri': sampleText,
- 'post': tagClose
+ pre: tagOpen,
+ peri: sampleText,
+ post: tagClose
}
);
}
@@ -95,64 +136,58 @@
// Explose API publicly
mw.toolbar = toolbar;
- $( document ).ready( function () {
- var buttons, i, b, $iframe;
+ $( function () {
+ var i, b, $iframe, editBox, scrollTop, $editForm;
// currentFocus is used to determine where to insert tags
- currentFocused = $( '#wpTextbox1' );
+ $currentFocused = $( '#wpTextbox1' );
// Populate the selector cache for $toolbar
$toolbar = $( '#toolbar' );
- // Legacy: Merge buttons from mwCustomEditButtons
- buttons = [].concat( queue, window.mwCustomEditButtons );
- // Clear queue
- queue.length = 0;
- for ( i = 0; i < buttons.length; i++ ) {
- b = buttons[i];
+ 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 legacy mwCustomEditButtons
+ // 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 toolbar.buttons queue.
+ // instead of to the queue.
// It is important that this is after the one and only loop through
- // the the toolbar.buttons queue
+ // 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
- */
- ( function scrollEditBox() {
- var editBox, scrollTop, $editForm;
-
- 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;
- });
+ // 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;
+ });
+ }
- $( 'textarea, input:text' ).focus( function () {
- currentFocused = $(this);
- });
+ // Apply to dynamically created textboxes as well as normal ones
+ $( document ).on( 'focus', 'textarea, input:text', function () {
+ $currentFocused = $( this );
+ } );
- // HACK: make currentFocused work with the usability iframe
+ // HACK: make $currentFocused work with the usability iframe
// With proper focus detection support (HTML 5!) this'll be much cleaner
// TODO: Get rid of this WikiEditor code from MediaWiki core!
$iframe = $( '.wikiEditor-ui-text iframe' );
@@ -161,7 +196,7 @@
// for IE
.add( $iframe.get( 0 ).contentWindow.document.body )
.focus( function () {
- currentFocused = $iframe;
+ $currentFocused = $iframe;
} );
}
});
diff --git a/resources/mediawiki.action/mediawiki.action.edit.preview.js b/resources/mediawiki.action/mediawiki.action.edit.preview.js
index cddf6ccf..c5cd61ef 100644
--- a/resources/mediawiki.action/mediawiki.action.edit.preview.js
+++ b/resources/mediawiki.action/mediawiki.action.edit.preview.js
@@ -7,14 +7,16 @@
* @param {jQuery.Event} e
*/
function doLivePreview( e ) {
- var $wikiPreview, copySelectors, removeSelectors, $copyElements, $spinner,
+ 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)
@@ -34,16 +36,13 @@
'#p-lang',
// Editing-related
'.templatesUsed',
+ '.limitreport',
'.mw-summary-preview'
];
$copyElements = $( copySelectors.join( ',' ) );
// Not shown during normal preview, to be removed if present
- removeSelectors = [
- '.mw-newarticletext'
- ];
-
- $( removeSelectors.join( ',' ) ).remove();
+ $( '.mw-newarticletext' ).remove();
$spinner = $.createSpinner( {
size: 'large',
@@ -51,36 +50,29 @@
});
$wikiPreview.before( $spinner );
$spinner.css( {
- position: 'absolute',
marginTop: $spinner.height()
} );
- // Make sure preview area is at least as tall as 2x the height of the spinner.
- // 1x because if its smaller, it will spin behind the edit toolbar.
- // (this happens on the first preview when editPreview is still empty)
- // 2x because the spinner has 1x margin top breathing room.
- $wikiPreview.css( 'minHeight', $spinner.height() * 2 );
// 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' );
+ $copyElements.animate( { opacity: 0.4 }, 'fast' );
$previewDataHolder = $( '<div>' );
- targetUrl = $( '#editform' ).attr( 'action' );
+ targetUrl = $editform.attr( 'action' );
// Gather all the data from the form
- postData = $( '#editform' ).formToArray();
+ 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
- // Though that requires figuring out how to conver that raw data into proper HTML.
+ // 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;
+
// 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++ ) {
@@ -92,43 +84,52 @@
.attr( 'class', $from.attr( 'class' ) );
}
+ // Deprecated: Use mw.hook instead
+ $( mw ).trigger( 'LivePreviewDone', [copySelectors] );
+
+ mw.hook( 'wikipage.content' ).fire( $wikiPreview );
+
$spinner.remove();
$copyElements.animate( {
opacity: 1
}, 'fast' );
-
- $( mw ).trigger( 'LivePreviewDone', [copySelectors] );
} );
}
- $( document ).ready( function () {
+ $( 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 reponse.
+ // 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>' ).prop( 'id', 'p-lang' )
+ $( '<div>' ).attr( 'id', 'p-lang' )
);
}
if ( !$( '.mw-summary-preview' ).length ) {
$( '.editCheckboxes' ).before(
- $( '<div>' ).prop( 'className', 'mw-summary-preview' )
+ $( '<div>' ).addClass( 'mw-summary-preview' )
);
}
if ( !document.getElementById( 'wikiDiff' ) && document.getElementById( 'wikiPreview' ) ) {
$( '#wikiPreview' ).after(
- $( '<div>' ).prop( 'id', 'wikiDiff')
+ $( '<div>' ).attr( 'id', 'wikiDiff' )
);
}
- // Make sure diff styles are loaded
- mw.loader.load( 'mediawiki.action.history.diff' );
-
+ // 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 );
} );
diff --git a/resources/mediawiki.action/mediawiki.action.edit.styles.css b/resources/mediawiki.action/mediawiki.action.edit.styles.css
new file mode 100644
index 00000000..4a2bab3d
--- /dev/null
+++ b/resources/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/mediawiki.action/mediawiki.action.history.diff.css b/resources/mediawiki.action/mediawiki.action.history.diff.css
index 10473be7..31ca1078 100644
--- a/resources/mediawiki.action/mediawiki.action.history.diff.css
+++ b/resources/mediawiki.action/mediawiki.action.history.diff.css
@@ -1,8 +1,36 @@
/*
** Diff rendering
*/
-table.diff, td.diff-otitle, td.diff-ntitle {
+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,
@@ -10,14 +38,14 @@ td.diff-ntitle {
text-align: center;
}
-td.diff-marker {
- text-align: right;
+td.diff-lineno {
font-weight: bold;
- font-size: 1.25em;
}
-td.diff-lineno {
+td.diff-marker {
+ text-align: right;
font-weight: bold;
+ font-size: 1.25em;
}
td.diff-addedline,
@@ -27,10 +55,6 @@ td.diff-context {
vertical-align: top;
white-space: -moz-pre-wrap;
white-space: pre-wrap;
-}
-
-td.diff-addedline,
-td.diff-deletedline {
border-style: solid;
border-width: 1px 1px 1px 4px;
border-radius: 0.33em;
@@ -45,12 +69,9 @@ td.diff-deletedline {
}
td.diff-context {
- background: #f3f3f3;
- color: #333333;
- border-style: solid;
- border-width: 1px 1px 1px 4px;
+ background: #f9f9f9;
border-color: #e6e6e6;
- border-radius: 0.33em;
+ color: #333333;
}
.diffchange {
@@ -58,15 +79,6 @@ td.diff-context {
text-decoration: none;
}
-table.diff {
- border: none;
- width: 98%;
- border-spacing: 4px;
-
- /* Ensure that colums are of equal width */
- table-layout: fixed;
-}
-
td.diff-addedline .diffchange,
td.diff-deletedline .diffchange {
border-radius: 0.33em;
@@ -80,25 +92,3 @@ td.diff-addedline .diffchange {
td.diff-deletedline .diffchange {
background: #feeec8;
}
-
-table.diff td {
- padding: 0.33em 0.66em;
-}
-
-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;
-
- /* As fallback (FF<3.5, Opera <10.5), scrollbars will be added for very wide cells
- instead of text overflowing or widening
- */
- overflow: auto;
-}
diff --git a/resources/mediawiki.action/mediawiki.action.history.js b/resources/mediawiki.action/mediawiki.action.history.js
index 55f799e5..04f045a5 100644
--- a/resources/mediawiki.action/mediawiki.action.history.js
+++ b/resources/mediawiki.action/mediawiki.action.history.js
@@ -1,7 +1,7 @@
/**
* JavaScript for History action
*/
-jQuery( document ).ready( function ( $ ) {
+jQuery( function ( $ ) {
var $historyCompareForm = $( '#mw-history-compare' ),
$historySubmitter,
$lis = $( '#pagehistory > li' );
@@ -119,7 +119,7 @@ jQuery( document ).ready( function ( $ ) {
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/mediawiki.action/mediawiki.action.view.dblClickEdit.js b/resources/mediawiki.action/mediawiki.action.view.dblClickEdit.js
index 7a9ceee5..727a5251 100644
--- a/resources/mediawiki.action/mediawiki.action.view.dblClickEdit.js
+++ b/resources/mediawiki.action/mediawiki.action.view.dblClickEdit.js
@@ -1,14 +1,12 @@
/**
- * This module enables double-click-to-edit functionality
+ * This module enables double-click-to-edit functionality.
*/
( function ( mw, $ ) {
$( function () {
- var url = $( '#ca-edit a' ).attr( 'href' );
- if ( url ) {
- mw.util.$content.dblclick( function ( e ) {
- e.preventDefault();
- window.location = url;
- } );
- }
+ 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/mediawiki.action/mediawiki.action.view.postEdit.css b/resources/mediawiki.action/mediawiki.action.view.postEdit.css
new file mode 100644
index 00000000..be88337e
--- /dev/null
+++ b/resources/mediawiki.action/mediawiki.action.view.postEdit.css
@@ -0,0 +1,77 @@
+.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;
+ -webkit-box-shadow: 0 2px 5px 0 #ccc;
+ 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/mediawiki.action/mediawiki.action.view.postEdit.js b/resources/mediawiki.action/mediawiki.action.view.postEdit.js
new file mode 100644
index 00000000..6e4df9f0
--- /dev/null
+++ b/resources/mediawiki.action/mediawiki.action.view.postEdit.js
@@ -0,0 +1,76 @@
+( 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', 'wgCookiePrefix', 'wgCurRevisionId' ] ),
+ // This should match EditPage::POST_EDIT_COOKIE_KEY_PREFIX:
+ cookieKey = config.wgCookiePrefix + 'PostEditRevision' + config.wgCurRevisionId,
+ $div, id;
+
+ function showConfirmation( data ) {
+ data = data || {};
+ if ( data.message === undefined ) {
+ data.message = $.parseHTML( mw.message( 'postedit-confirmation', 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' && $.cookie( cookieKey ) === '1' ) {
+ $.cookie( cookieKey, null, { path: '/' } );
+ mw.config.set( 'wgPostEdit', true );
+
+ mw.hook( 'postEdit' ).fire();
+ }
+
+} ( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.action/mediawiki.action.view.rightClickEdit.js b/resources/mediawiki.action/mediawiki.action.view.rightClickEdit.js
index d02d4327..93befe3a 100644
--- a/resources/mediawiki.action/mediawiki.action.view.rightClickEdit.js
+++ b/resources/mediawiki.action/mediawiki.action.view.rightClickEdit.js
@@ -1,30 +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:(.editsection a)" selector because it performs very bad.
+ // 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, href;
-
- $edit = $( this ).find( '.editsection a' );
+ var $edit = $( this ).find( '.mw-editsection a' );
if ( !$edit.length ) {
return;
}
- // Get href of the editsection link
- href = $edit.prop( 'href' );
-
// 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 ( href && e.target.nodeName.toLowerCase() !== 'a' ) {
- window.location = href;
+ 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/mediawiki.api/mediawiki.api.category.js b/resources/mediawiki.api/mediawiki.api.category.js
index cc6f704f..98a9c54b 100644
--- a/resources/mediawiki.api/mediawiki.api.category.js
+++ b/resources/mediawiki.api/mediawiki.api.category.js
@@ -1,104 +1,133 @@
/**
- * Additional mw.Api methods to assist with API calls related to categories.
+ * @class mw.Api.plugin.category
*/
( function ( mw, $ ) {
$.extend( mw.Api.prototype, {
/**
* Determine if a category exists.
- * @param title {mw.Title}
- * @param success {Function} callback to pass boolean of category's existence
- * @param err {Function} optional callback to run if api error
- * @return ajax call object
+ * @param {mw.Title} 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, success, err ) {
- var params, ok;
- params = {
- prop: 'categoryinfo',
- titles: title.toString()
- };
- ok = function ( data ) {
- var exists = false;
- if ( data.query && data.query.pages ) {
- $.each( data.query.pages, function ( id, page ) {
- if ( page.categoryinfo ) {
- exists = true;
- }
- } );
- }
- success( exists );
- };
-
- return this.get( params, { ok: ok, err: err } );
+ isCategory: function ( title, ok, err ) {
+ var d = $.Deferred(),
+ apiPromise;
+
+ // Backwards compatibility (< MW 1.20)
+ d.done( ok ).fail( err );
+
+ apiPromise = this.get( {
+ prop: 'categoryinfo',
+ titles: title.toString()
+ } )
+ .done( function ( data ) {
+ var exists = false;
+ if ( data.query && data.query.pages ) {
+ $.each( data.query.pages, function ( id, page ) {
+ if ( page.categoryinfo ) {
+ exists = true;
+ }
+ } );
+ }
+ d.resolve( exists );
+ })
+ .fail( d.reject );
+
+ return d.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 prefix {String} prefix to match
- * @param success {Function} callback to pass matched categories to
- * @param err {Function} optional callback to run if api error
- * @return {jqXHR}
+ * @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, success, err ) {
+ getCategoriesByPrefix: function ( prefix, ok, err ) {
+ var d = $.Deferred(),
+ apiPromise;
+
+ // Backwards compatibility (< MW 1.20)
+ d.done( ok ).fail( err );
+
// Fetch with allpages to only get categories that have a corresponding description page.
- var params, ok;
- params = {
- 'list': 'allpages',
- 'apprefix': prefix,
- 'apnamespace': mw.config.get('wgNamespaceIds').category
- };
- ok = function ( data ) {
- var texts = [];
- if ( data.query && data.query.allpages ) {
- $.each( data.query.allpages, function ( i, category ) {
- texts.push( new mw.Title( category.title ).getNameText() );
- } );
- }
- success( texts );
- };
-
- return this.get( params, { ok: ok, err: err } );
+ apiPromise = this.get( {
+ list: 'allpages',
+ apprefix: prefix,
+ apnamespace: mw.config.get('wgNamespaceIds').category
+ } )
+ .done( function ( data ) {
+ var texts = [];
+ if ( data.query && data.query.allpages ) {
+ $.each( data.query.allpages, function ( i, category ) {
+ texts.push( new mw.Title( category.title ).getNameText() );
+ } );
+ }
+ d.resolve( texts );
+ })
+ .fail( d.reject );
+
+ return d.promise( { abort: apiPromise.abort } );
},
/**
* Get the categories that a particular page on the wiki belongs to
- * @param title {mw.Title}
- * @param success {Function} callback to pass categories to (or false, if title not found)
- * @param err {Function} optional callback to run if api error
- * @param async {Boolean} optional asynchronousness (default = true = async)
- * @return {jqXHR}
+ * @param {mw.Title} title
+ * @param {Function} [ok] Success callback (deprecated)
+ * @param {Function} [err] Error callback (deprecated)
+ * @param {boolean} [async=true] Asynchronousness
+ * @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, success, err, async ) {
- var params, ok;
- params = {
- prop: 'categories',
- titles: title.toString()
- };
- if ( async === undefined ) {
- async = true;
- }
- ok = function ( data ) {
- var ret = false;
- if ( data.query && data.query.pages ) {
- $.each( data.query.pages, function ( id, page ) {
- if ( page.categories ) {
- if ( typeof ret !== 'object' ) {
- ret = [];
+ getCategories: function ( title, ok, err, async ) {
+ var d = $.Deferred(),
+ apiPromise;
+
+ // Backwards compatibility (< MW 1.20)
+ d.done( ok ).fail( err );
+
+ apiPromise = this.get( {
+ prop: 'categories',
+ titles: title.toString()
+ }, {
+ async: async === undefined ? true : async
+ } )
+ .done( function ( data ) {
+ var ret = false;
+ if ( data.query && data.query.pages ) {
+ $.each( data.query.pages, function ( id, page ) {
+ if ( page.categories ) {
+ if ( typeof ret !== 'object' ) {
+ ret = [];
+ }
+ $.each( page.categories, function ( i, cat ) {
+ ret.push( new mw.Title( cat.title ) );
+ } );
}
- $.each( page.categories, function ( i, cat ) {
- ret.push( new mw.Title( cat.title ) );
- } );
- }
- } );
- }
- success( ret );
- };
-
- return this.get( params, { ok: ok, err: err, async: async } );
+ } );
+ }
+ d.resolve( ret );
+ } )
+ .fail( d.reject );
+
+ return d.promise( { abort: apiPromise.abort } );
}
} );
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.category
+ */
+
}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.api/mediawiki.api.edit.js b/resources/mediawiki.api/mediawiki.api.edit.js
index 49af9375..cc83a4b8 100644
--- a/resources/mediawiki.api/mediawiki.api.edit.js
+++ b/resources/mediawiki.api/mediawiki.api.edit.js
@@ -1,11 +1,8 @@
/**
- * Additional mw.Api methods to assist with API calls related to editing wiki pages.
+ * @class mw.Api.plugin.edit
*/
( function ( mw, $ ) {
- // Cache token so we don't have to keep fetching new ones for every single request.
- var cachedToken = null;
-
$.extend( mw.Api.prototype, {
/**
@@ -13,102 +10,53 @@
* If we have a cached token try using that, and if it fails, blank out the
* cached token and start over.
*
- * @param params {Object} API parameters
- * @param ok {Function} callback for success
- * @param err {Function} [optional] error callback
- * @return {jqXHR}
+ * @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 ) {
- var useTokenToPost, getTokenIfBad,
- api = this;
- if ( cachedToken === null ) {
- // We don't have a valid cached token, so get a fresh one and try posting.
- // We do not trap any 'badtoken' or 'notoken' errors, because we don't want
- // an infinite loop. If this fresh token is bad, something else is very wrong.
- useTokenToPost = function ( token ) {
- params.token = token;
- api.post( params, ok, err );
- };
- return api.getEditToken( useTokenToPost, err );
- } else {
- // We do have a token, but it might be expired. So if it is 'bad' then
- // start over with a new token.
- params.token = cachedToken;
- getTokenIfBad = function ( code, result ) {
- if ( code === 'badtoken' ) {
- // force a new token, clear any old one
- cachedToken = null;
- api.postWithEditToken( params, ok, err );
- } else {
- err( code, result );
- }
- };
- return api.post( params, { ok : ok, err : getTokenIfBad });
- }
+ return this.postWithToken( 'edit', params ).done( ok ).fail( err );
},
/**
- * Api helper to grab an edit token
- *
- * token callback has signature ( String token )
- * error callback has signature ( String code, Object results, XmlHttpRequest xhr, Exception exception )
- * Note that xhr and exception are only available for 'http_*' errors
- * code may be any http_* error code (see mw.Api), or 'token_missing'
+ * Api helper to grab an edit token.
*
- * @param tokenCallback {Function} received token callback
- * @param err {Function} error callback
- * @return {jqXHR}
+ * @param {Function} [ok] Success callback
+ * @param {Function} [err] Error callback
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {string} return.done.token Received token.
*/
- getEditToken: function ( tokenCallback, err ) {
- var parameters = {
- action: 'tokens',
- type: 'edit'
- },
- ok = function ( data ) {
- var token;
- // If token type is not available for this user,
- // key 'edittoken' is missing or can contain Boolean false
- if ( data.tokens && data.tokens.edittoken ) {
- token = data.tokens.edittoken;
- cachedToken = token;
- tokenCallback( token );
- } else {
- err( 'token-missing', data );
- }
- },
- ajaxOptions = {
- ok: ok,
- err: err,
- // Due to the API assuming we're logged out if we pass the callback-parameter,
- // we have to disable jQuery's callback system, and instead parse JSON string,
- // by setting 'jsonp' to false.
- jsonp: false
- };
-
- return this.get( parameters, ajaxOptions );
+ getEditToken: function ( ok, err ) {
+ return this.getToken( 'edit' ).done( ok ).fail( err );
},
/**
* Create a new section of the page.
- * @param title {mw.Title|String} target page
- * @param header {String}
- * @param message {String} wikitext message
- * @param ok {Function} success handler
- * @param err {Function} error handler
- * @return {jqXHR}
+ * @see #postWithEditToken
+ * @param {mw.Title|String} title Target page
+ * @param {string} header
+ * @param {string} message wikitext message
+ * @param {Function} [ok] Success handler
+ * @param {Function} [err] Error handler
+ * @return {jQuery.Promise}
*/
newSection: function ( title, header, message, ok, err ) {
- var params = {
+ return this.postWithEditToken( {
action: 'edit',
section: 'new',
format: 'json',
title: title.toString(),
summary: header,
text: message
- };
- return this.postWithEditToken( params, ok, err );
+ }, ok, err );
}
+ } );
- } );
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.edit
+ */
}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.api/mediawiki.api.js b/resources/mediawiki.api/mediawiki.api.js
index a184e3ca..cdc67679 100644
--- a/resources/mediawiki.api/mediawiki.api.js
+++ b/resources/mediawiki.api/mediawiki.api.js
@@ -1,15 +1,9 @@
-/**
- * mw.Api objects represent the API of a particular MediaWiki server.
- */
( function ( mw, $ ) {
- /**
- * @var defaultOptions {Object}
- * 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?
- */
+ // 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
@@ -26,26 +20,28 @@
dataType: 'json'
}
- };
+ },
+ tokenCache = {};
/**
* 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.
*
- * @todo Share API objects with exact same config.
- * @example
- * <code>
- * var api = new mw.Api();
- * api.get( {
- * action: 'query',
- * meta: 'userinfo'
- * }, {
- * ok: function () { console.log( arguments ); }
- * } );
- * </code>
+ * var api = new mw.Api();
+ * api.get( {
+ * action: 'query',
+ * meta: 'userinfo'
+ * } ).done ( function ( data ) {
+ * console.log( data );
+ * } );
+ *
+ * @class
*
* @constructor
- * @param options {Object} See defaultOptions documentation above. Ajax options can also be
- * overridden for each individual request to jQuery.ajax() later on.
+ * @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 ) {
@@ -69,13 +65,12 @@
/**
* Normalize the ajax options for compatibility and/or convenience methods.
*
- * @param {undefined|Object|Function} An object contaning one or more of options.ajax,
- * or just a success function (options.ajax.ok).
+ * @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 often used to pass ok/err callbacks)
+ // (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' ) {
@@ -87,8 +82,8 @@
/**
* Perform API get request
*
- * @param {Object} request parameters
- * @param {Object|Function} [optional] ajax options
+ * @param {Object} parameters
+ * @param {Object|Function} [ajaxOptions]
* @return {jQuery.Promise}
*/
get: function ( parameters, ajaxOptions ) {
@@ -99,10 +94,11 @@
/**
* Perform API post request
- * @todo Post actions for nonlocal will need proxy
*
- * @param {Object} request parameters
- * @param {Object|Function} [optional] ajax options
+ * TODO: Post actions for non-local hostnames will need proxy.
+ *
+ * @param {Object} parameters
+ * @param {Object|Function} [ajaxOptions]
* @return {jQuery.Promise}
*/
post: function ( parameters, ajaxOptions ) {
@@ -114,15 +110,14 @@
/**
* Perform the API call.
*
- * @param {Object} request parameters
- * @param {Object} ajax options
- * @return {jQuery.Promise}
- * - done: API response data as first argument
- * - fail: errorcode as first arg, details (string or object) as second arg.
+ * @param {Object} parameters
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise} Done: API response data. Fail: Error code
*/
ajax: function ( parameters, ajaxOptions ) {
var token,
- apiDeferred = $.Deferred();
+ apiDeferred = $.Deferred(),
+ xhr;
parameters = $.extend( {}, this.defaults.parameters, parameters );
ajaxOptions = $.extend( {}, this.defaults.ajax, ajaxOptions );
@@ -154,7 +149,7 @@
}
// Make the AJAX request
- $.ajax( ajaxOptions )
+ xhr = $.ajax( ajaxOptions )
// If AJAX fails, reject API call with error code 'http'
// and details in second argument.
.fail( function ( xhr, textStatus, exception ) {
@@ -179,15 +174,91 @@
} );
// Return the Promise
- return apiDeferred.promise().fail( function ( code, details ) {
+ return apiDeferred.promise( { abort: xhr.abort } ).fail( function ( code, details ) {
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
+ * @return {jQuery.Promise} See #post
+ */
+ postWithToken: function ( tokenType, params ) {
+ var api = this, hasOwn = tokenCache.hasOwnProperty;
+ if ( hasOwn.call( tokenCache, tokenType ) && tokenCache[tokenType] !== undefined ) {
+ params.token = tokenCache[tokenType];
+ return api.post( params ).then(
+ null,
+ function ( code ) {
+ if ( code === 'badtoken' ) {
+ // force a new token, clear any old one
+ tokenCache[tokenType] = params.token = undefined;
+ return api.post( params );
+ }
+ // Pass the promise forward, so the caller gets error codes
+ return this;
+ }
+ );
+ } else {
+ return api.getToken( tokenType ).then( function ( token ) {
+ tokenCache[tokenType] = params.token = token;
+ return api.post( params );
+ } );
+ }
+ },
+
+ /**
+ * Api helper to grab any token.
+ *
+ * @param {string} type Token type.
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {string} return.done.token Received token.
+ */
+ getToken: function ( type ) {
+ var apiPromise,
+ d = $.Deferred();
+
+ apiPromise = this.get( {
+ action: 'tokens',
+ type: type
+ }, {
+ // Due to the API assuming we're logged out if we pass the callback-parameter,
+ // we have to disable jQuery's callback system, and instead parse JSON string,
+ // by setting 'jsonp' to false.
+ // TODO: This concern seems genuine but no other module has it. Is it still
+ // needed and/or should we pass this by default?
+ } )
+ .done( function ( data ) {
+ // If token type is not available for this user,
+ // key '...token' is missing or can contain Boolean false
+ if ( data.tokens && data.tokens[type + 'token'] ) {
+ d.resolve( data.tokens[type + 'token'] );
+ } else {
+ d.reject( 'token-missing', data );
+ }
+ } )
+ .fail( d.reject );
+
+ return d.promise( { abort: apiPromise.abort } );
+ }
};
/**
- * @var {Array} List of errors we might receive from the API.
+ * @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.
*/
@@ -237,7 +308,9 @@
];
/**
- * @var {Array} List of warnings we might receive from the API.
+ * @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.
*/
diff --git a/resources/mediawiki.api/mediawiki.api.login.js b/resources/mediawiki.api/mediawiki.api.login.js
new file mode 100644
index 00000000..ccbae06c
--- /dev/null
+++ b/resources/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/mediawiki.api/mediawiki.api.parse.js b/resources/mediawiki.api/mediawiki.api.parse.js
index e8d1b3e6..c4d23b82 100644
--- a/resources/mediawiki.api/mediawiki.api.parse.js
+++ b/resources/mediawiki.api/mediawiki.api.parse.js
@@ -1,42 +1,45 @@
/**
- * mw.Api methods for parsing wikitext.
+ * @class mw.Api.plugin.parse
*/
( function ( mw, $ ) {
$.extend( mw.Api.prototype, {
/**
- * Convinience method for 'action=parse'. Parses wikitext into HTML.
+ * Convinience method for 'action=parse'.
*
- * @param wikiText {String}
- * @param ok {Function} [optional] deprecated (success callback)
- * @param err {Function} [optional] deprecated (error callback)
+ * @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 apiDeferred = $.Deferred();
+ parse: function ( wikitext, ok, err ) {
+ var d = $.Deferred(),
+ apiPromise;
// Backwards compatibility (< MW 1.20)
- if ( ok ) {
- apiDeferred.done( ok );
- }
- if ( err ) {
- apiDeferred.fail( err );
- }
+ d.done( ok ).fail( err );
- this.get( {
+ apiPromise = this.get( {
action: 'parse',
- text: wikiText
+ contentmodel: 'wikitext',
+ text: wikitext
} )
.done( function ( data ) {
if ( data.parse && data.parse.text && data.parse.text['*'] ) {
- apiDeferred.resolve( data.parse.text['*'] );
+ d.resolve( data.parse.text['*'] );
}
} )
- .fail( apiDeferred.reject );
+ .fail( d.reject );
- // Return the promise
- return apiDeferred.promise();
+ return d.promise( { abort: apiPromise.abort } );
}
} );
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.parse
+ */
+
}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.api/mediawiki.api.titleblacklist.js b/resources/mediawiki.api/mediawiki.api.titleblacklist.js
deleted file mode 100644
index 1f7e275a..00000000
--- a/resources/mediawiki.api/mediawiki.api.titleblacklist.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Additional mw.Api methods to assist with API calls to the API module of the TitleBlacklist extension.
- */
-
-( function ( mw, $ ) {
-
- $.extend( mw.Api.prototype, {
- /**
- * Convinience method for 'action=titleblacklist'.
- * Note: This action is not provided by MediaWiki core, but as part of the TitleBlacklist extension.
- *
- * @param title {mw.Title}
- * @param success {Function} Called on successfull request. First argument is false if title wasn't blacklisted,
- * object with 'reason', 'line' and 'message' properties if title was blacklisted.
- * @param err {Function} optional callback to run if api error
- * @return {jqXHR}
- */
- isBlacklisted: function ( title, success, err ) {
- var params = {
- action: 'titleblacklist',
- tbaction: 'create',
- tbtitle: title.toString()
- },
- ok = function ( data ) {
- var result;
-
- // this fails open (if nothing valid is returned by the api, allows the title)
- // also fails open when the API is not present, which will be most of the time
- // as this API module is part of the TitleBlacklist extension.
- if ( data.titleblacklist && data.titleblacklist.result && data.titleblacklist.result === 'blacklisted') {
- if ( data.titleblacklist.reason ) {
- result = {
- reason: data.titleblacklist.reason,
- line: data.titleblacklist.line,
- message: data.titleblacklist.message
- };
- } else {
- mw.log('mw.Api.titleblacklist::isBlacklisted> no reason data for blacklisted title', 'debug');
- result = { reason: 'Blacklisted, but no reason supplied', line: 'Unknown', message: null };
- }
- success( result );
- } else {
- success ( false );
- }
- };
-
- return this.get( params, { ok: ok, err: err } );
- }
-
- } );
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.api/mediawiki.api.watch.js b/resources/mediawiki.api/mediawiki.api.watch.js
index d3234421..49a4c622 100644
--- a/resources/mediawiki.api/mediawiki.api.watch.js
+++ b/resources/mediawiki.api/mediawiki.api.watch.js
@@ -1,56 +1,74 @@
/**
- * Additional mw.Api methods to assist with (un)watching wiki pages.
+ * @class mw.Api.plugin.watch
* @since 1.19
*/
( function ( mw, $ ) {
/**
- * @context {mw.Api}
+ * @private
+ * @context mw.Api
+ *
+ * @param {String|mw.Title} page Full page name or instance of mw.Title
+ * @param {Function} [ok] Success callback (deprecated)
+ * @param {Function} [err] Error callback (deprecated)
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {Object} return.done.watch
+ * @return {string} return.done.watch.title Full pagename
+ * @return {boolean} return.done.watch.watched
+ * @return {string} return.done.watch.message Parsed HTML of the confirmational interface message
*/
- function doWatchInternal( page, success, err, addParams ) {
- var params = {
+ function doWatchInternal( page, ok, err, addParams ) {
+ var params,
+ d = $.Deferred(),
+ apiPromise;
+
+ // Backwards compatibility (< MW 1.20)
+ d.done( ok ).fail( err );
+
+ params = {
action: 'watch',
title: String( page ),
token: mw.user.tokens.get( 'watchToken' ),
uselang: mw.config.get( 'wgUserLanguage' )
};
- function ok( data ) {
- success( data.watch );
- }
+
if ( addParams ) {
$.extend( params, addParams );
}
- return this.post( params, { ok: ok, err: err } );
+
+ apiPromise = this.post( params )
+ .done( function ( data ) {
+ d.resolve( data.watch );
+ } )
+ .fail( d.reject );
+
+ return d.promise( { abort: apiPromise.abort } );
}
$.extend( mw.Api.prototype, {
/**
- * Convinience method for 'action=watch'.
+ * Convenience method for `action=watch`.
*
- * @param page {String|mw.Title} Full page name or instance of mw.Title
- * @param success {Function} Callback to which the watch object will be passed.
- * Watch object contains properties 'title' (full pagename), 'watched' (boolean) and
- * 'message' (parsed HTML of the 'addedwatchtext' message).
- * @param err {Function} Error callback (optional)
- * @return {jqXHR}
+ * @inheritdoc #doWatchInternal
*/
- watch: function ( page, success, err ) {
- return doWatchInternal.call( this, page, success, err );
+ watch: function ( page, ok, err ) {
+ return doWatchInternal.call( this, page, ok, err );
},
/**
- * Convinience method for 'action=watch&unwatch=1'.
+ * Convenience method for `action=watch&unwatch=1`.
*
- * @param page {String|mw.Title} Full page name or instance of mw.Title
- * @param success {Function} Callback to which the watch object will be passed.
- * Watch object contains properties 'title' (full pagename), 'watched' (boolean) and
- * 'message' (parsed HTML of the 'removedwatchtext' message).
- * @param err {Function} Error callback (optional)
- * @return {jqXHR}
+ * @inheritdoc #doWatchInternal
*/
- unwatch: function ( page, success, err ) {
- return doWatchInternal.call( this, page, success, err, { unwatch: 1 } );
+ unwatch: function ( page, ok, err ) {
+ return doWatchInternal.call( this, page, ok, err, { unwatch: 1 } );
}
} );
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.watch
+ */
+
}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.language/languages/bs.js b/resources/mediawiki.language/languages/bs.js
index c0c77aaf..65eb5a6d 100644
--- a/resources/mediawiki.language/languages/bs.js
+++ b/resources/mediawiki.language/languages/bs.js
@@ -2,10 +2,10 @@
* Bosnian (bosanski) language functions
*/
-mediaWiki.language.convertGrammar = function( word, form ) {
- var grammarForms = mw.language.getData( 'bs', 'grammarForms' );
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'bs', 'grammarForms' );
if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word] ;
+ return grammarForms[form][word];
}
switch ( form ) {
case 'instrumental': // instrumental
diff --git a/resources/mediawiki.language/languages/dsb.js b/resources/mediawiki.language/languages/dsb.js
index a42a8f7f..b2c9c081 100644
--- a/resources/mediawiki.language/languages/dsb.js
+++ b/resources/mediawiki.language/languages/dsb.js
@@ -2,10 +2,10 @@
* Lower Sorbian (Dolnoserbski) language functions
*/
-mediaWiki.language.convertGrammar = function( word, form ) {
- var grammarForms = mw.language.getData( 'dsb', 'grammarForms' );
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'dsb', 'grammarForms' );
if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word] ;
+ return grammarForms[form][word];
}
switch ( form ) {
case 'instrumental': // instrumental
diff --git a/resources/mediawiki.language/languages/fi.js b/resources/mediawiki.language/languages/fi.js
index 374698dc..61c6c104 100644
--- a/resources/mediawiki.language/languages/fi.js
+++ b/resources/mediawiki.language/languages/fi.js
@@ -3,21 +3,24 @@
* @author Santhosh Thottingal
*/
-mediaWiki.language.convertGrammar = function( word, form ) {
- var grammarForms = mw.language.getData( 'fi', 'grammarForms' );
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms, aou, origWord;
+
+ grammarForms = mediaWiki.language.getData( 'fi', 'grammarForms' );
if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word] ;
+ return grammarForms[form][word];
}
// vowel harmony flag
- var aou = word.match( /[aou][^äöy]*$/i );
- var origWord = word;
+ 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 ) )
+ if ( word.match( /[bcdfghjklmnpqrstvwxz]$/i ) ) {
word += 'i';
+ }
switch ( form ) {
case 'genitive':
diff --git a/resources/mediawiki.language/languages/ga.js b/resources/mediawiki.language/languages/ga.js
index a27b489d..c13e8321 100644
--- a/resources/mediawiki.language/languages/ga.js
+++ b/resources/mediawiki.language/languages/ga.js
@@ -2,10 +2,11 @@
* Irish (Gaeilge) language functions
*/
-mediaWiki.language.convertGrammar = function( word, form ) {
- var grammarForms = mw.language.getData( 'ga', 'grammarForms' );
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ /*jshint onecase:true */
+ var grammarForms = mediaWiki.language.getData( 'ga', 'grammarForms' );
if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word] ;
+ return grammarForms[form][word];
}
switch ( form ) {
case 'ainmlae':
diff --git a/resources/mediawiki.language/languages/he.js b/resources/mediawiki.language/languages/he.js
index d35f77ed..13d457b2 100644
--- a/resources/mediawiki.language/languages/he.js
+++ b/resources/mediawiki.language/languages/he.js
@@ -3,26 +3,26 @@
*/
mediaWiki.language.convertGrammar = function( word, form ) {
- var grammarForms = mw.language.getData( 'he', 'grammarForms' );
+ var grammarForms = mediaWiki.language.getData( 'he', 'grammarForms' );
if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word] ;
+ 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.substr( 0, 1 ) === "ו" && word.substr( 0, 2 ) !== "וו" ) {
- word = "ו" + word;
+ if ( word.substr( 0, 1 ) === 'ו' && word.substr( 0, 2 ) !== 'וו' ) {
+ word = 'ו' + word;
}
// Remove the "He" if prefixed
- if ( word.substr( 0, 1 ) === "ה" ) {
+ if ( word.substr( 0, 1 ) === 'ה' ) {
word = word.substr( 1, word.length );
}
// Add a hyphen (maqaf) before numbers and non-Hebrew letters
- if ( word.substr( 0, 1 ) < "א" || word.substr( 0, 1 ) > "ת" ) {
- word = "־" + word;
+ if ( word.substr( 0, 1 ) < 'א' || word.substr( 0, 1 ) > 'ת' ) {
+ word = '־' + word;
}
}
return word;
diff --git a/resources/mediawiki.language/languages/hsb.js b/resources/mediawiki.language/languages/hsb.js
index 211d67b5..77dca75e 100644
--- a/resources/mediawiki.language/languages/hsb.js
+++ b/resources/mediawiki.language/languages/hsb.js
@@ -2,10 +2,10 @@
* Upper Sorbian (Hornjoserbsce) language functions
*/
-mediaWiki.language.convertGrammar = function( word, form ) {
- var grammarForms =mw.language.getData( 'hsb', 'grammarForms' );
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'hsb', 'grammarForms' );
if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word] ;
+ return grammarForms[form][word];
}
switch ( form ) {
case 'instrumental': // instrumental
diff --git a/resources/mediawiki.language/languages/hu.js b/resources/mediawiki.language/languages/hu.js
index eb3f1f3a..23b0c125 100644
--- a/resources/mediawiki.language/languages/hu.js
+++ b/resources/mediawiki.language/languages/hu.js
@@ -3,10 +3,10 @@
* @author Santhosh Thottingal
*/
-mediaWiki.language.convertGrammar = function( word, form ) {
- var grammarForms = mw.language.getData( 'hu', 'grammarForms' );
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'hu', 'grammarForms' );
if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word] ;
+ return grammarForms[form][word];
}
switch ( form ) {
case 'rol':
diff --git a/resources/mediawiki.language/languages/hy.js b/resources/mediawiki.language/languages/hy.js
index 215e7504..65081bdd 100644
--- a/resources/mediawiki.language/languages/hy.js
+++ b/resources/mediawiki.language/languages/hy.js
@@ -2,10 +2,11 @@
* Armenian (Հայերեն) language functions
*/
-mediaWiki.language.convertGrammar = function( word, form ) {
- var grammarForms = mw.language.getData( 'hy', 'grammarForms' );
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ /*jshint onecase:true */
+ var grammarForms = mediaWiki.language.getData( 'hy', 'grammarForms' );
if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word] ;
+ return grammarForms[form][word];
}
// These rules are not perfect, but they are currently only used for site names so it doesn't
@@ -13,14 +14,15 @@ mediaWiki.language.convertGrammar = function( word, form ) {
switch ( form ) {
case 'genitive': // սեռական հոլով
- if ( word.substr( -1 ) === 'ա' )
+ if ( word.substr( -1 ) === 'ա' ) {
word = word.substr( 0, word.length -1 ) + 'այի';
- else if ( word.substr( -1 ) === 'ո' )
+ } else if ( word.substr( -1 ) === 'ո' ) {
word = word.substr( 0, word.length - 1 ) + 'ոյի';
- else if ( word.substr( -4 ) === 'գիրք' )
+ } else if ( word.substr( -4 ) === 'գիրք' ) {
word = word.substr( 0, word.length - 4 ) + 'գրքի';
- else
+ } else {
word = word + 'ի';
+ }
break;
}
return word;
diff --git a/resources/mediawiki.language/languages/la.js b/resources/mediawiki.language/languages/la.js
index 313bb1c2..27110241 100644
--- a/resources/mediawiki.language/languages/la.js
+++ b/resources/mediawiki.language/languages/la.js
@@ -3,10 +3,10 @@
* @author Santhosh Thottingal
*/
-mediaWiki.language.convertGrammar = function( word, form ) {
- var grammarForms = mw.language.getData( 'la', 'grammarForms' );
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'la', 'grammarForms' );
if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word] ;
+ return grammarForms[form][word];
}
switch ( form ) {
case 'genitive':
diff --git a/resources/mediawiki.language/languages/os.js b/resources/mediawiki.language/languages/os.js
index 431e38c8..682b3903 100644
--- a/resources/mediawiki.language/languages/os.js
+++ b/resources/mediawiki.language/languages/os.js
@@ -4,23 +4,24 @@
*/
-mediaWiki.language.convertGrammar = function( word, form ) {
- var grammarForms = mw.language.getData( 'os', 'grammarForms' );
+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] ;
+ return grammarForms[form][word];
}
- // Ending for allative case
- var end_allative = 'мæ';
- // Variable for 'j' beetwen vowels
- var jot = '';
- // Variable for "-" for not Ossetic words
- var hyphen = '';
- // Variable for ending
- var ending = '';
// Checking if the $word is in plural form
if ( word.match( /тæ$/i ) ) {
word = word.substring( 0, word.length - 1 );
- end_allative = 'æм';
+ endAllative = 'æм';
}
// Works if word is in singular form.
// Checking if word ends on one of the vowels: е, ё, и, о, ы, э, ю, я.
@@ -45,10 +46,10 @@ mediaWiki.language.convertGrammar = function( word, form ) {
ending = hyphen + jot + 'æн';
break;
case 'allative':
- ending = hyphen + end_allative;
+ ending = hyphen + endAllative;
break;
case 'ablative':
- if ( jot == 'й' ) {
+ if ( jot === 'й' ) {
ending = hyphen + jot + 'æ';
}
else {
diff --git a/resources/mediawiki.language/languages/ru.js b/resources/mediawiki.language/languages/ru.js
index cfdbfc3b..1bc06326 100644
--- a/resources/mediawiki.language/languages/ru.js
+++ b/resources/mediawiki.language/languages/ru.js
@@ -2,27 +2,51 @@
* Russian (Русский) language functions
*/
-mediaWiki.language.convertGrammar = function( word, form ) {
- var grammarForms = mw.language.getData( 'ru', 'grammarForms' );
+// 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] ;
+ return grammarForms[form][word];
}
switch ( form ) {
case 'genitive': // родительный падеж
- if ( ( word.substr( word.length - 4 ) == 'вики' ) || ( word.substr( word.length - 4 ) == 'Вики' ) ) {
- }
- else if ( word.substr( word.length - 1 ) == 'ь' )
+ if ( word.substr( word.length - 1 ) === 'ь' ) {
word = word.substr(0, word.length - 1 ) + 'я';
- else if ( word.substr( word.length - 2 ) == 'ия' )
+ } else if ( word.substr( word.length - 2 ) === 'ия' ) {
word = word.substr(0, word.length - 2 ) + 'ии';
- else if ( word.substr( word.length - 2 ) == 'ка' )
+ } else if ( word.substr( word.length - 2 ) === 'ка' ) {
word = word.substr(0, word.length - 2 ) + 'ки';
- else if ( word.substr( word.length - 2 ) == 'ти' )
+ } else if ( word.substr( word.length - 2 ) === 'ти' ) {
word = word.substr(0, word.length - 2 ) + 'тей';
- else if ( word.substr( word.length - 2 ) == 'ды' )
+ } else if ( word.substr( word.length - 2 ) === 'ды' ) {
word = word.substr(0, word.length - 2 ) + 'дов';
- else if ( word.substr( word.length - 3 ) == 'ник' )
+ } else if ( word.substr( word.length - 3 ) === 'ные' ) {
+ word = word.substr(0, word.length - 3 ) + 'ных';
+ } else if ( word.substr( word.length - 3 ) === 'ник' ) {
word = word.substr(0, word.length - 3 ) + 'ника';
+ }
+ break;
+ case 'prepositional': // предложный падеж
+ if ( word.substr( word.length - 1 ) === 'ь' ) {
+ word = word.substr(0, word.length - 1 ) + 'е';
+ } else if ( word.substr( word.length - 2 ) === 'ия' ) {
+ word = word.substr(0, word.length - 2 ) + 'ии';
+ } else if ( word.substr( word.length - 2 ) === 'ка' ) {
+ word = word.substr(0, word.length - 2 ) + 'ке';
+ } else if ( word.substr( word.length - 2 ) === 'ти' ) {
+ word = word.substr(0, word.length - 2 ) + 'тях';
+ } else if ( word.substr( word.length - 2 ) === 'ды' ) {
+ word = word.substr(0, word.length - 2 ) + 'дах';
+ } else if ( word.substr( word.length - 3 ) === 'ные' ) {
+ word = word.substr(0, word.length - 3 ) + 'ных';
+ } else if ( word.substr( word.length - 3 ) === 'ник' ) {
+ word = word.substr(0, word.length - 3 ) + 'нике';
+ }
break;
}
return word;
diff --git a/resources/mediawiki.language/languages/sl.js b/resources/mediawiki.language/languages/sl.js
index acd00bfd..fb335b6a 100644
--- a/resources/mediawiki.language/languages/sl.js
+++ b/resources/mediawiki.language/languages/sl.js
@@ -2,10 +2,10 @@
* Slovenian (Slovenščina) language functions
*/
-mediaWiki.language.convertGrammar = function( word, form ) {
- var grammarForms = mw.language.getData( 'sl', 'grammarForms' );
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ var grammarForms = mediaWiki.language.getData( 'sl', 'grammarForms' );
if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word] ;
+ return grammarForms[form][word];
}
switch ( form ) {
case 'mestnik': // locative
diff --git a/resources/mediawiki.language/languages/uk.js b/resources/mediawiki.language/languages/uk.js
index ee110b06..5e56b66f 100644
--- a/resources/mediawiki.language/languages/uk.js
+++ b/resources/mediawiki.language/languages/uk.js
@@ -2,33 +2,35 @@
* Ukrainian (Українська) language functions
*/
-mediaWiki.language.convertGrammar = function( word, form ) {
- var grammarForms = mw.language.getData( 'uk', 'grammarForms' );
+mediaWiki.language.convertGrammar = function ( word, form ) {
+ /*jshint noempty:false */
+ var grammarForms = mediaWiki.language.getData( 'uk', 'grammarForms' );
if ( grammarForms && grammarForms[form] ) {
- return grammarForms[form][word] ;
+ return grammarForms[form][word];
}
switch ( form ) {
case 'genitive': // родовий відмінок
- if ( ( word.substr( word.length - 4 ) == 'вікі' ) || ( word.substr( word.length - 4 ) == 'Вікі' ) ) {
- }
- else if ( word.substr( word.length - 1 ) == 'ь' )
+ if ( ( word.substr( word.length - 4 ) === 'вікі' ) || ( word.substr( word.length - 4 ) === 'Вікі' ) ) {
+ } else if ( word.substr( word.length - 1 ) === 'ь' ) {
word = word.substr(0, word.length - 1 ) + 'я';
- else if ( word.substr( word.length - 2 ) == 'ія' )
+ } else if ( word.substr( word.length - 2 ) === 'ія' ) {
word = word.substr(0, word.length - 2 ) + 'ії';
- else if ( word.substr( word.length - 2 ) == 'ка' )
+ } else if ( word.substr( word.length - 2 ) === 'ка' ) {
word = word.substr(0, word.length - 2 ) + 'ки';
- else if ( word.substr( word.length - 2 ) == 'ти' )
+ } else if ( word.substr( word.length - 2 ) === 'ти' ) {
word = word.substr(0, word.length - 2 ) + 'тей';
- else if ( word.substr( word.length - 2 ) == 'ды' )
+ } else if ( word.substr( word.length - 2 ) === 'ды' ) {
word = word.substr(0, word.length - 2 ) + 'дов';
- else if ( word.substr( word.length - 3 ) == 'ник' )
+ } else if ( word.substr( word.length - 3 ) === 'ник' ) {
word = word.substr(0, word.length - 3 ) + 'ника';
+ }
break;
case 'accusative': // знахідний відмінок
- if ( ( word.substr( word.length - 4 ) == 'вікі' ) || ( word.substr( word.length - 4 ) == 'Вікі' ) ) {
+ if ( ( word.substr( word.length - 4 ) === 'вікі' ) || ( word.substr( word.length - 4 ) === 'Вікі' ) ) {
}
- else if ( word.substr( word.length - 2 ) == 'ія' )
+ else if ( word.substr( word.length - 2 ) === 'ія' ) {
word = word.substr(0, word.length - 2 ) + 'ію';
+ }
break;
}
return word;
diff --git a/resources/mediawiki.language/mediawiki.cldr.js b/resources/mediawiki.language/mediawiki.cldr.js
index 6660eca4..c3023cd5 100644
--- a/resources/mediawiki.language/mediawiki.cldr.js
+++ b/resources/mediawiki.language/mediawiki.cldr.js
@@ -1,8 +1,8 @@
/**
- * CLDR related utility methods
+ * CLDR related utility methods.
*/
-( function( mw ) {
- "use strict";
+( function ( mw ) {
+ 'use strict';
var cldr = {
/**
@@ -10,19 +10,20 @@
* In case none of the rules passed, we return pluralRules.length
* That means it is the "other" form.
* @param number
- * @param pluralRules
- * @return plural form index
+ * @param {Array} pluralRules
+ * @return {number} plural form index
*/
- getPluralForm: function( number, pluralRules ) {
- var pluralFormIndex = 0;
- for ( pluralFormIndex = 0; pluralFormIndex < pluralRules.length; pluralFormIndex++ ) {
- if ( mw.libs.pluralRuleParser( pluralRules[pluralFormIndex], number ) ) {
+ getPluralForm: function ( number, pluralRules ) {
+ var i;
+ for ( i = 0; i < pluralRules.length; i++ ) {
+ if ( mw.libs.pluralRuleParser( pluralRules[i], number ) ) {
break;
}
}
- return pluralFormIndex;
+ return i;
}
};
mw.cldr = cldr;
-} )( mediaWiki );
+
+}( mediaWiki ) );
diff --git a/resources/mediawiki.language/mediawiki.language.init.js b/resources/mediawiki.language/mediawiki.language.init.js
index 30307a37..937b89bb 100644
--- a/resources/mediawiki.language/mediawiki.language.init.js
+++ b/resources/mediawiki.language/mediawiki.language.init.js
@@ -2,7 +2,7 @@
* Base language object with methods for storing and getting
* language data.
*/
-( function ( mw, $ ) {
+( function ( mw ) {
var language = {
/**
@@ -58,4 +58,4 @@
mw.language = language;
-}( mediaWiki, jQuery ) );
+}( mediaWiki ) );
diff --git a/resources/mediawiki.language/mediawiki.language.js b/resources/mediawiki.language/mediawiki.language.js
index 935d4ff6..631d13df 100644
--- a/resources/mediawiki.language/mediawiki.language.js
+++ b/resources/mediawiki.language/mediawiki.language.js
@@ -43,12 +43,37 @@ var language = {
* @param forms array List of plural forms
* @return string Correct form for quantifier in this language
*/
- convertPlural: function( count, forms ) {
- var pluralFormIndex = 0;
+ convertPlural: function ( count, forms ) {
+ var pluralRules,
+ formCount,
+ form,
+ index,
+ equalsPosition,
+ pluralFormIndex = 0;
+
if ( !forms || forms.length === 0 ) {
return '';
}
- var pluralRules = mw.language.getData( mw.config.get( 'wgUserLanguage' ), 'pluralRules' );
+
+ // 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.substring( 0, equalsPosition ), 10 );
+ if ( formCount === count ) {
+ return form.substr( equalsPosition + 1 );
+ }
+ forms[index] = undefined;
+ }
+ }
+
+ // Remove explicit plural forms from the forms.
+ forms = $.map( forms, function ( form ) {
+ return form;
+ } );
+
+ pluralRules = mw.language.getData( mw.config.get( 'wgUserLanguage' ), 'pluralRules' );
if ( !pluralRules ) {
// default fallback.
return ( count === 1 ) ? forms[0] : forms[1];
@@ -73,43 +98,6 @@ var language = {
},
/**
- * Converts a number using digitTransformTable.
- *
- * @param {num} number Value to be converted
- * @param {boolean} integer Convert the return value to an integer
- */
- convertNumber: function( num, integer ) {
- var i, tmp, transformTable;
-
- if ( !mw.language.digitTransformTable ) {
- return num;
- }
- // Set the target Transform table:
- transformTable = mw.language.digitTransformTable;
- // 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;
- }
- var numberString = '' + num;
- var 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;
- },
-
- /**
* Provides an alternative text depending on specified gender.
* Usage {{gender:[gender|user object]|masculine|feminine|neutral}}.
* If second or third parameter are not specified, masculine is used.
@@ -121,7 +109,7 @@ var language = {
*
* @return string
*/
- gender: function( gender, forms ) {
+ gender: function ( gender, forms ) {
if ( !forms || forms.length === 0 ) {
return '';
}
@@ -151,10 +139,8 @@ var language = {
return grammarForms[form][word] || word;
}
return word;
- },
+ }
- // Digit Transform Table, populated by language classes where applicable
- digitTransformTable: mw.language.getData( mw.config.get( 'wgUserLanguage' ), 'digitTransformTable' )
};
$.extend( mw.language, language );
diff --git a/resources/mediawiki.language/mediawiki.language.months.js b/resources/mediawiki.language/mediawiki.language.months.js
new file mode 100644
index 00000000..3d4b7ee7
--- /dev/null
+++ b/resources/mediawiki.language/mediawiki.language.months.js
@@ -0,0 +1,54 @@
+/**
+ * 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
+ */
+ 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/mediawiki.language/mediawiki.language.numbers.js b/resources/mediawiki.language/mediawiki.language.numbers.js
new file mode 100644
index 00000000..fada6ce1
--- /dev/null
+++ b/resources/mediawiki.language/mediawiki.language.numbers.js
@@ -0,0 +1,243 @@
+/*
+ * Number related utilities for mediawiki.language
+ */
+( function ( mw, $ ) {
+
+ /**
+ * 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. Yields 'blah++++++'.
+ * pad('blah', 10, '+', true);
+ *
+ * @param {string} text The string to pad
+ * @param {Number} size To provide padding
+ * @param {string} ch Character to pad, defaults to '0'
+ * @param {Boolean} end 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;
+ }
+
+ /**
+ * Efficiently replicate a string n times.
+ *
+ * @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 );
+ str += 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
+ *
+ * @param {Number} value the number to be formatted, ignores sign
+ * @param {string} pattern the number portion of a pattern (e.g. `#,##0.00`)
+ * @param {string} options.decimalThe decimal separator
+ * @param {string} options.group The group separator
+ *
+ * @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.substr( 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].substr( valueParts[0].length - padLength );
+ }
+ }
+
+ // Add group separators
+ index = patternParts[0].lastIndexOf( ',' );
+
+ if ( index !== -1 ) {
+ groupSize = patternParts[0].length - index - 1;
+ remainder = patternParts[0].substr( 0, index );
+ index = remainder.lastIndexOf( ',' );
+ if ( index !== -1 ) {
+ groupSize2 = remainder.length - index - 1;
+ }
+ }
+
+ for ( whole = valueParts[0]; whole; ) {
+ off = whole.length - groupSize;
+
+ pieces.push( ( off > 0 ) ? whole.substr( off ) : whole );
+ whole = ( off > 0 ) ? whole.slice( 0, off ) : '';
+
+ if ( groupSize2 ) {
+ groupSize = groupSize2;
+ }
+ }
+ valueParts[0] = pieces.reverse().join( options.group );
+
+ return valueParts.join( options.decimal );
+ }
+
+ $.extend( mw.language, {
+
+ /**
+ * Converts a number using digitTransformTable.
+ *
+ * @param {Number} num Value to be converted
+ * @param {boolean} integer 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;
+ },
+
+ getDigitTransformTable: function () {
+ return mw.language.getData( mw.config.get( 'wgUserLanguage' ),
+ 'digitTransformTable' ) || [];
+ },
+
+ getSeparatorTransformTable: function () {
+ return mw.language.getData( mw.config.get( 'wgUserLanguage' ),
+ 'separatorTransformTable' ) || [];
+ },
+
+ /**
+ * Apply pattern to format value as a string using as per
+ * unicode.org 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
+ * @returns {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/mediawiki.less/mediawiki.mixins.less b/resources/mediawiki.less/mediawiki.mixins.less
new file mode 100644
index 00000000..19a715b9
--- /dev/null
+++ b/resources/mediawiki.less/mediawiki.mixins.less
@@ -0,0 +1,46 @@
+/**
+ * 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) when (embeddable(@url)) {
+ background-image: embed(@url);
+ background-image: url(@url)!ie;
+}
+
+.background-image(@url) when not (embeddable(@url)) {
+ background-image: url(@url);
+}
+
+/* Note gzip compression means that it is okay to embed twice */
+.background-image-svg(@svg, @fallback) {
+ background-image: url(@fallback);
+ /* SVG support using a transparent gradient to guarantee cross-browser
+ * compatibility (browsers able to understand gradient syntax support also SVG) */
+ /* @embed */ background-image: -webkit-linear-gradient(transparent, transparent), url(@svg);
+ /* @embed */ background-image: linear-gradient(transparent, transparent), url(@svg);
+}
+
+/* Caution: Does not support localisable images */
+.list-style-image(@url) when (embeddable(@url)) {
+ list-style-image: embed(@url);
+ list-style-image: url(@url)!ie;
+}
+
+.list-style-image(@url) when not (embeddable(@url)) {
+ list-style-image: url(@url);
+}
+
+.transition(@string) {
+ -webkit-transition: @string;
+ -moz-transition: @string;
+ -o-transition: @string;
+ transition: @string;
+}
diff --git a/resources/mediawiki.libs/CLDRPluralRuleParser.js b/resources/mediawiki.libs/CLDRPluralRuleParser.js
index 91bdc07d..3def37c5 100644
--- a/resources/mediawiki.libs/CLDRPluralRuleParser.js
+++ b/resources/mediawiki.libs/CLDRPluralRuleParser.js
@@ -1,7 +1,7 @@
-/* This is cldrpluralparser 1.0, ported to MediaWiki ResourceLoader */
+/* This is CLDRPluralRuleParser v1.1, ported to MediaWiki ResourceLoader */
/**
-* cldrpluralparser.js
+* CLDRPluralRuleParser.js
* A parser engine for CLDR plural rules.
*
* Copyright 2012 GPLV3+, Santhosh Thottingal
@@ -13,59 +13,83 @@
* @author Amir Aharoni
*/
+( function ( mw ) {
/**
* Evaluates a plural rule in CLDR syntax for a number
- * @param rule
- * @param number
- * @return true|false|null
+ * @param {string} rule
+ * @param {integer} number
+ * @return {boolean} true if evaluation passed, false if evaluation failed.
*/
-( function( mw ) {
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 | 'n' <EOL>
+ relation = is_relation | in_relation | within_relation
is_relation = expr 'is' ('not')? value
- in_relation = expr ('not')? 'in' range_list
+ in_relation = expr (('not')? 'in' | '=' | '!=') range_list
within_relation = expr ('not')? 'within' range_list
- expr = 'n' ('mod' value)?
+ 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].trim();
+
+ 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;
-
- var whitespace = makeRegexParser(/^\s+/);
- var digits = makeRegexParser(/^\d+/);
-
- var _n_ = makeStringParser('n');
- var _is_ = makeStringParser('is');
- var _mod_ = makeStringParser('mod');
- var _not_ = makeStringParser('not');
- var _in_ = makeStringParser('in');
- var _within_ = makeStringParser('within');
- var _range_ = makeStringParser('..');
- var _comma_ = makeStringParser(',');
- var _or_ = makeStringParser('or');
- var _and_ = makeStringParser('and');
+ 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);*/
+ // console.log.apply(console, arguments);
}
debug('pluralRuleParser', rule, number);
// Try parsers until one works, if none work return null
+
function choice(parserSyntax) {
- return function () {
+ return function() {
for (var i = 0; i < parserSyntax.length; i++) {
var result = parserSyntax[i]();
if (result !== null) {
@@ -79,6 +103,7 @@ function pluralRuleParser(rule, number) {
// 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 = [];
@@ -95,8 +120,9 @@ function pluralRuleParser(rule, number) {
// 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 () {
+ return function() {
var originalPos = pos;
var result = [];
var parsed = p();
@@ -113,21 +139,21 @@ function pluralRuleParser(rule, number) {
}
// Helpers -- just make parserSyntax out of simpler JS builtin types
-
function makeStringParser(s) {
var len = s.length;
- return function () {
+ return function() {
var result = null;
if (rule.substr(pos, len) === s) {
result = s;
pos += len;
}
+
return result;
};
}
function makeRegexParser(regex) {
- return function () {
+ return function() {
var matches = rule.substr(pos).match(regex);
if (matches === null) {
return null;
@@ -137,62 +163,166 @@ function pluralRuleParser(rule, number) {
};
}
+ /*
+ * 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");
+ debug(' -- failed n ', number);
return result;
}
- result = parseInt(number, 10);
- debug(" -- passed n ", 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;
}
- var expression = choice([mod, n]);
+ // 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([n, whitespace, _mod_, whitespace, digits]);
+ var result = sequence([operand, whitespace, choice([_mod_, _percent_]), whitespace, value]);
if (result === null) {
- debug(" -- failed mod");
+ debug(' -- failed mod');
return null;
}
- debug(" -- passed mod");
+ 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");
+ debug(' -- failed not');
return null;
- } else {
- return result[1];
}
+
+ return result[1];
}
+ // is_relation = expr 'is' ('not')? value
function is() {
- var result = sequence([expression, whitespace, _is_, nOrMore(0, not), whitespace, digits]);
+ var result = sequence([expression, whitespace, choice([_is_]), whitespace, value]);
if (result !== null) {
- debug(" -- passed is");
- if (result[3][0] === 'not') {
- return result[0] !== parseInt(result[5], 10);
- } else {
- return result[0] === parseInt(result[5], 10);
+ 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 is");
+ debug(' -- failed not_in');
return null;
}
+ // range_list = (range | value) (',' range_list)*
function rangeList() {
- // range_list = (range | value) (',' range_list)*
- var result = sequence([choice([range, digits]), nOrMore(0, rangeTail)]);
+ var result = sequence([choice([range, value]), nOrMore(0, rangeTail)]);
var resultList = [];
if (result !== null) {
- resultList = resultList.concat(result[0], result[1][0]);
+ resultList = resultList.concat(result[0]);
+ if (result[1][0]) {
+ resultList = resultList.concat(result[1][0]);
+ }
return resultList;
}
- debug(" -- failed rangeList");
+ debug(' -- failed rangeList');
return null;
}
@@ -202,110 +332,141 @@ function pluralRuleParser(rule, number) {
if (result !== null) {
return result[1];
}
- debug(" -- failed rangeTail");
+ debug(' -- failed rangeTail');
return null;
}
+ // range = value'..'value
+
function range() {
- var result = sequence([digits, _range_, digits]);
+ var i;
+ var result = sequence([value, _range_, value]);
if (result !== null) {
- debug(" -- passed range");
+ debug(' -- passed range');
var array = [];
var left = parseInt(result[0], 10);
var right = parseInt(result[2], 10);
- for ( i = left; i <= right; i++) {
+ for (i = left; i <= right; i++) {
array.push(i);
}
return array;
}
- debug(" -- failed range");
+ debug(' -- failed range');
return null;
}
function _in() {
// in_relation = expr ('not')? 'in' range_list
- var result = sequence([expression, nOrMore(0, not), whitespace, _in_, whitespace, rangeList]);
+ var result = sequence([expression, nOrMore(0, not), whitespace, choice([_in_, _equal_]), whitespace, rangeList]);
if (result !== null) {
- debug(" -- passed _in");
+ debug(' -- passed _in:' + result);
var range_list = result[5];
for (var i = 0; i < range_list.length; i++) {
- if (parseInt(range_list[i], 10) === result[0]) {
+ if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
return (result[1][0] !== 'not');
}
}
return (result[1][0] === 'not');
}
- debug(" -- failed _in ");
+ 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() {
- var result = sequence([expression, whitespace, _within_, whitespace, rangeList]);
+ // 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[4];
- return (parseInt( range_list[0],10 )<= result[0] && result[0] <= parseInt( range_list[1], 10));
+ 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 ");
+ debug(' -- failed within ');
return null;
}
+ // relation = is_relation | in_relation | within_relation
+ relation = choice([is, not_in, isnot, _in, within]);
- var relation = choice([is, _in, within]);
-
+ // and_condition = relation ('and' relation)*
function and() {
- var result = sequence([relation, whitespace, _and_, whitespace, condition]);
+ var result = sequence([relation, nOrMore(0, andTail)]);
if (result) {
- debug(" -- passed and");
- return result[0] && result[4];
+ 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");
+ debug(' -- failed and');
return null;
}
- function or() {
- var result = sequence([relation, whitespace, _or_, whitespace, condition]);
- if (result) {
- debug(" -- passed or");
- return result[0] || result[4];
+ // ('and' relation)*
+ function andTail() {
+ var result = sequence([whitespace, _and_, whitespace, relation]);
+ if (result !== null) {
+ debug(' -- passed andTail' + result);
+ return result[3];
}
- debug(" -- failed or");
+ debug(' -- failed andTail');
return null;
- }
- var condition = choice([and, or, relation]);
+ }
+ // ('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;
- function isInt(n) {
- return parseFloat(n) % 1 === 0;
}
+ // 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];
- function start() {
- if (!isInt(number)) {
- return false;
}
- var result = condition();
- return result;
+ return false;
}
-
- var result = start();
-
+ 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 || pos !== rule.length) {
- // throw new Error("Parse error at position " + pos.toString() + " in input: " + rule + " result is " + result);
+ if (result === null) {
+ throw new Error('Parse error at position ' + pos.toString() + ' for rule: ' + rule);
}
- return result;
-}
+ if (pos !== rule.length) {
+ debug('Warning: Rule not parsed completely. Parser stopped at ' + rule.substr(0, pos) + ' for rule: ' + rule);
+ }
-/* For module loaders, e.g. NodeJS, NPM */
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = pluralRuleParser;
+ return result;
}
/* pluralRuleParser ends here */
diff --git a/resources/mediawiki.libs/mediawiki.libs.jpegmeta.js b/resources/mediawiki.libs/mediawiki.libs.jpegmeta.js
index af49889a..22429246 100644
--- a/resources/mediawiki.libs/mediawiki.libs.jpegmeta.js
+++ b/resources/mediawiki.libs/mediawiki.libs.jpegmeta.js
@@ -274,7 +274,7 @@
this.JpegMeta.JpegFile.prototype._JFIF_IDENT = "JFIF\x00";
this.JpegMeta.JpegFile.prototype._JFXX_IDENT = "JFXX\x00";
- /* EXIF idents */
+ /* Exif idents */
this.JpegMeta.JpegFile.prototype._EXIF_IDENT = "Exif\x00";
/* TIFF types */
diff --git a/resources/mediawiki.page/mediawiki.page.gallery.js b/resources/mediawiki.page/mediawiki.page.gallery.js
new file mode 100644
index 00000000..147a869d
--- /dev/null
+++ b/resources/mediawiki.page/mediawiki.page.gallery.js
@@ -0,0 +1,248 @@
+/**
+ * Show gallery captions when focused. Copied directly from jquery.mw-jump.js.
+ * Also Dynamically resize images to justify them.
+ */
+( function ( $, mw ) {
+ $( 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,
+ aspect: imgWidth / imgHeight, // XXX: can divide by 0 ever happen?
+ 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,
+ hookInfo,
+ 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 ) ) {
+ mw.log( 'Skipping item ' + j + ' due to aspect: ' + 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.
+ mw.log( 'mw.page.gallery: Cannot fit row, aspect is ' + preferredHeight/curRowHeight );
+ 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.
+ mw.log( 'mw.page.gallery: Trying to resize row ' + i + ' to ' + preferredHeight + '?!' );
+ // Skip this row.
+ continue;
+ }
+ if ( preferredHeight < 5 ) {
+ // Well something clearly went wrong...
+ mw.log( {maxWidth: maxWidth, combinedPadding: combinedPadding, combinedAspect: combinedAspect, wantedWidth: wantedWidth } );
+ mw.log( 'mw.page.gallery: [BUG!] Fitting row ' + i + ' to too small a size: ' + preferredHeight );
+ // 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,
+ mw.log( 'mw.page.gallery: Tried to make image ' + newWidth + 'px wide but too narrow.' );
+ 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 ) );
+ }
+
+ hookInfo = {
+ fullWidth: newWidth + padding,
+ imgWidth: newWidth,
+ imgHeight: preferredHeight,
+ $innerDiv: $innerDiv,
+ $imageDiv: $imageDiv,
+ $outerDiv: $outerDiv,
+ // Whether the hook took action
+ resolved: false
+ };
+
+ /**
+ * Gallery resize.
+ *
+ * If your handler resizes an image, it should also set the resolved
+ * property to true. Additionally, because this module only exposes this
+ * logic temporarily, you should load your module in position top to
+ * ensure it is registered before this runs (FIXME: Don't use mw.hook)
+ *
+ * See TimedMediaHandler for an example.
+ *
+ * @event mediawiki_page_gallery_resize
+ * @member mw.hook
+ * @param {Object} hookInfo
+ */
+ mw.hook( 'mediawiki.page.gallery.resize' ).fire( hookInfo );
+
+ if ( !hookInfo.resolved ) {
+ 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, mediaWiki );
diff --git a/resources/mediawiki.page/mediawiki.page.image.pagination.js b/resources/mediawiki.page/mediawiki.page.image.pagination.js
new file mode 100644
index 00000000..fb44a76f
--- /dev/null
+++ b/resources/mediawiki.page/mediawiki.page.image.pagination.js
@@ -0,0 +1,51 @@
+/**
+ * Change multi-page image navigation so that the current page display can be changed
+ * without a page reload. Currently, the only image formats that can be multi-page images are
+ * PDF and DjVu files
+ */
+( function (mw, $) {
+ // Use jQuery's load function to specifically select and replace table.multipageimage's child
+ // tr with the new page's table.multipageimage's tr element.
+ // table.multipageimage always has only one row.
+ function loadPage( page ) {
+ var $multipageimage = $( 'table.multipageimage' ),
+ $spinner = $.createSpinner( {
+ size: 'large',
+ type: 'block'
+ } );
+
+ // Set the spinner's dimensions equal to the table's dimensions so that
+ // the current scroll position is not lost after the table is emptied prior to
+ // its contents being updated
+ $spinner.css( {
+ height: $multipageimage.find( 'tr' ).height(),
+ width: $multipageimage.find( 'tr' ).width()
+ } );
+
+ $multipageimage.empty().append( $spinner ).load(
+ page + ' table.multipageimage tr',
+ ajaxifyPageNavigation
+ );
+ }
+
+ function ajaxifyPageNavigation() {
+ // Intercept the default action of the links in the thumbnail navigation
+ $( '.multipageimagenavbox' ).one( 'click', 'a', function ( e ) {
+ loadPage( this.href );
+ e.preventDefault();
+ } );
+
+ // Prevent the submission of the page select form and instead call loadPage
+ $( 'form[name="pageselector"]' ).one( 'change submit', function ( e ) {
+ loadPage( this.action + '?' + $( this ).serialize() );
+ e.preventDefault();
+ } );
+ }
+
+ $( document ).ready( function() {
+ // The presence of table.multipageimage signifies that this file is a multi-page image
+ if( mw.config.get( 'wgNamespaceNumber' ) === 6 && $( 'table.multipageimage' ).length !== 0 ) {
+ ajaxifyPageNavigation();
+ }
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.page/mediawiki.page.patrol.ajax.js b/resources/mediawiki.page/mediawiki.page.patrol.ajax.js
new file mode 100644
index 00000000..75908eee
--- /dev/null
+++ b/resources/mediawiki.page/mediawiki.page.patrol.ajax.js
@@ -0,0 +1,63 @@
+/**
+ * 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;
+
+ // 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.post( {
+ action: 'patrol',
+ token: mw.user.tokens.get( 'patrolToken' ),
+ 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/mediawiki.page/mediawiki.page.ready.js b/resources/mediawiki.page/mediawiki.page.ready.js
index 370c3a19..ee416d67 100644
--- a/resources/mediawiki.page/mediawiki.page.ready.js
+++ b/resources/mediawiki.page/mediawiki.page.ready.js
@@ -1,24 +1,40 @@
-jQuery( document ).ready( function( $ ) {
+( function ( mw , $ ) {
+ var supportsPlaceholder = 'placeholder' in document.createElement( 'input' );
- /* Emulate placeholder if not supported by browser */
- if ( !( 'placeholder' in document.createElement( 'input' ) ) ) {
- $( 'input[placeholder]' ).placeholder();
- }
+ mw.hook( 'wikipage.content' ).add( function ( $content ) {
+ var $sortableTables;
- /* Enable makeCollapsible */
- $( '.mw-collapsible' ).makeCollapsible();
+ // Run jquery.placeholder polyfill if placeholder is not supported
+ if ( !supportsPlaceholder ) {
+ $content.find( 'input[placeholder]' ).placeholder();
+ }
- /* Lazy load jquery.tablesorter */
- if ( $( 'table.sortable' ).length ) {
- mw.loader.using( 'jquery.tablesorter', function() {
- $( 'table.sortable' ).tablesorter();
- });
- }
+ // Run jquery.makeCollapsible
+ $content.find( '.mw-collapsible' ).makeCollapsible();
- /* Enable CheckboxShiftClick */
- $( 'input[type=checkbox]:not(.noshiftselect)' ).checkboxShiftClick();
+ // Lazy load jquery.tablesorter
+ $sortableTables = $content.find( 'table.sortable' );
+ if ( $sortableTables.length ) {
+ mw.loader.using( 'jquery.tablesorter', function () {
+ $sortableTables.tablesorter();
+ } );
+ }
- /* Add accesskey hints to the tooltips */
- mw.util.updateTooltipAccessKeys();
+ // Run jquery.checkboxShiftClick
+ $content.find( 'input[type="checkbox"]:not(.noshiftselect)' ).checkboxShiftClick();
+ } );
-} );
+ // Things outside the wikipage content
+ $( function () {
+
+ 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
+ mw.util.updateTooltipAccessKeys();
+
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.page/mediawiki.page.startup.js b/resources/mediawiki.page/mediawiki.page.startup.js
index 6a11d3e1..38466818 100644
--- a/resources/mediawiki.page/mediawiki.page.startup.js
+++ b/resources/mediawiki.page/mediawiki.page.startup.js
@@ -5,14 +5,23 @@
// Client profile classes for <html>
// Allows for easy hiding/showing of JS or no-JS-specific UI elements
$( 'html' )
- .addClass('client-js' )
+ .addClass( 'client-js' )
.removeClass( 'client-nojs' );
- // Initialize utilities as soon as the document is ready (mw.util.$content,
- // messageBoxNew, profile, tooltip access keys, Table of contents toggle, ..).
- // Enqueued into domready from here instead of mediawiki.page.ready to ensure that it gets enqueued
- // before other modules hook into document ready, so that mw.util.$content (defined by mw.util.init),
- // is defined for them.
- $( mw.util.init );
+ $( function () {
+ // Initialize utilities as soon as the document is ready (mw.util.$content,
+ // messageBoxNew, profile, tooltip access keys, Table of contents toggle, ..).
+ // In the domready here instead of in mediawiki.page.ready to ensure that it gets enqueued
+ // before other modules hook into domready, so that mw.util.$content (defined by
+ // mw.util.init), is defined for them.
+ mw.util.init();
+
+ /**
+ * @event wikipage_content
+ * @member mw.hook
+ * @param {jQuery} $content
+ */
+ mw.hook( 'wikipage.content' ).fire( $( '#mw-content-text' ) );
+ } );
}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.page/mediawiki.page.watch.ajax.js b/resources/mediawiki.page/mediawiki.page.watch.ajax.js
index a7e059c4..e9afa4a2 100644
--- a/resources/mediawiki.page/mediawiki.page.watch.ajax.js
+++ b/resources/mediawiki.page/mediawiki.page.watch.ajax.js
@@ -24,13 +24,14 @@
otherAction = action === 'watch' ? 'unwatch' : 'watch';
accesskeyTip = $link.attr( 'title' ).match( mw.util.tooltipAccessKeyRegexp );
$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 ) {
+ if ( state === undefined ) {
$li.trigger( 'watchpage.mw', otherAction );
}
@@ -70,7 +71,7 @@
actionPaths = mw.config.get( 'wgActionPaths' );
- // @todo: Does MediaWiki give action path or query param
+ // @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 ) {
@@ -96,10 +97,10 @@
// Expose local methods
mw.page.watch = {
- 'updateWatchLink': updateWatchLink
+ updateWatchLink: updateWatchLink
};
- $( document ).ready( function () {
+ $( 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' );
@@ -134,7 +135,9 @@
otherAction = action === 'watch' ? 'unwatch' : 'watch';
$li = $link.closest( 'li' );
- mw.notify( $.parseHTML( watchResponse.message ), { tag: 'watch-self' } );
+ mw.notify( $.parseHTML( watchResponse.message ), {
+ tag: 'watch-self'
+ } );
// Set link to opposite
updateWatchLink( $link, otherAction );
@@ -144,7 +147,7 @@
if ( watchResponse.watched !== undefined ) {
$( '#wpWatchthis' ).prop( 'checked', true );
} else {
- $( '#wpWatchthis' ).removeProp( 'checked' );
+ $( '#wpWatchthis' ).prop( 'checked', false );
}
},
// Error
@@ -158,18 +161,18 @@
cleanTitle = title.replace( /_/g, ' ' );
link = mw.html.element(
'a', {
- href: mw.util.wikiGetlink( title ),
+ href: mw.util.getUrl( title ),
title: cleanTitle
}, cleanTitle
);
- msg = mw.messsage( 'watcherrortext', link );
+ msg = mw.message( 'watcherrortext', link );
// Report to user about the error
mw.notify( msg, { tag: 'watch-self' } );
}
);
- });
- });
+ } );
+ } );
}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.special/images/arrow-collapsed-ltr.png b/resources/mediawiki.special/images/arrow-collapsed-ltr.png
deleted file mode 100644
index 467a555a..00000000
--- a/resources/mediawiki.special/images/arrow-collapsed-ltr.png
+++ /dev/null
Binary files differ
diff --git a/resources/mediawiki.special/images/arrow-collapsed-rtl.png b/resources/mediawiki.special/images/arrow-collapsed-rtl.png
deleted file mode 100644
index 2246254f..00000000
--- a/resources/mediawiki.special/images/arrow-collapsed-rtl.png
+++ /dev/null
Binary files differ
diff --git a/resources/mediawiki.special/images/arrow-expanded.png b/resources/mediawiki.special/images/arrow-expanded.png
deleted file mode 100644
index 58a9fc66..00000000
--- a/resources/mediawiki.special/images/arrow-expanded.png
+++ /dev/null
Binary files differ
diff --git a/resources/mediawiki.special/images/glyph-people-large.png b/resources/mediawiki.special/images/glyph-people-large.png
new file mode 100644
index 00000000..0578be0b
--- /dev/null
+++ b/resources/mediawiki.special/images/glyph-people-large.png
Binary files differ
diff --git a/resources/mediawiki.special/images/icon-contributors.png b/resources/mediawiki.special/images/icon-contributors.png
new file mode 100644
index 00000000..f933aa69
--- /dev/null
+++ b/resources/mediawiki.special/images/icon-contributors.png
Binary files differ
diff --git a/resources/mediawiki.special/images/icon-edits.png b/resources/mediawiki.special/images/icon-edits.png
new file mode 100644
index 00000000..39f4f2de
--- /dev/null
+++ b/resources/mediawiki.special/images/icon-edits.png
Binary files differ
diff --git a/resources/mediawiki.special/images/icon-lock.png b/resources/mediawiki.special/images/icon-lock.png
new file mode 100644
index 00000000..03f0eecd
--- /dev/null
+++ b/resources/mediawiki.special/images/icon-lock.png
Binary files differ
diff --git a/resources/mediawiki.special/images/icon-pages.png b/resources/mediawiki.special/images/icon-pages.png
new file mode 100644
index 00000000..59513db2
--- /dev/null
+++ b/resources/mediawiki.special/images/icon-pages.png
Binary files differ
diff --git a/resources/mediawiki.special/mediawiki.special.block.js b/resources/mediawiki.special/mediawiki.special.block.js
index 6f79929b..b8bcf177 100644
--- a/resources/mediawiki.special/mediawiki.special.block.js
+++ b/resources/mediawiki.special/mediawiki.special.block.js
@@ -1,46 +1,46 @@
-/* JavaScript for Special:Block */
+/**
+ * JavaScript for Special:Block
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var $blockTarget = $( '#mw-bi-target' ),
+ $anonOnlyRow = $( '#mw-input-wpHardBlock' ).closest( 'tr' ),
+ $enableAutoblockRow = $( '#mw-input-wpAutoBlock' ).closest( 'tr' ),
+ $hideUser = $( '#mw-input-wpHideUser' ).closest( 'tr' ),
+ $watchUser = $( '#mw-input-wpWatch' ).closest( 'tr' );
-jQuery( function( $ ) {
+ function updateBlockOptions( instant ) {
+ var blocktarget = $.trim( $blockTarget.val() ),
+ isEmpty = blocktarget === '',
+ isIp = mw.util.isIPv4Address( blocktarget, true ) || mw.util.isIPv6Address( blocktarget, true ),
+ isIpRange = isIp && blocktarget.match( /\/\d+$/ );
- var DO_INSTANT = true,
- $blockTarget = $( '#mw-bi-target' ),
- $anonOnlyRow = $( '#mw-input-wpHardBlock' ).closest( 'tr' ),
- $enableAutoblockRow = $( '#mw-input-wpAutoBlock' ).closest( 'tr' ),
- $hideUser = $( '#mw-input-wpHideUser' ).closest( 'tr' ),
- $watchUser = $( '#mw-input-wpWatch' ).closest( 'tr' );
-
- var updateBlockOptions = function( instant ) {
- if ( !$blockTarget.length ) {
- return;
+ if ( isIp && !isEmpty ) {
+ $enableAutoblockRow.goOut( instant );
+ $hideUser.goOut( instant );
+ } else {
+ $enableAutoblockRow.goIn( instant );
+ $hideUser.goIn( instant );
+ }
+ if ( !isIp && !isEmpty ) {
+ $anonOnlyRow.goOut( instant );
+ } else {
+ $anonOnlyRow.goIn( instant );
+ }
+ if ( isIpRange && !isEmpty ) {
+ $watchUser.goOut( instant );
+ } else {
+ $watchUser.goIn( instant );
+ }
}
- var blocktarget = $.trim( $blockTarget.val() );
- var isEmpty = ( blocktarget === '' );
- var isIp = mw.util.isIPv4Address( blocktarget, true ) || mw.util.isIPv6Address( blocktarget, true );
- var isIpRange = isIp && blocktarget.match( /\/\d+$/ );
+ if ( $blockTarget.length ) {
+ // Bind functions so they're checked whenever stuff changes
+ $blockTarget.keyup( updateBlockOptions );
- if ( isIp && !isEmpty ) {
- $enableAutoblockRow.goOut( instant );
- $hideUser.goOut( instant );
- } else {
- $enableAutoblockRow.goIn( instant );
- $hideUser.goIn( instant );
- }
- if ( !isIp && !isEmpty ) {
- $anonOnlyRow.goOut( instant );
- } else {
- $anonOnlyRow.goIn( instant );
+ // Call them now to set initial state (ie. Special:Block/Foobar?wpBlockExpiry=2+hours)
+ updateBlockOptions( /* instant= */ true );
}
- if ( isIpRange && !isEmpty ) {
- $watchUser.goOut( instant );
- } else {
- $watchUser.goIn( instant );
- }
- };
-
- // Bind functions so they're checked whenever stuff changes
- $blockTarget.keyup( updateBlockOptions );
+ } );
+}( mediaWiki, jQuery ) );
- // Call them now to set initial state (ie. Special:Block/Foobar?wpBlockExpiry=2+hours)
- updateBlockOptions( DO_INSTANT );
-});
diff --git a/resources/mediawiki.special/mediawiki.special.changeemail.js b/resources/mediawiki.special/mediawiki.special.changeemail.js
index cab0bbd3..2d22bad0 100644
--- a/resources/mediawiki.special/mediawiki.special.changeemail.js
+++ b/resources/mediawiki.special/mediawiki.special.changeemail.js
@@ -1,42 +1,42 @@
-/*
+/**
* JavaScript for Special:ChangeEmail
*/
( function ( mw, $ ) {
+ /**
+ * Given an email validity status (true, false, null) update the label CSS class
+ */
+ function updateMailValidityLabel( mail ) {
+ var isValid = mw.util.validateEmail( mail ),
+ $label = $( '#mw-emailaddress-validity' );
-/**
- * Given an email validity status (true, false, null) update the label CSS class
- */
-function updateMailValidityLabel( mail ) {
- var isValid = mw.util.validateEmail( mail ),
- $label = $( '#mw-emailaddress-validity' );
-
- // We allow empty address
- if( isValid === null ) {
- $label.text( '' ).removeClass( 'valid invalid' );
+ // We allow empty address
+ if ( isValid === null ) {
+ $label.text( '' ).removeClass( 'valid invalid' );
- // Valid
- } else if ( isValid ) {
- $label.text( mw.msg( 'email-address-validity-valid' ) ).addClass( 'valid' ).removeClass( 'invalid' );
+ // Valid
+ } else if ( isValid ) {
+ $label.text( mw.msg( 'email-address-validity-valid' ) ).addClass( 'valid' ).removeClass( 'invalid' );
- // Not valid
- } else {
- $label.text( mw.msg( 'email-address-validity-invalid' ) ).addClass( 'invalid' ).removeClass( 'valid' );
+ // Not valid
+ } else {
+ $label.text( mw.msg( 'email-address-validity-invalid' ) ).addClass( 'invalid' ).removeClass( 'valid' );
+ }
}
-}
-$( document ).ready( function () {
- // Lame tip to let user know if its email is valid. See bug 22449
- // Only bind once for 'blur' so that the user can fill it in without errors
- // After that look at every keypress for direct feedback if it was invalid onblur
- $( '#wpNewEmail' ).one( 'blur', function () {
- if ( $( '#mw-emailaddress-validity' ).length === 0 ) {
- $(this).after( '<label for="wpNewEmail" id="mw-emailaddress-validity"></label>' );
- }
- updateMailValidityLabel( $(this).val() );
- $(this).keyup( function () {
- updateMailValidityLabel( $(this).val() );
+ $( function () {
+ // Lame tip to let user know if its email is valid. See bug 22449.
+ // Only bind once for 'blur' so that the user can fill it in without errors;
+ // after that, look at every keypress for immediate feedback.
+ $( '#wpNewEmail' ).one( 'blur', function () {
+ var $this = $( this );
+ if ( $( '#mw-emailaddress-validity' ).length === 0 ) {
+ $this.after( '<label for="wpNewEmail" id="mw-emailaddress-validity"></label>' );
+ }
+
+ updateMailValidityLabel( $this.val() );
+ $this.keyup( function () {
+ updateMailValidityLabel( $this.val() );
+ } );
} );
} );
-} );
-
}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.special/mediawiki.special.changeslist.css b/resources/mediawiki.special/mediawiki.special.changeslist.css
index 8a5421e8..5e4af7b6 100644
--- a/resources/mediawiki.special/mediawiki.special.changeslist.css
+++ b/resources/mediawiki.special/mediawiki.special.changeslist.css
@@ -2,62 +2,6 @@
* Styling for Special:Watchlist and Special:RecentChanges
*/
-table.mw-enhanced-rc {
- border: 0;
- border-spacing: 0;
-}
-
-table.mw-enhanced-rc th,
-table.mw-enhanced-rc td {
- padding: 0;
- vertical-align: top;
-}
-
-td.mw-enhanced-rc {
- white-space: nowrap;
- font-family: monospace;
-}
-
-.mw-enhanced-rc-time {
- font-family: monospace;
-}
-
-table.mw-enhanced-rc td.mw-enhanced-rc-nested {
- padding-left: 1em;
-}
-
-/* Show/hide arrows in enhanced changeslist */
-.mw-enhanced-rc .collapsible-expander {
- float: none;
-}
-
-/* If JS is disabled, the arrow shouldn't be shown */
-.client-nojs .mw-enhancedchanges-arrow.mw-collapsible-toggle {
- display: none;
-}
-
-.mw-enhancedchanges-arrow {
- display: inline-block;
- *display: inline; /* IE7 and below */
- zoom: 1;
- width: 15px;
- height: 15px;
-}
-
-.mw-enhancedchanges-arrow.mw-enhancedchanges-arrow-space {
- background: none;
-}
-
-.mw-enhancedchanges-arrow.mw-collapsible-toggle-collapsed {
- /* @embed */
- background: url(images/arrow-collapsed-ltr.png) no-repeat left center;
-}
-
-.mw-enhancedchanges-arrow.mw-collapsible-toggle-expanded {
- /* @embed */
- background: url(images/arrow-expanded.png) no-repeat left center;
-}
-
.mw-changeslist-line-watched .mw-title {
font-weight: bold;
}
diff --git a/resources/mediawiki.special/mediawiki.special.changeslist.enhanced.css b/resources/mediawiki.special/mediawiki.special.changeslist.enhanced.css
new file mode 100644
index 00000000..bed580d7
--- /dev/null
+++ b/resources/mediawiki.special/mediawiki.special.changeslist.enhanced.css
@@ -0,0 +1,66 @@
+/**
+ * Styling for Special:Watchlist and Special:RecentChanges when preference 'usenewrc'
+ * a.k.a. Enhanced Recent Changes is enabled.
+ */
+
+table.mw-enhanced-rc {
+ border: 0;
+ border-spacing: 0;
+}
+
+table.mw-enhanced-rc th,
+table.mw-enhanced-rc td {
+ padding: 0;
+ vertical-align: top;
+}
+
+td.mw-enhanced-rc {
+ white-space: nowrap;
+ font-family: monospace;
+}
+
+.mw-enhanced-rc-time {
+ font-family: monospace;
+}
+
+table.mw-enhanced-rc td.mw-enhanced-rc-nested {
+ padding-left: 1em;
+}
+
+/* Show/hide arrows in enhanced changeslist */
+.mw-enhanced-rc .collapsible-expander {
+ float: none;
+}
+
+/* If JS is disabled, the arrows or the placeholder space shouldn't be shown */
+.client-nojs .mw-enhancedchanges-arrow-space {
+ display: none;
+}
+
+/*
+ * And if it's enabled, let's optimize the collapsing a little: hide the rows
+ * that would be hidden by jquery.makeCollapsible with CSS to save us some
+ * reflows and repaints. This doesn't work on browsers that don't fully support
+ * CSS2 (IE6), but it's okay, this will be done in JavaScript with old degraded
+ * performance instead.
+ */
+.client-js table.mw-enhanced-rc.mw-collapsed tr + tr {
+ display: none;
+}
+
+.mw-enhancedchanges-arrow-space {
+ display: inline-block;
+ *display: inline; /* IE7 and below */
+ zoom: 1;
+ width: 15px;
+ height: 15px;
+}
+
+/* let it look like it is clickable */
+.mw-enhancedchanges-arrow.mw-collapsible-toggle {
+ cursor: pointer;
+}
+
+.mw-enhanced-watched .mw-enhanced-rc-time {
+ font-weight: bold;
+}
diff --git a/resources/mediawiki.special/mediawiki.special.createAccount.css b/resources/mediawiki.special/mediawiki.special.createAccount.css
new file mode 100644
index 00000000..11d00e75
--- /dev/null
+++ b/resources/mediawiki.special/mediawiki.special.createAccount.css
@@ -0,0 +1,89 @@
+/* Disable the underline that Vector puts on h2 headings, and bold them. */
+.mw-ui-container h2 {
+ border: 0;
+ font-weight: bold;
+}
+
+/**** shuffled CAPTCHA ****/
+#wpCaptchaWord {
+ margin-top: 6px;
+}
+
+.mw-createacct-captcha-container {
+ background-color: #f8f8f8;
+ border: 1px solid #c9c9c9;
+ padding: 10px;
+ text-align: center;
+}
+
+.mw-createacct-captcha-assisted {
+ display: block;
+ margin-top: 0.5em;
+}
+
+/* Put a border around the fancycaptcha-image-container. */
+.mw-createacct-captcha-and-reload {
+ border: 1px solid #c9c9c9;
+ display: table-cell; /* Other display formats end up too wide */
+ width: 270px;
+ background-color: #FFF;
+}
+
+/* Make the fancycaptcha-image-container full-width within its parent. */
+.fancycaptcha-image-container
+{
+ width: 100%;
+}
+
+/**** Benefits column CSS to the right (if it fits) of the form. ****/
+.mw-ui-container #userloginForm {
+ float: left;
+}
+
+div.mw-createacct-benefits-container {
+ /* Keeps this column compact and close to the form, but tends to squish contents. */
+ float: left;
+}
+
+div.mw-createacct-benefits-container h2 {
+ margin-bottom: 30px;
+}
+
+.mw-number-text.icon-edits {
+ /* @embed */
+ background: url(images/icon-edits.png) no-repeat left center;
+}
+
+.mw-number-text.icon-pages {
+ /* @embed */
+ background: url(images/icon-pages.png) no-repeat left center;
+}
+
+.mw-number-text.icon-contributors {
+ /* @embed */
+ background: url(images/icon-contributors.png) no-repeat left center;
+}
+
+/* Special font for numbers in benefits*/
+div.mw-number-text h3 {
+ top: 0;
+ margin: 0;
+ padding: 0;
+ color: #252525;
+ font-family: 'Georgia', serif;
+ font-weight: normal;
+ font-size: 2.2em;
+ line-height: 1.2;
+ text-align: center;
+}
+
+/* Contains a number and explanatory text, with space for an icon */
+div.mw-number-text {
+ display: block;
+ font-size: 1.2em;
+ color: #444;
+ margin-top: 1em;
+ padding: 0 0 0 95px; /* 80px wide icon plus "margin" */
+ min-height: 75px; /* matches max icon height, ensures icon emblem is visible */
+ text-align: center;
+}
diff --git a/resources/mediawiki.special/mediawiki.special.createAccount.js b/resources/mediawiki.special/mediawiki.special.createAccount.js
new file mode 100644
index 00000000..5cbb1ee0
--- /dev/null
+++ b/resources/mediawiki.special/mediawiki.special.createAccount.js
@@ -0,0 +1,112 @@
+/**
+ * JavaScript for Create account form (Special:UserLogin?type=signup).
+ */
+( function ( mw, $ ) {
+
+ // When sending password by email, hide the password input fields.
+ // This function doesn't need to be loaded early by ResourceLoader, but is tiny.
+ function hidePasswordOnEmail() {
+ // Always required if checked, otherwise it depends, so we use the original
+ var $emailLabel = $( 'label[for="wpEmail"]' ),
+ originalText = $emailLabel.text(),
+ requiredText = mw.message( 'createacct-emailrequired' ).text(),
+ $createByMailCheckbox = $( '#wpCreateaccountMail' ),
+ $beforePwds = $( '.mw-row-password:first' ).prev(),
+ $pwds;
+
+ function updateForCheckbox() {
+ var checked = $createByMailCheckbox.prop( 'checked' );
+ if ( checked ) {
+ $pwds = $( '.mw-row-password' ).detach();
+ $emailLabel.text( requiredText );
+ } else {
+ if ( $pwds ) {
+ $beforePwds.after( $pwds );
+ $pwds = null;
+ }
+ $emailLabel.text( originalText );
+ }
+ }
+
+ $createByMailCheckbox.on( 'change', updateForCheckbox );
+ updateForCheckbox();
+ }
+
+ // Move the FancyCaptcha image into a more attractive container.
+ // This function does need to be run early by ResourceLoader.
+ function adjustFancyCaptcha() {
+ var $content = $( '#mw-content-text' ),
+ $submit = $content.find( '#wpCreateaccount' ),
+ tabIndex,
+ $captchaStuff,
+ $captchaImageContainer,
+ // JavaScript can't yet parse the message createacct-imgcaptcha-help when it
+ // contains a MediaWiki transclusion, so PHP parses it and sends the HTML.
+ helpMsg = mw.config.get( 'wgCreateacctImgcaptchaHelp' ),
+ helpHtml = '';
+
+ /*
+ * CAPTCHA
+ * The CAPTCHA is in a div style="captcha" at the top of the form.
+ * If it's a FancyCaptcha, then we remove it and insert it lower down,
+ * in a customized div with just what we need (e.g. no
+ * fancycaptcha-createaccount message).
+ */
+ if ( !$submit.length) {
+ return;
+ }
+ tabIndex = $submit.prop( 'tabindex' ) - 1;
+ $captchaStuff = $content.find ( '.captcha' );
+
+ if ( $captchaStuff.length ) {
+
+ // The FancyCaptcha has this class in the ConfirmEdit extension
+ // after 2013-04-18.
+ $captchaImageContainer = $captchaStuff.find( '.fancycaptcha-image-container' );
+ if ( $captchaImageContainer.length !== 1 ) {
+ return;
+ }
+
+ $captchaStuff.remove();
+
+ if ( helpMsg) {
+ helpHtml = '<small class="mw-createacct-captcha-assisted">' + helpMsg + '</small>';
+ }
+
+ // Insert another div before the submit button that will include the
+ // repositioned FancyCaptcha div, an input field, and possible help.
+ $submit.closest( 'div' )
+ .before( [
+ '<div>',
+ '<label for="wpCaptchaWord">' + mw.message( 'createacct-captcha' ).escaped() + '</label>',
+ '<div class="mw-createacct-captcha-container">',
+ '<div class="mw-createacct-captcha-and-reload" />',
+ '<input id="wpCaptchaWord" name="wpCaptchaWord" type="text" placeholder="' +
+ mw.message( 'createacct-imgcaptcha-ph' ).escaped() +
+ '" tabindex="' + tabIndex + '" autocapitalize="off" autocorrect="off">',
+ helpHtml,
+ '</div>',
+ '</div>'
+ ].join( '' )
+ );
+
+ // Stick the FancyCaptcha container inside our bordered and framed parents.
+ $captchaImageContainer
+ .prependTo( $content.find( '.mw-createacct-captcha-and-reload' ) );
+
+ // Find the input field, add the text (if any) of the existing CAPTCHA
+ // field (although usually it's blanked out on every redisplay),
+ // and after it move over the hidden field that tells the CAPTCHA
+ // what to do.
+ $content.find( '#wpCaptchaWord' )
+ .val( $captchaStuff.find( '#wpCaptchaWord' ).val() )
+ .after( $captchaStuff.find( '#wpCaptchaId' ) );
+ }
+ }
+
+ $( function () {
+ adjustFancyCaptcha();
+ hidePasswordOnEmail();
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.special/mediawiki.special.javaScriptTest.js b/resources/mediawiki.special/mediawiki.special.javaScriptTest.js
index 808d5fe8..a560ca95 100644
--- a/resources/mediawiki.special/mediawiki.special.javaScriptTest.js
+++ b/resources/mediawiki.special/mediawiki.special.javaScriptTest.js
@@ -8,7 +8,7 @@
// (only if a framework was found, not on error pages).
$( '#mw-javascripttest-summary.mw-javascripttest-frameworkfound' ).append( function () {
- var $html = $( '<p><label for="useskin">'
+ var $html = $( '<p><label for="useskin">'
+ mw.message( 'javascripttest-pagetext-skins' ).escaped()
+ ' '
+ '</label></p>' ),
diff --git a/resources/mediawiki.special/mediawiki.special.js b/resources/mediawiki.special/mediawiki.special.js
index 3526cef4..8edb1cbe 100644
--- a/resources/mediawiki.special/mediawiki.special.js
+++ b/resources/mediawiki.special/mediawiki.special.js
@@ -1 +1,5 @@
-mw.special = {};
+/*
+ * Namespace for mediawiki.special.* modules
+ */
+
+mediaWiki.special = {};
diff --git a/resources/mediawiki.special/mediawiki.special.movePage.js b/resources/mediawiki.special/mediawiki.special.movePage.js
index 68c2ed07..922eba5e 100644
--- a/resources/mediawiki.special/mediawiki.special.movePage.js
+++ b/resources/mediawiki.special/mediawiki.special.movePage.js
@@ -1,5 +1,6 @@
-/* JavaScript for Special:MovePage */
-
-jQuery( function( $ ) {
+/**
+ * JavaScript for Special:MovePage
+ */
+jQuery( function ( $ ) {
$( '#wpReason, #wpNewTitleMain' ).byteLimit();
-});
+} );
diff --git a/resources/mediawiki.special/mediawiki.special.pagesWithProp.css b/resources/mediawiki.special/mediawiki.special.pagesWithProp.css
new file mode 100644
index 00000000..7ef75d0c
--- /dev/null
+++ b/resources/mediawiki.special/mediawiki.special.pagesWithProp.css
@@ -0,0 +1,4 @@
+/* Distinguish actual data from information about it being hidden visually */
+.prop-value-hidden {
+ font-style: italic;
+}
diff --git a/resources/mediawiki.special/mediawiki.special.preferences.js b/resources/mediawiki.special/mediawiki.special.preferences.js
index 47872907..03d93d00 100644
--- a/resources/mediawiki.special/mediawiki.special.preferences.js
+++ b/resources/mediawiki.special/mediawiki.special.preferences.js
@@ -1,175 +1,199 @@
-/*
+/**
* JavaScript for Special:Preferences
*/
-jQuery( document ).ready( function ( $ ) {
-$( '#prefsubmit' ).attr( 'id', 'prefcontrol' );
-var $preftoc = $('<ul id="preftoc"></ul>');
-var $preferences = $( '#preferences' )
- .addClass( 'jsprefs' )
- .before( $preftoc );
-
-var $fieldsets = $preferences.children( 'fieldset' )
- .hide()
- .addClass( 'prefsection' );
+jQuery( function ( $ ) {
+ var $preftoc, $preferences, $fieldsets, $legends,
+ hash,
+ $tzSelect, $tzTextbox, $localtimeHolder, servertime;
+
+ $( '#prefsubmit' ).attr( 'id', 'prefcontrol' );
+
+ $preftoc = $('<ul id="preftoc"></ul>');
+ $preferences = $( '#preferences' )
+ .addClass( 'jsprefs' )
+ .before( $preftoc );
+ $fieldsets = $preferences.children( 'fieldset' )
+ .hide()
+ .addClass( 'prefsection' );
+ $legends = $fieldsets
+ .children( 'legend' )
+ .addClass( 'mainLegend' );
+
+ /**
+ * It uses document.getElementById for security reasons (HTML injections in $()).
+ *
+ * @param String name: the name of a tab without the prefix ("mw-prefsection-")
+ * @param String mode: [optional] A hash will be set according to the current
+ * open section. Set mode 'noHash' to surpress this.
+ */
+ function switchPrefTab( name, mode ) {
+ var $tab, scrollTop;
+ // Handle hash manually to prevent jumping,
+ // therefore save and restore scrollTop to prevent jumping.
+ scrollTop = $( window ).scrollTop();
+ if ( mode !== 'noHash' ) {
+ window.location.hash = '#mw-prefsection-' + name;
+ }
+ $( window ).scrollTop( scrollTop );
+
+ $preftoc.find( 'li' ).removeClass( 'selected' );
+ $tab = $( document.getElementById( 'preftab-' + name ) );
+ if ( $tab.length ) {
+ $tab.parent().addClass( 'selected' );
+ $preferences.children( 'fieldset' ).hide();
+ $( document.getElementById( 'mw-prefsection-' + name ) ).show();
+ }
+ }
-var $legends = $fieldsets.children( 'legend' )
- .addClass( 'mainLegend' );
+ // Populate the prefToc
+ $legends.each( function ( i, legend ) {
+ var $legend = $(legend),
+ ident, $li, $a;
+ if ( i === 0 ) {
+ $legend.parent().show();
+ }
+ ident = $legend.parent().attr( 'id' );
+
+ $li = $( '<li>' )
+ .addClass( i === 0 ? 'selected' : '' );
+ $a = $( '<a>' )
+ .attr( {
+ id: ident.replace( 'mw-prefsection', 'preftab' ),
+ href: '#' + ident
+ } )
+ .text( $legend.text() );
+ $li.append( $a );
+ $preftoc.append( $li );
+ } );
+
+ // If we've reloaded the page or followed an open-in-new-window,
+ // make the selected tab visible.
+ hash = window.location.hash;
+ if ( hash.match( /^#mw-prefsection-[\w\-]+/ ) ) {
+ switchPrefTab( hash.replace( '#mw-prefsection-' , '' ) );
+ }
-/**
- * It uses document.getElementById for security reasons (html injections in
- * jQuery()).
- *
- * @param String name: the name of a tab without the prefix ("mw-prefsection-")
- * @param String mode: [optional] A hash will be set according to the current
- * open section. Set mode 'noHash' to surpress this.
- */
-function switchPrefTab( name, mode ) {
- var $tab, scrollTop;
- // Handle hash manually to prevent jumping,
- // therefore save and restore scrollTop to prevent jumping.
- scrollTop = $( window ).scrollTop();
- if ( mode !== 'noHash' ) {
- window.location.hash = '#mw-prefsection-' + name;
+ // In browsers that support the onhashchange event we will not bind click
+ // handlers and instead let the browser do the default behavior (clicking the
+ // <a href="#.."> will naturally set the hash, handled by onhashchange.
+ // But other things that change the hash will also be catched (e.g. using
+ // the Back and Forward browser navigation).
+ // Note the special check for IE "compatibility" mode.
+ if ( 'onhashchange' in window &&
+ ( document.documentMode === undefined || document.documentMode >= 8 )
+ ) {
+ $(window).on( 'hashchange' , function () {
+ var hash = window.location.hash;
+ if ( hash.match( /^#mw-prefsection-[\w\-]+/ ) ) {
+ switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
+ } else if ( hash === '' ) {
+ switchPrefTab( 'personal', 'noHash' );
+ }
+ });
+ // In older browsers we'll bind a click handler as fallback.
+ // We must not have onhashchange *and* the click handlers, other wise
+ // the click handler calls switchPrefTab() which sets the hash value,
+ // which triggers onhashcange and calls switchPrefTab() again.
+ } else {
+ $preftoc.on( 'click', 'li a', function ( e ) {
+ switchPrefTab( $( this ).attr( 'href' ).replace( '#mw-prefsection-', '' ) );
+ e.preventDefault();
+ });
}
- $( window ).scrollTop( scrollTop );
-
- $preftoc.find( 'li' ).removeClass( 'selected' );
- $tab = $( document.getElementById( 'preftab-' + name ) );
- if ( $tab.length ) {
- $tab.parent().addClass( 'selected' );
- $preferences.children( 'fieldset' ).hide();
- $( document.getElementById( 'mw-prefsection-' + name ) ).show();
+
+ /**
+ * Timezone functions.
+ * Guesses Timezone from browser and updates fields onchange
+ */
+
+ $tzSelect = $( '#mw-input-wptimecorrection' );
+ $tzTextbox = $( '#mw-input-wptimecorrection-other' );
+ $localtimeHolder = $( '#wpLocalTime' );
+ servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 );
+
+ function minutesToHours( min ) {
+ var tzHour = Math.floor( Math.abs( min ) / 60 ),
+ tzMin = Math.abs( min ) % 60,
+ tzString = ( ( min >= 0 ) ? '' : '-' ) + ( ( tzHour < 10 ) ? '0' : '' ) + tzHour +
+ ':' + ( ( tzMin < 10 ) ? '0' : '' ) + tzMin;
+ return tzString;
}
-}
-// Populate the prefToc
-$legends.each( function ( i, legend ) {
- var $legend = $(legend);
- if ( i === 0 ) {
- $legend.parent().show();
+ function hoursToMinutes( hour ) {
+ var minutes,
+ arr = hour.split( ':' );
+
+ arr[0] = parseInt( arr[0], 10 );
+
+ if ( arr.length === 1 ) {
+ // Specification is of the form [-]XX
+ minutes = arr[0] * 60;
+ } else {
+ // Specification is of the form [-]XX:XX
+ minutes = Math.abs( arr[0] ) * 60 + parseInt( arr[1], 10 );
+ if ( arr[0] < 0 ) {
+ minutes *= -1;
+ }
+ }
+ // Gracefully handle non-numbers.
+ if ( isNaN( minutes ) ) {
+ return 0;
+ } else {
+ return minutes;
+ }
}
- var ident = $legend.parent().attr( 'id' );
-
- var $li = $( '<li/>', {
- 'class' : ( i === 0 ) ? 'selected' : null
- });
- var $a = $( '<a/>', {
- text : $legend.text(),
- id : ident.replace( 'mw-prefsection', 'preftab' ),
- href : '#' + ident
- });
- $li.append( $a );
- $preftoc.append( $li );
-} );
-// If we've reloaded the page or followed an open-in-new-window,
-// make the selected tab visible.
-var hash = window.location.hash;
-if ( hash.match( /^#mw-prefsection-[\w\-]+/ ) ) {
- switchPrefTab( hash.replace( '#mw-prefsection-' , '' ) );
-}
-
-// In browsers that support the onhashchange event we will not bind click
-// handlers and instead let the browser do the default behavior (clicking the
-// <a href="#.."> will naturally set the hash, handled by onhashchange.
-// But other things that change the hash will also be catched (e.g. using
-// the Back and Forward browser navigation).
-if ( 'onhashchange' in window ) {
- $(window).on( 'hashchange' , function () {
- var hash = window.location.hash;
- if ( hash.match( /^#mw-prefsection-[\w\-]+/ ) ) {
- switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
- } else if ( hash === '' ) {
- switchPrefTab( 'personal', 'noHash' );
+ function updateTimezoneSelection () {
+ var minuteDiff, localTime,
+ type = $tzSelect.val();
+
+ if ( type === 'guess' ) {
+ // Get browser timezone & fill it in
+ minuteDiff = -( new Date().getTimezoneOffset() );
+ $tzTextbox.val( minutesToHours( minuteDiff ) );
+ $tzSelect.val( 'other' );
+ $tzTextbox.prop( 'disabled', false );
+ } else if ( type === 'other' ) {
+ // Grab data from the textbox, parse it.
+ minuteDiff = hoursToMinutes( $tzTextbox.val() );
+ } else {
+ // Grab data from the $tzSelect value
+ minuteDiff = parseInt( type.split( '|' )[1], 10 ) || 0;
+ $tzTextbox.val( minutesToHours( minuteDiff ) );
}
- });
-// In older browsers we'll bind a click handler as fallback.
-// We must not have onhashchange *and* the click handlers, other wise
-// the click handler calls switchPrefTab() which sets the hash value,
-// which triggers onhashcange and calls switchPrefTab() again.
-} else {
- $preftoc.on( 'click', 'li a', function ( e ) {
- switchPrefTab( $( this ).attr( 'href' ).replace( '#mw-prefsection-', '' ) );
- e.preventDefault();
- });
-}
-/**
-* Timezone functions.
-* Guesses Timezone from browser and updates fields onchange
-*/
-
-var $tzSelect = $( '#mw-input-wptimecorrection' );
-var $tzTextbox = $( '#mw-input-wptimecorrection-other' );
-
-var $localtimeHolder = $( '#wpLocalTime' );
-var servertime = parseInt( $( 'input[name=wpServerTime]' ).val(), 10 );
-var minuteDiff = 0;
-
-var minutesToHours = function ( min ) {
- var tzHour = Math.floor( Math.abs( min ) / 60 );
- var tzMin = Math.abs( min ) % 60;
- var tzString = ( ( min >= 0 ) ? '' : '-' ) + ( ( tzHour < 10 ) ? '0' : '' ) + tzHour +
- ':' + ( ( tzMin < 10 ) ? '0' : '' ) + tzMin;
- return tzString;
-};
-
-var hoursToMinutes = function ( hour ) {
- var arr = hour.split( ':' );
- arr[0] = parseInt( arr[0], 10 );
-
- var minutes;
- if ( arr.length == 1 ) {
- // Specification is of the form [-]XX
- minutes = arr[0] * 60;
- } else {
- // Specification is of the form [-]XX:XX
- minutes = Math.abs( arr[0] ) * 60 + parseInt( arr[1], 10 );
- if ( arr[0] < 0 ) {
- minutes *= -1;
+ // Determine local time from server time and minutes difference, for display.
+ localTime = servertime + minuteDiff;
+
+ // Bring time within the [0,1440) range.
+ while ( localTime < 0 ) {
+ localTime += 1440;
}
+ while ( localTime >= 1440 ) {
+ localTime -= 1440;
+ }
+ $localtimeHolder.text( minutesToHours( localTime ) );
}
- // Gracefully handle non-numbers.
- if ( isNaN( minutes ) ) {
- return 0;
- } else {
- return minutes;
- }
-};
-
-var updateTimezoneSelection = function () {
- var type = $tzSelect.val();
- if ( type == 'guess' ) {
- // Get browser timezone & fill it in
- minuteDiff = -new Date().getTimezoneOffset();
- $tzTextbox.val( minutesToHours( minuteDiff ) );
- $tzSelect.val( 'other' );
- $tzTextbox.get( 0 ).disabled = false;
- } else if ( type == 'other' ) {
- // Grab data from the textbox, parse it.
- minuteDiff = hoursToMinutes( $tzTextbox.val() );
- } else {
- // Grab data from the $tzSelect value
- minuteDiff = parseInt( type.split( '|' )[1], 10 ) || 0;
- $tzTextbox.val( minutesToHours( minuteDiff ) );
+
+ if ( $tzSelect.length && $tzTextbox.length ) {
+ $tzSelect.change( updateTimezoneSelection );
+ $tzTextbox.blur( updateTimezoneSelection );
+ updateTimezoneSelection();
}
- // Determine local time from server time and minutes difference, for display.
- var localTime = servertime + minuteDiff;
+ // Preserve the tab after saving the preferences
+ // Not using cookies, because their deletion results are inconsistent.
+ // Not using jStorage due to its enormous size (for this feature)
+ if ( window.sessionStorage ) {
+ if ( sessionStorage.getItem( 'mediawikiPreferencesTab' ) !== null ) {
+ switchPrefTab( sessionStorage.getItem( 'mediawikiPreferencesTab' ), 'noHash' );
+ }
+ // Deleting the key, the tab states should be reset until we press Save
+ sessionStorage.removeItem( 'mediawikiPreferencesTab' );
- // Bring time within the [0,1440) range.
- while ( localTime < 0 ) {
- localTime += 1440;
- }
- while ( localTime >= 1440 ) {
- localTime -= 1440;
+ $( '#mw-prefs-form' ).submit( function () {
+ var storageData = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
+ sessionStorage.setItem( 'mediawikiPreferencesTab', storageData );
+ } );
}
- $localtimeHolder.text( minutesToHours( localTime ) );
-};
-
-if ( $tzSelect.length && $tzTextbox.length ) {
- $tzSelect.change( function () { updateTimezoneSelection(); } );
- $tzTextbox.blur( function () { updateTimezoneSelection(); } );
- updateTimezoneSelection();
-}
} );
diff --git a/resources/mediawiki.special/mediawiki.special.recentchanges.js b/resources/mediawiki.special/mediawiki.special.recentchanges.js
index 7996d935..79d793af 100644
--- a/resources/mediawiki.special/mediawiki.special.recentchanges.js
+++ b/resources/mediawiki.special/mediawiki.special.recentchanges.js
@@ -1,39 +1,34 @@
-/* JavaScript for Special:RecentChanges */
+/**
+ * JavaScript for Special:RecentChanges
+ */
( function ( mw, $ ) {
+ var rc, $checkboxes, $select;
- var checkboxes = [ 'nsassociated', 'nsinvert' ];
-
- /**
- * @var select {jQuery}
- */
- var $select = null;
-
- var rc = mw.special.recentchanges = {
-
+ rc = {
/**
* Handler to disable/enable the namespace selector checkboxes when the
* special 'all' namespace is selected/unselected respectively.
*/
updateCheckboxes: function () {
// The option element for the 'all' namespace has an empty value
- var isAllNS = $select.find('option:selected').val() === '';
+ var isAllNS = $select.val() === '';
// Iterates over checkboxes and propagate the selected option
- $.each( checkboxes, function ( i, id ) {
- $( '#' + id ).prop( 'disabled', isAllNS );
- });
+ $checkboxes.prop( 'disabled', isAllNS );
},
init: function () {
- // Populate
$select = $( '#namespace' );
+ $checkboxes = $( '#nsassociated, #nsinvert' );
// Bind to change event, and trigger once to set the initial state of the checkboxes.
- $select.change( rc.updateCheckboxes ).change();
+ rc.updateCheckboxes();
+ $select.change( rc.updateCheckboxes );
}
};
- // Run when document is ready
$( rc.init );
+ mw.special.recentchanges = rc;
+
}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.special/mediawiki.special.search.js b/resources/mediawiki.special/mediawiki.special.search.js
index 04954e8d..035252bf 100644
--- a/resources/mediawiki.special/mediawiki.special.search.js
+++ b/resources/mediawiki.special/mediawiki.special.search.js
@@ -1,49 +1,53 @@
-/*
+/**
* JavaScript for Special:Search
*/
-( function( $, mw ) { $( function() {
+( function ( mw, $ ) {
+ $( function () {
+ var $checkboxes, $headerLinks;
-// Emulate HTML5 autofocus behavior in non HTML5 compliant browsers
-if ( !( 'autofocus' in document.createElement( 'input' ) ) ) {
- $( 'input[autofocus]:first' ).focus();
-}
+ // Emulate HTML5 autofocus behavior in non HTML5 compliant browsers
+ if ( !( 'autofocus' in document.createElement( 'input' ) ) ) {
+ $( 'input[autofocus]' ).eq( 0 ).focus();
+ }
-// Create check all/none button
-var $checkboxes = $('#powersearch input[id^=mw-search-ns]');
-$('#mw-search-togglebox').append(
- $('<label />')
- .text(mw.msg('powersearch-togglelabel'))
-).append(
- $('<input type="button" />')
- .attr('id', 'mw-search-toggleall')
- .attr('value', mw.msg('powersearch-toggleall'))
- .click( function() {
- $checkboxes.prop('checked', true);
- } )
-).append(
- $('<input type="button" />')
- .attr('id', 'mw-search-togglenone')
- .attr('value', mw.msg('powersearch-togglenone'))
- .click( function() {
- $checkboxes.prop('checked', false);
- } )
-);
+ // Create check all/none button
+ $checkboxes = $('#powersearch input[id^=mw-search-ns]');
+ $('#mw-search-togglebox').append(
+ $('<label>')
+ .text(mw.msg('powersearch-togglelabel'))
+ ).append(
+ $('<input type="button" />')
+ .attr( 'id', 'mw-search-toggleall' )
+ .prop( 'value', mw.msg('powersearch-toggleall' ) )
+ .click( function () {
+ $checkboxes.prop('checked', true);
+ } )
+ ).append(
+ $('<input type="button" />')
+ .attr( 'id', 'mw-search-togglenone' )
+ .prop( 'value', mw.msg('powersearch-togglenone' ) )
+ .click( function() {
+ $checkboxes.prop( 'checked', false );
+ } )
+ );
-// Change the header search links to what user entered
-var headerLinks = $('.search-types a');
-$('#searchText, #powerSearchText').change(function() {
- var searchterm = $(this).val();
- headerLinks.each( function() {
- var parts = $(this).attr('href').split( 'search=' );
- var lastpart = '';
- var prefix = 'search=';
- if( parts.length > 1 && parts[1].indexOf('&') >= 0 ) {
- lastpart = parts[1].substring( parts[1].indexOf('&') );
- } else {
- prefix = '&search=';
- }
- this.href = parts[0] + prefix + encodeURIComponent( searchterm ) + lastpart;
- });
-}).trigger('change');
+ // Change the header search links to what user entered
+ $headerLinks = $( '.search-types a' );
+ $( '#searchText, #powerSearchText' ).change( function () {
+ var searchterm = $(this).val();
+ $headerLinks.each( function () {
+ var parts = $(this).attr('href').split( 'search=' ),
+ lastpart = '',
+ prefix = 'search=';
+ if ( parts.length > 1 && parts[1].indexOf('&') >= 0 ) {
+ lastpart = parts[1].substring( parts[1].indexOf('&') );
+ } else {
+ prefix = '&search=';
+ }
+ this.href = parts[0] + prefix + encodeURIComponent( searchterm ) + lastpart;
+ });
+ }).trigger( 'change' );
+
+ } );
-} ); } )( jQuery, mediaWiki );
+}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki.special/mediawiki.special.undelete.js b/resources/mediawiki.special/mediawiki.special.undelete.js
index 33b80275..0dea3ef9 100644
--- a/resources/mediawiki.special/mediawiki.special.undelete.js
+++ b/resources/mediawiki.special/mediawiki.special.undelete.js
@@ -1,10 +1,11 @@
-/*
- * JavaScript for Specical:Undelete
+/**
+ * JavaScript for Special:Undelete
*/
-jQuery( document ).ready( function( $ ) {
- $( '#mw-undelete-invert' ).click( function( e ) {
+jQuery( function ( $ ) {
+ $( '#mw-undelete-invert' ).click( function ( e ) {
+ $( '#undelete input[type="checkbox"]' ).prop( 'checked', function ( i, val ) {
+ return !val;
+ } );
e.preventDefault();
- $( '#undelete' ).find( 'input:checkbox' )
- .prop( 'checked', function( i, val ) { return !val; } );
} );
} );
diff --git a/resources/mediawiki.special/mediawiki.special.upload.js b/resources/mediawiki.special/mediawiki.special.upload.js
index 63e89713..3f40c549 100644
--- a/resources/mediawiki.special/mediawiki.special.upload.js
+++ b/resources/mediawiki.special/mediawiki.special.upload.js
@@ -6,12 +6,12 @@
/**
* Add a preview to the upload form
*/
- $( function ( $ ) {
+ $( function () {
/**
* Is the FileAPI available with sufficient functionality?
*/
function hasFileAPI() {
- return typeof window.FileReader !== 'undefined';
+ return window.FileReader !== undefined;
}
/**
@@ -25,7 +25,7 @@
* @return boolean
*/
function fileIsPreviewable( file ) {
- var known = ['image/png', 'image/gif', 'image/jpeg', 'image/svg+xml'],
+ var known = ['image/png', 'image/gif', 'image/jpeg', 'image/svg+xml'],
tooHuge = 10 * 1024 * 1024;
return ( $.inArray( file.type, known ) !== -1 ) && file.size > 0 && file.size < tooHuge;
}
@@ -43,23 +43,26 @@
* @param {File} file
*/
function showPreview( file ) {
- var previewSize = 180,
+ var $canvas,
+ ctx,
+ meta,
+ previewSize = 180,
thumb = $( '<div id="mw-upload-thumbnail" class="thumb tright">' +
'<div class="thumbinner">' +
'<div class="mw-small-spinner" style="width: 180px; height: 180px"></div>' +
'<div class="thumbcaption"><div class="filename"></div><div class="fileinfo"></div></div>' +
'</div>' +
'</div>' );
+
thumb.find( '.filename' ).text( file.name ).end()
.find( '.fileinfo' ).text( prettySize( file.size ) ).end();
- var $canvas = $('<canvas width="' + previewSize + '" height="' + previewSize + '" ></canvas>'),
- ctx = $canvas[0].getContext( '2d' );
+ $canvas = $('<canvas width="' + previewSize + '" height="' + previewSize + '" ></canvas>');
+ ctx = $canvas[0].getContext( '2d' );
$( '#mw-htmlform-source' ).parent().prepend( thumb );
- var meta;
- fetchPreview( file, function( dataURL ) {
- var img = new Image(),
+ fetchPreview( file, function ( dataURL ) {
+ var img = new Image(),
rotation = 0;
if ( meta && meta.tiff && meta.tiff.Orientation ) {
@@ -79,7 +82,8 @@
}
img.onload = function () {
- var width, height, x, y, dx, dy, logicalWidth, logicalHeight;
+ var info, width, height, x, y, dx, dy, logicalWidth, logicalHeight;
+
// Fit the image within the previewSizexpreviewSize box
if ( img.width > img.height ) {
width = previewSize;
@@ -129,12 +133,14 @@
thumb.find('.mw-small-spinner').replaceWith($canvas);
// Image size
- var info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) +
+ info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) +
', ' + prettySize( file.size );
+
$( '#mw-upload-thumbnail .fileinfo' ).text( info );
};
img.src = dataURL;
}, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) {
+ /*jshint camelcase: false, nomen: false */
try {
meta = mw.libs.jpegmeta( data, file.fileName );
meta._binary_data = null;
@@ -171,9 +177,10 @@
// However, our JPEG metadata library wants a string.
// So, this is going to be an ugly conversion.
reader.onload = function() {
- var buffer = new Uint8Array( reader.result ),
+ var i,
+ buffer = new Uint8Array( reader.result ),
string = '';
- for ( var i = 0; i < buffer.byteLength; i++ ) {
+ for ( i = 0; i < buffer.byteLength; i++ ) {
string += String.fromCharCode( buffer[i] );
}
callbackBinary( string );
@@ -196,7 +203,7 @@
} else {
// This ends up decoding the file to base-64 and back again, which
// feels horribly inefficient.
- reader.onload = function() {
+ reader.onload = function () {
callback( reader.result );
};
reader.readAsDataURL( file );
@@ -230,22 +237,29 @@
* Check if the file does not exceed the maximum size
*/
function checkMaxUploadSize( file ) {
+ var maxSize, $error;
+
function getMaxUploadSize( type ) {
var sizes = mw.config.get( 'wgMaxUploadSize' );
+
if ( sizes[type] !== undefined ) {
return sizes[type];
}
return sizes['*'];
}
+
$( '.mw-upload-source-error' ).remove();
- var maxSize = getMaxUploadSize( 'file' );
+ maxSize = getMaxUploadSize( 'file' );
if ( file.size > maxSize ) {
- var error = $( '<p class="error mw-upload-source-error" id="wpSourceTypeFile-error">' +
- mw.message( 'largefileserver', file.size, maxSize ).escaped() + '</p>' );
- $( '#wpUploadFile' ).after( error );
+ $error = $( '<p class="error mw-upload-source-error" id="wpSourceTypeFile-error">' +
+ mw.message( 'largefileserver', file.size, maxSize ).escaped() + '</p>' );
+
+ $( '#wpUploadFile' ).after( $error );
+
return false;
}
+
return true;
}
@@ -276,23 +290,30 @@
/**
* Disable all upload source fields except the selected one
*/
- $( function ( $ ) {
- var i, row,
- rows = $( '.mw-htmlform-field-UploadSourceField' );
- for ( i = rows.length; i; i-- ) {
- row = rows[i - 1];
- $( 'input[name="wpSourceType"]', row ).change( ( function () {
- var currentRow = row; // Store current row in our own scope
- return function () {
- $( '.mw-upload-source-error' ).remove();
- if ( this.checked ) {
- // Disable all inputs
- $( 'input[name!="wpSourceType"]', rows ).prop( 'disabled', true );
- // Re-enable the current one
- $( 'input', currentRow ).prop( 'disabled', false );
- }
- };
- }() ) );
+ $( function () {
+ var i, $row,
+ $rows = $( '.mw-htmlform-field-UploadSourceField' );
+
+ function createHandler( $currentRow ) {
+ /**
+ * @param {jQuery.Event}
+ */
+ return function () {
+ $( '.mw-upload-source-error' ).remove();
+ if ( this.checked ) {
+ // Disable all inputs
+ $rows.find( 'input[name!="wpSourceType"]' ).prop( 'disabled', true );
+ // Re-enable the current one
+ $currentRow.find( 'input' ).prop( 'disabled', false );
+ }
+ };
+ }
+
+ for ( i = $rows.length; i; i-- ) {
+ $row = $rows.eq(i - 1);
+ $row
+ .find( 'input[name="wpSourceType"]' )
+ .change( createHandler( $row ) );
}
} );
diff --git a/resources/mediawiki.special/mediawiki.special.userLogin.css b/resources/mediawiki.special/mediawiki.special.userLogin.css
new file mode 100644
index 00000000..24c8d771
--- /dev/null
+++ b/resources/mediawiki.special/mediawiki.special.userLogin.css
@@ -0,0 +1,39 @@
+/* Styles just for VForm user login */
+#mw-userlogin-help {
+ text-align: center;
+}
+
+.mw-ui-vform .mw-secure {
+ /* @embed */
+ background: url(images/icon-lock.png) no-repeat scroll left center transparent;
+ margin: 0 0 0 1px;
+ padding: 0 0 0 11px;
+}
+
+/* The login form invites users to create an account */
+#mw-createaccount-cta {
+ width: 20em;
+ height: 10em;
+ /* @embed */
+ background: url(images/glyph-people-large.png) no-repeat 50%;
+ margin: 0 auto;
+}
+
+#mw-createaccount-cta h3,
+#mw-createaccount-another h3 {
+ font-size: 0.9em;
+ font-weight: normal;
+ text-align: center;
+}
+
+#mw-createaccount-cta h3 {
+ padding-top: 4em;
+}
+
+#mw-createaccount-join {
+ margin-left: 0.75em;
+ /* Separate from background image */
+ box-shadow: 4px 4px 4px 4px rgba(255, 255, 255, 1);
+ width: auto;
+ display: inline-block;
+}
diff --git a/resources/mediawiki.special/mediawiki.special.vforms.css b/resources/mediawiki.special/mediawiki.special.vforms.css
new file mode 100644
index 00000000..768a9c6e
--- /dev/null
+++ b/resources/mediawiki.special/mediawiki.special.vforms.css
@@ -0,0 +1,46 @@
+/*
+ * When inside the VForm style, disable the border that Vector and other skins
+ * put on the div surrounding the login/create account form.
+ * Also disable the margin and padding that Vector puts around the form.
+ */
+.mw-ui-container #userloginForm,
+.mw-ui-container #userlogin {
+ border: 0;
+ margin: 0;
+ padding: 0;
+}
+
+/* Reposition and resize language links, which appear on a per-wiki basis */
+.mw-ui-container #languagelinks {
+ margin-bottom: 2em;
+ font-size: 0.8em;
+}
+
+/* Put some space under template's header, which may contain CAPTCHA HTML.*/
+section.mw-form-header {
+ margin-bottom: 10px;
+}
+
+/*
+ * Styles for information boxes.
+ */
+.mw-ui-vform .errorbox,
+.mw-ui-vform .warningbox,
+.mw-ui-vform .successbox {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ font-size: 0.9em;
+ margin: 0 0 1em 0;
+ padding: 0.5em;
+ word-wrap: break-word;
+}
+
+/*
+ * Override the right margin of the form to give space in case a benefits
+ * column appears to the side.
+ *
+ */
+.mw-ui-container #userloginForm {
+ margin-right: 100px;
+}
diff --git a/resources/mediawiki.ui/mediawiki.ui.default.css b/resources/mediawiki.ui/mediawiki.ui.default.css
new file mode 100644
index 00000000..b0726165
--- /dev/null
+++ b/resources/mediawiki.ui/mediawiki.ui.default.css
@@ -0,0 +1,272 @@
+@charset "UTF-8";
+/**
+ * Provide Agora appearance for mw-ui-* classes when using a skin other than
+ * Vector.
+ * Compass builds these Agora styles from source Sass files in
+ * extensions/Agora/modules/scss
+ */
+/* _effects.scss */
+/* Mixins for visual effects in CSS3 */
+/* line 7, sourcefiles/scss/components/_utilities.scss */
+.mw-ui-flush-left {
+ float: left;
+ margin-left: 0;
+ padding-left: 0;
+}
+
+/* line 11, sourcefiles/scss/components/_utilities.scss */
+.mw-ui-flush-right {
+ float: right;
+ margin-right: 0;
+ padding-right: 0;
+}
+
+/* line 15, sourcefiles/scss/components/_utilities.scss */
+.mw-ui-center-block {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* line 4, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button {
+ display: -moz-inline-stack;
+ display: inline-block;
+ vertical-align: middle;
+ *vertical-align: auto;
+ zoom: 1;
+ *display: inline;
+ padding: 0.4em 1em 0.4em 1em;
+ margin: 0;
+ background-color: #c9c9c9;
+ *background-color: #c9c9c9;
+ *zoom: 1;
+ filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FFDCDCDC', endColorstr='#FFC9C9C9');
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #dcdcdc), color-stop(100%, #c9c9c9));
+ background-image: -webkit-linear-gradient(top, #dcdcdc, #c9c9c9);
+ background-image: -moz-linear-gradient(top, #dcdcdc, #c9c9c9);
+ background-image: -o-linear-gradient(top, #dcdcdc, #c9c9c9);
+ background-image: linear-gradient(top, #dcdcdc, #c9c9c9);
+ color: black;
+ text-shadow: 0 1px 1px rgba(201, 201, 201, 0.3);
+ border: 1px solid #c4c4c4;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ -ms-border-radius: 3px;
+ -o-border-radius: 3px;
+ border-radius: 3px;
+ vertical-align: middle;
+ text-align: center;
+ text-decoration: none;
+ font-weight: bold;
+ cursor: pointer;
+}
+/* line 38, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button:hover, .mw-ui-button.mw-ui-hover {
+ background-color: gainsboro;
+ *background-color: gainsboro;
+ *zoom: 1;
+ filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FFE9E9E9', endColorstr='#FFDCDCDC');
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #e9e9e9), color-stop(100%, #dcdcdc));
+ background-image: -webkit-linear-gradient(top, #e9e9e9, #dcdcdc);
+ background-image: -moz-linear-gradient(top, #e9e9e9, #dcdcdc);
+ background-image: -o-linear-gradient(top, #e9e9e9, #dcdcdc);
+ background-image: linear-gradient(top, #e9e9e9, #dcdcdc);
+ text-decoration: none;
+}
+/* line 44, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button:active, .mw-ui-button.mw-ui-active {
+ background-image: none;
+ background-color: #c1c1c1;
+ text-shadow: none;
+}
+/* line 54, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button:disabled, .mw-ui-button.mw-ui-disabled {
+ background-image: none;
+ background-color: #c9c9c9;
+ opacity: 0.5;
+ text-shadow: none;
+}
+/* line 30, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button:disabled, .mw-ui-button.mw-ui-disabled {
+ cursor: default;
+}
+/* line 36, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button.mw-ui-big {
+ font-size: 1.3em;
+}
+/* line 41, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button.mw-ui-block {
+ display: block;
+ width: 100%;
+}
+
+/* line 49, sourcefiles/scss/components/default/_buttons.scss */
+a.mw-ui-button {
+ text-decoration: none;
+}
+
+/* line 56, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button-group > * {
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ -ms-border-radius: 0;
+ -o-border-radius: 0;
+ border-radius: 0;
+ float: left;
+}
+/* line 60, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button-group > *:first-child {
+ -moz-border-radius-topleft: 3px;
+ -webkit-border-top-left-radius: 3px;
+ border-top-left-radius: 3px;
+ -moz-border-radius-bottomleft: 3px;
+ -webkit-border-bottom-left-radius: 3px;
+ border-bottom-left-radius: 3px;
+}
+/* line 65, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button-group > *:last-child {
+ -moz-border-radius-topright: 3px;
+ -webkit-border-top-right-radius: 3px;
+ border-top-right-radius: 3px;
+ -moz-border-radius-bottomright: 3px;
+ -webkit-border-bottom-right-radius: 3px;
+ border-bottom-right-radius: 3px;
+}
+
+/* line 14, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 290px;
+}
+/* line 20, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform > div {
+ display: block;
+ margin: 0 0 15px 0;
+ padding: 0;
+ width: 100%;
+}
+/* line 28, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform > div input,
+.mw-ui-vform > div .mw-ui-button {
+ display: block;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ margin: 0;
+ width: 100%;
+}
+/* line 37, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]) {
+ border-style: solid;
+ border-width: 1px;
+ border-color: #c9c9c9;
+ color: #252525;
+ padding: 0.35em 0 0.35em 0.5em;
+}
+/* line 11, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]):focus {
+ box-shadow: #4091ed 0px 0px 5px;
+ border-color: #4091ed;
+}
+/* line 13, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]):focus:not([type=checkbox]):not([type=radio]) {
+ outline: 0;
+}
+/* line 41, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform > div label {
+ display: block;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ font-size: 0.9em;
+ color: #4a4a4a;
+ width: auto;
+ margin: 0 0 0.2em 0;
+ padding: 0;
+}
+/* line 38, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-vform > div label * {
+ font-weight: normal;
+}
+/* line 52, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform > div input[type="checkbox"],
+.mw-ui-vform > div input[type="radio"] {
+ display: inline;
+ -webkit-box-sizing: content-box;
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ width: auto;
+}
+/* line 63, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform .error {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ font-size: 0.9em;
+ margin: 0 0 1em 0;
+ padding: 0.5em;
+ color: #cc0000;
+ border: 1px solid #fac5c5;
+ background-color: #fae3e3;
+ text-shadow: 0 1px #fae3e3;
+ word-wrap: break-word;
+}
+
+/* line 86, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform-div {
+ display: block;
+ margin: 0 0 15px 0;
+ padding: 0;
+ width: 100%;
+}
+
+/* line 96, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-input {
+ border-style: solid;
+ border-width: 1px;
+ border-color: #c9c9c9;
+ color: #252525;
+ padding: 0.35em 0 0.35em 0.5em;
+}
+/* line 11, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-input:focus {
+ box-shadow: #4091ed 0px 0px 5px;
+ border-color: #4091ed;
+}
+/* line 13, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-input:focus:not([type=checkbox]):not([type=radio]) {
+ outline: 0;
+}
+
+/* line 103, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-label {
+ font-size: 0.9em;
+ color: #4a4a4a;
+}
+/* line 38, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-label * {
+ font-weight: normal;
+}
+
+/* line 112, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-checkbox-label, .mw-ui-radio-label {
+ margin-bottom: 0.5em;
+ cursor: pointer;
+ vertical-align: bottom;
+ line-height: normal;
+ font-weight: normal;
+}
+/* line 54, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-checkbox-label > input[type="checkbox"], .mw-ui-checkbox-label > input[type="radio"], .mw-ui-radio-label > input[type="checkbox"], .mw-ui-radio-label > input[type="radio"] {
+ width: auto;
+ height: auto;
+ margin: 0 0.1em 0em 0;
+ padding: 0;
+ border-style: solid;
+ border-width: 1px;
+ border-color: #c9c9c9;
+ cursor: pointer;
+}
diff --git a/resources/mediawiki.ui/mediawiki.ui.vector.css b/resources/mediawiki.ui/mediawiki.ui.vector.css
new file mode 100644
index 00000000..fd9e0915
--- /dev/null
+++ b/resources/mediawiki.ui/mediawiki.ui.vector.css
@@ -0,0 +1,414 @@
+@charset "UTF-8";
+/**
+ * Provide Agora appearance for mw-ui-* classes when using the Vector skin.
+ * Compass builds these Agora styles from source Sass files in
+ * extensions/Agora/modules/scss
+ */
+/* _effects.scss */
+/* Mixins for visual effects in CSS3 */
+/* line 7, sourcefiles/scss/components/_utilities.scss */
+.mw-ui-flush-left {
+ float: left;
+ margin-left: 0;
+ padding-left: 0;
+}
+
+/* line 11, sourcefiles/scss/components/_utilities.scss */
+.mw-ui-flush-right {
+ float: right;
+ margin-right: 0;
+ padding-right: 0;
+}
+
+/* line 15, sourcefiles/scss/components/_utilities.scss */
+.mw-ui-center-block {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* line 4, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button {
+ display: -moz-inline-stack;
+ display: inline-block;
+ vertical-align: middle;
+ *vertical-align: auto;
+ zoom: 1;
+ *display: inline;
+ padding: 0.4em 1em 0.4em 1em;
+ margin: 0;
+ background-color: #c9c9c9;
+ *background-color: #c9c9c9;
+ *zoom: 1;
+ filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FFDCDCDC', endColorstr='#FFC9C9C9');
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #dcdcdc), color-stop(100%, #c9c9c9));
+ background-image: -webkit-linear-gradient(top, #dcdcdc, #c9c9c9);
+ background-image: -moz-linear-gradient(top, #dcdcdc, #c9c9c9);
+ background-image: -o-linear-gradient(top, #dcdcdc, #c9c9c9);
+ background-image: linear-gradient(top, #dcdcdc, #c9c9c9);
+ color: black;
+ text-shadow: 0 1px 1px rgba(201, 201, 201, 0.3);
+ border: 1px solid #c4c4c4;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ -ms-border-radius: 3px;
+ -o-border-radius: 3px;
+ border-radius: 3px;
+ vertical-align: middle;
+ text-align: center;
+ text-decoration: none;
+ font-weight: bold;
+ cursor: pointer;
+}
+/* line 38, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button:hover, .mw-ui-button.mw-ui-hover {
+ background-color: gainsboro;
+ *background-color: gainsboro;
+ *zoom: 1;
+ filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FFE9E9E9', endColorstr='#FFDCDCDC');
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #e9e9e9), color-stop(100%, #dcdcdc));
+ background-image: -webkit-linear-gradient(top, #e9e9e9, #dcdcdc);
+ background-image: -moz-linear-gradient(top, #e9e9e9, #dcdcdc);
+ background-image: -o-linear-gradient(top, #e9e9e9, #dcdcdc);
+ background-image: linear-gradient(top, #e9e9e9, #dcdcdc);
+ text-decoration: none;
+}
+/* line 44, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button:active, .mw-ui-button.mw-ui-active {
+ background-image: none;
+ background-color: #c1c1c1;
+ text-shadow: none;
+}
+/* line 54, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button:disabled, .mw-ui-button.mw-ui-disabled {
+ background-image: none;
+ background-color: #c9c9c9;
+ opacity: 0.5;
+ text-shadow: none;
+}
+/* line 30, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button:disabled, .mw-ui-button.mw-ui-disabled {
+ cursor: default;
+}
+/* line 36, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button.mw-ui-big {
+ font-size: 1.3em;
+}
+/* line 41, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button.mw-ui-block {
+ display: block;
+ width: 100%;
+}
+
+/* line 49, sourcefiles/scss/components/default/_buttons.scss */
+a.mw-ui-button {
+ text-decoration: none;
+}
+
+/* line 56, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button-group > * {
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ -ms-border-radius: 0;
+ -o-border-radius: 0;
+ border-radius: 0;
+ float: left;
+}
+/* line 60, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button-group > *:first-child {
+ -moz-border-radius-topleft: 3px;
+ -webkit-border-top-left-radius: 3px;
+ border-top-left-radius: 3px;
+ -moz-border-radius-bottomleft: 3px;
+ -webkit-border-bottom-left-radius: 3px;
+ border-bottom-left-radius: 3px;
+}
+/* line 65, sourcefiles/scss/components/default/_buttons.scss */
+.mw-ui-button-group > *:last-child {
+ -moz-border-radius-topright: 3px;
+ -webkit-border-top-right-radius: 3px;
+ border-top-right-radius: 3px;
+ -moz-border-radius-bottomright: 3px;
+ -webkit-border-bottom-right-radius: 3px;
+ border-bottom-right-radius: 3px;
+}
+
+/* line 3, sourcefiles/scss/components/vector/_buttons.scss */
+.mw-ui-button {
+ font-size: 1em;
+ line-height: 1.4em;
+}
+/* line 6, sourcefiles/scss/components/vector/_buttons.scss */
+.mw-ui-button.mw-ui-primary {
+ background-color: #3366bb;
+ *background-color: #3366bb;
+ *zoom: 1;
+ filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FF4779CD', endColorstr='#FF3366BB');
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #4779cd), color-stop(100%, #3366bb));
+ background-image: -webkit-linear-gradient(top, #4779cd, #3366bb);
+ background-image: -moz-linear-gradient(top, #4779cd, #3366bb);
+ background-image: -o-linear-gradient(top, #4779cd, #3366bb);
+ background-image: linear-gradient(top, #4779cd, #3366bb);
+ color: white;
+ text-shadow: 0 1px 1px rgba(51, 102, 187, 0.75);
+ border: 1px solid #3162b3;
+}
+/* line 38, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button.mw-ui-primary:hover, .mw-ui-button.mw-ui-primary.mw-ui-hover {
+ background-color: #4779cd;
+ *background-color: #4779cd;
+ *zoom: 1;
+ filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FF5B88D2', endColorstr='#FF4779CD');
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #5b88d2), color-stop(100%, #4779cd));
+ background-image: -webkit-linear-gradient(top, #5b88d2, #4779cd);
+ background-image: -moz-linear-gradient(top, #5b88d2, #4779cd);
+ background-image: -o-linear-gradient(top, #5b88d2, #4779cd);
+ background-image: linear-gradient(top, #5b88d2, #4779cd);
+ text-decoration: none;
+}
+/* line 44, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button.mw-ui-primary:active, .mw-ui-button.mw-ui-primary.mw-ui-active {
+ background-image: none;
+ background-color: #305faf;
+ text-shadow: none;
+}
+/* line 54, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button.mw-ui-primary:disabled, .mw-ui-button.mw-ui-primary.mw-ui-disabled {
+ background-image: none;
+ background-color: #3366bb;
+ opacity: 0.5;
+ text-shadow: none;
+}
+/* line 10, sourcefiles/scss/components/vector/_buttons.scss */
+.mw-ui-button.mw-ui-constructive {
+ background-color: #27aa65;
+ *background-color: #27aa65;
+ *zoom: 1;
+ filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FF2EC977', endColorstr='#FF27AA65');
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #2ec977), color-stop(100%, #27aa65));
+ background-image: -webkit-linear-gradient(top, #2ec977, #27aa65);
+ background-image: -moz-linear-gradient(top, #2ec977, #27aa65);
+ background-image: -o-linear-gradient(top, #2ec977, #27aa65);
+ background-image: linear-gradient(top, #2ec977, #27aa65);
+ color: white;
+ text-shadow: 0 1px 1px rgba(39, 170, 101, 0.75);
+ border: 1px solid #25a260;
+}
+/* line 38, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button.mw-ui-constructive:hover, .mw-ui-button.mw-ui-constructive.mw-ui-hover {
+ background-color: #2ec977;
+ *background-color: #2ec977;
+ *zoom: 1;
+ filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FF3ED384', endColorstr='#FF2EC977');
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #3ed384), color-stop(100%, #2ec977));
+ background-image: -webkit-linear-gradient(top, #3ed384, #2ec977);
+ background-image: -moz-linear-gradient(top, #3ed384, #2ec977);
+ background-image: -o-linear-gradient(top, #3ed384, #2ec977);
+ background-image: linear-gradient(top, #3ed384, #2ec977);
+ text-decoration: none;
+}
+/* line 44, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button.mw-ui-constructive:active, .mw-ui-button.mw-ui-constructive.mw-ui-active {
+ background-image: none;
+ background-color: #249e5e;
+ text-shadow: none;
+}
+/* line 54, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button.mw-ui-constructive:disabled, .mw-ui-button.mw-ui-constructive.mw-ui-disabled {
+ background-image: none;
+ background-color: #27aa65;
+ opacity: 0.5;
+ text-shadow: none;
+}
+/* line 14, sourcefiles/scss/components/vector/_buttons.scss */
+.mw-ui-button.mw-ui-destructive {
+ background-color: #cc0000;
+ *background-color: #cc0000;
+ *zoom: 1;
+ filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FFF20000', endColorstr='#FFCC0000');
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f20000), color-stop(100%, #cc0000));
+ background-image: -webkit-linear-gradient(top, #f20000, #cc0000);
+ background-image: -moz-linear-gradient(top, #f20000, #cc0000);
+ background-image: -o-linear-gradient(top, #f20000, #cc0000);
+ background-image: linear-gradient(top, #f20000, #cc0000);
+ color: white;
+ text-shadow: 0 1px 1px rgba(204, 0, 0, 0.75);
+ border: 1px solid #c20000;
+}
+/* line 38, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button.mw-ui-destructive:hover, .mw-ui-button.mw-ui-destructive.mw-ui-hover {
+ background-color: #f20000;
+ *background-color: #f20000;
+ *zoom: 1;
+ filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FFFF0D0D', endColorstr='#FFF20000');
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ff0d0d), color-stop(100%, #f20000));
+ background-image: -webkit-linear-gradient(top, #ff0d0d, #f20000);
+ background-image: -moz-linear-gradient(top, #ff0d0d, #f20000);
+ background-image: -o-linear-gradient(top, #ff0d0d, #f20000);
+ background-image: linear-gradient(top, #ff0d0d, #f20000);
+ text-decoration: none;
+}
+/* line 44, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button.mw-ui-destructive:active, .mw-ui-button.mw-ui-destructive.mw-ui-active {
+ background-image: none;
+ background-color: #bd0000;
+ text-shadow: none;
+}
+/* line 54, sourcefiles/scss/mixins/_effects.scss */
+.mw-ui-button.mw-ui-destructive:disabled, .mw-ui-button.mw-ui-destructive.mw-ui-disabled {
+ background-image: none;
+ background-color: #cc0000;
+ opacity: 0.5;
+ text-shadow: none;
+}
+
+/* line 14, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 290px;
+}
+/* line 20, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform > div {
+ display: block;
+ margin: 0 0 15px 0;
+ padding: 0;
+ width: 100%;
+}
+/* line 28, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform > div input,
+.mw-ui-vform > div .mw-ui-button {
+ display: block;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ margin: 0;
+ width: 100%;
+}
+/* line 37, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]) {
+ border-style: solid;
+ border-width: 1px;
+ border-color: #c9c9c9;
+ color: #252525;
+ padding: 0.35em 0 0.35em 0.5em;
+}
+/* line 11, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]):focus {
+ box-shadow: #4091ed 0px 0px 5px;
+ border-color: #4091ed;
+}
+/* line 13, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-vform > div input:not([type=button]):not([type=submit]):not([type=file]):focus:not([type=checkbox]):not([type=radio]) {
+ outline: 0;
+}
+/* line 41, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform > div label {
+ display: block;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ font-size: 0.9em;
+ color: #4a4a4a;
+ width: auto;
+ margin: 0 0 0.2em 0;
+ padding: 0;
+}
+/* line 38, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-vform > div label * {
+ font-weight: normal;
+}
+/* line 52, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform > div input[type="checkbox"],
+.mw-ui-vform > div input[type="radio"] {
+ display: inline;
+ -webkit-box-sizing: content-box;
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ width: auto;
+}
+/* line 63, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform .error {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ font-size: 0.9em;
+ margin: 0 0 1em 0;
+ padding: 0.5em;
+ color: #cc0000;
+ border: 1px solid #fac5c5;
+ background-color: #fae3e3;
+ text-shadow: 0 1px #fae3e3;
+ word-wrap: break-word;
+}
+
+/* line 86, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-vform-div {
+ display: block;
+ margin: 0 0 15px 0;
+ padding: 0;
+ width: 100%;
+}
+
+/* line 96, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-input {
+ border-style: solid;
+ border-width: 1px;
+ border-color: #c9c9c9;
+ color: #252525;
+ padding: 0.35em 0 0.35em 0.5em;
+}
+/* line 11, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-input:focus {
+ box-shadow: #4091ed 0px 0px 5px;
+ border-color: #4091ed;
+}
+/* line 13, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-input:focus:not([type=checkbox]):not([type=radio]) {
+ outline: 0;
+}
+
+/* line 103, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-label {
+ font-size: 0.9em;
+ color: #4a4a4a;
+}
+/* line 38, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-label * {
+ font-weight: normal;
+}
+
+/* line 112, sourcefiles/scss/components/default/_forms.scss */
+.mw-ui-checkbox-label, .mw-ui-radio-label {
+ margin-bottom: 0.5em;
+ cursor: pointer;
+ vertical-align: bottom;
+ line-height: normal;
+ font-weight: normal;
+}
+/* line 54, sourcefiles/scss/mixins/_forms.scss */
+.mw-ui-checkbox-label > input[type="checkbox"], .mw-ui-checkbox-label > input[type="radio"], .mw-ui-radio-label > input[type="checkbox"], .mw-ui-radio-label > input[type="radio"] {
+ width: auto;
+ height: auto;
+ margin: 0 0.1em 0em 0;
+ padding: 0;
+ border-style: solid;
+ border-width: 1px;
+ border-color: #c9c9c9;
+ cursor: pointer;
+}
+
+/* line 5, sourcefiles/scss/components/vector/_forms.scss */
+.mw-ui-vform,
+.mw-ui-vform > div input,
+.mw-ui-input {
+ font-size: 1em;
+ line-height: 1.4em;
+}
+
+/* line 3, sourcefiles/scss/components/vector/_containers.scss */
+.mw-ui-container {
+ font-size: 1em;
+ line-height: 1.4em;
+}
diff --git a/resources/mediawiki.ui/sourcefiles/Makefile b/resources/mediawiki.ui/sourcefiles/Makefile
new file mode 100644
index 00000000..dea90139
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/Makefile
@@ -0,0 +1,24 @@
+DATE=$(shell date +%I:%M%p)
+CHECK=\033[32m✔\033[39m
+HR=\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#
+
+build:
+ @echo "\n${HR}"
+ @echo "Building Agora..."
+ @echo "${HR}\n"
+ @compass compile
+ @echo "Compiling Compass project... ${CHECK} Done"
+ @rm -rf .sass-cache
+ @echo "Removing .sass-cache... ${CHECK} Done"
+ @echo "\n${HR}"
+ @echo "Agora successfully built at ${DATE}."
+ @echo "${HR}\n"
+
+all: build
+
+watch:
+ @echo "\n${HR}"
+ @echo "Watching SCSS files for Agora..."
+ @echo "${HR}\n"
+ @compass watch
+ @echo "Started watching modules/scss at ${DATE}..."
diff --git a/resources/mediawiki.ui/sourcefiles/config.rb b/resources/mediawiki.ui/sourcefiles/config.rb
new file mode 100644
index 00000000..28c65240
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/config.rb
@@ -0,0 +1,27 @@
+# Require any additional compass plugins here.
+
+# Set this to the root of your project when deployed:
+# (unused so far): http_path = "/"
+
+# Output to parent of build directory
+css_dir = ".."
+sass_dir = "scss"
+# (unused so far): images_dir = "modules/img"
+# (unused so far): javascripts_dir = "modules/js"
+
+# You can select your preferred output style here (can be overridden via the command line):
+# output_style = :expanded or :nested or :compact or :compressed
+output_style = :expanded
+
+# To enable relative paths to assets via compass helper functions. Uncomment:
+relative_assets = true
+
+# To disable debugging comments that display the original location of your selectors. Uncomment:
+line_comments = true
+
+
+# If you prefer the indented syntax, you might want to regenerate this
+# project again passing --syntax sass, or you can uncomment this:
+# preferred_syntax = :sass
+# and then run:
+# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass
diff --git a/resources/mediawiki.ui/sourcefiles/scss/components/_default.scss b/resources/mediawiki.ui/sourcefiles/scss/components/_default.scss
new file mode 100644
index 00000000..e7090ebc
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/components/_default.scss
@@ -0,0 +1,3 @@
+@import "utilities";
+@import "default/buttons";
+@import "default/forms"; \ No newline at end of file
diff --git a/resources/mediawiki.ui/sourcefiles/scss/components/_utilities.scss b/resources/mediawiki.ui/sourcefiles/scss/components/_utilities.scss
new file mode 100644
index 00000000..4f1dba2f
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/components/_utilities.scss
@@ -0,0 +1,17 @@
+// Generic helper classes that could be used in many elements/layouts
+
+// --------------------------------------------------------------------------
+// Positioning
+// --------------------------------------------------------------------------
+
+.mw-ui-flush-left {
+ @include agora-flush-left;
+}
+
+.mw-ui-flush-right {
+ @include agora-flush-right;
+}
+
+.mw-ui-center-block {
+ @include agora-center-block;
+} \ No newline at end of file
diff --git a/resources/mediawiki.ui/sourcefiles/scss/components/_vector.scss b/resources/mediawiki.ui/sourcefiles/scss/components/_vector.scss
new file mode 100644
index 00000000..d7cb34ae
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/components/_vector.scss
@@ -0,0 +1,4 @@
+@import "utilities";
+@import "vector/buttons";
+@import "vector/forms";
+@import "vector/containers";
diff --git a/resources/mediawiki.ui/sourcefiles/scss/components/default/_buttons.scss b/resources/mediawiki.ui/sourcefiles/scss/components/default/_buttons.scss
new file mode 100644
index 00000000..d67810f7
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/components/default/_buttons.scss
@@ -0,0 +1,69 @@
+$buttonBorderRadius: 3px;
+
+// Button styling
+.mw-ui-button {
+ // Container layout
+ @include inline-block;
+ padding: 0.4em 1em 0.4em 1em;
+ margin: 0;
+
+ // Container styling
+ @include buttonColors($agoraGray);
+ @include border-radius($buttonBorderRadius);
+
+ // Content styling
+ vertical-align: middle;
+
+ text: {
+ align: center;
+ decoration: none;
+ }
+
+ font: {
+ weight: bold;
+ }
+
+ // Interaction styling
+ cursor: pointer;
+
+ &:disabled,
+ &.mw-ui-disabled {
+ cursor: default;
+ }
+
+ // Button sizes and displays
+ // -----------------------------------------
+ &.mw-ui-big {
+ font: {
+ size: $baseFontSize * 1.3;
+ }
+ }
+ &.mw-ui-block {
+ display: block;
+ width: 100%;
+ }
+}
+
+// This overrides an underline declaration on a:hover and a:focus in commonElements.css, which the
+// class alone isn't specific enough to do
+a.mw-ui-button {
+ text: {
+ decoration: none;
+ }
+}
+
+// Button groups
+.mw-ui-button-group > * {
+ @include border-radius(0);
+ float: left;
+
+ &:first-child{
+ @include border-top-left-radius($buttonBorderRadius);
+ @include border-bottom-left-radius($buttonBorderRadius);
+ }
+
+ &:last-child{
+ @include border-top-right-radius($buttonBorderRadius);
+ @include border-bottom-right-radius($buttonBorderRadius);
+ }
+}
diff --git a/resources/mediawiki.ui/sourcefiles/scss/components/default/_forms.scss b/resources/mediawiki.ui/sourcefiles/scss/components/default/_forms.scss
new file mode 100644
index 00000000..a9cec39a
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/components/default/_forms.scss
@@ -0,0 +1,114 @@
+// Form elements and layouts
+
+// --------------------------------------------------------------------------
+// Layouts
+// --------------------------------------------------------------------------
+
+// The FancyCaptcha image CAPTCHA used on WMF wikis drives the width of the
+// 'VForm' design, the form can't be narrower than this.
+$captchaContainerWidth: 290px;
+$defaultFormWidth: $captchaContainerWidth;
+
+// Style a compact vertical stacked form ("VForm") and the elements in divs
+// within it.
+.mw-ui-vform {
+ @include box-sizing(border-box);
+
+ width: $defaultFormWidth;
+
+ // Immediate divs in a vform are block and spaced-out.
+ & > div {
+ display: block;
+ margin: 0 0 15px 0;
+ padding: 0;
+ width: 100%;
+
+ // MW currently doesn't use the type attribute everywhere on inputs.
+ input,
+ .mw-ui-button {
+ display: block;
+ @include box-sizing(border-box);
+ margin: 0;
+ width: 100%;
+ }
+
+ // We exclude these because they'll generally use mw-ui-button.
+ // Otherwise, we'll unintentionally override that.
+ input:not([type=button]):not([type=submit]):not([type=file]), {
+ @include agora-field-styling; // mixins/_forms.scss
+ }
+
+ label {
+ display: block;
+ @include box-sizing(border-box);
+ @include agora-label-styling;
+ width: auto;
+ margin: 0 0 0.2em 0;
+ padding: 0;
+ }
+
+ // Override input styling just for checkboxes and radio inputs.
+ input[type="checkbox"],
+ input[type="radio"] {
+ display: inline;
+ @include box-sizing(content-box);
+ width: auto;
+ }
+
+ }
+
+ // HTMLForm uses error, SpecialUserlogin (login and create account) uses
+ // errorbox.
+ // TODO move errorbox from mediawiki.special.vforms.css into here.
+ .error {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ font-size: 0.9em;
+ margin: 0 0 1em 0;
+ padding: 0.5em;
+ color: #cc0000;
+ border: 1px solid #fac5c5;
+ background-color: #fae3e3;
+ text-shadow: 0 1px #fae3e3;
+ word-wrap: break-word;
+ }
+}
+
+// --------------------------------------------------------------------------
+// Elements
+// --------------------------------------------------------------------------
+
+// Apply this to individual elements to style them.
+// You generally don't need to use this class on divs within an Agora
+// form container such as mw-ui-vform
+// XXX DRY: This repeats earlier styling, use an @include agora-div-styling ?
+.mw-ui-vform-div {
+ display: block;
+ margin: 0 0 15px 0;
+ padding: 0;
+ width: 100%;
+}
+
+// Apply mw-ui-input to individual input fields to style them.
+// You generally don't need to use this class if <input> is within an Agora
+// form container such as mw-ui-vform
+.mw-ui-input {
+ @include agora-field-styling; // mixins/_forms.scss
+}
+
+// Apply mw-ui-label to individual elements to style them.
+// You generally don't need to use this class if <label> is within an Agora
+// form container such as mw-ui-vform
+.mw-ui-label {
+ @include agora-label-styling; // mixins/_forms.scss
+}
+
+// Nesting an input checkbox or radio button inside a label with this class
+// improves alignment, e.g.
+// <label class="mw-ui-checkbox-label">
+// <input type="checkbox">The label text
+// </label>
+.mw-ui-checkbox-label, .mw-ui-radio-label {
+ @include agora-inline-label-styling;
+}
diff --git a/resources/mediawiki.ui/sourcefiles/scss/components/vector/_buttons.scss b/resources/mediawiki.ui/sourcefiles/scss/components/vector/_buttons.scss
new file mode 100644
index 00000000..8d5f0b6a
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/components/vector/_buttons.scss
@@ -0,0 +1,19 @@
+@import "../default/buttons"; // Layer Vector on top of the default settings.
+
+.mw-ui-button {
+ // Button colors determined by function.
+ // -----------------------------------------
+ &.mw-ui-primary {
+ @include buttonColors($agoraBlue);
+ }
+
+ &.mw-ui-constructive {
+ @include buttonColors($agoraGreen);
+ }
+
+ &.mw-ui-destructive {
+ @include buttonColors($agoraRed);
+ }
+
+ @include vector-type;
+}
diff --git a/resources/mediawiki.ui/sourcefiles/scss/components/vector/_containers.scss b/resources/mediawiki.ui/sourcefiles/scss/components/vector/_containers.scss
new file mode 100644
index 00000000..ed01603d
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/components/vector/_containers.scss
@@ -0,0 +1,5 @@
+// No default settings for containers yet.
+
+.mw-ui-container {
+ @include vector-type;
+}
diff --git a/resources/mediawiki.ui/sourcefiles/scss/components/vector/_forms.scss b/resources/mediawiki.ui/sourcefiles/scss/components/vector/_forms.scss
new file mode 100644
index 00000000..73ea24e2
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/components/vector/_forms.scss
@@ -0,0 +1,7 @@
+@import "../default/forms"; // Layer Vector on top of the default settings.
+
+.mw-ui-vform,
+.mw-ui-vform > div input,
+.mw-ui-input {
+ @include vector-type;
+}
diff --git a/resources/mediawiki.ui/sourcefiles/scss/mediawiki.ui.default.scss b/resources/mediawiki.ui/sourcefiles/scss/mediawiki.ui.default.scss
new file mode 100644
index 00000000..e6db5237
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/mediawiki.ui.default.scss
@@ -0,0 +1,16 @@
+/**
+ * Provide Agora appearance for mw-ui-* classes when using a skin other than
+ * Vector.
+ * Compass builds these Agora styles from source Sass files in
+ * extensions/Agora/modules/scss
+ */
+
+@charset "UTF-8";
+
+@import "compass";
+
+@import "settings/all";
+
+@import "mixins/all";
+
+@import "components/default";
diff --git a/resources/mediawiki.ui/sourcefiles/scss/mediawiki.ui.vector.scss b/resources/mediawiki.ui/sourcefiles/scss/mediawiki.ui.vector.scss
new file mode 100644
index 00000000..ac113eec
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/mediawiki.ui.vector.scss
@@ -0,0 +1,15 @@
+/**
+ * Provide Agora appearance for mw-ui-* classes when using the Vector skin.
+ * Compass builds these Agora styles from source Sass files in
+ * extensions/Agora/modules/scss
+ */
+
+@charset "UTF-8";
+
+@import "compass";
+
+@import "settings/all";
+
+@import "mixins/all";
+
+@import "components/vector";
diff --git a/resources/mediawiki.ui/sourcefiles/scss/mixins/_all.scss b/resources/mediawiki.ui/sourcefiles/scss/mixins/_all.scss
new file mode 100644
index 00000000..adc48cd8
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/mixins/_all.scss
@@ -0,0 +1,4 @@
+@import "utilities";
+@import "type";
+@import "effects";
+@import "forms"; \ No newline at end of file
diff --git a/resources/mediawiki.ui/sourcefiles/scss/mixins/_effects.scss b/resources/mediawiki.ui/sourcefiles/scss/mixins/_effects.scss
new file mode 100644
index 00000000..2efff820
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/mixins/_effects.scss
@@ -0,0 +1,62 @@
+/* _effects.scss */
+
+/* Mixins for visual effects in CSS3 */
+
+// ----------------------------------------------------------------------------
+// Gradients
+// ----------------------------------------------------------------------------
+@mixin vertical-gradient ($startColor: lighten($agoraGray, 95%), $endColor: $agoraGray) {
+ // Fallback
+ background-color: $endColor;
+ *background-color: $endColor; // IE7
+
+ // IE6-8
+ @include filter-gradient($startColor, $endColor, vertical);
+
+ // IE9+, Opera, Gecko, WebKit
+ @include background-image(linear-gradient(top, $startColor, $endColor));
+}
+
+// ----------------------------------------------------------------------------
+// Button styling
+// ----------------------------------------------------------------------------
+@mixin buttonColors ($baseColor: $agoraGray) {
+ // Background color
+ @include vertical-gradient(lighten($baseColor, 7.5%), $baseColor);
+
+ @if $baseColor == $agoraGray {
+ color: black;
+ @include text-shadow(0 1px 1px rgba($baseColor, 0.3));
+ } @else {
+ color: white;
+ @include text-shadow(0 1px 1px rgba($baseColor, 0.75));
+ }
+
+ border: 1px solid darken($baseColor, 2%);
+
+ &:hover,
+ &.mw-ui-hover {
+ @include vertical-gradient(lighten($baseColor, 12.5%), lighten($baseColor, 7.5%));
+ text-decoration: none;
+ }
+
+ &:active,
+ &.mw-ui-active {
+ background: {
+ image: none;
+ color: darken($baseColor, 3%);
+ }
+
+ text-shadow: none;
+ }
+
+ &:disabled,
+ &.mw-ui-disabled {
+ background: {
+ image: none;
+ color: $baseColor;
+ }
+ opacity: 0.5;
+ text-shadow: none;
+ }
+}
diff --git a/resources/mediawiki.ui/sourcefiles/scss/mixins/_forms.scss b/resources/mediawiki.ui/sourcefiles/scss/mixins/_forms.scss
new file mode 100644
index 00000000..0f3f6ad3
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/mixins/_forms.scss
@@ -0,0 +1,66 @@
+// Font is not included.
+// For Vector, that should be layered on top with vector-type
+@mixin agora-field-styling() {
+
+ border: {
+ style: solid;
+ width: 1px;
+ color: $agoraGray;
+ };
+
+ &:focus {
+ // Styling focus of native checkboxes etc on Mac is almost impossible.
+ &:not([type=checkbox]):not([type=radio]) {
+ @include reset-focus; // Removes OS field focus
+ };
+
+ // @include box-shadow generates unneeded prefixes
+ // https://github.com/chriseppstein/compass/issues/1054 , so specify
+ // directly.
+ box-shadow: $agoraBlueShadow 0px 0px 5px;
+
+ border: {
+ color: $agoraBlueShadow;
+ };
+ }
+
+ color: $agoraTextColor;
+ padding: 0.35em 0 0.35em 0.5em;
+}
+
+@mixin agora-label-styling() {
+ font: {
+ //weight: bold;
+ size: 0.9em;
+ };
+ color: darken($agoraGray, 50%);
+
+ & * {
+ font-weight: normal;
+ }
+}
+
+@mixin 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 0em 0;
+ padding: 0;
+ border: {
+ style: solid;
+ width: 1px;
+ color: $agoraGray;
+ }
+ cursor: pointer;
+ }
+}
diff --git a/resources/mediawiki.ui/sourcefiles/scss/mixins/_type.scss b/resources/mediawiki.ui/sourcefiles/scss/mixins/_type.scss
new file mode 100644
index 00000000..8a93a08b
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/mixins/_type.scss
@@ -0,0 +1,6 @@
+@mixin vector-type {
+ font: {
+ size: $baseFontSize;
+ }
+ line-height: $baseLineHeight;
+} \ No newline at end of file
diff --git a/resources/mediawiki.ui/sourcefiles/scss/mixins/_utilities.scss b/resources/mediawiki.ui/sourcefiles/scss/mixins/_utilities.scss
new file mode 100644
index 00000000..71a93b60
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/mixins/_utilities.scss
@@ -0,0 +1,19 @@
+@mixin agora-flush-left() {
+ float: left;
+ margin-left: 0;
+ padding-left: 0;
+}
+
+@mixin agora-flush-right() {
+ float: right;
+ margin-right: 0;
+ padding-right: 0;
+}
+
+@mixin agora-center-block() {
+ display: block;
+ margin: {
+ left: auto;
+ right: auto;
+ };
+} \ No newline at end of file
diff --git a/resources/mediawiki.ui/sourcefiles/scss/settings/_all.scss b/resources/mediawiki.ui/sourcefiles/scss/settings/_all.scss
new file mode 100644
index 00000000..21ac292f
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/settings/_all.scss
@@ -0,0 +1,2 @@
+@import "colors";
+@import "typography"; \ No newline at end of file
diff --git a/resources/mediawiki.ui/sourcefiles/scss/settings/_colors.scss b/resources/mediawiki.ui/sourcefiles/scss/settings/_colors.scss
new file mode 100644
index 00000000..0c18bdb4
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/settings/_colors.scss
@@ -0,0 +1,17 @@
+// Grays
+// -----------------------------------------
+$agoraGray: #c9c9c9;
+$agoraTextColor: #252525;
+
+// Blues
+// -----------------------------------------
+$agoraBlue: #3366bb;
+$agoraBlueShadow: #4091ed;
+
+// Greens
+// -----------------------------------------
+$agoraGreen: #27aa65;
+
+// Reds
+// -----------------------------------------
+$agoraRed: #cc0000;
diff --git a/resources/mediawiki.ui/sourcefiles/scss/settings/_typography.scss b/resources/mediawiki.ui/sourcefiles/scss/settings/_typography.scss
new file mode 100644
index 00000000..013d12b3
--- /dev/null
+++ b/resources/mediawiki.ui/sourcefiles/scss/settings/_typography.scss
@@ -0,0 +1,5 @@
+$baseFontSize: 1em;
+$baseLineHeight: 1.4 * $baseFontSize;
+$baseFontColor: $agoraTextColor;
+
+$smallFontSize: 0.75em; \ No newline at end of file
diff --git a/resources/mediawiki/images/arrow-collapsed-ltr.png b/resources/mediawiki/images/arrow-collapsed-ltr.png
new file mode 100644
index 00000000..b17e578b
--- /dev/null
+++ b/resources/mediawiki/images/arrow-collapsed-ltr.png
Binary files differ
diff --git a/resources/mediawiki/images/arrow-collapsed-rtl.png b/resources/mediawiki/images/arrow-collapsed-rtl.png
new file mode 100644
index 00000000..a834548e
--- /dev/null
+++ b/resources/mediawiki/images/arrow-collapsed-rtl.png
Binary files differ
diff --git a/resources/mediawiki/images/arrow-expanded.png b/resources/mediawiki/images/arrow-expanded.png
new file mode 100644
index 00000000..2bec798e
--- /dev/null
+++ b/resources/mediawiki/images/arrow-expanded.png
Binary files differ
diff --git a/resources/mediawiki/mediawiki.Title.js b/resources/mediawiki/mediawiki.Title.js
index 33cca585..5038c515 100644
--- a/resources/mediawiki/mediawiki.Title.js
+++ b/resources/mediawiki/mediawiki.Title.js
@@ -1,189 +1,368 @@
-/**
- * mediaWiki.Title
- *
+/*!
* @author Neil Kandalgaonkar, 2010
- * @author Timo Tijhof, 2011
+ * @author Timo Tijhof, 2011-2013
* @since 1.18
- *
- * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds, wgCaseSensitiveNamespaces), mw.util.wikiGetlink
*/
( function ( mw, $ ) {
- /* Local space */
-
/**
- * Title
- * @constructor
+ * @class mw.Title
+ *
+ * Parse titles into an object struture. Note that when using the constructor
+ * directly, passing invalid titles will result in an exception. Use #newFromText to use the
+ * logic directly and get null for invalid titles which is easier to work with.
*
- * @param title {String} Title of the page. If no second argument given,
- * this will be searched for a namespace.
- * @param namespace {Number} (optional) Namespace id. If given, title will be taken as-is.
- * @return {Title} this
+ * @constructor
+ * @param {string} title Title of the page. If no second argument given,
+ * this will be searched for a namespace
+ * @param {number} [namespace=NS_MAIN] If given, will used as default namespace for the given title
+ * @throws {Error} When the title is invalid
*/
function Title( title, namespace ) {
- this.ns = 0; // integer namespace id
- this.name = null; // name in canonical 'database' form
- this.ext = null; // extension
-
- if ( arguments.length === 2 ) {
- setNameAndExtension( this, title );
- this.ns = fixNsId( namespace );
- } else if ( arguments.length === 1 ) {
- setAll( this, title );
+ var parsed = parse( title, namespace );
+ if ( !parsed ) {
+ throw new Error( 'Unable to parse title' );
}
+
+ this.namespace = parsed.namespace;
+ this.title = parsed.title;
+ this.ext = parsed.ext;
+ this.fragment = parsed.fragment;
+
return this;
}
-var
- /**
- * Public methods (defined later)
- */
- fn,
+ /* Private members */
- /**
- * Strip some illegal chars: control chars, colon, less than, greater than,
- * brackets, braces, pipe, whitespace and normal spaces. This still leaves some insanity
- * intact, like unicode bidi chars, but it's a good start..
- * @param s {String}
- * @return {String}
- */
- clean = function ( s ) {
- if ( s !== undefined ) {
- return s.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '_' );
- }
- },
+ var
/**
- * Convert db-key to readable text.
- * @param s {String}
- * @return {String}
+ * @private
+ * @static
+ * @property NS_MAIN
*/
- text = function ( s ) {
- if ( s !== null && s !== undefined ) {
- return s.replace( /_/g, ' ' );
- } else {
- return '';
- }
- },
+ NS_MAIN = 0,
/**
- * Sanitize name.
+ * @private
+ * @static
+ * @property NS_TALK
*/
- fixName = function ( s ) {
- return clean( $.trim( s ) );
- },
+ NS_TALK = 1,
/**
- * Sanitize name.
+ * @private
+ * @static
+ * @property NS_SPECIAL
*/
- fixExt = function ( s ) {
- return clean( s );
- },
+ NS_SPECIAL = -1,
/**
- * Sanitize namespace id.
- * @param id {Number} Namespace id.
- * @return {Number|Boolean} The id as-is or boolean false if invalid.
+ * Get the namespace id from a namespace name (either from the localized, canonical or alias
+ * name).
+ *
+ * Example: On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or
+ * even 'Bild'.
+ *
+ * @private
+ * @static
+ * @method getNsIdByName
+ * @param {string} ns Namespace name (case insensitive, leading/trailing space ignored)
+ * @return {number|boolean} Namespace id or boolean false
*/
- fixNsId = function ( id ) {
- // wgFormattedNamespaces is an object of *string* key-vals (ie. arr["0"] not arr[0] )
- var ns = mw.config.get( 'wgFormattedNamespaces' )[id.toString()];
+ getNsIdByName = function ( ns ) {
+ var id;
- // Check only undefined (may be false-y, such as '' (main namespace) ).
- if ( ns === undefined ) {
+ // Don't cast non-strings to strings, because null or undefined should not result in
+ // returning the id of a potential namespace called "Null:" (e.g. on null.example.org/wiki)
+ // Also, toLowerCase throws exception on null/undefined, because it is a String method.
+ if ( typeof ns !== 'string' ) {
+ return false;
+ }
+ ns = ns.toLowerCase();
+ id = mw.config.get( 'wgNamespaceIds' )[ns];
+ if ( id === undefined ) {
return false;
- } else {
- return Number( id );
}
+ return id;
},
+ rUnderscoreTrim = /^_+|_+$/g,
+
+ rSplit = /^(.+?)_*:_*(.*)$/,
+
+ // See Title.php#getTitleInvalidRegex
+ rInvalid = new RegExp(
+ '[^' + mw.config.get( 'wgLegalTitleChars' ) + ']' +
+ // URL percent encoding sequences interfere with the ability
+ // to round-trip titles -- you can't link to them consistently.
+ '|%[0-9A-Fa-f]{2}' +
+ // XML/HTML character references produce similar issues.
+ '|&[A-Za-z0-9\u0080-\uFFFF]+;' +
+ '|&#[0-9]+;' +
+ '|&#x[0-9A-Fa-f]+;'
+ ),
+
/**
- * Get namespace id from namespace name by any known namespace/id pair (localized, canonical or alias).
+ * Internal helper for #constructor and #newFromtext.
+ *
+ * Based on Title.php#secureAndSplit
*
- * @example On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or even 'Bild'.
- * @param ns {String} Namespace name (case insensitive, leading/trailing space ignored).
- * @return {Number|Boolean} Namespace id or boolean false if unrecognized.
+ * @private
+ * @static
+ * @method parse
+ * @param {string} title
+ * @param {number} [defaultNamespace=NS_MAIN]
+ * @return {Object|boolean}
*/
- getNsIdByName = function ( ns ) {
- // Don't cast non-strings to strings, because null or undefined
- // should not result in returning the id of a potential namespace
- // called "Null:" (e.g. on nullwiki.example.org)
- // Also, toLowerCase throws exception on null/undefined, because
- // it is a String.prototype method.
- if ( typeof ns !== 'string' ) {
+ parse = function ( title, defaultNamespace ) {
+ var namespace, m, id, i, fragment, ext;
+
+ namespace = defaultNamespace === undefined ? NS_MAIN : defaultNamespace;
+
+ title = title
+ // Normalise whitespace to underscores and remove duplicates
+ .replace( /[ _\s]+/g, '_' )
+ // Trim underscores
+ .replace( rUnderscoreTrim, '' );
+
+ if ( title === '' ) {
return false;
}
- ns = clean( $.trim( ns.toLowerCase() ) ); // Normalize
- var id = mw.config.get( 'wgNamespaceIds' )[ns];
- if ( id === undefined ) {
- mw.log( 'mw.Title: Unrecognized namespace: ' + ns );
+
+ // Process initial colon
+ if ( title.charAt( 0 ) === ':' ) {
+ // Initial colon means main namespace instead of specified default
+ namespace = NS_MAIN;
+ title = title
+ // Strip colon
+ .substr( 1 )
+ // Trim underscores
+ .replace( rUnderscoreTrim, '' );
+ }
+
+ // Process namespace prefix (if any)
+ m = title.match( rSplit );
+ if ( m ) {
+ id = getNsIdByName( m[1] );
+ if ( id !== false ) {
+ // Ordinary namespace
+ namespace = id;
+ title = m[2];
+
+ // For Talk:X pages, make sure X has no "namespace" prefix
+ if ( namespace === NS_TALK && ( m = title.match( rSplit ) ) ) {
+ // Disallow titles like Talk:File:x (subject should roundtrip: talk:file:x -> file:x -> file_talk:x)
+ if ( getNsIdByName( m[1] ) !== false ) {
+ return false;
+ }
+ }
+ }
+ }
+
+ // Process fragment
+ i = title.indexOf( '#' );
+ if ( i === -1 ) {
+ fragment = null;
+ } else {
+ fragment = title
+ // Get segment starting after the hash
+ .substr( i + 1 )
+ // Convert to text
+ // NB: Must not be trimmed ("Example#_foo" is not the same as "Example#foo")
+ .replace( /_/g, ' ' );
+
+ title = title
+ // Strip hash
+ .substr( 0, i )
+ // Trim underscores, again (strips "_" from "bar" in "Foo_bar_#quux")
+ .replace( rUnderscoreTrim, '' );
+ }
+
+
+ // Reject illegal characters
+ if ( title.match( rInvalid ) ) {
+ return false;
+ }
+
+ // Disallow titles that browsers or servers might resolve as directory navigation
+ if (
+ title.indexOf( '.' ) !== -1 && (
+ title === '.' || title === '..' ||
+ title.indexOf( './' ) === 0 ||
+ title.indexOf( '../' ) === 0 ||
+ title.indexOf( '/./' ) !== -1 ||
+ title.indexOf( '/../' ) !== -1 ||
+ title.substr( -2 ) === '/.' ||
+ title.substr( -3 ) === '/..'
+ )
+ ) {
+ return false;
+ }
+
+ // Disallow magic tilde sequence
+ if ( title.indexOf( '~~~' ) !== -1 ) {
return false;
}
- return fixNsId( id );
+
+ // Disallow titles exceeding the 255 byte size limit (size of underlying database field)
+ // Except for special pages, e.g. [[Special:Block/Long name]]
+ // Note: The PHP implementation also asserts that even in NS_SPECIAL, the title should
+ // be less than 512 bytes.
+ if ( namespace !== NS_SPECIAL && $.byteLength( title ) > 255 ) {
+ return false;
+ }
+
+ // Can't make a link to a namespace alone.
+ if ( title === '' && namespace !== NS_MAIN ) {
+ return false;
+ }
+
+ // Any remaining initial :s are illegal.
+ if ( title.charAt( 0 ) === ':' ) {
+ return false;
+ }
+
+ // For backwards-compatibility with old mw.Title, we separate the extension from the
+ // rest of the title.
+ i = title.lastIndexOf( '.' );
+ if ( i === -1 || title.length <= i + 1 ) {
+ // Extensions are the non-empty segment after the last dot
+ ext = null;
+ } else {
+ ext = title.substr( i + 1 );
+ title = title.substr( 0, i );
+ }
+
+ return {
+ namespace: namespace,
+ title: title,
+ ext: ext,
+ fragment: fragment
+ };
},
/**
- * Helper to extract namespace, name and extension from a string.
+ * Convert db-key to readable text.
*
- * @param title {mw.Title}
- * @param raw {String}
- * @return {mw.Title}
+ * @private
+ * @static
+ * @method text
+ * @param {string} s
+ * @return {string}
*/
- setAll = function ( title, s ) {
- // In normal browsers the match-array contains null/undefined if there's no match,
- // IE returns an empty string.
- var matches = s.match( /^(?:([^:]+):)?(.*?)(?:\.(\w+))?$/ ),
- ns_match = getNsIdByName( matches[1] );
-
- // Namespace must be valid, and title must be a non-empty string.
- if ( ns_match && typeof matches[2] === 'string' && matches[2] !== '' ) {
- title.ns = ns_match;
- title.name = fixName( matches[2] );
- if ( typeof matches[3] === 'string' && matches[3] !== '' ) {
- title.ext = fixExt( matches[3] );
- }
+ text = function ( s ) {
+ if ( s !== null && s !== undefined ) {
+ return s.replace( /_/g, ' ' );
} else {
- // Consistency with MediaWiki PHP: Unknown namespace -> fallback to main namespace.
- title.ns = 0;
- setNameAndExtension( title, s );
+ return '';
}
- return title;
},
+ // Polyfill for ES5 Object.create
+ createObject = Object.create || ( function () {
+ return function ( o ) {
+ function Title() {}
+ if ( o !== Object( o ) ) {
+ throw new Error( 'Cannot inherit from a non-object' );
+ }
+ Title.prototype = o;
+ return new Title();
+ };
+ }() );
+
+
+ /* Static members */
+
/**
- * Helper to extract name and extension from a string.
+ * Constructor for Title objects with a null return instead of an exception for invalid titles.
*
- * @param title {mw.Title}
- * @param raw {String}
- * @return {mw.Title}
+ * @static
+ * @method
+ * @param {string} title
+ * @param {number} [namespace=NS_MAIN] Default namespace
+ * @return {mw.Title|null} A valid Title object or null if the title is invalid
*/
- setNameAndExtension = function ( title, raw ) {
- // In normal browsers the match-array contains null/undefined if there's no match,
- // IE returns an empty string.
- var matches = raw.match( /^(?:)?(.*?)(?:\.(\w+))?$/ );
-
- // Title must be a non-empty string.
- if ( typeof matches[1] === 'string' && matches[1] !== '' ) {
- title.name = fixName( matches[1] );
- if ( typeof matches[2] === 'string' && matches[2] !== '' ) {
- title.ext = fixExt( matches[2] );
- }
- } else {
- throw new Error( 'mw.Title: Could not parse title "' + raw + '"' );
+ Title.newFromText = function ( title, namespace ) {
+ var t, parsed = parse( title, namespace );
+ if ( !parsed ) {
+ return null;
}
- return title;
+
+ t = createObject( Title.prototype );
+ t.namespace = parsed.namespace;
+ t.title = parsed.title;
+ t.ext = parsed.ext;
+ t.fragment = parsed.fragment;
+
+ return t;
};
+ /**
+ * Get the file title from an image element
+ *
+ * var title = mw.Title.newFromImg( $( 'img:first' ) );
+ *
+ * @static
+ * @param {HTMLElement|jQuery} img The image to use as a base
+ * @return {mw.Title|null} The file title or null if unsuccessful
+ */
+ Title.newFromImg = function ( img ) {
+ var matches, i, regex, src, decodedSrc,
+
+ // thumb.php-generated thumbnails
+ thumbPhpRegex = /thumb\.php/,
+
+ regexes = [
+ // Thumbnails
+ /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s\/]+)\/[0-9]+px-\1[^\s\/]*$/,
+
+ // Thumbnails in non-hashed upload directories
+ /\/([^\s\/]+)\/[0-9]+px-\1[^\s\/]*$/,
+
+ // Full size images
+ /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s\/]+)$/,
+
+ // Full-size images in non-hashed upload directories
+ /\/([^\s\/]+)$/
+ ],
+
+ recount = regexes.length;
+
+ src = img.jquery ? img[0].src : img.src;
+
+ matches = src.match( thumbPhpRegex );
+
+ if ( matches ) {
+ return mw.Title.newFromText( 'File:' + mw.util.getParamValue( 'f', src ) );
+ }
+
+ decodedSrc = decodeURIComponent( src );
+
+ for ( i = 0; i < recount; i++ ) {
+ regex = regexes[i];
+ matches = decodedSrc.match( regex );
+
+ if ( matches && matches[1] ) {
+ return mw.Title.newFromText( 'File:' + matches[1] );
+ }
+ }
- /* Static space */
+ return null;
+ };
/**
* Whether this title exists on the wiki.
- * @param title {mixed} prefixed db-key name (string) or instance of Title
- * @return {mixed} Boolean true/false if the information is available. Otherwise null.
+ *
+ * @static
+ * @param {string|mw.Title} title prefixed db-key name (string) or instance of Title
+ * @return {boolean|null} Boolean if the information is available, otherwise null
*/
Title.exists = function ( title ) {
- var type = $.type( title ), obj = Title.exist.pages, match;
+ var match,
+ type = $.type( title ),
+ obj = Title.exist.pages;
+
if ( type === 'string' ) {
match = obj[title];
} else if ( type === 'object' && title instanceof Title ) {
@@ -191,27 +370,34 @@ var
} else {
throw new Error( 'mw.Title.exists: title must be a string or an instance of Title' );
}
+
if ( typeof match === 'boolean' ) {
return match;
}
+
return null;
};
- /**
- * @var Title.exist {Object}
- */
Title.exist = {
/**
- * @var Title.exist.pages {Object} Keyed by PrefixedDb title.
* Boolean true value indicates page does exist.
+ *
+ * @static
+ * @property {Object} exist.pages Keyed by PrefixedDb title.
*/
pages: {},
+
/**
- * @example Declare existing titles: Title.exist.set(['User:John_Doe', ...]);
- * @example Declare titles nonexistent: Title.exist.set(['File:Foo_bar.jpg', ...], false);
- * @param titles {String|Array} Title(s) in strict prefixedDb title form.
- * @param state {Boolean} (optional) State of the given titles. Defaults to true.
- * @return {Boolean}
+ * Example to declare existing titles:
+ * Title.exist.set(['User:John_Doe', ...]);
+ * Eample to declare titles nonexistent:
+ * Title.exist.set(['File:Foo_bar.jpg', ...], false);
+ *
+ * @static
+ * @property exist.set
+ * @param {string|Array} titles Title(s) in strict prefixedDb title form
+ * @param {boolean} [state=true] State of the given titles
+ * @return {boolean}
*/
set: function ( titles, state ) {
titles = $.isArray( titles ) ? titles : [titles];
@@ -224,119 +410,176 @@ var
}
};
- /* Public methods */
+ /* Public members */
- fn = {
+ Title.prototype = {
constructor: Title,
/**
- * Get the namespace number.
- * @return {Number}
+ * Get the namespace number
+ *
+ * Example: 6 for "File:Example_image.svg".
+ *
+ * @return {number}
*/
- getNamespaceId: function (){
- return this.ns;
+ getNamespaceId: function () {
+ return this.namespace;
},
/**
- * Get the namespace prefix (in the content-language).
- * In NS_MAIN this is '', otherwise namespace name plus ':'
- * @return {String}
+ * Get the namespace prefix (in the content language)
+ *
+ * Example: "File:" for "File:Example_image.svg".
+ * In #NS_MAIN this is '', otherwise namespace name plus ':'
+ *
+ * @return {string}
*/
- getNamespacePrefix: function (){
- return mw.config.get( 'wgFormattedNamespaces' )[this.ns].replace( / /g, '_' ) + (this.ns === 0 ? '' : ':');
+ getNamespacePrefix: function () {
+ return this.namespace === NS_MAIN ?
+ '' :
+ ( mw.config.get( 'wgFormattedNamespaces' )[ this.namespace ].replace( / /g, '_' ) + ':' );
},
/**
- * The name, like "Foo_bar"
- * @return {String}
+ * Get the page name without extension or namespace prefix
+ *
+ * Example: "Example_image" for "File:Example_image.svg".
+ *
+ * For the page title (full page name without namespace prefix), see #getMain.
+ *
+ * @return {string}
*/
getName: function () {
- if ( $.inArray( this.ns, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) {
- return this.name;
+ if ( $.inArray( this.namespace, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) {
+ return this.title;
} else {
- return $.ucFirst( this.name );
+ return $.ucFirst( this.title );
}
},
/**
- * The name, like "Foo bar"
- * @return {String}
+ * Get the page name (transformed by #text)
+ *
+ * Example: "Example image" for "File:Example_image.svg".
+ *
+ * For the page title (full page name without namespace prefix), see #getMainText.
+ *
+ * @return {string}
*/
getNameText: function () {
return text( this.getName() );
},
/**
- * Get full name in prefixed DB form, like File:Foo_bar.jpg,
- * most useful for API calls, anything that must identify the "title".
+ * Get the extension of the page name (if any)
+ *
+ * @return {string|null} Name extension or null if there is none
*/
- getPrefixedDb: function () {
- return this.getNamespacePrefix() + this.getMain();
+ getExtension: function () {
+ return this.ext;
},
/**
- * Get full name in text form, like "File:Foo bar.jpg".
- * @return {String}
+ * Shortcut for appendable string to form the main page name.
+ *
+ * Returns a string like ".json", or "" if no extension.
+ *
+ * @return {string}
*/
- getPrefixedText: function () {
- return text( this.getPrefixedDb() );
+ getDotExtension: function () {
+ return this.ext === null ? '' : '.' + this.ext;
},
/**
- * The main title (without namespace), like "Foo_bar.jpg"
- * @return {String}
+ * Get the main page name (transformed by #text)
+ *
+ * Example: "Example_image.svg" for "File:Example_image.svg".
+ *
+ * @return {string}
*/
getMain: function () {
return this.getName() + this.getDotExtension();
},
/**
- * The "text" form, like "Foo bar.jpg"
- * @return {String}
+ * Get the main page name (transformed by #text)
+ *
+ * Example: "Example image.svg" for "File:Example_image.svg".
+ *
+ * @return {string}
*/
getMainText: function () {
return text( this.getMain() );
},
/**
- * Get the extension (returns null if there was none)
- * @return {String|null} extension
+ * Get the full page name
+ *
+ * Eaxample: "File:Example_image.svg".
+ * Most useful for API calls, anything that must identify the "title".
+ *
+ * @return {string}
*/
- getExtension: function () {
- return this.ext;
+ getPrefixedDb: function () {
+ return this.getNamespacePrefix() + this.getMain();
},
/**
- * Convenience method: return string like ".jpg", or "" if no extension
- * @return {String}
+ * Get the full page name (transformed by #text)
+ *
+ * Example: "File:Example image.svg" for "File:Example_image.svg".
+ *
+ * @return {string}
*/
- getDotExtension: function () {
- return this.ext === null ? '' : '.' + this.ext;
+ getPrefixedText: function () {
+ return text( this.getPrefixedDb() );
},
/**
- * Return the URL to this title
- * @return {String}
+ * Get the fragment (if any).
+ *
+ * Note that this method (by design) does not include the hash character and
+ * the value is not url encoded.
+ *
+ * @return {string|null}
+ */
+ getFragment: function () {
+ return this.fragment;
+ },
+
+ /**
+ * Get the URL to this title
+ *
+ * @see mw.util#getUrl
+ * @return {string}
*/
getUrl: function () {
- return mw.util.wikiGetlink( this.toString() );
+ return mw.util.getUrl( this.toString() );
},
/**
* Whether this title exists on the wiki.
- * @return {mixed} Boolean true/false if the information is available. Otherwise null.
+ *
+ * @see #static-method-exists
+ * @return {boolean|null} Boolean if the information is available, otherwise null
*/
exists: function () {
return Title.exists( this );
}
};
- // Alias
- fn.toString = fn.getPrefixedDb;
- fn.toText = fn.getPrefixedText;
+ /**
+ * @alias #getPrefixedDb
+ * @method
+ */
+ Title.prototype.toString = Title.prototype.getPrefixedDb;
+
- // Assign
- Title.prototype = fn;
+ /**
+ * @alias #getPrefixedText
+ * @method
+ */
+ Title.prototype.toText = Title.prototype.getPrefixedText;
// Expose
mw.Title = Title;
diff --git a/resources/mediawiki/mediawiki.Uri.js b/resources/mediawiki/mediawiki.Uri.js
index bd12b214..a2d4d6cb 100644
--- a/resources/mediawiki/mediawiki.Uri.js
+++ b/resources/mediawiki/mediawiki.Uri.js
@@ -61,11 +61,11 @@
/**
* Function that's useful when constructing the URI string -- we frequently encounter the pattern of
* having to add something to the URI as we go, but only if it's present, and to include a character before or after if so.
- * @param {String} to prepend, if value not empty
- * @param {String} value to include, if not empty
- * @param {String} to append, if value not empty
- * @param {Boolean} raw -- if true, do not URI encode
- * @return {String}
+ * @param {string|undefined} pre To prepend.
+ * @param {string} val To include.
+ * @param {string} post To append.
+ * @param {boolean} raw If true, val will not be encoded.
+ * @return {string} Result.
*/
function cat( pre, val, post, raw ) {
if ( val === undefined || val === null || val === '' ) {
@@ -76,8 +76,8 @@
// Regular expressions to parse many common URIs.
var parser = {
- strict: /^(?:([^:\/?#]+):)?(?:\/\/(?:(?:([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)?((?:[^?#\/]*\/)*[^?#]*)(?:\?([^#]*))?(?:#(.*))?/,
- loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?(?:(?:([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?((?:\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?[^?#\/]*)(?:\?([^#]*))?(?:#(.*))?/
+ strict: /^(?:([^:\/?#]+):)?(?:\/\/(?:(?:([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?([^:\/?#]*)(?::(\d*))?)?((?:[^?#\/]*\/)*[^?#]*)(?:\?([^#]*))?(?:#(.*))?/,
+ loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?(?:(?:([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?([^:\/?#]*)(?::(\d*))?((?:\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?[^?#\/]*)(?:\?([^#]*))?(?:#(.*))?/
},
// The order here matches the order of captured matches in the above parser regexes.
@@ -103,14 +103,14 @@
/**
* Constructs URI object. Throws error if arguments are illegal/impossible, or otherwise don't parse.
* @constructor
- * @param {Object|String} URI string, or an Object with appropriate properties (especially another URI object to clone).
+ * @param {Object|string} uri URI string, or an Object with appropriate properties (especially another URI object to clone).
* Object must have non-blank 'protocol', 'host', and 'path' properties.
- * This parameter is optional. If omitted (or set to undefined, null or empty string), then an object will be created
- * for the default uri of this constructor (e.g. document.location for mw.Uri in MediaWiki core).
- * @param {Object|Boolean} Object with options, or (backwards compatibility) a boolean for strictMode
- * - strictMode {Boolean} Trigger strict mode parsing of the url. Default: false
- * - overrideKeys {Boolean} Wether to let duplicate query parameters override eachother (true) or automagically
- * convert to an array (false, default).
+ * This parameter is optional. If omitted (or set to undefined, null or empty string), then an object will be created
+ * for the default uri of this constructor (e.g. document.location for mw.Uri in MediaWiki core).
+ * @param {Object|boolean} Object with options, or (backwards compatibility) a boolean for strictMode
+ * - {boolean} strictMode Trigger strict mode parsing of the url. Default: false
+ * - {boolean} overrideKeys Wether to let duplicate query parameters override eachother (true) or automagically
+ * convert to an array (false, default).
*/
function Uri( uri, options ) {
options = typeof options === 'object' ? options : { strictMode: !!options };
@@ -158,7 +158,7 @@
}
if ( this.path && this.path.charAt( 0 ) !== '/' ) {
// A real relative URL, relative to defaultUri.path. We can't really handle that since we cannot
- // figure out whether the last path compoennt of defaultUri.path is a directory or a file.
+ // figure out whether the last path component of defaultUri.path is a directory or a file.
throw new Error( 'Bad constructor arguments' );
}
if ( !( this.protocol && this.host && this.path ) ) {
@@ -169,8 +169,8 @@
/**
* Standard encodeURIComponent, with extra stuff to make all browsers work similarly and more compliant with RFC 3986
* Similar to rawurlencode from PHP and our JS library mw.util.rawurlencode, but we also replace space with a +
- * @param {String} string
- * @return {String} encoded for URI
+ * @param {string} s String to encode.
+ * @return {string} Encoded string for URI.
*/
Uri.encode = function ( s ) {
return encodeURIComponent( s )
@@ -180,9 +180,9 @@
};
/**
- * Standard decodeURIComponent, with '+' to space
- * @param {String} string encoded for URI
- * @return {String} decoded string
+ * Standard decodeURIComponent, with '+' to space.
+ * @param {string} s String encoded for URI.
+ * @return {string} Decoded string.
*/
Uri.decode = function ( s ) {
return decodeURIComponent( s.replace( /\+/g, '%20' ) );
@@ -192,16 +192,16 @@
/**
* Parse a string and set our properties accordingly.
- * @param {String} URI
+ * @param {string} str URI
* @param {Object} options
- * @return {Boolean} success
+ * @return {boolean} Success.
*/
parse: function ( str, options ) {
var q,
uri = this,
matches = parser[ options.strictMode ? 'strict' : 'loose' ].exec( str );
$.each( properties, function ( i, property ) {
- uri[ property ] = matches[ i+1 ];
+ uri[ property ] = matches[ i + 1 ];
} );
// uri.query starts out as the query string; we will parse it into key-val pairs then make
@@ -210,7 +210,7 @@
q = {};
// using replace to iterate over a string
if ( uri.query ) {
- uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ($0, $1, $2, $3) {
+ uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ( $0, $1, $2, $3 ) {
var k, v;
if ( $1 ) {
k = Uri.decode( $1 );
@@ -240,7 +240,7 @@
/**
* Returns user and password portion of a URI.
- * @return {String}
+ * @return {string}
*/
getUserInfo: function () {
return cat( '', this.user, cat( ':', this.password, '' ) );
@@ -248,7 +248,7 @@
/**
* Gets host and port portion of a URI.
- * @return {String}
+ * @return {string}
*/
getHostPort: function () {
return this.host + cat( ':', this.port, '' );
@@ -257,7 +257,7 @@
/**
* Returns the userInfo and host and port portion of the URI.
* In most real-world URLs, this is simply the hostname, but it is more general.
- * @return {String}
+ * @return {string}
*/
getAuthority: function () {
return cat( '', this.getUserInfo(), '@' ) + this.getHostPort();
@@ -266,7 +266,7 @@
/**
* Returns the query arguments of the URL, encoded into a string
* Does not preserve the order of arguments passed into the URI. Does handle escaping.
- * @return {String}
+ * @return {string}
*/
getQueryString: function () {
var args = [];
@@ -274,7 +274,13 @@
var k = Uri.encode( key ),
vals = $.isArray( val ) ? val : [ val ];
$.each( vals, function ( i, v ) {
- args.push( k + ( v === null ? '' : '=' + Uri.encode( v ) ) );
+ if ( v === null ) {
+ args.push( k );
+ } else if ( k === 'title' ) {
+ args.push( k + '=' + mw.util.wikiUrlencode( v ) );
+ } else {
+ args.push( k + '=' + Uri.encode( v ) );
+ }
} );
} );
return args.join( '&' );
@@ -282,7 +288,7 @@
/**
* Returns everything after the authority section of the URI
- * @return {String}
+ * @return {string}
*/
getRelativePath: function () {
return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' );
@@ -290,7 +296,7 @@
/**
* Gets the entire URI string. May not be precisely the same as input due to order of query arguments.
- * @return {String} the URI string
+ * @return {string} The URI string.
*/
toString: function () {
return this.protocol + '://' + this.getAuthority() + this.getRelativePath();
diff --git a/resources/mediawiki/mediawiki.debug.css b/resources/mediawiki/mediawiki.debug.css
index 149e1bff..513cb847 100644
--- a/resources/mediawiki/mediawiki.debug.css
+++ b/resources/mediawiki/mediawiki.debug.css
@@ -1,6 +1,5 @@
.mw-debug {
width: 100%;
- text-align: left;
background-color: #eee;
border-top: 1px solid #aaa;
}
diff --git a/resources/mediawiki/mediawiki.debug.js b/resources/mediawiki/mediawiki.debug.js
index 1ad1a623..986917a1 100644
--- a/resources/mediawiki/mediawiki.debug.js
+++ b/resources/mediawiki/mediawiki.debug.js
@@ -96,7 +96,7 @@
buildHtml: function () {
var $container, $bits, panes, id, gitInfo;
- $container = $( '<div id="mw-debug-toolbar" class="mw-debug"></div>' );
+ $container = $( '<div id="mw-debug-toolbar" class="mw-debug" lang="en" dir="ltr"></div>' );
$bits = $( '<div class="mw-debug-bits"></div>' );
@@ -187,9 +187,7 @@
.text( 'Time: ' + this.data.time.toFixed( 5 ) );
bitDiv( 'memory' )
- .text( 'Memory: ' + this.data.memory )
- .append( $( '<span title="Peak usage"></span>' ).text( ' (' + this.data.memoryPeak + ')' ) );
-
+ .text( 'Memory: ' + this.data.memory + ' (Peak: ' + this.data.memoryPeak + ')' );
$bits.appendTo( $container );
@@ -231,7 +229,7 @@
$( '<colgroup>' ).css( 'width', 350 ).appendTo( $table );
- entryTypeText = function( entryType ) {
+ entryTypeText = function ( entryType ) {
switch ( entryType ) {
case 'log':
return 'Log';
diff --git a/resources/mediawiki/mediawiki.feedback.js b/resources/mediawiki/mediawiki.feedback.js
index 634d02b1..1afe51ef 100644
--- a/resources/mediawiki/mediawiki.feedback.js
+++ b/resources/mediawiki/mediawiki.feedback.js
@@ -1,5 +1,5 @@
/**
- * mediawiki.Feedback
+ * mediawiki.feedback
*
* @author Ryan Kaldari, 2010
* @author Neil Kandalgaonkar, 2010-11
@@ -68,17 +68,28 @@
mw.Feedback.prototype = {
setup: function () {
- var fb = this;
+ var $feedbackPageLink,
+ $bugNoteLink,
+ $bugsListLink,
+ fb = this;
- var $feedbackPageLink = $( '<a>' )
- .attr( { 'href': fb.title.getUrl(), 'target': '_blank' } )
- .css( { 'white-space': 'nowrap' } );
+ $feedbackPageLink = $( '<a>' )
+ .attr( {
+ href: fb.title.getUrl(),
+ target: '_blank'
+ } )
+ .css( {
+ whiteSpace: 'nowrap'
+ } );
- var $bugNoteLink = $( '<a>' ).attr( { 'href': '#' } ).click( function () {
+ $bugNoteLink = $( '<a>' ).attr( { href: '#' } ).click( function () {
fb.displayBugs();
} );
- var $bugsListLink = $( '<a>' ).attr( { 'href': fb.bugsListLink, 'target': '_blank' } );
+ $bugsListLink = $( '<a>' ).attr( {
+ href: fb.bugsListLink,
+ target: '_blank'
+ } );
// TODO: Use a stylesheet instead of these inline styles
this.$dialog =
@@ -108,7 +119,7 @@
),
$( '<div class="feedback-mode feedback-submitting" style="text-align: center; margin: 3em 0;"></div>' ).append(
mw.msg( 'feedback-adding' ),
- $( '<br/>' ),
+ $( '<br>' ),
$( '<span class="feedback-spinner"></span>' )
),
$( '<div class="feedback-mode feedback-thanks" style="text-align: center; margin:1em"></div>' ).msg(
@@ -148,9 +159,9 @@
},
displayBugs: function () {
- var fb = this;
+ var fb = this,
+ bugsButtons = {};
this.display( 'bugs' );
- var bugsButtons = {};
bugsButtons[ mw.msg( 'feedback-bugnew' ) ] = function () {
window.open( fb.bugsLink, '_blank' );
};
@@ -163,9 +174,9 @@
},
displayThanks: function () {
- var fb = this;
+ var fb = this,
+ closeButton = {};
this.display( 'thanks' );
- var closeButton = {};
closeButton[ mw.msg( 'feedback-close' ) ] = function () {
fb.$dialog.dialog( 'close' );
};
@@ -181,14 +192,14 @@
* message: {String}
*/
displayForm: function ( contents ) {
- var fb = this;
+ var fb = this,
+ formButtons = {};
this.subjectInput.value = ( contents && contents.subject ) ? contents.subject : '';
this.messageInput.value = ( contents && contents.message ) ? contents.message : '';
this.display( 'form' );
// Set up buttons for dialog box. We have to do it the hard way since the json keys are localized
- var formButtons = {};
formButtons[ mw.msg( 'feedback-submit' ) ] = function () {
fb.submit();
};
@@ -199,10 +210,10 @@
},
displayError: function ( message ) {
- var fb = this;
+ var fb = this,
+ closeButton = {};
this.display( 'error' );
this.$dialog.find( '.feedback-error-msg' ).msg( message );
- var closeButton = {};
closeButton[ mw.msg( 'feedback-close' ) ] = function () {
fb.$dialog.dialog( 'close' );
};
@@ -231,7 +242,7 @@
}
}
- function err( code, info ) {
+ function err() {
// ajax request failed
fb.displayError( 'feedback-error3' );
}
diff --git a/resources/mediawiki/mediawiki.hidpi.js b/resources/mediawiki/mediawiki.hidpi.js
new file mode 100644
index 00000000..ecee450c
--- /dev/null
+++ b/resources/mediawiki/mediawiki.hidpi.js
@@ -0,0 +1,5 @@
+jQuery( function ( $ ) {
+ // Apply hidpi images on DOM-ready
+ // Some may have already partly preloaded at low resolution.
+ $( 'body' ).hidpi();
+} );
diff --git a/resources/mediawiki/mediawiki.htmlform.js b/resources/mediawiki/mediawiki.htmlform.js
index a4753b99..de068598 100644
--- a/resources/mediawiki/mediawiki.htmlform.js
+++ b/resources/mediawiki/mediawiki.htmlform.js
@@ -1,64 +1,128 @@
/**
- * Utility functions for jazzing up HTMLForm elements
+ * Utility functions for jazzing up HTMLForm elements.
*/
-( function ( $ ) {
+( function ( mw, $ ) {
-/**
- * jQuery plugin to fade or snap to visible state.
- *
- * @param boolean instantToggle (optional)
- * @return jQuery
- */
-$.fn.goIn = function ( instantToggle ) {
- if ( instantToggle === true ) {
- return $(this).show();
- }
- return $(this).stop( true, true ).fadeIn();
-};
+ /**
+ * jQuery plugin to fade or snap to visible state.
+ *
+ * @param {boolean} instantToggle [optional]
+ * @return {jQuery}
+ */
+ $.fn.goIn = function ( instantToggle ) {
+ if ( instantToggle === true ) {
+ return $(this).show();
+ }
+ return $(this).stop( true, true ).fadeIn();
+ };
-/**
- * jQuery plugin to fade or snap to hiding state.
- *
- * @param boolean instantToggle (optional)
- * @return jQuery
- */
-$.fn.goOut = function ( instantToggle ) {
- if ( instantToggle === true ) {
- return $(this).hide();
- }
- return $(this).stop( true, true ).fadeOut();
-};
+ /**
+ * jQuery plugin to fade or snap to hiding state.
+ *
+ * @param {boolean} instantToggle [optional]
+ * @return jQuery
+ */
+ $.fn.goOut = function ( instantToggle ) {
+ if ( instantToggle === true ) {
+ return $(this).hide();
+ }
+ return $(this).stop( true, true ).fadeOut();
+ };
-/**
- * Bind a function to the jQuery object via live(), and also immediately trigger
- * the function on the objects with an 'instant' parameter set to true
- * @param callback function taking one parameter, which is Bool true when the event
- * is called immediately, and the EventArgs object when triggered from an event
- */
-$.fn.liveAndTestAtStart = function ( callback ){
- $(this)
- .live( 'change', callback )
- .each( function ( index, element ){
- callback.call( this, true );
- } );
-};
+ /**
+ * Bind a function to the jQuery object via live(), and also immediately trigger
+ * the function on the objects with an 'instant' parameter set to true.
+ * @param {Function} callback Takes one parameter, which is {true} when the
+ * event is called immediately, and {jQuery.Event} when triggered from an event.
+ */
+ $.fn.liveAndTestAtStart = function ( callback ){
+ $(this)
+ .live( 'change', callback )
+ .each( function () {
+ callback.call( this, true );
+ } );
+ };
-// Document ready:
-$( function () {
+ $( function () {
- // Animate the SelectOrOther fields, to only show the text field when
- // 'other' is selected.
- $( '.mw-htmlform-select-or-other' ).liveAndTestAtStart( function ( instant ) {
- var $other = $( '#' + $(this).attr( 'id' ) + '-other' );
- $other = $other.add( $other.siblings( 'br' ) );
- if ( $(this).val() === 'other' ) {
- $other.goIn( instant );
- } else {
- $other.goOut( instant );
- }
- });
+ // Animate the SelectOrOther fields, to only show the text field when
+ // 'other' is selected.
+ $( '.mw-htmlform-select-or-other' ).liveAndTestAtStart( function ( instant ) {
+ var $other = $( '#' + $(this).attr( 'id' ) + '-other' );
+ $other = $other.add( $other.siblings( 'br' ) );
+ if ( $(this).val() === 'other' ) {
+ $other.goIn( instant );
+ } else {
+ $other.goOut( instant );
+ }
+ });
-});
+ } );
+ function addMulti( $oldContainer, $container ) {
+ var name = $oldContainer.find( 'input:first-child' ).attr( 'name' ),
+ oldClass = ( ' ' + $oldContainer.attr( 'class' ) + ' ' ).replace( /(mw-htmlform-field-HTMLMultiSelectField|mw-chosen)/g, '' ),
+ $select = $( '<select>' ),
+ dataPlaceholder = mw.message( 'htmlform-chosen-placeholder' );
+ oldClass = $.trim( oldClass );
+ $select.attr( {
+ name: name,
+ multiple: 'multiple',
+ 'data-placeholder': dataPlaceholder.plain(),
+ 'class': 'htmlform-chzn-select mw-input ' + oldClass
+ } );
+ $oldContainer.find( 'input' ).each( function () {
+ var $oldInput = $(this),
+ checked = $oldInput.prop( 'checked' ),
+ $option = $( '<option>' );
+ $option.prop( 'value', $oldInput.prop( 'value' ) );
+ if ( checked ) {
+ $option.prop( 'selected', true );
+ }
+ $option.text( $oldInput.prop( 'value' ) );
+ $select.append( $option );
+ } );
+ $container.append( $select );
+ }
-}( jQuery ) );
+ function convertCheckboxesToMulti( $oldContainer, type ) {
+ var $fieldLabel = $( '<td>' ),
+ $td = $( '<td>' ),
+ $fieldLabelText = $( '<label>' ),
+ $container;
+ if ( type === 'tr' ) {
+ addMulti( $oldContainer, $td );
+ $container = $( '<tr>' );
+ $container.append( $td );
+ } else if ( type === 'div' ) {
+ $fieldLabel = $( '<div>' );
+ $container = $( '<div>' );
+ addMulti( $oldContainer, $container );
+ }
+ $fieldLabel.attr( 'class', 'mw-label' );
+ $fieldLabelText.text( $oldContainer.find( '.mw-label label' ).text() );
+ $fieldLabel.append( $fieldLabelText );
+ $container.prepend( $fieldLabel );
+ $oldContainer.replaceWith( $container );
+ return $container;
+ }
+
+ if ( $( '.mw-chosen' ).length ) {
+ mw.loader.using( 'jquery.chosen', function () {
+ $( '.mw-chosen' ).each( function () {
+ var type = this.nodeName.toLowerCase(),
+ $converted = convertCheckboxesToMulti( $( this ), type );
+ $converted.find( '.htmlform-chzn-select' ).chosen( { width: 'auto' } );
+ } );
+ } );
+ }
+
+ $( function () {
+ var $matrixTooltips = $( '.mw-htmlform-matrix .mw-htmlform-tooltip' );
+ if ( $matrixTooltips.length ) {
+ mw.loader.using( 'jquery.tipsy', function () {
+ $matrixTooltips.tipsy( { gravity: 's' } );
+ } );
+ }
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki/mediawiki.icon.css b/resources/mediawiki/mediawiki.icon.css
new file mode 100644
index 00000000..f61b7257
--- /dev/null
+++ b/resources/mediawiki/mediawiki.icon.css
@@ -0,0 +1,15 @@
+/* General-purpose icons via CSS. Classes here should be named "mw-icon-*". */
+
+/* For the collapsed and expanded arrows, we also provide selectors to make it
+ * easy to use them with jquery.makeCollapsible. */
+.mw-icon-arrow-collapsed,
+.mw-collapsible-arrow.mw-collapsible-toggle-collapsed {
+ /* @embed */
+ background: url(images/arrow-collapsed-ltr.png) no-repeat left bottom;
+}
+
+.mw-icon-arrow-expanded,
+.mw-collapsible-arrow.mw-collapsible-toggle-expanded {
+ /* @embed */
+ background: url(images/arrow-expanded.png) no-repeat left bottom;
+}
diff --git a/resources/mediawiki/mediawiki.inspect.js b/resources/mediawiki/mediawiki.inspect.js
new file mode 100644
index 00000000..2f2ca335
--- /dev/null
+++ b/resources/mediawiki/mediawiki.inspect.js
@@ -0,0 +1,204 @@
+/*!
+ * Tools for inspecting page composition and performance.
+ *
+ * @author Ori Livneh
+ * @since 1.22
+ */
+/*jshint devel:true */
+( function ( mw, $ ) {
+
+ function sortByProperty( array, prop, descending ) {
+ var order = descending ? -1 : 1;
+ return array.sort( function ( a, b ) {
+ return a[prop] > b[prop] ? order : a[prop] < b[prop] ? -order : 0;
+ } );
+ }
+
+ /**
+ * @class mw.inspect
+ * @singleton
+ */
+ var inspect = {
+
+ /**
+ * Calculate the byte size of a ResourceLoader module.
+ *
+ * @param {string} moduleName The name of the module
+ * @return {number|null} Module size in bytes or null
+ */
+ getModuleSize: function ( moduleName ) {
+ var module = mw.loader.moduleRegistry[ moduleName ],
+ payload = 0;
+
+ if ( mw.loader.getState( moduleName ) !== 'ready' ) {
+ return null;
+ }
+
+ if ( !module.style && !module.script ) {
+ return null;
+ }
+
+ // Tally CSS
+ if ( module.style && $.isArray( module.style.css ) ) {
+ $.each( module.style.css, function ( i, stylesheet ) {
+ payload += $.byteLength( stylesheet );
+ } );
+ }
+
+ // Tally JavaScript
+ if ( $.isFunction( module.script ) ) {
+ payload += $.byteLength( module.script.toString() );
+ }
+
+ return payload;
+ },
+
+ /**
+ * Given CSS source, count both the total number of selectors it
+ * contains and the number which match some element in the current
+ * document.
+ *
+ * @param {string} css CSS source
+ * @return Selector counts
+ * @return {number} return.selectors Total number of selectors
+ * @return {number} return.matched Number of matched selectors
+ */
+ auditSelectors: function ( css ) {
+ var selectors = { total: 0, matched: 0 },
+ style = document.createElement( 'style' ),
+ sheet, rules;
+
+ style.textContent = css;
+ document.body.appendChild( style );
+ // Standards-compliant browsers use .sheet.cssRules, IE8 uses .styleSheet.rules…
+ sheet = style.sheet || style.styleSheet;
+ rules = sheet.cssRules || sheet.rules;
+ $.each( rules, function ( index, rule ) {
+ selectors.total++;
+ if ( document.querySelector( rule.selectorText ) !== null ) {
+ selectors.matched++;
+ }
+ } );
+ document.body.removeChild( style );
+ return selectors;
+ },
+
+ /**
+ * Get a list of all loaded ResourceLoader modules.
+ *
+ * @return {Array} List of module names
+ */
+ getLoadedModules: function () {
+ return $.grep( mw.loader.getModuleNames(), function ( module ) {
+ return mw.loader.getState( module ) === 'ready';
+ } );
+ },
+
+ /**
+ * Print tabular data to the console, using console.table, console.log,
+ * or mw.log (in declining order of preference).
+ *
+ * @param {Array} data Tabular data represented as an array of objects
+ * with common properties.
+ */
+ dumpTable: function ( data ) {
+ try {
+ // Bartosz made me put this here.
+ if ( window.opera ) { throw window.opera; }
+ // Use Function.prototype#call to force an exception on Firefox,
+ // which doesn't define console#table but doesn't complain if you
+ // try to invoke it.
+ console.table.call( console, data );
+ return;
+ } catch (e) {}
+ try {
+ console.log( $.toJSON( data, null, 2 ) );
+ return;
+ } catch (e) {}
+ mw.log( data );
+ },
+
+ /**
+ * Generate and print one more reports. When invoked with no arguments,
+ * print all reports.
+ *
+ * @param {string...} [reports] Report names to run, or unset to print
+ * all available reports.
+ */
+ runReports: function () {
+ var reports = arguments.length > 0 ?
+ Array.prototype.slice.call( arguments ) :
+ $.map( inspect.reports, function ( v, k ) { return k; } );
+
+ $.each( reports, function ( index, name ) {
+ inspect.dumpTable( inspect.reports[name]() );
+ } );
+ },
+
+ /**
+ * @class mw.inspect.reports
+ * @singleton
+ */
+ reports: {
+ /**
+ * Generate a breakdown of all loaded modules and their size in
+ * kilobytes. Modules are ordered from largest to smallest.
+ */
+ size: function () {
+ // Map each module to a descriptor object.
+ var modules = $.map( inspect.getLoadedModules(), function ( module ) {
+ return {
+ name: module,
+ size: inspect.getModuleSize( module )
+ };
+ } );
+
+ // Sort module descriptors by size, largest first.
+ sortByProperty( modules, 'size', true );
+
+ // Convert size to human-readable string.
+ $.each( modules, function ( i, module ) {
+ module.size = module.size > 1024 ?
+ ( module.size / 1024 ).toFixed( 2 ) + ' KB' :
+ ( module.size !== null ? module.size + ' B' : null );
+ } );
+
+ return modules;
+ },
+
+ /**
+ * For each module with styles, count the number of selectors, and
+ * count how many match against some element currently in the DOM.
+ */
+ css: function () {
+ var modules = [];
+
+ $.each( inspect.getLoadedModules(), function ( index, name ) {
+ var css, stats, module = mw.loader.moduleRegistry[name];
+
+ try {
+ css = module.style.css.join();
+ } catch (e) { return; } // skip
+
+ stats = inspect.auditSelectors( css );
+ modules.push( {
+ module: name,
+ allSelectors: stats.total,
+ matchedSelectors: stats.matched,
+ percentMatched: stats.total !== 0 ?
+ ( stats.matched / stats.total * 100 ).toFixed( 2 ) + '%' : null
+ } );
+ } );
+ sortByProperty( modules, 'allSelectors', true );
+ return modules;
+ },
+ }
+ };
+
+ if ( mw.config.get( 'debug' ) ) {
+ mw.log( 'mw.inspect: reports are not available in debug mode.' );
+ }
+
+ mw.inspect = inspect;
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki/mediawiki.jqueryMsg.js b/resources/mediawiki/mediawiki.jqueryMsg.js
index 86af31ff..70b9be93 100644
--- a/resources/mediawiki/mediawiki.jqueryMsg.js
+++ b/resources/mediawiki/mediawiki.jqueryMsg.js
@@ -3,18 +3,98 @@
* See: http://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs
*
* @author neilk@wikimedia.org
+* @author mflaschen@wikimedia.org
*/
( function ( mw, $ ) {
- var slice = Array.prototype.slice,
+ var oldParser,
+ slice = Array.prototype.slice,
parserDefaults = {
magic : {
'SITENAME' : mw.config.get( 'wgSiteName' )
},
+ // This is a whitelist based on, but simpler than, Sanitizer.php.
+ // Self-closing tags are not currently supported.
+ allowedHtmlElements : [
+ 'b',
+ 'i'
+ ],
+ // Key tag name, value allowed attributes for that tag.
+ // See Sanitizer::setupAttributeWhitelist
+ allowedHtmlCommonAttributes : [
+ // HTML
+ 'id',
+ 'class',
+ 'style',
+ 'lang',
+ 'dir',
+ 'title',
+
+ // WAI-ARIA
+ 'role'
+ ],
+
+ // Attributes allowed for specific elements.
+ // Key is element name in lower case
+ // Value is array of allowed attributes for that element
+ allowedHtmlAttributesByElement : {},
messages : mw.messages,
- language : mw.language
+ language : mw.language,
+
+ // Same meaning as in mediawiki.js.
+ //
+ // Only 'text', 'parse', and 'escaped' are supported, and the
+ // actual escaping for 'escaped' is done by other code (generally
+ // through mediawiki.js).
+ //
+ // However, note that this default only
+ // applies to direct calls to jqueryMsg. The default for mediawiki.js itself
+ // is 'text', including when it uses jqueryMsg.
+ format: 'parse'
+
};
/**
+ * Wrapper around jQuery append that converts all non-objects to TextNode so append will not
+ * convert what it detects as an htmlString to an element.
+ *
+ * Object elements of children (jQuery, HTMLElement, TextNode, etc.) will be left as is.
+ *
+ * @param {jQuery} $parent Parent node wrapped by jQuery
+ * @param {Object|string|Array} children What to append, with the same possible types as jQuery
+ * @return {jQuery} $parent
+ */
+ function appendWithoutParsing( $parent, children ) {
+ var i, len;
+
+ if ( !$.isArray( children ) ) {
+ children = [children];
+ }
+
+ for ( i = 0, len = children.length; i < len; i++ ) {
+ if ( typeof children[i] !== 'object' ) {
+ children[i] = document.createTextNode( children[i] );
+ }
+ }
+
+ return $parent.append( children );
+ }
+
+ /**
+ * Decodes the main HTML entities, those encoded by mw.html.escape.
+ *
+ * @param {string} encode Encoded string
+ * @return {string} String with those entities decoded
+ */
+ function decodePrimaryHtmlEntities( encoded ) {
+ return encoded
+ .replace( /&#039;/g, '\'' )
+ .replace( /&quot;/g, '"' )
+ .replace( /&lt;/g, '<' )
+ .replace( /&gt;/g, '>' )
+ .replace( /&amp;/g, '&' );
+ }
+
+ /**
* Given parser options, return a function that parses a key and replacements, returning jQuery object
* @param {Object} parser options
* @return {Function} accepting ( String message key, String replacement1, String replacement2 ... ) and returning {jQuery}
@@ -30,12 +110,12 @@
* @return {jQuery}
*/
return function ( args ) {
- var key = args[0];
- var argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 );
+ var key = args[0],
+ argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 );
try {
return parser.parse( key, argsArray );
} catch ( e ) {
- return $( '<span>' ).append( key + ': ' + e.message );
+ return $( '<span>' ).text( key + ': ' + e.message );
}
};
}
@@ -56,19 +136,32 @@
* @return {Function} function suitable for assigning to window.gM
*/
mw.jqueryMsg.getMessageFunction = function ( options ) {
- var failableParserFn = getFailableParserFn( options );
+ var failableParserFn = getFailableParserFn( options ),
+ format;
+
+ if ( options && options.format !== undefined ) {
+ format = options.format;
+ } else {
+ format = parserDefaults.format;
+ }
+
/**
* N.B. replacements are variadic arguments or an array in second parameter. In other words:
* somefunction(a, b, c, d)
* is equivalent to
* somefunction(a, [b, c, d])
*
- * @param {String} message key
- * @param {Array} optional replacements (can also specify variadically)
- * @return {String} rendered HTML as string
+ * @param {string} key Message key.
+ * @param {Array|mixed} replacements Optional variable replacements (variadically or an array).
+ * @return {string} Rendered HTML.
*/
- return function ( /* key, replacements */ ) {
- return failableParserFn( arguments ).html();
+ return function () {
+ var failableResult = failableParserFn( arguments );
+ if ( format === 'text' || format === 'escaped' ) {
+ return failableResult.text();
+ } else {
+ return failableResult.html();
+ }
};
};
@@ -93,14 +186,16 @@
* somefunction(a, [b, c, d])
*
* We append to 'this', which in a jQuery plugin context will be the selected elements.
- * @param {String} message key
- * @param {Array} optional replacements (can also specify variadically)
+ * @param {string} key Message key.
+ * @param {Array|mixed} replacements Optional variable replacements (variadically or an array).
* @return {jQuery} this
*/
- return function ( /* key, replacements */ ) {
+ return function () {
var $target = this.empty();
+ // TODO: Simply appendWithoutParsing( $target, failableParserFn( arguments ).contents() )
+ // or Simply appendWithoutParsing( $target, failableParserFn( arguments ) )
$.each( failableParserFn( arguments ).contents(), function ( i, node ) {
- $target.append( node );
+ appendWithoutParsing( $target, node );
} );
return $target;
};
@@ -113,20 +208,36 @@
*/
mw.jqueryMsg.parser = function ( options ) {
this.settings = $.extend( {}, parserDefaults, options );
+ this.settings.onlyCurlyBraceTransform = ( this.settings.format === 'text' || this.settings.format === 'escaped' );
+
this.emitter = new mw.jqueryMsg.htmlEmitter( this.settings.language, this.settings.magic );
};
mw.jqueryMsg.parser.prototype = {
- // cache, map of mediaWiki message key to the AST of the message. In most cases, the message is a string so this is identical.
- // (This is why we would like to move this functionality server-side).
+ /**
+ * Cache mapping MediaWiki message keys and the value onlyCurlyBraceTransform, to the AST of the message.
+ *
+ * In most cases, the message is a string so this is identical.
+ * (This is why we would like to move this functionality server-side).
+ *
+ * The two parts of the key are separated by colon. For example:
+ *
+ * "message-key:true": ast
+ *
+ * if they key is "message-key" and onlyCurlyBraceTransform is true.
+ *
+ * This cache is shared by all instances of mw.jqueryMsg.parser.
+ *
+ * @static
+ */
astCache: {},
/**
* Where the magic happens.
* Parses a message from the key, and swaps in replacements as necessary, wraps in jQuery
* If an error is thrown, returns original key, and logs the error
- * @param {String} message key
- * @param {Array} replacements for $1, $2... $n
+ * @param {String} key Message key.
+ * @param {Array} replacements Variable replacements for $1, $2... $n
* @return {jQuery}
*/
parse: function ( key, replacements ) {
@@ -139,16 +250,19 @@
* @return {String|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing
*/
getAst: function ( key ) {
- if ( this.astCache[ key ] === undefined ) {
- var wikiText = this.settings.messages.get( key );
+ var cacheKey = [key, this.settings.onlyCurlyBraceTransform].join( ':' ), wikiText;
+
+ if ( this.astCache[ cacheKey ] === undefined ) {
+ wikiText = this.settings.messages.get( key );
if ( typeof wikiText !== 'string' ) {
- wikiText = "\\[" + key + "\\]";
+ wikiText = '\\[' + key + '\\]';
}
- this.astCache[ key ] = this.wikiTextToAst( wikiText );
+ this.astCache[ cacheKey ] = this.wikiTextToAst( wikiText );
}
- return this.astCache[ key ];
+ return this.astCache[ cacheKey ];
},
- /*
+
+ /**
* Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
*
* CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already.
@@ -159,18 +273,29 @@
* @return {Mixed} abstract syntax tree
*/
wikiTextToAst: function ( input ) {
+ var pos, settings = this.settings, concat = Array.prototype.concat,
+ regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets,
+ doubleQuote, singleQuote, backslash, anyCharacter, asciiAlphabetLiteral,
+ escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
+ whitespace, dollar, digits, htmlDoubleQuoteAttributeValue, htmlSingleQuoteAttributeValue,
+ htmlAttributeEquals, openHtmlStartTag, optionalForwardSlash, openHtmlEndTag, closeHtmlTag,
+ openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openWikilink, closeWikilink, templateName, pipe, colon,
+ templateContents, openTemplate, closeTemplate,
+ nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result;
// Indicates current position in input as we parse through it.
// Shared among all parsing functions below.
- var pos = 0;
+ pos = 0;
+
// =========================================================
// parsing combinators - could be a library on its own
// =========================================================
// Try parsers until one works, if none work return null
function choice( ps ) {
return function () {
- for ( var i = 0; i < ps.length; i++ ) {
- var result = ps[i]();
+ var i, result;
+ for ( i = 0; i < ps.length; i++ ) {
+ result = ps[i]();
if ( result !== null ) {
return result;
}
@@ -181,10 +306,11 @@
// try several ps in a row, all must succeed or return null
// this is the only eager one
function sequence( ps ) {
- var originalPos = pos;
- var result = [];
- for ( var i = 0; i < ps.length; i++ ) {
- var res = ps[i]();
+ var i, res,
+ originalPos = pos,
+ result = [];
+ for ( i = 0; i < ps.length; i++ ) {
+ res = ps[i]();
if ( res === null ) {
pos = originalPos;
return null;
@@ -197,9 +323,9 @@
// must succeed a minimum of n times or return null
function nOrMore( n, p ) {
return function () {
- var originalPos = pos;
- var result = [];
- var parsed = p();
+ var originalPos = pos,
+ result = [],
+ parsed = p();
while ( parsed !== null ) {
result.push( parsed );
parsed = p();
@@ -232,6 +358,15 @@
return result;
};
}
+
+ /**
+ * Makes a regex parser, given a RegExp object.
+ * The regex being passed in should start with a ^ to anchor it to the start
+ * of the string.
+ *
+ * @param {RegExp} regex anchored regex
+ * @return {Function} function to parse input based on the regex
+ */
function makeRegexParser( regex ) {
return function () {
var matches = input.substr( pos ).match( regex );
@@ -258,11 +393,23 @@
// but some debuggers can't tell you exactly where they come from. Also the mutually
// recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF)
// This may be because, to save code, memoization was removed
- var regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ );
- var regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/);
- var regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/);
- var backslash = makeStringParser( "\\" );
- var anyCharacter = makeRegexParser( /^./ );
+
+ regularLiteral = makeRegexParser( /^[^{}\[\]$<\\]/ );
+ regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/);
+ regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/);
+ regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ );
+
+ backslash = makeStringParser( '\\' );
+ doubleQuote = makeStringParser( '"' );
+ singleQuote = makeStringParser( '\'' );
+ anyCharacter = makeRegexParser( /^./ );
+
+ openHtmlStartTag = makeStringParser( '<' );
+ optionalForwardSlash = makeRegexParser( /^\/?/ );
+ openHtmlEndTag = makeStringParser( '</' );
+ htmlAttributeEquals = makeRegexParser( /^\s*=\s*/ );
+ closeHtmlTag = makeRegexParser( /^\s*>/ );
+
function escapedLiteral() {
var result = sequence( [
backslash,
@@ -270,36 +417,54 @@
] );
return result === null ? null : result[1];
}
- var escapedOrLiteralWithoutSpace = choice( [
+ escapedOrLiteralWithoutSpace = choice( [
escapedLiteral,
regularLiteralWithoutSpace
] );
- var escapedOrLiteralWithoutBar = choice( [
+ escapedOrLiteralWithoutBar = choice( [
escapedLiteral,
regularLiteralWithoutBar
] );
- var escapedOrRegularLiteral = choice( [
+ escapedOrRegularLiteral = choice( [
escapedLiteral,
regularLiteral
] );
// Used to define "literals" without spaces, in space-delimited situations
function literalWithoutSpace() {
- var result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
- return result === null ? null : result.join('');
+ var result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
+ return result === null ? null : result.join('');
}
// Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default
// it is not a literal in the parameter
function literalWithoutBar() {
- var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
- return result === null ? null : result.join('');
+ var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
+ return result === null ? null : result.join('');
}
+
+ // Used for wikilink page names. Like literalWithoutBar, but
+ // without allowing escapes.
+ function unescapedLiteralWithoutBar() {
+ var result = nOrMore( 1, regularLiteralWithoutBar )();
+ return result === null ? null : result.join('');
+ }
+
function literal() {
- var result = nOrMore( 1, escapedOrRegularLiteral )();
- return result === null ? null : result.join('');
+ var result = nOrMore( 1, escapedOrRegularLiteral )();
+ return result === null ? null : result.join('');
}
- var whitespace = makeRegexParser( /^\s+/ );
- var dollar = makeStringParser( '$' );
- var digits = makeRegexParser( /^\d+/ );
+
+ function curlyBraceTransformExpressionLiteral() {
+ var result = nOrMore( 1, regularLiteralWithSquareBrackets )();
+ return result === null ? null : result.join('');
+ }
+
+ asciiAlphabetLiteral = makeRegexParser( /[A-Za-z]+/ );
+ htmlDoubleQuoteAttributeValue = makeRegexParser( /^[^"]*/ );
+ htmlSingleQuoteAttributeValue = makeRegexParser( /^[^']*/ );
+
+ whitespace = makeRegexParser( /^\s+/ );
+ dollar = makeStringParser( '$' );
+ digits = makeRegexParser( /^\d+/ );
function replacement() {
var result = sequence( [
@@ -311,20 +476,28 @@
}
return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ];
}
- var openExtlink = makeStringParser( '[' );
- var closeExtlink = makeStringParser( ']' );
- // this extlink MUST have inner text, e.g. [foo] not allowed; [foo bar] is allowed
+ openExtlink = makeStringParser( '[' );
+ closeExtlink = makeStringParser( ']' );
+ // this extlink MUST have inner contents, e.g. [foo] not allowed; [foo bar] [foo <i>bar</i>], etc. are allowed
function extlink() {
- var result = null;
- var parsedResult = sequence( [
+ var result, parsedResult;
+ result = null;
+ parsedResult = sequence( [
openExtlink,
nonWhitespaceExpression,
whitespace,
- expression,
+ nOrMore( 1, expression ),
closeExtlink
] );
if ( parsedResult !== null ) {
- result = [ 'LINK', parsedResult[1], parsedResult[3] ];
+ result = [ 'EXTLINK', parsedResult[1] ];
+ // TODO (mattflaschen, 2013-03-22): Clean this up if possible.
+ // It's avoiding CONCAT for single nodes, so they at least doesn't get the htmlEmitter span.
+ if ( parsedResult[3].length === 1 ) {
+ result.push( parsedResult[3][0] );
+ } else {
+ result.push( ['CONCAT'].concat( parsedResult[3] ) );
+ }
}
return result;
}
@@ -341,41 +514,212 @@
if ( result === null ) {
return null;
}
- return [ 'LINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ];
+ return [ 'EXTLINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ];
}
- var openLink = makeStringParser( '[[' );
- var closeLink = makeStringParser( ']]' );
- function link() {
- var result = null;
- var parsedResult = sequence( [
- openLink,
- expression,
- closeLink
+ openWikilink = makeStringParser( '[[' );
+ closeWikilink = makeStringParser( ']]' );
+ pipe = makeStringParser( '|' );
+
+ function template() {
+ var result = sequence( [
+ openTemplate,
+ templateContents,
+ closeTemplate
+ ] );
+ return result === null ? null : result[1];
+ }
+
+ wikilinkPage = choice( [
+ unescapedLiteralWithoutBar,
+ template
+ ] );
+
+ function pipedWikilink() {
+ var result = sequence( [
+ wikilinkPage,
+ pipe,
+ expression
+ ] );
+ return result === null ? null : [ result[0], result[2] ];
+ }
+
+ wikilinkContents = choice( [
+ pipedWikilink,
+ wikilinkPage // unpiped link
+ ] );
+
+ function wikilink() {
+ var result, parsedResult, parsedLinkContents;
+ result = null;
+
+ parsedResult = sequence( [
+ openWikilink,
+ wikilinkContents,
+ closeWikilink
] );
if ( parsedResult !== null ) {
- result = [ 'WLINK', parsedResult[1] ];
+ parsedLinkContents = parsedResult[1];
+ result = [ 'WIKILINK' ].concat( parsedLinkContents );
}
return result;
}
- var templateName = transform(
+
+ // TODO: Support data- if appropriate
+ function doubleQuotedHtmlAttributeValue() {
+ var parsedResult = sequence( [
+ doubleQuote,
+ htmlDoubleQuoteAttributeValue,
+ doubleQuote
+ ] );
+ return parsedResult === null ? null : parsedResult[1];
+ }
+
+ function singleQuotedHtmlAttributeValue() {
+ var parsedResult = sequence( [
+ singleQuote,
+ htmlSingleQuoteAttributeValue,
+ singleQuote
+ ] );
+ return parsedResult === null ? null : parsedResult[1];
+ }
+
+ function htmlAttribute() {
+ var parsedResult = sequence( [
+ whitespace,
+ asciiAlphabetLiteral,
+ htmlAttributeEquals,
+ choice( [
+ doubleQuotedHtmlAttributeValue,
+ singleQuotedHtmlAttributeValue
+ ] )
+ ] );
+ return parsedResult === null ? null : [parsedResult[1], parsedResult[3]];
+ }
+
+ /**
+ * Checks if HTML is allowed
+ *
+ * @param {string} startTagName HTML start tag name
+ * @param {string} endTagName HTML start tag name
+ * @param {Object} attributes array of consecutive key value pairs,
+ * with index 2 * n being a name and 2 * n + 1 the associated value
+ * @return {boolean} true if this is HTML is allowed, false otherwise
+ */
+ function isAllowedHtml( startTagName, endTagName, attributes ) {
+ var i, len, attributeName;
+
+ startTagName = startTagName.toLowerCase();
+ endTagName = endTagName.toLowerCase();
+ if ( startTagName !== endTagName || $.inArray( startTagName, settings.allowedHtmlElements ) === -1 ) {
+ return false;
+ }
+
+ for ( i = 0, len = attributes.length; i < len; i += 2 ) {
+ attributeName = attributes[i];
+ if ( $.inArray( attributeName, settings.allowedHtmlCommonAttributes ) === -1 &&
+ $.inArray( attributeName, settings.allowedHtmlAttributesByElement[startTagName] || [] ) === -1 ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ function htmlAttributes() {
+ var parsedResult = nOrMore( 0, htmlAttribute )();
+ // Un-nest attributes array due to structure of jQueryMsg operations (see emit).
+ return concat.apply( ['HTMLATTRIBUTES'], parsedResult );
+ }
+
+ // Subset of allowed HTML markup.
+ // Most elements and many attributes allowed on the server are not supported yet.
+ function html() {
+ var result = null, parsedOpenTagResult, parsedHtmlContents,
+ parsedCloseTagResult, wrappedAttributes, attributes,
+ startTagName, endTagName, startOpenTagPos, startCloseTagPos,
+ endOpenTagPos, endCloseTagPos;
+
+ // Break into three sequence calls. That should allow accurate reconstruction of the original HTML, and requiring an exact tag name match.
+ // 1. open through closeHtmlTag
+ // 2. expression
+ // 3. openHtmlEnd through close
+ // This will allow recording the positions to reconstruct if HTML is to be treated as text.
+
+ startOpenTagPos = pos;
+ parsedOpenTagResult = sequence( [
+ openHtmlStartTag,
+ asciiAlphabetLiteral,
+ htmlAttributes,
+ optionalForwardSlash,
+ closeHtmlTag
+ ] );
+
+ if ( parsedOpenTagResult === null ) {
+ return null;
+ }
+
+ endOpenTagPos = pos;
+ startTagName = parsedOpenTagResult[1];
+
+ parsedHtmlContents = nOrMore( 0, expression )();
+
+ startCloseTagPos = pos;
+ parsedCloseTagResult = sequence( [
+ openHtmlEndTag,
+ asciiAlphabetLiteral,
+ closeHtmlTag
+ ] );
+
+ if ( parsedCloseTagResult === null ) {
+ // Closing tag failed. Return the start tag and contents.
+ return [ 'CONCAT', input.substring( startOpenTagPos, endOpenTagPos ) ].concat( parsedHtmlContents );
+ }
+
+ endCloseTagPos = pos;
+ endTagName = parsedCloseTagResult[1];
+ wrappedAttributes = parsedOpenTagResult[2];
+ attributes = wrappedAttributes.slice( 1 );
+ if ( isAllowedHtml( startTagName, endTagName, attributes) ) {
+ result = [ 'HTMLELEMENT', startTagName, wrappedAttributes ].concat( parsedHtmlContents );
+ } else {
+ // HTML is not allowed, so contents will remain how
+ // it was, while HTML markup at this level will be
+ // treated as text
+ // E.g. assuming script tags are not allowed:
+ //
+ // <script>[[Foo|bar]]</script>
+ //
+ // results in '&lt;script&gt;' and '&lt;/script&gt;'
+ // (not treated as an HTML tag), surrounding a fully
+ // parsed HTML link.
+ //
+ // Concatenate everything from the tag, flattening the contents.
+ result = [ 'CONCAT', input.substring( startOpenTagPos, endOpenTagPos ) ].concat( parsedHtmlContents, input.substring( startCloseTagPos, endCloseTagPos ) );
+ }
+
+ return result;
+ }
+
+ templateName = transform(
// see $wgLegalTitleChars
// not allowing : due to the need to catch "PLURAL:$1"
makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+\-]+/ ),
function ( result ) { return result.toString(); }
);
function templateParam() {
- var result = sequence( [
+ var expr, result;
+ result = sequence( [
pipe,
nOrMore( 0, paramExpression )
] );
if ( result === null ) {
return null;
}
- var expr = result[1];
- // use a "CONCAT" operator if there are multiple nodes, otherwise return the first node, raw.
- return expr.length > 1 ? [ "CONCAT" ].concat( expr ) : expr[0];
+ expr = result[1];
+ // use a CONCAT operator if there are multiple nodes, otherwise return the first node, raw.
+ return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[0];
}
- var pipe = makeStringParser( '|' );
+
function templateWithReplacement() {
var result = sequence( [
templateName,
@@ -392,8 +736,8 @@
] );
return result === null ? null : [ result[0], result[2] ];
}
- var colon = makeStringParser(':');
- var templateContents = choice( [
+ colon = makeStringParser(':');
+ templateContents = choice( [
function () {
var res = sequence( [
// templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}}
@@ -414,59 +758,70 @@
return [ res[0] ].concat( res[1] );
}
] );
- var openTemplate = makeStringParser('{{');
- var closeTemplate = makeStringParser('}}');
- function template() {
- var result = sequence( [
- openTemplate,
- templateContents,
- closeTemplate
- ] );
- return result === null ? null : result[1];
- }
- var nonWhitespaceExpression = choice( [
+ openTemplate = makeStringParser('{{');
+ closeTemplate = makeStringParser('}}');
+ nonWhitespaceExpression = choice( [
template,
- link,
+ wikilink,
extLinkParam,
extlink,
replacement,
literalWithoutSpace
] );
- var paramExpression = choice( [
+ paramExpression = choice( [
template,
- link,
+ wikilink,
extLinkParam,
extlink,
replacement,
literalWithoutBar
] );
- var expression = choice( [
+
+ expression = choice( [
template,
- link,
+ wikilink,
extLinkParam,
extlink,
replacement,
+ html,
literal
] );
- function start() {
- var result = nOrMore( 0, expression )();
+
+ // Used when only {{-transformation is wanted, for 'text'
+ // or 'escaped' formats
+ curlyBraceTransformExpression = choice( [
+ template,
+ replacement,
+ curlyBraceTransformExpressionLiteral
+ ] );
+
+
+ /**
+ * Starts the parse
+ *
+ * @param {Function} rootExpression root parse function
+ */
+ function start( rootExpression ) {
+ var result = nOrMore( 0, rootExpression )();
if ( result === null ) {
return null;
}
- return [ "CONCAT" ].concat( result );
+ return [ 'CONCAT' ].concat( result );
}
// everything above this point is supposed to be stateless/static, but
// I am deferring the work of turning it into prototypes & objects. It's quite fast enough
// finally let's do some actual work...
- var result = start();
+
+ // If you add another possible rootExpression, you must update the astCache key scheme.
+ result = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression );
/*
* For success, the p must have gotten to the end of the input
* and returned a non-null.
* n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
*/
- if (result === null || pos !== input.length) {
- throw new Error( "Parse error at position " + pos.toString() + " in input: " + input );
+ if ( result === null || pos !== input.length ) {
+ throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + input );
}
return result;
}
@@ -491,18 +846,20 @@
* @return {Mixed} single-string node or array of nodes suitable for jQuery appending
*/
this.emit = function ( node, replacements ) {
- var ret = null;
- var jmsg = this;
+ var ret, subnodes, operation,
+ jmsg = this;
switch ( typeof node ) {
case 'string':
case 'number':
ret = node;
break;
- case 'object': // node is an array of nodes
- var subnodes = $.map( node.slice( 1 ), function ( n ) {
+ // typeof returns object for arrays
+ case 'object':
+ // node is an array of nodes
+ subnodes = $.map( node.slice( 1 ), function ( n ) {
return jmsg.emit( n, replacements );
} );
- var operation = node[0].toLowerCase();
+ operation = node[0].toLowerCase();
if ( typeof jmsg[operation] === 'function' ) {
ret = jmsg[ operation ]( subnodes, replacements );
} else {
@@ -540,11 +897,12 @@
$.each( nodes, function ( i, node ) {
if ( node instanceof jQuery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) {
$.each( node.contents(), function ( j, childNode ) {
- $span.append( childNode );
+ appendWithoutParsing( $span, childNode );
} );
} else {
- // strings, integers, anything else
- $span.append( node );
+ // Let jQuery append nodes, arrays of nodes and jQuery objects
+ // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings)
+ appendWithoutParsing( $span, node );
}
} );
return $span;
@@ -555,7 +913,7 @@
* Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ].
* if the specified parameter is not found return the same string
* (e.g. "$99" -> parameter 98 -> not found -> return "$99" )
- * TODO throw error if nodes.length > 1 ?
+ * TODO: Throw error if nodes.length > 1 ?
* @param {Array} of one element, integer, n >= 0
* @return {String} replacement
*/
@@ -563,13 +921,7 @@
var index = parseInt( nodes[0], 10 );
if ( index < replacements.length ) {
- if ( typeof arg === 'string' ) {
- // replacement is a string, escape it
- return mw.html.escape( replacements[index] );
- } else {
- // replacement is no string, don't touch!
- return replacements[index];
- }
+ return replacements[index];
} else {
// index not found, fallback to displaying variable
return '$' + ( index + 1 );
@@ -578,10 +930,71 @@
/**
* Transform wiki-link
- * TODO unimplemented
+ *
+ * TODO:
+ * It only handles basic cases, either no pipe, or a pipe with an explicit
+ * anchor.
+ *
+ * It does not attempt to handle features like the pipe trick.
+ * However, the pipe trick should usually not be present in wikitext retrieved
+ * from the server, since the replacement is done at save time.
+ * It may, though, if the wikitext appears in extension-controlled content.
+ *
+ * @param nodes
*/
- wlink: function ( nodes ) {
- return 'unimplemented';
+ wikilink: function ( nodes ) {
+ var page, anchor, url;
+
+ page = nodes[0];
+ url = mw.util.getUrl( page );
+
+ // [[Some Page]] or [[Namespace:Some Page]]
+ if ( nodes.length === 1 ) {
+ anchor = page;
+ }
+
+ /*
+ * [[Some Page|anchor text]] or
+ * [[Namespace:Some Page|anchor]
+ */
+ else {
+ anchor = nodes[1];
+ }
+
+ return $( '<a />' ).attr( {
+ title: page,
+ href: url
+ } ).text( anchor );
+ },
+
+ /**
+ * Converts array of HTML element key value pairs to object
+ *
+ * @param {Array} nodes array of consecutive key value pairs, with index 2 * n being a name and 2 * n + 1 the associated value
+ * @return {Object} object mapping attribute name to attribute value
+ */
+ htmlattributes: function ( nodes ) {
+ var i, len, mapping = {};
+ for ( i = 0, len = nodes.length; i < len; i += 2 ) {
+ mapping[nodes[i]] = decodePrimaryHtmlEntities( nodes[i + 1] );
+ }
+ return mapping;
+ },
+
+ /**
+ * Handles an (already-validated) HTML element.
+ *
+ * @param {Array} nodes nodes to process when creating element
+ * @return {jQuery|Array} jQuery node for valid HTML or array for disallowed element
+ */
+ htmlelement: function ( nodes ) {
+ var tagName, attributes, contents, $element;
+
+ tagName = nodes.shift();
+ attributes = nodes.shift();
+ contents = nodes;
+ $element = $( document.createElement( tagName ) ).attr( attributes );
+ return appendWithoutParsing( $element, contents );
},
/**
@@ -593,10 +1006,10 @@
* @param {Array} of two elements, {jQuery|Function|String} and {String}
* @return {jQuery}
*/
- link: function ( nodes ) {
- var arg = nodes[0];
- var contents = nodes[1];
- var $el;
+ extlink: function ( nodes ) {
+ var $el,
+ arg = nodes[0],
+ contents = nodes[1];
if ( arg instanceof jQuery ) {
$el = arg;
} else {
@@ -607,12 +1020,11 @@
$el.attr( 'href', arg.toString() );
}
}
- $el.append( contents );
- return $el;
+ return appendWithoutParsing( $el, contents );
},
/**
- * This is basically use a combination of replace + link (link with parameter
+ * This is basically use a combination of replace + external link (link with parameter
* as url), but we don't want to run the regular replace here-on: inserting a
* url as href-attribute of a link will automatically escape it already, so
* we don't want replace to (manually) escape it as well.
@@ -620,7 +1032,7 @@
* @param {Array} of one element, integer, n >= 0
* @return {String} replacement
*/
- linkparam: function ( nodes, replacements ) {
+ extlinkparam: function ( nodes, replacements ) {
var replacement,
index = parseInt( nodes[0], 10 );
if ( index < replacements.length) {
@@ -628,7 +1040,7 @@
} else {
replacement = '$' + ( index + 1 );
}
- return this.link( [ replacement, nodes[1] ] );
+ return this.extlink( [ replacement, nodes[1] ] );
},
/**
@@ -639,25 +1051,32 @@
* @return {String} selected pluralized form according to current language
*/
plural: function ( nodes ) {
- var count = parseFloat( this.language.convertNumber( nodes[0], true ) );
- var forms = nodes.slice(1);
+ var forms, count;
+ count = parseFloat( this.language.convertNumber( nodes[0], true ) );
+ forms = nodes.slice(1);
return forms.length ? this.language.convertPlural( count, forms ) : '';
},
/**
- * Transform parsed structure into gender
- * Usage {{gender:[gender| mw.user object ] | masculine|feminine|neutral}}.
- * @param {Array} of nodes, [ {String|mw.User}, {String}, {String} , {String} ]
+ * Transform parsed structure according to gender.
+ * Usage {{gender:[ gender | mw.user object ] | masculine form|feminine form|neutral form}}.
+ * The first node is either a string, which can be "male" or "female",
+ * or a User object (not a username).
+ *
+ * @param {Array} of nodes, [ {String|mw.User}, {String}, {String}, {String} ]
* @return {String} selected gender form according to current language
*/
gender: function ( nodes ) {
- var gender;
- if ( nodes[0] && nodes[0].options instanceof mw.Map ){
+ var gender, forms;
+
+ if ( nodes[0] && nodes[0].options instanceof mw.Map ) {
gender = nodes[0].options.get( 'gender' );
} else {
gender = nodes[0];
}
- var forms = nodes.slice(1);
+
+ forms = nodes.slice( 1 );
+
return this.language.gender( gender, forms );
},
@@ -668,9 +1087,33 @@
* @return {String} selected grammatical form according to current language
*/
grammar: function ( nodes ) {
- var form = nodes[0];
- var word = nodes[1];
+ var form = nodes[0],
+ word = nodes[1];
return word && form && this.language.convertGrammar( word, form );
+ },
+
+ /**
+ * Tranform parsed structure into a int: (interface language) message include
+ * Invoked by putting {{int:othermessage}} into a message
+ * @param {Array} of nodes
+ * @return {string} Other message
+ */
+ int: function ( nodes ) {
+ return mw.jqueryMsg.getMessageFunction()( nodes[0].toLowerCase() );
+ },
+
+ /**
+ * Takes an unformatted number (arab, no group separators and . as decimal separator)
+ * and outputs it in the localized digit script and formatted with decimal
+ * separator, according to the current language
+ * @param {Array} of nodes
+ * @return {Number|String} formatted number
+ */
+ formatnum: function ( nodes ) {
+ var isInteger = ( nodes[1] && nodes[1] === 'R' ) ? true : false,
+ number = nodes[0];
+
+ return this.language.convertNumber( number, isInteger );
}
};
// Deprecated! don't rely on gM existing.
@@ -681,17 +1124,24 @@
$.fn.msg = mw.jqueryMsg.getPlugin();
// Replace the default message parser with jqueryMsg
- var oldParser = mw.Message.prototype.parser;
+ oldParser = mw.Message.prototype.parser;
mw.Message.prototype.parser = function () {
+ var messageFunction;
+
// TODO: should we cache the message function so we don't create a new one every time? Benchmark this maybe?
// Caching is somewhat problematic, because we do need different message functions for different maps, so
// we'd have to cache the parser as a member of this.map, which sounds a bit ugly.
// Do not use mw.jqueryMsg unless required
- if ( this.map.get( this.key ).indexOf( '{{' ) < 0 ) {
+ if ( this.format === 'plain' || !/\{\{|[\[<>]/.test(this.map.get( this.key ) ) ) {
// Fall back to mw.msg's simple parser
return oldParser.apply( this );
}
- var messageFunction = mw.jqueryMsg.getMessageFunction( { 'messages': this.map } );
+
+ messageFunction = mw.jqueryMsg.getMessageFunction( {
+ 'messages': this.map,
+ // For format 'escaped', escaping part is handled by mediawiki.js
+ 'format': this.format
+ } );
return messageFunction( this.key, this.parameters );
};
diff --git a/resources/mediawiki/mediawiki.jqueryMsg.peg b/resources/mediawiki/mediawiki.jqueryMsg.peg
index e059ed1d..7879d6fa 100644
--- a/resources/mediawiki/mediawiki.jqueryMsg.peg
+++ b/resources/mediawiki/mediawiki.jqueryMsg.peg
@@ -37,6 +37,7 @@ templateParam
templateName
= tn:[A-Za-z_]+ { return tn.join('').toUpperCase() }
+/* TODO: Update to reflect separate piped and unpiped handling */
link
= "[[" w:expression "]]" { return [ 'WLINK', w ]; }
diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js
index 19112aed..80223e5d 100644
--- a/resources/mediawiki/mediawiki.js
+++ b/resources/mediawiki/mediawiki.js
@@ -1,26 +1,81 @@
-/*
- * Core MediaWiki JavaScript Library
+/**
+ * Base library for MediaWiki.
+ *
+ * @class mw
+ * @alternateClassName mediaWiki
+ * @singleton
*/
-/*global mw:true */
+
var mw = ( function ( $, undefined ) {
- "use strict";
+ 'use strict';
/* Private Members */
var hasOwn = Object.prototype.hasOwnProperty,
slice = Array.prototype.slice;
+ /**
+ * Log a message to window.console, if possible. Useful to force logging of some
+ * errors that are otherwise hard to detect (I.e., this logs also in production mode).
+ * Gets console references in each invocation, so that delayed debugging tools work
+ * fine. No need for optimization here, which would only result in losing logs.
+ *
+ * @private
+ * @param {string} msg text for the log entry.
+ * @param {Error} [e]
+ */
+ function log( msg, e ) {
+ var console = window.console;
+ if ( console && console.log ) {
+ console.log( msg );
+ // If we have an exception object, log it through .error() to trigger
+ // proper stacktraces in browsers that support it. There are no (known)
+ // browsers that don't support .error(), that do support .log() and
+ // have useful exception handling through .log().
+ if ( e && console.error ) {
+ console.error( String( e ), e );
+ }
+ }
+ }
+
/* Object constructors */
/**
- * Map
- *
* Creates an object that can be read from or written to from prototype functions
* that allow both single and multiple variables at once.
*
- * @param global boolean Whether to store the values in the global window
+ * @example
+ *
+ * var addies, wanted, results;
+ *
+ * // Create your address book
+ * addies = new mw.Map();
+ *
+ * // This data could be coming from an external source (eg. API/AJAX)
+ * addies.set( {
+ * 'John Doe' : '10 Wall Street, New York, USA',
+ * 'Jane Jackson' : '21 Oxford St, London, UK',
+ * 'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL'
+ * } );
+ *
+ * wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson'];
+ *
+ * // You can detect missing keys first
+ * if ( !addies.exists( wanted ) ) {
+ * // One or more are missing (in this case: "George Johnson")
+ * mw.log( 'One or more names were not found in your address book' );
+ * }
+ *
+ * // Or just let it give you what it can
+ * results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' );
+ * mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK"
+ * mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US"
+ *
+ * @class mw.Map
+ *
+ * @constructor
+ * @param {boolean} [global=false] Whether to store the values in the global window
* object or a exclusively in the object property 'values'.
- * @return Map
*/
function Map( global ) {
this.values = global === true ? window : {};
@@ -33,32 +88,32 @@ var mw = ( function ( $, undefined ) {
*
* If called with no arguments, all values will be returned.
*
- * @param selection mixed String key or array of keys to get values for.
- * @param fallback mixed Value to use in case key(s) do not exist (optional).
+ * @param {string|Array} selection String key or array of keys to get values for.
+ * @param {Mixed} [fallback] Value to use in case key(s) do not exist.
* @return mixed If selection was a string returns the value or null,
* If selection was an array, returns an object of key/values (value is null if not found),
* If selection was not passed or invalid, will return the 'values' object member (be careful as
* objects are always passed by reference in JavaScript!).
- * @return Values as a string or object, null if invalid/inexistant.
+ * @return {string|Object|null} Values as a string or object, null if invalid/inexistant.
*/
get: function ( selection, fallback ) {
var results, i;
+ // If we only do this in the `return` block, it'll fail for the
+ // call to get() from the mutli-selection block.
+ fallback = arguments.length > 1 ? fallback : null;
if ( $.isArray( selection ) ) {
selection = slice.call( selection );
results = {};
- for ( i = 0; i < selection.length; i += 1 ) {
+ for ( i = 0; i < selection.length; i++ ) {
results[selection[i]] = this.get( selection[i], fallback );
}
return results;
}
if ( typeof selection === 'string' ) {
- if ( this.values[selection] === undefined ) {
- if ( fallback !== undefined ) {
- return fallback;
- }
- return null;
+ if ( !hasOwn.call( this.values, selection ) ) {
+ return fallback;
}
return this.values[selection];
}
@@ -74,8 +129,8 @@ var mw = ( function ( $, undefined ) {
/**
* Sets one or multiple key/value pairs.
*
- * @param selection {mixed} String key or array of keys to set values for.
- * @param value {mixed} Value to set (optional, only in use when key is a string)
+ * @param {string|Object} selection String key to set value for, or object mapping keys to values.
+ * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
* @return {Boolean} This returns true on success, false on failure.
*/
set: function ( selection, value ) {
@@ -87,7 +142,7 @@ var mw = ( function ( $, undefined ) {
}
return true;
}
- if ( typeof selection === 'string' && value !== undefined ) {
+ if ( typeof selection === 'string' && arguments.length > 1 ) {
this.values[selection] = value;
return true;
}
@@ -97,37 +152,40 @@ var mw = ( function ( $, undefined ) {
/**
* Checks if one or multiple keys exist.
*
- * @param selection {mixed} String key or array of keys to check
- * @return {Boolean} Existence of key(s)
+ * @param {Mixed} selection String key or array of keys to check
+ * @return {boolean} Existence of key(s)
*/
exists: function ( selection ) {
var s;
if ( $.isArray( selection ) ) {
- for ( s = 0; s < selection.length; s += 1 ) {
- if ( this.values[selection[s]] === undefined ) {
+ for ( s = 0; s < selection.length; s++ ) {
+ if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) {
return false;
}
}
return true;
}
- return this.values[selection] !== undefined;
+ return typeof selection === 'string' && hasOwn.call( this.values, selection );
}
};
/**
- * Message
+ * Object constructor for messages.
+ *
+ * Similar to the Message class in MediaWiki PHP.
+ *
+ * Format defaults to 'text'.
*
- * Object constructor for messages,
- * similar to the Message class in MediaWiki PHP.
+ * @class mw.Message
*
- * @param map Map Instance of mw.Map
- * @param key String
- * @param parameters Array
- * @return Message
+ * @constructor
+ * @param {mw.Map} map Message storage
+ * @param {string} key
+ * @param {Array} [parameters]
*/
function Message( map, key, parameters ) {
- this.format = 'plain';
+ this.format = 'text';
this.map = map;
this.key = key;
this.parameters = parameters === undefined ? [] : slice.call( parameters );
@@ -137,8 +195,11 @@ var mw = ( function ( $, undefined ) {
Message.prototype = {
/**
* Simple message parser, does $N replacement and nothing else.
+ *
* This may be overridden to provide a more complex message parser.
*
+ * The primary override is in mediawiki.jqueryMsg.
+ *
* This function will not be called for nonexistent messages.
*/
parser: function () {
@@ -152,8 +213,8 @@ var mw = ( function ( $, undefined ) {
/**
* Appends (does not replace) parameters for replacement to the .parameters property.
*
- * @param parameters Array
- * @return Message
+ * @param {Array} parameters
+ * @chainable
*/
params: function ( parameters ) {
var i;
@@ -166,25 +227,21 @@ var mw = ( function ( $, undefined ) {
/**
* Converts message object to it's string form based on the state of format.
*
- * @return string Message as a string in the current form or <key> if key does not exist.
+ * @return {string} Message as a string in the current form or `<key>` if key does not exist.
*/
toString: function () {
var text;
if ( !this.exists() ) {
// Use <key> as text if key does not exist
- if ( this.format !== 'plain' ) {
- // format 'escape' and 'parse' need to have the brackets and key html escaped
+ if ( this.format === 'escaped' || this.format === 'parse' ) {
+ // format 'escaped' and 'parse' need to have the brackets and key html escaped
return mw.html.escape( '<' + this.key + '>' );
}
return '<' + this.key + '>';
}
- if ( this.format === 'plain' ) {
- // @todo FIXME: Although not applicable to core Message,
- // Plugins like jQueryMsg should be able to distinguish
- // between 'plain' (only variable replacement and plural/gender)
- // and actually parsing wikitext to HTML.
+ if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
text = this.parser();
}
@@ -193,15 +250,16 @@ var mw = ( function ( $, undefined ) {
text = mw.html.escape( text );
}
- if ( this.format === 'parse' ) {
- text = this.parser();
- }
-
return text;
},
/**
- * Changes format to parse and converts message to string
+ * Changes format to 'parse' and converts message to string
+ *
+ * If jqueryMsg is loaded, this parses the message text from wikitext
+ * (where supported) to HTML
+ *
+ * Otherwise, it is equivalent to plain.
*
* @return {string} String form of parsed message
*/
@@ -211,7 +269,10 @@ var mw = ( function ( $, undefined ) {
},
/**
- * Changes format to plain and converts message to string
+ * Changes format to 'plain' and converts message to string
+ *
+ * This substitutes parameters, but otherwise does not change the
+ * message text.
*
* @return {string} String form of plain message
*/
@@ -221,7 +282,23 @@ var mw = ( function ( $, undefined ) {
},
/**
- * Changes the format to html escaped and converts message to string
+ * Changes format to 'text' and converts message to string
+ *
+ * If jqueryMsg is loaded, {{-transformation is done where supported
+ * (such as {{plural:}}, {{gender:}}, {{int:}}).
+ *
+ * Otherwise, it is equivalent to plain.
+ */
+ text: function () {
+ this.format = 'text';
+ return this.toString();
+ },
+
+ /**
+ * Changes the format to 'escaped' and converts message to string
+ *
+ * This is equivalent to using the 'text' format (see text method), then
+ * HTML-escaping the output.
*
* @return {string} String form of html escaped message
*/
@@ -233,7 +310,8 @@ var mw = ( function ( $, undefined ) {
/**
* Checks if message exists
*
- * @return {string} String form of parsed message
+ * @see mw.Map#exists
+ * @return {boolean}
*/
exists: function () {
return this.map.exists( this.key );
@@ -244,82 +322,98 @@ var mw = ( function ( $, undefined ) {
/* Public Members */
/**
- * Dummy function which in debug mode can be replaced with a function that
- * emulates console.log in console-less environments.
+ * Dummy placeholder for {@link mw.log}
+ * @method
*/
- log: function () { },
+ log: ( function () {
+ var log = function () {};
+ log.warn = function () {};
+ log.deprecate = function ( obj, key, val ) {
+ obj[key] = val;
+ };
+ return log;
+ }() ),
- /**
- * @var constructor Make the Map constructor publicly available.
- */
+ // Make the Map constructor publicly available.
Map: Map,
- /**
- * @var constructor Make the Message constructor publicly available.
- */
+ // Make the Message constructor publicly available.
Message: Message,
/**
- * List of configuration values
+ * Map of configuration values
+ *
+ * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
+ * on MediaWiki.org.
*
- * Dummy placeholder. Initiated in startUp module as a new instance of mw.Map().
- * If $wgLegacyJavaScriptGlobals is true, this Map will have its values
- * in the global window object.
+ * If `$wgLegacyJavaScriptGlobals` is true, this Map will put its values in the
+ * global window object.
+ *
+ * @property {mw.Map} config
*/
+ // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule with an instance of `mw.Map`.
config: null,
/**
- * @var object
- *
* Empty object that plugins can be installed in.
+ * @property
*/
libs: {},
- /* Extension points */
-
+ /**
+ * Access container for deprecated functionality that can be moved from
+ * from their legacy location and attached to this object (e.g. a global
+ * function that is deprecated and as stop-gap can be exposed through here).
+ *
+ * This was reserved for future use but never ended up being used.
+ *
+ * @deprecated since 1.22: Let deprecated identifiers keep their original name
+ * and use mw.log#deprecate to create an access container for tracking.
+ * @property
+ */
legacy: {},
/**
* Localization system
+ * @property {mw.Map}
*/
messages: new Map(),
/* Public Methods */
/**
- * Gets a message object, similar to wfMessage()
+ * Get a message object.
+ *
+ * Similar to wfMessage() in MediaWiki PHP.
*
- * @param key string Key of message to get
- * @param parameter_1 mixed First argument in a list of variadic arguments,
- * each a parameter for $N replacement in messages.
- * @return Message
+ * @param {string} key Key of message to get
+ * @param {Mixed...} parameters Parameters for the $N replacements in messages.
+ * @return {mw.Message}
*/
- message: function ( key, parameter_1 /* [, parameter_2] */ ) {
- var parameters;
- // Support variadic arguments
- if ( parameter_1 !== undefined ) {
- parameters = slice.call( arguments );
- parameters.shift();
- } else {
- parameters = [];
- }
+ message: function ( key ) {
+ // Variadic arguments
+ var parameters = slice.call( arguments, 1 );
return new Message( mw.messages, key, parameters );
},
/**
- * Gets a message string, similar to wfMessage()
+ * Get a message string using 'text' format.
*
- * @param key string Key of message to get
- * @param parameters mixed First argument in a list of variadic arguments,
- * each a parameter for $N replacement in messages.
- * @return String.
+ * Similar to wfMsg() in MediaWiki PHP.
+ *
+ * @see mw.Message
+ * @param {string} key Key of message to get
+ * @param {Mixed...} parameters Parameters for the $N replacements in messages.
+ * @return {string}
*/
- msg: function ( /* key, parameter_1, parameter_2, .. */ ) {
+ msg: function () {
return mw.message.apply( mw.message, arguments ).toString();
},
/**
* Client-side module loader which integrates with the MediaWiki ResourceLoader
+ * @class mw.loader
+ * @singleton
*/
loader: ( function () {
@@ -338,29 +432,32 @@ var mw = ( function ( $, undefined ) {
* mw.loader.implement.
*
* Format:
- * {
- * 'moduleName': {
- * 'version': ############## (unix timestamp),
- * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
- * 'group': 'somegroup', (or) null,
- * 'source': 'local', 'someforeignwiki', (or) null
- * 'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing'
- * 'script': ...,
- * 'style': ...,
- * 'messages': { 'key': 'value' },
- * }
- * }
+ * {
+ * 'moduleName': {
+ * 'version': ############## (unix timestamp),
+ * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
+ * 'group': 'somegroup', (or) null,
+ * 'source': 'local', 'someforeignwiki', (or) null
+ * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
+ * 'script': ...,
+ * 'style': ...,
+ * 'messages': { 'key': 'value' },
+ * }
+ * }
+ *
+ * @property
+ * @private
*/
var registry = {},
- /**
- * Mapping of sources, keyed by source-id, values are objects.
- * Format:
- * {
- * 'sourceId': {
- * 'loadScript': 'http://foo.bar/w/load.php'
- * }
- * }
- */
+ //
+ // Mapping of sources, keyed by source-id, values are objects.
+ // Format:
+ // {
+ // 'sourceId': {
+ // 'loadScript': 'http://foo.bar/w/load.php'
+ // }
+ // }
+ //
sources = {},
// List of modules which will be loaded as when ready
batch = [],
@@ -369,7 +466,11 @@ var mw = ( function ( $, undefined ) {
// List of callback functions waiting for modules to be ready to be called
jobs = [],
// Selector cache for the marker element. Use getMarker() to get/use the marker!
- $marker = null;
+ $marker = null,
+ // Buffer for addEmbeddedCSS.
+ cssBuffer = '',
+ // Callbacks for addEmbeddedCSS.
+ cssCallbacks = $.Callbacks();
/* Private methods */
@@ -392,12 +493,13 @@ var mw = ( function ( $, undefined ) {
/**
* Create a new style tag and add it to the DOM.
*
- * @param text String: CSS text
- * @param nextnode mixed: [optional] An Element or jQuery object for an element where
- * the style tag should be inserted before. Otherwise appended to the <head>.
- * @return HTMLStyleElement
+ * @private
+ * @param {string} text CSS text
+ * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be
+ * inserted before. Otherwise it will be appended to `<head>`.
+ * @return {HTMLElement} Reference to the created `<style>` element.
*/
- function addStyleTag( text, nextnode ) {
+ function newStyleTag( text, nextnode ) {
var s = document.createElement( 'style' );
// Insert into document before setting cssText (bug 33305)
if ( nextnode ) {
@@ -429,76 +531,112 @@ var mw = ( function ( $, undefined ) {
}
/**
- * Checks if certain cssText is safe to append to
- * a stylesheet.
+ * Checks whether it is safe to add this css to a stylesheet.
*
- * Right now it only makes sure that cssText containing @import
- * rules will end up in a new stylesheet (as those only work when
- * placed at the start of a stylesheet; bug 35562).
- * This could later be extended to take care of other bugs, such as
- * the IE cssRules limit - not the same as the IE styleSheets limit).
+ * @private
+ * @param {string} cssText
+ * @return {boolean} False if a new one must be created.
*/
- function canExpandStylesheetWith( $style, cssText ) {
+ function canExpandStylesheetWith( cssText ) {
+ // Makes sure that cssText containing `@import`
+ // rules will end up in a new stylesheet (as those only work when
+ // placed at the start of a stylesheet; bug 35562).
return cssText.indexOf( '@import' ) === -1;
}
- function addEmbeddedCSS( cssText ) {
+ /**
+ * Add a bit of CSS text to the current browser page.
+ *
+ * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
+ * or create a new one based on whether the given `cssText` is safe for extension.
+ *
+ * @param {string} [cssText=cssBuffer] If called without cssText,
+ * the internal buffer will be inserted instead.
+ * @param {Function} [callback]
+ */
+ function addEmbeddedCSS( cssText, callback ) {
var $style, styleEl;
- $style = getMarker().prev();
- // Re-use <style> tags if possible, this to try to stay
- // under the IE stylesheet limit (bug 31676).
- // Also verify that the the element before Marker actually is one
- // that came from ResourceLoader, and not a style tag that some
- // other script inserted before our marker, or, more importantly,
- // it may not be a style tag at all (could be <meta> or <script>).
- if (
- $style.data( 'ResourceLoaderDynamicStyleTag' ) === true &&
- canExpandStylesheetWith( $style, cssText )
- ) {
- // There's already a dynamic <style> tag present and
- // canExpandStylesheetWith() gave a green light to append more to it.
- styleEl = $style.get( 0 );
- if ( styleEl.styleSheet ) {
- try {
- styleEl.styleSheet.cssText += cssText; // IE
- } catch ( e ) {
- log( 'addEmbeddedCSS fail\ne.message: ' + e.message, e );
- }
- } else {
- styleEl.appendChild( document.createTextNode( String( cssText ) ) );
+
+ if ( callback ) {
+ cssCallbacks.add( callback );
+ }
+
+ // Yield once before inserting the <style> tag. There are likely
+ // more calls coming up which we can combine this way.
+ // Appending a stylesheet and waiting for the browser to repaint
+ // is fairly expensive, this reduces it (bug 45810)
+ if ( cssText ) {
+ // Be careful not to extend the buffer with css that needs a new stylesheet
+ if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
+ // Linebreak for somewhat distinguishable sections
+ // (the rl-cachekey comment separating each)
+ cssBuffer += '\n' + cssText;
+ // TODO: Use requestAnimationFrame in the future which will
+ // perform even better by not injecting styles while the browser
+ // is paiting.
+ setTimeout( function () {
+ // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
+ // (below version 13) has the non-standard behaviour of passing a
+ // numerical "lateness" value as first argument to this callback
+ // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
+ addEmbeddedCSS();
+ } );
+ return;
}
+
+ // This is a delayed call and we got a buffer still
+ } else if ( cssBuffer ) {
+ cssText = cssBuffer;
+ cssBuffer = '';
} else {
- $( addStyleTag( cssText, getMarker() ) )
- .data( 'ResourceLoaderDynamicStyleTag', true );
+ // This is a delayed call, but buffer is already cleared by
+ // another delayed call.
+ return;
}
- }
- function compare( a, b ) {
- var i;
- if ( a.length !== b.length ) {
- return false;
- }
- for ( i = 0; i < b.length; i += 1 ) {
- if ( $.isArray( a[i] ) ) {
- if ( !compare( a[i], b[i] ) ) {
- return false;
+ // By default, always create a new <style>. Appending text
+ // to a <style> tag means the contents have to be re-parsed (bug 45810).
+ // Except, of course, in IE below 9, in there we default to
+ // re-using and appending to a <style> tag due to the
+ // IE stylesheet limit (bug 31676).
+ if ( 'documentMode' in document && document.documentMode <= 9 ) {
+
+ $style = getMarker().prev();
+ // Verify that the the element before Marker actually is a
+ // <style> tag and one that came from ResourceLoader
+ // (not some other style tag or even a `<meta>` or `<script>`).
+ if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
+ // There's already a dynamic <style> tag present and
+ // canExpandStylesheetWith() gave a green light to append more to it.
+ styleEl = $style.get( 0 );
+ if ( styleEl.styleSheet ) {
+ try {
+ styleEl.styleSheet.cssText += cssText; // IE
+ } catch ( e ) {
+ log( 'addEmbeddedCSS fail', e );
+ }
+ } else {
+ styleEl.appendChild( document.createTextNode( String( cssText ) ) );
}
- }
- if ( a[i] !== b[i] ) {
- return false;
+ cssCallbacks.fire().empty();
+ return;
}
}
- return true;
+
+ $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
+
+ cssCallbacks.fire().empty();
}
/**
* Generates an ISO8601 "basic" string from a UNIX timestamp
+ * @private
*/
function formatVersionNumber( timestamp ) {
- var pad = function ( a, b, c ) {
- return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
- },
- d = new Date();
+ var d = new Date();
+ function pad( a, b, c ) {
+ return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
+ }
d.setTime( timestamp * 1000 );
return [
pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
@@ -509,15 +647,16 @@ var mw = ( function ( $, undefined ) {
/**
* Resolves dependencies and detects circular references.
*
- * @param module String Name of the top-level module whose dependencies shall be
+ * @private
+ * @param {string} module Name of the top-level module whose dependencies shall be
* resolved and sorted.
- * @param resolved Array Returns a topological sort of the given module and its
+ * @param {Array} resolved Returns a topological sort of the given module and its
* dependencies, such that later modules depend on earlier modules. The array
* contains the module names. If the array contains already some module names,
* this function appends its result to the pre-existing array.
- * @param unresolved Object [optional] Hash used to track the current dependency
+ * @param {Object} [unresolved] Hash used to track the current dependency
* chain; used to report loops in the dependency graph.
- * @throws Error if any unregistered module or a dependency loop is encountered
+ * @throws {Error} If any unregistered module or a dependency loop is encountered
*/
function sortDependencies( module, resolved, unresolved ) {
var n, deps, len;
@@ -566,9 +705,10 @@ var mw = ( function ( $, undefined ) {
* Gets a list of module names that a module depends on in their proper dependency
* order.
*
- * @param module string module name or array of string module names
- * @return list of dependencies, including 'module'.
- * @throws Error if circular reference is detected
+ * @private
+ * @param {string} module Module name or array of string module names
+ * @return {Array} list of dependencies, including 'module'.
+ * @throws {Error} If circular reference is detected
*/
function resolve( module ) {
var m, resolved;
@@ -597,10 +737,11 @@ var mw = ( function ( $, undefined ) {
* One can also filter for 'unregistered', which will return the
* modules names that don't have a registry entry.
*
- * @param states string or array of strings of module states to filter by
- * @param modules array list of module names to filter (optional, by default the entire
+ * @private
+ * @param {string|string[]} states Module states to filter by
+ * @param {Array} [modules] List of module names to filter (optional, by default the entire
* registry is used)
- * @return array list of filtered module names
+ * @return {Array} List of filtered module names
*/
function filter( states, modules ) {
var list, module, s, m;
@@ -642,44 +783,22 @@ var mw = ( function ( $, undefined ) {
* Determine whether all dependencies are in state 'ready', which means we may
* execute the module or job now.
*
- * @param dependencies Array dependencies (module names) to be checked.
- *
- * @return Boolean true if all dependencies are in state 'ready', false otherwise
+ * @private
+ * @param {Array} dependencies Dependencies (module names) to be checked.
+ * @return {boolean} True if all dependencies are in state 'ready', false otherwise
*/
function allReady( dependencies ) {
return filter( 'ready', dependencies ).length === dependencies.length;
}
/**
- * Log a message to window.console, if possible. Useful to force logging of some
- * errors that are otherwise hard to detect (I.e., this logs also in production mode).
- * Gets console references in each invocation, so that delayed debugging tools work
- * fine. No need for optimization here, which would only result in losing logs.
- *
- * @param msg String text for the log entry.
- * @param e Error [optional] to also log.
- */
- function log( msg, e ) {
- var console = window.console;
- if ( console && console.log ) {
- console.log( msg );
- // If we have an exception object, log it through .error() to trigger
- // proper stacktraces in browsers that support it. There are no (known)
- // browsers that don't support .error(), that do support .log() and
- // have useful exception handling through .log().
- if ( e && console.error ) {
- console.error( e );
- }
- }
- }
-
- /**
* A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
* and modules that depend upon this module. if the given module failed, propagate the 'error'
* state up the dependency tree; otherwise, execute all jobs/modules that now have all their
* dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
*
- * @param module String name of module that entered one of the states 'ready', 'error', or 'missing'.
+ * @private
+ * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
*/
function handlePending( module ) {
var j, job, hasErrors, m, stateChange;
@@ -712,22 +831,18 @@ var mw = ( function ( $, undefined ) {
j -= 1;
try {
if ( hasErrors ) {
- throw new Error ("Module " + module + " failed.");
+ if ( $.isFunction( job.error ) ) {
+ job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] );
+ }
} else {
if ( $.isFunction( job.ready ) ) {
job.ready();
}
}
} catch ( e ) {
- if ( $.isFunction( job.error ) ) {
- try {
- job.error( e, [module] );
- } catch ( ex ) {
- // A user-defined operation raised an exception. Swallow to protect
- // our state machine!
- log( 'Exception thrown by job.error()', ex );
- }
- }
+ // A user-defined callback raised an exception.
+ // Swallow it to protect our state machine!
+ log( 'Exception thrown by job.error', e );
}
}
}
@@ -747,27 +862,32 @@ var mw = ( function ( $, undefined ) {
* Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
* depending on whether document-ready has occurred yet and whether we are in async mode.
*
- * @param src String: URL to script, will be used as the src attribute in the script tag
- * @param callback Function: Optional callback which will be run when the script is done
+ * @private
+ * @param {string} src URL to script, will be used as the src attribute in the script tag
+ * @param {Function} [callback] Callback which will be run when the script is done
*/
function addScript( src, callback, async ) {
/*jshint evil:true */
- var script, head,
- done = false;
+ var script, head, done;
// Using isReady directly instead of storing it locally from
// a $.fn.ready callback (bug 31895).
if ( $.isReady || async ) {
- // jQuery's getScript method is NOT better than doing this the old-fashioned way
- // because jQuery will eval the script's code, and errors will not have sane
- // line numbers.
+ // Can't use jQuery.getScript because that only uses <script> for cross-domain,
+ // it uses XHR and eval for same-domain scripts, which we don't want because it
+ // messes up line numbers.
+ // The below is based on jQuery ([jquery@1.8.2]/src/ajax/script.js)
+
+ // IE-safe way of getting the <head>. document.head isn't supported
+ // in old IE, and doesn't work when in the <head>.
+ done = false;
+ head = document.getElementsByTagName( 'head' )[0] || document.body;
+
script = document.createElement( 'script' );
- script.setAttribute( 'src', src );
- script.setAttribute( 'type', 'text/javascript' );
+ script.async = true;
+ script.src = src;
if ( $.isFunction( callback ) ) {
- // Attach handlers for all browsers (based on jQuery.ajax)
script.onload = script.onreadystatechange = function () {
-
if (
!done
&& (
@@ -775,24 +895,20 @@ var mw = ( function ( $, undefined ) {
|| /loaded|complete/.test( script.readyState )
)
) {
-
done = true;
- callback();
+ // Handle memory leak in IE
+ script.onload = script.onreadystatechange = null;
- // Handle memory leak in IE. This seems to fail in
- // IE7 sometimes (Permission Denied error when
- // accessing script.parentNode) so wrap it in
- // a try catch.
- try {
- script.onload = script.onreadystatechange = null;
- if ( script.parentNode ) {
- script.parentNode.removeChild( script );
- }
-
- // Dereference the script
- script = undefined;
- } catch ( e ) { }
+ // Detach the element from the document
+ if ( script.parentNode ) {
+ script.parentNode.removeChild( script );
+ }
+
+ // Dereference the element from javascript
+ script = undefined;
+
+ callback();
}
};
}
@@ -800,20 +916,17 @@ var mw = ( function ( $, undefined ) {
if ( window.opera ) {
// Appending to the <head> blocks rendering completely in Opera,
// so append to the <body> after document ready. This means the
- // scripts only start loading after the document has been rendered,
+ // scripts only start loading after the document has been rendered,
// but so be it. Opera users don't deserve faster web pages if their
- // browser makes it impossible
- $( function () { document.body.appendChild( script ); } );
+ // browser makes it impossible.
+ $( function () {
+ document.body.appendChild( script );
+ } );
} else {
- // IE-safe way of getting the <head> . document.documentElement.head doesn't
- // work in scripts that run in the <head>
- head = document.getElementsByTagName( 'head' )[0];
- ( document.body || head ).appendChild( script );
+ head.appendChild( script );
}
} else {
- document.write( mw.html.element(
- 'script', { 'type': 'text/javascript', 'src': src }, ''
- ) );
+ document.write( mw.html.element( 'script', { 'src': src }, '' ) );
if ( $.isFunction( callback ) ) {
// Document.write is synchronous, so this is called when it's done
// FIXME: that's a lie. doc.write isn't actually synchronous
@@ -825,10 +938,12 @@ var mw = ( function ( $, undefined ) {
/**
* Executes a loaded module, making it ready to use
*
- * @param module string module name to execute
+ * @private
+ * @param {string} module Module name to execute
*/
function execute( module ) {
- var key, value, media, i, urls, script, markModuleReady, nestedAddScript;
+ var key, value, media, i, urls, cssHandle, checkCssHandles,
+ cssHandlesRegistered = false;
if ( registry[module] === undefined ) {
throw new Error( 'Module has not been registered yet: ' + module );
@@ -837,12 +952,13 @@ var mw = ( function ( $, undefined ) {
} else if ( registry[module].state === 'loading' ) {
throw new Error( 'Module has not completed loading yet: ' + module );
} else if ( registry[module].state === 'ready' ) {
- throw new Error( 'Module has already been loaded: ' + module );
+ throw new Error( 'Module has already been executed: ' + module );
}
/**
* Define loop-function here for efficiency
* and to avoid re-using badly scoped variables.
+ * @ignore
*/
function addLink( media, url ) {
var el = document.createElement( 'link' );
@@ -854,6 +970,87 @@ var mw = ( function ( $, undefined ) {
el.href = url;
}
+ function runScript() {
+ var script, markModuleReady, nestedAddScript;
+ try {
+ script = registry[module].script;
+ markModuleReady = function () {
+ registry[module].state = 'ready';
+ handlePending( module );
+ };
+ nestedAddScript = function ( arr, callback, async, i ) {
+ // Recursively call addScript() in its own callback
+ // for each element of arr.
+ if ( i >= arr.length ) {
+ // We're at the end of the array
+ callback();
+ return;
+ }
+
+ addScript( arr[i], function () {
+ nestedAddScript( arr, callback, async, i + 1 );
+ }, async );
+ };
+
+ if ( $.isArray( script ) ) {
+ nestedAddScript( script, markModuleReady, registry[module].async, 0 );
+ } else if ( $.isFunction( script ) ) {
+ registry[module].state = 'ready';
+ script( $ );
+ handlePending( module );
+ }
+ } catch ( e ) {
+ // This needs to NOT use mw.log because these errors are common in production mode
+ // and not in debug mode, such as when a symbol that should be global isn't exported
+ log( 'Exception thrown by ' + module, e );
+ registry[module].state = 'error';
+ handlePending( module );
+ }
+ }
+
+ // This used to be inside runScript, but since that is now fired asychronously
+ // (after CSS is loaded) we need to set it here right away. It is crucial that
+ // when execute() is called this is set synchronously, otherwise modules will get
+ // executed multiple times as the registry will state that it isn't loading yet.
+ registry[module].state = 'loading';
+
+ // Add localizations to message system
+ if ( $.isPlainObject( registry[module].messages ) ) {
+ mw.messages.set( registry[module].messages );
+ }
+
+ if ( $.isReady || registry[module].async ) {
+ // Make sure we don't run the scripts until all (potentially asynchronous)
+ // stylesheet insertions have completed.
+ ( function () {
+ var pending = 0;
+ checkCssHandles = function () {
+ // cssHandlesRegistered ensures we don't take off too soon, e.g. when
+ // one of the cssHandles is fired while we're still creating more handles.
+ if ( cssHandlesRegistered && pending === 0 && runScript ) {
+ runScript();
+ runScript = undefined; // Revoke
+ }
+ };
+ cssHandle = function () {
+ var check = checkCssHandles;
+ pending++;
+ return function () {
+ if (check) {
+ pending--;
+ check();
+ check = undefined; // Revoke
+ }
+ };
+ };
+ }() );
+ } else {
+ // We are in blocking mode, and so we can't afford to wait for CSS
+ cssHandle = function () {};
+ // Run immediately
+ checkCssHandles = runScript;
+ }
+
// Process styles (see also mw.loader.implement)
// * back-compat: { <media>: css }
// * back-compat: { <media>: [url, ..] }
@@ -872,7 +1069,7 @@ var mw = ( function ( $, undefined ) {
// Strings are pre-wrapped in "@media". The media-type was just ""
// (because it had to be set to something).
// This is one of the reasons why this format is no longer used.
- addEmbeddedCSS( value );
+ addEmbeddedCSS( value, cssHandle() );
} else {
// back-compat: { <media>: [url, ..] }
media = key;
@@ -889,7 +1086,7 @@ var mw = ( function ( $, undefined ) {
addLink( media, value[i] );
} else if ( key === 'css' ) {
// { "css": [css, ..] }
- addEmbeddedCSS( value[i] );
+ addEmbeddedCSS( value[i], cssHandle() );
}
}
// Not an array, but a regular object
@@ -906,61 +1103,24 @@ var mw = ( function ( $, undefined ) {
}
}
- // Add localizations to message system
- if ( $.isPlainObject( registry[module].messages ) ) {
- mw.messages.set( registry[module].messages );
- }
-
- // Execute script
- try {
- script = registry[module].script;
- markModuleReady = function () {
- registry[module].state = 'ready';
- handlePending( module );
- };
- nestedAddScript = function ( arr, callback, async, i ) {
- // Recursively call addScript() in its own callback
- // for each element of arr.
- if ( i >= arr.length ) {
- // We're at the end of the array
- callback();
- return;
- }
-
- addScript( arr[i], function () {
- nestedAddScript( arr, callback, async, i + 1 );
- }, async );
- };
-
- if ( $.isArray( script ) ) {
- registry[module].state = 'loading';
- nestedAddScript( script, markModuleReady, registry[module].async, 0 );
- } else if ( $.isFunction( script ) ) {
- registry[module].state = 'ready';
- script( $ );
- handlePending( module );
- }
- } catch ( e ) {
- // This needs to NOT use mw.log because these errors are common in production mode
- // and not in debug mode, such as when a symbol that should be global isn't exported
- log( 'Exception thrown by ' + module + ': ' + e.message, e );
- registry[module].state = 'error';
- handlePending( module );
- }
+ // Kick off.
+ cssHandlesRegistered = true;
+ checkCssHandles();
}
/**
* Adds a dependencies to the queue with optional callbacks to be run
* when the dependencies are ready or fail
*
- * @param dependencies string module name or array of string module names
- * @param ready function callback to execute when all dependencies are ready
- * @param error function callback to execute when any dependency fails
- * @param async (optional) If true, load modules asynchronously even if
- * document ready has not yet occurred
+ * @private
+ * @param {string|string[]} dependencies Module name or array of string module names
+ * @param {Function} [ready] Callback to execute when all dependencies are ready
+ * @param {Function} [error] Callback to execute when any dependency fails
+ * @param {boolean} [async] If true, load modules asynchronously even if
+ * document ready has not yet occurred.
*/
function request( dependencies, ready, error, async ) {
- var regItemDeps, regItemDepLen, n;
+ var n;
// Allow calling by single module name
if ( typeof dependencies === 'string' ) {
@@ -1012,6 +1172,7 @@ var mw = ( function ( $, undefined ) {
/**
* Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
* to a query string of the form foo.bar,baz|bar.baz,quux
+ * @private
*/
function buildModulesString( moduleMap ) {
var arr = [], p, prefix;
@@ -1025,14 +1186,15 @@ var mw = ( function ( $, undefined ) {
/**
* Asynchronously append a script tag to the end of the body
* that invokes load.php
- * @param moduleMap {Object}: Module map, see buildModulesString()
- * @param currReqBase {Object}: Object with other parameters (other than 'modules') to use in the request
- * @param sourceLoadScript {String}: URL of load.php
- * @param async {Boolean}: If true, use an asynchrounous request even if document ready has not yet occurred
+ * @private
+ * @param {Object} moduleMap Module map, see #buildModulesString
+ * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
+ * @param {string} sourceLoadScript URL of load.php
+ * @param {boolean} async If true, use an asynchronous request even if document ready has not yet occurred
*/
function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
var request = $.extend(
- { 'modules': buildModulesString( moduleMap ) },
+ { modules: buildModulesString( moduleMap ) },
currReqBase
);
request = sortQuery( request );
@@ -1043,10 +1205,24 @@ var mw = ( function ( $, undefined ) {
/* Public Methods */
return {
- addStyleTag: addStyleTag,
+ /**
+ * The module registry is exposed as an aid for debugging and inspecting page
+ * state; it is not a public interface for modifying the registry.
+ *
+ * @see #registry
+ * @property
+ * @private
+ */
+ moduleRegistry: registry,
/**
- * Requests dependencies from server, loading and executing when things when ready.
+ * @inheritdoc #newStyleTag
+ * @method
+ */
+ addStyleTag: newStyleTag,
+
+ /**
+ * Batch-request queued dependencies from the server.
*/
work: function () {
var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
@@ -1127,9 +1303,9 @@ var mw = ( function ( $, undefined ) {
}
}
- currReqBase = $.extend( { 'version': formatVersionNumber( maxVersion ) }, reqBase );
+ currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
// For user modules append a user name to the request.
- if ( group === "user" && mw.config.get( 'wgUserName' ) !== null ) {
+ if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
currReqBase.user = mw.config.get( 'wgUserName' );
}
currReqBaseLength = $.param( currReqBase ).length;
@@ -1183,10 +1359,10 @@ var mw = ( function ( $, undefined ) {
/**
* Register a source.
*
- * @param id {String}: Short lowercase a-Z string representing a source, only used internally.
- * @param props {Object}: Object containing only the loadScript property which is a url to
- * the load.php location of the source.
- * @return {Boolean}
+ * @param {string} id Short lowercase a-Z string representing a source, only used internally.
+ * @param {Object} props Object containing only the loadScript property which is a url to
+ * the load.php location of the source.
+ * @return {boolean}
*/
addSource: function ( id, props ) {
var source;
@@ -1208,15 +1384,15 @@ var mw = ( function ( $, undefined ) {
},
/**
- * Registers a module, letting the system know about it and its
+ * Register a module, letting the system know about it and its
* properties. Startup modules contain calls to this function.
*
- * @param module {String}: Module name
- * @param version {Number}: Module version number as a timestamp (falls backs to 0)
- * @param dependencies {String|Array|Function}: One string or array of strings of module
+ * @param {string} module Module name
+ * @param {number} version Module version number as a timestamp (falls backs to 0)
+ * @param {string|Array|Function} dependencies One string or array of strings of module
* names on which this module depends, or a function that returns that array.
- * @param group {String}: Group which the module is in (optional, defaults to null)
- * @param source {String}: Name of the source. Defaults to local.
+ * @param {string} [group=null] Group which the module is in
+ * @param {string} [source='local'] Name of the source
*/
register: function ( module, version, dependencies, group, source ) {
var m;
@@ -1242,15 +1418,15 @@ var mw = ( function ( $, undefined ) {
}
// List the module as registered
registry[module] = {
- 'version': version !== undefined ? parseInt( version, 10 ) : 0,
- 'dependencies': [],
- 'group': typeof group === 'string' ? group : null,
- 'source': typeof source === 'string' ? source: 'local',
- 'state': 'registered'
+ version: version !== undefined ? parseInt( version, 10 ) : 0,
+ dependencies: [],
+ group: typeof group === 'string' ? group : null,
+ source: typeof source === 'string' ? source: 'local',
+ state: 'registered'
};
if ( typeof dependencies === 'string' ) {
// Allow dependencies to be given as a single module name
- registry[module].dependencies = [dependencies];
+ registry[module].dependencies = [ dependencies ];
} else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
// Allow dependencies to be given as an array of module names
// or a function which returns an array
@@ -1259,26 +1435,27 @@ var mw = ( function ( $, undefined ) {
},
/**
- * Implements a module, giving the system a course of action to take
- * upon loading. Results of a request for one or more modules contain
- * calls to this function.
+ * Implement a module given the components that make up the module.
+ *
+ * When #load or #using requests one or more modules, the server
+ * response contain calls to this function.
*
* All arguments are required.
*
- * @param {String} module Name of module
+ * @param {string} module Name of module
* @param {Function|Array} script Function with module code or Array of URLs to
- * be used as the src attribute of a new <script> tag.
+ * be used as the src attribute of a new `<script>` tag.
* @param {Object} style Should follow one of the following patterns:
- * { "css": [css, ..] }
- * { "url": { <media>: [url, ..] } }
- * And for backwards compatibility (needs to be supported forever due to caching):
- * { <media>: css }
- * { <media>: [url, ..] }
+ * { "css": [css, ..] }
+ * { "url": { <media>: [url, ..] } }
+ * And for backwards compatibility (needs to be supported forever due to caching):
+ * { <media>: css }
+ * { <media>: [url, ..] }
*
- * The reason css strings are not concatenated anymore is bug 31676. We now check
- * whether it's safe to extend the stylesheet (see canExpandStylesheetWith).
+ * The reason css strings are not concatenated anymore is bug 31676. We now check
+ * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
*
- * @param {Object} msgs List of key/value pairs to be passed through mw.messages.set
+ * @param {Object} msgs List of key/value pairs to be added to {@link mw#messages}.
*/
implement: function ( module, script, style, msgs ) {
// Validate input
@@ -1316,12 +1493,12 @@ var mw = ( function ( $, undefined ) {
},
/**
- * Executes a function as soon as one or more required modules are ready
+ * Execute a function as soon as one or more required modules are ready.
*
- * @param dependencies {String|Array} Module name or array of modules names the callback
+ * @param {string|Array} dependencies Module name or array of modules names the callback
* dependends on to be ready before executing
- * @param ready {Function} callback to execute when all dependencies are ready (optional)
- * @param error {Function} callback to execute when if dependencies have a errors (optional)
+ * @param {Function} [ready] callback to execute when all dependencies are ready
+ * @param {Function} [error] callback to execute when if dependencies have a errors
*/
using: function ( dependencies, ready, error ) {
var tod = typeof dependencies;
@@ -1331,7 +1508,7 @@ var mw = ( function ( $, undefined ) {
}
// Allow calling with a single dependency as a string
if ( tod === 'string' ) {
- dependencies = [dependencies];
+ dependencies = [ dependencies ];
}
// Resolve entire dependency map
dependencies = resolve( dependencies );
@@ -1353,20 +1530,20 @@ var mw = ( function ( $, undefined ) {
},
/**
- * Loads an external script or one or more modules for future use
+ * Load an external script or one or more modules.
*
- * @param modules {mixed} Either the name of a module, array of modules,
+ * @param {string|Array} modules Either the name of a module, array of modules,
* or a URL of an external script or style
- * @param type {String} mime-type to use if calling with a URL of an
+ * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an
* external script or style; acceptable values are "text/css" and
* "text/javascript"; if no type is provided, text/javascript is assumed.
- * @param async {Boolean} (optional) If true, load modules asynchronously
- * even if document ready has not yet occurred. If false (default),
- * block before document ready and load async after. If not set, true will
- * be assumed if loading a URL, and false will be assumed otherwise.
+ * @param {boolean} [async] If true, load modules asynchronously
+ * even if document ready has not yet occurred. If false, block before
+ * document ready and load async after. If not set, true will be
+ * assumed if loading a URL, and false will be assumed otherwise.
*/
load: function ( modules, type, async ) {
- var filtered, m, module;
+ var filtered, m, module, l;
// Validate input
if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
@@ -1381,11 +1558,13 @@ var mw = ( function ( $, undefined ) {
async = true;
}
if ( type === 'text/css' ) {
- $( 'head' ).append( $( '<link>', {
- rel: 'stylesheet',
- type: 'text/css',
- href: modules
- } ) );
+ // IE7-8 throws security warnings when inserting a <link> tag
+ // with a protocol-relative URL set though attributes (instead of
+ // properties) - when on HTTPS. See also bug #.
+ l = document.createElement( 'link' );
+ l.rel = 'stylesheet';
+ l.href = modules;
+ $( 'head' ).append( l );
return;
}
if ( type === 'text/javascript' || type === undefined ) {
@@ -1396,7 +1575,7 @@ var mw = ( function ( $, undefined ) {
throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
}
// Called with single module
- modules = [modules];
+ modules = [ modules ];
}
// Filter out undefined modules, otherwise resolve() will throw
@@ -1427,14 +1606,14 @@ var mw = ( function ( $, undefined ) {
return;
}
// Since some modules are not yet ready, queue up a request.
- request( filtered, null, null, async );
+ request( filtered, undefined, undefined, async );
},
/**
- * Changes the state of a module
+ * Change the state of one or more modules.
*
- * @param module {String|Object} module name or object of module name/state pairs
- * @param state {String} state name
+ * @param {string|Object} module module name or object of module name/state pairs
+ * @param {string} state state name
*/
state: function ( module, state ) {
var m;
@@ -1448,7 +1627,7 @@ var mw = ( function ( $, undefined ) {
if ( registry[module] === undefined ) {
mw.loader.register( module );
}
- if ( $.inArray(state, ['ready', 'error', 'missing']) !== -1
+ if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
&& registry[module].state !== state ) {
// Make sure pending modules depending on this one get executed if their
// dependencies are now fulfilled!
@@ -1460,9 +1639,9 @@ var mw = ( function ( $, undefined ) {
},
/**
- * Gets the version of a module
+ * Get the version of a module.
*
- * @param module string name of module to get version for
+ * @param {string} module Name of module to get version for
*/
getVersion: function ( module ) {
if ( registry[module] !== undefined && registry[module].version !== undefined ) {
@@ -1472,16 +1651,17 @@ var mw = ( function ( $, undefined ) {
},
/**
- * @deprecated since 1.18 use mw.loader.getVersion() instead
+ * @inheritdoc #getVersion
+ * @deprecated since 1.18 use #getVersion instead
*/
version: function () {
return mw.loader.getVersion.apply( mw.loader, arguments );
},
/**
- * Gets the state of a module
+ * Get the state of a module.
*
- * @param module string name of module to get state for
+ * @param {string} module name of module to get state for
*/
getState: function ( module ) {
if ( registry[module] !== undefined && registry[module].state !== undefined ) {
@@ -1502,19 +1682,52 @@ var mw = ( function ( $, undefined ) {
},
/**
- * For backwards-compatibility with Squid-cached pages. Loads mw.user
+ * Load the `mediawiki.user` module.
+ *
+ * For backwards-compatibility with cached pages from before 2013 where:
+ *
+ * - the `mediawiki.user` module didn't exist yet
+ * - `mw.user` was still part of mediawiki.js
+ * - `mw.loader.go` still existed and called after `mw.loader.load()`
*/
go: function () {
mw.loader.load( 'mediawiki.user' );
+ },
+
+ /**
+ * @inheritdoc mw.inspect#runReports
+ * @method
+ */
+ inspect: function () {
+ var args = slice.call( arguments );
+ mw.loader.using( 'mediawiki.inspect', function () {
+ mw.inspect.runReports.apply( mw.inspect, args );
+ } );
}
+
};
}() ),
- /** HTML construction helper functions */
+ /**
+ * HTML construction helper functions
+ *
+ * @example
+ *
+ * var Html, output;
+ *
+ * Html = mw.html;
+ * output = Html.element( 'div', {}, new Html.Raw(
+ * Html.element( 'img', { src: '<' } )
+ * ) );
+ * mw.log( output ); // <div><img src="&lt;"/></div>
+ *
+ * @class mw.html
+ * @singleton
+ */
html: ( function () {
function escapeCallback( s ) {
switch ( s ) {
- case "'":
+ case '\'':
return '&#039;';
case '"':
return '&quot;';
@@ -1530,46 +1743,24 @@ var mw = ( function ( $, undefined ) {
return {
/**
* Escape a string for HTML. Converts special characters to HTML entities.
- * @param s The string to escape
+ * @param {string} s The string to escape
*/
escape: function ( s ) {
return s.replace( /['"<>&]/g, escapeCallback );
},
/**
- * Wrapper object for raw HTML passed to mw.html.element().
- * @constructor
- */
- Raw: function ( value ) {
- this.value = value;
- },
-
- /**
- * Wrapper object for CDATA element contents passed to mw.html.element()
- * @constructor
- */
- Cdata: function ( value ) {
- this.value = value;
- },
-
- /**
* Create an HTML element string, with safe escaping.
*
- * @param name The tag name.
- * @param attrs An object with members mapping element names to values
- * @param contents The contents of the element. May be either:
+ * @param {string} name The tag name.
+ * @param {Object} attrs An object with members mapping element names to values
+ * @param {Mixed} contents The contents of the element. May be either:
* - string: The string is escaped.
* - null or undefined: The short closing form is used, e.g. <br/>.
* - this.Raw: The value attribute is included without escaping.
* - this.Cdata: The value attribute is included, and an exception is
* thrown if it contains an illegal ETAGO delimiter.
* See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
- *
- * Example:
- * var h = mw.html;
- * return h.element( 'div', {},
- * new h.Raw( h.element( 'img', {src: '<'} ) ) );
- * Returns <div><img src="&lt;"/></div>
*/
element: function ( name, attrs, contents ) {
var v, attrName, s = '<' + name;
@@ -1618,6 +1809,22 @@ var mw = ( function ( $, undefined ) {
}
s += '</' + name + '>';
return s;
+ },
+
+ /**
+ * Wrapper object for raw HTML passed to mw.html.element().
+ * @class mw.html.Raw
+ */
+ Raw: function ( value ) {
+ this.value = value;
+ },
+
+ /**
+ * Wrapper object for CDATA element contents passed to mw.html.element()
+ * @class mw.html.Cdata
+ */
+ Cdata: function ( value ) {
+ this.value = value;
}
};
}() ),
@@ -1626,7 +1833,87 @@ var mw = ( function ( $, undefined ) {
user: {
options: new Map(),
tokens: new Map()
- }
+ },
+
+ /**
+ * Registry and firing of events.
+ *
+ * MediaWiki has various interface components that are extended, enhanced
+ * or manipulated in some other way by extensions, gadgets and even
+ * in core itself.
+ *
+ * This framework helps streamlining the timing of when these other
+ * code paths fire their plugins (instead of using document-ready,
+ * which can and should be limited to firing only once).
+ *
+ * Features like navigating to other wiki pages, previewing an edit
+ * and editing itself – without a refresh – can then retrigger these
+ * hooks accordingly to ensure everything still works as expected.
+ *
+ * Example usage:
+ *
+ * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
+ * mw.hook( 'wikipage.content' ).fire( $content );
+ *
+ * Handlers can be added and fired for arbitrary event names at any time. The same
+ * event can be fired multiple times. The last run of an event is memorized
+ * (similar to `$(document).ready` and `$.Deferred().done`).
+ * This means if an event is fired, and a handler added afterwards, the added
+ * function will be fired right away with the last given event data.
+ *
+ * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
+ * Thus allowing flexible use and optimal maintainability and authority control.
+ * You can pass around the `add` and/or `fire` method to another piece of code
+ * without it having to know the event name (or `mw.hook` for that matter).
+ *
+ * var h = mw.hook( 'bar.ready' );
+ * new mw.Foo( .. ).fetch( { callback: h.fire } );
+ *
+ * Note: Events are documented with an underscore instead of a dot in the event
+ * name due to jsduck not supporting dots in that position.
+ *
+ * @class mw.hook
+ */
+ hook: ( function () {
+ var lists = {};
+
+ /**
+ * Create an instance of mw.hook.
+ *
+ * @method hook
+ * @member mw
+ * @param {string} name Name of hook.
+ * @return {mw.hook}
+ */
+ return function ( name ) {
+ var list = lists[name] || ( lists[name] = $.Callbacks( 'memory' ) );
+
+ return {
+ /**
+ * Register a hook handler
+ * @param {Function...} handler Function to bind.
+ * @chainable
+ */
+ add: list.add,
+
+ /**
+ * Unregister a hook handler
+ * @param {Function...} handler Function to unbind.
+ * @chainable
+ */
+ remove: list.remove,
+
+ /**
+ * Run a hook.
+ * @param {Mixed...} data
+ * @chainable
+ */
+ fire: function () {
+ return list.fireWith( null, slice.call( arguments ) );
+ }
+ };
+ };
+ }() )
};
}( jQuery ) );
diff --git a/resources/mediawiki/mediawiki.log.js b/resources/mediawiki/mediawiki.log.js
index 4ea1a881..75e4c961 100644
--- a/resources/mediawiki/mediawiki.log.js
+++ b/resources/mediawiki/mediawiki.log.js
@@ -1,4 +1,4 @@
-/**
+/*!
* Logger for MediaWiki javascript.
* Implements the stub left by the main 'mediawiki' module.
*
@@ -9,15 +9,20 @@
( function ( mw, $ ) {
/**
+ * @class mw.log
+ * @singleton
+ */
+
+ /**
* Logs a message to the console.
*
* In the case the browser does not have a console API, a console is created on-the-fly by appending
- * a <div id="mw-log-console"> element to the bottom of the body and then appending this and future
+ * a `<div id="mw-log-console">` element to the bottom of the body and then appending this and future
* messages to that, instead of the console.
*
- * @param {String} First in list of variadic messages to output to console.
+ * @param {string...} msg Messages to output to console.
*/
- mw.log = function ( /* logmsg, logmsg, */ ) {
+ mw.log = function () {
// Turn arguments into an array
var args = Array.prototype.slice.call( arguments ),
// Allow log messages to use a configured prefix to identify the source window (ie. frame)
@@ -41,7 +46,7 @@
':' + ( d.getSeconds() < 10 ? '0' + d.getSeconds() : d.getSeconds() ) +
'.' + ( d.getMilliseconds() < 10 ? '00' + d.getMilliseconds() : ( d.getMilliseconds() < 100 ? '0' + d.getMilliseconds() : d.getMilliseconds() ) ),
$log = $( '#mw-log-console' );
-
+
if ( !$log.length ) {
$log = $( '<div id="mw-log-console"></div>' ).css( {
overflow: 'auto',
@@ -54,7 +59,7 @@
hovzer.update();
}
$log.append(
- $( '<div></div>' )
+ $( '<div>' )
.css( {
borderBottom: 'solid 1px #DDDDDD',
fontSize: 'small',
@@ -68,4 +73,54 @@
} );
};
+ /**
+ * Write a message the console's warning channel.
+ * Also logs a stacktrace for easier debugging.
+ * Each action is silently ignored if the browser doesn't support it.
+ *
+ * @param {string...} msg Messages to output to console
+ */
+ mw.log.warn = function () {
+ var console = window.console;
+ if ( console && console.warn ) {
+ console.warn.apply( console, arguments );
+ if ( console.trace ) {
+ console.trace();
+ }
+ }
+ };
+
+ /**
+ * Create a property in a host object that, when accessed, will produce
+ * a deprecation warning in the console with backtrace.
+ *
+ * @param {Object} obj Host object of deprecated property
+ * @param {string} key Name of property to create in `obj`
+ * @param {Mixed} val The value this property should return when accessed
+ * @param {string} [msg] Optional text to include in the deprecation message.
+ */
+ mw.log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
+ obj[key] = val;
+ } : function ( obj, key, val, msg ) {
+ msg = 'MWDeprecationWarning: Use of "' + key + '" property is deprecated.' +
+ ( msg ? ( ' ' + msg ) : '' );
+ try {
+ Object.defineProperty( obj, key, {
+ configurable: true,
+ enumerable: true,
+ get: function () {
+ mw.log.warn( msg );
+ return val;
+ },
+ set: function ( newVal ) {
+ mw.log.warn( msg );
+ val = newVal;
+ }
+ } );
+ } catch ( err ) {
+ // IE8 can throw on Object.defineProperty
+ obj[key] = val;
+ }
+ };
+
}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki/mediawiki.notification.css b/resources/mediawiki/mediawiki.notification.css
index 9a7b651d..3aa358ac 100644
--- a/resources/mediawiki/mediawiki.notification.css
+++ b/resources/mediawiki/mediawiki.notification.css
@@ -2,15 +2,25 @@
* Stylesheet for mediawiki.notification module
*/
-#mw-notification-area {
+.mw-notification-area {
position: absolute;
- top: 1em;
- right: 1em;
+ top: 0;
+ right: 0;
+ padding: 1em 1em 0 0;
width: 20em;
line-height: 1.35;
z-index: 10000;
}
+.mw-notification-area-floating {
+ position: fixed;
+}
+
+* html .mw-notification-area-floating {
+ /* Make it at least 'absolute' in IE6 since 'fixed' is not supported */
+ position: absolute;
+}
+
.mw-notification {
padding: 0.25em 1em;
margin-bottom: 0.5em;
diff --git a/resources/mediawiki/mediawiki.notification.js b/resources/mediawiki/mediawiki.notification.js
index 58a3ab6a..4ede8096 100644
--- a/resources/mediawiki/mediawiki.notification.js
+++ b/resources/mediawiki/mediawiki.notification.js
@@ -1,24 +1,24 @@
-/**
- * Implements mediaWiki.notification library
- */
( function ( mw, $ ) {
'use strict';
- var isPageReady = false,
- isInitialized = false,
- preReadyNotifQueue = [],
- /**
- * @var {jQuery}
- * The #mw-notification-area div that all notifications are contained inside.
- */
- $area = null;
+ var notification,
+ // The #mw-notification-area div that all notifications are contained inside.
+ $area,
+ isPageReady = false,
+ preReadyNotifQueue = [];
/**
* Creates a Notification object for 1 message.
- * Does not insert anything into the document (see .start()).
+ * Does not insert anything into the document (see #start).
+ *
+ * The "_" in the name is to avoid a bug (http://github.com/senchalabs/jsduck/issues/304)
+ * It is not part of the actual class name.
+ *
+ * @class mw.Notification_
+ * @alternateClassName mw.Notification
+ * @private
*
* @constructor
- * @see mw.notification.notify
*/
function Notification( message, options ) {
var $notification, $notificationTitle, $notificationContent;
@@ -88,7 +88,9 @@
// Other notification elements matching the same tag
$tagMatches,
outerHeight,
- placeholderHeight;
+ placeholderHeight,
+ autohideCount,
+ notif;
if ( this.isOpen ) {
return;
@@ -164,10 +166,11 @@
}
} );
+ notif = this;
+
// Create a clear placeholder we can use to make the notifications around the notification that is being
// replaced expand or contract gracefully to fit the height of the new notification.
- var self = this;
- self.$replacementPlaceholder = $( '<div>' )
+ notif.$replacementPlaceholder = $( '<div>' )
// Set the height to the space the previous notification or placeholder took
.css( 'height', outerHeight )
// Make sure that this placeholder is at the very end of this tagged notification group
@@ -181,7 +184,7 @@
// Reset the notification position after we've finished the space animation
// However do not do it if the placeholder was removed because another tagged
// notification went and closed this one.
- if ( self.$replacementPlaceholder ) {
+ if ( notif.$replacementPlaceholder ) {
$notification.css( 'position', '' );
}
// Finally, remove the placeholder from the DOM
@@ -206,7 +209,7 @@
// By default a notification is paused.
// If this notification is within the first {autoHideLimit} notifications then
// start the auto-hide timer as soon as it's created.
- var autohideCount = $area.find( '.mw-notification-autohide' ).length;
+ autohideCount = $area.find( '.mw-notification-autohide' ).length;
if ( autohideCount <= notification.autoHideLimit ) {
this.resume();
}
@@ -253,6 +256,7 @@
*
* @param {Object} options An object containing options for the closing of the notification.
* These are typically only used internally.
+ *
* - speed: Use a close speed different than the default 'slow'.
* - placeholder: Set to false to disable the placeholder transition.
*/
@@ -326,7 +330,7 @@
/**
* Helper function, take a list of notification divs and call
- * a function on the Notification instance attached to them
+ * a function on the Notification instance attached to them.
*
* @param {jQuery} $notifications A jQuery object containing notification divs
* @param {string} fn The name of the function to call on the Notification instance
@@ -341,40 +345,58 @@
}
/**
- * Initialisation
- * (don't call before document ready)
+ * Initialisation.
+ * Must only be called once, and not before the document is ready.
+ * @ignore
*/
function init() {
- if ( !isInitialized ) {
- isInitialized = true;
- $area = $( '<div id="mw-notification-area"></div>' )
- // Pause auto-hide timers when the mouse is in the notification area.
- .on( {
- mouseenter: notification.pause,
- mouseleave: notification.resume
- } )
- // When clicking on a notification close it.
- .on( 'click', '.mw-notification', function () {
- var notif = $( this ).data( 'mw.notification' );
- if ( notif ) {
- notif.close();
- }
- } )
- // Stop click events from <a> tags from propogating to prevent clicking.
- // on links from hiding a notification.
- .on( 'click', 'a', function ( e ) {
- e.stopPropagation();
- } );
-
- // Prepend the notification area to the content area and save it's object.
- mw.util.$content.prepend( $area );
+ var offset, $window = $( window );
+
+ $area = $( '<div id="mw-notification-area" class="mw-notification-area mw-notification-area-layout"></div>' )
+ // Pause auto-hide timers when the mouse is in the notification area.
+ .on( {
+ mouseenter: notification.pause,
+ mouseleave: notification.resume
+ } )
+ // When clicking on a notification close it.
+ .on( 'click', '.mw-notification', function () {
+ var notif = $( this ).data( 'mw.notification' );
+ if ( notif ) {
+ notif.close();
+ }
+ } )
+ // Stop click events from <a> tags from propogating to prevent clicking.
+ // on links from hiding a notification.
+ .on( 'click', 'a', function ( e ) {
+ e.stopPropagation();
+ } );
+
+ // Prepend the notification area to the content area and save it's object.
+ mw.util.$content.prepend( $area );
+ offset = $area.offset();
+
+ function updateAreaMode() {
+ var isFloating = $window.scrollTop() > offset.top;
+ $area
+ .toggleClass( 'mw-notification-area-floating', isFloating )
+ .toggleClass( 'mw-notification-area-layout', !isFloating );
}
+
+ $window.on( 'scroll', updateAreaMode );
+
+ // Initial mode
+ updateAreaMode();
}
- var notification = {
+ /**
+ * @class mw.notification
+ * @singleton
+ */
+ notification = {
/**
* Pause auto-hide timers for all notifications.
* Notifications will not auto-hide until resume is called.
+ * @see mw.Notification#pause
*/
pause: function () {
callEachNotification(
@@ -385,13 +407,13 @@
/**
* Resume any paused auto-hide timers from the beginning.
- * Only the first {autoHideLimit} timers will be resumed.
+ * Only the first #autoHideLimit timers will be resumed.
*/
resume: function () {
callEachNotification(
- // Only call resume on the first {autoHideLimit} notifications.
- // Exclude noautohide notifications to avoid bugs where {autoHideLimit}
- // { autoHide: false } notifications are at the start preventing any
+ // Only call resume on the first #autoHideLimit notifications.
+ // Exclude noautohide notifications to avoid bugs where #autoHideLimit
+ // `{ autoHide: false }` notifications are at the start preventing any
// auto-hide notifications from being autohidden.
$area.children( '.mw-notification-autohide' ).slice( 0, notification.autoHideLimit ),
'resume'
@@ -401,10 +423,10 @@
/**
* Display a notification message to the user.
*
- * @param {mixed} message The DOM-element, jQuery object, mw.Message instance,
- * or plaintext string to be used as the message.
+ * @param {HTMLElement|jQuery|mw.Message|string} message
* @param {Object} options The options to use for the notification.
- * See mw.notification.defaults for details.
+ * See #defaults for details.
+ * @return {Object} Object with a close function to close the notification
*/
notify: function ( message, options ) {
var notif;
@@ -417,25 +439,27 @@
} else {
preReadyNotifQueue.push( notif );
}
+ return { close: $.proxy( notif.close, notif ) };
},
/**
- * @var {Object}
- * The defaults for mw.notification.notify's options parameter
- * autoHide:
- * A boolean indicating whether the notifification should automatically
- * be hidden after shown. Or if it should persist.
+ * @property {Object}
+ * The defaults for #notify options parameter.
+ *
+ * - autoHide:
+ * A boolean indicating whether the notifification should automatically
+ * be hidden after shown. Or if it should persist.
*
- * tag:
- * An optional string. When a notification is tagged only one message
- * with that tag will be displayed. Trying to display a new notification
- * with the same tag as one already being displayed will cause the other
- * notification to be closed and this new notification to open up inside
- * the same place as the previous notification.
+ * - tag:
+ * An optional string. When a notification is tagged only one message
+ * with that tag will be displayed. Trying to display a new notification
+ * with the same tag as one already being displayed will cause the other
+ * notification to be closed and this new notification to open up inside
+ * the same place as the previous notification.
*
- * title:
- * An optional title for the notification. Will be displayed above the
- * content. Usually in bold.
+ * - title:
+ * An optional title for the notification. Will be displayed above the
+ * content. Usually in bold.
*/
defaults: {
autoHide: true,
@@ -444,20 +468,20 @@
},
/**
- * @var {number}
+ * @property {number}
* Number of seconds to wait before auto-hiding notifications.
*/
autoHideSeconds: 5,
/**
- * @var {number}
+ * @property {number}
* Maximum number of notifications to count down auto-hide timers for.
- * Only the first {autoHideLimit} notifications being displayed will
+ * Only the first #autoHideLimit notifications being displayed will
* auto-hide. Any notifications further down in the list will only start
* counting down to auto-hide after the first few messages have closed.
*
* This basically represents the number of notifications the user should
- * be able to process in {autoHideSeconds} time.
+ * be able to process in #autoHideSeconds time.
*/
autoHideLimit: 3
};
diff --git a/resources/mediawiki/mediawiki.notify.js b/resources/mediawiki/mediawiki.notify.js
index 3bf2a896..743d6517 100644
--- a/resources/mediawiki/mediawiki.notify.js
+++ b/resources/mediawiki/mediawiki.notify.js
@@ -1,20 +1,28 @@
/**
- * Implements mediaWiki.notify function
+ * @class mw.plugin.notify
*/
-( function ( mw ) {
+( function ( mw, $ ) {
'use strict';
/**
- * @see mw.notification.notify
+ * @see mw.notification#notify
+ * @param message
+ * @param options
+ * @return {jQuery.Promise}
*/
mw.notify = function ( message, options ) {
+ var d = $.Deferred();
// Don't bother loading the whole notification system if we never use it.
mw.loader.using( 'mediawiki.notification', function () {
- // Don't bother calling mw.loader.using a second time after we've already loaded mw.notification.
- mw.notify = mw.notification.notify;
// Call notify with the notification the user requested of us.
- mw.notify( message, options );
- } );
+ d.resolve( mw.notification.notify( message, options ) );
+ }, d.reject );
+ return d.promise();
};
-}( mediaWiki ) ); \ No newline at end of file
+ /**
+ * @class mw
+ * @mixins mw.plugin.notify
+ */
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki/mediawiki.searchSuggest.css b/resources/mediawiki/mediawiki.searchSuggest.css
new file mode 100644
index 00000000..0fb862b9
--- /dev/null
+++ b/resources/mediawiki/mediawiki.searchSuggest.css
@@ -0,0 +1,16 @@
+/* Make sure the links are not underlined or colored, ever. */
+/* There is already a :focus / :hover indication on the <div>. */
+.suggestions a.mw-searchSuggest-link,
+.suggestions a.mw-searchSuggest-link:hover,
+.suggestions a.mw-searchSuggest-link:active,
+.suggestions a.mw-searchSuggest-link:focus {
+ text-decoration: none;
+ color: black;
+}
+
+.suggestions-result-current a.mw-searchSuggest-link,
+.suggestions-result-current a.mw-searchSuggest-link:hover,
+.suggestions-result-current a.mw-searchSuggest-link:active,
+.suggestions-result-current a.mw-searchSuggest-link:focus {
+ color: white;
+}
diff --git a/resources/mediawiki/mediawiki.searchSuggest.js b/resources/mediawiki/mediawiki.searchSuggest.js
index 99a55576..7f078626 100644
--- a/resources/mediawiki/mediawiki.searchSuggest.js
+++ b/resources/mediawiki/mediawiki.searchSuggest.js
@@ -2,8 +2,8 @@
* Add search suggestions to the search form.
*/
( function ( mw, $ ) {
- $( document ).ready( function ( $ ) {
- var map, searchboxesSelectors,
+ $( function () {
+ var map, resultRenderCache, searchboxesSelectors,
// Region where the suggestions box will appear directly below
// (using the same width). Can be a container element or the input
// itself, depending on what suits best in the environment.
@@ -41,12 +41,95 @@
return;
}
+ // Compute form data for search suggestions functionality.
+ function computeResultRenderCache( context ) {
+ var $form, formAction, baseHref, linkParams;
+
+ // Compute common parameters for links' hrefs
+ $form = context.config.$region.closest( 'form' );
+
+ formAction = $form.attr( 'action' );
+ baseHref = formAction + ( formAction.match(/\?/) ? '&' : '?' );
+
+ linkParams = {};
+ $.each( $form.serializeArray(), function ( idx, obj ) {
+ linkParams[ obj.name ] = obj.value;
+ } );
+
+ return {
+ textParam: context.data.$textbox.attr( 'name' ),
+ linkParams: linkParams,
+ baseHref: baseHref
+ };
+ }
+
+ // The function used to render the suggestions.
+ function renderFunction( text, context ) {
+ if ( !resultRenderCache ) {
+ resultRenderCache = computeResultRenderCache( context );
+ }
+
+ // linkParams object is modified and reused
+ resultRenderCache.linkParams[ resultRenderCache.textParam ] = text;
+
+ // this is the container <div>, jQueryfied
+ this
+ .append(
+ // the <span> is needed for $.autoEllipsis to work
+ $( '<span>' )
+ .css( 'whiteSpace', 'nowrap' )
+ .text( text )
+ )
+ .wrap(
+ $( '<a>' )
+ .attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) )
+ .addClass( 'mw-searchSuggest-link' )
+ );
+ }
+
+ function specialRenderFunction( query, context ) {
+ var $el = this;
+
+ if ( !resultRenderCache ) {
+ resultRenderCache = computeResultRenderCache( context );
+ }
+
+ // linkParams object is modified and reused
+ resultRenderCache.linkParams[ resultRenderCache.textParam ] = query;
+
+ if ( $el.children().length === 0 ) {
+ $el
+ .append(
+ $( '<div>' )
+ .addClass( 'special-label' )
+ .text( mw.msg( 'searchsuggest-containing' ) ),
+ $( '<div>' )
+ .addClass( 'special-query' )
+ .text( query )
+ .autoEllipsis()
+ )
+ .show();
+ } else {
+ $el.find( '.special-query' )
+ .text( query )
+ .autoEllipsis();
+ }
+
+ if ( $el.parent().hasClass( 'mw-searchSuggest-link' ) ) {
+ $el.parent().attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) + '&fulltext=1' );
+ } else {
+ $el.wrap(
+ $( '<a>' )
+ .attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) + '&fulltext=1' )
+ .addClass( 'mw-searchSuggest-link' )
+ );
+ }
+ }
+
// General suggestions functionality for all search boxes
searchboxesSelectors = [
// Primary searchbox on every page in standard skins
'#searchInput',
- // Secondary searchbox in legacy skins (LegacyTemplate::searchForm uses id "searchInput + unique id")
- '#searchInput2',
// Special:Search
'#powerSearchText',
'#searchText',
@@ -56,39 +139,31 @@
$( searchboxesSelectors.join(', ') )
.suggestions( {
fetch: function ( query ) {
- var $el, jqXhr;
+ var $el;
if ( query.length !== 0 ) {
- $el = $(this);
- jqXhr = $.ajax( {
- url: mw.util.wikiScript( 'api' ),
- data: {
- format: 'json',
- action: 'opensearch',
- search: query,
- namespace: 0,
- suggest: ''
- },
- dataType: 'json',
- success: function ( data ) {
- if ( $.isArray( data ) && data.length ) {
- $el.suggestions( 'suggestions', data[1] );
- }
- }
- });
- $el.data( 'request', jqXhr );
+ $el = $( this );
+ $el.data( 'request', ( new mw.Api() ).get( {
+ action: 'opensearch',
+ search: query,
+ namespace: 0,
+ suggest: ''
+ } ).done( function ( data ) {
+ $el.suggestions( 'suggestions', data[1] );
+ } ) );
}
},
cancel: function () {
- var jqXhr = $(this).data( 'request' );
+ var apiPromise = $( this ).data( 'request' );
// If the delay setting has caused the fetch to have not even happened
- // yet, the jqXHR object will have never been set.
- if ( jqXhr && $.isFunction( jqXhr.abort ) ) {
- jqXhr.abort();
- $(this).removeData( 'request' );
+ // yet, the apiPromise object will have never been set.
+ if ( apiPromise && $.isFunction( apiPromise.abort ) ) {
+ apiPromise.abort();
+ $( this ).removeData( 'request' );
}
},
result: {
+ render: renderFunction,
select: function ( $input ) {
$input.closest( 'form' ).submit();
}
@@ -110,39 +185,16 @@
return;
}
- // Placeholder text for search box
- $searchInput
- .attr( 'placeholder', mw.msg( 'searchsuggest-search' ) )
- .placeholder();
-
// Special suggestions functionality for skin-provided search box
$searchInput.suggestions( {
result: {
+ render: renderFunction,
select: function ( $input ) {
$input.closest( 'form' ).submit();
}
},
special: {
- render: function ( query ) {
- var $el = this;
- if ( $el.children().length === 0 ) {
- $el
- .append(
- $( '<div>' )
- .addClass( 'special-label' )
- .text( mw.msg( 'searchsuggest-containing' ) ),
- $( '<div>' )
- .addClass( 'special-query' )
- .text( query )
- .autoEllipsis()
- )
- .show();
- } else {
- $el.find( '.special-query' )
- .text( query )
- .autoEllipsis();
- }
- },
+ render: specialRenderFunction,
select: function ( $input ) {
$input.closest( 'form' ).append(
$( '<input type="hidden" name="fulltext" value="1"/>' )
diff --git a/resources/mediawiki/mediawiki.user.js b/resources/mediawiki/mediawiki.user.js
index e64d2e84..3e375fb6 100644
--- a/resources/mediawiki/mediawiki.user.js
+++ b/resources/mediawiki/mediawiki.user.js
@@ -1,67 +1,60 @@
-/*
- * Implementation for mediaWiki.user
+/**
+ * @class mw.user
+ * @singleton
*/
-
( function ( mw, $ ) {
+ var user,
+ callbacks = {},
+ // Extend the skeleton mw.user from mediawiki.js
+ // This is kind of ugly but we're stuck with this for b/c reasons
+ options = mw.user.options || new mw.Map(),
+ tokens = mw.user.tokens || new mw.Map();
/**
- * User object
+ * Get the current user's groups or rights
+ *
+ * @private
+ * @param {string} info One of 'groups' or 'rights'
+ * @param {Function} callback
*/
- function User( options, tokens ) {
- var user, callbacks;
-
- /* Private Members */
-
- user = this;
- callbacks = {};
-
- /**
- * Gets the current user's groups or rights.
- * @param {String} info: One of 'groups' or 'rights'.
- * @param {Function} callback
- */
- function getUserInfo( info, callback ) {
- var api;
- if ( callbacks[info] ) {
- callbacks[info].add( callback );
- return;
- }
- callbacks.rights = $.Callbacks('once memory');
- callbacks.groups = $.Callbacks('once memory');
+ function getUserInfo( info, callback ) {
+ var api;
+ if ( callbacks[info] ) {
callbacks[info].add( callback );
- api = new mw.Api();
- api.get( {
- action: 'query',
- meta: 'userinfo',
- uiprop: 'rights|groups'
- } ).always( function ( data ) {
- var rights, groups;
- if ( data.query && data.query.userinfo ) {
- rights = data.query.userinfo.rights;
- groups = data.query.userinfo.groups;
- }
- callbacks.rights.fire( rights || [] );
- callbacks.groups.fire( groups || [] );
- } );
+ return;
}
+ callbacks.rights = $.Callbacks('once memory');
+ callbacks.groups = $.Callbacks('once memory');
+ callbacks[info].add( callback );
+ api = new mw.Api();
+ api.get( {
+ action: 'query',
+ meta: 'userinfo',
+ uiprop: 'rights|groups'
+ } ).always( function ( data ) {
+ var rights, groups;
+ if ( data.query && data.query.userinfo ) {
+ rights = data.query.userinfo.rights;
+ groups = data.query.userinfo.groups;
+ }
+ callbacks.rights.fire( rights || [] );
+ callbacks.groups.fire( groups || [] );
+ } );
+ }
- /* Public Members */
-
- this.options = options || new mw.Map();
-
- this.tokens = tokens || new mw.Map();
-
- /* Public Methods */
+ mw.user = user = {
+ options: options,
+ tokens: tokens,
/**
- * Generates a random user session ID (32 alpha-numeric characters).
+ * Generate a random user session ID (32 alpha-numeric characters)
*
* This information would potentially be stored in a cookie to identify a user during a
* session or series of sessions. Its uniqueness should not be depended on.
*
- * @return String: Random set of 32 alpha-numeric characters
+ * @return {string} Random set of 32 alpha-numeric characters
*/
- function generateId() {
+ generateRandomSessionId: function () {
var i, r,
id = '',
seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
@@ -70,126 +63,156 @@
id += seed.substring( r, r + 1 );
}
return id;
- }
+ },
/**
- * Gets the current user's name.
+ * Get the current user's database id
+ *
+ * Not to be confused with #id.
*
- * @return Mixed: User name string or null if users is anonymous
+ * @return {number} Current user's id, or 0 if user is anonymous
*/
- this.getName = function () {
+ getId: function () {
+ return mw.config.get( 'wgUserId', 0 );
+ },
+
+ /**
+ * Get the current user's name
+ *
+ * @return {string|null} User name string or null if user is anonymous
+ */
+ getName: function () {
return mw.config.get( 'wgUserName' );
- };
+ },
+
+ /**
+ * @inheritdoc #getName
+ * @deprecated since 1.20 use #getName instead
+ */
+ name: function () {
+ return user.getName();
+ },
/**
- * @deprecated since 1.20 use mw.user.getName() instead
+ * Get date user registered, if available
+ *
+ * @return {Date|boolean|null} Date user registered, or false for anonymous users, or
+ * null when data is not available
*/
- this.name = function () {
- return this.getName();
- };
+ getRegistration: function () {
+ var registration = mw.config.get( 'wgUserRegistration' );
+ if ( user.isAnon() ) {
+ return false;
+ } else if ( registration === null ) {
+ // Information may not be available if they signed up before
+ // MW began storing this.
+ return null;
+ } else {
+ return new Date( registration );
+ }
+ },
/**
- * Checks if the current user is anonymous.
+ * Whether the current user is anonymous
*
- * @return Boolean
+ * @return {boolean}
*/
- this.isAnon = function () {
+ isAnon: function () {
return user.getName() === null;
- };
+ },
/**
- * @deprecated since 1.20 use mw.user.isAnon() instead
+ * @inheritdoc #isAnon
+ * @deprecated since 1.20 use #isAnon instead
*/
- this.anonymous = function () {
+ anonymous: function () {
return user.isAnon();
- };
+ },
/**
- * Gets a random session ID automatically generated and kept in a cookie.
+ * Get an automatically generated random ID (stored in a session cookie)
*
* This ID is ephemeral for everyone, staying in their browser only until they close
* their browser.
*
- * @return String: User name or random session ID
+ * @return {string} Random session ID
*/
- this.sessionId = function () {
+ sessionId: function () {
var sessionId = $.cookie( 'mediaWiki.user.sessionId' );
- if ( typeof sessionId === 'undefined' || sessionId === null ) {
- sessionId = generateId();
- $.cookie( 'mediaWiki.user.sessionId', sessionId, { 'expires': null, 'path': '/' } );
+ if ( sessionId === undefined || sessionId === null ) {
+ sessionId = user.generateRandomSessionId();
+ $.cookie( 'mediaWiki.user.sessionId', sessionId, { expires: null, path: '/' } );
}
return sessionId;
- };
+ },
/**
- * Gets the current user's name or the session ID
+ * Get the current user's name or the session ID
*
- * @return String: User name or random session ID
+ * Not to be confused with #getId.
+ *
+ * @return {string} User name or random session ID
*/
- this.id = function() {
- var name = user.getName();
- if ( name ) {
- return name;
- }
- return user.sessionId();
- };
+ id: function () {
+ return user.getName() || user.sessionId();
+ },
/**
- * Gets the user's bucket, placing them in one at random based on set odds if needed.
- *
- * @param key String: Name of bucket
- * @param options Object: Bucket configuration options
- * @param options.buckets Object: List of bucket-name/relative-probability pairs (required,
- * must have at least one pair)
- * @param options.version Number: Version of bucket test, changing this forces rebucketing
- * (optional, default: 0)
- * @param options.tracked Boolean: Track the event of bucketing through the API module of
- * the ClickTracking extension (optional, default: false)
- * @param options.expires Number: Length of time (in days) until the user gets rebucketed
- * (optional, default: 30)
- * @return String: Bucket name - the randomly chosen key of the options.buckets object
+ * Get the user's bucket (place them in one if not done already)
*
- * @example
* mw.user.bucket( 'test', {
- * 'buckets': { 'ignored': 50, 'control': 25, 'test': 25 },
- * 'version': 1,
- * 'tracked': true,
- * 'expires': 7
+ * buckets: { ignored: 50, control: 25, test: 25 },
+ * version: 1,
+ * expires: 7
* } );
+ *
+ * @param {string} key Name of bucket
+ * @param {Object} options Bucket configuration options
+ * @param {Object} options.buckets List of bucket-name/relative-probability pairs (required,
+ * must have at least one pair)
+ * @param {number} [options.version=0] Version of bucket test, changing this forces
+ * rebucketing
+ * @param {number} [options.expires=30] Length of time (in days) until the user gets
+ * rebucketed
+ * @return {string} Bucket name - the randomly chosen key of the `options.buckets` object
*/
- this.bucket = function ( key, options ) {
+ bucket: function ( key, options ) {
var cookie, parts, version, bucket,
range, k, rand, total;
options = $.extend( {
buckets: {},
version: 0,
- tracked: false,
expires: 30
}, options || {} );
cookie = $.cookie( 'mediaWiki.user.bucket:' + key );
// Bucket information is stored as 2 integers, together as version:bucket like: "1:2"
- if ( typeof cookie === 'string' && cookie.length > 2 && cookie.indexOf( ':' ) > 0 ) {
+ if ( typeof cookie === 'string' && cookie.length > 2 && cookie.indexOf( ':' ) !== -1 ) {
parts = cookie.split( ':' );
if ( parts.length > 1 && Number( parts[0] ) === options.version ) {
version = Number( parts[0] );
bucket = String( parts[1] );
}
}
+
if ( bucket === undefined ) {
if ( !$.isPlainObject( options.buckets ) ) {
- throw 'Invalid buckets error. Object expected for options.buckets.';
+ throw new Error( 'Invalid bucket. Object expected for options.buckets.' );
}
+
version = Number( options.version );
+
// Find range
range = 0;
for ( k in options.buckets ) {
range += options.buckets[k];
}
+
// Select random value within range
rand = Math.random() * range;
+
// Determine which bucket the value landed in
total = 0;
for ( k in options.buckets ) {
@@ -199,39 +222,34 @@
break;
}
}
- if ( options.tracked ) {
- mw.loader.using( 'jquery.clickTracking', function () {
- $.trackAction(
- 'mediaWiki.user.bucket:' + key + '@' + version + ':' + bucket
- );
- } );
- }
+
$.cookie(
'mediaWiki.user.bucket:' + key,
version + ':' + bucket,
- { 'path': '/', 'expires': Number( options.expires ) }
+ { path: '/', expires: Number( options.expires ) }
);
}
+
return bucket;
- };
+ },
/**
- * Gets the current user's groups.
+ * Get the current user's groups
+ *
+ * @param {Function} callback
*/
- this.getGroups = function ( callback ) {
+ getGroups: function ( callback ) {
getUserInfo( 'groups', callback );
- };
+ },
/**
- * Gets the current user's rights.
+ * Get the current user's rights
+ *
+ * @param {Function} callback
*/
- this.getRights = function ( callback ) {
+ getRights: function ( callback ) {
getUserInfo( 'rights', callback );
- };
- }
-
- // Extend the skeleton mw.user from mediawiki.js
- // This is kind of ugly but we're stuck with this for b/c reasons
- mw.user = new User( mw.user.options, mw.user.tokens );
+ }
+ };
}( mediaWiki, jQuery ) );
diff --git a/resources/mediawiki/mediawiki.util.js b/resources/mediawiki/mediawiki.util.js
index 29284384..7383df2d 100644
--- a/resources/mediawiki/mediawiki.util.js
+++ b/resources/mediawiki/mediawiki.util.js
@@ -1,10 +1,11 @@
-/**
- * Implements mediaWiki.util library
- */
( function ( mw, $ ) {
'use strict';
- // Local cache and alias
+ /**
+ * Utility library
+ * @class mw.util
+ * @singleton
+ */
var util = {
/**
@@ -12,7 +13,7 @@
* (don't call before document ready)
*/
init: function () {
- var profile, $tocTitle, $tocToggleLink, hideTocCookie;
+ var profile;
/* Set tooltipAccessKeyPrefix */
profile = $.client.profile();
@@ -28,13 +29,10 @@
profile.platform === 'mac'
// Chrome on Mac
? 'ctrl-option-'
- : profile.platform === 'win'
- // Chrome on Windows
- // (both alt- and alt-shift work, but alt-f triggers Chrome wrench menu
- // which alt-shift-f does not)
- ? 'alt-shift-'
- // Chrome on other (Ubuntu?)
- : 'alt-'
+ // 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
@@ -55,14 +53,16 @@
|| profile.name === 'konqueror' ) ) {
util.tooltipAccessKeyPrefix = 'ctrl-';
- // Firefox 2.x and later
- } else if ( profile.name === 'firefox' && profile.versionBase > '1' ) {
+ // Firefox/Iceweasel 2.x and later
+ } else if ( ( profile.name === 'firefox' || profile.name === 'iceweasel' )
+ && profile.versionBase > '1' ) {
util.tooltipAccessKeyPrefix = 'alt-shift-';
}
/* Fill $content var */
util.$content = ( function () {
- var $content, selectors = [
+ var i, l, $content, selectors;
+ selectors = [
// The preferred standard for setting $content (class="mw-body")
// You may also use (class="mw-body mw-body-primary") if you use
// mw-body in multiple locations.
@@ -94,7 +94,7 @@
// not inserted bodytext yet. But in any case <body> should always exist
'body'
];
- for ( var i = 0, l = selectors.length; i < l; i++ ) {
+ for ( i = 0, l = selectors.length; i < l; i++ ) {
$content = $( selectors[i] ).first();
if ( $content.length ) {
return $content;
@@ -106,29 +106,32 @@
} )();
// Table of contents toggle
- $tocTitle = $( '#toctitle' );
- $tocToggleLink = $( '#togglelink' );
- // Only add it if there is a TOC and there is no toggle added already
- if ( $( '#toc' ).length && $tocTitle.length && !$tocToggleLink.length ) {
- hideTocCookie = $.cookie( 'mw_hidetoc' );
+ mw.hook( 'wikipage.content' ).add( function () {
+ var $tocTitle, $tocToggleLink, hideTocCookie;
+ $tocTitle = $( '#toctitle' );
+ $tocToggleLink = $( '#togglelink' );
+ // Only add it if there is a TOC and there is no toggle added already
+ if ( $( '#toc' ).length && $tocTitle.length && !$tocToggleLink.length ) {
+ hideTocCookie = $.cookie( 'mw_hidetoc' );
$tocToggleLink = $( '<a href="#" class="internal" id="togglelink"></a>' )
.text( mw.msg( 'hidetoc' ) )
.click( function ( e ) {
e.preventDefault();
util.toggleToc( $(this) );
} );
- $tocTitle.append(
- $tocToggleLink
- .wrap( '<span class="toctoggle"></span>' )
- .parent()
- .prepend( '&nbsp;[' )
- .append( ']&nbsp;' )
- );
-
- if ( hideTocCookie === '1' ) {
- util.toggleToc( $tocToggleLink );
+ $tocTitle.append(
+ $tocToggleLink
+ .wrap( '<span class="toctoggle"></span>' )
+ .parent()
+ .prepend( '&nbsp;[' )
+ .append( ']&nbsp;' )
+ );
+
+ if ( hideTocCookie === '1' ) {
+ util.toggleToc( $tocToggleLink );
+ }
}
- }
+ } );
},
/* Main body */
@@ -136,7 +139,7 @@
/**
* Encode the string like PHP's rawurlencode
*
- * @param str string String to be encoded
+ * @param {string} str String to be encoded.
*/
rawurlencode: function ( str ) {
str = String( str );
@@ -150,7 +153,7 @@
* We want / and : to be included as literal characters in our title URLs
* as they otherwise fatally break the title
*
- * @param str string String to be encoded
+ * @param {string} str String to be encoded.
*/
wikiUrlencode: function ( str ) {
return util.rawurlencode( str )
@@ -158,19 +161,26 @@
},
/**
- * Get the link to a page name (relative to wgServer)
+ * Get the link to a page name (relative to `wgServer`),
*
- * @param str String: Page name to get the link for.
- * @return String: Location for a page with name of 'str' or boolean false on error.
+ * @param {string} str Page name to get the link for.
+ * @param {Object} params A mapping of query parameter names to values,
+ * e.g. { action: 'edit' }. Optional.
+ * @return {string} Location for a page with name of `str` or boolean false on error.
*/
- wikiGetlink: function ( str ) {
- return mw.config.get( 'wgArticlePath' ).replace( '$1',
+ getUrl: function ( str, params ) {
+ var url = mw.config.get( 'wgArticlePath' ).replace( '$1',
util.wikiUrlencode( typeof str === 'string' ? str : mw.config.get( 'wgPageName' ) ) );
+ if ( params && !$.isEmptyObject( params ) ) {
+ url += url.indexOf( '?' ) !== -1 ? '&' : '?';
+ url += $.param( params );
+ }
+ return url;
},
/**
* Get address to a script in the wiki root.
- * For index.php use mw.config.get( 'wgScript' )
+ * For index.php use `mw.config.get( 'wgScript' )`.
*
* @since 1.18
* @param str string Name of script (eg. 'api'), defaults to 'index'
@@ -190,20 +200,18 @@
/**
* Append a new style block to the head and return the CSSStyleSheet object.
- * Use .ownerNode to access the <style> element, or use mw.loader.addStyleTag.
+ * Use .ownerNode to access the `<style>` element, or use mw.loader#addStyleTag.
* This function returns the styleSheet object for convience (due to cross-browsers
* difference as to where it is located).
- * @example
- * <code>
- * var sheet = mw.util.addCSS('.foobar { display: none; }');
- * $(foo).click(function () {
- * // Toggle the sheet on and off
- * sheet.disabled = !sheet.disabled;
- * });
- * </code>
*
- * @param text string CSS to be appended
- * @return CSSStyleSheet (use .ownerNode to get to the <style> element)
+ * var sheet = mw.util.addCSS('.foobar { display: none; }');
+ * $(foo).click(function () {
+ * // Toggle the sheet on and off
+ * sheet.disabled = !sheet.disabled;
+ * });
+ *
+ * @param {string} text CSS to be appended
+ * @return {CSSStyleSheet} Use .ownerNode to get to the `<style>` element.
*/
addCSS: function ( text ) {
var s = mw.loader.addStyleTag( text );
@@ -213,10 +221,10 @@
/**
* Hide/show the table of contents element
*
- * @param $toggleLink jQuery A jQuery object of the toggle link.
- * @param callback function Function to be called after the toggle is
- * completed (including the animation) (optional)
- * @return mixed Boolean visibility of the toc (true if it's visible)
+ * @param {jQuery} $toggleLink A jQuery object of the toggle link.
+ * @param {Function} [callback] Function to be called after the toggle is
+ * completed (including the animation).
+ * @return {Mixed} Boolean visibility of the toc (true if it's visible)
* or Null if there was no table of contents.
*/
toggleToc: function ( $toggleLink, callback ) {
@@ -253,12 +261,14 @@
* Grab the URL parameter value for the given parameter.
* Returns null if not found.
*
- * @param param string The parameter name.
- * @param url string URL to search through (optional)
- * @return mixed Parameter value or null.
+ * @param {string} param The parameter name.
+ * @param {string} [url=document.location.href] URL to search through, defaulting to the current document's URL.
+ * @return {Mixed} Parameter value or null.
*/
getParamValue: function ( param, url ) {
- url = url || document.location.href;
+ if ( url === undefined ) {
+ url = document.location.href;
+ }
// Get last match, stop at hash
var re = new RegExp( '^[^#]*[&?]' + $.escapeRE( param ) + '=([^&#]*)' ),
m = re.exec( url );
@@ -271,17 +281,26 @@
},
/**
- * @var string
+ * @property {string}
* Access key prefix. Will be re-defined based on browser/operating system
- * detection in mw.util.init().
+ * detection in mw.util#init.
*/
tooltipAccessKeyPrefix: 'alt-',
/**
- * @var RegExp
+ * @property {RegExp}
* Regex to match accesskey tooltips.
+ *
+ * Should match:
+ *
+ * - "ctrl-option-"
+ * - "alt-shift-"
+ * - "ctrl-alt-"
+ * - "ctrl-"
+ *
+ * The accesskey is matched in group $6.
*/
- tooltipAccessKeyRegexp: /\[(ctrl-)?(alt-)?(shift-)?(esc-)?(.)\]$/,
+ tooltipAccessKeyRegexp: /\[(ctrl-)?(option-)?(alt-)?(shift-)?(esc-)?(.)\]$/,
/**
* Add the appropriate prefix to the accesskey shown in the tooltip.
@@ -289,8 +308,7 @@
* otherwise, all the nodes that will probably have accesskeys by
* default are updated.
*
- * @param $nodes {Array|jQuery} [optional] A jQuery object, or array
- * of elements to update.
+ * @param {Array|jQuery} [$nodes] A jQuery object, or array of nodes to update.
*/
updateTooltipAccessKeys: function ( $nodes ) {
if ( !$nodes ) {
@@ -303,18 +321,18 @@
}
$nodes.attr( 'title', function ( i, val ) {
- if ( val && util.tooltipAccessKeyRegexp.exec( val ) ) {
+ if ( val && util.tooltipAccessKeyRegexp.test( val ) ) {
return val.replace( util.tooltipAccessKeyRegexp,
- '[' + util.tooltipAccessKeyPrefix + '$5]' );
+ '[' + util.tooltipAccessKeyPrefix + '$6]' );
}
return val;
} );
},
/*
- * @var jQuery
- * A jQuery object that refers to the content area element
- * Populated by init().
+ * @property {jQuery}
+ * A jQuery object that refers to the content area element.
+ * Populated by #init.
*/
$content: null,
@@ -329,28 +347,28 @@
*
* By default the new link will be added to the end of the list. To
* add the link before a given existing item, pass the DOM node
- * (document.getElementById( 'foobar' )) or the jQuery-selector
- * ( '#foobar' ) of that item.
+ * (e.g. `document.getElementById( 'foobar' )`) or a jQuery-selector
+ * (e.g. `'#foobar'`) for that item.
*
- * @example mw.util.addPortletLink(
- * 'p-tb', 'http://mediawiki.org/',
- * 'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org ', 'm', '#t-print'
- * )
+ * mw.util.addPortletLink(
+ * 'p-tb', 'http://mediawiki.org/',
+ * 'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org ', 'm', '#t-print'
+ * );
*
- * @param portlet string ID of the target portlet ( 'p-cactions' or 'p-personal' etc.)
- * @param href string Link URL
- * @param text string Link text
- * @param id string ID of the new item, should be unique and preferably have
- * the appropriate prefix ( 'ca-', 'pt-', 'n-' or 't-' )
- * @param tooltip string Text to show when hovering over the link, without accesskey suffix
- * @param accesskey string Access key to activate this link (one character, try
- * to avoid conflicts. Use $( '[accesskey=x]' ).get() in the console to
- * see if 'x' is already used.
- * @param nextnode mixed DOM Node or jQuery-selector string of the item that the new
- * item should be added before, should be another item in the same
- * list, it will be ignored otherwise
+ * @param {string} portlet ID of the target portlet ( 'p-cactions' or 'p-personal' etc.)
+ * @param {string} href Link URL
+ * @param {string} text Link text
+ * @param {string} [id] ID of the new item, should be unique and preferably have
+ * the appropriate prefix ( 'ca-', 'pt-', 'n-' or 't-' )
+ * @param {string} [tooltip] Text to show when hovering over the link, without accesskey suffix
+ * @param {string} [accesskey] Access key to activate this link (one character, try
+ * to avoid conflicts. Use `$( '[accesskey=x]' ).get()` in the console to
+ * see if 'x' is already used.
+ * @param {HTMLElement|jQuery|string} [nextnode] Element or jQuery-selector string to the item that
+ * the new item should be added before, should be another item in the same
+ * list, it will be ignored otherwise
*
- * @return mixed The DOM Node of the added item (a ListItem or Anchor element,
+ * @return {HTMLElement|null} The added element (a ListItem or Anchor element,
* depending on the skin) or null if no element was added to the document.
*/
addPortletLink: function ( portlet, href, text, id, tooltip, accesskey, nextnode ) {
@@ -366,88 +384,86 @@
$link.attr( 'title', tooltip );
}
- // Some skins don't have any portlets
- // just add it to the bottom of their 'sidebar' element as a fallback
- switch ( mw.config.get( 'skin' ) ) {
- case 'standard':
- case 'cologneblue':
- $( '#quickbar' ).append( $link.after( '<br/>' ) );
- return $link[0];
- case 'nostalgia':
- $( '#searchform' ).before( $link ).before( ' &#124; ' );
- return $link[0];
- default: // Skins like chick, modern, monobook, myskin, simple, vector...
-
- // Select the specified portlet
- $portlet = $( '#' + portlet );
- if ( $portlet.length === 0 ) {
- return null;
- }
- // Select the first (most likely only) unordered list inside the portlet
- $ul = $portlet.find( 'ul' ).eq( 0 );
+ // Select the specified portlet
+ $portlet = $( '#' + portlet );
+ if ( $portlet.length === 0 ) {
+ return null;
+ }
+ // Select the first (most likely only) unordered list inside the portlet
+ $ul = $portlet.find( 'ul' ).eq( 0 );
- // If it didn't have an unordered list yet, create it
- if ( $ul.length === 0 ) {
+ // If it didn't have an unordered list yet, create it
+ if ( $ul.length === 0 ) {
- $ul = $( '<ul>' );
+ $ul = $( '<ul>' );
- // If there's no <div> inside, append it to the portlet directly
- if ( $portlet.find( 'div:first' ).length === 0 ) {
- $portlet.append( $ul );
- } else {
- // otherwise if there's a div (such as div.body or div.pBody)
- // append the <ul> to last (most likely only) div
- $portlet.find( 'div' ).eq( -1 ).append( $ul );
- }
- }
- // Just in case..
- if ( $ul.length === 0 ) {
- return null;
+ // If there's no <div> inside, append it to the portlet directly
+ if ( $portlet.find( 'div:first' ).length === 0 ) {
+ $portlet.append( $ul );
+ } else {
+ // otherwise if there's a div (such as div.body or div.pBody)
+ // append the <ul> to last (most likely only) div
+ $portlet.find( 'div' ).eq( -1 ).append( $ul );
}
+ }
+ // Just in case..
+ if ( $ul.length === 0 ) {
+ return null;
+ }
- // Unhide portlet if it was hidden before
- $portlet.removeClass( 'emptyPortlet' );
+ // Unhide portlet if it was hidden before
+ $portlet.removeClass( 'emptyPortlet' );
- // Wrap the anchor tag in a list item (and a span if $portlet is a Vector tab)
- // and back up the selector to the list item
- if ( $portlet.hasClass( 'vectorTabs' ) ) {
- $item = $link.wrap( '<li><span></span></li>' ).parent().parent();
- } else {
- $item = $link.wrap( '<li></li>' ).parent();
- }
+ // Wrap the anchor tag in a list item (and a span if $portlet is a Vector tab)
+ // and back up the selector to the list item
+ if ( $portlet.hasClass( 'vectorTabs' ) ) {
+ $item = $link.wrap( '<li><span></span></li>' ).parent().parent();
+ } else {
+ $item = $link.wrap( '<li></li>' ).parent();
+ }
- // Implement the properties passed to the function
- if ( id ) {
- $item.attr( 'id', id );
- }
+ // Implement the properties passed to the function
+ if ( id ) {
+ $item.attr( 'id', id );
+ }
+
+ if ( tooltip ) {
+ // Trim any existing accesskey hint and the trailing space
+ tooltip = $.trim( tooltip.replace( util.tooltipAccessKeyRegexp, '' ) );
if ( accesskey ) {
- $link.attr( 'accesskey', accesskey );
tooltip += ' [' + accesskey + ']';
- $link.attr( 'title', tooltip );
}
- if ( accesskey && tooltip ) {
+ $link.attr( 'title', tooltip );
+ if ( accesskey ) {
util.updateTooltipAccessKeys( $link );
}
+ }
- // Where to put our node ?
- // - nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js)
- if ( nextnode && nextnode.parentNode === $ul[0] ) {
- $(nextnode).before( $item );
-
- // - nextnode is a CSS selector for jQuery
- } else if ( typeof nextnode === 'string' && $ul.find( nextnode ).length !== 0 ) {
- $ul.find( nextnode ).eq( 0 ).before( $item );
+ if ( accesskey ) {
+ $link.attr( 'accesskey', accesskey );
+ }
- // If the jQuery selector isn't found within the <ul>,
- // or if nextnode was invalid or not passed at all,
- // then just append it at the end of the <ul> (this is the default behaviour)
- } else {
+ if ( nextnode ) {
+ if ( nextnode.nodeType || typeof nextnode === 'string' ) {
+ // nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js)
+ // or nextnode is a CSS selector for jQuery
+ nextnode = $ul.find( nextnode );
+ } else if ( !nextnode.jquery || ( nextnode.length && nextnode[0].parentNode !== $ul[0] ) ) {
+ // Fallback
$ul.append( $item );
+ return $item[0];
}
+ if ( nextnode.length === 1 ) {
+ // nextnode is a jQuery object that represents exactly one element
+ nextnode.before( $item );
+ return $item[0];
+ }
+ }
+ // Fallback (this is the default behavior)
+ $ul.append( $item );
+ return $item[0];
- return $item[0];
- }
},
/**
@@ -455,9 +471,9 @@
* something, replacing any previous message.
* Calling with no arguments, with an empty string or null will hide the message
*
- * @param message {mixed} The DOM-element, jQuery object or HTML-string to be put inside the message box.
+ * @param {Mixed} message The DOM-element, jQuery object or HTML-string to be put inside the message box.
* to allow CSS/JS to hide different boxes. null = no class used.
- * @depreceated Use mw.notify
+ * @deprecated since 1.20 Use mw#notify
*/
jsMessage: function ( message ) {
if ( !arguments.length || message === '' || message === null ) {
@@ -475,87 +491,80 @@
* according to HTML5 specification. Please note the specification
* does not validate a domain with one character.
*
- * @todo FIXME: should be moved to or replaced by a JavaScript validation module.
+ * FIXME: should be moved to or replaced by a validation module.
*
- * @param mailtxt string E-mail address to be validated.
- * @return mixed Null if mailtxt was an empty string, otherwise true/false
- * is determined by validation.
+ * @param {string} mailtxt E-mail address to be validated.
+ * @return {boolean|null} Null if `mailtxt` was an empty string, otherwise true/false
+ * as determined by validation.
*/
validateEmail: function ( mailtxt ) {
- var rfc5322_atext, rfc1034_ldh_str, HTML5_email_regexp;
+ var rfc5322Atext, rfc1034LdhStr, html5EmailRegexp;
if ( mailtxt === '' ) {
return null;
}
- /**
- * HTML5 defines a string as valid e-mail address if it matches
- * the ABNF:
- * 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
- * With:
- * - atext : defined in RFC 5322 section 3.2.3
- * - ldh-str : defined in RFC 1034 section 3.5
- *
- * (see STD 68 / RFC 5234 http://tools.ietf.org/html/std68):
- */
-
- /**
- * First, define the RFC 5322 'atext' which is pretty easy:
- * atext = ALPHA / DIGIT / ; Printable US-ASCII
- "!" / "#" / ; characters not including
- "$" / "%" / ; specials. Used for atoms.
- "&" / "'" /
- "*" / "+" /
- "-" / "/" /
- "=" / "?" /
- "^" / "_" /
- "`" / "{" /
- "|" / "}" /
- "~"
- */
- rfc5322_atext = "a-z0-9!#$%&'*+\\-/=?^_`{|}~";
-
- /**
- * Next define the RFC 1034 'ldh-str'
- * <domain> ::= <subdomain> | " "
- * <subdomain> ::= <label> | <subdomain> "." <label>
- * <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
- * <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
- * <let-dig-hyp> ::= <let-dig> | "-"
- * <let-dig> ::= <letter> | <digit>
- */
- rfc1034_ldh_str = "a-z0-9\\-";
-
- HTML5_email_regexp = new RegExp(
+ // HTML5 defines a string as valid e-mail address if it matches
+ // the ABNF:
+ // 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
+ // With:
+ // - atext : defined in RFC 5322 section 3.2.3
+ // - ldh-str : defined in RFC 1034 section 3.5
+ //
+ // (see STD 68 / RFC 5234 http://tools.ietf.org/html/std68)
+ // First, define the RFC 5322 'atext' which is pretty easy:
+ // atext = ALPHA / DIGIT / ; Printable US-ASCII
+ // "!" / "#" / ; characters not including
+ // "$" / "%" / ; specials. Used for atoms.
+ // "&" / "'" /
+ // "*" / "+" /
+ // "-" / "/" /
+ // "=" / "?" /
+ // "^" / "_" /
+ // "`" / "{" /
+ // "|" / "}" /
+ // "~"
+ rfc5322Atext = 'a-z0-9!#$%&\'*+\\-/=?^_`{|}~';
+
+ // Next define the RFC 1034 'ldh-str'
+ // <domain> ::= <subdomain> | " "
+ // <subdomain> ::= <label> | <subdomain> "." <label>
+ // <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
+ // <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
+ // <let-dig-hyp> ::= <let-dig> | "-"
+ // <let-dig> ::= <letter> | <digit>
+ rfc1034LdhStr = 'a-z0-9\\-';
+
+ html5EmailRegexp = new RegExp(
// start of string
'^'
+
// User part which is liberal :p
- '[' + rfc5322_atext + '\\.]+'
+ '[' + rfc5322Atext + '\\.]+'
+
// 'at'
'@'
+
// Domain first part
- '[' + rfc1034_ldh_str + ']+'
+ '[' + rfc1034LdhStr + ']+'
+
// Optional second part and following are separated by a dot
- '(?:\\.[' + rfc1034_ldh_str + ']+)*'
+ '(?:\\.[' + rfc1034LdhStr + ']+)*'
+
// End of string
'$',
// RegExp is case insensitive
'i'
);
- return (null !== mailtxt.match( HTML5_email_regexp ) );
+ return (null !== mailtxt.match( html5EmailRegexp ) );
},
/**
* Note: borrows from IP::isIPv4
*
- * @param address string
- * @param allowBlock boolean
- * @return boolean
+ * @param {string} address
+ * @param {boolean} allowBlock
+ * @return {boolean}
*/
isIPv4Address: function ( address, allowBlock ) {
if ( typeof address !== 'string' ) {
@@ -572,9 +581,9 @@
/**
* Note: borrows from IP::isIPv6
*
- * @param address string
- * @param allowBlock boolean
- * @return boolean
+ * @param {string} address
+ * @param {boolean} allowBlock
+ * @return {boolean}
*/
isIPv6Address: function ( address, allowBlock ) {
if ( typeof address !== 'string' ) {
@@ -603,6 +612,13 @@
}
};
+ /**
+ * @method wikiGetlink
+ * @inheritdoc #getUrl
+ * @deprecated since 1.23 Use #getUrl instead.
+ */
+ mw.log.deprecate( util, 'wikiGetlink', util.getUrl, 'Use mw.util.getUrl instead.' );
+
mw.util = util;
}( mediaWiki, jQuery ) );
diff --git a/resources/startup.js b/resources/startup.js
index 7951af06..b6a27d2d 100644
--- a/resources/startup.js
+++ b/resources/startup.js
@@ -3,31 +3,48 @@
* continue loading the jquery and mediawiki modules. This code should work on
* even the most ancient of browsers, so be very careful when editing.
*/
+
/**
* Returns false when run in a black-listed browser
*
* This function will be deleted after it's used, so do not expand it to be
- * generally useful beyond startup
+ * generally useful beyond startup.
*
- * jQuery has minimum requirements of:
- * * Internet Explorer 6.0+
- * * Firefox 3.6+
- * * Safari 5.0+
- * * Opera 11+
- * * Chrome
+ * See also:
+ * - https://www.mediawiki.org/wiki/Compatibility#Browser
+ * - http://jquerymobile.com/gbs/
+ * - http://jquery.com/browser-support/
*/
-function isCompatible() {
- // IE < 6.0
- if ( navigator.appVersion.indexOf( 'MSIE' ) !== -1
- && parseFloat( navigator.appVersion.split( 'MSIE' )[1] ) < 6 )
- {
- return false;
+
+/*jshint unused: false */
+function isCompatible( ua ) {
+ if ( ua === undefined ) {
+ ua = navigator.userAgent;
}
- // @todo FIXME: Firefox < 3.6
- // @todo FIXME: Safari < 5.0
- // @todo FIXME: Opera < 11
- return true;
+
+ // MediaWiki JS or jQuery is known to have issues with:
+ return !(
+ // Internet Explorer < 6
+ ( ua.indexOf( 'MSIE' ) !== -1 && parseFloat( ua.split( 'MSIE' )[1] ) < 6 ) ||
+ // Firefox < 3
+ ( ua.indexOf( 'Firefox/' ) !== -1 && parseFloat( ua.split( 'Firefox/' )[1] ) < 3 ) ||
+ // BlackBerry < 6
+ ua.match( /BlackBerry[^\/]*\/[1-5]\./ ) ||
+ // Open WebOS < 1.5
+ ua.match( /webOS\/1\.[0-4]/ ) ||
+ // Anything PlayStation based.
+ ua.match( /PlayStation/i ) ||
+ // Any Symbian based browsers
+ ua.match( /SymbianOS|Series60/ ) ||
+ // Any NetFront based browser
+ ua.match( /NetFront/ ) ||
+ // Opera Mini, all versions
+ ua.match( /Opera Mini/ ) ||
+ // Nokia's Ovi Browser
+ ua.match( /S40OviBrowser/ )
+ );
}
+
/**
- * The startUp() function will be generated and added here (at the bottom)
+ * The startUp() function will be auto-generated and added below.
*/