diff options
Diffstat (limited to 'resources')
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 Binary files differnew file mode 100644 index 00000000..3611ae4a --- /dev/null +++ b/resources/jquery.chosen/chosen-sprite.png diff --git a/resources/jquery.chosen/chosen-sprite@2x.png b/resources/jquery.chosen/chosen-sprite@2x.png Binary files differnew file mode 100644 index 00000000..bd61d963 --- /dev/null +++ b/resources/jquery.chosen/chosen-sprite@2x.png 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 Binary files differindex fef8c4b5..ef17cc32 100644 --- a/resources/jquery.tipsy/images/tipsy.png +++ b/resources/jquery.tipsy/images/tipsy.png 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 Binary files differindex 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 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 Binary files differindex 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 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 Binary files differindex 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 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 Binary files differindex 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 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 Binary files differindex 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 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 Binary files differindex 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 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 Binary files differindex 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 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 Binary files differindex 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 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 Binary files differindex 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 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 Binary files differindex 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 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 Binary files differindex 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 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 Binary files differdeleted file mode 100644 index 8f7cf74e..00000000 --- a/resources/jquery.ui/themes/vector/images/button-blue-hover-large.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-blue-hover.png b/resources/jquery.ui/themes/vector/images/button-blue-hover.png Binary files differdeleted file mode 100644 index 1fc28163..00000000 --- a/resources/jquery.ui/themes/vector/images/button-blue-hover.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-blue-large.png b/resources/jquery.ui/themes/vector/images/button-blue-large.png Binary files differdeleted file mode 100644 index 22ee5e59..00000000 --- a/resources/jquery.ui/themes/vector/images/button-blue-large.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-blue.png b/resources/jquery.ui/themes/vector/images/button-blue.png Binary files differdeleted file mode 100644 index 2e6d121a..00000000 --- a/resources/jquery.ui/themes/vector/images/button-blue.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-disabled-blue.png b/resources/jquery.ui/themes/vector/images/button-disabled-blue.png Binary files differdeleted file mode 100644 index 28eb1fcd..00000000 --- a/resources/jquery.ui/themes/vector/images/button-disabled-blue.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-disabled-green.png b/resources/jquery.ui/themes/vector/images/button-disabled-green.png Binary files differdeleted file mode 100644 index 0e29d85b..00000000 --- a/resources/jquery.ui/themes/vector/images/button-disabled-green.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-disabled-red.png b/resources/jquery.ui/themes/vector/images/button-disabled-red.png Binary files differdeleted file mode 100644 index ede69988..00000000 --- a/resources/jquery.ui/themes/vector/images/button-disabled-red.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-disabled.png b/resources/jquery.ui/themes/vector/images/button-disabled.png Binary files differdeleted file mode 100644 index e4e4ec1c..00000000 --- a/resources/jquery.ui/themes/vector/images/button-disabled.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-down-blue.png b/resources/jquery.ui/themes/vector/images/button-down-blue.png Binary files differdeleted file mode 100644 index 766e5746..00000000 --- a/resources/jquery.ui/themes/vector/images/button-down-blue.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-down-green.png b/resources/jquery.ui/themes/vector/images/button-down-green.png Binary files differdeleted file mode 100644 index 90969c39..00000000 --- a/resources/jquery.ui/themes/vector/images/button-down-green.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-down-red.png b/resources/jquery.ui/themes/vector/images/button-down-red.png Binary files differdeleted file mode 100644 index f6257298..00000000 --- a/resources/jquery.ui/themes/vector/images/button-down-red.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-down.png b/resources/jquery.ui/themes/vector/images/button-down.png Binary files differdeleted file mode 100644 index c6467571..00000000 --- a/resources/jquery.ui/themes/vector/images/button-down.png +++ /dev/null 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 Binary files differdeleted file mode 100644 index dd8851e1..00000000 --- a/resources/jquery.ui/themes/vector/images/button-green-hover-large.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-green-hover.png b/resources/jquery.ui/themes/vector/images/button-green-hover.png Binary files differdeleted file mode 100644 index 19c39911..00000000 --- a/resources/jquery.ui/themes/vector/images/button-green-hover.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-green-large.png b/resources/jquery.ui/themes/vector/images/button-green-large.png Binary files differdeleted file mode 100644 index a8e830eb..00000000 --- a/resources/jquery.ui/themes/vector/images/button-green-large.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-green.png b/resources/jquery.ui/themes/vector/images/button-green.png Binary files differdeleted file mode 100644 index 54c418e9..00000000 --- a/resources/jquery.ui/themes/vector/images/button-green.png +++ /dev/null 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 Binary files differdeleted file mode 100644 index f76f7b05..00000000 --- a/resources/jquery.ui/themes/vector/images/button-large-disabled-green.png +++ /dev/null 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 Binary files differdeleted file mode 100644 index f997431b..00000000 --- a/resources/jquery.ui/themes/vector/images/button-large-off-green.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-off-blue.png b/resources/jquery.ui/themes/vector/images/button-off-blue.png Binary files differdeleted file mode 100644 index 82dedb53..00000000 --- a/resources/jquery.ui/themes/vector/images/button-off-blue.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-off-green.png b/resources/jquery.ui/themes/vector/images/button-off-green.png Binary files differdeleted file mode 100644 index 109907f0..00000000 --- a/resources/jquery.ui/themes/vector/images/button-off-green.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-off-red.png b/resources/jquery.ui/themes/vector/images/button-off-red.png Binary files differdeleted file mode 100644 index 5a85b8aa..00000000 --- a/resources/jquery.ui/themes/vector/images/button-off-red.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-off.png b/resources/jquery.ui/themes/vector/images/button-off.png Binary files differdeleted file mode 100644 index cc5eb119..00000000 --- a/resources/jquery.ui/themes/vector/images/button-off.png +++ /dev/null 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 Binary files differdeleted file mode 100644 index 6f0dbd5d..00000000 --- a/resources/jquery.ui/themes/vector/images/button-orange-hover-large.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-orange-hover.png b/resources/jquery.ui/themes/vector/images/button-orange-hover.png Binary files differdeleted file mode 100644 index a1077c58..00000000 --- a/resources/jquery.ui/themes/vector/images/button-orange-hover.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-orange-large.png b/resources/jquery.ui/themes/vector/images/button-orange-large.png Binary files differdeleted file mode 100644 index 3d7f37f6..00000000 --- a/resources/jquery.ui/themes/vector/images/button-orange-large.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-orange.png b/resources/jquery.ui/themes/vector/images/button-orange.png Binary files differdeleted file mode 100644 index 02347cf9..00000000 --- a/resources/jquery.ui/themes/vector/images/button-orange.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-over-blue.png b/resources/jquery.ui/themes/vector/images/button-over-blue.png Binary files differdeleted file mode 100644 index 9edb7aa2..00000000 --- a/resources/jquery.ui/themes/vector/images/button-over-blue.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-over-green.png b/resources/jquery.ui/themes/vector/images/button-over-green.png Binary files differdeleted file mode 100644 index 47a0b1b8..00000000 --- a/resources/jquery.ui/themes/vector/images/button-over-green.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-over-red.png b/resources/jquery.ui/themes/vector/images/button-over-red.png Binary files differdeleted file mode 100644 index a2445365..00000000 --- a/resources/jquery.ui/themes/vector/images/button-over-red.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-over.png b/resources/jquery.ui/themes/vector/images/button-over.png Binary files differdeleted file mode 100644 index 558f1f80..00000000 --- a/resources/jquery.ui/themes/vector/images/button-over.png +++ /dev/null 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 Binary files differdeleted file mode 100644 index 11ccef06..00000000 --- a/resources/jquery.ui/themes/vector/images/button-red-hover-large.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-red-hover.png b/resources/jquery.ui/themes/vector/images/button-red-hover.png Binary files differdeleted file mode 100644 index 55a61743..00000000 --- a/resources/jquery.ui/themes/vector/images/button-red-hover.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-red-large.png b/resources/jquery.ui/themes/vector/images/button-red-large.png Binary files differdeleted file mode 100644 index 8d089efd..00000000 --- a/resources/jquery.ui/themes/vector/images/button-red-large.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/button-red.png b/resources/jquery.ui/themes/vector/images/button-red.png Binary files differdeleted file mode 100644 index 7a292fc1..00000000 --- a/resources/jquery.ui/themes/vector/images/button-red.png +++ /dev/null diff --git a/resources/jquery.ui/themes/vector/images/titlebar-fade.png b/resources/jquery.ui/themes/vector/images/titlebar-fade.png Binary files differindex f9fde8b3..12a80c88 100644 --- a/resources/jquery.ui/themes/vector/images/titlebar-fade.png +++ b/resources/jquery.ui/themes/vector/images/titlebar-fade.png 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 Binary files differindex 83d6ff84..84ed2a2d 100644 --- a/resources/jquery/images/jquery.arrowSteps.divider-ltr.png +++ b/resources/jquery/images/jquery.arrowSteps.divider-ltr.png diff --git a/resources/jquery/images/jquery.arrowSteps.divider-rtl.png b/resources/jquery/images/jquery.arrowSteps.divider-rtl.png Binary files differindex 529d7b84..7cfbfeba 100644 --- a/resources/jquery/images/jquery.arrowSteps.divider-rtl.png +++ b/resources/jquery/images/jquery.arrowSteps.divider-rtl.png diff --git a/resources/jquery/images/jquery.arrowSteps.head-ltr.png b/resources/jquery/images/jquery.arrowSteps.head-ltr.png Binary files differindex 3289617d..eb070280 100644 --- a/resources/jquery/images/jquery.arrowSteps.head-ltr.png +++ b/resources/jquery/images/jquery.arrowSteps.head-ltr.png diff --git a/resources/jquery/images/jquery.arrowSteps.head-rtl.png b/resources/jquery/images/jquery.arrowSteps.head-rtl.png Binary files differindex 3d9f70cb..7ea2fdb5 100644 --- a/resources/jquery/images/jquery.arrowSteps.head-rtl.png +++ b/resources/jquery/images/jquery.arrowSteps.head-rtl.png diff --git a/resources/jquery/images/jquery.arrowSteps.tail-ltr.png b/resources/jquery/images/jquery.arrowSteps.tail-ltr.png Binary files differindex 92b872b2..3ad990b6 100644 --- a/resources/jquery/images/jquery.arrowSteps.tail-ltr.png +++ b/resources/jquery/images/jquery.arrowSteps.tail-ltr.png diff --git a/resources/jquery/images/marker.png b/resources/jquery/images/marker.png Binary files differindex 3929bbb5..19efb6ce 100644 --- a/resources/jquery/images/marker.png +++ b/resources/jquery/images/marker.png diff --git a/resources/jquery/images/mask.png b/resources/jquery/images/mask.png Binary files differindex b0a4d406..fe08de0e 100644 --- a/resources/jquery/images/mask.png +++ b/resources/jquery/images/mask.png 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 ( [text] ) - var $toggleLink = - $( '<a href="#"></a>' ) - .text( collapsetext ) - .wrap( '<span class="mw-collapsible-toggle"></span>' ) - .parent() - .prepend( ' [' ) - .append( '] ' ) - .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( ' [' ) + .append( '] ' ) + .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 "&"; - case "<": return "<"; - case ">": return ">"; - default: return s; + case '\'': + return '''; + case '"': + return '"'; + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; } }); } @@ -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 ? " " : " "; }, - 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 ? "<" : "<", close = QUnit.jsDump.HTML ? ">" : ">", 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 Binary files differnew file mode 100644 index 00000000..8ec604ea --- /dev/null +++ b/resources/mediawiki.action/images/green-checkmark.png 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">×</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 Binary files differdeleted file mode 100644 index 467a555a..00000000 --- a/resources/mediawiki.special/images/arrow-collapsed-ltr.png +++ /dev/null diff --git a/resources/mediawiki.special/images/arrow-collapsed-rtl.png b/resources/mediawiki.special/images/arrow-collapsed-rtl.png Binary files differdeleted file mode 100644 index 2246254f..00000000 --- a/resources/mediawiki.special/images/arrow-collapsed-rtl.png +++ /dev/null diff --git a/resources/mediawiki.special/images/arrow-expanded.png b/resources/mediawiki.special/images/arrow-expanded.png Binary files differdeleted file mode 100644 index 58a9fc66..00000000 --- a/resources/mediawiki.special/images/arrow-expanded.png +++ /dev/null diff --git a/resources/mediawiki.special/images/glyph-people-large.png b/resources/mediawiki.special/images/glyph-people-large.png Binary files differnew file mode 100644 index 00000000..0578be0b --- /dev/null +++ b/resources/mediawiki.special/images/glyph-people-large.png diff --git a/resources/mediawiki.special/images/icon-contributors.png b/resources/mediawiki.special/images/icon-contributors.png Binary files differnew file mode 100644 index 00000000..f933aa69 --- /dev/null +++ b/resources/mediawiki.special/images/icon-contributors.png diff --git a/resources/mediawiki.special/images/icon-edits.png b/resources/mediawiki.special/images/icon-edits.png Binary files differnew file mode 100644 index 00000000..39f4f2de --- /dev/null +++ b/resources/mediawiki.special/images/icon-edits.png diff --git a/resources/mediawiki.special/images/icon-lock.png b/resources/mediawiki.special/images/icon-lock.png Binary files differnew file mode 100644 index 00000000..03f0eecd --- /dev/null +++ b/resources/mediawiki.special/images/icon-lock.png diff --git a/resources/mediawiki.special/images/icon-pages.png b/resources/mediawiki.special/images/icon-pages.png Binary files differnew file mode 100644 index 00000000..59513db2 --- /dev/null +++ b/resources/mediawiki.special/images/icon-pages.png 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 Binary files differnew file mode 100644 index 00000000..b17e578b --- /dev/null +++ b/resources/mediawiki/images/arrow-collapsed-ltr.png diff --git a/resources/mediawiki/images/arrow-collapsed-rtl.png b/resources/mediawiki/images/arrow-collapsed-rtl.png Binary files differnew file mode 100644 index 00000000..a834548e --- /dev/null +++ b/resources/mediawiki/images/arrow-collapsed-rtl.png diff --git a/resources/mediawiki/images/arrow-expanded.png b/resources/mediawiki/images/arrow-expanded.png Binary files differnew file mode 100644 index 00000000..2bec798e --- /dev/null +++ b/resources/mediawiki/images/arrow-expanded.png 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( /'/g, '\'' ) + .replace( /"/g, '"' ) + .replace( /</g, '<' ) + .replace( />/g, '>' ) + .replace( /&/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 '<script>' and '</script>' + // (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="<"/></div> + * + * @class mw.html + * @singleton + */ html: ( function () { function escapeCallback( s ) { switch ( s ) { - case "'": + case '\'': return '''; case '"': return '"'; @@ -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="<"/></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( ' [' ) - .append( '] ' ) - ); - - if ( hideTocCookie === '1' ) { - util.toggleToc( $tocToggleLink ); + $tocTitle.append( + $tocToggleLink + .wrap( '<span class="toctoggle"></span>' ) + .parent() + .prepend( ' [' ) + .append( '] ' ) + ); + + 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( ' | ' ); - 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. */ |