Merge "Return early when page id is less than 1"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 13 Feb 2014 00:33:31 +0000 (00:33 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 13 Feb 2014 00:33:31 +0000 (00:33 +0000)
238 files changed:
.jshintignore
RELEASE-NOTES-1.23
docs/hooks.txt
includes/DefaultSettings.php
includes/WebRequest.php
includes/api/ApiParse.php
includes/api/ApiQueryLangLinks.php
includes/cache/MessageCache.php
includes/clientpool/RedisConnectionPool.php
includes/db/DatabaseSqlite.php
includes/db/LoadMonitor.php
includes/filebackend/SwiftFileBackend.php
includes/filebackend/lockmanager/RedisLockManager.php
includes/job/JobQueueRedis.php
includes/job/aggregator/JobQueueAggregatorRedis.php
includes/libs/MultiHttpClient.php
includes/objectcache/RedisBagOStuff.php
includes/specials/SpecialSearch.php
includes/specials/SpecialUserrights.php
languages/messages/MessagesBe_tarask.php
languages/messages/MessagesCo.php
languages/messages/MessagesDe.php
languages/messages/MessagesDiq.php
languages/messages/MessagesEn.php
languages/messages/MessagesEo.php
languages/messages/MessagesEs.php
languages/messages/MessagesHu.php
languages/messages/MessagesIs.php
languages/messages/MessagesKiu.php
languages/messages/MessagesMk.php
languages/messages/MessagesMn.php
languages/messages/MessagesPl.php
languages/messages/MessagesQqq.php
languages/messages/MessagesSr_ec.php
languages/messages/MessagesSr_el.php
languages/messages/MessagesSv.php
languages/messages/MessagesTe.php
languages/messages/MessagesWar.php
languages/messages/MessagesZh_hans.php
maintenance/jsduck/config.json
maintenance/language/messages.inc
resources/Resources.php
resources/mediawiki.action/mediawiki.action.edit.preview.js
resources/oojs/.gitignore [new file with mode: 0644]
resources/oojs/i18n/ace.json [new file with mode: 0644]
resources/oojs/i18n/af.json [new file with mode: 0644]
resources/oojs/i18n/am.json [new file with mode: 0644]
resources/oojs/i18n/ar.json [new file with mode: 0644]
resources/oojs/i18n/arc.json [new file with mode: 0644]
resources/oojs/i18n/ast.json [new file with mode: 0644]
resources/oojs/i18n/az.json [new file with mode: 0644]
resources/oojs/i18n/ba.json [new file with mode: 0644]
resources/oojs/i18n/bcl.json [new file with mode: 0644]
resources/oojs/i18n/be-tarask.json [new file with mode: 0644]
resources/oojs/i18n/be.json [new file with mode: 0644]
resources/oojs/i18n/bg.json [new file with mode: 0644]
resources/oojs/i18n/bn.json [new file with mode: 0644]
resources/oojs/i18n/br.json [new file with mode: 0644]
resources/oojs/i18n/bs.json [new file with mode: 0644]
resources/oojs/i18n/ca.json [new file with mode: 0644]
resources/oojs/i18n/ce.json [new file with mode: 0644]
resources/oojs/i18n/ckb.json [new file with mode: 0644]
resources/oojs/i18n/co.json [new file with mode: 0644]
resources/oojs/i18n/cs.json [new file with mode: 0644]
resources/oojs/i18n/cu.json [new file with mode: 0644]
resources/oojs/i18n/cy.json [new file with mode: 0644]
resources/oojs/i18n/da.json [new file with mode: 0644]
resources/oojs/i18n/de.json [new file with mode: 0644]
resources/oojs/i18n/diq.json [new file with mode: 0644]
resources/oojs/i18n/dsb.json [new file with mode: 0644]
resources/oojs/i18n/el.json [new file with mode: 0644]
resources/oojs/i18n/eml.json [new file with mode: 0644]
resources/oojs/i18n/en.json [new file with mode: 0644]
resources/oojs/i18n/eo.json [new file with mode: 0644]
resources/oojs/i18n/es.json [new file with mode: 0644]
resources/oojs/i18n/et.json [new file with mode: 0644]
resources/oojs/i18n/eu.json [new file with mode: 0644]
resources/oojs/i18n/fa.json [new file with mode: 0644]
resources/oojs/i18n/fi.json [new file with mode: 0644]
resources/oojs/i18n/fo.json [new file with mode: 0644]
resources/oojs/i18n/fr.json [new file with mode: 0644]
resources/oojs/i18n/frr.json [new file with mode: 0644]
resources/oojs/i18n/fur.json [new file with mode: 0644]
resources/oojs/i18n/gl.json [new file with mode: 0644]
resources/oojs/i18n/gu.json [new file with mode: 0644]
resources/oojs/i18n/he.json [new file with mode: 0644]
resources/oojs/i18n/hi.json [new file with mode: 0644]
resources/oojs/i18n/hr.json [new file with mode: 0644]
resources/oojs/i18n/hsb.json [new file with mode: 0644]
resources/oojs/i18n/hu.json [new file with mode: 0644]
resources/oojs/i18n/hy.json [new file with mode: 0644]
resources/oojs/i18n/ia.json [new file with mode: 0644]
resources/oojs/i18n/id.json [new file with mode: 0644]
resources/oojs/i18n/ie.json [new file with mode: 0644]
resources/oojs/i18n/ilo.json [new file with mode: 0644]
resources/oojs/i18n/is.json [new file with mode: 0644]
resources/oojs/i18n/it.json [new file with mode: 0644]
resources/oojs/i18n/ja.json [new file with mode: 0644]
resources/oojs/i18n/jv.json [new file with mode: 0644]
resources/oojs/i18n/ka.json [new file with mode: 0644]
resources/oojs/i18n/kk-cyrl.json [new file with mode: 0644]
resources/oojs/i18n/ko.json [new file with mode: 0644]
resources/oojs/i18n/krc.json [new file with mode: 0644]
resources/oojs/i18n/kw.json [new file with mode: 0644]
resources/oojs/i18n/ky.json [new file with mode: 0644]
resources/oojs/i18n/lb.json [new file with mode: 0644]
resources/oojs/i18n/lmo.json [new file with mode: 0644]
resources/oojs/i18n/lt.json [new file with mode: 0644]
resources/oojs/i18n/lv.json [new file with mode: 0644]
resources/oojs/i18n/mg.json [new file with mode: 0644]
resources/oojs/i18n/min.json [new file with mode: 0644]
resources/oojs/i18n/mk.json [new file with mode: 0644]
resources/oojs/i18n/ml.json [new file with mode: 0644]
resources/oojs/i18n/mr.json [new file with mode: 0644]
resources/oojs/i18n/ms.json [new file with mode: 0644]
resources/oojs/i18n/nap.json [new file with mode: 0644]
resources/oojs/i18n/nb.json [new file with mode: 0644]
resources/oojs/i18n/nds-nl.json [new file with mode: 0644]
resources/oojs/i18n/nds.json [new file with mode: 0644]
resources/oojs/i18n/ne.json [new file with mode: 0644]
resources/oojs/i18n/nl.json [new file with mode: 0644]
resources/oojs/i18n/nn.json [new file with mode: 0644]
resources/oojs/i18n/om.json [new file with mode: 0644]
resources/oojs/i18n/or.json [new file with mode: 0644]
resources/oojs/i18n/pa.json [new file with mode: 0644]
resources/oojs/i18n/pl.json [new file with mode: 0644]
resources/oojs/i18n/pms.json [new file with mode: 0644]
resources/oojs/i18n/ps.json [new file with mode: 0644]
resources/oojs/i18n/pt-br.json [new file with mode: 0644]
resources/oojs/i18n/pt.json [new file with mode: 0644]
resources/oojs/i18n/qqq.json [new file with mode: 0644]
resources/oojs/i18n/qu.json [new file with mode: 0644]
resources/oojs/i18n/ro.json [new file with mode: 0644]
resources/oojs/i18n/roa-tara.json [new file with mode: 0644]
resources/oojs/i18n/ru.json [new file with mode: 0644]
resources/oojs/i18n/sah.json [new file with mode: 0644]
resources/oojs/i18n/scn.json [new file with mode: 0644]
resources/oojs/i18n/sh.json [new file with mode: 0644]
resources/oojs/i18n/si.json [new file with mode: 0644]
resources/oojs/i18n/sk.json [new file with mode: 0644]
resources/oojs/i18n/sl.json [new file with mode: 0644]
resources/oojs/i18n/sq.json [new file with mode: 0644]
resources/oojs/i18n/sr-ec.json [new file with mode: 0644]
resources/oojs/i18n/sv.json [new file with mode: 0644]
resources/oojs/i18n/sw.json [new file with mode: 0644]
resources/oojs/i18n/ta.json [new file with mode: 0644]
resources/oojs/i18n/te.json [new file with mode: 0644]
resources/oojs/i18n/th.json [new file with mode: 0644]
resources/oojs/i18n/tl.json [new file with mode: 0644]
resources/oojs/i18n/tr.json [new file with mode: 0644]
resources/oojs/i18n/tt-cyrl.json [new file with mode: 0644]
resources/oojs/i18n/ug-arab.json [new file with mode: 0644]
resources/oojs/i18n/uk.json [new file with mode: 0644]
resources/oojs/i18n/uz.json [new file with mode: 0644]
resources/oojs/i18n/vec.json [new file with mode: 0644]
resources/oojs/i18n/vi.json [new file with mode: 0644]
resources/oojs/i18n/vo.json [new file with mode: 0644]
resources/oojs/i18n/wuu.json [new file with mode: 0644]
resources/oojs/i18n/yi.json [new file with mode: 0644]
resources/oojs/i18n/yo.json [new file with mode: 0644]
resources/oojs/i18n/zh-hans.json [new file with mode: 0644]
resources/oojs/i18n/zh-hant.json [new file with mode: 0644]
resources/oojs/i18n/zh-hk.json [new file with mode: 0644]
resources/oojs/i18n/zh-tw.json [new file with mode: 0644]
resources/oojs/images/fade-down.png [new file with mode: 0644]
resources/oojs/images/fade-up.png [new file with mode: 0644]
resources/oojs/images/icons/accept.png [new file with mode: 0644]
resources/oojs/images/icons/accept.svg [new file with mode: 0644]
resources/oojs/images/icons/add-item.png [new file with mode: 0644]
resources/oojs/images/icons/add-item.svg [new file with mode: 0644]
resources/oojs/images/icons/advanced.png [new file with mode: 0644]
resources/oojs/images/icons/advanced.svg [new file with mode: 0644]
resources/oojs/images/icons/alert.png [new file with mode: 0644]
resources/oojs/images/icons/alert.svg [new file with mode: 0644]
resources/oojs/images/icons/arched-arrow-ltr.png [new file with mode: 0644]
resources/oojs/images/icons/arched-arrow-ltr.svg [new file with mode: 0644]
resources/oojs/images/icons/arched-arrow-rtl.png [new file with mode: 0644]
resources/oojs/images/icons/arched-arrow-rtl.svg [new file with mode: 0644]
resources/oojs/images/icons/check.png [new file with mode: 0644]
resources/oojs/images/icons/check.svg [new file with mode: 0644]
resources/oojs/images/icons/clear.png [new file with mode: 0644]
resources/oojs/images/icons/clear.svg [new file with mode: 0644]
resources/oojs/images/icons/close.png [new file with mode: 0644]
resources/oojs/images/icons/close.svg [new file with mode: 0644]
resources/oojs/images/icons/code.png [new file with mode: 0644]
resources/oojs/images/icons/code.svg [new file with mode: 0644]
resources/oojs/images/icons/collapse.png [new file with mode: 0644]
resources/oojs/images/icons/collapse.svg [new file with mode: 0644]
resources/oojs/images/icons/comment.png [new file with mode: 0644]
resources/oojs/images/icons/comment.svg [new file with mode: 0644]
resources/oojs/images/icons/expand.png [new file with mode: 0644]
resources/oojs/images/icons/expand.svg [new file with mode: 0644]
resources/oojs/images/icons/help.png [new file with mode: 0644]
resources/oojs/images/icons/help.svg [new file with mode: 0644]
resources/oojs/images/icons/history.png [new file with mode: 0644]
resources/oojs/images/icons/history.svg [new file with mode: 0644]
resources/oojs/images/icons/link.png [new file with mode: 0644]
resources/oojs/images/icons/link.svg [new file with mode: 0644]
resources/oojs/images/icons/menu.png [new file with mode: 0644]
resources/oojs/images/icons/menu.svg [new file with mode: 0644]
resources/oojs/images/icons/move-ltr.png [new file with mode: 0644]
resources/oojs/images/icons/move-ltr.svg [new file with mode: 0644]
resources/oojs/images/icons/move-rtl.png [new file with mode: 0644]
resources/oojs/images/icons/move-rtl.svg [new file with mode: 0644]
resources/oojs/images/icons/picture.png [new file with mode: 0644]
resources/oojs/images/icons/picture.svg [new file with mode: 0644]
resources/oojs/images/icons/remove-item.png [new file with mode: 0644]
resources/oojs/images/icons/remove-item.svg [new file with mode: 0644]
resources/oojs/images/icons/remove.png [new file with mode: 0644]
resources/oojs/images/icons/remove.svg [new file with mode: 0644]
resources/oojs/images/icons/search.png [new file with mode: 0644]
resources/oojs/images/icons/search.svg [new file with mode: 0644]
resources/oojs/images/icons/settings.png [new file with mode: 0644]
resources/oojs/images/icons/settings.svg [new file with mode: 0644]
resources/oojs/images/icons/tag.png [new file with mode: 0644]
resources/oojs/images/icons/tag.svg [new file with mode: 0644]
resources/oojs/images/icons/window.png [new file with mode: 0644]
resources/oojs/images/icons/window.svg [new file with mode: 0644]
resources/oojs/images/indicators/down.png [new file with mode: 0644]
resources/oojs/images/indicators/down.svg [new file with mode: 0644]
resources/oojs/images/indicators/required.png [new file with mode: 0644]
resources/oojs/images/indicators/required.svg [new file with mode: 0644]
resources/oojs/images/indicators/up.png [new file with mode: 0644]
resources/oojs/images/indicators/up.svg [new file with mode: 0644]
resources/oojs/images/tail.svg [new file with mode: 0644]
resources/oojs/images/textures/pending.gif [new file with mode: 0644]
resources/oojs/images/textures/transparency.png [new file with mode: 0644]
resources/oojs/images/toolbar-shadow.png [new file with mode: 0644]
resources/oojs/oojs-ui.js [new file with mode: 0644]
resources/oojs/oojs-ui.svg.css [new file with mode: 0644]
resources/sinonjs/sinon-1.8.1.js [new file with mode: 0644]
resources/sinonjs/sinon-ie-1.8.1.js [new file with mode: 0644]
skins/vector/components/tabs.less
skins/vector/vector.js
tests/qunit/QUnitTestResources.php
tests/qunit/data/testrunner.js
tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js
thumb.php

index db4ac44..b161f1f 100644 (file)
@@ -11,8 +11,8 @@ extensions/
 node_modules/
 resources/jquery/jquery.appear.js
 resources/jquery/jquery.async.js
-resources/jquery/jquery.cycle.all.js
 resources/jquery/jquery.cookie.js
+resources/jquery/jquery.cycle.all.js
 resources/jquery/jquery.farbtastic.js
 resources/jquery/jquery.form.js
 resources/jquery/jquery.hoverIntent.js
@@ -23,12 +23,13 @@ resources/jquery/jquery.mockjax.js
 resources/jquery/jquery.qunit.js
 resources/jquery/jquery.validate.js
 resources/jquery/jquery.xmldom.js
+resources/jquery.chosen/chosen.jquery.js
 resources/jquery.effects/
 resources/jquery.tipsy/
 resources/jquery.ui/
 resources/mediawiki.libs/
-resources/jquery.chosen/chosen.jquery.js
 resources/oojs/
+resources/sinonjs/
 
 # github.com/jshint/jshint/issues/729
 tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js
index 4fc198a..16bdf1e 100644 (file)
@@ -86,6 +86,8 @@ production.
 * New user accounts' personal and talk pages are now watched by them by default.
 * Added SkinTemplateGetLanguageLink hook to allow changing the html of language
   links.
+* Added MessageCache::get hook as a new way to customize messages across
+  multiple sites.
 
 === Bug fixes in 1.23 ===
 * (bug 41759) The "updated since last visit" markers (on history pages, recent
@@ -164,6 +166,8 @@ production.
   possible page restriction (protection) levels and types.
 * Added prop 'limitreportdata' and 'limitreporthtml' to action=parse.
 * (bug 58627) Provide language names on action=parse&prop=langlinks.
+* Deprecated llurl= in favour of llprop=url for action=query&prop=langlinks.
+* Added llprop=langname and llprop=autonym for action=query&prop=langlinks.
 
 === Languages updated in 1.23 ===
 
@@ -216,6 +220,9 @@ changes to languages because of Bugzilla reports.
   3 headings)" was removed.
 * (bug 52810) Preference "Justify paragraphs" was removed.
 * OutputPage::showErrorPage raises a notice if arguments are incoherent.
+* Thumbnails that keep failing to render in thumb.php will be rate-limited
+  againt further render attempts for 1 hour. $wgAttemptFailureEpoch can be
+  altered to reset all rate-limited thumbnails at once.
 
 ==== Removed classes ====
 * FakeMemCachedClient (deprecated in 1.18)
index 390da77..627fcab 100644 (file)
@@ -1667,6 +1667,12 @@ $mediaWiki: The $mediawiki object
 $title: title of the message (string)
 $message: value (string), change it to the message you want to define
 
+'MessageCache::get': When fetching a message. Can be used to override the key
+for customisations. Given and returned message key must be in special format:
+1) first letter must be in lower case according to the content language.
+2) spaces must be replaced with underscores
+&$key: message key (string)
+
 'MessageCacheReplace': When a message page is changed. Useful for updating
 caches.
 $title: name of the page changed.
index 5ce31fd..bb80beb 100644 (file)
@@ -1031,6 +1031,14 @@ $wgTiffThumbnailType = false;
  */
 $wgThumbnailEpoch = '20030516000000';
 
+/**
+ * Certain operations are avoided if there were too many recent failures,
+ * for example, thumbnail generation. Bump this value to invalidate all
+ * memory of failed operations and thus allow further attempts to resume.
+ * This is useful when a cause for the failures has been found and fixed.
+ */
+$wgAttemptFailureEpoch = 1;
+
 /**
  * If set, inline scaled images will still produce "<img>" tags ready for
  * output instead of showing an error message.
@@ -5956,15 +5964,16 @@ $wgExtensionMessagesFiles = array();
  * @par Complex example:
  * @code
  *    $wgMessagesDirs['VisualEditor'] = array(
- *        __DIR__ . '/i18n',
- *        __DIR__ . '/modules/ve-core/i18n',
- *        __DIR__ . '/modules/qunit/localisation',
- *        __DIR__ . '/modules/oojs-ui/messages',
+ *        __DIR__ . '/lib/ve/modules/ve/i18n',
+ *        __DIR__ . '/modules/ve-mw/i18n',
+ *        __DIR__ . '/modules/ve-wmf/i18n',
  *    )
  * @endcode
  * @since 1.23
  */
-$wgMessagesDirs = array();
+$wgMessagesDirs = array(
+       "$IP/resources/oojs/i18n",
+);
 
 /**
  * Array of files with list(s) of extension entry points to be used in
index a52894d..399facf 100644 (file)
@@ -795,12 +795,11 @@ class WebRequest {
         * defaults if not given. The limit must be positive and is capped at 5000.
         * Offset must be positive but is not capped.
         *
-        * @param int $deflimit limit to use if no input and the user hasn't set the option.
+        * @param $deflimit Integer: limit to use if no input and the user hasn't set the option.
         * @param string $optionname to specify an option other than rclimit to pull from.
-        * @param int $hardlimit the maximum upper limit to allow, usually 5000
         * @return array first element is limit, second is offset
         */
-       public function getLimitOffset( $deflimit = 50, $optionname = 'rclimit', $hardlimit = 5000 ) {
+       public function getLimitOffset( $deflimit = 50, $optionname = 'rclimit' ) {
                global $wgUser;
 
                $limit = $this->getInt( 'limit', 0 );
@@ -813,8 +812,8 @@ class WebRequest {
                if ( $limit <= 0 ) {
                        $limit = $deflimit;
                }
-               if ( $limit > $hardlimit ) {
-                       $limit = $hardlimit; # We have *some* limits...
+               if ( $limit > 5000 ) {
+                       $limit = 5000; # We have *some* limits...
                }
 
                $offset = $this->getInt( 'offset', 0 );
index f5c072a..47ad80f 100644 (file)
@@ -746,6 +746,7 @@ class ApiParse extends ApiBase {
 
        public function getParamDescription() {
                $p = $this->getModulePrefix();
+               $wikitext = CONTENT_MODEL_WIKITEXT;
 
                return array(
                        'text' => "Text to parse. Use {$p}title or {$p}contentmodel to control the content model",
index a20b855..5a45a28 100644 (file)
@@ -41,11 +41,18 @@ class ApiQueryLangLinks extends ApiQueryBase {
                }
 
                $params = $this->extractRequestParams();
+               $prop = array_flip( (array)$params['prop'] );
 
                if ( isset( $params['title'] ) && !isset( $params['lang'] ) ) {
                        $this->dieUsageMsg( array( 'missingparam', 'lang' ) );
                }
 
+               // Handle deprecated param
+               $this->requireMaxOneParameter( $params, 'url', 'prop' );
+               if ( $params['url'] ) {
+                       $prop = array( 'url' => 1 );
+               }
+
                $this->addFields( array(
                        'll_from',
                        'll_lang',
@@ -104,12 +111,18 @@ class ApiQueryLangLinks extends ApiQueryBase {
                                break;
                        }
                        $entry = array( 'lang' => $row->ll_lang );
-                       if ( $params['url'] ) {
+                       if ( isset( $prop['url'] ) ) {
                                $title = Title::newFromText( "{$row->ll_lang}:{$row->ll_title}" );
                                if ( $title ) {
                                        $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
                                }
                        }
+                       if ( isset( $prop['langname'] ) ) {
+                               $entry['langname'] = Language::fetchLanguageName( $row->ll_lang, $params['inlanguagecode'] );
+                       }
+                       if ( isset( $prop['autonym'] ) ) {
+                               $entry['autonym'] = Language::fetchLanguageName( $row->ll_lang );
+                       }
                        ApiResult::setContent( $entry, $row->ll_title );
                        $fit = $this->addPageSubItem( $row->ll_from, $entry );
                        if ( !$fit ) {
@@ -124,6 +137,7 @@ class ApiQueryLangLinks extends ApiQueryBase {
        }
 
        public function getAllowedParams() {
+               global $wgContLang;
                return array(
                        'limit' => array(
                                ApiBase::PARAM_DFLT => 10,
@@ -133,7 +147,18 @@ class ApiQueryLangLinks extends ApiQueryBase {
                                ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
                        ),
                        'continue' => null,
-                       'url' => false,
+                       'url' => array(
+                               ApiBase::PARAM_DFLT => false,
+                               ApiBase::PARAM_DEPRECATED => true,
+                       ),
+                       'prop' => array(
+                               ApiBase::PARAM_ISMULTI => true,
+                               ApiBase::PARAM_TYPE => array(
+                                       'url',
+                                       'langname',
+                                       'autonym',
+                               )
+                       ),
                        'lang' => null,
                        'title' => null,
                        'dir' => array(
@@ -143,6 +168,7 @@ class ApiQueryLangLinks extends ApiQueryBase {
                                        'descending'
                                )
                        ),
+                       'inlanguagecode' => $wgContLang->getCode(),
                );
        }
 
@@ -150,10 +176,18 @@ class ApiQueryLangLinks extends ApiQueryBase {
                return array(
                        'limit' => 'How many langlinks to return',
                        'continue' => 'When more results are available, use this to continue',
-                       'url' => 'Whether to get the full URL',
+                       'url' => "Whether to get the full URL (Cannot be used with {$this->getModulePrefix()}prop)",
+                       'prop' => array(
+                               'Which additional properties to get for each interlanguage link',
+                               ' url      - Adds the full URL',
+                               ' langname - Adds the localised language name (best effort, use CLDR extension)',
+                               "            Use {$this->getModulePrefix()}inlanguagecode to control the language",
+                               ' autonym  - Adds the native language name',
+                       ),
                        'lang' => 'Language code',
                        'title' => "Link to search for. Must be used with {$this->getModulePrefix()}lang",
                        'dir' => 'The direction in which to list',
+                       'inlanguagecode' => 'Language code for localised language names',
                );
        }
 
@@ -165,6 +199,14 @@ class ApiQueryLangLinks extends ApiQueryBase {
                                        ApiBase::PROP_TYPE => 'string',
                                        ApiBase::PROP_NULLABLE => true
                                ),
+                               'langname' => array(
+                                       ApiBase::PROP_TYPE => 'string',
+                                       ApiBase::PROP_NULLABLE => true
+                               ),
+                               'autonym' => array(
+                                       ApiBase::PROP_TYPE => 'string',
+                                       ApiBase::PROP_NULLABLE => true
+                               ),
                                '*' => 'string'
                        )
                );
@@ -175,9 +217,14 @@ class ApiQueryLangLinks extends ApiQueryBase {
        }
 
        public function getPossibleErrors() {
-               return array_merge( parent::getPossibleErrors(), array(
-                       array( 'missingparam', 'lang' ),
-               ) );
+               return array_merge( parent::getPossibleErrors(),
+                       $this->getRequireMaxOneParameterErrorMessages(
+                               array( 'url', 'prop' )
+                       ),
+                       array(
+                               array( 'missingparam', 'lang' ),
+                       )
+               );
        }
 
        public function getExamples() {
index 3dee806..daaa915 100644 (file)
@@ -728,11 +728,17 @@ class MessageCache {
 
                // Normalise title-case input (with some inlining)
                $lckey = strtr( $key, ' ', '_' );
-               if ( ord( $key ) < 128 ) {
+               if ( ord( $lckey ) < 128 ) {
                        $lckey[0] = strtolower( $lckey[0] );
-                       $uckey = ucfirst( $lckey );
                } else {
                        $lckey = $wgContLang->lcfirst( $lckey );
+               }
+
+               wfRunHooks( 'MessageCache::get', array( &$lckey ) );
+
+               if ( ord( $lckey ) < 128 ) {
+                       $uckey = ucfirst( $lckey );
+               } else {
                        $uckey = $wgContLang->ucfirst( $lckey );
                }
 
index 983d90a..9e702e3 100644 (file)
@@ -277,9 +277,24 @@ class RedisConnectionPool {
         * @param string $server
         * @param RedisConnRef $cref
         * @param RedisException $e
+        * @deprecated 1.23
         */
        public function handleException( $server, RedisConnRef $cref, RedisException $e ) {
-               wfDebugLog( 'redis', "Redis exception on server $server: " . $e->getMessage() );
+               return $this->handleError( $cref, $e );
+       }
+
+       /**
+        * The redis extension throws an exception in response to various read, write
+        * and protocol errors. Sometimes it also closes the connection, sometimes
+        * not. The safest response for us is to explicitly destroy the connection
+        * object and let it be reopened during the next request.
+        *
+        * @param RedisConnRef $cref
+        * @param RedisException $e
+        */
+       public function handleError( RedisConnRef $cref, RedisException $e ) {
+               $server = $cref->getServer();
+               wfDebugLog( 'redis', "Redis exception on server $server: " . $e->getMessage() . "\n" );
                foreach ( $this->connections[$server] as $key => $connection ) {
                        if ( $cref->isConnIdentical( $connection['conn'] ) ) {
                                $this->idlePoolSize -= $connection['free'] ? 1 : 0;
index fa827d1..2e23d73 100644 (file)
@@ -41,8 +41,11 @@ class DatabaseSqlite extends DatabaseBase {
        /** @var PDO */
        protected $mConn;
 
+       /** @var FSLockManager (hopefully on the same server as the DB) */
+       protected $lockMgr;
+
        function __construct( $p = null ) {
-               global $wgSharedDB;
+               global $wgSharedDB, $wgSQLiteDataDir;
 
                if ( !is_array( $p ) ) { // legacy calling pattern
                        wfDeprecated( __METHOD__ . " method called without parameter array.", "1.22" );
@@ -67,6 +70,8 @@ class DatabaseSqlite extends DatabaseBase {
                                }
                        }
                }
+
+               $this->lockMgr = new FSLockManager( array( 'lockDirectory' => "$wgSQLiteDataDir/locks" ) );
        }
 
        /**
@@ -866,6 +871,22 @@ class DatabaseSqlite extends DatabaseBase {
                return $s;
        }
 
+       public function lock( $lockName, $method, $timeout = 5 ) {
+               global $wgSQLiteDataDir;
+
+               if ( !is_dir( "$wgSQLiteDataDir/locks" ) ) { // create dir as needed
+                       if ( !is_writable( $wgSQLiteDataDir ) || !mkdir( "$wgSQLiteDataDir/locks" ) ) {
+                               throw new DBError( "Cannot create directory \"$wgSQLiteDataDir/locks\"." );
+                       }
+               }
+
+               return $this->lockMgr->lock( array( $lockName ), LockManager::LOCK_EX, $timeout )->isOK();
+       }
+
+       public function unlock( $lockName, $method ) {
+               return $this->lockMgr->unlock( array( $lockName ), LockManager::LOCK_EX )->isOK();
+       }
+
        /**
         * Build a concatenation list to feed into a SQL query
         *
index b6ba4f2..fa2dd99 100644 (file)
@@ -32,7 +32,7 @@ interface LoadMonitor {
         *
         * @param LoadBalancer $parent
         */
-       function __construct( $parent );
+       public function __construct( $parent );
 
        /**
         * Perform pre-connection load ratio adjustment.
@@ -40,7 +40,7 @@ interface LoadMonitor {
         * @param string|bool $group The selected query group. Default: false
         * @param string|bool $wiki Default: false
         */
-       function scaleLoads( &$loads, $group = false, $wiki = false );
+       public function scaleLoads( &$loads, $group = false, $wiki = false );
 
        /**
         * Return an estimate of replication lag for each server
@@ -50,22 +50,17 @@ interface LoadMonitor {
         *
         * @return array
         */
-       function getLagTimes( $serverIndexes, $wiki );
+       public function getLagTimes( $serverIndexes, $wiki );
 }
 
 class LoadMonitorNull implements LoadMonitor {
-       function __construct( $parent ) {
+       public function __construct( $parent ) {
        }
 
-       function scaleLoads( &$loads, $group = false, $wiki = false ) {
+       public function scaleLoads( &$loads, $group = false, $wiki = false ) {
        }
 
-       /**
-        * @param array $serverIndexes
-        * @param string $wiki
-        * @return array
-        */
-       function getLagTimes( $serverIndexes, $wiki ) {
+       public function getLagTimes( $serverIndexes, $wiki ) {
                return array_fill_keys( $serverIndexes, 0 );
        }
 }
@@ -80,33 +75,21 @@ class LoadMonitorMySQL implements LoadMonitor {
        /** @var LoadBalancer */
        public $parent;
 
-       /**
-        * @param LoadBalancer $parent
-        */
-       function __construct( $parent ) {
+       public function __construct( $parent ) {
                $this->parent = $parent;
        }
 
-       /**
-        * @param array $loads
-        * @param bool $group
-        * @param bool $wiki
-        */
-       function scaleLoads( &$loads, $group = false, $wiki = false ) {
+       public function scaleLoads( &$loads, $group = false, $wiki = false ) {
        }
 
-       /**
-        * @param array $serverIndexes
-        * @param string $wiki
-        * @return array
-        */
-       function getLagTimes( $serverIndexes, $wiki ) {
+       public function getLagTimes( $serverIndexes, $wiki ) {
                if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) {
                        // Single server only, just return zero without caching
                        return array( 0 => 0 );
                }
 
-               wfProfileIn( __METHOD__ );
+               $section = new ProfileSection( __METHOD__ );
+
                $expiry = 5;
                $requestRate = 10;
 
@@ -124,7 +107,6 @@ class LoadMonitorMySQL implements LoadMonitor {
                        $chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
                        if ( mt_rand( 0, $chance ) != 0 ) {
                                unset( $times['timestamp'] ); // hide from caller
-                               wfProfileOut( __METHOD__ );
 
                                return $times;
                        }
@@ -142,7 +124,6 @@ class LoadMonitorMySQL implements LoadMonitor {
                } elseif ( is_array( $times ) ) {
                        # Could not acquire lock but an old cache exists, so use it
                        unset( $times['timestamp'] ); // hide from caller
-                       wfProfileOut( __METHOD__ );
 
                        return $times;
                }
@@ -163,8 +144,6 @@ class LoadMonitorMySQL implements LoadMonitor {
                $wgMemc->set( $memcKey, $times, $expiry + 10 );
                unset( $times['timestamp'] ); // hide from caller
 
-               wfProfileOut( __METHOD__ );
-
                return $times;
        }
 }
index d524cc2..caf15aa 100644 (file)
@@ -748,46 +748,42 @@ class SwiftFileBackend extends FileBackendStore {
                $ep = array_diff_key( $params, array( 'srcs' => 1 ) ); // for error logging
                // Blindly create tmp files and stream to them, catching any exception if the file does
                // not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata().
-               foreach ( array_chunk( $params['srcs'], $params['concurrency'] ) as $pathBatch ) {
-                       $reqs = array(); // (path => op)
-
-                       foreach ( $pathBatch as $path ) { // each path in this concurrent batch
-                               list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
-                               if ( $srcRel === null || !$auth ) {
-                                       $contents[$path] = false;
-                                       continue;
-                               }
-                               $data = false;
-                               // Create a new temporary memory file...
-                               $handle = fopen( 'php://temp', 'wb' );
-                               if ( $handle ) {
-                                       $reqs[$path] = array(
-                                               'method' => 'GET',
-                                               'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
-                                               'headers' => $this->authTokenHeaders( $auth )
-                                                       + $this->headersFromParams( $params ),
-                                               'stream' => $handle,
-                                       );
-                               } else {
-                                       $data = false;
-                               }
-                               $contents[$path] = $data;
+               $reqs = array(); // (path => op)
+
+               foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
+                       list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
+                       if ( $srcRel === null || !$auth ) {
+                               $contents[$path] = false;
+                               continue;
+                       }
+                       // Create a new temporary memory file...
+                       $handle = fopen( 'php://temp', 'wb' );
+                       if ( $handle ) {
+                               $reqs[$path] = array(
+                                       'method'  => 'GET',
+                                       'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
+                                       'headers' => $this->authTokenHeaders( $auth )
+                                               + $this->headersFromParams( $params ),
+                                       'stream'  => $handle,
+                               );
                        }
+                       $contents[$path] = false;
+               }
 
-                       $reqs = $this->http->runMulti( $reqs );
-                       foreach ( $reqs as $path => $op ) {
-                               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
-                               if ( $rcode >= 200 && $rcode <= 299 ) {
-                                       rewind( $op['stream'] ); // start from the beginning
-                                       $contents[$path] = stream_get_contents( $op['stream'] );
-                               } elseif ( $rcode === 404 ) {
-                                       $contents[$path] = false;
-                               } else {
-                                       $this->onError( null, __METHOD__,
-                                               array( 'src' => $path ) + $ep, $rerr, $rcode, $rdesc );
-                               }
-                               fclose( $op['stream'] ); // close open handle
+               $opts = array( 'maxConnsPerHost' => $params['concurrency'] );
+               $reqs = $this->http->runMulti( $reqs, $opts );
+               foreach ( $reqs as $path => $op ) {
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
+                       if ( $rcode >= 200 && $rcode <= 299 ) {
+                               rewind( $op['stream'] ); // start from the beginning
+                               $contents[$path] = stream_get_contents( $op['stream'] );
+                       } elseif ( $rcode === 404 ) {
+                               $contents[$path] = false;
+                       } else {
+                               $this->onError( null, __METHOD__,
+                                       array( 'src' => $path ) + $ep, $rerr, $rcode, $rdesc );
                        }
+                       fclose( $op['stream'] ); // close open handle
                }
 
                return $contents;
@@ -1078,54 +1074,52 @@ class SwiftFileBackend extends FileBackendStore {
                $ep = array_diff_key( $params, array( 'srcs' => 1 ) ); // for error logging
                // Blindly create tmp files and stream to them, catching any exception if the file does
                // not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata().
-               foreach ( array_chunk( $params['srcs'], $params['concurrency'] ) as $pathBatch ) {
-                       $reqs = array(); // (path => op)
-
-                       foreach ( $pathBatch as $path ) { // each path in this concurrent batch
-                               list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
-                               if ( $srcRel === null || !$auth ) {
-                                       $tmpFiles[$path] = null;
-                                       continue;
-                               }
-                               $tmpFile = null;
-                               // Get source file extension
-                               $ext = FileBackend::extensionFromPath( $path );
-                               // Create a new temporary file...
-                               $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
-                               if ( $tmpFile ) {
-                                       $handle = fopen( $tmpFile->getPath(), 'wb' );
-                                       if ( $handle ) {
-                                               $reqs[$path] = array(
-                                                       'method' => 'GET',
-                                                       'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
-                                                       'headers' => $this->authTokenHeaders( $auth )
-                                                               + $this->headersFromParams( $params ),
-                                                       'stream' => $handle,
-                                               );
-                                       } else {
-                                               $tmpFile = null;
-                                       }
-                               }
-                               $tmpFiles[$path] = $tmpFile;
-                       }
+               $reqs = array(); // (path => op)
 
-                       $reqs = $this->http->runMulti( $reqs );
-                       foreach ( $reqs as $path => $op ) {
-                               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
-                               fclose( $op['stream'] ); // close open handle
-                               if ( $rcode >= 200 && $rcode <= 299
-                                       // double check that the disk is not full/broken
-                                       && $tmpFiles[$path]->getSize() == $rhdrs['content-length']
-                               ) {
-                                       // good
-                               } elseif ( $rcode === 404 ) {
-                                       $tmpFiles[$path] = false;
+               foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
+                       list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
+                       if ( $srcRel === null || !$auth ) {
+                               $tmpFiles[$path] = null;
+                               continue;
+                       }
+                       // Get source file extension
+                       $ext = FileBackend::extensionFromPath( $path );
+                       // Create a new temporary file...
+                       $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
+                       if ( $tmpFile ) {
+                               $handle = fopen( $tmpFile->getPath(), 'wb' );
+                               if ( $handle ) {
+                                       $reqs[$path] = array(
+                                               'method'  => 'GET',
+                                               'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
+                                               'headers' => $this->authTokenHeaders( $auth )
+                                                       + $this->headersFromParams( $params ),
+                                               'stream'  => $handle,
+                                       );
                                } else {
-                                       $tmpFiles[$path] = null;
-                                       $this->onError( null, __METHOD__,
-                                               array( 'src' => $path ) + $ep, $rerr, $rcode, $rdesc );
+                                       $tmpFile = null;
                                }
                        }
+                       $tmpFiles[$path] = $tmpFile;
+               }
+
+               $opts = array( 'maxConnsPerHost' => $params['concurrency'] );
+               $reqs = $this->http->runMulti( $reqs, $opts );
+               foreach ( $reqs as $path => $op ) {
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
+                       fclose( $op['stream'] ); // close open handle
+                       if ( $rcode >= 200 && $rcode <= 299
+                               // double check that the disk is not full/broken
+                               && $tmpFiles[$path]->getSize() == $rhdrs['content-length']
+                       ) {
+                               // good
+                       } elseif ( $rcode === 404 ) {
+                               $tmpFiles[$path] = false;
+                       } else {
+                               $tmpFiles[$path] = null;
+                               $this->onError( null, __METHOD__,
+                                       array( 'src' => $path ) + $ep, $rerr, $rcode, $rdesc );
+                       }
                }
 
                return $tmpFiles;
@@ -1540,6 +1534,7 @@ class SwiftFileBackend extends FileBackendStore {
                                                'auth_token' => $rhdrs['x-auth-token'],
                                                'storage_url' => $rhdrs['x-storage-url']
                                        );
+                                       $this->srvCache->set( $cacheKey, $this->authCreds, ceil( $this->authTTL / 2 ) );
                                        $this->authSessionTimestamp = time();
                                } elseif ( $rcode === 401 ) {
                                        $this->onError( null, __METHOD__, array(), "Authentication failed.", $rcode );
@@ -1591,7 +1586,7 @@ class SwiftFileBackend extends FileBackendStore {
         * @return string
         */
        private function getCredsCacheKey( $username ) {
-               return wfMemcKey( 'backend', $this->getName(), 'usercreds', $username );
+               return 'swiftcredentials:' . md5( $username . ':' . $this->swiftAuthUrl );
        }
 
        /**
index e37e567..ff4cba5 100644 (file)
@@ -153,7 +153,7 @@ LUA;
                        );
                } catch ( RedisException $e ) {
                        $res = false;
-                       $this->redisPool->handleException( $server, $conn, $e );
+                       $this->redisPool->handleError( $conn, $e );
                }
 
                if ( $res === false ) {
@@ -221,7 +221,7 @@ LUA;
                        );
                } catch ( RedisException $e ) {
                        $res = false;
-                       $this->redisPool->handleException( $server, $conn, $e );
+                       $this->redisPool->handleError( $conn, $e );
                }
 
                if ( $res === false ) {
index e0641b5..3422664 100644 (file)
@@ -120,7 +120,7 @@ class JobQueueRedis extends JobQueue {
                try {
                        return $conn->lSize( $this->getQueueKey( 'l-unclaimed' ) );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
        }
 
@@ -141,7 +141,7 @@ class JobQueueRedis extends JobQueue {
 
                        return array_sum( $conn->exec() );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
        }
 
@@ -158,7 +158,7 @@ class JobQueueRedis extends JobQueue {
                try {
                        return $conn->zSize( $this->getQueueKey( 'z-delayed' ) );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
        }
 
@@ -175,7 +175,7 @@ class JobQueueRedis extends JobQueue {
                try {
                        return $conn->zSize( $this->getQueueKey( 'z-abandoned' ) );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
        }
 
@@ -229,7 +229,7 @@ class JobQueueRedis extends JobQueue {
                        JobQueue::incrStats( 'job-insert-duplicate', $this->type,
                                count( $items ) - $failed - $pushed );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
 
                return true;
@@ -330,7 +330,7 @@ LUA;
                                $job = $this->getJobFromFields( $item ); // may be false
                        } while ( !$job ); // job may be false if invalid
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
 
                return $job;
@@ -442,7 +442,7 @@ LUA;
                                        return false;
                                }
                        } catch ( RedisException $e ) {
-                               $this->throwRedisException( $this->server, $conn, $e );
+                               $this->throwRedisException( $conn, $e );
                        }
                }
 
@@ -473,7 +473,7 @@ LUA;
                        // Update the timestamp of the last root job started at the location...
                        return $conn->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL ); // 2 weeks
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
        }
 
@@ -494,7 +494,7 @@ LUA;
                        // Get the last time this root job was enqueued
                        $timestamp = $conn->get( $this->getRootJobCacheKey( $params['rootJobSignature'] ) );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
 
                // Check if a new root job was started at the location after this one's...
@@ -519,7 +519,7 @@ LUA;
 
                        return ( $conn->delete( $keys ) !== false );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
        }
 
@@ -542,7 +542,7 @@ LUA;
                                } )
                        );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
        }
 
@@ -565,7 +565,7 @@ LUA;
                                } )
                        );
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
        }
 
@@ -580,8 +580,8 @@ LUA;
        protected function doGetSiblingQueueSizes( array $types ) {
                $sizes = array(); // (type => size)
                $types = array_values( $types ); // reindex
+               $conn = $this->getConnection();
                try {
-                       $conn = $this->getConnection();
                        $conn->multi( Redis::PIPELINE );
                        foreach ( $types as $type ) {
                                $conn->lSize( $this->getQueueKey( 'l-unclaimed', $type ) );
@@ -593,7 +593,7 @@ LUA;
                                }
                        }
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
 
                return $sizes;
@@ -623,7 +623,7 @@ LUA;
 
                        return $job;
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
        }
 
@@ -707,7 +707,7 @@ LUA;
                                JobQueue::incrStats( 'job-abandon', $this->type, $abandoned );
                        }
                } catch ( RedisException $e ) {
-                       $this->throwRedisException( $this->server, $conn, $e );
+                       $this->throwRedisException( $conn, $e );
                }
 
                return $count;
@@ -822,13 +822,12 @@ LUA;
        }
 
        /**
-        * @param $server string
         * @param $conn RedisConnRef
         * @param $e RedisException
         * @throws JobQueueError
         */
-       protected function throwRedisException( $server, RedisConnRef $conn, $e ) {
-               $this->redisPool->handleException( $server, $conn, $e );
+       protected function throwRedisException( RedisConnRef $conn, $e ) {
+               $this->redisPool->handleError( $conn, $e );
                throw new JobQueueError( "Redis server error: {$e->getMessage()}\n" );
        }
 
index c654933..2aec3c9 100644 (file)
@@ -175,7 +175,7 @@ class JobQueueAggregatorRedis extends JobQueueAggregator {
         * @return void
         */
        protected function handleException( RedisConnRef $conn, $e ) {
-               $this->redisPool->handleException( $conn->getServer(), $conn, $e );
+               $this->redisPool->handleError( $conn, $e );
        }
 
        /**
index 0675b38..00cd257 100644 (file)
@@ -1,9 +1,29 @@
 <?php
+/**
+ * HTTP service client
+ *
+ * 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
+ */
 
 /**
  * Class to handle concurrent HTTP requests
  *
- * HTTP request maps use the following format:
+ * HTTP request maps are arrays that use the following format:
  *   - method   : GET/HEAD/PUT/POST/DELETE
  *   - url      : HTTP/HTTPS URL
  *   - query    : <query parameter field/value associative array> (uses RFC 3986)
@@ -14,6 +34,7 @@
  *                array bodies are encoded as multipart/form-data and strings
  *                use application/x-www-form-urlencoded (headers sent automatically)
  *   - stream   : resource to stream the HTTP response body to
+ * Request maps can use integer index 0 instead of 'method' and 1 instead of 'url'.
  *
  * @author Aaron Schulz
  * @since 1.23
@@ -24,12 +45,20 @@ class MultiHttpClient {
        /** @var string|null SSL certificates path  */
        protected $caBundlePath;
        /** @var integer */
-       protected $connTimeout;
+       protected $connTimeout = 10;
        /** @var integer */
-       protected $reqTimeout;
+       protected $reqTimeout = 300;
+       /** @var bool */
+       protected $usePipelining = false;
+       /** @var integer */
+       protected $maxConnsPerHost = 50;
 
        /**
         * @param array $options
+        *   - connTimeout     : default connection timeout
+        *   - reqTimeout      : default request timeout
+        *   - usePipelining   : whether to use HTTP pipelining if possible (for all hosts)
+        *   - maxConnsPerHost : maximum number of concurrent connections (per host)
         */
        public function __construct( array $options ) {
                if ( isset( $options['caBundlePath'] ) ) {
@@ -38,9 +67,11 @@ class MultiHttpClient {
                                throw new Exception( "Cannot find CA bundle: " . $this->caBundlePath );
                        }
                }
-               static $defaults = array( 'connTimeout' => 10, 'reqTimeout' => 300 );
-               foreach ( $defaults as $key => $default ) {
-                       $this->$key = isset( $options[$key] ) ? $options[$key] : $default;
+               static $opts = array( 'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost' );
+               foreach ( $opts as $key ) {
+                       if ( isset( $options[$key] ) ) {
+                               $this->$key = $options[$key];
+                       }
                }
        }
 
@@ -58,15 +89,18 @@ class MultiHttpClient {
         *              list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $req;
         *  </code>
         * @param array $req HTTP request array
+        * @param array $opts
+        *   - connTimeout    : connection timeout per request
+        *   - reqTimeout     : post-connection timeout per request
         * @return array Response array for request
         */
-       public function run( array $req ) {
-               $req = $this->runMulti( array( $req ) );
+       final public function run( array $req, array $opts = array() ) {
+               $req = $this->runMulti( array( $req ), $opts );
                return $req[0]['response'];
        }
 
        /**
-        * Execute a set of HTTP(S) request concurrently
+        * Execute a set of HTTP(S) requests concurrently
         *
         * The maps are returned by this method with the 'response' field set to a map of:
         *   - code    : HTTP response code or 0 if there was a serious cURL error
@@ -79,13 +113,19 @@ class MultiHttpClient {
         *              list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $req;
         *  </code>
         * All headers in the 'headers' field are normalized to use lower case names.
-        * This is true for the request headers and the response headers.
+        * This is true for the request headers and the response headers. Integer-indexed
+        * method/URL entries will also be changed to use the corresponding string keys.
         *
         * @param array $req Map of HTTP request arrays
+        * @param array $opts
+        *   - connTimeout     : connection timeout per request
+        *   - reqTimeout      : post-connection timeout per request
+        *   - usePipelining   : whether to use HTTP pipelining if possible
+        *   - maxConnsPerHost : maximum number of concurrent connections (per host)
         * @return array $reqs With response array populated for each
         */
-       public function runMulti( array $reqs ) {
-               $multiHandle = $this->getCurlMulti();
+       public function runMulti( array $reqs, array $opts = array() ) {
+               $chm = $this->getCurlMulti();
 
                // Normalize $reqs and add all of the required cURL handles...
                $handles = array();
@@ -97,6 +137,14 @@ class MultiHttpClient {
                                'body'     => '',
                                'error'    => ''
                        );
+                       if ( isset( $req[0] ) ) {
+                               $req['method'] = $req[0]; // short-form
+                               unset( $req[0] );
+                       }
+                       if ( isset( $req[1] ) ) {
+                               $req['url'] = $req[1]; // short-form
+                               unset( $req[1] );
+                       }
                        if ( !isset( $req['method'] ) ) {
                                throw new Exception( "Request has no 'method' field set." );
                        } elseif ( !isset( $req['url'] ) ) {
@@ -114,34 +162,54 @@ class MultiHttpClient {
                                $req['body'] = '';
                                $req['headers']['content-length'] = 0;
                        }
-                       $handles[$index] = $this->getCurlHandle( $req );
+                       $handles[$index] = $this->getCurlHandle( $req, $opts );
                        if ( count( $reqs ) > 1 ) {
                                // https://github.com/guzzle/guzzle/issues/349
                                curl_setopt( $handles[$index], CURLOPT_FORBID_REUSE, true );
                        }
-                       curl_multi_add_handle( $multiHandle, $handles[$index] );
                }
+               unset( $req ); // don't assign over this by accident
 
-               // Execute the cURL handles concurrently...
-               $active = null; // handles still being processed
-               do {
-                       // Do any available work...
+               $indexes = array_keys( $reqs );
+               if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5
+                       if ( isset( $opts['usePipelining'] ) ) {
+                               curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$opts['usePipelining'] );
+                       }
+                       if ( isset( $opts['maxConnsPerHost'] ) ) {
+                               // Keep these sockets around as they may be needed later in the request
+                               curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$opts['maxConnsPerHost'] );
+                       }
+               }
+
+               // @TODO: use a per-host rolling handle window (e.g. CURLMOPT_MAX_HOST_CONNECTIONS)
+               $batches = array_chunk( $indexes, $this->maxConnsPerHost );
+
+               foreach ( $batches as $batch ) {
+                       // Attach all cURL handles for this batch
+                       foreach ( $batch as $index ) {
+                               curl_multi_add_handle( $chm, $handles[$index] );
+                       }
+                       // Execute the cURL handles concurrently...
+                       $active = null; // handles still being processed
                        do {
-                               $mrc = curl_multi_exec( $multiHandle, $active );
-                       } while ( $mrc == CURLM_CALL_MULTI_PERFORM );
-                       // Wait (if possible) for available work...
-                       if ( $active > 0 && $mrc == CURLM_OK ) {
-                               if ( curl_multi_select( $multiHandle, 10 ) == -1 ) {
-                                       // PHP bug 63411; http://curl.haxx.se/libcurl/c/curl_multi_fdset.html
-                                       usleep( 5000 ); // 5ms
+                               // Do any available work...
+                               do {
+                                       $mrc = curl_multi_exec( $chm, $active );
+                               } while ( $mrc == CURLM_CALL_MULTI_PERFORM );
+                               // Wait (if possible) for available work...
+                               if ( $active > 0 && $mrc == CURLM_OK ) {
+                                       if ( curl_multi_select( $chm, 10 ) == -1 ) {
+                                               // PHP bug 63411; http://curl.haxx.se/libcurl/c/curl_multi_fdset.html
+                                               usleep( 5000 ); // 5ms
+                                       }
                                }
-                       }
-               } while ( $active > 0 && $mrc == CURLM_OK );
+                       } while ( $active > 0 && $mrc == CURLM_OK );
+               }
 
                // Remove all of the added cURL handles and check for errors...
                foreach ( $reqs as $index => &$req ) {
                        $ch = $handles[$index];
-                       curl_multi_remove_handle( $multiHandle, $ch );
+                       curl_multi_remove_handle( $chm, $ch );
                        if ( curl_errno( $ch ) !== 0 ) {
                                $req['error'] = "(curl error: " . curl_errno( $ch ) . ") " . curl_error( $ch );
                        }
@@ -158,19 +226,31 @@ class MultiHttpClient {
                                unset( $req['_closeHandle'] );
                        }
                }
+               unset( $req ); // don't assign over this by accident
+
+               // Restore the default settings
+               if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5
+                       curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$this->usePipelining );
+                       curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
+               }
 
                return $reqs;
        }
 
        /**
         * @param array $req HTTP request map
+        * @param array $opts
+        *   - connTimeout    : default connection timeout
+        *   - reqTimeout     : default request timeout
         * @return resource
         */
-       protected function getCurlHandle( array &$req ) {
+       protected function getCurlHandle( array &$req, array $opts = array() ) {
                $ch = curl_init();
 
-               curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, $this->connTimeout );
-               curl_setopt( $ch, CURLOPT_TIMEOUT, $this->reqTimeout );
+               curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT,
+                       isset( $opts['connTimeout'] ) ? $opts['connTimeout'] : $this->connTimeout );
+               curl_setopt( $ch, CURLOPT_TIMEOUT,
+                       isset( $opts['reqTimeout'] ) ? $opts['reqTimeout'] : $this->reqTimeout );
                curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
                curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 );
                curl_setopt( $ch, CURLOPT_HEADER, 0 );
@@ -290,7 +370,12 @@ class MultiHttpClient {
         */
        protected function getCurlMulti() {
                if ( !$this->multiHandle ) {
-                       $this->multiHandle = curl_multi_init();
+                       $cmh = curl_multi_init();
+                       if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5
+                               curl_multi_setopt( $cmh, CURLMOPT_PIPELINING, (int)$this->usePipelining );
+                               curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
+                       }
+                       $this->multiHandle = $cmh;
                }
                return $this->multiHandle;
        }
index 56f2128..427143c 100644 (file)
@@ -84,7 +84,7 @@ class RedisBagOStuff extends BagOStuff {
                        $result = $this->unserialize( $value );
                } catch ( RedisException $e ) {
                        $result = false;
-                       $this->handleException( $server, $conn, $e );
+                       $this->handleException( $conn, $e );
                }
 
                $this->logRequest( 'get', $key, $server, $result );
@@ -108,7 +108,7 @@ class RedisBagOStuff extends BagOStuff {
                        }
                } catch ( RedisException $e ) {
                        $result = false;
-                       $this->handleException( $server, $conn, $e );
+                       $this->handleException( $conn, $e );
                }
 
                $this->logRequest( 'set', $key, $server, $result );
@@ -142,7 +142,7 @@ class RedisBagOStuff extends BagOStuff {
                        $result = ( $conn->exec() == array( true ) );
                } catch ( RedisException $e ) {
                        $result = false;
-                       $this->handleException( $server, $conn, $e );
+                       $this->handleException( $conn, $e );
                }
 
                $this->logRequest( 'cas', $key, $server, $result );
@@ -162,7 +162,7 @@ class RedisBagOStuff extends BagOStuff {
                        $result = true;
                } catch ( RedisException $e ) {
                        $result = false;
-                       $this->handleException( $server, $conn, $e );
+                       $this->handleException( $conn, $e );
                }
 
                $this->logRequest( 'delete', $key, $server, $result );
@@ -201,7 +201,7 @@ class RedisBagOStuff extends BagOStuff {
                                        }
                                }
                        } catch ( RedisException $e ) {
-                               $this->handleException( $server, $conn, $e );
+                               $this->handleException( $conn, $e );
                        }
                }
 
@@ -229,7 +229,7 @@ class RedisBagOStuff extends BagOStuff {
                        }
                } catch ( RedisException $e ) {
                        $result = false;
-                       $this->handleException( $server, $conn, $e );
+                       $this->handleException( $conn, $e );
                }
 
                $this->logRequest( 'add', $key, $server, $result );
@@ -260,7 +260,7 @@ class RedisBagOStuff extends BagOStuff {
                        }
                } catch ( RedisException $e ) {
                        $result = false;
-                       $this->handleException( $server, $conn, $e );
+                       $this->handleException( $conn, $e );
                }
 
                $this->logRequest( 'replace', $key, $server, $result );
@@ -290,7 +290,7 @@ class RedisBagOStuff extends BagOStuff {
                        $result = $this->unserialize( $conn->incrBy( $key, $value ) );
                } catch ( RedisException $e ) {
                        $result = false;
-                       $this->handleException( $server, $conn, $e );
+                       $this->handleException( $conn, $e );
                }
 
                $this->logRequest( 'incr', $key, $server, $result );
@@ -352,8 +352,8 @@ class RedisBagOStuff extends BagOStuff {
         * not. The safest response for us is to explicitly destroy the connection
         * object and let it be reopened during the next request.
         */
-       protected function handleException( $server, RedisConnRef $conn, $e ) {
-               $this->redisPool->handleException( $server, $conn, $e );
+       protected function handleException( RedisConnRef $conn, $e ) {
+               $this->redisPool->handleError( $conn, $e );
        }
 
        /**
index 588a313..b89522d 100644 (file)
@@ -105,7 +105,6 @@ class SpecialSearch extends SpecialPage {
 
                if ( $request->getVal( 'fulltext' )
                        || !is_null( $request->getVal( 'offset' ) )
-                       || !is_null( $request->getVal( 'searchx' ) )
                ) {
                        $this->showResults( $search );
                } else {
@@ -120,7 +119,7 @@ class SpecialSearch extends SpecialPage {
         */
        public function load() {
                $request = $this->getRequest();
-               list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, '', 500 );
+               list( $this->limit, $this->offset ) = $request->getLimitOffset( 20 );
                $this->mPrefix = $request->getVal( 'prefix', '' );
 
                $user = $this->getUser();
index b4757e0..a6b3602 100644 (file)
@@ -152,7 +152,10 @@ class UserrightsPage extends SpecialPage {
                        }
 
                        $targetUser = $status->value;
-                       $targetUser->clearInstanceCache();
+                       if ( $targetUser instanceof User ) { // UserRightsProxy doesn't have this method (bug 61252)
+                               $targetUser->clearInstanceCache(); // bug 38989
+                       }
+
 
                        if ( $request->getVal( 'conflictcheck-originalgroups' ) !== implode( ',', $targetUser->getGroups() ) ) {
                                $out->addWikiMsg( 'userrights-conflict' );
index 2b07a0e..25f654b 100644 (file)
@@ -2742,7 +2742,7 @@ $1',
 'range_block_disabled' => 'Адміністратарам забаронена блякаваць дыяпазоны.',
 'ipb_expiry_invalid' => 'Няслушны тэрмін блякаваньня.',
 'ipb_expiry_temp' => 'Блякаваньні са схаваньнем імя ўдзельніка павінны быць бестэрміновымі.',
-'ipb_hide_invalid' => 'Ð\9dемагÑ\87Ñ\8bма Ñ\81Ñ\85аваÑ\86Ñ\8c Ð³Ñ\8dÑ\82Ñ\8b Ñ\80аÑ\85Ñ\83нак; Ð²ÐµÑ\80агодна Ð·Ñ\8c Ñ\8fго Ð·Ñ\80облена Ð·Ð°Ñ\88маÑ\82 Ñ\80Ñ\8dдагаванÑ\8cнÑ\8fÑ\9e.',
+'ipb_hide_invalid' => 'Ð\9dемагÑ\87Ñ\8bма Ñ\81Ñ\85аваÑ\86Ñ\8c Ð³Ñ\8dÑ\82Ñ\8b Ñ\80аÑ\85Ñ\83нак; Ð·Ñ\8c Ñ\8fго Ð·Ñ\80облена Ð±Ð¾Ð»Ñ\8cÑ\88 Ñ\87Ñ\8bм {{PLURAL:$1|$1 Ñ\80Ñ\8dдагаванÑ\8cне|$1 Ñ\80Ñ\8dдагаванÑ\8cнÑ\96|$1 Ñ\80Ñ\8dдагаванÑ\8cнÑ\8fÑ\9e}}.',
 'ipb_already_blocked' => '«$1» ужо заблякаваны',
 'ipb-needreblock' => '$1 ужо заблякаваны. Вы жадаеце зьмяніць парамэтры?',
 'ipb-otherblocks-header' => '{{PLURAL:$1|1=Іншае блякаваньне|Іншыя блякаваньні}}',
index 6f968a4..3295de0 100644 (file)
@@ -296,6 +296,7 @@ $messages = array(
 'previewnote' => "'''Attentu: questa ùn hè ch'è una previsualisazzione.'''
 E to mudifiche ùn sò ancora state salvate!",
 'editing' => 'Mudifica di $1',
+'creating' => 'A pagina $1 hà da esse creata',
 'editingsection' => 'Mudifica di $1 (sezzione)',
 'editingcomment' => 'Mudifica di $1 (cummentu)',
 'editconflict' => 'Cunflittu di mudificazione: $1',
@@ -322,6 +323,7 @@ Parechji mudelli ùn seranu micca inclusi.",
 'revision-info' => 'Versione di e $4 à e $5 di $2',
 'previousrevision' => '← Versione menu ricente',
 'currentrevisionlink' => 'Ultima revisione',
+'cur' => 'att',
 'page_first' => 'prima',
 'history-fieldset-title' => 'Parcorre a cronolugia',
 'history-show-deleted' => 'Solu quelli cancellati',
@@ -729,6 +731,9 @@ Parechji mudelli ùn seranu micca inclusi.",
 'anonymous' => '{{PLURAL:$1|Utilizatore anonimu|Utilizatori anonimi}} di {{SITENAME}}',
 'others' => 'altri',
 
+# Info page
+'pageinfo-toolboxlink' => 'Infurmazione annantu à a pagina',
+
 # Media information
 'file-nohires' => 'Una diversione incù una risoluzione più alta ùn hè micca dispunibile.',
 'show-big-image' => 'Schedariu originale',
index eb5df0b..df6c555 100644 (file)
@@ -650,7 +650,7 @@ $messages = array(
 'redirectedfrom' => '(Weitergeleitet von $1)',
 'redirectpagesub' => 'Weiterleitung',
 'lastmodifiedat' => 'Diese Seite wurde zuletzt am $1 um $2 Uhr geändert.',
-'viewcount' => 'Diese Seite wurde bisher {{PLURAL:$1|einmal|$1-mal}} abgerufen.',
+'viewcount' => 'Diese Seite wurde bisher {{PLURAL:$1|einmal|$1 mal}} abgerufen.',
 'protectedpage' => 'Geschützte Seite',
 'jumpto' => 'Wechseln zu:',
 'jumptonavigation' => 'Navigation',
index a2f9373..7e6a757 100644 (file)
@@ -836,7 +836,7 @@ Nuştışê xo qonrol kerên, ya zi [[Special:UserLogin/signup|yew hesabo newe a
 'login-userblocked' => 'No karber/na karbere blokekerdeyo/blokekerdiya. Cıkewtışi rê musade çıniyo.',
 'wrongpassword' => 'Parola ğeleta. Rêna / fına bıcerrebne .',
 'wrongpasswordempty' => 'Parola tola, venga. tekrar bınuse.',
-'passwordtooshort' => 'Derganiya parola wa tewr tayn {{PLURAL:$1|1 karakter|$1 karakteran}} dı bo.',
+'passwordtooshort' => 'Paroley gani tewr senık be {{PLURAL:$1|1 karakter|$1 karakteran}} derg bê.',
 'password-name-match' => 'Parola u nameyê şıma gani zeypê (seypê) nêbo.',
 'password-login-forbidden' => 'No namey karberi u parola karkerdışê cı  kerdo xırab.',
 'mailmypassword' => 'Parola reset ke',
@@ -874,8 +874,8 @@ Bıne vındere u newe ra dest pê bıkere.',
 'login-abort-generic' => 'Dekewtışê şıma xırabo-terkneyayo',
 'loginlanguagelabel' => 'Zıwan: $1',
 'suspicious-userlogout' => 'Waştişê tu ya veciyayişi kebul nibiya cunki ihtimal o ke waştiş yew browser ya zi proksiyê heripiyaye ra ameya.',
-'createacct-another-realname-tip' => 'Nameyo raştay keyfiyo.
-Şıma namey xo raştay bınusne se xebtiyayışan de namey şıma do bıaso.',
+'createacct-another-realname-tip' => 'Nameyo raştıkên keyfiyo.
+Şıma nameyo xoyo raştıkên ke bımocnê, seba iştırakanê karberi be ney ra istıfade beno.',
 
 # Email sending
 'php-mail-error-unknown' => "PHP's mail() fonksiyoni de xırabin vıcyê.",
@@ -949,9 +949,9 @@ Kerem ke verdi dekewten $1 bıpawe.',
 
 # Special:ResetTokens
 'resettokens' => 'Nışanan reset ke',
-'resettokens-text' => 'Tiya de hesab de şımaya eleqedar tay malumati kılite icazeti şıma şeni  sıfır keri.
+'resettokens-text' => 'Şıma tiya de hesabê şıma ra elaqedar tayê kılitê icazetê cıresayışê melumati şenê sıfır kerê.
 
-Şıma na ğırabina kerda vıla se yana hesab de şıma de xırabin esta se ney bıkeri.',
+Şıma be ğeletiye ra ke nê kerdê vıla ya zi hesabê şıma de xırabiye ke esta, naye bıkerê.',
 'resettokens-no-tokens' => 'Nışanê reseti çıniyê',
 'resettokens-legend' => 'Nışanan reset ke',
 'resettokens-tokens' => 'Nışani:',
@@ -1162,7 +1162,7 @@ Pel ca ra esto.',
 'content-not-allowed-here' => '"$1" sero per da [[$2]] rê mısade nêdeyêno',
 'editwarning-warning' => 'ihtimal o ke wexta şıma peli ra bıveci, vurnayiş o ke şıma kerdo, hewna şiyêro .
 eke şıma kewtê hesabê xo, no hişyari tercihanê xo ra şıma eşkeni "Vurnayış"\'i vındarne.',
-'editpage-notsupportedcontentformat-title' => 'Formata zerreki qebul nêvinena',
+'editpage-notsupportedcontentformat-title' => 'Formatê zerreki qebul nêbeno',
 
 # Content models
 'content-model-wikitext' => 'wikimetin',
@@ -1371,8 +1371,8 @@ no vurnayişo ke şıma keni kontrol bıkere yew pelo kehen nêbo.',
 'showhideselectedversions' => 'Revizyonanê weçinıtan bımocne/bınımne',
 'editundo' => 'peyser bıgê',
 'diff-empty' => '(Babetna niyo)',
-'diff-multi-sameuser' => '({{PLURAL:$1|Yew revizyono miyanên|$1 revizyonê miyanêni}} terefê {{PLURAL:$2|yew karberi|$2 karberan}} nêmocno)',
-'diff-multi-otherusers' => '({{PLURAL:$1|Yew revizyono miyanên|$1 revizyonê miyanêni}} terefê {{PLURAL:$2|yew karberi|$2 karberan}} nêmocno)',
+'diff-multi-sameuser' => '(Terefê eyni karberi ra {{PLURAL:$1|yew revizyono miyanên nêmocno|$1 revizyonê miyanêni nêmocnê}})',
+'diff-multi-otherusers' => '(Terefê {{PLURAL:$2|yew karberi|$2 karberan}} ra {{PLURAL:$1|yew revizyono miyanên nêmocno|$1 revizyonê miyanêni nêmocnê}})',
 'diff-multi-manyusers' => '({{PLURAL:$1|jew timar kerdışo qıckeko|$1 timar kerdışo qıckeko}} timar kerdo, $2 {{PLURAL:$2|Karber|karberi}} memocne)',
 'difference-missing-revision' => 'Ferqê {{PLURAL:$2|Yew rewizyonê|$2 rewizyonê}} {{PLURAL:$2|dı|dı}} ($1) sero çıniyo.
 
@@ -1380,7 +1380,7 @@ No normal de werênayış dê pelanê besterneyan dı ena xırabin asena.
 Detayê besternayışi [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} tiya dı] aseno.',
 
 # Search results
-'searchresults' => 'Neticeya geyrayışi',
+'searchresults' => 'Neticeyê geyrayışi',
 'searchresults-title' => 'Qandê "$1" neticeyê geyrayışi',
 'toomanymatches' => 'Zêde teki (zewci) peyser çarnay, şıma rê zehmet, be persê do bin ra bıcerrebnên.',
 'titlematches' => 'Tekê (zewcê) sernameyê pele',
@@ -1393,7 +1393,7 @@ Detayê besternayışi [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}
 'shown-title' => 'bimocne $1î  {{PLURAL:$1|netice|neticeyan}} ser her pel',
 'viewprevnext' => '($1 {{int:pipe-separator}} $2) ($3) bıvênên',
 'searchmenu-exists' => "''Ena 'Wikipediya de ser \"[[:\$1]]\" yew pel esto'''",
-'searchmenu-new' => '<strong>Na wiki de pela "[[:$1]]"\'i vıraze!</strong> {{PLURAL:$2|0=|Zewmi prea ke şıma geyrayê cı ay bıvinê.|Nericanê cı geyrayış da xo bıvinê.}}',
+'searchmenu-new' => '<strong>Na wiki de pela "[[:$1]]" vıraze!</strong> {{PLURAL:$2|0=|Sewbina pela ke şıma geyrayê cı aye bıvênê.|Yew zi neticanê cıgeyrayışê xo bıvênê.}}',
 'searchprofile-articles' => 'Pelê tedeestey',
 'searchprofile-project' => 'Pelê peşti û procey',
 'searchprofile-images' => 'Multimedya',
@@ -1419,7 +1419,7 @@ Detayê besternayışi [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}
 'searchrelated' => 'eleqeyın',
 'searchall' => 'pêro',
 'showingresults' => '#<strong>$2</strong> netican ra {{PLURAL:$1|<strong>1</strong> netice cêr dero|<strong>$1</strong> neticey cêr derê}}.',
-'showingresultsinrange' => '{{PLURAL:$1|<strong>1</strong> netice|<strong>$1</strong> neticey}} ra #<strong>$2</strong> hetana #<strong>$3</strong>.êyê cêrde asenê.',
+'showingresultsinrange' => '{{PLURAL:$1|<strong>1</strong> netice|<strong>$1</strong> neticey}} be mabeynê #<strong>$2</strong> ra be #<strong>$3</strong> cêr asenê.',
 'showingresultsnum' => '#<strong>$2</strong> netican ra {{PLURAL:$3|<strong>1</strong> netice cêr dero|<strong>$3</strong> neticey cêr derê}}.',
 'showingresultsheader' => "{{PLURAL:$5|Neticeyê '''$1''' of '''$3'''|Neticeyanê '''$1 - $2''' hetê '''$3'''}} qe '''$4'''",
 'search-nonefound' => 'Zey perskerdışê şıma netice nêvêniya.',
@@ -1518,8 +1518,8 @@ Na game tepeya nêerziyena.',
 'prefs-help-signature' => 'Peran de vatenana de vatışi"<nowiki>~~~~</nowiki>" ya do imza bé, no bahdo beno çerğé imza u wahdey zemani',
 'badsig' => 'Îmzayê tu raşt niyo.
 Etiketê HTMLî kontrol bike.',
-'badsiglength' => 'İmzayê şıma zaf dergo.
-$1 gani bınê no {{PLURAL:$1|karakter|karakter}} de bıbo.',
+'badsiglength' => 'İmzaya şıma zaf derga.
+$1 gani {{PLURAL:$1|karakter|karakteran}} ra şenık bo.',
 'yourgender' => 'Çıçiy cı esto?',
 'gender-unknown' => 'Ez detay nivana',
 'gender-male' => 'Perané wiki camérd deyne ezo vırnena',
@@ -4121,13 +4121,13 @@ Ti hem zi eşkeno [[Special:EditWatchlist|use the standard editor]].',
 'version-svn-revision' => '(r$2)',
 'version-license' => 'Lisansê MediaWiki',
 'version-ext-license' => 'Lisans',
-'version-ext-colheader-name' => 'Dergen',
+'version-ext-colheader-name' => 'Dergiye',
 'version-ext-colheader-version' => 'Versiyon',
 'version-ext-colheader-license' => 'Lisans',
-'version-ext-colheader-description' => 'Akerdış',
-'version-ext-colheader-credits' => 'Nuşti',
-'version-license-title' => 'Semedê $1 lisans',
-'version-credits-title' => 'Semedê $1 krediy',
+'version-ext-colheader-description' => 'Şınasnayış',
+'version-ext-colheader-credits' => 'Nuştekari',
+'version-license-title' => 'Semedê $1 ra lisans',
+'version-credits-title' => 'Semedê $1 ra krediy',
 'version-poweredby-credits' => "Ena wiki, dezginda '''[https://www.mediawiki.org/ MediaWiki]''' ya piya vıraziyaya, heqê telifi © 2001-$1 $2.",
 'version-poweredby-others' => 'Zewmi',
 'version-poweredby-translators' => "Açernere translatewiki.net'i",
@@ -4347,7 +4347,7 @@ satır ê ke pê ney # # destpêkenê zey mışore/mıjore muamele vineno.
 'api-error-overwrite' => 'Ser yew dosyayê ke hama esta, ser ey qeyd nibena.',
 'api-error-stashfailed' => 'Xırabiya zerrek:Wasteri idari dosyey kerdi vıni.',
 'api-error-publishfailed' => 'Xetaya zerrey: Cıgeyrayoği nêşiya dosyaya rocaniye akero.',
-'api-error-stasherror' => 'Dosya cay berkerden de xeta vıcyê',
+'api-error-stasherror' => 'Dosya embari rê ke bar biye xeta veciye.',
 'api-error-timeout' => 'Cıwab dayışê wasteri peyra mend.',
 'api-error-unclassified' => 'Yew xeteyê nizanyeni biya.',
 'api-error-unknown-code' => "$1'dı jew xeta vıciye",
index 88b729d..5e92d3d 100644 (file)
@@ -3577,6 +3577,7 @@ $2',
 'thumbnail_image-type'     => 'Image type not supported',
 'thumbnail_gd-library'     => 'Incomplete GD library configuration: Missing function $1',
 'thumbnail_image-missing'  => 'File seems to be missing: $1',
+'thumbnail_image-failure-limit' => 'There have been too many recent failed attempts ($1 or more) to render this thumbnail. Please try again later.',
 
 # Special:Import
 'import'                     => 'Import pages',
index 2e28dc3..256c3e9 100644 (file)
@@ -590,7 +590,7 @@ $1',
 
 'ok' => 'Ek!',
 'retrievedfrom' => 'Elŝutita el  "$1"',
-'youhavenewmessages' => 'Vi havas $1 ($2).',
+'youhavenewmessages' => '{{PLURAL:$3|Vi havas}} $1 ($2).',
 'youhavenewmessagesfromusers' => 'Riceviĝis $1 de {{PLURAL:$3|alia uzanto|$3 uzantoj}} ($2).',
 'youhavenewmessagesmanyusers' => 'Riceviĝis $1 de multaj uzantoj ($2).',
 'newmessageslinkplural' => '{{PLURAL:$1|nova mesaĝo|999=novaj mesaĝoj}}',
@@ -741,6 +741,7 @@ Ne forgesu ŝanĝi viajn [[Special:Preferences|{{SITENAME}}-preferojn]]',
 'yourname' => 'Salutnomo:',
 'userlogin-yourname' => 'Uzantonomo',
 'userlogin-yourname-ph' => 'Enigu vian uzantonomon',
+'createacct-another-username-ph' => 'Enigu la salutnomon:',
 'yourpassword' => 'Pasvorto:',
 'userlogin-yourpassword' => 'Pasvorto',
 'userlogin-yourpassword-ph' => 'Enigu vian pasvorton',
@@ -777,6 +778,7 @@ Ne forgesu ŝanĝi viajn [[Special:Preferences|{{SITENAME}}-preferojn]]',
 'createacct-emailrequired' => 'Retpoŝta adreso',
 'createacct-emailoptional' => 'Retpoŝta adreso (nedeviga)',
 'createacct-email-ph' => 'Enigu vian retpoŝtan adreson',
+'createacct-another-email-ph' => 'Enigu la retpoŝtan adreson',
 'createaccountmail' => 'Uzi provizoran hazardsignan pasvorton kaj sendi ĝin al la retpoŝta adreso ĉi-suba',
 'createacct-realname' => 'Vera nomo (nedeviga)',
 'createaccountreason' => 'Kialo:',
@@ -933,6 +935,7 @@ Provizora pasvorto: $2',
 'changeemail-cancel' => 'Nuligi',
 
 # Special:ResetTokens
+'resettokens' => 'Renovigi ŝlosilojn',
 'resettokens-no-tokens' => 'Ne estas ŝlosiloj renovigeblaj.',
 'resettokens-legend' => 'Renovigi ŝlosilojn',
 'resettokens-tokens' => 'Ŝlosiloj:',
@@ -1695,6 +1698,7 @@ indekso pro troŝarĝita servilo. Intertempe, vi povas serĉi per <i>guglo</i> a
 
 # Recent changes
 'nchanges' => '$1 {{PLURAL:$1|ŝanĝo|ŝanĝoj}}',
+'enhancedrc-since-last-visit' => '$1 {{PLURAL:$1|ekde lasta vizito}}',
 'enhancedrc-history' => 'historio',
 'recentchanges' => 'Lastaj ŝanĝoj',
 'recentchanges-legend' => 'Opcioj pri lastaj ŝanĝoj',
@@ -1989,6 +1993,7 @@ Kiam oni filtras ĝin laŭ uzanto, nur la aktuala versio de la dosiero estos mon
 'listfiles_size' => 'Grandeco',
 'listfiles_description' => 'Priskribo',
 'listfiles_count' => 'Versioj',
+'listfiles-latestversion' => 'Nuna versio',
 'listfiles-latestversion-yes' => 'Jes',
 'listfiles-latestversion-no' => 'Ne',
 
@@ -2086,6 +2091,12 @@ Bonvolu kontroli aliajn ligilojn al la ŝablonoj antaŭ ol forigi ilin.',
 'randompage' => 'Hazarda paĝo',
 'randompage-nopages' => 'Ne ekzistas paĝoj en la {{PLURAL:$2|nomspaco|nomspacoj}}: "$1".',
 
+# Random page in category
+'randomincategory' => 'Hazarda paĝo en kategorio',
+'randomincategory-invalidcategory' => '"$1" ne estas valida kategoria nomo.',
+'randomincategory-nopages' => 'Ne estas paĝoj en la kategorio [[:Category:$1|$1]].',
+'randomincategory-selectcategory-submit' => 'Ek',
+
 # Random redirect
 'randomredirect' => 'Hazarda alidirekto',
 'randomredirect-nopages' => 'Estas neniuj alidirektiloj en la nomspaco "$1".',
@@ -2113,6 +2124,7 @@ Bonvolu kontroli aliajn ligilojn al la ŝablonoj antaŭ ol forigi ilin.',
 
 'pageswithprop' => 'Paĝoj kun paĝa atributo',
 'pageswithprop-legend' => 'Paĝoj kun paĝa atributo',
+'pageswithprop-text' => 'Ĉi tiu paĝo listigas paĝoj kiu uzas iajn paĝajn ecojn.',
 'pageswithprop-prop' => 'Nomo de la atributo:',
 'pageswithprop-submit' => 'Ek',
 
@@ -3067,6 +3079,7 @@ Datoj de versioj kaj nomoj de redaktantoj estos preservitaj.
 'tooltip-undo' => '"Malfari" malfaris ĉi tiun redakton kaj malfermas la redakto-paĝon en antaŭvida reĝimo. Permesas aldoni kialon en la resumo.',
 'tooltip-preferences-save' => 'Konservi preferojn',
 'tooltip-summary' => 'Enigu mallongan resumon',
+'interlanguage-link-title' => '$1 — $2',
 
 # Stylesheets
 'common.css' => '/* La jena CSS influos la aspekton de ĉiaj temoj. */',
@@ -3151,6 +3164,7 @@ Datoj de versioj kaj nomoj de redaktantoj estos preservitaj.
 'pageinfo-magic-words' => '{{PLURAL:$1|Magia vorto|Magiaj vortoj}} ($1)',
 'pageinfo-hidden-categories' => '{{PLURAL:$1|Kaŝita kategorio|Kaŝitaj kategorioj}} ($1)',
 'pageinfo-templates' => '{{PLURAL:$1|Inkluzivita ŝablono|Inkluzivitaj ŝablonoj}} ($1)',
+'pageinfo-transclusions' => '{{PLURAL:$1|Paĝo transinkluzivita|Paĝoj transinkluzivitaj}} en ($1)',
 'pageinfo-toolboxlink' => 'Informoj pri la paĝo',
 'pageinfo-redirectsto' => 'Alidirektas al',
 'pageinfo-redirectsto-info' => 'Informo',
@@ -3925,6 +3939,11 @@ Oni devis doni al vi [{{SERVER}}{{SCRIPTPATH}}/COPYING ekzempleron de la GNU Gen
 
 # Special:Redirect
 'redirect-submit' => 'Ek',
+'redirect-value' => 'Valoro:',
+'redirect-user' => 'Salutnomo',
+'redirect-revision' => 'Revizio de la paĝo',
+'redirect-file' => 'Dosiernomo',
+'redirect-not-exists' => 'Valoro ne trovita',
 
 # Special:FileDuplicateSearch
 'fileduplicatesearch' => 'Serĉu duplikatajn dosierojn',
@@ -3972,12 +3991,16 @@ Oni devis doni al vi [{{SERVER}}{{SCRIPTPATH}}/COPYING ekzempleron de la GNU Gen
 'tags' => 'Validaj ŝanĝaj etikedoj',
 'tag-filter' => '[[Special:Tags|Etikeda]] filtrilo:',
 'tag-filter-submit' => 'Filtrilo',
+'tag-list-wrapper' => '([[Special:Tags|{{PLURAL:$1|Etikedo|Etikedoj}}]]: $2)',
 'tags-title' => 'Etikedoj',
 'tags-intro' => 'Ĉi tiu paĝo montras la etikedojn kun kiuj la programaro markus redakton, kaj iliaj signifoj.',
 'tags-tag' => 'Etikeda nomo',
 'tags-display-header' => 'Aspekto en ŝanĝaj listoj',
 'tags-description-header' => 'Plena priskribo pri signifo',
+'tags-active-header' => 'Aktiva',
 'tags-hitcount-header' => 'Markitaj ŝanĝoj',
+'tags-active-yes' => 'Jes',
+'tags-active-no' => 'Ne',
 'tags-edit' => 'redakti',
 'tags-hitcount' => '$1 {{PLURAL:$1|ŝanĝo|ŝanĝoj}}',
 
@@ -3996,7 +4019,8 @@ Oni devis doni al vi [{{SERVER}}{{SCRIPTPATH}}/COPYING ekzempleron de la GNU Gen
 'dberr-header' => 'Ĉi tiu vikio havas problemon',
 'dberr-problems' => 'Bedaŭrinde, ĉi tiu retejo suferas pro teknikaj problemoj.',
 'dberr-again' => 'Bonvolu atendi kelkajn minutojn kaj reŝargi.',
-'dberr-info' => '(Ne povas kontakti la datenbazan servilon: $1)',
+'dberr-info' => '(Ne eblas kontakti la datenbazan servilon: $1)',
+'dberr-info-hidden' => '(Ne eblas kontakti la datenbazan servilon)',
 'dberr-usegoogle' => 'Vi povas serĉi Guglon dume.',
 'dberr-outofdate' => 'Notu ke iliaj indeksoj de nia enhavo eble ne estas ĝisdatigaj.',
 'dberr-cachederror' => 'Jen kaŝmemorigita kopio de la petita paĝo, kaj eble ne estas ĝisdatigita.',
@@ -4128,6 +4152,14 @@ Aŭ vi povas uzi la facilan formularon sube. Via komento estos aldonita al la pa
 'duration-centuries' => '$1 {{PLURAL:$1|jarcento|jarcentoj}}',
 'duration-millennia' => '$1 {{PLURAL:$1|jarmilo|jarmiloj}}',
 
+# Image rotation
+'rotate-comment' => 'Bildo pivotita $1 {{PLURAL:$1|gradon|gradojn}} dekstren',
+
+# Limit report
+'limitreport-cputime-value' => '$1 {{PLURAL:$1|sekundo|sekundoj}}',
+'limitreport-walltime-value' => '$1 {{PLURAL:$1|sekundo|sekundoj}}',
+'limitreport-postexpandincludesize-value' => '$1/$2 {{PLURAL:$2|bitoko|bitokoj}}',
+
 # Special:ExpandTemplates
 'expandtemplates' => 'Ampleksigi ŝablonojn',
 'expand_templates_intro' => 'Ĉi tiu speciala paĝo traktas tekston kaj ampleksigas ĉiujn ŝablonojn en ĝi rekursie.
@@ -4144,4 +4176,6 @@ Aŭ vi povas uzi la facilan formularon sube. Via komento estos aldonita al la pa
 'expand_templates_generate_xml' => 'Montri XML-sintaksarbon',
 'expand_templates_preview' => 'Antaŭrigardo',
 
+# Unknown messages
+'uploadinvalidxml' => 'Ne eblas interpreti la XML-sintakson en la alŝutita dosiero',
 );
index 0463e4d..0558289 100644 (file)
@@ -1439,6 +1439,7 @@ Nota que usar los enlaces de navegación borrará las selecciones de esta column
 'showhideselectedversions' => 'Mostrar/ocultar versiones seleccionadas',
 'editundo' => 'deshacer',
 'diff-empty' => '(Sin diferencias)',
+'diff-multi-sameuser' => '({{PLURAL:$1|Una revisión intermedia|$1 revisiones intermedias}} por el mismo usuario no mostrado)',
 'diff-multi-manyusers' => '(No se {{PLURAL:$1|muestra una edición intermedia|muestran $1 ediciones intermedias}} de {{PLURAL:$2|un usuario|$2 usuarios}})',
 'difference-missing-revision' => 'No {{PLURAL:$2|se ha encontrado|se han encontrado}} {{PLURAL:$2|una revisión|$2 revisiones}} de esta diferencia ($1).
 
index 2491b83..0aa09fe 100644 (file)
@@ -2327,6 +2327,7 @@ Ezután minden, a lapon vagy annak vitalapján történő változást ott fogsz
 'watchmethod-list' => 'a legfrissebb szerkesztésekben található figyelt lapok',
 'watchlistcontains' => 'A figyelőlistádon {{PLURAL:$1|egy|$1}} lap szerepel.',
 'iteminvalidname' => "Probléma a '$1' elemmel: érvénytelen név...",
+'wlnote2' => 'Alább az utolsó {{PLURAL:$1| <strong> $1 </strong> óra}} változásai láthatók. A lista frissítésének ideje  $2 $3',
 'wlshowlast' => 'Az elmúlt $1 órában | $2 napon | $3 történt változtatások legyenek láthatóak',
 'watchlist-options' => 'A figyelőlista beállításai',
 
index 3753680..93d4a8c 100644 (file)
@@ -1282,7 +1282,8 @@ Frekari upplýsingar eru í [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENA
 'shown-title' => 'Sýna $1 {{PLURAL:$1|niðurstöðu|niðurstöður}} á hverri síðu',
 'viewprevnext' => 'Skoða ($1 {{int:pipe-separator}} $2) ($3).',
 'searchmenu-exists' => "'''Það er síða að nafni „[[:$1]]“ á þessum wiki'''",
-'searchmenu-new' => "'''Skapaðu síðuna \"[[:\$1]]\" á þessum wiki!'''",
+'searchmenu-new' => '<strong>Skapaðu síðuna "[[:$1]]" á þessum wiki!<strong>
+Sjá einnig {{PLURAL:$2|0=|leitarniðurstöðuna|leitarniðurstöðurnar}}.',
 'searchprofile-articles' => 'Efnissíður',
 'searchprofile-project' => 'Hjálpar- og verkefnasíður',
 'searchprofile-images' => 'Margmiðlanir',
@@ -1622,7 +1623,7 @@ Tölvupóstfang þitt er ekki gefið upp þegar aðrir notendur hafa samband vi
 'rclistfrom' => 'Sýna breytingar frá og með $1',
 'rcshowhideminor' => '$1 minniháttar breytingar',
 'rcshowhidebots' => '$1 vélmenni',
-'rcshowhideliu' => '$1 innskráða notendur',
+'rcshowhideliu' => '$1 skráðir notendur',
 'rcshowhideanons' => '$1 óinnskráða notendur',
 'rcshowhidepatr' => '$1 vaktaðar breytingar',
 'rcshowhidemine' => '$1 mínar breytingar',
index 0359aad..56787df 100644 (file)
@@ -468,7 +468,7 @@ Nustena cı qontrol ke.',
 Kerem ke, oncia bıcerrebne.',
 'wrongpasswordempty' => 'Parola thale kota cı.
 Kerem ke, oncia bıcerrebne.',
-'passwordtooshort' => 'Paroley tewr senık ebe {{PLURAL:$1|1 karakter|$1 karakteru}} gunê derg bê.',
+'passwordtooshort' => 'Paroley gunê tewr senık ebe {{PLURAL:$1|1 karakter|$1 karakteru}} derg bê.',
 'password-name-match' => 'Parola sıma namê sımaê karberi ra gunê ferqın bo.',
 'password-login-forbidden' => 'Namê nê karberi u gurenaena parola qedeğen biya.',
 'mailmypassword' => 'E-mail sera parola newiye bırusne',
@@ -794,7 +794,7 @@ Diqet kerê, beno ke tedeestê {{SITENAME}} uza endi rozane niyê.",
 'badsig' => "İmza kala nêvêrdiye.
 Etiketê ''HTML''i qontrol ke.",
 'badsiglength' => 'İmza to zaf derga.
-Gunê $1 {{PLURAL:$1|herfe|herfun}} ra senık bo.',
+$1 gunê {{PLURAL:$1|herfe|herfu}} ra senık bo.',
 'yourgender' => 'Cınsiyet:',
 'gender-male' => 'Cüamêrd',
 'gender-female' => 'Cüanıke',
index 5c3bfe7..233205a 100644 (file)
@@ -577,7 +577,7 @@ $messages = array(
 'viewtalkpage' => 'Видете го разговорот',
 'otherlanguages' => 'На други јазици',
 'redirectedfrom' => '(Пренасочено од $1)',
-'redirectpagesub' => 'СÑ\82Ñ\80аниÑ\86а Ð·Ð° Ð¿Ñ\80енаÑ\81оÑ\87Ñ\83ваÑ\9aе',
+'redirectpagesub' => 'Ð\9fÑ\80енаÑ\81оÑ\87Ñ\83ваÑ\87ка Ñ\81Ñ\82Ñ\80аниÑ\86а',
 'lastmodifiedat' => 'Последната промена на страницава е извршена на $1 г. во $2 ч.',
 'viewcount' => 'Оваа страница била посетена {{PLURAL:$1|еднаш|$1 пати}}.',
 'protectedpage' => 'Заштитена страница',
@@ -2948,7 +2948,7 @@ $1',
 'move-page' => 'Премести $1',
 'move-page-legend' => 'Премести страница',
 'movepagetext' => "Со користењето на овој образец можете да преименувате страница, преместувајќи ја целата нејзина историја под ново име.
-Стариот наслов ќе стане страница за пренасочување кон новиот наслов.
+Стариот наслов ќе стане пренасочувачка страница кон новиот наслов.
 Автоматски можете да ги подновите пренасочувањата кои покажуваат кон првобитниот наслов.
 Ако не изберете автоматско подновување, проверете на [[Special:DoubleRedirects|двојни]] или [[Special:BrokenRedirects|прекинати пренасочувања]].
 На вас е одговорноста да се осигурате дека врските ќе продолжат да насочуваат таму за каде се предвидени.
@@ -2959,7 +2959,7 @@ $1',
 Ова може да биде драстична и неочекувана промена за популарна страница;
 осигурајте се дека сте ги разбрале последиците од ова пред да продолжите.",
 'movepagetext-noredirectfixer' => "Со користењето на овој образец можете да преименувате страница, преместувајќи ја целата нејзина историја под ново име.
-Стариот наслов ќе стане страница за пренасочување кон новиот наслов.
+Стариот наслов ќе стане пренасочувачка страница кон новиот наслов.
 Автоматски можете да ги подновите пренасочувањата кои покажуваат кон првобитниот наслов.
 Не заборавајте да проверите [[Special:DoubleRedirects|двојни]] и [[Special:BrokenRedirects|прекинати пренасочувања]].
 На вас е одговорноста да се осигурате дека врските ќе продолжат да насочуваат таму за каде се предвидени.
index effdc8a..49338d7 100644 (file)
@@ -166,7 +166,7 @@ $messages = array(
 'category-empty' => "''Одоогийн байдлаар энэ ангилалд хуудас, медиа файл байхгүй байна.''",
 'hidden-categories' => '{{PLURAL:$1|Нуугдсан ангилал|Нуугдсан ангиллууд}}',
 'hidden-category-category' => 'Нуугдсан ангиллууд',
-'category-subcat-count' => '{{PLURAL:$2|ЭнÑ\8d Ð°Ð½Ð³Ð¸Ð»Ð°Ð»Ð´ Ð´Ð°Ñ\80ааÑ\85 Ð´Ñ\8dд Ð°Ð½Ð³Ð¸Ð»Ð°Ð» Ð» Ð±Ð°Ð¹Ð½Ð°.|ЭнÑ\8d Ð°Ð½Ð³Ð¸ Ð´Ð¾Ñ\82Ñ\80оо $2 Ð°Ð½Ð³Ð¸, Ð±Ò¯Ð»Ñ\8dгÑ\82Ñ\8dй. Ò®Ò¯Ð½Ñ\8dÑ\8dÑ\81 $1 Ð´Ð¾Ð¾Ñ\80 Ñ\85аÑ\80агдаж Ð±Ð°Ð¹Ð½Ð°.}}',
+'category-subcat-count' => '{{PLURAL:$2|ЭнÑ\8d Ð±Ò¯Ð»Ñ\8dг Ð·Ó©Ð²Ñ\85өн Ð´Ð°Ñ\80ааÑ\85 Ð´Ñ\8dд Ð±Ò¯Ð»Ñ\8dгÑ\82Ñ\8dй.|ЭнÑ\8d Ð±Ò¯Ð»Ñ\8dг Ð½Ð¸Ð¹Ñ\82 $2 -ооÑ\81 {{PLURAL:$1| Ð±Ò¯Ð»Ñ\8dгÑ\82Ñ\8dй.|$1 Ð±Ò¯Ð»Ð³Ò¯Ò¯Ð´Ñ\82Ñ\8dй.}}}}',
 'category-subcat-count-limited' => 'Энэ ангилалд {{PLURAL:$1| дэд ангилал|$1-н дэд ангилалууд}} байна.',
 'category-article-count' => '{{PLURAL:$2|Энд нэг хуудас байна.|Энд $2 хуудас байна. Үүнээс $1 доор харагдаж байна.}}',
 'category-article-count-limited' => 'Энэ ангилалд дараах {{PLURAL:$1|хуудас|$1 хуудаснууд}} байна.',
@@ -253,7 +253,7 @@ $messages = array(
 'articlepage' => 'Өгүүллийг үзэх',
 'talk' => 'Хэлэлцүүлэг',
 'views' => 'Харагдацууд',
-'toolbox' => 'Ð\91агаж Ñ\85Ñ\8dÑ\80Ñ\8dгÑ\81лүүд',
+'toolbox' => 'Ð¥Ñ\8dÑ\80Ñ\8dглүүÑ\80',
 'userpage' => 'Хэрэглэгчийн хуудсыг үзэх',
 'projectpage' => 'Төслийн хуудсыг үзэх',
 'imagepage' => 'Файлын хуудсыг үзэх',
@@ -1048,7 +1048,7 @@ $1",
 'shown-title' => 'Хуудас бүрд $1 {{PLURAL:$1|үр дүн}} гаргах',
 'viewprevnext' => 'Үзэх: ($1 {{int:pipe-separator}} $2) ($3)',
 'searchmenu-exists' => "'''Энэ викид \"[[:\$1]]\" гэсэн хуудас байна'''",
-'searchmenu-new' => "'''Энэ викид \"[[:\$1]]\" гэсэн хуудсыг үүсгэх!'''",
+'searchmenu-new' => '<strong> Энэ викид "[[:$1]]" хуудсыг үүсгэх!</strong> {{PLURAL:$2|0=|Мөн хайлтаар олдсон хуудсаа харна.|Мөн хайлтаар олдсон хуудсаа харна.}}',
 'searchprofile-articles' => 'Агуулгын хуудсууд',
 'searchprofile-project' => 'Тусламжийн болон төслийн хуудсууд',
 'searchprofile-images' => 'Мультмедиа',
@@ -1365,7 +1365,7 @@ $1 тэмдэгтээс богино байх ёстой.',
 'rclistfrom' => '$1-с хойших шинэ засваруудыг үзүүлэх',
 'rcshowhideminor' => 'Бага зэргийн засваруудыг $1',
 'rcshowhidebots' => 'Роботуудыг $1',
-'rcshowhideliu' => 'Ð\91Ò¯Ñ\80Ñ\82гÑ\8dлÑ\82Ñ\8dй Ñ\85Ñ\8dÑ\80Ñ\8dглÑ\8dгÑ\87дийг $1',
+'rcshowhideliu' => 'Ð\9dийÑ\82 $1 Ð±Ò¯Ñ\80Ñ\82гÑ\8dгдÑ\81Ñ\8dн Ñ\85Ñ\8dÑ\80Ñ\8dглÑ\8dгÑ\87ид',
 'rcshowhideanons' => 'Бүртгэлгүй хэрэглэгчдийг $1',
 'rcshowhidepatr' => 'Хянагдаж буй засваруудыг $1',
 'rcshowhidemine' => 'Миний засваруудыг $1',
@@ -2239,7 +2239,7 @@ $1',
 'contributions' => '{{GENDER:$1|Хэрэглэгчийн }} оруулсан хувь нэмэр',
 'contributions-title' => '$1 хэрэглэгчийн хувь нэмэр',
 'mycontris' => 'Оруулсан хувь нэмэр',
-'contribsub2' => 'Хэрэглэгч: $1 ($2)',
+'contribsub2' => 'Хэрэглэгч: {{GENDER:$3|$1}} ($2)',
 'nocontribs' => 'Энэ шалгуурт тохирох өөрчилсөн зүйлүүд олдсонгүй.',
 'uctop' => '(одоох)',
 'month' => 'Дараах сараас (өмнөх засварууд нь ч орно):',
@@ -3424,6 +3424,23 @@ $5
 # API errors
 'api-error-filename-tooshort' => 'Файлын нэр хэтэрхий урт байна.',
 'api-error-filetype-banned' => 'Ийм төрлийн файлыг хорьсон байна.',
+'api-error-illegal-filename' => 'Ийм хэрэглэгчийн нэр өгөх боломжгүй.',
+'api-error-internal-error' => 'Өөрийн алдаа: файлыг чинь upload хийх явцад алдаа гарлаа.',
+'api-error-mustbeloggedin' => 'файлаа upload хийхийн тулд эхлээд хэрэглэгчээр нэвтэр.',
+'api-error-mustbeposted' => 'Өөрийн алдаа: HTTP POST төрлийн хандалт шаардлагатай.',
+'api-error-noimageinfo' => 'upload хийгдсэн боловч файлын талаар ямарч мэдээлэл сервер өгсөнгүй.',
+'api-error-nomodule' => 'Өөрийн алдаа: upload хийх модулийг зааж өгөөгүй байна.',
+'api-error-ok-but-empty' => 'Өөрийн алдаа: Серверээс хариу ирсэнгүй.',
+'api-error-overwrite' => 'Ижил нэртэй файл оруулах хориотой.',
+'api-error-stashfailed' => 'Өөрийн алдаа: Серверт түр файл хадгалахад алдаа гарлаа.',
+'api-error-timeout' => 'Сервер хариу өгөлгүй удлаа.',
+'api-error-unclassified' => 'Тодорхойгүй алдаа гарлаа.',
+'api-error-unknown-code' => 'Тодорхойгүй алдаа: "$1".',
+'api-error-unknown-error' => 'Өөрийн алдаа: upload хийх үед алдаа гарлаа.',
+'api-error-unknown-warning' => 'Тодорхойгүй сануулга: $1',
+'api-error-unknownerror' => 'Тодорхойгүй алдаа: $1',
+'api-error-uploaddisabled' => 'Энэ викид upload хийхийг хориглосон.',
+'api-error-verification-error' => 'Файлын төрөл буруу, эсвэл дутуу татагдсан.',
 
 # Durations
 'duration-seconds' => '$1 {{PLURAL:$1|секунд|секунд}}',
index 04f64e3..baf86f0 100644 (file)
@@ -1194,6 +1194,7 @@ Argument ten będzie pominięty.',
 'undo-success' => 'Edycja może zostać wycofana. Porównaj ukazane poniżej różnice między wersjami, a następnie zapisz zmiany.',
 'undo-failure' => 'Edycja nie może zostać wycofana z powodu konfliktu z wersjami pośrednimi.',
 'undo-norev' => 'Edycja nie może być cofnięta, ponieważ nie istnieje lub została usunięta.',
+'undo-nochange' => 'Wygląda na to, że edycja została już anulowana.',
 'undo-summary' => 'Anulowanie wersji $1 autora [[Special:Contributions/$2|$2]] ([[User talk:$2|dyskusja]])',
 'undo-summary-username-hidden' => 'Anulowanie wersji $1 autorstwa ukrytego użytkownika',
 
@@ -1418,6 +1419,7 @@ Zazwyczaj jest to spowodowane przestarzałym linkiem do usuniętej strony. Powó
 'searchrelated' => 'pokrewne',
 'searchall' => 'wszystkie',
 'showingresults' => "Poniżej znajduje się lista {{PLURAL:$1|z '''1''' wynikiem|'''$1''' wyników}}, rozpoczynając od wyniku numer '''$2'''.",
+'showingresultsinrange' => 'Poniżej wyświetlono {{PLURAL:$1|<strong>1</strong> wynik|<strong>$1</strong> wyniki|<strong>$1</strong> wyników}} w zakresie # od <strong>$2</strong> # do <strong>$3</strong>.',
 'showingresultsnum' => "Poniżej znajduje się lista {{PLURAL:$3|z '''1''' wynikiem|'''$3''' wyników}}, rozpoczynając od wyniku numer '''$2'''.",
 'showingresultsheader' => "{{PLURAL:$5|Wynik '''$1''' z '''$3'''|Wyniki '''$1 – $2''' z '''$3'''}} dla '''$4'''",
 'search-nonefound' => 'Brak wyników spełniających kryteria podane w zapytaniu.',
index 356be9b..b004b12 100644 (file)
@@ -91,6 +91,7 @@
  * @author Mihai
  * @author Minh Nguyen
  * @author Moha
+ * @author MongolWiki
  * @author Mormegil
  * @author Mpradeep
  * @author Murma174
@@ -398,7 +399,6 @@ Parameters:
 'category-subcat-count' => 'This message is displayed at the top of a category page showing the number of pages in the category.
 
 Parameters:
-* $1 - number of subcategories shown
 * $2 - total number of subcategories in category',
 'category-subcat-count-limited' => 'This message is displayed at the top of a category page showing the number of pages in the category when not all pages in a category are counted.
 
@@ -7269,6 +7269,8 @@ See also:
 *$1 is a function name of the GD library',
 'thumbnail_image-missing' => 'This is the parameter 1 of the message {{msg-mw|thumbnail error}}.
 *$1 is the path incl. filename of the missing image',
+'thumbnail_image-failure-limit' => 'This is the parameter 1 of the message {{msg-mw|thumbnail error}}.
+*$1 is the maximum allowed number of failed attempts',
 
 # Special:Import
 'import' => 'The title of the special page [[Special:Import]];',
@@ -9649,7 +9651,7 @@ Quotation marks, for quoting, sometimes titles etc., depending on the language.
 
 See: [[w:Non-English usage of quotation marks|Non-English usage of quotation marks on Wikipedia]].
 
-Parameters: 
+Parameters:
 * $1 - text to be wrapped in quotation marks',
 
 # Multipage image navigation
index 63db689..f715235 100644 (file)
@@ -833,7 +833,7 @@ $2',
 'userlogin' => 'Пријава/регистрација',
 'userloginnocreate' => 'Пријава',
 'logout' => 'Одјава',
-'userlogout' => 'Ð\9eдÑ\98ава',
+'userlogout' => 'Ð\9eдÑ\98ави Ð¼Ðµ',
 'notloggedin' => 'Нисте пријављени',
 'userlogin-noaccount' => 'Немате налог?',
 'userlogin-joinproject' => 'Отворите га',
index 80ec328..c140704 100644 (file)
@@ -732,7 +732,7 @@ Imajte na umu da neke stranice mogu nastaviti da se prikazuju kao da ste još pr
 'userlogin' => 'Prijava/registracija',
 'userloginnocreate' => 'Prijava',
 'logout' => 'Odjava',
-'userlogout' => 'Odjava',
+'userlogout' => 'Odjavi me',
 'notloggedin' => 'Niste prijavljeni',
 'userlogin-noaccount' => 'Nemate nalog?',
 'userlogin-joinproject' => 'Otvorite ga',
index d32c4dd..6b654b5 100644 (file)
@@ -1423,6 +1423,7 @@ Detaljer kan hittas i [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}
 'searchrelated' => 'relaterad',
 'searchall' => 'alla',
 'showingresults' => "Nedan visas upp till {{PLURAL:$1|'''1''' post|'''$1''' poster}} från och med nummer '''$2'''.",
+'showingresultsinrange' => 'Nedan visas upp till {{PLURAL:$3|<strong>1</strong> resultat|<strong>$1</strong> resultat}} mellan nummer <strong>$2</strong> och nummer <strong>$3</strong>.',
 'showingresultsnum' => "Nedan visas {{PLURAL:$3|'''1''' post|'''$3''' poster}} från och med nummer '''$2'''.",
 'showingresultsheader' => "{{PLURAL:$5|Resultat '''$1''' av '''$3'''|Resultat '''$1 - $2''' av '''$3'''}} för '''$4'''",
 'search-nonefound' => 'Inga resultat matchade frågan.',
@@ -1864,6 +1865,8 @@ Om du fortfarande vill ladda upp din fil, var god gå tillbaka och välj ett nyt
 Om du ändå vill ladda upp din fil, gå då tillbaka och använd ett annat namn. [[File:$1|thumb|center|$1]]',
 'file-exists-duplicate' => 'Den här filen är en dubblett till följande {{PLURAL:$1|fil|filer}}:',
 'file-deleted-duplicate' => 'En identisk fil till den här filen ([[:$1]]) har tidigare raderats. Du bör kontrollera den filens raderingshistorik innan du fortsätter att återuppladda den.',
+'file-deleted-duplicate-notitle' => 'En identisk fil till den här filen har tidigare raderats och titeln har undanhållits.
+Du borde be någon som kan se undanhållen fildata att granska situationen innan du försöker ladda upp den.',
 'uploadwarning' => 'Uppladdningsvarning',
 'uploadwarning-text' => 'Var god och ändra filbeskrivningen nedanför och försök igen.',
 'savefile' => 'Spara fil',
@@ -2517,7 +2520,7 @@ Se $2 för noteringar om de senaste raderingarna.',
 'delete-edit-reasonlist' => 'Redigera anledningar för radering',
 'delete-toobig' => 'Denna sida har en lång redigeringshistorik med mer än $1 {{PLURAL:$1|sidversion|sidversioner}}. Borttagning av sådana sidor har begränsats för att förhindra oavsiktliga driftstörningar på {{SITENAME}}.',
 'delete-warning-toobig' => 'Denna sida har en lång redigeringshistorik med mer än $1 {{PLURAL:$1|sidversion|sidversioner}}. Att radera sidan kan skapa problem med hanteringen av databasen på {{SITENAME}}; var försiktig.',
-'deleting-backlinks-warning' => "'''Varning:''' Andra sidor länkar till sidan som du är på väg att radera.",
+'deleting-backlinks-warning' => "'''Varning:''' Andra sidor länkar till eller inkluderar sidan som du är på väg att radera.",
 
 # Rollback
 'rollback' => 'Rulla tillbaka ändringar',
@@ -2817,7 +2820,7 @@ Se [[Special:BlockList|blockeringslistan]] för en översikt av gällande blocke
 'range_block_disabled' => 'Möjligheten för administratörer att blockera intervall av IP-adresser har stängts av.',
 'ipb_expiry_invalid' => 'Ogiltig varaktighetstid.',
 'ipb_expiry_temp' => 'För att dölja användarnamnet måste blockeringen vara permanent.',
-'ipb_hide_invalid' => 'Kan inte undanhålla detta konto; det kan ha för många redigeringar.',
+'ipb_hide_invalid' => 'Kan inte undanhålla detta konto; det har fler än {{PLURAL:$1|en redigering|$1 redigeringar}}.',
 'ipb_already_blocked' => '"$1" är redan blockerad',
 'ipb-needreblock' => '$1 är redan blockerad. Vill du ändra inställningarna?',
 'ipb-otherblocks-header' => 'Andra {{PLURAL:$1|blockering|blockeringar}}',
@@ -3047,6 +3050,7 @@ Spara den på din dator och ladda upp den här.',
 'import-error-special' => 'Sidan "$1" är inte importerad eftersom den tillhör en särskild namnrymd som inte tillåter sidor.',
 'import-error-invalid' => 'Sidan "$1" är inte importerad eftersom dess namn är ogiltigt.',
 'import-error-unserialize' => 'Versionen $2 av sidan "$1" kunde inte avserialiseras. Versionen rapporterades för att använda innehållsmodellen $3, som serialiserades som $4.',
+'import-error-bad-location' => 'Sidversionen $2 som använder innehållsmodellen $3 kan inte lagras på "$1" i denna wiki eftersom modellen inte stöds på den där sidan.',
 'import-options-wrong' => 'Fel {{PLURAL:$2|alternativ|alternativ}}: <nowiki>$1</nowiki>',
 'import-rootpage-invalid' => 'Angiven grundsida är en ogiltig titel.',
 'import-rootpage-nosubpage' => 'Namnrymden "$1" till grundsidan tillåter inte undersidor.',
index e726387..4be19de 100644 (file)
@@ -148,12 +148,12 @@ $messages = array(
 'tog-hidepatrolled' => 'ఇటీవలి మార్పులలో నిఘా ఉన్న మార్పులను దాచు',
 'tog-newpageshidepatrolled' => 'కొత్త పేజీల జాబితా నుంచి నిఘా ఉన్న పేజీలను దాచు',
 'tog-extendwatchlist' => 'కేవలం ఇటీవలి మార్పులే కాక, మార్పులన్నీ చూపించటానికి నా వీక్షణా జాబితాను పెద్దది చేయి',
-'tog-usenewrc' => 'à°\87à°\9fà±\80వలి à°®à°¾à°°à±\8dà°ªà±\81à°²à±\81 à°®à°°à°¿à°¯à±\81 à°µà±\80à°\95à±\8dà°·à°£ à°\9cాబితాలలà±\8b à°®à°¾à°°à±\8dà°ªà±\81లనà±\81 à°ªà±\87à°\9cà±\80 à°µà°¾à°°à°¿గా చూపించు',
+'tog-usenewrc' => 'à°\87à°\9fà±\80వలి à°®à°¾à°°à±\8dà°ªà±\81à°²à±\81 à°®à°°à°¿à°¯à±\81 à°µà±\80à°\95à±\8dà°·à°£ à°\9cాబితాలలà±\8b à°®à°¾à°°à±\8dà°ªà±\81లనà±\81 à°ªà±\87à°\9cà±\80 à°µà°¾à°°à±\80గా చూపించు',
 'tog-numberheadings' => 'శీర్షికలకు అప్రమేయంగా వరుస సంఖ్యలు చేర్చు',
 'tog-showtoolbar' => 'దిద్దుబాటు పనిముట్ల పట్టీని చూపించు',
 'tog-editondblclick' => 'డబుల్‌ క్లిక్కు చేసినప్పుడు పేజీని మార్చు',
 'tog-editsectiononrightclick' => 'విభాగాల శీర్షికల మీద కుడినొక్కుతో విభాగపు దిద్దుబాటును చేతనంచేయి',
-'tog-rememberpassword' => 'ఈ విహారిణిలో నా ప్రవేశాన్ని గుర్తుంచుకో (గరిష్ఠంగా $1 {{PLURAL:$1|రోజు|రోజుల}}కి)',
+'tog-rememberpassword' => 'ఈ విహారిణిలో నా ప్రవేశాన్ని (గరిష్ఠంగా $1 {{PLURAL:$1|రోజు|రోజుల}} పాటు) గుర్తుంచుకో',
 'tog-watchcreations' => 'నేను సృష్టించే పేజీలను మరియు దస్త్రాలను నా వీక్షణ జాబితాకు చేర్చు',
 'tog-watchdefault' => 'నేను మార్చే పేజీలను మరియు దస్త్రాలను నా వీక్షణ జాబితాకు చేర్చు',
 'tog-watchmoves' => 'నేను తరలించిన పేజీలను మరియు దస్త్రాలను నా వీక్షణ జాబితాకు చేర్చు',
@@ -177,7 +177,7 @@ $messages = array(
 'tog-watchlisthideanons' => 'అజ్ఞాత వాడుకరుల మార్పులను విక్షణా జాబితాలో చూపించకు',
 'tog-watchlisthidepatrolled' => 'నిఘా ఉన్న మార్పులను వీక్షణజాబితా నుంచి దాచిపెట్టు',
 'tog-ccmeonemails' => 'నేను ఇతర వాడుకరులకు పంపించే ఈ-మెయిళ్ల కాపీలను నాకు కూడా పంపు',
-'tog-diffonly' => 'తేడాలను చూపిస్తున్నపుడు, కింద చూపించే పేజీలోని సమాచారాన్ని చూపించొద్దు',
+'tog-diffonly' => 'తేడాల కింద, పేజీలోని సమాచారాన్ని చూపించొద్దు',
 'tog-showhiddencats' => 'దాచిన వర్గాలను చూపించు',
 'tog-norollbackdiff' => 'రద్దు చేసాక తేడాలు చూపించవద్దు',
 'tog-useeditwarning' => 'ఏదైనా పేజీని నేను వదిలివెళ్తున్నప్పుడు దానిలో భద్రపరచని మార్పులు ఉంటే నన్ను హెచ్చరించు',
@@ -366,7 +366,7 @@ $messages = array(
 'otherlanguages' => 'ఇతర భాషలలో',
 'redirectedfrom' => '($1 నుండి మళ్ళించబడింది)',
 'redirectpagesub' => 'దారిమార్పు పుట',
-'lastmodifiedat' => 'à°\88 à°ªà±\87à°\9cà±\80à°\95à°¿ $2, $1à°¨ à°\9aివరి à°®à°¾à°°à±\8dà°ªà±\81 à°\9cà°°à°¿à°\97à°¿à°¨ది.',
+'lastmodifiedat' => 'à°\88 à°ªà±\87à°\9cà±\80à°²à±\8b à°\9aివరి à°®à°¾à°°à±\8dà°ªà±\81 $1 à°¨ $2 à°\95à±\81 à°\9cà°°à°¿à°\97à°¿à°\82ది.',
 'viewcount' => 'ఈ పేజీ {{PLURAL:$1|ఒక్క సారి|$1 సార్లు}} దర్శించబడింది.',
 'protectedpage' => 'సంరక్షణలోని పేజీ',
 'jumpto' => 'ఇక్కడికి గెంతు:',
@@ -386,8 +386,8 @@ $1',
 'aboutpage' => 'Project:గురించి',
 'copyright' => 'విషయం $1 కి లోబడి లభ్యం, వేరుగా పేర్కొంటే తప్ప.',
 'copyrightpage' => '{{ns:project}}:ప్రచురణ హక్కులు',
-'currentevents' => 'à°\87à°ªà±\8dà°ªà°\9fà°¿ à°®à±\81à°\9aà±\8dà°\9aà°\9fà±\8dలు',
-'currentevents-url' => 'Project:à°\87à°ªà±\8dà°ªà°\9fà°¿ à°®à±\81à°\9aà±\8dà°\9aà°\9fà±\8dలు',
+'currentevents' => 'వరà±\8dతమాన à°\98à°\9fà°¨లు',
+'currentevents-url' => 'Project:వరà±\8dతమాన à°\98à°\9fà°¨లు',
 'disclaimers' => 'అస్వీకారములు',
 'disclaimerpage' => 'Project:సాధారణ నిష్పూచీ',
 'edithelp' => 'దిద్దుబాటు సహాయం',
@@ -945,7 +945,7 @@ $2
 'sectioneditnotsupported-text' => 'ఈ పేజీలో విభాగాల దిద్దుబాటుకి తోడ్పాటు లేదు.',
 'permissionserrors' => 'అనుమతి లోపం',
 'permissionserrorstext' => 'కింద పేర్కొన్న {{PLURAL:$1|కారణం|కారణాల}} మూలంగా, ఆ పని చెయ్యడానికి మీకు అనుమతిలేదు:',
-'permissionserrorstext-withaction' => 'ఈ క్రింది {{PLURAL:$1|కారణం|కారణాల}} వల్ల, మీకు $2 అనుమతి లేదు:',
+'permissionserrorstext-withaction' => 'ఈ క్రింది {{PLURAL:$1|కారణం|కారణాల}} వల్ల, $2 అనుమతి మీకు లేదు:',
 'recreate-moveddeleted-warn' => "'''హెచ్చరిక: ఇంతకు మునుపు ఒకసారి తొలగించిన పేజీని మళ్లీ సృష్టిద్దామని మీరు ప్రయత్నిస్తున్నారు.'''
 
 ఈ పేజీపై మార్పులు చేసేముందు, అవి ఇక్కడ ఉండతగినవేనా కాదా అని ఒకసారి ఆలోచించండి.
@@ -982,8 +982,8 @@ $2
 
 పార్సరు {{PLURAL:$2|పిలుపు|పిలుపులు}} $2 కంటే తక్కువ ఉండాలి,  ప్రస్తుతం {{PLURAL:$1|$1 పిలుపు ఉంది|$1  పిలుపులు ఉన్నాయి}}.',
 'expensive-parserfunction-category' => 'పార్సరు సందేశాలు అధికంగా ఉన్న పేజీలు',
-'post-expand-template-inclusion-warning' => "'''హెచ్చరిక''': మూస చేర్పు సైజు చాలా పెద్దదిగా ఉంది.
-à°\95à±\8aà°¨à±\8dని à°®à±\82సలనà±\81 à°\9aà±\87à°°à±\8dà°\9aà°²à±\87à°¦à±\81.",
+'post-expand-template-inclusion-warning' => '<strong>హెచ్చరిక:</strong> మూస ఇముడ్పు సైజు చాలా పెద్దదిగా ఉంది.
+à°\95à±\8aà°¨à±\8dని à°®à±\82సలà±\81 à°\87మడà±\8dà°\9aబడవà±\81.',
 'post-expand-template-inclusion-category' => 'మూస చేర్పు సైజును అధిగమించిన పేజీలు',
 'post-expand-template-argument-warning' => 'హెచ్చరిక: చాల పెద్ద సైజున్న మూస ఆర్గ్యుమెంటు, కనీసం ఒకటి, ఈ పేజీలో ఉంది.
 ఈ ఆర్గ్యుమెంట్లను వదలివేసాం.',
@@ -1095,8 +1095,8 @@ $3 ఇచ్చిన కారణం: ''$2''",
 'revdelete-hide-user' => 'దిద్దుబాటు చేసినవారి వాడుకరి పేరు/ఐపీ చిరునామా',
 'revdelete-hide-restricted' => 'డేటాను అందరిలాగే నిర్వాహకులకు కూడా కనబడనివ్వకు',
 'revdelete-radio-same' => '(మార్చకు)',
-'revdelete-radio-set' => 'దాà°\9aà°¿à°¨',
-'revdelete-radio-unset' => 'à°\9aà±\82పిన',
+'revdelete-radio-set' => 'దాà°\9aà±\81',
+'revdelete-radio-unset' => 'à°\9aà±\82పిà°\82à°\9aà±\81',
 'revdelete-suppress' => 'డేటాను అందరిలాగే నిర్వాహకులకు కూడా కనబడనివ్వకు',
 'revdelete-unsuppress' => 'పునస్థాపిత కూర్పులపై నిబంధనలను తీసివెయ్యి',
 'revdelete-log' => 'కారణం:',
@@ -1192,7 +1192,7 @@ $1",
 'prevn' => 'క్రితం {{PLURAL:$1|$1}}',
 'nextn' => 'తరువాతి {{PLURAL:$1|$1}}',
 'prevn-title' => 'గత $1 {{PLURAL:$1|ఫలితం|ఫలితాలు}}',
-'nextn-title' => 'తదà±\81పరి $1 {{PLURAL:$1|ఫలితం|ఫలితాలు}}',
+'nextn-title' => 'తరà±\81వాతి $1 {{PLURAL:$1|ఫలితం|ఫలితాలు}}',
 'shown-title' => 'పేజీకి $1 {{PLURAL:$1|ఫలితాన్ని|ఫలితాలను}} చూపించు',
 'viewprevnext' => '($1 {{int:pipe-separator}} $2) ($3) చూపించు.',
 'searchmenu-exists' => "'''ఈ వికీలో \"[[:\$1]]\" అనే పేజీ ఉంది'''",
@@ -1421,7 +1421,7 @@ $1",
 'right-reupload-shared' => 'స్థానికంగా ఉమ్మడి మీడియా సొరుగులోని ఫైళ్ళను అధిక్రమించు',
 'right-upload_by_url' => 'URL అడ్రసునుండి ఫైలును అప్‌లోడు చెయ్యి',
 'right-purge' => 'పేజీకి సంబంధించిన సైటు కాషెను, నిర్ధారణ కోరకుండానే తొలగించు',
-'right-autoconfirmed' => 'à°\85à°°à±\8dà°§ à°¸à°\82à°°à°\95à±\8dషణలà±\8b à°\89à°¨à±\8dà°¨ à°ªà±\87à°\9cà±\80లలà±\8b à°¦à°¿à°¦à±\8dà°¦à±\81బాà°\9fà±\81 à°\9aà±\86à°¯à±\8dయి',
+'right-autoconfirmed' => 'à°\90à°ªà±\80 à°\86ధారిత à°°à±\87à°\9fà±\81 à°ªà°°à°¿à°®à°¿à°¤à±\81à°²à±\81 à°ªà±\8dరభావà°\82 à°\9aà±\82పవà±\81',
 'right-bot' => 'ఆటోమాటిక్ ప్రాసెస్ లాగా భావించబడు',
 'right-nominornewtalk' => 'చర్చా పేజీల్లో జరిగిన అతి చిన్న మార్పులకు కొత్తసందేశము వచ్చిందన్న సూచన చెయ్యవద్దు',
 'right-apihighlimits' => 'API ప్రశ్నల్లో ఉన్నత పరిమితులను వాడు',
@@ -1507,7 +1507,7 @@ $1",
 'action-protect' => 'ఈ పేజీకి సంరక్షణా స్థాయిని మార్చే',
 'action-rollback' => 'ఏదైనా పేజీలో మార్పులు చేసిన చివరి వాడుకరి యొక్క మార్పులను త్వరితంగా వెనక్కి తీసుకెళ్ళు',
 'action-import' => 'మరో వికీ నుండి ఈ పేజీని దిగుమతి చెయ్యి',
-'action-importupload' => 'à°\8eà°\97à±\81మతి à°\9aà±\87సిన à°«à±\88à°²à±\81 à°¨à±\81à°\82à°¡à°¿ à°\88 à°ªà±\87à°\9cà±\80à°²à±\8bనిà°\95ి దిగుమతి చేసే',
+'action-importupload' => 'à°«à±\88à°²à±\81 à°\8eà°\95à±\8dà°\95à°¿à°\82à°ªà±\81 à°¨à±\81à°\82à°¡ి దిగుమతి చేసే',
 'action-patrol' => 'ఇతరుల మార్పులను పర్యవేక్షించినవిగా గుర్తించే',
 'action-autopatrol' => 'మీ మార్పులను పర్యవేక్షించినవిగా గుర్తించే',
 'action-unwatchedpages' => 'వీక్షణలో లేని పేజీల జాబితాని చూసే',
@@ -1781,8 +1781,7 @@ https://www.mediawiki.org/wiki/Manual:Image_Authorization చూడండి.',
 'upload_source_file' => ' (మీ కంప్యూటర్లో ఒక ఫైలు)',
 
 # Special:ListFiles
-'listfiles-summary' => 'ఈ ప్రత్యేక పేజీ ఇప్పటి వరకూ ఎక్కించిన దస్త్రాలన్నింటినీ చూపిస్తుంది.
-వాడుకరి పేరు మీద వడపోసినప్పుడు, ఆ వాడుకరి ఎక్కించిన కూర్పు ఆ దస్త్రం యొక్క సరికొత్త కూర్పు అయితేనే చూపిస్తుంది.',
+'listfiles-summary' => 'ఈ ప్రత్యేక పేజీ, ఎక్కించిన ఫైళ్ళన్నిటినీ చూపిస్తుంది.',
 'listfiles_search_for' => 'మీడియా పేరుకై వెతుకు:',
 'imgfile' => 'దస్త్రం',
 'listfiles' => 'దస్త్రాల జాబితా',
@@ -1807,7 +1806,7 @@ https://www.mediawiki.org/wiki/Manual:Image_Authorization చూడండి.',
 'filehist-current' => 'ప్రస్తుత',
 'filehist-datetime' => 'తేదీ/సమయం',
 'filehist-thumb' => 'నఖచిత్రం',
-'filehist-thumbtext' => '$1 à°¯à±\8aà°\95à±\8dà°\95 à°¨à°\96à°\9aà°¿à°¤à±\8dà°° à°\95à±\82à°°à±\8dà°ªà±\81',
+'filehist-thumbtext' => '$1 à°¨à°¾à°\9fà°¿ à°\95à±\82à°°à±\8dà°ªà±\81 à°¯à±\8aà°\95à±\8dà°\95 à°¨à°\96à°\9aà°¿à°¤à±\8dà°°à°\82',
 'filehist-nothumb' => 'నఖచిత్రం లేదు',
 'filehist-user' => 'వాడుకరి',
 'filehist-dimensions' => 'కొలతలు',
@@ -2268,7 +2267,7 @@ $UNWATCHURL కి వెళ్ళండి.
 చివరి మార్పులు చేసినవారు: [[User:$3|$3]] ([[User talk:$3|చర్చ]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).',
 'editcomment' => "దిద్దుబాటు సారాశం: \"''\$1''\".",
 'revertpage' => '[[Special:Contributions/$2|$2]] ([[User talk:$2|చర్చ]]) చేసిన మార్పులను [[User:$1|$1]] యొక్క చివరి కూర్పు వరకు తిప్పికొట్టారు.',
-'revertpage-nouser' => '(తొలగించిన వాడుకరిపేరు) చేసిన మార్పులను [[User:$1|$1]] యొక్క చివరి కూర్పుకి తిప్పికొట్టారు',
+'revertpage-nouser' => 'దాచబడిన వాడుకరి చేసిన మార్పులను [[User:$1|$1]] యొక్క చివరి కూర్పుకి తిప్పికొట్టారు',
 'rollback-success' => '$1 చేసిన దిద్దుబాట్లను వెనక్కు తీసుకెళ్ళాం; తిరిగి $2 చేసిన చివరి కూర్పుకు మార్చాం.',
 
 # Edit tokens
@@ -2479,8 +2478,8 @@ $1',
 'ipb-confirm' => 'నిరోధాన్ని ధృవపరచండి',
 'badipaddress' => 'సరైన ఐ.పి. అడ్రసు కాదు',
 'blockipsuccesssub' => 'నిరోధం విజయవంతం అయింది',
-'blockipsuccesstext' => '[[Special:Contributions/$1|$1]] నిరోధించబడింది.
-<br />నిరోధాల సమీక్ష కొరకు [[Special:BlockList|ఐ.పి. నిరొధాల జాబితా]] చూడండి.',
+'blockipsuccesstext' => '[[Special:Contributions/$1|$1]] నిరోధించబడింది.<br />
+నిరోధాల సమీక్ష కొరకు [[Special:BlockList|నిరోధాల జాబితా]] చూడండి.',
 'ipb-blockingself' => 'మిమ్మల్ని మీరే నిరోధించుకోబోతున్నారు! అదే మీ నిశ్చయమా?',
 'ipb-edit-dropdown' => 'నిరోధపు కారణాలను మార్చండి',
 'ipb-unblock-addr' => '$1 పై ఉన్న నిరోధాన్ని తొలగించండి',
@@ -2644,7 +2643,7 @@ $1',
 'movesubpagetext' => 'ఈ పేజీకి క్రింద చూపించిన $1 {{PLURAL:$1|ఉపపేజీ ఉంది|ఉపపేజీలు ఉన్నాయి}}.',
 'movenosubpage' => 'ఈ పేజీకి ఉపపేజీలు ఏమీ లేవు.',
 'movereason' => 'కారణం:',
-'revertmove' => 'తరలిà°\82à°ªà±\81à°¨à±\81 à°°à°¦à±\8dà°¦à±\81à°\9aà±\87యి',
+'revertmove' => 'à°µà±\86à°¨à°\95à±\8dà°\95à±\81 à°¤à°¿à°ªà±\8dà°ªà±\81',
 'delete_and_move' => 'తొలగించి, తరలించు',
 'delete_and_move_text' => '==తొలగింపు అవసరం==
 
@@ -2673,7 +2672,7 @@ $1',
 దయచేసి మరొక పేరుని ఎంచుకోండి.',
 
 # Export
-'export' => 'à°\8eà°\97à±\81మతి à°ªà±\87à°\9cà±\80à°²à±\81',
+'export' => 'à°ªà±\87à°\9cà±\80à°² à°\8eà°\97à±\81మతి',
 'exporttext' => 'ఎంచుకున్న పేజీ లేదా పేజీలలోని వ్యాసం మరియు పేజీ చరితం లను XML లో ఎగుమతి చేసుకోవచ్చు. MediaWiki ని ఉపయోగించి Special:Import page ద్వారా దీన్ని వేరే వికీ లోకి దిగుమతి చేసుకోవచ్చు.
 
 పేజీలను ఎగుమతి చేసందుకు, కింద ఇచ్చిన టెక్స్టు బాక్సులో పేజీ పేర్లను లైనుకో పేరు చొప్పున ఇవ్వండి. ప్రస్తుత కూర్పుతో పాటు పాత కూర్పులు కూడా కావాలా, లేక ప్రస్తుత కూర్పు మాత్రమే చాలా అనే విషయం కూడా ఇవ్వవచ్చు.
@@ -2806,7 +2805,7 @@ $2',
 'tooltip-ca-delete' => 'ఈ పేజీని తొలగించండి',
 'tooltip-ca-undelete' => 'ఈ పేజీని తొలగించడానికి ముందు చేసిన మార్పులను పునఃస్థాపించు',
 'tooltip-ca-move' => 'ఈ పేజీని తరలించండి',
-'tooltip-ca-watch' => 'à°\88 à°ªà±\87à°\9cà±\80ని à°®à±\80 à°µà°¿à°\95à±\8dషణా à°\9cాబితాà°\95à°¿ చేర్చుకోండి',
+'tooltip-ca-watch' => 'à°\88 à°ªà±\87à°\9cà±\80ని à°®à±\80 à°µà±\80à°\95à±\8dà°·à°£ à°\9cాబితాà°\95à±\81 చేర్చుకోండి',
 'tooltip-ca-unwatch' => 'ఈ పేజీని మీ విక్షణా జాబితా నుండి తొలగించండి',
 'tooltip-search' => '{{SITENAME}} లో వెతకండి',
 'tooltip-search-go' => 'ఇదే పేరుతో పేజీ ఉంటే అక్కడికి తీసుకెళ్ళు',
@@ -2815,7 +2814,7 @@ $2',
 'tooltip-n-mainpage' => 'తలపుటను చూడండి',
 'tooltip-n-mainpage-description' => 'మొదటి పుటను చూడండి',
 'tooltip-n-portal' => 'ప్రాజెక్టు గురించి, మీరేం చేయవచ్చు, సమాచారం ఎక్కడ దొరుకుతుంది',
-'tooltip-n-currentevents' => 'à°\87à°ªà±\8dà°ªà°\9fà°¿ à°®à±\81à°\9aà±\8dà°\9aà°\9fà±\8dà°² à°¯à±\8aà°\95à±\8dà°\95 à°®à±\81à°¨à±\81à°ªà°\9fà°¿ à°®à°\82దలనà±\81 à°¤à±\86à°²à±\81à°¸à±\81à°\95à±\8aà°¨à±\81డి',
+'tooltip-n-currentevents' => 'వరà±\8dతమాన à°\98à°\9fనల à°¯à±\8aà°\95à±\8dà°\95 à°¨à±\87పథà±\8dయానà±\8dని à°¤à±\86à°²à±\81à°¸à±\81à°\95à±\8bà°\82డి',
 'tooltip-n-recentchanges' => 'వికీలో ఇటీవల జరిగిన మార్పుల జాబితా.',
 'tooltip-n-randompage' => 'ఓ యాదృచ్చిక పేజీని చూడండి',
 'tooltip-n-help' => 'తెలుసుకోడానికి ఓ మంచి ప్రదేశం.',
@@ -3570,11 +3569,22 @@ $5
 'version-parser-function-hooks' => 'పార్సరుకు కొక్కాలు',
 'version-hook-name' => 'కొక్కెం పేరు',
 'version-hook-subscribedby' => 'ఉపయోగిస్తున్నవి',
-'version-version' => '(సంచిక $1)',
-'version-license' => 'లైసెన్సు',
+'version-version' => '(కూర్పు $1)',
+'version-license' => 'MediaWiki లైసెన్సు',
+'version-ext-license' => 'లైసెన్సు',
+'version-ext-colheader-name' => 'పొడిగింత',
+'version-ext-colheader-version' => 'కూర్పు',
+'version-ext-colheader-license' => 'లైసెన్సు',
+'version-ext-colheader-description' => 'వివరణ',
+'version-ext-colheader-credits' => 'కర్తలు',
+'version-license-title' => '$1 కోసం లైసెన్సు',
+'version-license-not-found' => 'ఈ పొడిగింతకు వివరమైన లైసెన్సు సమాచారమేమీ కనబడలేదు.',
+'version-credits-title' => '$1 యొక్క శ్రేయస్సులు',
+'version-credits-not-found' => 'ఈ పొడిగింతకు వివరమైన శ్రేయస్సు సమాచారమేమీ కనబడలేదు.',
 'version-poweredby-credits' => "ఈ వికీ  '''[https://www.mediawiki.org/ మీడియావికీ]'''చే శక్తిమంతం, కాపీహక్కులు  © 2001-$1 $2.",
 'version-poweredby-others' => 'ఇతరులు',
 'version-poweredby-translators' => 'translatewiki.net అనువాదకులు',
+'version-credits-summary' => 'కింది వ్యక్తులు [[Special:Version|MediaWiki]] కి చేసిన సేవకుగాను, వారిని గుర్తించదలచాం.',
 'version-license-info' => 'మీడియావికీ అన్నది స్వేచ్ఛా మృదూపకరణం; మీరు దీన్ని పునఃపంపిణీ చేయవచ్చు మరియు/లేదా ఫ్రీ సాఫ్ట్&zwnj;వేర్ ఫౌండేషన్ ప్రచురించిన గ్నూ జనరల్ పబ్లిక్ లైసెస్సు వెర్షను 2 లేదా (మీ ఎంపిక ప్రకారం) అంతకంటే కొత్త వెర్షను యొక్క నియమాలకు లోబడి మార్చుకోవచ్చు.
 
 మీడియావికీ ప్రజోపయోగ ఆకాంక్షతో పంపిణీ చేయబడుతుంది, కానీ ఎటువంటి వారంటీ లేకుండా; కనీసం ఏదైనా ప్రత్యేక ఉద్దేశానికి సరిపడుతుందని గానీ లేదా వస్తుత్వం యొక్క అంతర్నిహిత వారంటీ లేకుండా. మరిన్ని వివరాలకు గ్నూ జనరల్ పబ్లిక్ లైసెన్సుని చూడండి.
@@ -3610,8 +3620,7 @@ $5
 'specialpages' => 'ప్రత్యేక పేజీలు',
 'specialpages-note-top' => 'సూచిక',
 'specialpages-note' => '* మామూలు ప్రత్యేక పుటలు.
-* <strong class="mw-specialpagerestricted">నియంత్రిత ప్రత్యేక పుటలు.</strong>
-* <span class="mw-specialpagecached">Cached ప్రత్యేక పుటలు (పాతబడి ఉండొచ్చు).</span>',
+* <span class="mw-specialpagerestricted">నియంత్రిత ప్రత్యేక పుటలు.</span>',
 'specialpages-group-maintenance' => 'నిర్వహణా నివేదికలు',
 'specialpages-group-other' => 'ఇతర ప్రత్యేక పేజీలు',
 'specialpages-group-login' => 'ప్రవేశించండి / ఖాతాను సృష్టించుకోండి',
@@ -3643,11 +3652,13 @@ $5
 'tags' => 'సరైన మార్పు ట్యాగులు',
 'tag-filter' => '[[Special:Tags|ట్యాగుల]] వడపోత:',
 'tag-filter-submit' => 'వడపోయి',
+'tag-list-wrapper' => '([[Special:Tags|{{PLURAL:$1|ట్యాగు|ట్యాగులు}}]]: $2)',
 'tags-title' => 'టాగులు',
 'tags-intro' => 'ఈ పేజీ మృదూపకరణం మార్పులకు ఇచ్చే ట్యాగులను, మరియు వాటి అర్ధాలను చూపిస్తుంది.',
 'tags-tag' => 'ట్యాగు పేరు',
 'tags-display-header' => 'మార్పుల జాబితాలో కనపించు రీతి',
 'tags-description-header' => 'అర్థం యొక్క పూర్తి వివరణ',
+'tags-active-header' => 'క్రియాశీలం?',
 'tags-hitcount-header' => 'ట్యాగులున్న మార్పులు',
 'tags-active-yes' => 'అవును',
 'tags-active-no' => 'కాదు',
@@ -3670,6 +3681,7 @@ $5
 'dberr-problems' => 'క్షమించండి! ఈ సైటు సాంకేతిక సమస్యలని ఎదుర్కొంటుంది.',
 'dberr-again' => 'కొన్ని నిమిషాలాగి మళ్ళీ ప్రయత్నించండి.',
 'dberr-info' => '(డాటాబేసు సర్వరుని సంధానించలేకున్నాం: $1)',
+'dberr-info-hidden' => '(డేటాబేసు సర్వరును కాంటాక్టు చెయ్యలేకున్నాం)',
 'dberr-usegoogle' => 'ఈలోపు మీరు గూగుల్ ద్వారా వెతకడానికి ప్రయత్నించండి.',
 'dberr-outofdate' => 'మా విషయం యొక్క వారి సూచీలు అంత తాజావి కావపోవచ్చని గమనించండి.',
 'dberr-cachederror' => 'అభ్యర్థించిన పేజీ యొక్క కోశం లోని కాపీ ఇది, అంత తాజాది కాకపోవచ్చు.',
@@ -3701,9 +3713,16 @@ $5
 'logentry-delete-event-legacy' => '$3 లో లాగ్ ఘటనల కన్పట్టటాన్ని (విజిబిలిటీ) $1 {{GENDER:$2|మార్చారు}}',
 'logentry-delete-revision-legacy' => 'పేజీ $3 లో కూర్పుల కన్పట్టటాన్ని (విజిబిలిటీ) $1 {{GENDER:$2|మార్చారు}}',
 'logentry-suppress-delete' => 'పేజీ $3 ని $1 {{GENDER:$2|అణచిపెట్టారు}}',
+'logentry-suppress-event' => '$3 లోని {{PLURAL:$5|ఒక లాగ్ ఘటన|$5 లాగ్ ఘటనల}} ప్రేక్షకత్వాన్ని $1 రహస్యంగా {{GENDER:$2|మార్చారు}}: $4',
+'logentry-suppress-revision' => '$3 పేజీ యొక్క {{PLURAL:$5|ఒక కూర్పు|$5 కూర్పుల}} ప్రేక్షకత్వాన్ని $1 రహస్యంగా {{GENDER:$2|మార్చారు}}: $4',
+'logentry-suppress-event-legacy' => '$3 లోని లాగ్ ఘటనల ప్రేక్షకత్వాన్ని $1 రహస్యంగా {{GENDER:$2|మార్చారు}}',
+'logentry-suppress-revision-legacy' => 'పేజీ $3 యొక్క కూర్పుల ప్రేక్షకత్వాన్ని $1 రహస్యంగా {{GENDER:$2|మార్చారు}}',
 'revdelete-content-hid' => 'కంటెంట్ దాచబడింది',
 'revdelete-summary-hid' => 'మార్పుల సారాంశాన్ని దాచారు',
 'revdelete-uname-hid' => 'వాడుకరి పేరుని దాచారు',
+'revdelete-content-unhid' => 'కంటెంట్ బయటపెట్టబడింది',
+'revdelete-summary-unhid' => 'దిద్దుబాటు సారాంశం బయటపెట్టబడింది',
+'revdelete-uname-unhid' => 'వాడుకరిపేరు బయటపెట్టబడింది',
 'revdelete-restricted' => 'నిర్వాహకులకు ఆంక్షలు విధించాను',
 'revdelete-unrestricted' => 'నిర్వాహకులకున్న ఆంక్షలను ఎత్తేశాను',
 'logentry-move-move' => '$1, పేజీ $3 ను $4 కు {{GENDER:$2|తరలించారు}}',
@@ -3730,6 +3749,7 @@ $5
 'feedback-cancel' => 'రద్దుచేయి',
 'feedback-submit' => 'ప్రతిస్పందనను దాఖలుచేయి',
 'feedback-adding' => 'ఫీడ్‍బ్యాకును పేజీలోకి చేరుస్తున్నాం...',
+'feedback-error1' => 'లోపం: API నుండి గుర్తుపట్టలేని ఫలితం',
 'feedback-error2' => 'దోషము: సవరణ విఫలమైంది',
 'feedback-error3' => 'లోపం: API నుండి ప్రతిస్పందన లేదు',
 'feedback-thanks' => 'కృతజ్ఞతలు! మీ ప్రతిస్పందనను “[$2 $1]” పేజీలో చేర్చాం.',
@@ -3758,13 +3778,20 @@ $5
 'api-error-filetype-banned' => 'ఈ రకపు దస్త్రాలని నిషేధించారు.',
 'api-error-filetype-banned-type' => '$1, అనుమతించబడిన {{PLURAL:$4|ఫైలు రకం కాదు|ఫైలు రకాలు కాదు}}. అనుమతించబడిన {{PLURAL:$3|ఫైలు రకం|ఫైలు రకాలు}}: $2.',
 'api-error-filetype-missing' => 'ఫైలుపేరులో ఓ ఎక్స్టెన్షను లేదు.',
+'api-error-hookaborted' => 'మీరు చేయ ప్రయత్నించిన మార్పును ఓ పొడిగింత అడ్డుకుంది.',
 'api-error-http' => 'అంతర్గత దోషము: సేవకానికి అనుసంధానమవలేకపోతున్నది.',
 'api-error-illegal-filename' => 'ఆ పైల్ పేరు అనుమతించబడదు.',
+'api-error-internal-error' => 'అంతర్గత లోపం: ఈ వికీలో మీ ఎక్కింపును ప్రాసెసు చెయ్యడంలో ఎదో తప్పు జరిగింది.',
 'api-error-invalid-file-key' => 'అంతర్గత దోషము: తాత్కాలిక నిల్వలో ఫైల్ కనపడలేదు.',
 'api-error-mustbeloggedin' => 'దస్త్రాలను ఎక్కించడానికి మీరు ప్రవేశించివుండాలి.',
+'api-error-noimageinfo' => 'ఎక్కింపు జయప్రదమైంది. కానీ సర్వరు, ఆ ఫైలు గురించిన సమాచారమేమీ ఇవ్వలేదు.',
 'api-error-nomodule' => 'అంతర్గత దోషము: ఎక్కింపు పర్వికము అమర్చబడలేదు.',
 'api-error-ok-but-empty' => 'అంతర్గత దోషము: సేవకము నుండి ఎటువంటి స్పందనా లేదు.',
+'api-error-overwrite' => 'ఈసరికే ఉన్న ఫైలును తిరగరాయడానికి అనుమతి లేదు.',
 'api-error-stashfailed' => 'అంతర్గత పొరపాటు: తాత్కాలిక దస్త్రాన్ని భద్రపరచడంలో సేవకి విఫలమైంది.',
+'api-error-publishfailed' => 'అంతర్గత లోపం: తాత్కాలిక ఫైలును ప్రచురించడంలో సర్వరు విఫలమైంది.',
+'api-error-stasherror' => 'ఫైలును ఖాజానాకు ఎక్కించడంలో లోపం దొర్లింది.',
+'api-error-timeout' => 'సర్వరు ఆశించిన సమయం లోపు స్పందించలేదు.',
 'api-error-unclassified' => 'ఒక తెలియని దోషము సంభవించినది',
 'api-error-unknown-code' => 'తెలియని పొరపాటు: "$1".',
 'api-error-unknown-error' => 'అంతర్గత పొరపాటు: మీ దస్త్రాన్ని ఎక్కించేప్పుడు ఏదో పొరపాటు జరిగింది.',
@@ -3784,6 +3811,9 @@ $5
 'duration-centuries' => '$1 {{PLURAL:$1|శతాబ్దం|శతాబ్దాలు}}',
 'duration-millennia' => '$1 {{PLURAL:$1|సహస్రాబ్దం|సహస్రాబ్దాలు}}',
 
+# Image rotation
+'rotate-comment' => 'బొమ్మ సవ్యదిశలో $1 {{PLURAL:$1|డిగ్రీ|డిగ్రీలు}} తిప్పబడింది',
+
 # Limit report
 'limitreport-cputime' => 'CPU సమయం వినియోగం',
 'limitreport-cputime-value' => '$1 {{PLURAL:$1|క్షణం|క్షణాలు}}',
@@ -3799,9 +3829,12 @@ $5
 'expand_templates_input' => 'విస్తరించవలసిన పాఠ్యం:',
 'expand_templates_output' => 'ఫలితం',
 'expand_templates_xml_output' => 'XML ఔట్&zwnj;పుట్',
+'expand_templates_html_output' => 'ముడి HTML ఔట్‍పుట్',
 'expand_templates_ok' => 'సరే',
 'expand_templates_remove_comments' => 'వ్యాఖ్యలను తొలగించు',
+'expand_templates_remove_nowiki' => 'ఫలితంలో <nowiki> ట్యాగులను అణచిపెట్టు',
 'expand_templates_generate_xml' => 'XML పార్స్ ట్రీని చూపించు',
+'expand_templates_generate_rawhtml' => 'ముడి HTML ను చూపించు',
 'expand_templates_preview' => 'మునుజూపు',
 
 );
index 6b7f1e8..545f9d9 100644 (file)
@@ -275,7 +275,7 @@ $messages = array(
 'articlepage' => 'Kitaa in may sulod nga pakli',
 'talk' => 'Hiruhimangraw',
 'views' => 'Mga paglantaw',
-'toolbox' => 'Garamiton',
+'toolbox' => 'Mga higamit',
 'userpage' => 'Kitaa in pakli hin gumaramit',
 'projectpage' => 'Kitaa in pakli hin proyekto',
 'imagepage' => 'Kitaa in pakli hin fayl',
@@ -335,8 +335,8 @@ $1',
 'youhavenewmessages' => 'Mayda ka $1 ($2).',
 'youhavenewmessagesfromusers' => 'May-ada ka $1 tikang ha {{PLURAL:$3|iba nga gumaramit|$3 mga gumaramit}} ($2).',
 'youhavenewmessagesmanyusers' => 'May-ada ka $1 tikang ha damo nga mga gumaramit ($2).',
-'newmessageslinkplural' => '{{PLURAL:$1|uska bag-o nga mensahe|bag-o nga mga mensahe}}',
-'newmessagesdifflinkplural' => '$1 {{PLURAL:$1|nga pagbag-o|nga mga pagbag-o}}',
+'newmessageslinkplural' => '{{PLURAL:$1|usa ka bag-o nga mensahe|999=ka bag-o nga mga mensahe}}',
+'newmessagesdifflinkplural' => '$1 {{PLURAL:$1|nga pagbag-o|999=nga mga pagbag-o}}',
 'youhavenewmessagesmulti' => 'Mayda ka mga bag-o nga mensahe ha $1',
 'editsection' => 'igliwat',
 'editold' => 'igliwat',
@@ -466,7 +466,8 @@ An magdudurmara nga nagtrangka hini in naghatag hini nga eksplenasyon: "$3".',
 'invalidtitle-knownnamespace' => 'Titulo nga inbalido nga may pan-ngaran "$2 ngan teksto nga "$3"',
 'invalidtitle-unknownnamespace' => 'Diri ginkakarawat nga titulo tungod mayda ini hin mga diri nakikilala nga ngaran-lat\'ang ihap $1 ngan teksto "$2"',
 'exception-nologin' => 'Diri nakalog-in',
-'exception-nologin-text' => 'Ini nga pakli o pagbuhat in nagkikinahanglan nga ikaw in mag-log-in ha dinhi nga wiki.',
+'exception-nologin-text' => 'Alayon [[Special:Userlogin|pagsakob]] basi makakadto hiní nga pakli o buruhatón.',
+'exception-nologin-text-manual' => 'Alayon $1 basi makakadto hini nga pakli o buruhatón.',
 
 # Virus scanner
 'virus-badscanner' => "Maraot nga configuration: Waray kasabti nga virus scanner: ''$1''",
@@ -513,7 +514,7 @@ Ayaw kalimti pagbalyo han imo [[Special:Preferences|{{SITENAME}} preperensya]].'
 'gotaccount' => '¿Mayda kana akawnt? $1.',
 'gotaccountlink' => 'Sakob',
 'userlogin-resetlink' => 'Nangalimot han imo detalye han pagsakob?',
-'userlogin-resetpassword-link' => 'Ig-reset an imo tigaman-pagsakob',
+'userlogin-resetpassword-link' => '¿Nangalimot ka han imo tigaman-pansulod?',
 'helplogin-url' => 'Help:Pag-log-in',
 'userlogin-helplink' => '[[{{MediaWiki:helplogin-url}}|Bulig han pag-log-in]]',
 'userlogin-loggedin' => 'Nakalog-in kana komo hi {{GENDER:$1|$1}}.
@@ -569,7 +570,7 @@ Alayon pagutro pagbutang.',
 'passwordtooshort' => 'An tigaman-pagsulod dapat diri maubos hit {{PLURAL:$1|1 nga agi|$1 nga agi}}.',
 'password-name-match' => 'An imo tigaman-pagsulod in kinahanglan iba ha imo agnay-hiton-gumaramit.',
 'password-login-forbidden' => 'An paggamit hini nga agnay-hit-gumaramit ngan tigaman-pagsulod in diri gintutugotan.',
-'mailmypassword' => 'Ig-e-mail an bag-o nga tigaman-pagsulod',
+'mailmypassword' => 'Ig-reset an tigaman-pagsulod',
 'passwordremindertitle' => 'Bag-o nga diri-pirmihan nga tigaman-pagsulod para han {{SITENAME}}',
 'passwordremindertext' => 'May-ada tawo (posible ikaw, tikang ha IP address nga $1) in umaro hin bag-o nga tigaman-pagsakob para han {{SITENAME}} ($4). Uska temporaryo nga tigaman-pagsakob para han gumaramit 
 "$2" in nahimo ngan ginbutang nga "$3". Kun ini an imo panuyuan, kinahanglanon nim maglog-in ngan pumili hin bag-o nga tigaman-pagsakob yana.
@@ -581,16 +582,17 @@ Kun iba nga tawo an naghimo ini nga paalayon, o kun nakahinumdom ka han imo tiga
 'passwordsent' => 'Uska bag-o nga password in ginpadangat ha e-mail address nga nakarehistro kan "$1".
 Alayon paglog-in utro kahuman mo makarawat ini.',
 'blocked-mailpassword' => 'An imo IP address in ginpugong ha pag-edit, ngan tungod hini in diri gintutugotan paggamit han password recovery function para malikyan an abuso.',
-'eauthentsent' => 'Uska kompirmasyon nga e-mail in ginpadangan ha gin-ngaranan nga e-mail address.
-San-o matagan pa hin iba nga e-mail para ha imo akawnt, kinahanglan mo sundon an mga surundan nga nakasurat ha e-mail, para makompirma nga imo gud ito akawnt.',
+'eauthentsent' => 'Mayda e-mail hin pagkumpirma nga ginpadará hini nga ginhatag nga e-mail adres.
+
+San-o magatagán pa hin ibá nga e-mail it akwant, kinahanglan nimo sundon an mga tugon nga nahabutáng han email basi makumpirma nga imo gud itón akawnt.',
 'throttled-mailpassword' => 'Usa nga tigaman-pagnakob reset email in ginpadangat na, ha sakob han urhi nga  {{PLURAL:$1|oras|$1 ka mga oras}}.
 Basi diri ini maabuso, uusa la nga tigaman-panakob in igpapadangat kada {{PLURAL:$1|oras|$1 ka mga oras}}.',
 'mailerror' => 'Sayop han pagpadangat hin surat: $1',
 'acct_creation_throttle_hit' => 'An mga bisita hinin nga wiki nga nagamit hit imo IP address in naghimo hin {{PLURAL:$1|1 nga akawnt|$1 nga mga akawnt}} ha sulod han urhi nga adlaw, kun diin ini an pinakadamo nga gintutugotan para han sulod nga takna.
 
 An resulta, an mga bisita nga nagamit hini nga IP address in diri na makakahimo hin akawnt, ha pagkayana.',
-'emailauthenticated' => 'Ginpamatuod an imo e-mail adres han $2 ha $3.',
-'emailnotauthenticated' => 'An imo email address in diri pa otentikado.
+'emailauthenticated' => 'Ginkumpirma an imo e-mail adres han han $2 ha $3.',
+'emailnotauthenticated' => 'Diri pa nakumpirma an imo email adres.
 Waray email nga igpapadangat ha mga masunod nga higamit.',
 'noemailprefs' => 'Igbutang an imo email address ha imo preperensya para umandar ini nga mga higamit.',
 'emailconfirmlink' => 'Igkompirma an imo e-mail address',
@@ -629,6 +631,8 @@ Para mahuman paglalog-on, kinahanglan mo magbutang hin bag-o nga tigaman-panakob
 'retypenew' => 'Utroha pagbutang an bag-o nga tigaman-pagsulod:',
 'resetpass_submit' => 'Igbutang an password ngan log in',
 'changepassword-success' => 'Malinamposon an pagbal-iw hit imo tigaman-panakob!',
+'changepassword-throttled' => 'Damo na nga mga paningkamot hin pagsakob an imo ginhimò.
+Alayon paghulat hin $1 san-o ka umutro.',
 'resetpass_forbidden' => 'Diri mababalyoan an mga tigaman-pagsulod',
 'resetpass-no-info' => 'Kinahanglan mo paglog-in para direkta ka makasakob dinhi nga pakli.',
 'resetpass-submit-loggedin' => 'Igbal-iw an tigaman-pagsulod',
@@ -641,7 +645,7 @@ Imo malinamposon nga ginsalyuan an imo tigaman-panakob o umaro ka na hin bag-o n
 # Special:PasswordReset
 'passwordreset' => 'igreset an tigaman-hit-pagsulod',
 'passwordreset-text-one' => 'Kompletoha ini nga porma paramakareset hin imo tigaman-panakob.',
-'passwordreset-text-many' => '{{PLURAL:$1|Butanga ha usa nga mga surodlan para mareset iton imo tigaman-panakob.}}',
+'passwordreset-text-many' => '{{PLURAL:$1|Butanga it usa nga mga surodlan basi makakarawat ko hin temporaryo nga tigaman-pansulod pinaagi ha email.}}',
 'passwordreset-legend' => 'igreset an tigaman-hit-pagsulod',
 'passwordreset-disabled' => 'Waray ginpaandar an password reset hini nga wiki.',
 'passwordreset-emaildisabled' => 'Mga mga higamit ha email in waray pinaandar hini nga wiki.',
@@ -651,6 +655,7 @@ Imo malinamposon nga ginsalyuan an imo tigaman-panakob o umaro ka na hin bag-o n
 'passwordreset-capture-help' => 'Kun imo igtsek ini nga kahon, an email (lakip an temporaryo nga tigaman-panakob) in igpapakita ha imo labot la han ginpadangat ha gumaramit.',
 'passwordreset-email' => 'E-mail adres:',
 'passwordreset-emailtitle' => 'Mga detalye han akawnt ha {{SITENAME}}',
+'passwordreset-emailtext-ip' => '{{PLURAL:$3|Iní nga temporaryo nga tigaman-pansulod|Iní nga mga temporaryo nga tigaman-pansulod}} ma-waray bali hin {{PLURAL:$5|usa ka adlaw|$5 nga mga adlaw}}.',
 'passwordreset-emailelement' => 'Agnay han gumaramit: $1
 Temporaryo nga tigaman han pagsakob: $2',
 'passwordreset-emailsent' => 'Ginpadangat an password reset email.',
@@ -949,7 +954,7 @@ Diri mo ini malalabtan.',
 'shown-title' => 'Kitaa $1 {{PLURAL:$1|resulta|mga resulta}} kada pakli',
 'viewprevnext' => 'Kitaa an ($1 {{int:pipe-separator}} $2) ($3)',
 'searchmenu-exists' => "'''May-ada pakli nga nakangaran hin \"[[:\$1]]\" hini nga wiki.'''",
-'searchmenu-new' => "'''Himoa an pakli \"[[:\$1]]\" hini nga wiki!'''",
+'searchmenu-new' => '<strong>Himoa an pakli nga "[[:$1]]" dinhi nga wiki!</strong> {{PLURAL:$2|0=|Kitaa gihapon an pakli nga nabilngan han imo pagbiling.|Kitaa gihapon an mga nabilngan nga ginmawas han pagbiling.}}',
 'searchprofile-articles' => 'Mga unod nga pakli',
 'searchprofile-project' => 'Mga Bulig ngan Proyekto nga pakli',
 'searchprofile-images' => 'Multimedia',
@@ -1227,7 +1232,7 @@ Diri ka gintutugotan pagliwat han mga katungod han gumaramit ha iba nga mga wiki
 'rclistfrom' => 'Pakit-a an mga ginbag-ohan tikang han $1',
 'rcshowhideminor' => '$1 gudti nga mga pagliwat',
 'rcshowhidebots' => '$1 mga bot',
-'rcshowhideliu' => '$1 mga naka-log-in nga gumaramit',
+'rcshowhideliu' => '$1 ka rehistrado nga gumaramit',
 'rcshowhideanons' => '$1 waray nagpakilala nga mga gumaramit',
 'rcshowhidepatr' => '$1 mga pinatrolya nga mga paliwat',
 'rcshowhidemine' => '$1 akon mga ginliwat',
@@ -1567,7 +1572,7 @@ An paglaladawan han iya [$2 fayl han paglaladawan nga pakli] didto in ginpapakit
 'prevpage' => 'Nahiuna nga pakli ($1)',
 'allpagesfrom' => 'Igpakita an mga pakli nga nagtitikang ha:',
 'allpagesto' => 'Igpakita an mga pakli nga nahuhuman ha:',
-'allarticles' => 'Ngatanan nga mga artikulo',
+'allarticles' => 'Ngatanan nga mga barasahon',
 'allinnamespace' => "Ngatanan nga mga pakli ($1 ngaran-lat'ang)",
 'allpagessubmit' => 'Kadto-a',
 'allpages-bad-ns' => '{{SITENAME}} in waray ngaran-lat\'ang nga "$1".',
@@ -2106,7 +2111,7 @@ $1',
 'file-info-size' => '$1 × $2 nga pixel, kadako han fayl: $3, MIME nga tipo: $4',
 'file-nohires' => 'Waray mas hiruhitaas nga resolusyon.',
 'svg-long-desc' => 'SVG nga fayl, ginbabanabanahan nga $1 × $2 nga mga pixel, kadako han fayl: $3',
-'show-big-image' => 'Bug-os nga resolusyon',
+'show-big-image' => 'Orihinal nga paypay',
 'show-big-image-preview' => 'Kadako hin nga pahiuna nga pagawas: $1.',
 'show-big-image-other' => 'Iba {{PLURAL:$2|nga resolusyon|nga mga resolusyon}}: $1.',
 'show-big-image-size' => '$1 × $2 nga mga pixel',
index eecd793..44ef5b1 100644 (file)
@@ -2894,7 +2894,7 @@ $1被封禁的理由是“$2”',
 'allmessages-filter-legend' => '过滤',
 'allmessages-filter' => '按自定义状态过滤:',
 'allmessages-filter-unmodified' => '未修改',
-'allmessages-filter-all' => '所有',
+'allmessages-filter-all' => '全部',
 'allmessages-filter-modified' => '曾修改',
 'allmessages-prefix' => '以前缀过滤:',
 'allmessages-language' => '语言:',
index e50a054..dae4e43 100644 (file)
@@ -7,6 +7,7 @@
        "--warnings": ["-no_doc"],
        "--builtin-classes": true,
        "--output": "../../docs/js",
+       "--external": "HTMLElement,HTMLDocument,Window",
        "--": [
                "./external.js",
                "../../resources/mediawiki/mediawiki.js",
index ed85223..6e6c3ed 100644 (file)
@@ -2490,6 +2490,7 @@ $wgMessageStructure = array(
                'thumbnail_image-type',
                'thumbnail_gd-library',
                'thumbnail_image-missing',
+               'thumbnail_image-failure-limit'
        ),
        'import' => array(
                'import',
index a02459f..3ae330b 100644 (file)
@@ -1266,8 +1266,8 @@ return array(
                'targets' => array( 'desktop', 'mobile' ),
        ),
 
-       /* OOJS */
-       // WARNING: oojs is NOT COMPATIBLE with older browsers and
+       /* OOjs */
+       // WARNING: OOjs and OOjs-UI are NOT COMPATIBLE with older browsers and
        // WILL BREAK if loaded in browsers that don't support ES5
        'oojs' => array(
                'scripts' => array(
@@ -1275,4 +1275,23 @@ return array(
                ),
                'targets' => array( 'desktop', 'mobile' ),
        ),
+
+       'oojs-ui' => array(
+               'scripts' => array(
+                       'resources/oojs/oojs-ui.js',
+               ),
+               'styles' => array(
+                       'resources/oojs/oojs-ui.svg.css',
+               ),
+               'messages' => array(
+                       'ooui-dialog-action-close',
+                       'ooui-outline-control-move-down',
+                       'ooui-outline-control-move-up',
+                       'ooui-toolbar-more',
+               ),
+               'dependencies' => array(
+                       'oojs',
+               ),
+               'targets' => array( 'desktop', 'mobile' ),
+       ),
 );
index 43642d0..4c2fc3a 100644 (file)
 
                $previewDataHolder = $( '<div>' );
                targetUrl = $editform.attr( 'action' );
+               targetUrl += targetUrl.indexOf( '?' ) !== -1 ? '&' : '?';
+               targetUrl += $.param( {
+                       debug: mw.config.get( 'debug' ),
+                       uselang: mw.config.get( 'wgUserLanguage' ),
+                       useskin: mw.config.get( 'skin' )
+               } );
 
                // Gather all the data from the form
                postData = $editform.formToArray();
diff --git a/resources/oojs/.gitignore b/resources/oojs/.gitignore
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/resources/oojs/i18n/ace.json b/resources/oojs/i18n/ace.json
new file mode 100644 (file)
index 0000000..554ae57
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Si Gam Acèh"
+        ]
+    },
+    "ooui-dialog-action-close": "Tôp",
+    "ooui-outline-control-move-down": "Pinah item u yup",
+    "ooui-outline-control-move-up": "Pinah item u ateuëh",
+    "ooui-toolbar-more": "Lom"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/af.json b/resources/oojs/i18n/af.json
new file mode 100644 (file)
index 0000000..a622f89
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "@metadata": {
+        "authors": [
+            "Naudefj"
+        ]
+    },
+    "ooui-dialog-action-close": "Sluit",
+    "ooui-outline-control-move-down": "Skuif item af",
+    "ooui-outline-control-move-up": "Skuif item op"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/am.json b/resources/oojs/i18n/am.json
new file mode 100644 (file)
index 0000000..61e4ff6
--- /dev/null
@@ -0,0 +1,8 @@
+{
+    "@metadata": {
+        "authors": [
+            "Elfalem"
+        ]
+    },
+    "ooui-dialog-action-close": "ለመዝጋት"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ar.json b/resources/oojs/i18n/ar.json
new file mode 100644 (file)
index 0000000..65e1364
--- /dev/null
@@ -0,0 +1,18 @@
+{
+    "@metadata": {
+        "authors": [
+            "Ciphers",
+            "Claw eg",
+            "Elfalem",
+            "Jdforrester",
+            "Mido",
+            "OsamaK",
+            "زكريا",
+            "مشعل الحربي"
+        ]
+    },
+    "ooui-dialog-action-close": "أغلق",
+    "ooui-outline-control-move-down": "انقل العنصر للأسفل",
+    "ooui-outline-control-move-up": "انقل العنصر للأعلى",
+    "ooui-toolbar-more": "مزيد"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/arc.json b/resources/oojs/i18n/arc.json
new file mode 100644 (file)
index 0000000..0f9e75d
--- /dev/null
@@ -0,0 +1,8 @@
+{
+    "@metadata": {
+        "authors": [
+            "Basharh"
+        ]
+    },
+    "ooui-dialog-action-close": "ܣܟܘܪ"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ast.json b/resources/oojs/i18n/ast.json
new file mode 100644 (file)
index 0000000..959ea23
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "@metadata": {
+        "authors": [
+            "Basharh",
+            "Bishnu Saikia",
+            "Xuacu"
+        ]
+    },
+    "ooui-dialog-action-close": "Zarrar",
+    "ooui-outline-control-move-down": "Mover abaxo l'elementu",
+    "ooui-outline-control-move-up": "Mover arriba l'elementu",
+    "ooui-toolbar-more": "Más"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/az.json b/resources/oojs/i18n/az.json
new file mode 100644 (file)
index 0000000..8bfcf88
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Cekli829",
+            "Interfase",
+            "Jduranboger"
+        ]
+    },
+    "ooui-dialog-action-close": "Bağla",
+    "ooui-outline-control-move-down": "Bəndi aşağı apar",
+    "ooui-outline-control-move-up": "Bəndi yuxarı apar"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ba.json b/resources/oojs/i18n/ba.json
new file mode 100644 (file)
index 0000000..4af0114
--- /dev/null
@@ -0,0 +1,15 @@
+{
+    "@metadata": {
+        "authors": [
+            "AiseluRB",
+            "Amire80",
+            "Assele",
+            "Haqmar",
+            "Sagan",
+            "Рустам Нурыев"
+        ]
+    },
+    "ooui-dialog-action-close": "Ябырға",
+    "ooui-outline-control-move-down": "Аҫҡа күсерергә",
+    "ooui-outline-control-move-up": "Өҫкә күсерергә"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/bcl.json b/resources/oojs/i18n/bcl.json
new file mode 100644 (file)
index 0000000..aff451e
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Geopoet",
+            "Sky Harbor"
+        ]
+    },
+    "ooui-dialog-action-close": "Seraduhon",
+    "ooui-outline-control-move-down": "Balyuhon an aytem paibaba",
+    "ooui-outline-control-move-up": "Balyuhon an aytem paitaas",
+    "ooui-toolbar-more": "Kadugangan"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/be-tarask.json b/resources/oojs/i18n/be-tarask.json
new file mode 100644 (file)
index 0000000..5922f61
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "@metadata": {
+        "authors": [
+            "EugeneZelenko",
+            "Wizardist",
+            "Чаховіч Уладзіслаў",
+            "Zedlik"
+        ]
+    },
+    "ooui-dialog-action-close": "Закрыць",
+    "ooui-outline-control-move-down": "Перасунуць ніжэй",
+    "ooui-outline-control-move-up": "Перасунуць вышэй",
+    "ooui-toolbar-more": "Болей"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/be.json b/resources/oojs/i18n/be.json
new file mode 100644 (file)
index 0000000..3058ab8
--- /dev/null
@@ -0,0 +1,8 @@
+{
+    "@metadata": {
+        "authors": [
+            "Чаховіч Уладзіслаў"
+        ]
+    },
+    "ooui-dialog-action-close": "Закрыць"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/bg.json b/resources/oojs/i18n/bg.json
new file mode 100644 (file)
index 0000000..67e664b
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "DCLXVI",
+            "Hristofor.mirchev",
+            "පසිඳු කාවින්ද"
+        ]
+    },
+    "ooui-dialog-action-close": "Затваряне",
+    "ooui-toolbar-more": "Още"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/bn.json b/resources/oojs/i18n/bn.json
new file mode 100644 (file)
index 0000000..c321ab1
--- /dev/null
@@ -0,0 +1,16 @@
+{
+    "@metadata": {
+        "authors": [
+            "Aftab1995",
+            "Bellayet",
+            "Jayantanth",
+            "Nasir8891",
+            "Runab",
+            "Sayak Sarkar"
+        ]
+    },
+    "ooui-dialog-action-close": "বন্ধ",
+    "ooui-outline-control-move-down": "আইটেম নিচে স্থানান্তর",
+    "ooui-outline-control-move-up": "আইটেম উপরে স্থানান্তর",
+    "ooui-toolbar-more": "আরও"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/br.json b/resources/oojs/i18n/br.json
new file mode 100644 (file)
index 0000000..38eb9e8
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Fohanno",
+            "Fulup",
+            "Y-M D"
+        ]
+    },
+    "ooui-dialog-action-close": "Serriñ",
+    "ooui-outline-control-move-down": "Lakaat an elfenn da ziskenn",
+    "ooui-outline-control-move-up": "Lakaat an elfenn da bignat"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/bs.json b/resources/oojs/i18n/bs.json
new file mode 100644 (file)
index 0000000..7449f07
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "DzWiki"
+        ]
+    },
+    "ooui-dialog-action-close": "Zatvori",
+    "ooui-outline-control-move-down": "Premjesti stavku dole",
+    "ooui-outline-control-move-up": "Premjesti stavku gore",
+    "ooui-toolbar-more": "Više"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ca.json b/resources/oojs/i18n/ca.json
new file mode 100644 (file)
index 0000000..61bb1f6
--- /dev/null
@@ -0,0 +1,17 @@
+{
+    "@metadata": {
+        "authors": [
+            "Alvaro Vidal-Abarca",
+            "Amire80",
+            "Arnaugir",
+            "Pginer",
+            "QuimGil",
+            "SMP",
+            "Vriullop"
+        ]
+    },
+    "ooui-dialog-action-close": "Tanca",
+    "ooui-outline-control-move-down": "Baixa element",
+    "ooui-outline-control-move-up": "Puja element",
+    "ooui-toolbar-more": "Més"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ce.json b/resources/oojs/i18n/ce.json
new file mode 100644 (file)
index 0000000..1e145ea
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Amire80",
+            "Умар"
+        ]
+    },
+    "ooui-dialog-action-close": "ДӀачӀагӀа",
+    "ooui-outline-control-move-down": "Лаха яккха элемент",
+    "ooui-outline-control-move-up": "Лаккха яккха элемент",
+    "ooui-toolbar-more": "Кхин тӀе"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ckb.json b/resources/oojs/i18n/ckb.json
new file mode 100644 (file)
index 0000000..839f4a8
--- /dev/null
@@ -0,0 +1,9 @@
+{
+    "@metadata": {
+        "authors": [
+            "Calak",
+            "Muhammed taha"
+        ]
+    },
+    "ooui-dialog-action-close": "دایخە"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/co.json b/resources/oojs/i18n/co.json
new file mode 100644 (file)
index 0000000..e5edb21
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "@metadata": {
+        "authors": [
+            "Paulu"
+        ]
+    },
+    "ooui-dialog-action-close": "Chjude",
+    "ooui-outline-control-move-down": "Fà falà l'ogettu",
+    "ooui-outline-control-move-up": "Fà cullà l'ogettu"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/cs.json b/resources/oojs/i18n/cs.json
new file mode 100644 (file)
index 0000000..9661ec6
--- /dev/null
@@ -0,0 +1,20 @@
+{
+    "@metadata": {
+        "authors": [
+            "Chmee2",
+            "Jkjk",
+            "Juandev",
+            "Koo6",
+            "Littledogboy",
+            "Michaelbrabec",
+            "Mormegil",
+            "Polda18",
+            "Tchoř",
+            "ශ්වෙත"
+        ]
+    },
+    "ooui-dialog-action-close": "Zavřít",
+    "ooui-outline-control-move-down": "Přesunout položku dolů",
+    "ooui-outline-control-move-up": "Přesunout položku nahoru",
+    "ooui-toolbar-more": "Další"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/cu.json b/resources/oojs/i18n/cu.json
new file mode 100644 (file)
index 0000000..fa9b1cf
--- /dev/null
@@ -0,0 +1,8 @@
+{
+    "@metadata": {
+        "authors": [
+            "ОйЛ"
+        ]
+    },
+    "ooui-dialog-action-close": "ꙁакрꙑи"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/cy.json b/resources/oojs/i18n/cy.json
new file mode 100644 (file)
index 0000000..d37912d
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "@metadata": {
+        "authors": [
+            "Lloffiwr",
+            "Robin Owain",
+            "ОйЛ"
+        ]
+    },
+    "ooui-dialog-action-close": "Caeer",
+    "ooui-outline-control-move-down": "Symud yr eitem lawr",
+    "ooui-outline-control-move-up": "Symud yr eitem lan",
+    "ooui-toolbar-more": "Rhagor"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/da.json b/resources/oojs/i18n/da.json
new file mode 100644 (file)
index 0000000..bf47bcb
--- /dev/null
@@ -0,0 +1,17 @@
+{
+    "@metadata": {
+        "authors": [
+            "Cgtdk",
+            "Christian List",
+            "EileenSanda",
+            "Laketown",
+            "Palnatoke",
+            "Simeondahl",
+            "Tehnix"
+        ]
+    },
+    "ooui-dialog-action-close": "Luk",
+    "ooui-outline-control-move-down": "Flyt ned",
+    "ooui-outline-control-move-up": "Flyt op",
+    "ooui-toolbar-more": "Mere"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/de.json b/resources/oojs/i18n/de.json
new file mode 100644 (file)
index 0000000..3a66648
--- /dev/null
@@ -0,0 +1,20 @@
+{
+    "@metadata": {
+        "authors": [
+            "APPER",
+            "G.Hagedorn",
+            "Inkowik",
+            "Jcornelius",
+            "Jdforrester",
+            "Kghbln",
+            "Metalhead64",
+            "Murma174",
+            "Se4598",
+            "Tomabrafix"
+        ]
+    },
+    "ooui-dialog-action-close": "Schließen",
+    "ooui-outline-control-move-down": "Element nach unten verschieben",
+    "ooui-outline-control-move-up": "Element nach oben verschieben",
+    "ooui-toolbar-more": "Mehr"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/diq.json b/resources/oojs/i18n/diq.json
new file mode 100644 (file)
index 0000000..bb0ac35
--- /dev/null
@@ -0,0 +1,16 @@
+{
+    "@metadata": {
+        "authors": [
+            "Erdemaslancan",
+            "Gorizon",
+            "Kghbln",
+            "Marmase",
+            "Mirzali",
+            "Se4598"
+        ]
+    },
+    "ooui-dialog-action-close": "Racnê",
+    "ooui-outline-control-move-down": "Bendi bere cêr",
+    "ooui-outline-control-move-up": "Bendi bere cor",
+    "ooui-toolbar-more": "Zewbi"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/dsb.json b/resources/oojs/i18n/dsb.json
new file mode 100644 (file)
index 0000000..0f47587
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Michawiki"
+        ]
+    },
+    "ooui-dialog-action-close": "Zacyniś",
+    "ooui-outline-control-move-down": "Element dołoj pśesunuś",
+    "ooui-outline-control-move-up": "Element górjej pśesunuś",
+    "ooui-toolbar-more": "Wěcej"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/el.json b/resources/oojs/i18n/el.json
new file mode 100644 (file)
index 0000000..66051f1
--- /dev/null
@@ -0,0 +1,18 @@
+{
+    "@metadata": {
+        "authors": [
+            "Astralnet",
+            "Dipa1965",
+            "Evropi",
+            "FocalPoint",
+            "Geraki",
+            "Glavkos",
+            "Nikosguard",
+            "Tifa93"
+        ]
+    },
+    "ooui-dialog-action-close": "Κλείσιμο",
+    "ooui-outline-control-move-down": "Μετακίνηση προς τα κάτω",
+    "ooui-outline-control-move-up": "Μετακίνηση προς τα πάνω",
+    "ooui-toolbar-more": "Περισσότερα"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/eml.json b/resources/oojs/i18n/eml.json
new file mode 100644 (file)
index 0000000..5dd09f5
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Gloria sah",
+            "Lévi"
+        ]
+    },
+    "ooui-dialog-action-close": "Sèra",
+    "ooui-outline-control-move-down": "Spôsta in bâs",
+    "ooui-outline-control-move-up": "Spôsta in êlt",
+    "ooui-toolbar-more": "Êter"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/en.json b/resources/oojs/i18n/en.json
new file mode 100644 (file)
index 0000000..d402de8
--- /dev/null
@@ -0,0 +1,23 @@
+{
+    "@metadata": {
+        "authors": [
+            "Trevor Parscal",
+            "Ed Sanders",
+            "James D. Forrester",
+            "Raimond Spekking",
+            "Erik Moeller",
+            "Moriel Schottlender",
+            "Yuki Shira",
+            "Siebrand Mazeland",
+            "Rob Moen",
+            "Timo Tijhof",
+            "Roan Kattouw",
+            "Christian Williams",
+            "Amir E. Aharoni"
+        ]
+    },
+    "ooui-dialog-action-close": "Close",
+    "ooui-outline-control-move-down": "Move item down",
+    "ooui-outline-control-move-up": "Move item up",
+    "ooui-toolbar-more": "More"
+}
diff --git a/resources/oojs/i18n/eo.json b/resources/oojs/i18n/eo.json
new file mode 100644 (file)
index 0000000..51f3261
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "@metadata": {
+        "authors": [
+            "Happy5214",
+            "KuboF",
+            "Shirayuki",
+            "Yekrats"
+        ]
+    },
+    "ooui-dialog-action-close": "Fermi",
+    "ooui-outline-control-move-down": "Movi eron suben",
+    "ooui-outline-control-move-up": "Movi eron supren",
+    "ooui-toolbar-more": "Pli"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/es.json b/resources/oojs/i18n/es.json
new file mode 100644 (file)
index 0000000..0d822bc
--- /dev/null
@@ -0,0 +1,23 @@
+{
+    "@metadata": {
+        "authors": [
+            "Armando-Martin",
+            "Aruizdr",
+            "Benfutbol10",
+            "DJ Nietzsche",
+            "Erdemaslancan",
+            "Fitoschido",
+            "Imre",
+            "Invadinado",
+            "Jdforrester",
+            "Jduranboger",
+            "PoLuX124",
+            "Ralgis",
+            "Thehelpfulone"
+        ]
+    },
+    "ooui-dialog-action-close": "Cerrar",
+    "ooui-outline-control-move-down": "Mover abajo",
+    "ooui-outline-control-move-up": "Mover arriba",
+    "ooui-toolbar-more": "Más"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/et.json b/resources/oojs/i18n/et.json
new file mode 100644 (file)
index 0000000..4af8dbe
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Avjoska",
+            "Pikne"
+        ]
+    },
+    "ooui-dialog-action-close": "Sule",
+    "ooui-outline-control-move-down": "Liiguta üksust allapoole",
+    "ooui-outline-control-move-up": "Liiguta üksust ülespoole",
+    "ooui-toolbar-more": "Veel"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/eu.json b/resources/oojs/i18n/eu.json
new file mode 100644 (file)
index 0000000..5d3f08b
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "@metadata": {
+        "authors": [
+            "An13sa",
+            "Unai Fdz. de Betoño",
+            "Xabier Armendaritz"
+        ]
+    },
+    "ooui-dialog-action-close": "Itxi",
+    "ooui-outline-control-move-down": "Mugitu itema beherantz",
+    "ooui-outline-control-move-up": "Mugitu itema gorantz",
+    "ooui-toolbar-more": "Gehiago"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/fa.json b/resources/oojs/i18n/fa.json
new file mode 100644 (file)
index 0000000..173acd7
--- /dev/null
@@ -0,0 +1,19 @@
+{
+    "@metadata": {
+        "authors": [
+            "Dalba",
+            "Ebraminio",
+            "Jdforrester",
+            "Ladsgroup",
+            "Mjbmr",
+            "Nojan Madinehi",
+            "Reza1615",
+            "Taha",
+            "درفش کاویانی"
+        ]
+    },
+    "ooui-dialog-action-close": "بستن",
+    "ooui-outline-control-move-down": "انتقال مورد به پایین",
+    "ooui-outline-control-move-up": "انتقال مورد به بالا",
+    "ooui-toolbar-more": "بیشتر"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/fi.json b/resources/oojs/i18n/fi.json
new file mode 100644 (file)
index 0000000..dcd367f
--- /dev/null
@@ -0,0 +1,23 @@
+{
+    "@metadata": {
+        "authors": [
+            "Beluga",
+            "Crt",
+            "Harriv",
+            "Linnea",
+            "Nedergard",
+            "Nike",
+            "Olli",
+            "Pxos",
+            "Samoasambia",
+            "Silvonen",
+            "Skalman",
+            "Stryn",
+            "VezonThunder"
+        ]
+    },
+    "ooui-dialog-action-close": "Sulje",
+    "ooui-outline-control-move-down": "Siirrä kohdetta alaspäin",
+    "ooui-outline-control-move-up": "Siirrä kohdetta ylöspäin",
+    "ooui-toolbar-more": "Lisää"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/fo.json b/resources/oojs/i18n/fo.json
new file mode 100644 (file)
index 0000000..00a48ff
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "EileenSanda"
+        ]
+    },
+    "ooui-dialog-action-close": "Lat aftur",
+    "ooui-outline-control-move-down": "Flyt lutin niður",
+    "ooui-outline-control-move-up": "Flyt lutin upp",
+    "ooui-toolbar-more": "Meira"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/fr.json b/resources/oojs/i18n/fr.json
new file mode 100644 (file)
index 0000000..eb24b5a
--- /dev/null
@@ -0,0 +1,35 @@
+{
+    "@metadata": {
+        "authors": [
+            "Automatik",
+            "Benoit Rochon",
+            "Boniface",
+            "Brunoperel",
+            "Crochet.david",
+            "DavidL",
+            "Dereckson",
+            "Gomoko",
+            "Guillom",
+            "Hello71",
+            "Jean-Frédéric",
+            "Linedwell",
+            "Ltrlg",
+            "Metroitendo",
+            "NemesisIII",
+            "Nicolas NALLET",
+            "Npettiaux",
+            "Rastus Vernon",
+            "Seb35",
+            "Sherbrooke",
+            "Tpt",
+            "Trizek",
+            "Urhixidur",
+            "Verdy p",
+            "Wyz"
+        ]
+    },
+    "ooui-dialog-action-close": "Fermer",
+    "ooui-outline-control-move-down": "Faire descendre l’élément",
+    "ooui-outline-control-move-up": "Faire monter l’élément",
+    "ooui-toolbar-more": "Plus"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/frr.json b/resources/oojs/i18n/frr.json
new file mode 100644 (file)
index 0000000..ee95b8a
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "ChrisPtDe",
+            "Murma174"
+        ]
+    },
+    "ooui-dialog-action-close": "Slütj",
+    "ooui-outline-control-move-down": "Element efter onern sküüw",
+    "ooui-outline-control-move-up": "Element efter boowen sküüw",
+    "ooui-toolbar-more": "Muar"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/fur.json b/resources/oojs/i18n/fur.json
new file mode 100644 (file)
index 0000000..18ea383
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Klenje",
+            "Tocaibon"
+        ]
+    },
+    "ooui-dialog-action-close": "Siere",
+    "ooui-outline-control-move-down": "sposte sot",
+    "ooui-outline-control-move-up": "sposte in su",
+    "ooui-toolbar-more": "Altri"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/gl.json b/resources/oojs/i18n/gl.json
new file mode 100644 (file)
index 0000000..5d0928f
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "@metadata": {
+        "authors": [
+            "Alison",
+            "Kscanne",
+            "Toliño"
+        ]
+    },
+    "ooui-dialog-action-close": "Pechar",
+    "ooui-outline-control-move-down": "Mover o elemento abaixo",
+    "ooui-outline-control-move-up": "Mover o elemento arriba",
+    "ooui-toolbar-more": "Máis"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/gu.json b/resources/oojs/i18n/gu.json
new file mode 100644 (file)
index 0000000..65ec22b
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "@metadata": {
+        "authors": [
+            "Ashok modhvadia",
+            "KartikMistry",
+            "The Discoverer"
+        ]
+    },
+    "ooui-dialog-action-close": "બંધ કરો",
+    "ooui-outline-control-move-down": "વસ્તુ નીચે ખસેડો",
+    "ooui-outline-control-move-up": "વસ્તુ ઉપર ખસેડો",
+    "ooui-toolbar-more": "વધુ"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/he.json b/resources/oojs/i18n/he.json
new file mode 100644 (file)
index 0000000..31b693c
--- /dev/null
@@ -0,0 +1,22 @@
+{
+    "@metadata": {
+        "authors": [
+            "Amire80",
+            "ExampleTomer",
+            "Guycn2",
+            "Matanya",
+            "Mooeypoo",
+            "Orsa",
+            "Shimmin Beg",
+            "אור שפירא",
+            "חיים",
+            "ערן",
+            "פוילישער",
+            "קיפודנחש"
+        ]
+    },
+    "ooui-dialog-action-close": "סגירה",
+    "ooui-outline-control-move-down": "להזיז את הפריט מטה",
+    "ooui-outline-control-move-up": "להזיז את הפריט מעלה",
+    "ooui-toolbar-more": "עוד"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/hi.json b/resources/oojs/i18n/hi.json
new file mode 100644 (file)
index 0000000..8b79d34
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "@metadata": {
+        "authors": [
+            "Ansumang",
+            "Devayon",
+            "Rajesh",
+            "Siddhartha Ghai"
+        ]
+    },
+    "ooui-dialog-action-close": "बंद करें",
+    "ooui-outline-control-move-down": "प्रविष्टि नीचे ले जाएँ",
+    "ooui-outline-control-move-up": "प्रविष्टि ऊपर ले जाएँ",
+    "ooui-toolbar-more": "अधिक"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/hr.json b/resources/oojs/i18n/hr.json
new file mode 100644 (file)
index 0000000..1c3f925
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "@metadata": {
+        "authors": [
+            "MaGa",
+            "Roberta F.",
+            "SpeedyGonsales"
+        ]
+    },
+    "ooui-dialog-action-close": "zatvori",
+    "ooui-outline-control-move-down": "Premjesti stavku dolje",
+    "ooui-outline-control-move-up": "Premjesti stavku gore",
+    "ooui-toolbar-more": "Više mogućnosti"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/hsb.json b/resources/oojs/i18n/hsb.json
new file mode 100644 (file)
index 0000000..861c6e5
--- /dev/null
@@ -0,0 +1,9 @@
+{
+    "@metadata": {
+        "authors": [
+            "J budissin",
+            "Michawiki"
+        ]
+    },
+    "ooui-dialog-action-close": "Začinić"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/hu.json b/resources/oojs/i18n/hu.json
new file mode 100644 (file)
index 0000000..9f7b435
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "@metadata": {
+        "authors": [
+            "Dj",
+            "Einstein2",
+            "Misibacsi",
+            "ViDam"
+        ]
+    },
+    "ooui-dialog-action-close": "Bezár",
+    "ooui-outline-control-move-down": "Elem mozgatása lefelé",
+    "ooui-outline-control-move-up": "Elem mozgatása felfelé",
+    "ooui-toolbar-more": "Tovább..."
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/hy.json b/resources/oojs/i18n/hy.json
new file mode 100644 (file)
index 0000000..f6cb90b
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Vacio",
+            "Xelgen"
+        ]
+    },
+    "ooui-dialog-action-close": "Փակել",
+    "ooui-outline-control-move-down": "Իջեցնել կետը",
+    "ooui-outline-control-move-up": "Բարձրացնել կետը",
+    "ooui-toolbar-more": "Ավելին"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ia.json b/resources/oojs/i18n/ia.json
new file mode 100644 (file)
index 0000000..e335553
--- /dev/null
@@ -0,0 +1,8 @@
+{
+    "@metadata": {
+        "authors": [
+            "McDutchie"
+        ]
+    },
+    "ooui-dialog-action-close": "Clauder"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/id.json b/resources/oojs/i18n/id.json
new file mode 100644 (file)
index 0000000..6d3ba4d
--- /dev/null
@@ -0,0 +1,18 @@
+{
+    "@metadata": {
+        "authors": [
+            "Farras",
+            "Ilham151096",
+            "Iwan Novirion",
+            "Iyan",
+            "Kenrick95",
+            "McDutchie",
+            "Rv77ax",
+            "William Surya Permana"
+        ]
+    },
+    "ooui-dialog-action-close": "Tutup",
+    "ooui-outline-control-move-down": "Pindahkan butir ke bawah",
+    "ooui-outline-control-move-up": "Pindahkan butir ke atas",
+    "ooui-toolbar-more": "Lainnya"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ie.json b/resources/oojs/i18n/ie.json
new file mode 100644 (file)
index 0000000..84d002d
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Makuba"
+        ]
+    },
+    "ooui-dialog-action-close": "Terminar",
+    "ooui-outline-control-move-down": "Mover element a infra",
+    "ooui-outline-control-move-up": "Mover element a supra",
+    "ooui-toolbar-more": "Plu"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ilo.json b/resources/oojs/i18n/ilo.json
new file mode 100644 (file)
index 0000000..15f42e5
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Lam-ang"
+        ]
+    },
+    "ooui-dialog-action-close": "Irekep",
+    "ooui-outline-control-move-down": "Ipababa ti banag",
+    "ooui-outline-control-move-up": "Ipangato ti banag",
+    "ooui-toolbar-more": "Adu pay"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/is.json b/resources/oojs/i18n/is.json
new file mode 100644 (file)
index 0000000..efe0e67
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Maxí",
+            "Snævar"
+        ]
+    },
+    "ooui-dialog-action-close": "Loka",
+    "ooui-outline-control-move-down": "Færa atriða niður",
+    "ooui-outline-control-move-up": "Færa atriða upp",
+    "ooui-toolbar-more": "Fleira"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/it.json b/resources/oojs/i18n/it.json
new file mode 100644 (file)
index 0000000..6158cff
--- /dev/null
@@ -0,0 +1,21 @@
+{
+    "@metadata": {
+        "authors": [
+            "Beta16",
+            "Darth Kule",
+            "Doc.mari",
+            "Eleonora negri",
+            "Elitre",
+            "F. Cosoleto",
+            "FRacco",
+            "Gianfranco",
+            "Minerva Titani",
+            "Raoli",
+            "Una giornata uggiosa '94"
+        ]
+    },
+    "ooui-dialog-action-close": "Chiudi",
+    "ooui-outline-control-move-down": "Sposta in basso",
+    "ooui-outline-control-move-up": "Sposta in alto",
+    "ooui-toolbar-more": "Altro"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ja.json b/resources/oojs/i18n/ja.json
new file mode 100644 (file)
index 0000000..789fbeb
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "@metadata": {
+        "authors": [
+            "Fryed-peach",
+            "Miya",
+            "Penn Station",
+            "Shirayuki"
+        ]
+    },
+    "ooui-dialog-action-close": "閉じる",
+    "ooui-outline-control-move-down": "項目を下に移動させる",
+    "ooui-outline-control-move-up": "項目を上に移動させる",
+    "ooui-toolbar-more": "その他"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/jv.json b/resources/oojs/i18n/jv.json
new file mode 100644 (file)
index 0000000..a362079
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Gleki",
+            "NoiX180",
+            "Pras"
+        ]
+    },
+    "ooui-dialog-action-close": "Tutup",
+    "ooui-outline-control-move-down": "Pindhahaken butir mangandhap"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ka.json b/resources/oojs/i18n/ka.json
new file mode 100644 (file)
index 0000000..78180af
--- /dev/null
@@ -0,0 +1,16 @@
+{
+    "@metadata": {
+        "authors": [
+            "BRUTE",
+            "David1010",
+            "Gleki",
+            "ITshnik",
+            "MIKHEIL",
+            "NoiX180",
+            "Pras"
+        ]
+    },
+    "ooui-dialog-action-close": "დახურვა",
+    "ooui-outline-control-move-down": "ელემენტის ქვემოთ გადატანა",
+    "ooui-outline-control-move-up": "ელემენტის ზემოთ გადატანა"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/kk-cyrl.json b/resources/oojs/i18n/kk-cyrl.json
new file mode 100644 (file)
index 0000000..4c27b07
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Arystanbek"
+        ]
+    },
+    "ooui-dialog-action-close": "Жабу",
+    "ooui-outline-control-move-down": "Элементті төмен жылжыту",
+    "ooui-outline-control-move-up": "Элементті жоғары жылжыту",
+    "ooui-toolbar-more": "толығырақ"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ko.json b/resources/oojs/i18n/ko.json
new file mode 100644 (file)
index 0000000..f1f61df
--- /dev/null
@@ -0,0 +1,15 @@
+{
+    "@metadata": {
+        "authors": [
+            "Freebiekr",
+            "Hym411",
+            "Kwj2772",
+            "LFM",
+            "아라"
+        ]
+    },
+    "ooui-dialog-action-close": "닫기",
+    "ooui-outline-control-move-down": "항목을 아래로 옮기기",
+    "ooui-outline-control-move-up": "항목을 위로 옮기기",
+    "ooui-toolbar-more": "더 보기"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/krc.json b/resources/oojs/i18n/krc.json
new file mode 100644 (file)
index 0000000..f629139
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "@metadata": {
+        "authors": [
+            "Iltever"
+        ]
+    },
+    "ooui-dialog-action-close": "Джаб",
+    "ooui-outline-control-move-down": "Элементни тюбюне кёчюр",
+    "ooui-outline-control-move-up": "Элементни башына кёчюр"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/kw.json b/resources/oojs/i18n/kw.json
new file mode 100644 (file)
index 0000000..95a9b91
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "@metadata": {
+        "authors": [
+            "George Animal",
+            "Nrowe",
+            "Purodha"
+        ]
+    },
+    "ooui-dialog-action-close": "Degea"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ky.json b/resources/oojs/i18n/ky.json
new file mode 100644 (file)
index 0000000..2d62bda
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Chorobek",
+            "George Animal",
+            "Nrowe",
+            "Tynchtyk Chorotegin",
+            "Викиней"
+        ]
+    },
+    "ooui-dialog-action-close": "Жабуу"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/lb.json b/resources/oojs/i18n/lb.json
new file mode 100644 (file)
index 0000000..a18894e
--- /dev/null
@@ -0,0 +1,17 @@
+{
+    "@metadata": {
+        "authors": [
+            "Autokrator",
+            "Chorobek",
+            "Robby",
+            "Soued031",
+            "Tynchtyk Chorotegin",
+            "UV",
+            "Викиней"
+        ]
+    },
+    "ooui-dialog-action-close": "Zoumaachen",
+    "ooui-outline-control-move-down": "Element erof réckelen",
+    "ooui-outline-control-move-up": "Element erop réckelen",
+    "ooui-toolbar-more": "Méi"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/lmo.json b/resources/oojs/i18n/lmo.json
new file mode 100644 (file)
index 0000000..e506b7a
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Ninonino"
+        ]
+    },
+    "ooui-dialog-action-close": "Sèra",
+    "ooui-outline-control-move-down": "Spòsta 'n zó",
+    "ooui-outline-control-move-up": "Spòsta 'n sö",
+    "ooui-toolbar-more": "Amò"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/lt.json b/resources/oojs/i18n/lt.json
new file mode 100644 (file)
index 0000000..b3a16e8
--- /dev/null
@@ -0,0 +1,9 @@
+{
+    "@metadata": {
+        "authors": [
+            "Audriusa",
+            "Eitvys200"
+        ]
+    },
+    "ooui-dialog-action-close": "Uždaryti"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/lv.json b/resources/oojs/i18n/lv.json
new file mode 100644 (file)
index 0000000..c633339
--- /dev/null
@@ -0,0 +1,15 @@
+{
+    "@metadata": {
+        "authors": [
+            "Admresdeserv.",
+            "Audriusa",
+            "Eitvys200",
+            "Papuass",
+            "PeterisP"
+        ]
+    },
+    "ooui-dialog-action-close": "Aizvērt",
+    "ooui-outline-control-move-down": "Pārvietot vienumu uz leju",
+    "ooui-outline-control-move-up": "Pārvietot vienumu uz augšu",
+    "ooui-toolbar-more": "Vairāk"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/mg.json b/resources/oojs/i18n/mg.json
new file mode 100644 (file)
index 0000000..dcb5fd5
--- /dev/null
@@ -0,0 +1,8 @@
+{
+    "@metadata": {
+        "authors": [
+            "Jagwar"
+        ]
+    },
+    "ooui-dialog-action-close": "Hidiana"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/min.json b/resources/oojs/i18n/min.json
new file mode 100644 (file)
index 0000000..55174c0
--- /dev/null
@@ -0,0 +1,9 @@
+{
+    "@metadata": {
+        "authors": [
+            "Iwan Novirion",
+            "Jagwar"
+        ]
+    },
+    "ooui-dialog-action-close": "Tutuik"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/mk.json b/resources/oojs/i18n/mk.json
new file mode 100644 (file)
index 0000000..b363a45
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "@metadata": {
+        "authors": [
+            "Bjankuloski06",
+            "Brest",
+            "Iwan Novirion"
+        ]
+    },
+    "ooui-dialog-action-close": "Затвори",
+    "ooui-outline-control-move-down": "Помести надолу",
+    "ooui-outline-control-move-up": "Помести нагоре",
+    "ooui-toolbar-more": "Повеќе"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ml.json b/resources/oojs/i18n/ml.json
new file mode 100644 (file)
index 0000000..355e337
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "@metadata": {
+        "authors": [
+            "Kavya Manohar",
+            "Praveenp",
+            "Santhosh.thottingal",
+            "Vssun"
+        ]
+    },
+    "ooui-dialog-action-close": "അടയ്ക്കുക",
+    "ooui-outline-control-move-down": "ഇനം താഴേയ്ക്ക് മാറ്റുക",
+    "ooui-outline-control-move-up": "ഇനം മുകളിലേയ്ക്ക് മാറ്റുക",
+    "ooui-toolbar-more": "കൂടുതൽ"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/mr.json b/resources/oojs/i18n/mr.json
new file mode 100644 (file)
index 0000000..d4db84f
--- /dev/null
@@ -0,0 +1,16 @@
+{
+    "@metadata": {
+        "authors": [
+            "Kaajawa",
+            "Mahitgar",
+            "Praju23",
+            "V.narsikar",
+            "Ydyashad",
+            "संतोष दहिवळ"
+        ]
+    },
+    "ooui-dialog-action-close": "बंद करा",
+    "ooui-outline-control-move-down": "घटक (आयटम) खाली सरकवा",
+    "ooui-outline-control-move-up": "घटक (आयटम) वर सरकवा",
+    "ooui-toolbar-more": "अधिक"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ms.json b/resources/oojs/i18n/ms.json
new file mode 100644 (file)
index 0000000..21aef50
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Anakmalaysia",
+            "Aurora"
+        ]
+    },
+    "ooui-dialog-action-close": "Tutup",
+    "ooui-outline-control-move-down": "Alihkan perkara ke bawah",
+    "ooui-outline-control-move-up": "Alihkan perkara ke atas",
+    "ooui-toolbar-more": "Lagi"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/nap.json b/resources/oojs/i18n/nap.json
new file mode 100644 (file)
index 0000000..6b0b3ec
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Chelin",
+            "Chrisportelli",
+            "PiRSquared17"
+        ]
+    },
+    "ooui-dialog-action-close": "Chiure",
+    "ooui-toolbar-more": "Atro"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/nb.json b/resources/oojs/i18n/nb.json
new file mode 100644 (file)
index 0000000..7cdecaa
--- /dev/null
@@ -0,0 +1,15 @@
+{
+    "@metadata": {
+        "authors": [
+            "Danmichaelo",
+            "Event",
+            "Jeblad",
+            "Laaknor",
+            "Njardarlogar"
+        ]
+    },
+    "ooui-dialog-action-close": "Lukk",
+    "ooui-outline-control-move-down": "Flytt ned",
+    "ooui-outline-control-move-up": "Flytt opp",
+    "ooui-toolbar-more": "Mer"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/nds-nl.json b/resources/oojs/i18n/nds-nl.json
new file mode 100644 (file)
index 0000000..81f8a43
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "@metadata": {
+        "authors": [
+            "Servien"
+        ]
+    },
+    "ooui-dialog-action-close": "Sluten",
+    "ooui-outline-control-move-down": "Onderwarp ummeneer zetten",
+    "ooui-outline-control-move-up": "Onderwarp umhoge zetten"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/nds.json b/resources/oojs/i18n/nds.json
new file mode 100644 (file)
index 0000000..d0806d0
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Zylbath"
+        ]
+    },
+    "ooui-dialog-action-close": "Dichtmaken",
+    "ooui-outline-control-move-down": "Element na ünnen schuven",
+    "ooui-outline-control-move-up": "Element na baven schuven",
+    "ooui-toolbar-more": "Mehr"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ne.json b/resources/oojs/i18n/ne.json
new file mode 100644 (file)
index 0000000..ae948c6
--- /dev/null
@@ -0,0 +1,8 @@
+{
+    "@metadata": {
+        "authors": [
+            "RajeshPandey",
+            "सरोज कुमार ढकाल"
+        ]
+    }
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/nl.json b/resources/oojs/i18n/nl.json
new file mode 100644 (file)
index 0000000..75db0a7
--- /dev/null
@@ -0,0 +1,25 @@
+{
+    "@metadata": {
+        "authors": [
+            "Bluyten",
+            "Breghtje",
+            "Catrope",
+            "Flightmare",
+            "Hansmuller",
+            "Jdforrester",
+            "Keegan",
+            "Konovalov",
+            "RajeshPandey",
+            "Romaine",
+            "SPQRobin",
+            "Saruman",
+            "Siebrand",
+            "Southparkfan",
+            "सरोज कुमार ढकाल"
+        ]
+    },
+    "ooui-dialog-action-close": "Sluiten",
+    "ooui-outline-control-move-down": "Item omlaag verplaatsen",
+    "ooui-outline-control-move-up": "Item omhoog verplaatsen",
+    "ooui-toolbar-more": "Meer"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/nn.json b/resources/oojs/i18n/nn.json
new file mode 100644 (file)
index 0000000..dd86f5e
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Jeblad",
+            "Njardarlogar"
+        ]
+    },
+    "ooui-dialog-action-close": "Lat att",
+    "ooui-outline-control-move-down": "Flytt element ned",
+    "ooui-outline-control-move-up": "Flytt element opp",
+    "ooui-toolbar-more": "Fleire"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/om.json b/resources/oojs/i18n/om.json
new file mode 100644 (file)
index 0000000..dca7b7d
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Cedric31",
+            "Tumsaa"
+        ]
+    },
+    "ooui-dialog-action-close": "Cufi",
+    "ooui-outline-control-move-down": "Gad buusi",
+    "ooui-outline-control-move-up": "Ol baasi",
+    "ooui-toolbar-more": "Dabalata"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/or.json b/resources/oojs/i18n/or.json
new file mode 100644 (file)
index 0000000..35721a1
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "@metadata": {
+        "authors": [
+            "Odisha1",
+            "Psubhashish",
+            "ଶିତିକଣ୍ଠ ଦାଶ"
+        ]
+    },
+    "ooui-dialog-action-close": "ବନ୍ଦ କରିବେ"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/pa.json b/resources/oojs/i18n/pa.json
new file mode 100644 (file)
index 0000000..6c76d7f
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Amikeco",
+            "Babanwalia",
+            "Bouron",
+            "Nasir8891"
+        ]
+    },
+    "ooui-dialog-action-close": "বন্ধ"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/pl.json b/resources/oojs/i18n/pl.json
new file mode 100644 (file)
index 0000000..ba33322
--- /dev/null
@@ -0,0 +1,22 @@
+{
+    "@metadata": {
+        "authors": [
+            "Babanwalia",
+            "Chrumps",
+            "Matma Rex",
+            "Mikołka",
+            "Nasir8891",
+            "Odie2",
+            "Rzuwig",
+            "Tar Lócesilion",
+            "Ty221",
+            "WTM",
+            "Woytecr",
+            "Wpedzich"
+        ]
+    },
+    "ooui-dialog-action-close": "Zamknij",
+    "ooui-outline-control-move-down": "Przenieś niżej",
+    "ooui-outline-control-move-up": "Przenieś wyżej",
+    "ooui-toolbar-more": "Więcej"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/pms.json b/resources/oojs/i18n/pms.json
new file mode 100644 (file)
index 0000000..bb8f113
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "@metadata": {
+        "authors": [
+            "Borichèt",
+            "Dragonòt",
+            "පසිඳු කාවින්ද"
+        ]
+    },
+    "ooui-dialog-action-close": "Saré",
+    "ooui-outline-control-move-down": "Fé calé giù l'element",
+    "ooui-outline-control-move-up": "Fé monté l'element",
+    "ooui-toolbar-more": "Ëd pi"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ps.json b/resources/oojs/i18n/ps.json
new file mode 100644 (file)
index 0000000..4f21707
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Ahmed-Najib-Biabani-Ibrahimkhel"
+        ]
+    },
+    "ooui-dialog-action-close": "تړل",
+    "ooui-outline-control-move-down": "توکی ښکته راوړل",
+    "ooui-outline-control-move-up": "توکی پورته راوړل",
+    "ooui-toolbar-more": "نور"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/pt-br.json b/resources/oojs/i18n/pt-br.json
new file mode 100644 (file)
index 0000000..f758660
--- /dev/null
@@ -0,0 +1,19 @@
+{
+    "@metadata": {
+        "authors": [
+            "Cainamarques",
+            "Dianakc",
+            "Fúlvio",
+            "Helder.wiki",
+            "HenriqueCrang",
+            "Jaideraf",
+            "Luckas",
+            "OTAVIO1981",
+            555
+        ]
+    },
+    "ooui-dialog-action-close": "Fechar",
+    "ooui-outline-control-move-down": "Mover item para baixo",
+    "ooui-outline-control-move-up": "Mover item para cima",
+    "ooui-toolbar-more": "Mais"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/pt.json b/resources/oojs/i18n/pt.json
new file mode 100644 (file)
index 0000000..a4dba27
--- /dev/null
@@ -0,0 +1,19 @@
+{
+    "@metadata": {
+        "authors": [
+            "Cainamarques",
+            "Fúlvio",
+            "GoEThe",
+            "Hamilton Abreu",
+            "Helder.wiki",
+            "Jaideraf",
+            "Jdforrester",
+            "Luckas",
+            "Vitorvicentevalente"
+        ]
+    },
+    "ooui-dialog-action-close": "Fechar",
+    "ooui-outline-control-move-down": "Mover item para baixo",
+    "ooui-outline-control-move-up": "Mover item para cima",
+    "ooui-toolbar-more": "Mais"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/qqq.json b/resources/oojs/i18n/qqq.json
new file mode 100644 (file)
index 0000000..78a70d9
--- /dev/null
@@ -0,0 +1,26 @@
+{
+    "@metadata": {
+        "authors": [
+            "Amire80",
+            "Beta16",
+            "Erik Moeller",
+            "Jdforrester",
+            "Lloffiwr",
+            "Mooeypoo",
+            "Mormegil",
+            "Nike",
+            "PoLuX124",
+            "Purodha",
+            "Raymond",
+            "Sagan",
+            "Sayak Sarkar",
+            "Shirayuki",
+            "Siebrand",
+            "Trevor Parscal"
+        ]
+    },
+    "ooui-dialog-action-close": "Label text for button to exit from dialog.\n\n{{Identical|Close}}",
+    "ooui-outline-control-move-down": "Tool tip for a button that moves items in a list down one place",
+    "ooui-outline-control-move-up": "Tool tip for a button that moves items in a list up one place",
+    "ooui-toolbar-more": "Label for the toolbar group that contains a list of all other available tools.\n{{Identical|More}}"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/qu.json b/resources/oojs/i18n/qu.json
new file mode 100644 (file)
index 0000000..9a412f5
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "AlimanRuna"
+        ]
+    },
+    "ooui-dialog-action-close": "Wichq'ay",
+    "ooui-outline-control-move-down": "Qallawata uraykuchiy",
+    "ooui-outline-control-move-up": "Qallawata huqariy",
+    "ooui-toolbar-more": "Aswan"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ro.json b/resources/oojs/i18n/ro.json
new file mode 100644 (file)
index 0000000..861b2fe
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "@metadata": {
+        "authors": [
+            "AlimanRuna",
+            "Firilacroco",
+            "Minisarm",
+            "Stelistcristi"
+        ]
+    },
+    "ooui-dialog-action-close": "Închide",
+    "ooui-outline-control-move-down": "Mută elementul mai jos",
+    "ooui-outline-control-move-up": "Mută elementul mai sus",
+    "ooui-toolbar-more": "Mai mult"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/roa-tara.json b/resources/oojs/i18n/roa-tara.json
new file mode 100644 (file)
index 0000000..c7699d6
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Joetaras"
+        ]
+    },
+    "ooui-dialog-action-close": "Achiude",
+    "ooui-outline-control-move-down": "Spuèste 'a vôsce sotte",
+    "ooui-outline-control-move-up": "Spuèste 'a vôsce sus",
+    "ooui-toolbar-more": "De cchiù"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ru.json b/resources/oojs/i18n/ru.json
new file mode 100644 (file)
index 0000000..be7c6a5
--- /dev/null
@@ -0,0 +1,25 @@
+{
+    "@metadata": {
+        "authors": [
+            "Amire80",
+            "DR",
+            "Eugrus",
+            "Iluvatar",
+            "KPu3uC B Poccuu",
+            "Kalan",
+            "MaxBioHazard",
+            "NBS",
+            "Niklem",
+            "Okras",
+            "Ole Yves",
+            "Putnik",
+            "Sunpriat",
+            "Yury Katkov",
+            "Умар"
+        ]
+    },
+    "ooui-dialog-action-close": "Закрыть",
+    "ooui-outline-control-move-down": "Переместить элемент вниз",
+    "ooui-outline-control-move-up": "Переместить элемент вверх",
+    "ooui-toolbar-more": "Ещё"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/sah.json b/resources/oojs/i18n/sah.json
new file mode 100644 (file)
index 0000000..9b3fcc8
--- /dev/null
@@ -0,0 +1,9 @@
+{
+    "@metadata": {
+        "authors": [
+            "Gazeb",
+            "HalanTul"
+        ]
+    },
+    "ooui-dialog-action-close": "Сап"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/scn.json b/resources/oojs/i18n/scn.json
new file mode 100644 (file)
index 0000000..a699911
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "@metadata": {
+        "authors": [
+            "Gazeb",
+            "Gmelfi",
+            "HalanTul"
+        ]
+    },
+    "ooui-dialog-action-close": "Chiùi",
+    "ooui-outline-control-move-down": "Sposta di sutta",
+    "ooui-outline-control-move-up": "Sposta di supra",
+    "ooui-toolbar-more": "Àutri cosi"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/sh.json b/resources/oojs/i18n/sh.json
new file mode 100644 (file)
index 0000000..5e29980
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "@metadata": {
+        "authors": [
+            "OC Ripper"
+        ]
+    },
+    "ooui-dialog-action-close": "Zatvori",
+    "ooui-outline-control-move-down": "Pomakni stavku dolje",
+    "ooui-outline-control-move-up": "Pomakni stavku gore"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/si.json b/resources/oojs/i18n/si.json
new file mode 100644 (file)
index 0000000..cf7a9fd
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Singhalawap",
+            "පසිඳු කාවින්ද",
+            "ශ්වෙත"
+        ]
+    },
+    "ooui-dialog-action-close": "නිමවන්න",
+    "ooui-outline-control-move-down": "අයිතමය පහලටදමන්න",
+    "ooui-outline-control-move-up": "අයිතමය ඉහලටදමන්න"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/sk.json b/resources/oojs/i18n/sk.json
new file mode 100644 (file)
index 0000000..60b6f43
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Mimarik",
+            "Teslaton"
+        ]
+    },
+    "ooui-dialog-action-close": "Zatvoriť",
+    "ooui-outline-control-move-down": "Posunúť položku nadol",
+    "ooui-outline-control-move-up": "Posunúť položku nahor",
+    "ooui-toolbar-more": "Viac"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/sl.json b/resources/oojs/i18n/sl.json
new file mode 100644 (file)
index 0000000..d5bffd9
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "@metadata": {
+        "authors": [
+            "Dbc334",
+            "Eleassar",
+            "Pinky sl",
+            "Yerpo"
+        ]
+    },
+    "ooui-dialog-action-close": "Zapri",
+    "ooui-outline-control-move-down": "Prestavi predmet nižje",
+    "ooui-outline-control-move-up": "Prestavi predmet višje",
+    "ooui-toolbar-more": "Več"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/sq.json b/resources/oojs/i18n/sq.json
new file mode 100644 (file)
index 0000000..424f1be
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Euriditi"
+        ]
+    },
+    "ooui-dialog-action-close": "Mbylle",
+    "ooui-outline-control-move-down": "Zhvendose artikullin më poshtë",
+    "ooui-outline-control-move-up": "Zhvendose artikullin më lart",
+    "ooui-toolbar-more": "Më tepër..."
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/sr-ec.json b/resources/oojs/i18n/sr-ec.json
new file mode 100644 (file)
index 0000000..973baec
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "@metadata": {
+        "authors": [
+            "Milicevic01",
+            "Nikola Smolenski",
+            "Милан Јелисавчић"
+        ]
+    },
+    "ooui-dialog-action-close": "Затвори",
+    "ooui-outline-control-move-down": "Премести ставку на доле",
+    "ooui-outline-control-move-up": "Премести ставку на горе",
+    "ooui-toolbar-more": "Више"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/sv.json b/resources/oojs/i18n/sv.json
new file mode 100644 (file)
index 0000000..74d654b
--- /dev/null
@@ -0,0 +1,20 @@
+{
+    "@metadata": {
+        "authors": [
+            "Ainali",
+            "Haxpett",
+            "Jopparn",
+            "Knuckles",
+            "Magol",
+            "Milicevic01",
+            "Per",
+            "Sendelbach",
+            "Skalman",
+            "WikiPhoenix"
+        ]
+    },
+    "ooui-dialog-action-close": "Stäng",
+    "ooui-outline-control-move-down": "Flytta ned objekt",
+    "ooui-outline-control-move-up": "Flytta upp objekt",
+    "ooui-toolbar-more": "Mer"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/sw.json b/resources/oojs/i18n/sw.json
new file mode 100644 (file)
index 0000000..1c61b06
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Lloffiwr",
+            "Muddyb Blast Producer"
+        ]
+    },
+    "ooui-dialog-action-close": "Funga",
+    "ooui-outline-control-move-down": "Sogeza kipengee chini",
+    "ooui-outline-control-move-up": "Sogeza kipengee juu",
+    "ooui-toolbar-more": "Zaidi"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ta.json b/resources/oojs/i18n/ta.json
new file mode 100644 (file)
index 0000000..a9795fd
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Jayarathina",
+            "Sank",
+            "Shanmugamp7",
+            "மதனாஹரன்"
+        ]
+    },
+    "ooui-dialog-action-close": "மூடுக"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/te.json b/resources/oojs/i18n/te.json
new file mode 100644 (file)
index 0000000..a1f1285
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "@metadata": {
+        "authors": [
+            "Arjunaraoc",
+            "Jayarathina",
+            "Sank",
+            "Shanmugamp7",
+            "Veeven",
+            "Visdaviva",
+            "மதனாஹரன்"
+        ]
+    },
+    "ooui-dialog-action-close": "మూయి"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/th.json b/resources/oojs/i18n/th.json
new file mode 100644 (file)
index 0000000..b7ee05a
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Supasate",
+            "Taweetham"
+        ]
+    },
+    "ooui-dialog-action-close": "ปิด",
+    "ooui-outline-control-move-down": "เลื่อนรายการลง",
+    "ooui-outline-control-move-up": "ย้ายรายการขึ้น"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/tl.json b/resources/oojs/i18n/tl.json
new file mode 100644 (file)
index 0000000..a073882
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "AnakngAraw",
+            "Sky Harbor"
+        ]
+    },
+    "ooui-dialog-action-close": "Isara",
+    "ooui-outline-control-move-down": "Ilipat ang aytem pababa",
+    "ooui-outline-control-move-up": "Ilipat ang aytem pataas",
+    "ooui-toolbar-more": "Marami pa"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/tr.json b/resources/oojs/i18n/tr.json
new file mode 100644 (file)
index 0000000..94d34a2
--- /dev/null
@@ -0,0 +1,17 @@
+{
+    "@metadata": {
+        "authors": [
+            "Emperyan",
+            "Incelemeelemani",
+            "LuCKY",
+            "Maidis",
+            "Rapsar",
+            "Talha Samil Cakir",
+            "TurkishStyles"
+        ]
+    },
+    "ooui-dialog-action-close": "Kapat",
+    "ooui-outline-control-move-down": "Ögeyi aşağı taşı",
+    "ooui-outline-control-move-up": "Ögeyi yukarı taşı",
+    "ooui-toolbar-more": "Daha fazla"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/tt-cyrl.json b/resources/oojs/i18n/tt-cyrl.json
new file mode 100644 (file)
index 0000000..1c0bd90
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "@metadata": {
+        "authors": [
+            "Ajdar"
+        ]
+    },
+    "ooui-dialog-action-close": "Ябу",
+    "ooui-outline-control-move-down": "Элементны аска күчерү",
+    "ooui-outline-control-move-up": "Элементны өскә күчерү"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/ug-arab.json b/resources/oojs/i18n/ug-arab.json
new file mode 100644 (file)
index 0000000..efba086
--- /dev/null
@@ -0,0 +1,9 @@
+{
+    "@metadata": {
+        "authors": [
+            "Sahran",
+            "Tel'et",
+            "Tifinaghes"
+        ]
+    }
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/uk.json b/resources/oojs/i18n/uk.json
new file mode 100644 (file)
index 0000000..9a47ad7
--- /dev/null
@@ -0,0 +1,24 @@
+{
+    "@metadata": {
+        "authors": [
+            "AS",
+            "Aced",
+            "Ahonc",
+            "Andriykopanytsia",
+            "Base",
+            "Perohanych",
+            "RLuts",
+            "Sahran",
+            "Sergento",
+            "Steve.rusyn",
+            "SteveR",
+            "Tel'et",
+            "Tifinaghes",
+            "Ата"
+        ]
+    },
+    "ooui-dialog-action-close": "Закрити",
+    "ooui-outline-control-move-down": "Перемістити елемент униз",
+    "ooui-outline-control-move-up": "Перемістити елемент вгору",
+    "ooui-toolbar-more": "Більше"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/uz.json b/resources/oojs/i18n/uz.json
new file mode 100644 (file)
index 0000000..473fc75
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "@metadata": {
+        "authors": [
+            "CoderSI",
+            "Noor2020",
+            "Sociologist",
+            "පසිඳු කාවින්ද"
+        ]
+    },
+    "ooui-dialog-action-close": "Yopish",
+    "ooui-outline-control-move-down": "Elementni pastga koʻchirish",
+    "ooui-outline-control-move-up": "Elementni yuqoriga koʻchirish",
+    "ooui-toolbar-more": "Yana"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/vec.json b/resources/oojs/i18n/vec.json
new file mode 100644 (file)
index 0000000..01833f7
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "@metadata": {
+        "authors": [
+            "Candalua",
+            "GatoSelvadego"
+        ]
+    },
+    "ooui-dialog-action-close": "Sara",
+    "ooui-outline-control-move-down": "Sposta in baso",
+    "ooui-outline-control-move-up": "Sposta in sima",
+    "ooui-toolbar-more": "Altro"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/vi.json b/resources/oojs/i18n/vi.json
new file mode 100644 (file)
index 0000000..b545ce6
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "@metadata": {
+        "authors": [
+            "Cheers!",
+            "Jdforrester",
+            "Minh Nguyen"
+        ]
+    },
+    "ooui-dialog-action-close": "Đóng",
+    "ooui-outline-control-move-down": "Chuyển mục xuống",
+    "ooui-outline-control-move-up": "Chuyển mục lên",
+    "ooui-toolbar-more": "Thêm"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/vo.json b/resources/oojs/i18n/vo.json
new file mode 100644 (file)
index 0000000..2ed7e2f
--- /dev/null
@@ -0,0 +1,9 @@
+{
+    "@metadata": {
+        "authors": [
+            "Malafaya"
+        ]
+    },
+    "ooui-dialog-action-close": "Färmükön",
+    "ooui-toolbar-more": "Pluikos"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/wuu.json b/resources/oojs/i18n/wuu.json
new file mode 100644 (file)
index 0000000..72aa48b
--- /dev/null
@@ -0,0 +1,9 @@
+{
+    "@metadata": {
+        "authors": [
+            "Malafaya",
+            "十弌"
+        ]
+    },
+    "ooui-toolbar-more": "還多"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/yi.json b/resources/oojs/i18n/yi.json
new file mode 100644 (file)
index 0000000..ab5c510
--- /dev/null
@@ -0,0 +1,13 @@
+{
+    "@metadata": {
+        "authors": [
+            "Malafaya",
+            "פוילישער",
+            "十弌"
+        ]
+    },
+    "ooui-dialog-action-close": "שליסן",
+    "ooui-outline-control-move-down": "רוקן עלעמענט אראפ",
+    "ooui-outline-control-move-up": "רוקן עלעמענט ארויף",
+    "ooui-toolbar-more": "נאך"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/yo.json b/resources/oojs/i18n/yo.json
new file mode 100644 (file)
index 0000000..f71d3dd
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "@metadata": {
+        "authors": [
+            "Demmy"
+        ]
+    },
+    "ooui-dialog-action-close": "Ìpadé",
+    "ooui-outline-control-move-down": "Sún onítòún sí sàlẹ̀",
+    "ooui-outline-control-move-up": "Sún onítòún s'ókè",
+    "ooui-toolbar-more": "Míràn"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/zh-hans.json b/resources/oojs/i18n/zh-hans.json
new file mode 100644 (file)
index 0000000..46cbae3
--- /dev/null
@@ -0,0 +1,25 @@
+{
+    "@metadata": {
+        "authors": [
+            "Anakmalaysia",
+            "Bencmq",
+            "Demmy",
+            "Hydra",
+            "Hzy980512",
+            "Liangent",
+            "Liuxinyu970226",
+            "Qiyue2001",
+            "Shirayuki",
+            "Shizhao",
+            "TianyinLee",
+            "Xiaomingyan",
+            "Yfdyh000",
+            "Zhangjintao",
+            "乌拉跨氪"
+        ]
+    },
+    "ooui-dialog-action-close": "关闭",
+    "ooui-outline-control-move-down": "下移项",
+    "ooui-outline-control-move-up": "上移项",
+    "ooui-toolbar-more": "更多"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/zh-hant.json b/resources/oojs/i18n/zh-hant.json
new file mode 100644 (file)
index 0000000..9aace2f
--- /dev/null
@@ -0,0 +1,22 @@
+{
+    "@metadata": {
+        "authors": [
+            "Anakmalaysia",
+            "Ch.Andrew",
+            "Hydra",
+            "Justincheng12345",
+            "Liflon",
+            "Liuxinyu970226",
+            "Qiyue2001",
+            "Radish10cm",
+            "Shirayuki",
+            "Simon Shek",
+            "Spring Roll Conan",
+            "Waihorace"
+        ]
+    },
+    "ooui-dialog-action-close": "關閉",
+    "ooui-outline-control-move-down": "向下移項",
+    "ooui-outline-control-move-up": "向上移項",
+    "ooui-toolbar-more": "更多"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/zh-hk.json b/resources/oojs/i18n/zh-hk.json
new file mode 100644 (file)
index 0000000..60e8fbd
--- /dev/null
@@ -0,0 +1,4 @@
+{
+    "@metadata": [],
+    "ooui-dialog-action-close": "關閉"
+}
\ No newline at end of file
diff --git a/resources/oojs/i18n/zh-tw.json b/resources/oojs/i18n/zh-tw.json
new file mode 100644 (file)
index 0000000..f7987e5
--- /dev/null
@@ -0,0 +1,7 @@
+{
+    "@metadata": [],
+    "ooui-dialog-action-close": "關閉",
+    "ooui-outline-control-move-down": "向下移",
+    "ooui-outline-control-move-up": "向上移",
+    "ooui-toolbar-more": "更多"
+}
\ No newline at end of file
diff --git a/resources/oojs/images/fade-down.png b/resources/oojs/images/fade-down.png
new file mode 100644 (file)
index 0000000..50c7931
Binary files /dev/null and b/resources/oojs/images/fade-down.png differ
diff --git a/resources/oojs/images/fade-up.png b/resources/oojs/images/fade-up.png
new file mode 100644 (file)
index 0000000..7a0cb87
Binary files /dev/null and b/resources/oojs/images/fade-up.png differ
diff --git a/resources/oojs/images/icons/accept.png b/resources/oojs/images/icons/accept.png
new file mode 100644 (file)
index 0000000..1075110
Binary files /dev/null and b/resources/oojs/images/icons/accept.png differ
diff --git a/resources/oojs/images/icons/accept.svg b/resources/oojs/images/icons/accept.svg
new file mode 100644 (file)
index 0000000..df78186
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="apply" style="opacity:0.75;">
+       <polygon id="check" style="fill-rule:evenodd;clip-rule:evenodd;" points="19.062,5.139 17.418,4 8.867,16.357 5.413,12.903 4,14.316 9.021,19.338"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/add-item.png b/resources/oojs/images/icons/add-item.png
new file mode 100644 (file)
index 0000000..aa36cd0
Binary files /dev/null and b/resources/oojs/images/icons/add-item.png differ
diff --git a/resources/oojs/images/icons/add-item.svg b/resources/oojs/images/icons/add-item.svg
new file mode 100644 (file)
index 0000000..ff95399
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24" height="24" viewBox="0, 0, 24, 24">
+  <g id="add-item">
+    <path d="M13,8 L11,8 L11,11 L8,11 L8,13 L11,13 L11,16 L13,16 L13,13 L16,13 L16,11 L13,11 z" fill="#000000"/>
+  </g>
+  <defs/>
+</svg>
diff --git a/resources/oojs/images/icons/advanced.png b/resources/oojs/images/icons/advanced.png
new file mode 100644 (file)
index 0000000..7f5ada5
Binary files /dev/null and b/resources/oojs/images/icons/advanced.png differ
diff --git a/resources/oojs/images/icons/advanced.svg b/resources/oojs/images/icons/advanced.svg
new file mode 100644 (file)
index 0000000..3e87cab
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="settings" style="opacity:0.75;">
+       <path id="gear" style="fill-rule:evenodd;clip-rule:evenodd;" d="M20.869,13.476C20.948,12.994,21,12.504,21,12
+               s-0.052-0.994-0.131-1.476l-2.463-0.259c-0.149-0.556-0.367-1.082-0.648-1.57l1.558-1.924c-0.576-0.806-1.281-1.511-2.087-2.087
+               l-1.924,1.558c-0.488-0.281-1.015-0.499-1.57-0.648l-0.259-2.463C12.994,3.052,12.504,3,12,3s-0.994,0.052-1.476,0.131
+               l-0.259,2.463C9.71,5.743,9.184,5.961,8.695,6.242L6.771,4.685C5.966,5.261,5.261,5.966,4.685,6.771l1.558,1.924
+               c-0.281,0.488-0.499,1.015-0.648,1.57l-2.463,0.259C3.052,11.006,3,11.496,3,12s0.052,0.994,0.131,1.476l2.463,0.259
+               c0.149,0.556,0.367,1.082,0.648,1.57l-1.558,1.924c0.576,0.806,1.281,1.511,2.087,2.087l1.924-1.558
+               c0.488,0.281,1.015,0.499,1.57,0.648l0.259,2.463C11.006,20.948,11.496,21,12,21s0.994-0.052,1.476-0.131l0.259-2.463
+               c0.556-0.149,1.082-0.367,1.57-0.648l1.924,1.558c0.806-0.576,1.511-1.281,2.087-2.087l-1.558-1.924
+               c0.281-0.488,0.499-1.015,0.648-1.57L20.869,13.476z M12,15.998c-2.209,0-3.998-1.789-3.998-3.998S9.791,8.002,12,8.002
+               S15.998,9.791,15.998,12S14.209,15.998,12,15.998z"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/alert.png b/resources/oojs/images/icons/alert.png
new file mode 100644 (file)
index 0000000..992ea2a
Binary files /dev/null and b/resources/oojs/images/icons/alert.png differ
diff --git a/resources/oojs/images/icons/alert.svg b/resources/oojs/images/icons/alert.svg
new file mode 100644 (file)
index 0000000..886a7c0
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="alert" style="opacity:0.75;">
+       <rect id="point" x="11" y="16" style="fill-rule:evenodd;clip-rule:evenodd;" width="2" height="2"/>
+       <polygon id="stroke" style="fill-rule:evenodd;clip-rule:evenodd;" points="13.516,10 10.516,10 11,15 13,15"/>
+       <path id="triangle" d="M12.017,5.974L19.536,19H4.496L12.017,5.974 M12.017,3.5c-0.544,0-1.088,0.357-1.5,1.071L2.532,18.402 C1.707,19.831,2.382,21,4.032,21H20c1.65,0,2.325-1.169,1.5-2.599L13.517,4.572C13.104,3.857,12.561,3.5,12.017,3.5L12.017,3.5z"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/arched-arrow-ltr.png b/resources/oojs/images/icons/arched-arrow-ltr.png
new file mode 100644 (file)
index 0000000..5db1c4d
Binary files /dev/null and b/resources/oojs/images/icons/arched-arrow-ltr.png differ
diff --git a/resources/oojs/images/icons/arched-arrow-ltr.svg b/resources/oojs/images/icons/arched-arrow-ltr.svg
new file mode 100644 (file)
index 0000000..5b343a5
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="arched-arrow-ltr" style="opacity:0.75;">
+       <path id="arrow" style="fill-rule:evenodd;clip-rule:evenodd;" d="M19.925,14.937l-2.391-6.901l-1.48,2.329 c-0.964-0.845-2.699-1.85-5.513-1.823c-4.887,0.046-6.524,4.244-6.524,4.244s2.753-2.639,6.925-1.949 c1.729,0.286,3.007,1.206,3.675,1.791l-1.474,2.319L19.925,14.937z"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/arched-arrow-rtl.png b/resources/oojs/images/icons/arched-arrow-rtl.png
new file mode 100644 (file)
index 0000000..7931971
Binary files /dev/null and b/resources/oojs/images/icons/arched-arrow-rtl.png differ
diff --git a/resources/oojs/images/icons/arched-arrow-rtl.svg b/resources/oojs/images/icons/arched-arrow-rtl.svg
new file mode 100644 (file)
index 0000000..bb5f10e
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="arched-arrow-rtl" style="opacity:0.75;">
+       <path id="arrow" style="fill-rule:evenodd;clip-rule:evenodd;" d="M13.401,8.542c-2.814-0.027-4.549,0.978-5.513,1.823 l-1.48-2.329l-2.391,6.901l6.782,0.009l-1.474-2.319c0.668-0.584,1.945-1.504,3.675-1.791c4.172-0.69,6.925,1.949,6.925,1.949 S18.288,8.588,13.401,8.542z"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/check.png b/resources/oojs/images/icons/check.png
new file mode 100644 (file)
index 0000000..82c3cb4
Binary files /dev/null and b/resources/oojs/images/icons/check.png differ
diff --git a/resources/oojs/images/icons/check.svg b/resources/oojs/images/icons/check.svg
new file mode 100644 (file)
index 0000000..e67cd6c
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24" height="24" viewBox="0, 0, 24, 24">
+  <g id="check">
+    <path d="M7.105,13.473 L8.527,12.05 L10.428,13.952 L15.238,7 L16.895,8.148 L10.635,17 z" fill="#000000"/>
+  </g>
+  <defs/>
+</svg>
diff --git a/resources/oojs/images/icons/clear.png b/resources/oojs/images/icons/clear.png
new file mode 100644 (file)
index 0000000..697dd62
Binary files /dev/null and b/resources/oojs/images/icons/clear.png differ
diff --git a/resources/oojs/images/icons/clear.svg b/resources/oojs/images/icons/clear.svg
new file mode 100644 (file)
index 0000000..d83eb02
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="clear" style="opacity:0.75;">
+       <path id="circle_with_strike" style="fill-rule:evenodd;clip-rule:evenodd;" d="M11.999,5.022c-3.853,0-6.977,3.124-6.977,6.978 c0,3.853,3.124,6.978,6.977,6.978c3.854,0,6.979-3.125,6.979-6.978C18.978,8.146,15.853,5.022,11.999,5.022z M6.886,12 c0-1.092,0.572-3.25,0.93-2.929l7.113,7.113c0.488,0.525-1.837,0.931-2.93,0.931C9.174,17.114,6.886,14.824,6.886,12z M16.184,14.929L9.07,7.816c-0.445-0.483,1.837-0.931,2.929-0.931c2.827,0,5.115,2.289,5.115,5.114 C17.114,13.092,16.75,15.542,16.184,14.929z"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/close.png b/resources/oojs/images/icons/close.png
new file mode 100644 (file)
index 0000000..f7eed9f
Binary files /dev/null and b/resources/oojs/images/icons/close.png differ
diff --git a/resources/oojs/images/icons/close.svg b/resources/oojs/images/icons/close.svg
new file mode 100644 (file)
index 0000000..a0118c2
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="close" style="opacity:0.75;">
+       <polygon id="x" style="fill-rule:evenodd;clip-rule:evenodd;" points="18.717,6.697 17.303,5.283 12,10.586 6.697,5.283 5.283,6.697 10.586,12 5.283,17.303 6.697,18.717 12,13.414 17.303,18.717 18.717,17.303 13.414,12            "/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/code.png b/resources/oojs/images/icons/code.png
new file mode 100644 (file)
index 0000000..a5ebdbf
Binary files /dev/null and b/resources/oojs/images/icons/code.png differ
diff --git a/resources/oojs/images/icons/code.svg b/resources/oojs/images/icons/code.svg
new file mode 100644 (file)
index 0000000..6f1ed53
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+        width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<g id="code" opacity="0.75">
+       <path id="left-bracket" d="M4,12v-1h1c1,0,1,0,1-1V7.614C6,7.1,6.024,6.718,6.073,6.472C6.127,6.22,6.212,6.009,6.33,5.839
+               C6.534,5.56,6.803,5.364,7.138,5.255C7.473,5.14,8.01,5,8.973,5H10v1H9.248c-0.457,0-0.77,0.191-0.936,0.408
+               C8.145,6.623,8,6.853,8,7.476v1.857c0,0.729-0.041,1.18-0.244,1.493c-0.2,0.307-0.562,0.529-1.09,0.667
+               c0.535,0.155,0.9,0.385,1.096,0.688C7.961,12.484,8,12.938,8,13.665v1.862c0,0.619,0.145,0.848,0.312,1.062
+               c0.166,0.22,0.479,0.407,0.936,0.407L10,17l0,0v1H8.973c-0.963,0-1.5-0.133-1.835-0.248c-0.335-0.109-0.604-0.307-0.808-0.591
+               c-0.118-0.165-0.203-0.374-0.257-0.625C6.024,16.283,6,15.9,6,15.387V13c0-1,0-1-1-1H4z"/>
+       <use transform="matrix(-1,0,0,1,24,0)" id="right-bracket" x="0" y="0" width="24" height="24" xlink:href="#left-bracket" />
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/collapse.png b/resources/oojs/images/icons/collapse.png
new file mode 100644 (file)
index 0000000..38b796f
Binary files /dev/null and b/resources/oojs/images/icons/collapse.png differ
diff --git a/resources/oojs/images/icons/collapse.svg b/resources/oojs/images/icons/collapse.svg
new file mode 100644 (file)
index 0000000..a89cebf
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="collapse" style="opacity:0.75;">
+       <polygon id="arrow" style="fill-rule:evenodd;clip-rule:evenodd;" points="6.697,15.714 12,10.412 17.303,15.714 18.717,14.3 12,7.583 5.283,14.3"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/comment.png b/resources/oojs/images/icons/comment.png
new file mode 100644 (file)
index 0000000..9546455
Binary files /dev/null and b/resources/oojs/images/icons/comment.png differ
diff --git a/resources/oojs/images/icons/comment.svg b/resources/oojs/images/icons/comment.svg
new file mode 100644 (file)
index 0000000..e052935
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="comment" style="opacity:0.75;">
+       <path id="speech_bubble" style="fill-rule:evenodd;clip-rule:evenodd;" d="M15,6H9C7.343,6,6,7.344,6,9v4c0,1.656,1.343,3,3,3v3 l3-3h3c1.657,0,3-1.344,3-3V9C18,7.344,16.657,6,15,6z"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/expand.png b/resources/oojs/images/icons/expand.png
new file mode 100644 (file)
index 0000000..e90aca1
Binary files /dev/null and b/resources/oojs/images/icons/expand.png differ
diff --git a/resources/oojs/images/icons/expand.svg b/resources/oojs/images/icons/expand.svg
new file mode 100644 (file)
index 0000000..b542f5f
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="expand" style="opacity:0.75;">
+       <polygon id="arrow" style="fill-rule:evenodd;clip-rule:evenodd;" points="17.303,8.283 12,13.586 6.697,8.283 5.283,9.697 12,16.414 18.717,9.697"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/help.png b/resources/oojs/images/icons/help.png
new file mode 100644 (file)
index 0000000..dca745b
Binary files /dev/null and b/resources/oojs/images/icons/help.png differ
diff --git a/resources/oojs/images/icons/help.svg b/resources/oojs/images/icons/help.svg
new file mode 100644 (file)
index 0000000..c68bdda
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="help" style="opacity:0.75;">
+       <path id="circle" style="fill-rule:evenodd;clip-rule:evenodd;" d="M12.001,2.085c-5.478,0-9.916,4.438-9.916,9.916 c0,5.476,4.438,9.914,9.916,9.914c5.476,0,9.914-4.438,9.914-9.914C21.915,6.523,17.477,2.085,12.001,2.085z M12.002,20.085 c-4.465,0-8.084-3.619-8.084-8.083c0-4.465,3.619-8.084,8.084-8.084c4.464,0,8.083,3.619,8.083,8.084 C20.085,16.466,16.466,20.085,12.002,20.085z"/>
+       <g id="question_mark">
+               <path id="top" style="fill-rule:evenodd;clip-rule:evenodd;" d="M11.766,6.688c-2.5,0-3.219,2.188-3.219,2.188l1.411,0.854 c0,0,0.298-0.791,0.901-1.229c0.516-0.375,1.625-0.625,2.219,0.125c0.701,0.885-0.17,1.587-1.078,2.719 C11.047,12.531,11,15,11,15h1.969c0,0,0.135-2.318,1.041-3.381c0.603-0.707,1.443-1.338,1.443-2.494S14.266,6.688,11.766,6.688z"/>
+               <rect id="bottom" x="11" y="16" style="fill-rule:evenodd;clip-rule:evenodd;" width="2" height="2"/>
+       </g>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/history.png b/resources/oojs/images/icons/history.png
new file mode 100644 (file)
index 0000000..c049931
Binary files /dev/null and b/resources/oojs/images/icons/history.png differ
diff --git a/resources/oojs/images/icons/history.svg b/resources/oojs/images/icons/history.svg
new file mode 100644 (file)
index 0000000..40c0ae3
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="history" style="opacity:0.75;">
+       <path id="clock_hands" style="fill-rule:evenodd;clip-rule:evenodd;" d="M17.26,15.076c0,0-2.385-1.935-4.005-3.062 c0.72-2.397,1.702-6.559,1.702-6.559s-4.35,5.363-4.877,6.699c-0.463,1.168,1.459,2.209,2.346,1.678 C14.326,14.383,17.26,15.076,17.26,15.076z"/>
+       <path id="arrow" style="fill-rule:evenodd;clip-rule:evenodd;" d="M12.086,2.085c-5.478,0-9.916,4.438-9.916,9.916 c0,1.783,0.476,3.454,1.301,4.898l-2.223,2.04h5.688v-5.219l-2.066,1.896c-0.55-1.088-0.866-2.312-0.866-3.615 c0-4.465,3.619-8.084,8.084-8.084c4.464,0,8.083,3.619,8.083,8.084c0,4.464-3.619,8.083-8.083,8.083 c-1.145,0-2.228-0.247-3.213-0.678l-0.833,1.634c1.235,0.557,2.602,0.874,4.045,0.874c5.476,0,9.914-4.438,9.914-9.914 C22,6.523,17.562,2.085,12.086,2.085z"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/link.png b/resources/oojs/images/icons/link.png
new file mode 100644 (file)
index 0000000..7dfa268
Binary files /dev/null and b/resources/oojs/images/icons/link.png differ
diff --git a/resources/oojs/images/icons/link.svg b/resources/oojs/images/icons/link.svg
new file mode 100644 (file)
index 0000000..dadf69c
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="link" style="opacity:0.75;">
+       <path id="right" d="M19.188,12.001c0,1.1-0.891,2.015-1.988,2.015l-4.195-0.015C13.543,15.089,13.968,16,15.002,16h3
+               C19.658,16,21,13.657,21,12s-1.342-4-2.998-4h-3c-1.034,0-1.459,0.911-1.998,1.999l4.195-0.015
+               C18.297,9.984,19.188,10.901,19.188,12.001z"/>
+       <path id="center" d="M8,12c0,0.535,0.42,1,0.938,1h6.109c0.518,0,0.938-0.465,0.938-1c0-0.534-0.42-1-0.938-1H8.938
+               C8.42,11,8,11.466,8,12z"/>
+       <path id="left" d="M4.816,11.999c0-1.1,0.891-2.015,1.988-2.015L11,9.999C10.461,8.911,10.036,8,9.002,8h-3
+               c-1.656,0-2.998,2.343-2.998,4s1.342,4,2.998,4h3c1.034,0,1.459-0.911,1.998-1.999l-4.195,0.015
+               C5.707,14.016,4.816,13.099,4.816,11.999z"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/menu.png b/resources/oojs/images/icons/menu.png
new file mode 100644 (file)
index 0000000..b5ac60f
Binary files /dev/null and b/resources/oojs/images/icons/menu.png differ
diff --git a/resources/oojs/images/icons/menu.svg b/resources/oojs/images/icons/menu.svg
new file mode 100644 (file)
index 0000000..657fab2
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="menu" style="opacity:0.75;">
+       <path id="lines" d="M6,15h12c0.553,0,1,0.447,1,1v1c0,0.553-0.447,1-1,1H6c-0.553,0-1-0.447-1-1v-1C5,15.447,5.447,15,6,15z M5,11v1
+               c0,0.553,0.447,1,1,1h12c0.553,0,1-0.447,1-1v-1c0-0.553-0.447-1-1-1H6C5.447,10,5,10.447,5,11z M5,6v1c0,0.553,0.447,1,1,1h12
+               c0.553,0,1-0.447,1-1V6c0-0.553-0.447-1-1-1H6C5.447,5,5,5.447,5,6z"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/move-ltr.png b/resources/oojs/images/icons/move-ltr.png
new file mode 100644 (file)
index 0000000..ded5f05
Binary files /dev/null and b/resources/oojs/images/icons/move-ltr.png differ
diff --git a/resources/oojs/images/icons/move-ltr.svg b/resources/oojs/images/icons/move-ltr.svg
new file mode 100644 (file)
index 0000000..a378a5d
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="move-ltr" style="opacity:0.75;">
+       <polygon id="arrow" style="fill-rule:evenodd;clip-rule:evenodd;" points="8.935,7.181 14.237,12.483 8.935,17.786
+               10.349,19.2 17.065,12.483 10.349,5.767"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/move-rtl.png b/resources/oojs/images/icons/move-rtl.png
new file mode 100644 (file)
index 0000000..fc6e62d
Binary files /dev/null and b/resources/oojs/images/icons/move-rtl.png differ
diff --git a/resources/oojs/images/icons/move-rtl.svg b/resources/oojs/images/icons/move-rtl.svg
new file mode 100644 (file)
index 0000000..c0b334b
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="move-rtl" style="opacity:0.75;">
+       <polygon id="arrow_9_" style="fill-rule:evenodd;clip-rule:evenodd;" points="15.065,17.786 9.763,12.483 15.065,7.181
+               13.651,5.767 6.935,12.483 13.651,19.2"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/picture.png b/resources/oojs/images/icons/picture.png
new file mode 100644 (file)
index 0000000..faf8af9
Binary files /dev/null and b/resources/oojs/images/icons/picture.png differ
diff --git a/resources/oojs/images/icons/picture.svg b/resources/oojs/images/icons/picture.svg
new file mode 100644 (file)
index 0000000..078ce10
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="picture" style="opacity:0.75;">
+       <path id="frame" style="fill-rule:evenodd;clip-rule:evenodd;" d="M18,4H6C4,3.993,3,4.993,3,6.993L3.014,16C3,18,4,18.988,6,19h12
+               c2-0.012,2.994-1,3-3.006V6.993C20.994,4.993,20,3.993,18,4z M19,17H5V6h14V17z"/>
+       <polygon id="mountains" style="fill-rule:evenodd;clip-rule:evenodd;" points="6,13.5 9.5,10 11.828,12.312 10.516,13.406
+               11.391,14.438 15.5,11 18,13 18,16 6,16"/>
+       <polygon id="sky" style="fill-rule:evenodd;clip-rule:evenodd;" points="6,12 9.516,7.844 12.562,11.016 15.5,9 18,11 18,7 6,7"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/remove-item.png b/resources/oojs/images/icons/remove-item.png
new file mode 100644 (file)
index 0000000..2f11db3
Binary files /dev/null and b/resources/oojs/images/icons/remove-item.png differ
diff --git a/resources/oojs/images/icons/remove-item.svg b/resources/oojs/images/icons/remove-item.svg
new file mode 100644 (file)
index 0000000..b95e7d3
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24" height="24" viewBox="0, 0, 24, 24">
+  <g id="remove-item">
+    <path d="M8,11 L16,11 L16,13 L8,13 z" fill="#000000"/>
+  </g>
+  <defs/>
+</svg>
diff --git a/resources/oojs/images/icons/remove.png b/resources/oojs/images/icons/remove.png
new file mode 100644 (file)
index 0000000..d7e116c
Binary files /dev/null and b/resources/oojs/images/icons/remove.png differ
diff --git a/resources/oojs/images/icons/remove.svg b/resources/oojs/images/icons/remove.svg
new file mode 100644 (file)
index 0000000..17c8d39
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="remove" style="opacity:0.75;">
+       <path id="trash_can" style="fill-rule:evenodd;clip-rule:evenodd;" d="M12,10h-1v6h1V10z M10,10H9v6h1V10z M14,10h-1v6h1V10z
+                M14,6V5H9v1H6v3h1v7.966l1,1.031v-0.074V18h6.984L15,17.982v0.015l1-1.031V9h1V6H14z M15,17H8V9h7V17z M16,8H7V7h9V8z"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/search.png b/resources/oojs/images/icons/search.png
new file mode 100644 (file)
index 0000000..df29792
Binary files /dev/null and b/resources/oojs/images/icons/search.png differ
diff --git a/resources/oojs/images/icons/search.svg b/resources/oojs/images/icons/search.svg
new file mode 100644 (file)
index 0000000..37feda4
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="search" style="opacity:0.75;">
+       <path id="magnifying_glass" d="M16.021,15.96l-2.374-2.375c-0.048-0.047-0.105-0.079-0.169-0.099c0.403-0.566,0.643-1.26,0.643-2.009
+               C14.12,9.557,12.563,8,10.644,8c-1.921,0-3.478,1.557-3.478,3.478c0,1.92,1.557,3.477,3.478,3.477c0.749,0,1.442-0.239,2.01-0.643
+               c0.019,0.063,0.051,0.121,0.098,0.169l2.375,2.374c0.19,0.189,0.543,0.143,0.79-0.104S16.21,16.15,16.021,15.96z M10.644,13.69
+               c-1.221,0-2.213-0.991-2.213-2.213c0-1.221,0.992-2.213,2.213-2.213c1.222,0,2.213,0.992,2.213,2.213
+               C12.856,12.699,11.865,13.69,10.644,13.69z"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/settings.png b/resources/oojs/images/icons/settings.png
new file mode 100644 (file)
index 0000000..b1b35e9
Binary files /dev/null and b/resources/oojs/images/icons/settings.png differ
diff --git a/resources/oojs/images/icons/settings.svg b/resources/oojs/images/icons/settings.svg
new file mode 100644 (file)
index 0000000..1464a79
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24" height="24" viewBox="0, 0, 24, 24">
+  <g id="settings" opacity="0.75">
+    <path d="M3,4 L6,4 L6,6 L3,6 z" fill="#000000"/>
+    <path d="M12,4 L21,4 L21,6 L12,6 z" fill="#000000"/>
+    <path d="M8,3 L10,3 C10.552,3 11,3.448 11,4 L11,6 C11,6.552 10.552,7 10,7 L8,7 C7.448,7 7,6.552 7,6 L7,4 C7,3.448 7.448,3 8,3 z" fill="#000000"/>
+    <path d="M3,11 L12,11 L12,13 L3,13 z" fill="#000000"/>
+    <path d="M18,11 L21,11 L21,13 L18,13 z" fill="#000000"/>
+    <path d="M14,10 L16,10 C16.552,10 17,10.448 17,11 L17,13 C17,13.552 16.552,14 16,14 L14,14 C13.448,14 13,13.552 13,13 L13,11 C13,10.448 13.448,10 14,10 z" fill="#000000"/>
+    <path d="M3,18 L9,18 L9,20 L3,20 z" fill="#000000"/>
+    <path d="M15,18 L21,18 L21,20 L15,20 z" fill="#000000"/>
+    <path d="M11,17 L13,17 C13.552,17 14,17.448 14,18 L14,20 C14,20.552 13.552,21 13,21 L11,21 C10.448,21 10,20.552 10,20 L10,18 C10,17.448 10.448,17 11,17 z" fill="#000000"/>
+  </g>
+  <defs/>
+</svg>
diff --git a/resources/oojs/images/icons/tag.png b/resources/oojs/images/icons/tag.png
new file mode 100644 (file)
index 0000000..722f4d7
Binary files /dev/null and b/resources/oojs/images/icons/tag.png differ
diff --git a/resources/oojs/images/icons/tag.svg b/resources/oojs/images/icons/tag.svg
new file mode 100644 (file)
index 0000000..d21e5e3
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="tag" style="opacity:0.75;">
+       <path d="M18.748,11.717c0.389,0.389,0.389,1.025,0,1.414l-4.949,4.95c-0.389,0.389-1.025,0.389-1.414,0l-6.01-6.01
+               c-0.389-0.389-0.707-1.157-0.707-1.707L5.667,6c0-0.55,0.45-1,1-1h4.364c0.55,0,1.318,0.318,1.707,0.707L18.748,11.717z
+                M8.104,7.456C7.525,8.032,7.526,8.97,8.103,9.549c0.578,0.577,1.516,0.577,2.095,0.001c0.576-0.578,0.576-1.517,0-2.095
+               C9.617,6.879,8.68,6.878,8.104,7.456z"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/icons/window.png b/resources/oojs/images/icons/window.png
new file mode 100644 (file)
index 0000000..3d48a3c
Binary files /dev/null and b/resources/oojs/images/icons/window.png differ
diff --git a/resources/oojs/images/icons/window.svg b/resources/oojs/images/icons/window.svg
new file mode 100644 (file)
index 0000000..621cf2c
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px"
+        height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g id="window" style="opacity:0.75;">
+       <rect id="title" x="7" y="10" width="10" height="1"/>
+       <path id="window" d="M16,19H8c-2.206,0-4-1.794-4-4V9c0-2.206,1.794-4,4-4h8c2.206,0,4,1.794,4,4v6C20,17.206,18.206,19,16,19z
+                M8,7C6.897,7,6,7.897,6,9v6c0,1.103,0.897,2,2,2h8c1.103,0,2-0.897,2-2V9c0-1.103-0.897-2-2-2H8z"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/indicators/down.png b/resources/oojs/images/indicators/down.png
new file mode 100644 (file)
index 0000000..47ff54c
Binary files /dev/null and b/resources/oojs/images/indicators/down.png differ
diff --git a/resources/oojs/images/indicators/down.svg b/resources/oojs/images/indicators/down.svg
new file mode 100644 (file)
index 0000000..c871f60
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="12px"
+        height="12px" viewBox="0 0 12 12" style="enable-background:new 0 0 12 12;" xml:space="preserve">
+<g id="down" style="opacity:0.75;">
+       <polygon id="arrow" style="fill-rule:evenodd;clip-rule:evenodd;" points="2.023,3 5.512,8.953 9,3"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/indicators/required.png b/resources/oojs/images/indicators/required.png
new file mode 100644 (file)
index 0000000..aeb35a3
Binary files /dev/null and b/resources/oojs/images/indicators/required.png differ
diff --git a/resources/oojs/images/indicators/required.svg b/resources/oojs/images/indicators/required.svg
new file mode 100644 (file)
index 0000000..7c60ec0
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="12" height="12" viewBox="0, 0, 12, 12">
+  <g id="required" opacity="0.75">
+    <path d="M7,0 L7,4.268 L10.696,2.134 L11.696,3.866 L8,6 L11.696,8.134 L10.696,9.866 L7,7.732 L7,12 L5,12 L5,7.732 L1.304,9.866 L0.304,8.134 L4,6 L0.304,3.866 L1.304,2.134 L5,4.268 L5,0 z" fill="#000000"/>
+  </g>
+  <defs/>
+</svg>
diff --git a/resources/oojs/images/indicators/up.png b/resources/oojs/images/indicators/up.png
new file mode 100644 (file)
index 0000000..b827f6d
Binary files /dev/null and b/resources/oojs/images/indicators/up.png differ
diff --git a/resources/oojs/images/indicators/up.svg b/resources/oojs/images/indicators/up.svg
new file mode 100644 (file)
index 0000000..a5d7f38
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="12px"
+        height="12px" viewBox="0 0 12 12" style="enable-background:new 0 0 12 12;" xml:space="preserve">
+<g id="up" style="opacity:0.75;">
+       <polygon id="arrow" style="fill-rule:evenodd;clip-rule:evenodd;" points="5.512,2.006 2,8 9.024,8                "/>
+</g>
+</svg>
diff --git a/resources/oojs/images/tail.svg b/resources/oojs/images/tail.svg
new file mode 100644 (file)
index 0000000..4df8bb2
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+        width="15px" height="8px" viewBox="0 0 15 8" style="enable-background:new 0 0 15 8;" xml:space="preserve">
+<g id="tail">
+       <polygon id="outline" style="fill-rule:evenodd;clip-rule:evenodd;fill:#808080;" points="7.609,2.499 2.096,8 13.125,8"/>
+       <polygon id="fill" style="fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;" points="7.609,3 2.598,8 12.622,8"/>
+</g>
+</svg>
diff --git a/resources/oojs/images/textures/pending.gif b/resources/oojs/images/textures/pending.gif
new file mode 100644 (file)
index 0000000..1194eed
Binary files /dev/null and b/resources/oojs/images/textures/pending.gif differ
diff --git a/resources/oojs/images/textures/transparency.png b/resources/oojs/images/textures/transparency.png
new file mode 100644 (file)
index 0000000..b8e36d3
Binary files /dev/null and b/resources/oojs/images/textures/transparency.png differ
diff --git a/resources/oojs/images/toolbar-shadow.png b/resources/oojs/images/toolbar-shadow.png
new file mode 100644 (file)
index 0000000..97e8d13
Binary files /dev/null and b/resources/oojs/images/toolbar-shadow.png differ
diff --git a/resources/oojs/oojs-ui.js b/resources/oojs/oojs-ui.js
new file mode 100644 (file)
index 0000000..a58f65d
--- /dev/null
@@ -0,0 +1,7325 @@
+/*!
+ * OOjs UI v0.1.0-pre (a290673bbd)
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2014 OOjs Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: Wed Feb 12 2014 13:52:08 GMT-0800 (PST)
+ */
+( function () {
+
+'use strict';
+/**
+ * Namespace for all classes, static methods and static properties.
+ *
+ * @class
+ * @singleton
+ */
+OO.ui = {};
+
+OO.ui.bind = $.proxy;
+
+/**
+ * Get the user's language and any fallback languages.
+ *
+ * These language codes are used to localize user interface elements in the user's language.
+ *
+ * In environments that provide a localization system, this function should be overridden to
+ * return the user's language(s). The default implementation returns English (en) only.
+ *
+ * @returns {string[]} Language codes, in descending order of priority
+ */
+OO.ui.getUserLanguages = function () {
+       return [ 'en' ];
+};
+
+/**
+ * Get a value in an object keyed by language code.
+ *
+ * @param {Object.<string,Mixed>} obj Object keyed by language code
+ * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
+ * @param {string} [fallback] Fallback code, used if no matching language can be found
+ * @returns {Mixed} Local value
+ */
+OO.ui.getLocalValue = function ( obj, lang, fallback ) {
+       var i, len, langs;
+
+       // Requested language
+       if ( obj[lang] ) {
+               return obj[lang];
+       }
+       // Known user language
+       langs = OO.ui.getUserLanguages();
+       for ( i = 0, len = langs.length; i < len; i++ ) {
+               lang = langs[i];
+               if ( obj[lang] ) {
+                       return obj[lang];
+               }
+       }
+       // Fallback language
+       if ( obj[fallback] ) {
+               return obj[fallback];
+       }
+       // First existing language
+       for ( lang in obj ) {
+               return obj[lang];
+       }
+
+       return undefined;
+};
+
+( function () {
+
+/**
+ * Message store for the default implementation of OO.ui.msg
+ *
+ * Environments that provide a localization system should not use this, but should override
+ * OO.ui.msg altogether.
+ *
+ * @private
+ */
+var messages = {
+       // Label text for button to exit from dialog
+       'ooui-dialog-action-close': 'Close',
+       // Tool tip for a button that moves items in a list down one place
+       'ooui-outline-control-move-down': 'Move item down',
+       // Tool tip for a button that moves items in a list up one place
+       'ooui-outline-control-move-up': 'Move item up',
+       // Label for the toolbar group that contains a list of all other available tools
+       'ooui-toolbar-more': 'More'
+};
+
+/**
+ * Get a localized message.
+ *
+ * In environments that provide a localization system, this function should be overridden to
+ * return the message translated in the user's language. The default implementation always returns
+ * English messages.
+ *
+ * After the message key, message parameters may optionally be passed. In the default implementation,
+ * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
+ * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
+ * they support unnamed, ordered message parameters.
+ *
+ * @abstract
+ * @param {string} key Message key
+ * @param {Mixed...} [params] Message parameters
+ * @returns {string} Translated message with parameters substituted
+ */
+OO.ui.msg = function ( key ) {
+       var message = messages[key], params = Array.prototype.slice.call( arguments, 1 );
+       if ( typeof message === 'string' ) {
+               // Perform $1 substitution
+               message = message.replace( /\$(\d+)/g, function ( unused, n ) {
+                       var i = parseInt( n, 10 );
+                       return params[i - 1] !== undefined ? params[i - 1] : '$' + n;
+               } );
+       } else {
+               // Return placeholder if message not found
+               message = '[' + key + ']';
+       }
+       return message;
+};
+
+OO.ui.deferMsg = function ( key ) {
+       return function () {
+               return OO.ui.msg( key );
+       };
+};
+
+OO.ui.resolveMsg = function ( msg ) {
+       if ( $.isFunction( msg ) ) {
+               return msg();
+       }
+       return msg;
+};
+
+} )();
+
+// Add more as you need
+OO.ui.Keys = {
+       'UNDEFINED': 0,
+       'BACKSPACE': 8,
+       'DELETE': 46,
+       'LEFT': 37,
+       'RIGHT': 39,
+       'UP': 38,
+       'DOWN': 40,
+       'ENTER': 13,
+       'END': 35,
+       'HOME': 36,
+       'TAB': 9,
+       'PAGEUP': 33,
+       'PAGEDOWN': 34,
+       'ESCAPE': 27,
+       'SHIFT': 16,
+       'SPACE': 32
+};
+/**
+ * DOM element abstraction.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Function} [$] jQuery for the frame the widget is in
+ * @cfg {string[]} [classes] CSS class names
+ * @cfg {jQuery} [$content] Content elements to append
+ */
+OO.ui.Element = function OoUiElement( config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Properties
+       this.$ = config.$ || OO.ui.Element.getJQuery( document );
+       this.$element = this.$( this.$.context.createElement( this.getTagName() ) );
+
+       // Initialization
+       if ( Array.isArray( config.classes ) ) {
+               this.$element.addClass( config.classes.join( ' ' ) );
+       }
+       if ( config.$content ) {
+               this.$element.append( config.$content );
+       }
+};
+
+/* Static Properties */
+
+/**
+ * @static
+ * @property
+ * @inheritable
+ */
+OO.ui.Element.static = {};
+
+/**
+ * HTML tag name.
+ *
+ * This may be ignored if getTagName is overridden.
+ *
+ * @static
+ * @property {string}
+ * @inheritable
+ */
+OO.ui.Element.static.tagName = 'div';
+
+/* Static Methods */
+
+/**
+ * Gets a jQuery function within a specific document.
+ *
+ * @static
+ * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
+ * @param {OO.ui.Frame} [frame] Frame of the document context
+ * @returns {Function} Bound jQuery function
+ */
+OO.ui.Element.getJQuery = function ( context, frame ) {
+       function wrapper( selector ) {
+               return $( selector, wrapper.context );
+       }
+
+       wrapper.context = this.getDocument( context );
+
+       if ( frame ) {
+               wrapper.frame = frame;
+       }
+
+       return wrapper;
+};
+
+/**
+ * Get the document of an element.
+ *
+ * @static
+ * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
+ * @returns {HTMLDocument} Document object
+ * @throws {Error} If context is invalid
+ */
+OO.ui.Element.getDocument = function ( obj ) {
+       var doc =
+               // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
+               ( obj[0] && obj[0].ownerDocument ) ||
+               // Empty jQuery selections might have a context
+               obj.context ||
+               // HTMLElement
+               obj.ownerDocument ||
+               // Window
+               obj.document ||
+               // HTMLDocument
+               ( obj.nodeType === 9 && obj );
+
+       if ( doc ) {
+               return doc;
+       }
+
+       throw new Error( 'Invalid context' );
+};
+
+/**
+ * Get the window of an element or document.
+ *
+ * @static
+ * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
+ * @returns {Window} Window object
+ */
+OO.ui.Element.getWindow = function ( obj ) {
+       var doc = this.getDocument( obj );
+       return doc.parentWindow || doc.defaultView;
+};
+
+/**
+ * Get the direction of an element or document.
+ *
+ * @static
+ * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
+ * @returns {string} Text direction, either `ltr` or `rtl`
+ */
+OO.ui.Element.getDir = function ( obj ) {
+       var isDoc, isWin;
+
+       if ( obj instanceof jQuery ) {
+               obj = obj[0];
+       }
+       isDoc = obj.nodeType === 9;
+       isWin = obj.document !== undefined;
+       if ( isDoc || isWin ) {
+               if ( isWin ) {
+                       obj = obj.document;
+               }
+               obj = obj.body;
+       }
+       return $( obj ).css( 'direction' );
+};
+
+/**
+ * Get the offset between two frames.
+ *
+ * TODO: Make this function not use recursion.
+ *
+ * @static
+ * @param {Window} from Window of the child frame
+ * @param {Window} [to=window] Window of the parent frame
+ * @param {Object} [offset] Offset to start with, used internally
+ * @returns {Object} Offset object, containing left and top properties
+ */
+OO.ui.Element.getFrameOffset = function ( from, to, offset ) {
+       var i, len, frames, frame, rect;
+
+       if ( !to ) {
+               to = window;
+       }
+       if ( !offset ) {
+               offset = { 'top': 0, 'left': 0 };
+       }
+       if ( from.parent === from ) {
+               return offset;
+       }
+
+       // Get iframe element
+       frames = from.parent.document.getElementsByTagName( 'iframe' );
+       for ( i = 0, len = frames.length; i < len; i++ ) {
+               if ( frames[i].contentWindow === from ) {
+                       frame = frames[i];
+                       break;
+               }
+       }
+
+       // Recursively accumulate offset values
+       if ( frame ) {
+               rect = frame.getBoundingClientRect();
+               offset.left += rect.left;
+               offset.top += rect.top;
+               if ( from !== to ) {
+                       this.getFrameOffset( from.parent, offset );
+               }
+       }
+       return offset;
+};
+
+/**
+ * Get the offset between two elements.
+ *
+ * @static
+ * @param {jQuery} $from
+ * @param {jQuery} $to
+ * @returns {Object} Translated position coordinates, containing top and left properties
+ */
+OO.ui.Element.getRelativePosition = function ( $from, $to ) {
+       var from = $from.offset(),
+               to = $to.offset();
+       return { 'top': Math.round( from.top - to.top ), 'left': Math.round( from.left - to.left ) };
+};
+
+/**
+ * Get element border sizes.
+ *
+ * @static
+ * @param {HTMLElement} el Element to measure
+ * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
+ */
+OO.ui.Element.getBorders = function ( el ) {
+       var doc = el.ownerDocument,
+               win = doc.parentWindow || doc.defaultView,
+               style = win && win.getComputedStyle ?
+                       win.getComputedStyle( el, null ) :
+                       el.currentStyle,
+               $el = $( el ),
+               top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
+               left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
+               bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
+               right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
+
+       return {
+               'top': Math.round( top ),
+               'left': Math.round( left ),
+               'bottom': Math.round( bottom ),
+               'right': Math.round( right )
+       };
+};
+
+/**
+ * Get dimensions of an element or window.
+ *
+ * @static
+ * @param {HTMLElement|Window} el Element to measure
+ * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
+ */
+OO.ui.Element.getDimensions = function ( el ) {
+       var $el, $win,
+               doc = el.ownerDocument || el.document,
+               win = doc.parentWindow || doc.defaultView;
+
+       if ( win === el || el === doc.documentElement ) {
+               $win = $( win );
+               return {
+                       'borders': { 'top': 0, 'left': 0, 'bottom': 0, 'right': 0 },
+                       'scroll': {
+                               'top': $win.scrollTop(),
+                               'left': $win.scrollLeft()
+                       },
+                       'scrollbar': { 'right': 0, 'bottom': 0 },
+                       'rect': {
+                               'top': 0,
+                               'left': 0,
+                               'bottom': $win.innerHeight(),
+                               'right': $win.innerWidth()
+                       }
+               };
+       } else {
+               $el = $( el );
+               return {
+                       'borders': this.getBorders( el ),
+                       'scroll': {
+                               'top': $el.scrollTop(),
+                               'left': $el.scrollLeft()
+                       },
+                       'scrollbar': {
+                               'right': $el.innerWidth() - el.clientWidth,
+                               'bottom': $el.innerHeight() - el.clientHeight
+                       },
+                       'rect': el.getBoundingClientRect()
+               };
+       }
+};
+
+/**
+ * Get closest scrollable container.
+ *
+ * Traverses up until either a scrollable element or the root is reached, in which case the window
+ * will be returned.
+ *
+ * @static
+ * @param {HTMLElement} el Element to find scrollable container for
+ * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
+ * @return {HTMLElement|Window} Closest scrollable container
+ */
+OO.ui.Element.getClosestScrollableContainer = function ( el, dimension ) {
+       var i, val,
+               props = [ 'overflow' ],
+               $parent = $( el ).parent();
+
+       if ( dimension === 'x' || dimension === 'y' ) {
+               props.push( 'overflow-' + dimension );
+       }
+
+       while ( $parent.length ) {
+               if ( $parent[0] === el.ownerDocument.body ) {
+                       return $parent[0];
+               }
+               i = props.length;
+               while ( i-- ) {
+                       val = $parent.css( props[i] );
+                       if ( val === 'auto' || val === 'scroll' ) {
+                               return $parent[0];
+                       }
+               }
+               $parent = $parent.parent();
+       }
+       return this.getDocument( el ).body;
+};
+
+/**
+ * Scroll element into view
+ *
+ * @static
+ * @param {HTMLElement} el Element to scroll into view
+ * @param {Object} [config={}] Configuration config
+ * @param {string} [config.duration] jQuery animation duration value
+ * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
+ *  to scroll in both directions
+ * @param {Function} [config.complete] Function to call when scrolling completes
+ */
+OO.ui.Element.scrollIntoView = function ( el, config ) {
+       // Configuration initialization
+       config = config || {};
+
+       var anim = {},
+               callback = typeof config.complete === 'function' && config.complete,
+               sc = this.getClosestScrollableContainer( el, config.direction ),
+               $sc = $( sc ),
+               eld = this.getDimensions( el ),
+               scd = this.getDimensions( sc ),
+               rel = {
+                       'top': eld.rect.top - ( scd.rect.top + scd.borders.top ),
+                       'bottom': scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
+                       'left': eld.rect.left - ( scd.rect.left + scd.borders.left ),
+                       'right': scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
+               };
+
+       if ( !config.direction || config.direction === 'y' ) {
+               if ( rel.top < 0 ) {
+                       anim.scrollTop = scd.scroll.top + rel.top;
+               } else if ( rel.top > 0 && rel.bottom < 0 ) {
+                       anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
+               }
+       }
+       if ( !config.direction || config.direction === 'x' ) {
+               if ( rel.left < 0 ) {
+                       anim.scrollLeft = scd.scroll.left + rel.left;
+               } else if ( rel.left > 0 && rel.right < 0 ) {
+                       anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
+               }
+       }
+       if ( !$.isEmptyObject( anim ) ) {
+               $sc.stop( true ).animate( anim, config.duration || 'fast' );
+               if ( callback ) {
+                       $sc.queue( function ( next ) {
+                               callback();
+                               next();
+                       } );
+               }
+       } else {
+               if ( callback ) {
+                       callback();
+               }
+       }
+};
+
+/* Methods */
+
+/**
+ * Get the HTML tag name.
+ *
+ * Override this method to base the result on instance information.
+ *
+ * @returns {string} HTML tag name
+ */
+OO.ui.Element.prototype.getTagName = function () {
+       return this.constructor.static.tagName;
+};
+
+/**
+ * Get the DOM document.
+ *
+ * @returns {HTMLDocument} Document object
+ */
+OO.ui.Element.prototype.getElementDocument = function () {
+       return OO.ui.Element.getDocument( this.$element );
+};
+
+/**
+ * Get the DOM window.
+ *
+ * @returns {Window} Window object
+ */
+OO.ui.Element.prototype.getElementWindow = function () {
+       return OO.ui.Element.getWindow( this.$element );
+};
+
+/**
+ * Get closest scrollable container.
+ *
+ * @method
+ * @see #static-method-getClosestScrollableContainer
+ */
+OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
+       return OO.ui.Element.getClosestScrollableContainer( this.$element[0] );
+};
+
+/**
+ * Scroll element into view
+ *
+ * @method
+ * @see #static-method-scrollIntoView
+ * @param {Object} [config={}]
+ */
+OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
+       return OO.ui.Element.scrollIntoView( this.$element[0], config );
+};
+
+( function () {
+       // Static
+       var specialFocusin;
+
+       function handler( e ) {
+               jQuery.event.simulate( 'focusin', e.target, jQuery.event.fix( e ), /* bubble = */ true );
+       }
+
+       specialFocusin = {
+               setup: function () {
+                       var doc = this.ownerDocument || this,
+                               attaches = $.data( doc, 'ooui-focusin-attaches' );
+                       if ( !attaches ) {
+                               doc.addEventListener( 'focus', handler, true );
+                       }
+                       $.data( doc, 'ooui-focusin-attaches', ( attaches || 0 ) + 1 );
+               },
+               teardown: function () {
+                       var doc = this.ownerDocument || this,
+                               attaches = $.data( doc, 'ooui-focusin-attaches' ) - 1;
+                       if ( !attaches ) {
+                               doc.removeEventListener( 'focus', handler, true );
+                               $.removeData( doc, 'ooui-focusin-attaches' );
+                       } else {
+                               $.data( doc, 'ooui-focusin-attaches', attaches );
+                       }
+               }
+       };
+
+       /**
+        * Bind a handler for an event on the DOM element.
+        *
+        * Uses jQuery internally for everything except for events which are
+        * known to have issues in the browser or in jQuery. This method
+        * should become obsolete eventually.
+        *
+        * @param {string} event
+        * @param {Function} callback
+        */
+       OO.ui.Element.prototype.onDOMEvent = function ( event, callback ) {
+               var orig;
+
+               if ( event === 'focusin' ) {
+                       // jQuery 1.8.3 has a bug with handling focusin events inside iframes.
+                       // Firefox doesn't support focusin at all, so we listen for 'focus' on the
+                       // document, and simulate a 'focusin' event on the target element and make
+                       // it bubble from there.
+                       //
+                       // - http://jsfiddle.net/sw3hr/
+                       // - http://bugs.jquery.com/ticket/14180
+                       // - https://github.com/jquery/jquery/commit/1cecf64e5aa4153
+
+                       // Replace jQuery's override with our own
+                       orig = $.event.special.focusin;
+                       $.event.special.focusin = specialFocusin;
+
+                       this.$element.on( event, callback );
+
+                       // Restore
+                       $.event.special.focusin = orig;
+
+               } else {
+                       this.$element.on( event, callback );
+               }
+       };
+
+       /**
+        * @param {string} event
+        * @param {Function} callback
+        */
+       OO.ui.Element.prototype.offDOMEvent = function ( event, callback ) {
+               var orig;
+               if ( event === 'focusin' ) {
+                       orig = $.event.special.focusin;
+                       $.event.special.focusin = specialFocusin;
+                       this.$element.off( event, callback );
+                       $.event.special.focusin = orig;
+               } else {
+                       this.$element.off( event, callback );
+               }
+       };
+}() );
+/**
+ * Embedded iframe with the same styles as its parent.
+ *
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.Frame = function OoUiFrame( config ) {
+       // Parent constructor
+       OO.ui.Element.call( this, config );
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+
+       // Properties
+       this.initialized = false;
+       this.config = config;
+
+       // Initialize
+       this.$element
+               .addClass( 'oo-ui-frame' )
+               .attr( { 'frameborder': 0, 'scrolling': 'no' } );
+
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.Frame, OO.ui.Element );
+
+OO.mixinClass( OO.ui.Frame, OO.EventEmitter );
+
+/* Static Properties */
+
+OO.ui.Frame.static.tagName = 'iframe';
+
+/* Events */
+
+/**
+ * @event initialize
+ */
+
+/* Static Methods */
+
+/**
+ * Transplant the CSS styles from as parent document to a frame's document.
+ *
+ * This loops over the style sheets in the parent document, and copies their nodes to the
+ * frame's document. It then polls the document to see when all styles have loaded, and once they
+ * have, invokes the callback.
+ *
+ * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting
+ * and invoke the callback anyway. This protects against cases like a display: none; iframe in
+ * Firefox, where the styles won't load until the iframe becomes visible.
+ *
+ * For details of how we arrived at the strategy used in this function, see #load.
+ *
+ * @static
+ * @method
+ * @inheritable
+ * @param {HTMLDocument} parentDoc Document to transplant styles from
+ * @param {HTMLDocument} frameDoc Document to transplant styles to
+ * @param {Function} [callback] Callback to execute once styles have loaded
+ * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up.
+ */
+OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, callback, timeout ) {
+       var i, numSheets, styleNode, newNode, timeoutID, pollNodeId, $pendingPollNodes,
+               $pollNodes = $( [] ),
+               // Fake font-family value
+               fontFamily = 'oo-ui-frame-transplantStyles-loaded';
+
+       for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) {
+               styleNode = parentDoc.styleSheets[i].ownerNode;
+               if ( callback && styleNode.nodeName.toLowerCase() === 'link' ) {
+                       // External stylesheet
+                       // Create a node with a unique ID that we're going to monitor to see when the CSS
+                       // has loaded
+                       pollNodeId = 'oo-ui-frame-transplantStyles-loaded-' + i;
+                       $pollNodes = $pollNodes.add( $( '<div>', frameDoc )
+                               .attr( 'id', pollNodeId )
+                               .appendTo( frameDoc.body )
+                       );
+
+                       // Add <style>@import url(...); #pollNodeId { font-family: ... }</style>
+                       // The font-family rule will only take effect once the @import finishes
+                       newNode = frameDoc.createElement( 'style' );
+                       newNode.textContent = '@import url(' + styleNode.href + ');\n' +
+                               '#' + pollNodeId + ' { font-family: ' + fontFamily + '; }';
+               } else {
+                       // Not an external stylesheet, or no polling required; just copy the node over
+                       newNode = frameDoc.importNode( styleNode, true );
+               }
+               frameDoc.head.appendChild( newNode );
+       }
+
+       if ( callback ) {
+               // Poll every 100ms until all external stylesheets have loaded
+               $pendingPollNodes = $pollNodes;
+               timeoutID = setTimeout( function pollExternalStylesheets() {
+                       while (
+                               $pendingPollNodes.length > 0 &&
+                               $pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily
+                       ) {
+                               $pendingPollNodes = $pendingPollNodes.slice( 1 );
+                       }
+
+                       if ( $pendingPollNodes.length === 0 ) {
+                               // We're done!
+                               if ( timeoutID !== null ) {
+                                       timeoutID = null;
+                                       $pollNodes.remove();
+                                       callback();
+                               }
+                       } else {
+                               timeoutID = setTimeout( pollExternalStylesheets, 100 );
+                       }
+               }, 100 );
+               // ...but give up after a while
+               if ( timeout !== 0 ) {
+                       setTimeout( function () {
+                               if ( timeoutID ) {
+                                       clearTimeout( timeoutID );
+                                       timeoutID = null;
+                                       $pollNodes.remove();
+                                       callback();
+                               }
+                       }, timeout || 5000 );
+               }
+       }
+};
+
+/* Methods */
+
+/**
+ * Load the frame contents.
+ *
+ * Once the iframe's stylesheets are loaded, the `initialize` event will be emitted.
+ *
+ * Sounds simple right? Read on...
+ *
+ * When you create a dynamic iframe using open/write/close, the window.load event for the
+ * iframe is triggered when you call close, and there's no further load event to indicate that
+ * everything is actually loaded.
+ *
+ * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could
+ * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets
+ * are added to document.styleSheets immediately, and the only way you can determine whether they've
+ * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But
+ * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded.
+ *
+ * The workaround is to change all `<link href="...">` tags to `<style>@import url(...)</style>` tags.
+ * Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets until
+ * the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the `@import`
+ * has finished. And because the contents of the `<style>` tag are from the same origin, accessing
+ * .cssRules is allowed.
+ *
+ * However, now that we control the styles we're injecting, we might as well do away with
+ * browser-specific polling hacks like document.styleSheets and .cssRules, and instead inject
+ * `<style>@import url(...); #foo { font-family: someValue; }</style>`, then create `<div id="foo">`
+ * and wait for its font-family to change to someValue. Because `@import` is blocking, the font-family
+ * rule is not applied until after the `@import` finishes.
+ *
+ * All this stylesheet injection and polling magic is in #transplantStyles.
+ *
+ * @fires initialize
+ */
+OO.ui.Frame.prototype.load = function () {
+       var win = this.$element.prop( 'contentWindow' ),
+               doc = win.document,
+               frame = this;
+
+       // Figure out directionality:
+       this.dir = this.$element.closest( '[dir]' ).prop( 'dir' ) || 'ltr';
+
+       // Initialize contents
+       doc.open();
+       doc.write(
+               '<!doctype html>' +
+               '<html>' +
+                       '<body class="oo-ui-frame-body oo-ui-' + this.dir + '" style="direction:' + this.dir + ';" dir="' + this.dir + '">' +
+                               '<div class="oo-ui-frame-content"></div>' +
+                       '</body>' +
+               '</html>'
+       );
+       doc.close();
+
+       // Properties
+       this.$ = OO.ui.Element.getJQuery( doc, this );
+       this.$content = this.$( '.oo-ui-frame-content' );
+       this.$document = this.$( doc );
+
+       this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0],
+               function () {
+                       frame.initialized = true;
+                       frame.emit( 'initialize' );
+               }
+       );
+};
+
+/**
+ * Run a callback as soon as the frame has been initialized.
+ *
+ * @param {Function} callback
+ */
+OO.ui.Frame.prototype.run = function ( callback ) {
+       if ( this.initialized ) {
+               callback();
+       } else {
+               this.once( 'initialize', callback );
+       }
+};
+
+/**
+ * Sets the size of the frame.
+ *
+ * @method
+ * @param {number} width Frame width in pixels
+ * @param {number} height Frame height in pixels
+ * @chainable
+ */
+OO.ui.Frame.prototype.setSize = function ( width, height ) {
+       this.$element.css( { 'width': width, 'height': height } );
+       return this;
+};
+/**
+ * Container for elements in a child frame.
+ *
+ * There are two ways to specify a title: set the static `title` property or provide a `title`
+ * property in the configuration options. The latter will override the former.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @param {OO.ui.WindowSet} windowSet Window set this dialog is part of
+ * @param {Object} [config] Configuration options
+ * @cfg {string|Function} [title] Title string or function that returns a string
+ * @cfg {string} [icon] Symbolic name of icon
+ * @fires initialize
+ */
+OO.ui.Window = function OoUiWindow( windowSet, config ) {
+       // Parent constructor
+       OO.ui.Element.call( this, config );
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+
+       // Properties
+       this.windowSet = windowSet;
+       this.visible = false;
+       this.opening = false;
+       this.closing = false;
+       this.title = OO.ui.resolveMsg( config.title || this.constructor.static.title );
+       this.icon = config.icon || this.constructor.static.icon;
+       this.frame = new OO.ui.Frame( { '$': this.$ } );
+       this.$frame = this.$( '<div>' );
+       this.$ = function () {
+               throw new Error( 'this.$() cannot be used until the frame has been initialized.' );
+       };
+
+       // Initialization
+       this.$element
+               .addClass( 'oo-ui-window' )
+               // Hide the window using visibility: hidden; while the iframe is still loading
+               // Can't use display: none; because that prevents the iframe from loading in Firefox
+               .css( 'visibility', 'hidden' )
+               .append( this.$frame );
+       this.$frame
+               .addClass( 'oo-ui-window-frame' )
+               .append( this.frame.$element );
+
+       // Events
+       this.frame.connect( this, { 'initialize': 'initialize' } );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.Window, OO.ui.Element );
+
+OO.mixinClass( OO.ui.Window, OO.EventEmitter );
+
+/* Events */
+
+/**
+ * Initialize contents.
+ *
+ * Fired asynchronously after construction when iframe is ready.
+ *
+ * @event initialize
+ */
+
+/**
+ * Open window.
+ *
+ * Fired after window has been opened.
+ *
+ * @event open
+ * @param {Object} data Window opening data
+ */
+
+/**
+ * Close window.
+ *
+ * Fired after window has been closed.
+ *
+ * @event close
+ * @param {Object} data Window closing data
+ */
+
+/* Static Properties */
+
+/**
+ * Symbolic name of icon.
+ *
+ * @static
+ * @inheritable
+ * @property {string}
+ */
+OO.ui.Window.static.icon = 'window';
+
+/**
+ * Window title.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function} Title string or function that returns a string
+ */
+OO.ui.Window.static.title = null;
+
+/* Methods */
+
+/**
+ * Check if window is visible.
+ *
+ * @method
+ * @returns {boolean} Window is visible
+ */
+OO.ui.Window.prototype.isVisible = function () {
+       return this.visible;
+};
+
+/**
+ * Check if window is opening.
+ *
+ * @method
+ * @returns {boolean} Window is opening
+ */
+OO.ui.Window.prototype.isOpening = function () {
+       return this.opening;
+};
+
+/**
+ * Check if window is closing.
+ *
+ * @method
+ * @returns {boolean} Window is closing
+ */
+OO.ui.Window.prototype.isClosing = function () {
+       return this.closing;
+};
+
+/**
+ * Get the window frame.
+ *
+ * @method
+ * @returns {OO.ui.Frame} Frame of window
+ */
+OO.ui.Window.prototype.getFrame = function () {
+       return this.frame;
+};
+
+/**
+ * Get the window set.
+ *
+ * @method
+ * @returns {OO.ui.WindowSet} Window set
+ */
+OO.ui.Window.prototype.getWindowSet = function () {
+       return this.windowSet;
+};
+
+/**
+ * Get the window title.
+ *
+ * @returns {string} Title text
+ */
+OO.ui.Window.prototype.getTitle = function () {
+       return this.title;
+};
+
+/**
+ * Get the window icon.
+ *
+ * @returns {string} Symbolic name of icon
+ */
+OO.ui.Window.prototype.getIcon = function () {
+       return this.icon;
+};
+
+/**
+ * Set the size of window frame.
+ *
+ * @param {number} [width=auto] Custom width
+ * @param {number} [height=auto] Custom height
+ * @chainable
+ */
+OO.ui.Window.prototype.setSize = function ( width, height ) {
+       if ( !this.frame.$content ) {
+               return;
+       }
+
+       this.frame.$element.css( {
+               'width': width === undefined ? 'auto' : width,
+               'height': height === undefined ? 'auto' : height
+       } );
+
+       return this;
+};
+
+/**
+ * Set the title of the window.
+ *
+ * @param {string|Function} title Title text or a function that returns text
+ * @chainable
+ */
+OO.ui.Window.prototype.setTitle = function ( title ) {
+       this.title = OO.ui.resolveMsg( title );
+       if ( this.$title ) {
+               this.$title.text( title );
+       }
+       return this;
+};
+
+/**
+ * Set the icon of the window.
+ *
+ * @param {string} icon Symbolic name of icon
+ * @chainable
+ */
+OO.ui.Window.prototype.setIcon = function ( icon ) {
+       if ( this.$icon ) {
+               this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
+       }
+       this.icon = icon;
+       if ( this.$icon ) {
+               this.$icon.addClass( 'oo-ui-icon-' + this.icon );
+       }
+
+       return this;
+};
+
+/**
+ * Set the position of window to fit with contents..
+ *
+ * @param {string} left Left offset
+ * @param {string} top Top offset
+ * @chainable
+ */
+OO.ui.Window.prototype.setPosition = function ( left, top ) {
+       this.$element.css( { 'left': left, 'top': top } );
+       return this;
+};
+
+/**
+ * Set the height of window to fit with contents.
+ *
+ * @param {number} [min=0] Min height
+ * @param {number} [max] Max height (defaults to content's outer height)
+ * @chainable
+ */
+OO.ui.Window.prototype.fitHeightToContents = function ( min, max ) {
+       var height = this.frame.$content.outerHeight();
+
+       this.frame.$element.css(
+               'height', Math.max( min || 0, max === undefined ? height : Math.min( max, height ) )
+       );
+
+       return this;
+};
+
+/**
+ * Set the width of window to fit with contents.
+ *
+ * @param {number} [min=0] Min height
+ * @param {number} [max] Max height (defaults to content's outer width)
+ * @chainable
+ */
+OO.ui.Window.prototype.fitWidthToContents = function ( min, max ) {
+       var width = this.frame.$content.outerWidth();
+
+       this.frame.$element.css(
+               'width', Math.max( min || 0, max === undefined ? width : Math.min( max, width ) )
+       );
+
+       return this;
+};
+
+/**
+ * Initialize window contents.
+ *
+ * The first time the window is opened, #initialize is called when it's safe to begin populating
+ * its contents. See #setup for a way to make changes each time the window opens.
+ *
+ * Once this method is called, this.$$ can be used to create elements within the frame.
+ *
+ * @method
+ * @fires initialize
+ * @chainable
+ */
+OO.ui.Window.prototype.initialize = function () {
+       // Properties
+       this.$ = this.frame.$;
+       this.$title = this.$( '<div class="oo-ui-window-title"></div>' )
+               .text( this.title );
+       this.$icon = this.$( '<div class="oo-ui-window-icon"></div>' )
+               .addClass( 'oo-ui-icon-' + this.icon );
+       this.$head = this.$( '<div class="oo-ui-window-head"></div>' );
+       this.$body = this.$( '<div class="oo-ui-window-body"></div>' );
+       this.$foot = this.$( '<div class="oo-ui-window-foot"></div>' );
+       this.$overlay = this.$( '<div class="oo-ui-window-overlay"></div>' );
+
+       // Initialization
+       this.frame.$content.append(
+               this.$head.append( this.$icon, this.$title ),
+               this.$body,
+               this.$foot,
+               this.$overlay
+       );
+
+       // Undo the visibility: hidden; hack from the constructor and apply display: none;
+       // We can do this safely now that the iframe has initialized
+       this.$element.hide().css( 'visibility', '' );
+
+       this.emit( 'initialize' );
+
+       return this;
+};
+
+/**
+ * Setup window for use.
+ *
+ * Each time the window is opened, once it's ready to be interacted with, this will set it up for
+ * use in a particular context, based on the `data` argument.
+ *
+ * When you override this method, you must call the parent method at the very beginning.
+ *
+ * @method
+ * @abstract
+ * @param {Object} [data] Window opening data
+ */
+OO.ui.Window.prototype.setup = function () {
+       // Override to do something
+};
+
+/**
+ * Tear down window after use.
+ *
+ * Each time the window is closed, and it's done being interacted with, this will tear it down and
+ * do something with the user's interactions within the window, based on the `data` argument.
+ *
+ * When you override this method, you must call the parent method at the very end.
+ *
+ * @method
+ * @abstract
+ * @param {Object} [data] Window closing data
+ */
+OO.ui.Window.prototype.teardown = function () {
+       // Override to do something
+};
+
+/**
+ * Open window.
+ *
+ * Do not override this method. See #setup for a way to make changes each time the window opens.
+ *
+ * @method
+ * @param {Object} [data] Window opening data
+ * @fires open
+ * @chainable
+ */
+OO.ui.Window.prototype.open = function ( data ) {
+       if ( !this.opening && !this.closing && !this.visible ) {
+               this.opening = true;
+               this.frame.run( OO.ui.bind( function () {
+                       this.$element.show();
+                       this.visible = true;
+                       this.frame.$element.focus();
+                       this.emit( 'opening', data );
+                       this.setup( data );
+                       this.emit( 'open', data );
+                       this.opening = false;
+               }, this ) );
+       }
+
+       return this;
+};
+
+/**
+ * Close window.
+ *
+ * See #teardown for a way to do something each time the window closes.
+ *
+ * @method
+ * @param {Object} [data] Window closing data
+ * @fires close
+ * @chainable
+ */
+OO.ui.Window.prototype.close = function ( data ) {
+       if ( !this.opening && !this.closing && this.visible ) {
+               this.frame.$content.find( ':focus' ).blur();
+               this.closing = true;
+               this.$element.hide();
+               this.visible = false;
+               this.emit( 'closing', data );
+               this.teardown( data );
+               this.emit( 'close', data );
+               this.closing = false;
+       }
+
+       return this;
+};
+/**
+ * Set of mutually exclusive windows.
+ *
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @param {OO.Factory} factory Window factory
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.WindowSet = function OoUiWindowSet( factory, config ) {
+       // Parent constructor
+       OO.ui.Element.call( this, config );
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+
+       // Properties
+       this.factory = factory;
+       this.windows = {};
+       this.currentWindow = null;
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-windowSet' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.WindowSet, OO.ui.Element );
+
+OO.mixinClass( OO.ui.WindowSet, OO.EventEmitter );
+
+/* Events */
+
+/**
+ * @event opening
+ * @param {OO.ui.Window} win Window that's being opened
+ * @param {Object} config Window opening information
+ */
+
+/**
+ * @event open
+ * @param {OO.ui.Window} win Window that's been opened
+ * @param {Object} config Window opening information
+ */
+
+/**
+ * @event closing
+ * @param {OO.ui.Window} win Window that's being closed
+ * @param {Object} config Window closing information
+ */
+
+/**
+ * @event close
+ * @param {OO.ui.Window} win Window that's been closed
+ * @param {Object} config Window closing information
+ */
+
+/* Methods */
+
+/**
+ * Handle a window that's being opened.
+ *
+ * @method
+ * @param {OO.ui.Window} win Window that's being opened
+ * @param {Object} [config] Window opening information
+ * @fires opening
+ */
+OO.ui.WindowSet.prototype.onWindowOpening = function ( win, config ) {
+       if ( this.currentWindow && this.currentWindow !== win ) {
+               this.currentWindow.close();
+       }
+       this.currentWindow = win;
+       this.emit( 'opening', win, config );
+};
+
+/**
+ * Handle a window that's been opened.
+ *
+ * @method
+ * @param {OO.ui.Window} win Window that's been opened
+ * @param {Object} [config] Window opening information
+ * @fires open
+ */
+OO.ui.WindowSet.prototype.onWindowOpen = function ( win, config ) {
+       this.emit( 'open', win, config );
+};
+
+/**
+ * Handle a window that's being closed.
+ *
+ * @method
+ * @param {OO.ui.Window} win Window that's being closed
+ * @param {Object} [config] Window closing information
+ * @fires closing
+ */
+OO.ui.WindowSet.prototype.onWindowClosing = function ( win, config ) {
+       this.currentWindow = null;
+       this.emit( 'closing', win, config );
+};
+
+/**
+ * Handle a window that's been closed.
+ *
+ * @method
+ * @param {OO.ui.Window} win Window that's been closed
+ * @param {Object} [config] Window closing information
+ * @fires close
+ */
+OO.ui.WindowSet.prototype.onWindowClose = function ( win, config ) {
+       this.emit( 'close', win, config );
+};
+
+/**
+ * Get the current window.
+ *
+ * @method
+ * @returns {OO.ui.Window} Current window
+ */
+OO.ui.WindowSet.prototype.getCurrentWindow = function () {
+       return this.currentWindow;
+};
+
+/**
+ * Return a given window.
+ *
+ * @param {string} name Symbolic name of window
+ * @return {OO.ui.Window} Window with specified name
+ */
+OO.ui.WindowSet.prototype.getWindow = function ( name ) {
+       var win;
+
+       if ( !this.factory.lookup( name ) ) {
+               throw new Error( 'Unknown window: ' + name );
+       }
+       if ( !( name in this.windows ) ) {
+               win = this.windows[name] = this.factory.create( name, this, { '$': this.$ } );
+               win.connect( this, {
+                       'opening': [ 'onWindowOpening', win ],
+                       'open': [ 'onWindowOpen', win ],
+                       'closing': [ 'onWindowClosing', win ],
+                       'close': [ 'onWindowClose', win ]
+               } );
+               this.$element.append( win.$element );
+               win.getFrame().load();
+       }
+       return this.windows[name];
+};
+/**
+ * Modal dialog box.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.Window
+ *
+ * @constructor
+ * @param {OO.ui.WindowSet} windowSet Window set this dialog is part of
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [footless] Hide foot
+ * @cfg {boolean} [small] Make the dialog small
+ */
+OO.ui.Dialog = function OoUiDialog( windowSet, config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Window.call( this, windowSet, config );
+
+       // Properties
+       this.visible = false;
+       this.footless = !!config.footless;
+       this.small = !!config.small;
+       this.onWindowMouseWheelHandler = OO.ui.bind( this.onWindowMouseWheel, this );
+       this.onDocumentKeyDownHandler = OO.ui.bind( this.onDocumentKeyDown, this );
+
+       // Events
+       this.$element.on( 'mousedown', false );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-dialog' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
+
+/* Static Properties */
+
+/**
+ * Symbolic name of dialog.
+ *
+ * @abstract
+ * @static
+ * @property {string}
+ * @inheritable
+ */
+OO.ui.Dialog.static.name = '';
+
+/* Methods */
+
+/**
+ * Handle close button click events.
+ *
+ * @method
+ */
+OO.ui.Dialog.prototype.onCloseButtonClick = function () {
+       this.close( { 'action': 'cancel' } );
+};
+
+/**
+ * Handle window mouse wheel events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse wheel event
+ */
+OO.ui.Dialog.prototype.onWindowMouseWheel = function () {
+       return false;
+};
+
+/**
+ * Handle document key down events.
+ *
+ * @method
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
+       switch ( e.which ) {
+               case OO.ui.Keys.PAGEUP:
+               case OO.ui.Keys.PAGEDOWN:
+               case OO.ui.Keys.END:
+               case OO.ui.Keys.HOME:
+               case OO.ui.Keys.LEFT:
+               case OO.ui.Keys.UP:
+               case OO.ui.Keys.RIGHT:
+               case OO.ui.Keys.DOWN:
+                       // Prevent any key events that might cause scrolling
+                       return false;
+       }
+};
+
+/**
+ * Handle frame document key down events.
+ *
+ * @method
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.Dialog.prototype.onFrameDocumentKeyDown = function ( e ) {
+       if ( e.which === OO.ui.Keys.ESCAPE ) {
+               this.close( { 'action': 'cancel' } );
+               return false;
+       }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.Dialog.prototype.initialize = function () {
+       // Parent method
+       OO.ui.Window.prototype.initialize.call( this );
+
+       // Properties
+       this.closeButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'frameless': true,
+               'icon': 'close',
+               'title': OO.ui.msg( 'ooui-dialog-action-close' )
+       } );
+
+       // Events
+       this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } );
+       this.frame.$document.on( 'keydown', OO.ui.bind( this.onFrameDocumentKeyDown, this ) );
+
+       // Initialization
+       this.frame.$content.addClass( 'oo-ui-dialog-content' );
+       if ( this.footless ) {
+               this.frame.$content.addClass( 'oo-ui-dialog-content-footless' );
+       }
+       if ( this.small ) {
+               this.$frame.addClass( 'oo-ui-window-frame-small' );
+       }
+       this.closeButton.$element.addClass( 'oo-ui-window-closeButton' );
+       this.$head.append( this.closeButton.$element );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.Dialog.prototype.setup = function ( data ) {
+       // Parent method
+       OO.ui.Window.prototype.setup.call( this, data );
+
+       // Prevent scrolling in top-level window
+       this.$( window ).on( 'mousewheel', this.onWindowMouseWheelHandler );
+       this.$( document ).on( 'keydown', this.onDocumentKeyDownHandler );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.Dialog.prototype.teardown = function ( data ) {
+       // Parent method
+       OO.ui.Window.prototype.teardown.call( this, data );
+
+       // Allow scrolling in top-level window
+       this.$( window ).off( 'mousewheel', this.onWindowMouseWheelHandler );
+       this.$( document ).off( 'keydown', this.onDocumentKeyDownHandler );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.Dialog.prototype.close = function ( data ) {
+       if ( !this.opening && !this.closing && this.visible ) {
+               // Trigger transition
+               this.$element.addClass( 'oo-ui-dialog-closing' );
+               // Allow transition to complete before actually closing
+               setTimeout( OO.ui.bind( function () {
+                       this.$element.removeClass( 'oo-ui-dialog-closing' );
+                       // Parent method
+                       OO.ui.Window.prototype.close.call( this, data );
+               }, this ), 250 );
+       }
+};
+/**
+ * Container for elements.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.Element
+ * @mixin OO.EventEmitter
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.Layout = function OoUiLayout( config ) {
+       // Initialize config
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Element.call( this, config );
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-layout' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.Layout, OO.ui.Element );
+
+OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
+/**
+ * User interface control.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.Element
+ * @mixin OO.EventEmitter
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [disabled=false] Disable
+ */
+OO.ui.Widget = function OoUiWidget( config ) {
+       // Initialize config
+       config = $.extend( { 'disabled': false }, config );
+
+       // Parent constructor
+       OO.ui.Element.call( this, config );
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+
+       // Properties
+       this.disabled = config.disabled;
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-widget' );
+       this.setDisabled( this.disabled );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.Widget, OO.ui.Element );
+
+OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
+
+/* Methods */
+
+/**
+ * Check if the widget is disabled.
+ *
+ * @method
+ * @param {boolean} Button is disabled
+ */
+OO.ui.Widget.prototype.isDisabled = function () {
+       return this.disabled;
+};
+
+/**
+ * Set the disabled state of the widget.
+ *
+ * This should probably change the widgets's appearance and prevent it from being used.
+ *
+ * @method
+ * @param {boolean} disabled Disable button
+ * @chainable
+ */
+OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
+       this.disabled = !!disabled;
+       if ( this.disabled ) {
+               this.$element.addClass( 'oo-ui-widget-disabled' );
+       } else {
+               this.$element.removeClass( 'oo-ui-widget-disabled' );
+       }
+       return this;
+};
+/**
+ * Element with a button.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {jQuery} $button Button node, assigned to #$button
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [frameless] Render button without a frame
+ * @cfg {number} [tabIndex] Button's tab index
+ */
+OO.ui.ButtonedElement = function OoUiButtonedElement( $button, config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Properties
+       this.$button = $button;
+       this.tabIndex = null;
+       this.active = false;
+       this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
+
+       // Events
+       this.$button.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-buttonedElement' );
+       this.$button
+               .addClass( 'oo-ui-buttonedElement-button' )
+               .attr( 'role', 'button' )
+               .prop( 'tabIndex', config.tabIndex || 0 );
+       if ( config.frameless ) {
+               this.$element.addClass( 'oo-ui-buttonedElement-frameless' );
+       } else {
+               this.$element.addClass( 'oo-ui-buttonedElement-framed' );
+       }
+};
+
+/* Methods */
+
+/**
+ * Handles mouse down events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.ButtonedElement.prototype.onMouseDown = function () {
+       this.tabIndex = this.$button.attr( 'tabIndex' );
+       // Remove the tab-index while the button is down to prevent the button from stealing focus
+       this.$button.removeAttr( 'tabIndex' );
+       this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
+};
+
+/**
+ * Handles mouse up events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse up event
+ */
+OO.ui.ButtonedElement.prototype.onMouseUp = function () {
+       // Restore the tab-index after the button is up to restore the button's accesssibility
+       this.$button.attr( 'tabIndex', this.tabIndex );
+       this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
+};
+
+/**
+ * Set active state.
+ *
+ * @method
+ * @param {boolean} [value] Make button active
+ * @chainable
+ */
+OO.ui.ButtonedElement.prototype.setActive = function ( value ) {
+       this.$element.toggleClass( 'oo-ui-buttonedElement-active', !!value );
+       return this;
+};
+/**
+ * Element that can be automatically clipped to visible boundaies.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {jQuery} $clippable Nodes to clip, assigned to #$clippable
+ */
+OO.ui.ClippableElement = function OoUiClippableElement( $clippable ) {
+       // Properties
+       this.$clippable = $clippable;
+       this.clipping = false;
+       this.clipped = false;
+       this.$clippableContainer = null;
+       this.$clippableScroller = null;
+       this.$clippableWindow = null;
+       this.idealWidth = null;
+       this.idealHeight = null;
+       this.onClippableContainerScrollHandler = OO.ui.bind( this.clip, this );
+       this.onClippableWindowResizeHandler = OO.ui.bind( this.clip, this );
+
+       // Initialization
+       this.$clippable.addClass( 'oo-ui-clippableElement-clippable' );
+};
+
+/* Methods */
+
+/**
+ * Set clipping.
+ *
+ * @method
+ * @param {boolean} value Enable clipping
+ * @chainable
+ */
+OO.ui.ClippableElement.prototype.setClipping = function ( value ) {
+       value = !!value;
+
+       if ( this.clipping !== value ) {
+               this.clipping = value;
+               if ( this.clipping ) {
+                       this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() );
+                       // If the clippable container is the body, we have to listen to scroll events and check
+                       // jQuery.scrollTop on the window because of browser inconsistencies
+                       this.$clippableScroller = this.$clippableContainer.is( 'body' ) ?
+                               this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) :
+                               this.$clippableContainer;
+                       this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
+                       this.$clippableWindow = this.$( this.getElementWindow() )
+                               .on( 'resize', this.onClippableWindowResizeHandler );
+                       // Initial clip after visible
+                       setTimeout( OO.ui.bind( this.clip, this ) );
+               } else {
+                       this.$clippableContainer = null;
+                       this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
+                       this.$clippableScroller = null;
+                       this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
+                       this.$clippableWindow = null;
+               }
+       }
+
+       return this;
+};
+
+/**
+ * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
+ *
+ * @method
+ * @return {boolean} Element will be clipped to the visible area
+ */
+OO.ui.ClippableElement.prototype.isClipping = function () {
+       return this.clipping;
+};
+
+/**
+ * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
+ *
+ * @method
+ * @return {boolean} Part of the element is being clipped
+ */
+OO.ui.ClippableElement.prototype.isClipped = function () {
+       return this.clipped;
+};
+
+/**
+ * Set the ideal size.
+ *
+ * @method
+ * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
+ * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
+ */
+OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
+       this.idealWidth = width;
+       this.idealHeight = height;
+};
+
+/**
+ * Clip element to visible boundaries and allow scrolling when needed.
+ *
+ * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
+ * overlapped by, the visible area of the nearest scrollable container.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.ClippableElement.prototype.clip = function () {
+       if ( !this.clipping ) {
+               // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
+               return this;
+       }
+
+       var buffer = 10,
+               cOffset = this.$clippable.offset(),
+               ccOffset = this.$clippableContainer.offset() || { 'top': 0, 'left': 0 },
+               ccHeight = this.$clippableContainer.innerHeight() - buffer,
+               ccWidth = this.$clippableContainer.innerWidth() - buffer,
+               scrollTop = this.$clippableScroller.scrollTop(),
+               scrollLeft = this.$clippableScroller.scrollLeft(),
+               desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
+               desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
+               naturalWidth = this.$clippable.prop( 'scrollWidth' ),
+               naturalHeight = this.$clippable.prop( 'scrollHeight' ),
+               clipWidth = desiredWidth < naturalWidth,
+               clipHeight = desiredHeight < naturalHeight;
+
+       if ( clipWidth ) {
+               this.$clippable.css( { 'overflow-x': 'auto', 'width': desiredWidth } );
+       } else {
+               this.$clippable.css( { 'overflow-x': '', 'width': this.idealWidth || '' } );
+       }
+       if ( clipHeight ) {
+               this.$clippable.css( { 'overflow-y': 'auto', 'height': desiredHeight } );
+       } else {
+               this.$clippable.css( { 'overflow-y': '', 'height': this.idealHeight || '' } );
+       }
+
+       this.clipped = clipWidth || clipHeight;
+
+       return this;
+};
+/**
+ * Element with named flags, used for styling, that can be added, removed and listed and checked.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive'
+ */
+OO.ui.FlaggableElement = function OoUiFlaggableElement( config ) {
+       // Config initialization
+       config = config || {};
+
+       // Properties
+       this.flags = {};
+
+       // Initialization
+       this.setFlags( config.flags );
+};
+
+/* Methods */
+
+/**
+ * Check if a flag is set.
+ *
+ * @method
+ * @param {string} flag Flag name to check
+ * @returns {boolean} Has flag
+ */
+OO.ui.FlaggableElement.prototype.hasFlag = function ( flag ) {
+       return flag in this.flags;
+};
+
+/**
+ * Get the names of all flags.
+ *
+ * @method
+ * @returns {string[]} flags Flag names
+ */
+OO.ui.FlaggableElement.prototype.getFlags = function () {
+       return Object.keys( this.flags );
+};
+
+/**
+ * Add one or more flags.
+ *
+ * @method
+ * @param {string[]|Object.<string, boolean>} flags List of flags to add, or list of set/remove
+ *  values, keyed by flag name
+ * @chainable
+ */
+OO.ui.FlaggableElement.prototype.setFlags = function ( flags ) {
+       var i, len, flag,
+               classPrefix = 'oo-ui-flaggableElement-';
+
+       if ( Array.isArray( flags ) ) {
+               for ( i = 0, len = flags.length; i < len; i++ ) {
+                       flag = flags[i];
+                       // Set
+                       this.flags[flag] = true;
+                       this.$element.addClass( classPrefix + flag );
+               }
+       } else if ( OO.isPlainObject( flags ) ) {
+               for ( flag in flags ) {
+                       if ( flags[flags] ) {
+                               // Set
+                               this.flags[flag] = true;
+                               this.$element.addClass( classPrefix + flag );
+                       } else {
+                               // Remove
+                               delete this.flags[flag];
+                               this.$element.removeClass( classPrefix + flag );
+                       }
+               }
+       }
+       return this;
+};
+/**
+ * Element containing a sequence of child elements.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {jQuery} $group Container node, assigned to #$group
+ * @param {Object} [config] Configuration options
+ * @cfg {Object.<string,string>} [aggregations] Events to aggregate, keyed by item event name
+ */
+OO.ui.GroupElement = function OoUiGroupElement( $group, config ) {
+       // Configuration
+       config = config || {};
+
+       // Properties
+       this.$group = $group;
+       this.items = [];
+       this.$items = this.$( [] );
+       this.aggregate = !$.isEmptyObject( config.aggregations );
+       this.aggregations = config.aggregations || {};
+};
+
+/* Methods */
+
+/**
+ * Get items.
+ *
+ * @method
+ * @returns {OO.ui.Element[]} Items
+ */
+OO.ui.GroupElement.prototype.getItems = function () {
+       return this.items.slice( 0 );
+};
+
+/**
+ * Add items.
+ *
+ * @method
+ * @param {OO.ui.Element[]} items Item
+ * @param {number} [index] Index to insert items at
+ * @chainable
+ */
+OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
+       var i, len, item, event, events, currentIndex,
+               $items = this.$( [] );
+
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+
+               // Check if item exists then remove it first, effectively "moving" it
+               currentIndex = this.items.indexOf( item );
+               if ( currentIndex >= 0 ) {
+                       this.removeItems( [ item ] );
+                       // Adjust index to compensate for removal
+                       if ( currentIndex < index ) {
+                               index--;
+                       }
+               }
+               // Add the item
+               if ( this.aggregate ) {
+                       events = {};
+                       for ( event in this.aggregations ) {
+                               events[event] = [ 'emit', this.aggregations[event], item ];
+                       }
+                       item.connect( this, events );
+               }
+               $items = $items.add( item.$element );
+       }
+
+       if ( index === undefined || index < 0 || index >= this.items.length ) {
+               this.$group.append( $items );
+               this.items.push.apply( this.items, items );
+       } else if ( index === 0 ) {
+               this.$group.prepend( $items );
+               this.items.unshift.apply( this.items, items );
+       } else {
+               this.$items.eq( index ).before( $items );
+               this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
+       }
+
+       this.$items = this.$items.add( $items );
+
+       return this;
+};
+
+/**
+ * Remove items.
+ *
+ * Items will be detached, not removed, so they can be used later.
+ *
+ * @method
+ * @param {OO.ui.Element[]} items Items to remove
+ * @chainable
+ */
+OO.ui.GroupElement.prototype.removeItems = function ( items ) {
+       var i, len, item, index;
+
+       // Remove specific items
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+               index = this.items.indexOf( item );
+               if ( index !== -1 ) {
+                       if ( this.aggregate ) {
+                               item.disconnect( this );
+                       }
+                       this.items.splice( index, 1 );
+                       item.$element.detach();
+                       this.$items = this.$items.not( item.$element );
+               }
+       }
+
+       return this;
+};
+
+/**
+ * Clear all items.
+ *
+ * Items will be detached, not removed, so they can be used later.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.GroupElement.prototype.clearItems = function () {
+       var i, len, item;
+
+       // Remove all items
+       if ( this.aggregate ) {
+               for ( i = 0, len = this.items.length; i < len; i++ ) {
+                       item.disconnect( this );
+               }
+       }
+       this.items = [];
+       this.$items.detach();
+       this.$items = this.$( [] );
+};
+/**
+ * Element containing an icon.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {jQuery} $icon Icon node, assigned to #$icon
+ * @param {Object} [config] Configuration options
+ * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID;
+ *  use the 'default' key to specify the icon to be used when there is no icon in the user's
+ *  language
+ */
+OO.ui.IconedElement = function OoUiIconedElement( $icon, config ) {
+       // Config intialization
+       config = config || {};
+
+       // Properties
+       this.$icon = $icon;
+       this.icon = null;
+
+       // Initialization
+       this.$icon.addClass( 'oo-ui-iconedElement-icon' );
+       this.setIcon( config.icon || this.constructor.static.icon );
+};
+
+/* Static Properties */
+
+OO.ui.IconedElement.static = {};
+
+/**
+ * Icon.
+ *
+ * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
+ *
+ * For i18n purposes, this property can be an object containing a `default` icon name property and
+ * additional icon names keyed by language code.
+ *
+ * Example of i18n icon definition:
+ *     { 'default': 'bold-a', 'en': 'bold-b', 'de': 'bold-f' }
+ *
+ * @static
+ * @inheritable
+ * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID;
+ *  use the 'default' key to specify the icon to be used when there is no icon in the user's
+ *  language
+ */
+OO.ui.IconedElement.static.icon = null;
+
+/* Methods */
+
+/**
+ * Set icon.
+ *
+ * @method
+ * @param {Object|string} icon Symbolic icon name, or map of icon names keyed by language ID;
+ *  use the 'default' key to specify the icon to be used when there is no icon in the user's
+ *  language
+ * @chainable
+ */
+OO.ui.IconedElement.prototype.setIcon = function ( icon ) {
+       icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
+
+       if ( this.icon ) {
+               this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
+       }
+       if ( typeof icon === 'string' ) {
+               icon = icon.trim();
+               if ( icon.length ) {
+                       this.$icon.addClass( 'oo-ui-icon-' + icon );
+                       this.icon = icon;
+               }
+       }
+       this.$element.toggleClass( 'oo-ui-iconedElement', !!this.icon );
+
+       return this;
+};
+
+/**
+ * Get icon.
+ *
+ * @method
+ * @returns {string} Icon
+ */
+OO.ui.IconedElement.prototype.getIcon = function () {
+       return this.icon;
+};
+/**
+ * Element containing an indicator.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {jQuery} $indicator Indicator node, assigned to #$indicator
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [indicator] Symbolic indicator name
+ * @cfg {string} [indicatorTitle] Indicator title text or a function that return text
+ */
+OO.ui.IndicatedElement = function OoUiIndicatedElement( $indicator, config ) {
+       // Config intialization
+       config = config || {};
+
+       // Properties
+       this.$indicator = $indicator;
+       this.indicator = null;
+       this.indicatorLabel = null;
+
+       // Initialization
+       this.$indicator.addClass( 'oo-ui-indicatedElement-indicator' );
+       this.setIndicator( config.indicator || this.constructor.static.indicator );
+       this.setIndicatorTitle( config.indicatorTitle  || this.constructor.static.indicatorTitle );
+};
+
+/* Static Properties */
+
+OO.ui.IndicatedElement.static = {};
+
+/**
+ * indicator.
+ *
+ * @static
+ * @inheritable
+ * @property {string|null} Symbolic indicator name or null for no indicator
+ */
+OO.ui.IndicatedElement.static.indicator = null;
+
+/**
+ * Indicator title.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function|null} Indicator title text, a function that return text or null for no
+ *  indicator title
+ */
+OO.ui.IndicatedElement.static.indicatorTitle = null;
+
+/* Methods */
+
+/**
+ * Set indicator.
+ *
+ * @method
+ * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
+ * @chainable
+ */
+OO.ui.IndicatedElement.prototype.setIndicator = function ( indicator ) {
+       if ( this.indicator ) {
+               this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
+               this.indicator = null;
+       }
+       if ( typeof indicator === 'string' ) {
+               indicator = indicator.trim();
+               if ( indicator.length ) {
+                       this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
+                       this.indicator = indicator;
+               }
+       }
+       this.$element.toggleClass( 'oo-ui-indicatedElement', !!this.indicator );
+
+       return this;
+};
+
+/**
+ * Set indicator label.
+ *
+ * @method
+ * @param {string|Function|null} indicator Indicator title text, a function that return text or null
+ *  for no indicator title
+ * @chainable
+ */
+OO.ui.IndicatedElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
+       this.indicatorTitle = indicatorTitle = OO.ui.resolveMsg( indicatorTitle );
+
+       if ( typeof indicatorTitle === 'string' && indicatorTitle.length ) {
+               this.$indicator.attr( 'title', indicatorTitle );
+       } else {
+               this.$indicator.removeAttr( 'title' );
+       }
+
+       return this;
+};
+
+/**
+ * Get indicator.
+ *
+ * @method
+ * @returns {string} title Symbolic name of indicator
+ */
+OO.ui.IndicatedElement.prototype.getIndicator = function () {
+       return this.indicator;
+};
+
+/**
+ * Get indicator title.
+ *
+ * @method
+ * @returns {string} Indicator title text
+ */
+OO.ui.IndicatedElement.prototype.getIndicatorTitle = function () {
+       return this.indicatorTitle;
+};
+/**
+ * Element containing a label.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {jQuery} $label Label node, assigned to #$label
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
+ */
+OO.ui.LabeledElement = function OoUiLabeledElement( $label, config ) {
+       // Config intialization
+       config = config || {};
+
+       // Properties
+       this.$label = $label;
+       this.label = null;
+
+       // Initialization
+       this.$label.addClass( 'oo-ui-labeledElement-label' );
+       this.setLabel( config.label || this.constructor.static.label );
+};
+
+/* Static Properties */
+
+OO.ui.LabeledElement.static = {};
+
+/**
+ * Label.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function|null} Label text; a function that returns a nodes or text; or null for
+ *  no label
+ */
+OO.ui.LabeledElement.static.label = null;
+
+/* Methods */
+
+/**
+ * Set the label.
+ *
+ * @method
+ * @param {jQuery|string|Function|null} label Label nodes; text; a function that retuns nodes or
+ *  text; or null for no label
+ * @chainable
+ */
+OO.ui.LabeledElement.prototype.setLabel = function ( label ) {
+       var empty = false;
+
+       this.label = label = OO.ui.resolveMsg( label ) || null;
+       if ( typeof label === 'string' && label.trim() ) {
+               this.$label.text( label );
+       } else if ( label instanceof jQuery ) {
+               this.$label.empty().append( label );
+       } else {
+               this.$label.empty();
+               empty = true;
+       }
+       this.$element.toggleClass( 'oo-ui-labeledElement', !empty );
+       this.$label.css( 'display', empty ? 'none' : '' );
+
+       return this;
+};
+
+/**
+ * Get the label.
+ *
+ * @method
+ * @returns {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
+ *  text; or null for no label
+ */
+OO.ui.LabeledElement.prototype.getLabel = function () {
+       return this.label;
+};
+
+/**
+ * Fit the label.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.LabeledElement.prototype.fitLabel = function () {
+       if ( this.$label.autoEllipsis ) {
+               this.$label.autoEllipsis( { 'hasSpan': false, 'tooltip': true } );
+       }
+       return this;
+};
+/**
+ * Popuppable element.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {number} [popupWidth=320] Width of popup
+ * @cfg {number} [popupHeight] Height of popup
+ * @cfg {Object} [popup] Configuration to pass to popup
+ */
+OO.ui.PopuppableElement = function OoUiPopuppableElement( config ) {
+       // Configuration initialization
+       config = $.extend( { 'popupWidth': 320 }, config );
+
+       // Properties
+       this.popup = new OO.ui.PopupWidget( $.extend(
+               { 'align': 'center', 'autoClose': true },
+               config.popup,
+               { '$': this.$, '$autoCloseIgnore': this.$element }
+       ) );
+       this.popupWidth = config.popupWidth;
+       this.popupHeight = config.popupHeight;
+};
+
+/* Methods */
+
+/**
+ * Get popup.
+ *
+ * @method
+ * @returns {OO.ui.PopupWidget} Popup widget
+ */
+OO.ui.PopuppableElement.prototype.getPopup = function () {
+       return this.popup;
+};
+
+/**
+ * Show popup.
+ *
+ * @method
+ */
+OO.ui.PopuppableElement.prototype.showPopup = function () {
+       this.popup.show().display( this.popupWidth, this.popupHeight );
+};
+
+/**
+ * Hide popup.
+ *
+ * @method
+ */
+OO.ui.PopuppableElement.prototype.hidePopup = function () {
+       this.popup.hide();
+};
+/**
+ * Element with a title.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {jQuery} $label Titled node, assigned to #$titled
+ * @param {Object} [config] Configuration options
+ * @cfg {string|Function} [title] Title text or a function that returns text
+ */
+OO.ui.TitledElement = function OoUiTitledElement( $titled, config ) {
+       // Config intialization
+       config = config || {};
+
+       // Properties
+       this.$titled = $titled;
+       this.title = null;
+
+       // Initialization
+       this.setTitle( config.title || this.constructor.static.title );
+};
+
+/* Static Properties */
+
+OO.ui.TitledElement.static = {};
+
+/**
+ * Title.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function} Title text or a function that returns text
+ */
+OO.ui.TitledElement.static.title = null;
+
+/* Methods */
+
+/**
+ * Set title.
+ *
+ * @method
+ * @param {string|Function|null} title Title text, a function that returns text or null for no title
+ * @chainable
+ */
+OO.ui.TitledElement.prototype.setTitle = function ( title ) {
+       this.title = title = OO.ui.resolveMsg( title ) || null;
+
+       if ( typeof title === 'string' && title.length ) {
+               this.$titled.attr( 'title', title );
+       } else {
+               this.$titled.removeAttr( 'title' );
+       }
+
+       return this;
+};
+
+/**
+ * Get title.
+ *
+ * @method
+ * @returns {string} Title string
+ */
+OO.ui.TitledElement.prototype.getTitle = function () {
+       return this.title;
+};
+/**
+ * Generic toolbar tool.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.IconedElement
+ *
+ * @constructor
+ * @param {OO.ui.ToolGroup} toolGroup
+ * @param {Object} [config] Configuration options
+ * @cfg {string|Function} [title] Title text or a function that returns text
+ */
+OO.ui.Tool = function OoUiTool( toolGroup, config ) {
+       // Config intialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Widget.call( this, config );
+
+       // Mixin constructors
+       OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
+
+       // Properties
+       this.toolGroup = toolGroup;
+       this.toolbar = this.toolGroup.getToolbar();
+       this.active = false;
+       this.$title = this.$( '<span>' );
+       this.$link = this.$( '<a>' );
+       this.title = null;
+
+       // Events
+       this.toolbar.connect( this, { 'updateState': 'onUpdateState' } );
+
+       // Initialization
+       this.$title.addClass( 'oo-ui-tool-title' );
+       this.$link
+               .addClass( 'oo-ui-tool-link' )
+               .append( this.$icon, this.$title );
+       this.$element
+               .data( 'oo-ui-tool', this )
+               .addClass(
+                       'oo-ui-tool ' + 'oo-ui-tool-name-' +
+                       this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
+               )
+               .append( this.$link );
+       this.setTitle( config.title || this.constructor.static.title );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
+
+OO.mixinClass( OO.ui.Tool, OO.ui.IconedElement );
+
+/* Events */
+
+/**
+ * @event select
+ */
+
+/* Static Properties */
+
+OO.ui.Tool.static.tagName = 'span';
+
+/**
+ * Symbolic name of tool.
+ *
+ * @abstract
+ * @static
+ * @property {string}
+ * @inheritable
+ */
+OO.ui.Tool.static.name = '';
+
+/**
+ * Tool group.
+ *
+ * @abstract
+ * @static
+ * @property {string}
+ * @inheritable
+ */
+OO.ui.Tool.static.group = '';
+
+/**
+ * Tool title.
+ *
+ * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
+ * is part of a list or menu tool group. If a trigger is associated with an action by the same name
+ * as the tool, a description of its keyboard shortcut for the appropriate platform will be
+ * appended to the title if the tool is part of a bar tool group.
+ *
+ * @abstract
+ * @static
+ * @property {string|Function} Title text or a function that returns text
+ * @inheritable
+ */
+OO.ui.Tool.static.title = '';
+
+/**
+ * Tool can be automatically added to tool groups.
+ *
+ * @static
+ * @property {boolean}
+ * @inheritable
+ */
+OO.ui.Tool.static.autoAdd = true;
+
+/**
+ * Check if this tool is compatible with given data.
+ *
+ * @method
+ * @static
+ * @inheritable
+ * @param {Mixed} data Data to check
+ * @returns {boolean} Tool can be used with data
+ */
+OO.ui.Tool.static.isCompatibleWith = function () {
+       return false;
+};
+
+/* Methods */
+
+/**
+ * Handle the toolbar state being updated.
+ *
+ * This is an abstract method that must be overridden in a concrete subclass.
+ *
+ * @abstract
+ * @method
+ */
+OO.ui.Tool.prototype.onUpdateState = function () {
+       throw new Error(
+               'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
+       );
+};
+
+/**
+ * Handle the tool being selected.
+ *
+ * This is an abstract method that must be overridden in a concrete subclass.
+ *
+ * @abstract
+ * @method
+ */
+OO.ui.Tool.prototype.onSelect = function () {
+       throw new Error(
+               'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
+       );
+};
+
+/**
+ * Check if the button is active.
+ *
+ * @method
+ * @param {boolean} Button is active
+ */
+OO.ui.Tool.prototype.isActive = function () {
+       return this.active;
+};
+
+/**
+ * Make the button appear active or inactive.
+ *
+ * @method
+ * @param {boolean} state Make button appear active
+ */
+OO.ui.Tool.prototype.setActive = function ( state ) {
+       this.active = !!state;
+       if ( this.active ) {
+               this.$element.addClass( 'oo-ui-tool-active' );
+       } else {
+               this.$element.removeClass( 'oo-ui-tool-active' );
+       }
+};
+
+/**
+ * Get the tool title.
+ *
+ * @method
+ * @param {string|Function} title Title text or a function that returns text
+ * @chainable
+ */
+OO.ui.Tool.prototype.setTitle = function ( title ) {
+       this.title = OO.ui.resolveMsg( title );
+       this.updateTitle();
+       return this;
+};
+
+/**
+ * Get the tool title.
+ *
+ * @method
+ * @returns {string} Title text
+ */
+OO.ui.Tool.prototype.getTitle = function () {
+       return this.title;
+};
+
+/**
+ * Get the tool's symbolic name.
+ *
+ * @method
+ * @returns {string} Symbolic name of tool
+ */
+OO.ui.Tool.prototype.getName = function () {
+       return this.constructor.static.name;
+};
+
+/**
+ * Update the title.
+ *
+ * @method
+ */
+OO.ui.Tool.prototype.updateTitle = function () {
+       var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
+               accelTooltips = this.toolGroup.constructor.static.accelTooltips,
+               accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
+               tooltipParts = [];
+
+       this.$title.empty()
+               .text( this.title )
+               .append(
+                       this.$( '<span>' )
+                               .addClass( 'oo-ui-tool-accel' )
+                               .text( accel )
+               );
+
+       if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
+               tooltipParts.push( this.title );
+       }
+       if ( accelTooltips && typeof accel === 'string' && accel.length ) {
+               tooltipParts.push( accel );
+       }
+       if ( tooltipParts.length ) {
+               this.$link.attr( 'title', tooltipParts.join( ' ' ) );
+       } else {
+               this.$link.removeAttr( 'title' );
+       }
+};
+
+/**
+ * Destroy tool.
+ *
+ * @method
+ */
+OO.ui.Tool.prototype.destroy = function () {
+       this.toolbar.disconnect( this );
+       this.$element.remove();
+};
+/**
+ * Collection of tool groups.
+ *
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
+ * @mixins OO.ui.GroupElement
+ *
+ * @constructor
+ * @param {OO.Factory} toolFactory Factory for creating tools
+ * @param {Object} [options] Configuration options
+ * @cfg {boolean} [actions] Add an actions section opposite to the tools
+ * @cfg {boolean} [shadow] Add a shadow below the toolbar
+ */
+OO.ui.Toolbar = function OoUiToolbar( toolFactory, options ) {
+       // Configuration initialization
+       options = options || {};
+
+       // Parent constructor
+       OO.ui.Element.call( this, options );
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+       OO.ui.GroupElement.call( this, this.$( '<div>' ) );
+
+       // Properties
+       this.toolFactory = toolFactory;
+       this.groups = [];
+       this.tools = {};
+       this.$bar = this.$( '<div>' );
+       this.$actions = this.$( '<div>' );
+       this.initialized = false;
+
+       // Events
+       this.$element
+               .add( this.$bar ).add( this.$group ).add( this.$actions )
+               .on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
+
+       // Initialization
+       this.$group.addClass( 'oo-ui-toolbar-tools' );
+       this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group );
+       if ( options.actions ) {
+               this.$actions.addClass( 'oo-ui-toolbar-actions' );
+               this.$bar.append( this.$actions );
+       }
+       this.$bar.append( '<div style="clear:both"></div>' );
+       if ( options.shadow ) {
+               this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
+       }
+       this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
+
+OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
+OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
+
+/* Methods */
+
+/**
+ * Get the tool factory.
+ *
+ * @method
+ * @returns {OO.Factory} Tool factory
+ */
+OO.ui.Toolbar.prototype.getToolFactory = function () {
+       return this.toolFactory;
+};
+
+/**
+ * Handles mouse down events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.Toolbar.prototype.onMouseDown = function ( e ) {
+       var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ),
+               $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
+       if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[0] === $closestWidgetToToolbar[0] ) {
+               return false;
+       }
+};
+
+/**
+ * Sets up handles and preloads required information for the toolbar to work.
+ * This must be called immediately after it is attached to a visible document.
+ */
+OO.ui.Toolbar.prototype.initialize = function () {
+       this.initialized = true;
+};
+
+/**
+ * Setup toolbar.
+ *
+ * Tools can be specified in the following ways:
+ *  - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
+ *  - All tools in a group: `{ 'group': 'group-name' }`
+ *  - All tools: `'*'` - Using this will make the group a list with a "More" label by default
+ *
+ * @method
+ * @param {Object.<string,Array>} groups List of tool group configurations
+ * @param {Array|string} [groups.include] Tools to include
+ * @param {Array|string} [groups.exclude] Tools to exclude
+ * @param {Array|string} [groups.promote] Tools to promote to the beginning
+ * @param {Array|string} [groups.demote] Tools to demote to the end
+ */
+OO.ui.Toolbar.prototype.setup = function ( groups ) {
+       var i, len, type, group,
+               items = [],
+               // TODO: Use a registry instead
+               defaultType = 'bar',
+               constructors = {
+                       'bar': OO.ui.BarToolGroup,
+                       'list': OO.ui.ListToolGroup,
+                       'menu': OO.ui.MenuToolGroup
+               };
+
+       // Cleanup previous groups
+       this.reset();
+
+       // Build out new groups
+       for ( i = 0, len = groups.length; i < len; i++ ) {
+               group = groups[i];
+               if ( group.include === '*' ) {
+                       // Apply defaults to catch-all groups
+                       if ( group.type === undefined ) {
+                               group.type = 'list';
+                       }
+                       if ( group.label === undefined ) {
+                               group.label = 'ooui-toolbar-more';
+                       }
+               }
+               type = constructors[group.type] ? group.type : defaultType;
+               items.push(
+                       new constructors[type]( this, $.extend( { '$': this.$ }, group ) )
+               );
+       }
+       this.addItems( items );
+};
+
+/**
+ * Remove all tools and groups from the toolbar.
+ */
+OO.ui.Toolbar.prototype.reset = function () {
+       var i, len;
+
+       this.groups = [];
+       this.tools = {};
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               this.items[i].destroy();
+       }
+       this.clearItems();
+};
+
+/**
+ * Destroys toolbar, removing event handlers and DOM elements.
+ *
+ * Call this whenever you are done using a toolbar.
+ */
+OO.ui.Toolbar.prototype.destroy = function () {
+       this.reset();
+       this.$element.remove();
+};
+
+/**
+ * Check if tool has not been used yet.
+ *
+ * @param {string} name Symbolic name of tool
+ * @return {boolean} Tool is available
+ */
+OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
+       return !this.tools[name];
+};
+
+/**
+ * Prevent tool from being used again.
+ *
+ * @param {OO.ui.Tool} tool Tool to reserve
+ */
+OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
+       this.tools[tool.getName()] = tool;
+};
+
+/**
+ * Allow tool to be used again.
+ *
+ * @param {OO.ui.Tool} tool Tool to release
+ */
+OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
+       delete this.tools[tool.getName()];
+};
+
+/**
+ * Get accelerator label for tool.
+ *
+ * This is a stub that should be overridden to provide access to accelerator information.
+ *
+ * @param {string} name Symbolic name of tool
+ * @returns {string|undefined} Tool accelerator label if available
+ */
+OO.ui.Toolbar.prototype.getToolAccelerator = function () {
+       return undefined;
+};
+/**
+ * Factory for tools.
+ *
+ * @class
+ * @extends OO.Factory
+ * @constructor
+ */
+OO.ui.ToolFactory = function OoUiToolFactory() {
+       // Parent constructor
+       OO.Factory.call( this );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
+
+/* Methods */
+
+OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
+       var i, len, included, promoted, demoted,
+               auto = [],
+               used = {};
+
+       // Collect included and not excluded tools
+       included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
+
+       // Promotion
+       promoted = this.extract( promote, used );
+       demoted = this.extract( demote, used );
+
+       // Auto
+       for ( i = 0, len = included.length; i < len; i++ ) {
+               if ( !used[included[i]] ) {
+                       auto.push( included[i] );
+               }
+       }
+
+       return promoted.concat( auto ).concat( demoted );
+};
+
+/**
+ * Get a flat list of names from a list of names or groups.
+ *
+ * Tools can be specified in the following ways:
+ *  - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
+ *  - All tools in a group: `{ 'group': 'group-name' }`
+ *  - All tools: `'*'`
+ *
+ * @private
+ * @param {Array|string} collection List of tools
+ * @param {Object} [used] Object with names that should be skipped as properties; extracted
+ *   names will be added as properties
+ * @return {string[]} List of extracted names
+ */
+OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
+       var i, len, item, name, tool,
+               names = [];
+
+       if ( collection === '*' ) {
+               for ( name in this.registry ) {
+                       tool = this.registry[name];
+                       if (
+                               // Only add tools by group name when auto-add is enabled
+                               tool.static.autoAdd &&
+                               // Exclude already used tools
+                               ( !used || !used[name] )
+                       ) {
+                               names.push( name );
+                               if ( used ) {
+                                       used[name] = true;
+                               }
+                       }
+               }
+       } else if ( Array.isArray( collection ) ) {
+               for ( i = 0, len = collection.length; i < len; i++ ) {
+                       item = collection[i];
+                       // Allow plain strings as shorthand for named tools
+                       if ( typeof item === 'string' ) {
+                               item = { 'name': item };
+                       }
+                       if ( OO.isPlainObject( item ) ) {
+                               if ( item.group ) {
+                                       for ( name in this.registry ) {
+                                               tool = this.registry[name];
+                                               if (
+                                                       // Include tools with matching group
+                                                       tool.static.group === item.group &&
+                                                       // Only add tools by group name when auto-add is enabled
+                                                       tool.static.autoAdd &&
+                                                       // Exclude already used tools
+                                                       ( !used || !used[name] )
+                                               ) {
+                                                       names.push( name );
+                                                       if ( used ) {
+                                                               used[name] = true;
+                                                       }
+                                               }
+                                       }
+                               }
+                               // Include tools with matching name and exclude already used tools
+                               else if ( item.name && ( !used || !used[item.name] ) ) {
+                                       names.push( item.name );
+                                       if ( used ) {
+                                               used[item.name] = true;
+                                       }
+                               }
+                       }
+               }
+       }
+       return names;
+};
+/**
+ * Collection of tools.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.GroupElement
+ *
+ * Tools can be specified in the following ways:
+ *  - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
+ *  - All tools in a group: `{ 'group': 'group-name' }`
+ *  - All tools: `'*'`
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ * @cfg {Array|string} [include=[]] List of tools to include
+ * @cfg {Array|string} [exclude=[]] List of tools to exclude
+ * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
+ * @cfg {Array|string} [demote=[]] List of tools to demote to the end
+ */
+OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Widget.call( this, config );
+
+       // Mixin constructors
+       OO.ui.GroupElement.call( this, this.$( '<div>' ) );
+
+       // Properties
+       this.toolbar = toolbar;
+       this.tools = {};
+       this.pressed = null;
+       this.include = config.include || [];
+       this.exclude = config.exclude || [];
+       this.promote = config.promote || [];
+       this.demote = config.demote || [];
+       this.onCapturedMouseUpHandler = OO.ui.bind( this.onCapturedMouseUp, this );
+
+       // Events
+       this.$element.on( {
+               'mousedown': OO.ui.bind( this.onMouseDown, this ),
+               'mouseup': OO.ui.bind( this.onMouseUp, this ),
+               'mouseover': OO.ui.bind( this.onMouseOver, this ),
+               'mouseout': OO.ui.bind( this.onMouseOut, this )
+       } );
+       this.toolbar.getToolFactory().connect( this, { 'register': 'onToolFactoryRegister' } );
+
+       // Initialization
+       this.$group.addClass( 'oo-ui-toolGroup-tools' );
+       this.$element
+               .addClass( 'oo-ui-toolGroup' )
+               .append( this.$group );
+       this.populate();
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
+
+OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
+
+/* Events */
+
+/**
+ * @event update
+ */
+
+/* Static Properties */
+
+/**
+ * Show labels in tooltips.
+ *
+ * @static
+ * @property {boolean}
+ * @inheritable
+ */
+OO.ui.ToolGroup.static.titleTooltips = false;
+
+/**
+ * Show acceleration labels in tooltips.
+ *
+ * @static
+ * @property {boolean}
+ * @inheritable
+ */
+OO.ui.ToolGroup.static.accelTooltips = false;
+
+/* Methods */
+
+/**
+ * Handle mouse down events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.ToolGroup.prototype.onMouseDown = function ( e ) {
+       if ( !this.disabled && e.which === 1 ) {
+               this.pressed = this.getTargetTool( e );
+               if ( this.pressed ) {
+                       this.pressed.setActive( true );
+                       this.getElementDocument().addEventListener(
+                               'mouseup', this.onCapturedMouseUpHandler, true
+                       );
+                       return false;
+               }
+       }
+};
+
+/**
+ * Handle captured mouse up events.
+ *
+ * @method
+ * @param {Event} e Mouse up event
+ */
+OO.ui.ToolGroup.prototype.onCapturedMouseUp = function ( e ) {
+       this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler, true );
+       // onMouseUp may be called a second time, depending on where the mouse is when the button is
+       // released, but since `this.pressed` will no longer be true, the second call will be ignored.
+       this.onMouseUp( e );
+};
+
+/**
+ * Handle mouse up events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse up event
+ */
+OO.ui.ToolGroup.prototype.onMouseUp = function ( e ) {
+       var tool = this.getTargetTool( e );
+
+       if ( !this.disabled && e.which === 1 && this.pressed && this.pressed === tool ) {
+               this.pressed.onSelect();
+       }
+
+       this.pressed = null;
+       return false;
+};
+
+/**
+ * Handle mouse over events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse over event
+ */
+OO.ui.ToolGroup.prototype.onMouseOver = function ( e ) {
+       var tool = this.getTargetTool( e );
+
+       if ( this.pressed && this.pressed === tool ) {
+               this.pressed.setActive( true );
+       }
+};
+
+/**
+ * Handle mouse out events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse out event
+ */
+OO.ui.ToolGroup.prototype.onMouseOut = function ( e ) {
+       var tool = this.getTargetTool( e );
+
+       if ( this.pressed && this.pressed === tool ) {
+               this.pressed.setActive( false );
+       }
+};
+
+/**
+ * Get the closest tool to a jQuery.Event.
+ *
+ * Only tool links are considered, which prevents other elements in the tool such as popups from
+ * triggering tool group interactions.
+ *
+ * @method
+ * @private
+ * @param {jQuery.Event} e
+ * @returns {OO.ui.Tool|null} Tool, `null` if none was found
+ */
+OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
+       var tool,
+               $item = this.$( e.target ).closest( '.oo-ui-tool-link' );
+
+       if ( $item.length ) {
+               tool = $item.parent().data( 'oo-ui-tool' );
+       }
+
+       return tool && !tool.isDisabled() ? tool : null;
+};
+
+/**
+ * Handle tool registry register events.
+ *
+ * If a tool is registered after the group is created, we must repopulate the list to account for:
+ * - a tool being added that may be included
+ * - a tool already included being overridden
+ *
+ * @param {string} name Symbolic name of tool
+ */
+OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
+       this.populate();
+};
+
+/**
+ * Get the toolbar this group is in.
+ *
+ * @return {OO.ui.Toolbar} Toolbar of group
+ */
+OO.ui.ToolGroup.prototype.getToolbar = function () {
+       return this.toolbar;
+};
+
+/**
+ * Add and remove tools based on configuration.
+ *
+ * @method
+ */
+OO.ui.ToolGroup.prototype.populate = function () {
+       var i, len, name, tool,
+               toolFactory = this.toolbar.getToolFactory(),
+               names = {},
+               add = [],
+               remove = [],
+               list = this.toolbar.getToolFactory().getTools(
+                       this.include, this.exclude, this.promote, this.demote
+               );
+
+       // Build a list of needed tools
+       for ( i = 0, len = list.length; i < len; i++ ) {
+               name = list[i];
+               if (
+                       // Tool exists
+                       toolFactory.lookup( name ) &&
+                       // Tool is available or is already in this group
+                       ( this.toolbar.isToolAvailable( name ) || this.tools[name] )
+               ) {
+                       tool = this.tools[name];
+                       if ( !tool ) {
+                               // Auto-initialize tools on first use
+                               this.tools[name] = tool = toolFactory.create( name, this );
+                               tool.updateTitle();
+                       }
+                       this.toolbar.reserveTool( tool );
+                       add.push( tool );
+                       names[name] = true;
+               }
+       }
+       // Remove tools that are no longer needed
+       for ( name in this.tools ) {
+               if ( !names[name] ) {
+                       this.tools[name].destroy();
+                       this.toolbar.releaseTool( this.tools[name] );
+                       remove.push( this.tools[name] );
+                       delete this.tools[name];
+               }
+       }
+       if ( remove.length ) {
+               this.removeItems( remove );
+       }
+       // Update emptiness state
+       if ( add.length ) {
+               this.$element.removeClass( 'oo-ui-toolGroup-empty' );
+       } else {
+               this.$element.addClass( 'oo-ui-toolGroup-empty' );
+       }
+       // Re-add tools (moving existing ones to new locations)
+       this.addItems( add );
+};
+
+/**
+ * Destroy tool group.
+ *
+ * @method
+ */
+OO.ui.ToolGroup.prototype.destroy = function () {
+       var name;
+
+       this.clearItems();
+       this.toolbar.getToolFactory().disconnect( this );
+       for ( name in this.tools ) {
+               this.toolbar.releaseTool( this.tools[name] );
+               this.tools[name].disconnect( this ).destroy();
+               delete this.tools[name];
+       }
+       this.$element.remove();
+};
+/**
+ * Layout made of a fieldset and optional legend.
+ *
+ * @class
+ * @extends OO.ui.Layout
+ * @mixins OO.ui.LabeledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [icon] Symbolic icon name
+ */
+OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
+       // Config initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Layout.call( this, config );
+
+       // Mixin constructors
+       OO.ui.LabeledElement.call( this, this.$( '<legend>' ), config );
+
+       // Initialization
+       if ( config.icon ) {
+               this.$element.addClass( 'oo-ui-fieldsetLayout-decorated' );
+               this.$label.addClass( 'oo-ui-icon-' + config.icon );
+       }
+       this.$element.addClass( 'oo-ui-fieldsetLayout' );
+       if ( config.icon || config.label ) {
+               this.$element
+                       .addClass( 'oo-ui-fieldsetLayout-labeled' )
+                       .append( this.$label );
+       }
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
+
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabeledElement );
+
+/* Static Properties */
+
+OO.ui.FieldsetLayout.static.tagName = 'fieldset';
+/**
+ * Layout made of proportionally sized columns and rows.
+ *
+ * @class
+ * @extends OO.ui.Layout
+ *
+ * @constructor
+ * @param {OO.ui.PanelLayout[]} panels Panels in the grid
+ * @param {Object} [config] Configuration options
+ * @cfg {number[]} [widths] Widths of columns as ratios
+ * @cfg {number[]} [heights] Heights of columns as ratios
+ */
+OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
+       var i, len, widths;
+
+       // Config initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Layout.call( this, config );
+
+       // Properties
+       this.panels = [];
+       this.widths = [];
+       this.heights = [];
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-gridLayout' );
+       for ( i = 0, len = panels.length; i < len; i++ ) {
+               this.panels.push( panels[i] );
+               this.$element.append( panels[i].$element );
+       }
+       if ( config.widths || config.heights ) {
+               this.layout( config.widths || [1], config.heights || [1] );
+       } else {
+               // Arrange in columns by default
+               widths = [];
+               for ( i = 0, len = this.panels.length; i < len; i++ ) {
+                       widths[i] = 1;
+               }
+               this.layout( widths, [1] );
+       }
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
+
+/* Events */
+
+/**
+ * @event layout
+ */
+
+/**
+ * @event update
+ */
+
+/* Static Properties */
+
+OO.ui.GridLayout.static.tagName = 'div';
+
+/* Methods */
+
+/**
+ * Set grid dimensions.
+ *
+ * @method
+ * @param {number[]} widths Widths of columns as ratios
+ * @param {number[]} heights Heights of rows as ratios
+ * @fires layout
+ * @throws {Error} If grid is not large enough to fit all panels
+ */
+OO.ui.GridLayout.prototype.layout = function ( widths, heights ) {
+       var x, y,
+               xd = 0,
+               yd = 0,
+               cols = widths.length,
+               rows = heights.length;
+
+       // Verify grid is big enough to fit panels
+       if ( cols * rows < this.panels.length ) {
+               throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' );
+       }
+
+       // Sum up denominators
+       for ( x = 0; x < cols; x++ ) {
+               xd += widths[x];
+       }
+       for ( y = 0; y < rows; y++ ) {
+               yd += heights[y];
+       }
+       // Store factors
+       this.widths = [];
+       this.heights = [];
+       for ( x = 0; x < cols; x++ ) {
+               this.widths[x] = widths[x] / xd;
+       }
+       for ( y = 0; y < rows; y++ ) {
+               this.heights[y] = heights[y] / yd;
+       }
+       // Synchronize view
+       this.update();
+       this.emit( 'layout' );
+};
+
+/**
+ * Update panel positions and sizes.
+ *
+ * @method
+ * @fires update
+ */
+OO.ui.GridLayout.prototype.update = function () {
+       var x, y, panel,
+               i = 0,
+               left = 0,
+               top = 0,
+               dimensions,
+               width = 0,
+               height = 0,
+               cols = this.widths.length,
+               rows = this.heights.length;
+
+       for ( y = 0; y < rows; y++ ) {
+               for ( x = 0; x < cols; x++ ) {
+                       panel = this.panels[i];
+                       width = this.widths[x];
+                       height = this.heights[y];
+                       dimensions = {
+                               'width': Math.round( width * 100 ) + '%',
+                               'height': Math.round( height * 100 ) + '%',
+                               'top': Math.round( top * 100 ) + '%'
+                       };
+                       // If RTL, reverse:
+                       if ( OO.ui.Element.getDir( this.$.context ) === 'rtl' ) {
+                               dimensions.right = Math.round( left * 100 ) + '%';
+                       } else {
+                               dimensions.left = Math.round( left * 100 ) + '%';
+                       }
+                       panel.$element.css( dimensions );
+                       i++;
+                       left += width;
+               }
+               top += height;
+               left = 0;
+       }
+
+       this.emit( 'update' );
+};
+
+/**
+ * Get a panel at a given position.
+ *
+ * The x and y position is affected by the current grid layout.
+ *
+ * @method
+ * @param {number} x Horizontal position
+ * @param {number} y Vertical position
+ * @returns {OO.ui.PanelLayout} The panel at the given postion
+ */
+OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
+       return this.panels[( x * this.widths.length ) + y];
+};
+/**
+ * Layout containing a series of pages.
+ *
+ * @class
+ * @extends OO.ui.Layout
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [continuous=false] Show all pages, one after another
+ * @cfg {boolean} [autoFocus=false] Focus on the first focusable element when changing to a page
+ * @cfg {boolean} [outlined=false] Show an outline
+ * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
+ * @cfg {Object[]} [adders] List of adders for controls, each with name, icon and title properties
+ */
+OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
+       // Initialize configuration
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Layout.call( this, config );
+
+       // Properties
+       this.currentPageName = null;
+       this.pages = {};
+       this.ignoreFocus = false;
+       this.stackLayout = new OO.ui.StackLayout( { '$': this.$, 'continuous': !!config.continuous } );
+       this.autoFocus = !!config.autoFocus;
+       this.outlined = !!config.outlined;
+       if ( this.outlined ) {
+               this.editable = !!config.editable;
+               this.adders = config.adders || null;
+               this.outlineControlsWidget = null;
+               this.outlineWidget = new OO.ui.OutlineWidget( { '$': this.$ } );
+               this.outlinePanel = new OO.ui.PanelLayout( { '$': this.$, 'scrollable': true } );
+               this.gridLayout = new OO.ui.GridLayout(
+                       [this.outlinePanel, this.stackLayout], { '$': this.$, 'widths': [1, 2] }
+               );
+               if ( this.editable ) {
+                       this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
+                               this.outlineWidget,
+                               { '$': this.$, 'adders': this.adders }
+                       );
+               }
+       }
+
+       // Events
+       this.stackLayout.connect( this, { 'set': 'onStackLayoutSet' } );
+       if ( this.outlined ) {
+               this.outlineWidget.connect( this, { 'select': 'onOutlineWidgetSelect' } );
+               // Event 'focus' does not bubble, but 'focusin' does
+               this.stackLayout.onDOMEvent( 'focusin', OO.ui.bind( this.onStackLayoutFocus, this ) );
+       }
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-bookletLayout' );
+       this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
+       if ( this.outlined ) {
+               this.outlinePanel.$element
+                       .addClass( 'oo-ui-bookletLayout-outlinePanel' )
+                       .append( this.outlineWidget.$element );
+               if ( this.editable ) {
+                       this.outlinePanel.$element
+                               .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
+                               .append( this.outlineControlsWidget.$element );
+               }
+               this.$element.append( this.gridLayout.$element );
+       } else {
+               this.$element.append( this.stackLayout.$element );
+       }
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.BookletLayout, OO.ui.Layout );
+
+/* Events */
+
+/**
+ * @event set
+ * @param {OO.ui.PageLayout} page Current page
+ */
+
+/**
+ * @event add
+ * @param {OO.ui.PageLayout[]} page Added pages
+ * @param {number} index Index pages were added at
+ */
+
+/**
+ * @event remove
+ * @param {OO.ui.PageLayout[]} pages Removed pages
+ */
+
+/* Methods */
+
+/**
+ * Handle stack layout focus.
+ *
+ * @method
+ * @param {jQuery.Event} e Focusin event
+ */
+OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
+       var name, $target;
+
+       if ( this.ignoreFocus ) {
+               // Avoid recursion from programmatic focus trigger in #onStackLayoutSet
+               return;
+       }
+
+       $target = $( e.target ).closest( '.oo-ui-pageLayout' );
+       for ( name in this.pages ) {
+               if ( this.pages[ name ].$element[0] === $target[0] ) {
+                       this.setPage( name );
+                       break;
+               }
+       }
+};
+
+/**
+ * Handle stack layout set events.
+ *
+ * @method
+ * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
+ */
+OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
+       if ( page ) {
+               page.scrollElementIntoView( { 'complete': OO.ui.bind( function () {
+                       this.ignoreFocus = true;
+                       if ( this.autoFocus ) {
+                               page.$element.find( ':input:first' ).focus();
+                       }
+                       this.ignoreFocus = false;
+               }, this ) } );
+       }
+};
+
+/**
+ * Handle outline widget select events.
+ *
+ * @method
+ * @param {OO.ui.OptionWidget|null} item Selected item
+ */
+OO.ui.BookletLayout.prototype.onOutlineWidgetSelect = function ( item ) {
+       if ( item ) {
+               this.setPage( item.getData() );
+       }
+};
+
+/**
+ * Check if booklet has an outline.
+ *
+ * @method
+ * @returns {boolean} Booklet is outlined
+ */
+OO.ui.BookletLayout.prototype.isOutlined = function () {
+       return this.outlined;
+};
+
+/**
+ * Check if booklet has editing controls.
+ *
+ * @method
+ * @returns {boolean} Booklet is outlined
+ */
+OO.ui.BookletLayout.prototype.isEditable = function () {
+       return this.editable;
+};
+
+/**
+ * Get the outline widget.
+ *
+ * @method
+ * @returns {OO.ui.OutlineWidget|null} Outline widget, or null if boolet has no outline
+ */
+OO.ui.BookletLayout.prototype.getOutline = function () {
+       return this.outlineWidget;
+};
+
+/**
+ * Get the outline controls widget. If the outline is not editable, null is returned.
+ *
+ * @method
+ * @returns {OO.ui.OutlineControlsWidget|null} The outline controls widget.
+ */
+OO.ui.BookletLayout.prototype.getOutlineControls = function () {
+       return this.outlineControlsWidget;
+};
+
+/**
+ * Get a page by name.
+ *
+ * @method
+ * @param {string} name Symbolic name of page
+ * @returns {OO.ui.PageLayout|undefined} Page, if found
+ */
+OO.ui.BookletLayout.prototype.getPage = function ( name ) {
+       return this.pages[name];
+};
+
+/**
+ * Get the current page name.
+ *
+ * @method
+ * @returns {string|null} Current page name
+ */
+OO.ui.BookletLayout.prototype.getPageName = function () {
+       return this.currentPageName;
+};
+
+/**
+ * Add a page to the layout.
+ *
+ * When pages are added with the same names as existing pages, the existing pages will be
+ * automatically removed before the new pages are added.
+ *
+ * @method
+ * @param {OO.ui.PageLayout[]} pages Pages to add
+ * @param {number} index Index to insert pages after
+ * @fires add
+ * @chainable
+ */
+OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
+       var i, len, name, page,
+               items = [],
+               remove = [];
+
+       for ( i = 0, len = pages.length; i < len; i++ ) {
+               page = pages[i];
+               name = page.getName();
+               if ( name in this.pages ) {
+                       // Remove page with same name
+                       remove.push( this.pages[name] );
+               }
+               this.pages[page.getName()] = page;
+               if ( this.outlined ) {
+                       items.push( new OO.ui.BookletOutlineItemWidget( name, page, { '$': this.$ } ) );
+               }
+       }
+       if ( remove.length ) {
+               this.removePages( remove );
+       }
+
+       if ( this.outlined && items.length ) {
+               this.outlineWidget.addItems( items, index );
+               this.updateOutlineWidget();
+       }
+       this.stackLayout.addItems( pages, index );
+       this.emit( 'add', pages, index );
+
+       return this;
+};
+
+/**
+ * Remove a page from the layout.
+ *
+ * @method
+ * @fires remove
+ * @chainable
+ */
+OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
+       var i, len, name, page,
+               items = [];
+
+       for ( i = 0, len = pages.length; i < len; i++ ) {
+               page = pages[i];
+               name = page.getName();
+               delete this.pages[name];
+               if ( this.outlined ) {
+                       items.push( this.outlineWidget.getItemFromData( name ) );
+               }
+       }
+       if ( this.outlined && items.length ) {
+               this.outlineWidget.removeItems( items );
+               this.updateOutlineWidget();
+       }
+       this.stackLayout.removeItems( pages );
+       this.emit( 'remove', pages );
+
+       return this;
+};
+
+/**
+ * Clear all pages from the layout.
+ *
+ * @method
+ * @fires remove
+ * @chainable
+ */
+OO.ui.BookletLayout.prototype.clearPages = function () {
+       var pages = this.stackLayout.getItems();
+
+       this.pages = {};
+       this.currentPageName = null;
+       if ( this.outlined ) {
+               this.outlineWidget.clearItems();
+       }
+       this.stackLayout.clearItems();
+
+       this.emit( 'remove', pages );
+
+       return this;
+};
+
+/**
+ * Set the current page by name.
+ *
+ * @method
+ * @fires set
+ * @param {string} name Symbolic name of page
+ */
+OO.ui.BookletLayout.prototype.setPage = function ( name ) {
+       var selectedItem,
+               page = this.pages[name];
+
+       if ( this.outlined ) {
+               selectedItem = this.outlineWidget.getSelectedItem();
+               if ( selectedItem && selectedItem.getData() !== name ) {
+                       this.outlineWidget.selectItem( this.outlineWidget.getItemFromData( name ) );
+               }
+       }
+
+       if ( page ) {
+               this.currentPageName = name;
+               this.stackLayout.setItem( page );
+               this.emit( 'set', page );
+       }
+};
+
+/**
+ * Call this after adding or removing items from the OutlineWidget.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.BookletLayout.prototype.updateOutlineWidget = function () {
+       // Auto-select first item when nothing is selected anymore
+       if ( !this.outlineWidget.getSelectedItem() ) {
+               this.outlineWidget.selectItem( this.outlineWidget.getFirstSelectableItem() );
+       }
+
+       return this;
+};
+/**
+ * Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
+ *
+ * @class
+ * @extends OO.ui.Layout
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [scrollable] Allow vertical scrolling
+ * @cfg {boolean} [padded] Pad the content from the edges
+ */
+OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
+       // Config initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Layout.call( this, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-panelLayout' );
+       if ( config.scrollable ) {
+               this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
+       }
+
+       if ( config.padded ) {
+               this.$element.addClass( 'oo-ui-panelLayout-padded' );
+       }
+
+       // Add directionality class:
+       this.$element.addClass( 'oo-ui-' + OO.ui.Element.getDir( this.$.context ) );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
+/**
+ * Page within an OO.ui.BookletLayout.
+ *
+ * @class
+ * @extends OO.ui.PanelLayout
+ *
+ * @constructor
+ * @param {string} name Unique symbolic name of page
+ * @param {Object} [config] Configuration options
+ * @param {string} [icon=''] Symbolic name of icon to display in outline
+ * @param {string} [indicator=''] Symbolic name of indicator to display in outline
+ * @param {string} [indicatorTitle=''] Description of indicator meaning to display in outline
+ * @param {string} [label=''] Label to display in outline
+ * @param {number} [level=0] Indentation level of item in outline
+ * @param {boolean} [movable=false] Page should be movable using outline controls
+ */
+OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
+       // Configuration initialization
+       config = $.extend( { 'scrollable': true }, config );
+
+       // Parent constructor
+       OO.ui.PanelLayout.call( this, config );
+
+       // Properties
+       this.name = name;
+       this.icon = config.icon || '';
+       this.indicator = config.indicator || '';
+       this.indicatorTitle = OO.ui.resolveMsg( config.indicatorTitle ) || '';
+       this.label = OO.ui.resolveMsg( config.label ) || '';
+       this.level = config.level || 0;
+       this.movable = !!config.movable;
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-pageLayout' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
+
+/* Methods */
+
+/**
+ * Get page name.
+ *
+ * @returns {string} Symbolic name of page
+ */
+OO.ui.PageLayout.prototype.getName = function () {
+       return this.name;
+};
+
+/**
+ * Get page icon.
+ *
+ * @returns {string} Symbolic name of icon
+ */
+OO.ui.PageLayout.prototype.getIcon = function () {
+       return this.icon;
+};
+
+/**
+ * Get page indicator.
+ *
+ * @returns {string} Symbolic name of indicator
+ */
+OO.ui.PageLayout.prototype.getIndicator = function () {
+       return this.indicator;
+};
+
+/**
+ * Get page indicator label.
+ *
+ * @returns {string} Description of indicator meaning
+ */
+OO.ui.PageLayout.prototype.getIndicatorTitle = function () {
+       return this.indicatorTitle;
+};
+
+/**
+ * Get page label.
+ *
+ * @returns {string} Label text
+ */
+OO.ui.PageLayout.prototype.getLabel = function () {
+       return this.label;
+};
+
+/**
+ * Get outline item indentation level.
+ *
+ * @returns {number} Indentation level
+ */
+OO.ui.PageLayout.prototype.getLevel = function () {
+       return this.level;
+};
+
+/**
+ * Check if page is movable using outline controls.
+ *
+ * @returns {boolean} Page is movable
+ */
+OO.ui.PageLayout.prototype.isMovable = function () {
+       return this.movable;
+};
+/**
+ * Layout containing a series of mutually exclusive pages.
+ *
+ * @class
+ * @extends OO.ui.PanelLayout
+ * @mixins OO.ui.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [continuous=false] Show all pages, one after another
+ * @cfg {string} [icon=''] Symbolic icon name
+ */
+OO.ui.StackLayout = function OoUiStackLayout( config ) {
+       // Config initialization
+       config = $.extend( { 'scrollable': true }, config );
+
+       // Parent constructor
+       OO.ui.PanelLayout.call( this, config );
+
+       // Mixin constructors
+       OO.ui.GroupElement.call( this, this.$element, config );
+
+       // Properties
+       this.currentItem = null;
+       this.continuous = !!config.continuous;
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-stackLayout' );
+       if ( this.continuous ) {
+               this.$element.addClass( 'oo-ui-stackLayout-continuous' );
+       }
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
+
+OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement );
+
+/* Events */
+
+/**
+ * @event set
+ * @param {OO.ui.PanelLayout|null} [item] Current item
+ */
+
+/* Methods */
+
+/**
+ * Add items.
+ *
+ * Adding an existing item (by value) will move it.
+ *
+ * @method
+ * @param {OO.ui.PanelLayout[]} items Items to add
+ * @param {number} [index] Index to insert items after
+ * @chainable
+ */
+OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
+       OO.ui.GroupElement.prototype.addItems.call( this, items, index );
+
+       if ( !this.currentItem && items.length ) {
+               this.setItem( items[0] );
+       }
+
+       return this;
+};
+
+/**
+ * Remove items.
+ *
+ * Items will be detached, not removed, so they can be used later.
+ *
+ * @method
+ * @param {OO.ui.PanelLayout[]} items Items to remove
+ * @chainable
+ */
+OO.ui.StackLayout.prototype.removeItems = function ( items ) {
+       OO.ui.GroupElement.prototype.removeItems.call( this, items );
+       if ( items.indexOf( this.currentItem ) !== -1 ) {
+               this.currentItem = null;
+               if ( !this.currentItem && this.items.length ) {
+                       this.setItem( this.items[0] );
+               }
+       }
+
+       return this;
+};
+
+/**
+ * Clear all items.
+ *
+ * Items will be detached, not removed, so they can be used later.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.StackLayout.prototype.clearItems = function () {
+       this.currentItem = null;
+       OO.ui.GroupElement.prototype.clearItems.call( this );
+
+       return this;
+};
+
+/**
+ * Show item.
+ *
+ * Any currently shown item will be hidden.
+ *
+ * @method
+ * @param {OO.ui.PanelLayout} item Item to show
+ * @chainable
+ */
+OO.ui.StackLayout.prototype.setItem = function ( item ) {
+       if ( !this.continuous ) {
+               this.$items.css( 'display', '' );
+       }
+       if ( this.items.indexOf( item ) !== -1 ) {
+               if ( !this.continuous ) {
+                       item.$element.css( 'display', 'block' );
+               }
+       } else {
+               item = null;
+       }
+       this.currentItem = item;
+       this.emit( 'set', item );
+
+       return this;
+};
+/**
+ * Horizontal bar layout of tools as icon buttons.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.ToolGroup
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
+       // Parent constructor
+       OO.ui.ToolGroup.call( this, toolbar, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-barToolGroup' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
+
+/* Static Properties */
+
+OO.ui.BarToolGroup.static.titleTooltips = true;
+
+OO.ui.BarToolGroup.static.accelTooltips = true;
+/**
+ * Popup list of tools with an icon and optional label.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.ToolGroup
+ * @mixins OO.ui.IconedElement
+ * @mixins OO.ui.IndicatedElement
+ * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.TitledElement
+ * @mixins OO.ui.ClippableElement
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.ToolGroup.call( this, toolbar, config );
+
+       // Mixin constructors
+       OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
+       OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
+       OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
+       OO.ui.TitledElement.call( this, this.$element, config );
+       OO.ui.ClippableElement.call( this, this.$group );
+
+       // Properties
+       this.active = false;
+       this.dragging = false;
+       this.onBlurHandler = OO.ui.bind( this.onBlur, this );
+       this.$handle = this.$( '<span>' );
+
+       // Events
+       this.$handle.on( {
+               'mousedown': OO.ui.bind( this.onHandleMouseDown, this ),
+               'mouseup': OO.ui.bind( this.onHandleMouseUp, this )
+       } );
+
+       // Initialization
+       this.$handle
+               .addClass( 'oo-ui-popupToolGroup-handle' )
+               .append( this.$icon, this.$label, this.$indicator );
+       this.$element
+               .addClass( 'oo-ui-popupToolGroup' )
+               .prepend( this.$handle );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
+
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatedElement );
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement );
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement );
+
+/* Static Properties */
+
+/* Methods */
+
+/**
+ * Handle focus being lost.
+ *
+ * The event is actually generated from a mouseup, so it is not a normal blur event object.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse up event
+ */
+OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
+       // Only deactivate when clicking outside the dropdown element
+       if ( this.$( e.target ).closest( '.oo-ui-popupToolGroup' )[0] !== this.$element[0] ) {
+               this.setActive( false );
+       }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.PopupToolGroup.prototype.onMouseUp = function ( e ) {
+       this.setActive( false );
+       return OO.ui.ToolGroup.prototype.onMouseUp.call( this, e );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.PopupToolGroup.prototype.onMouseDown = function ( e ) {
+       return OO.ui.ToolGroup.prototype.onMouseDown.call( this, e );
+};
+
+/**
+ * Handle mouse up events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse up event
+ */
+OO.ui.PopupToolGroup.prototype.onHandleMouseUp = function () {
+       return false;
+};
+
+/**
+ * Handle mouse down events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.PopupToolGroup.prototype.onHandleMouseDown = function ( e ) {
+       if ( !this.disabled && e.which === 1 ) {
+               this.setActive( !this.active );
+       }
+       return false;
+};
+
+/**
+ * Switch into active mode.
+ *
+ * When active, mouseup events anywhere in the document will trigger deactivation.
+ *
+ * @method
+ */
+OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
+       value = !!value;
+       if ( this.active !== value ) {
+               this.active = value;
+               if ( value ) {
+                       this.setClipping( true );
+                       this.$element.addClass( 'oo-ui-popupToolGroup-active' );
+                       this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
+               } else {
+                       this.setClipping( false );
+                       this.$element.removeClass( 'oo-ui-popupToolGroup-active' );
+                       this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
+               }
+       }
+};
+/**
+ * Drop down list layout of tools as labeled icon buttons.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.PopupToolGroup
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
+       // Parent constructor
+       OO.ui.PopupToolGroup.call( this, toolbar, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-listToolGroup' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
+
+/* Static Properties */
+
+OO.ui.ListToolGroup.static.accelTooltips = true;
+/**
+ * Drop down menu layout of tools as selectable menu items.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.PopupToolGroup
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.PopupToolGroup.call( this, toolbar, config );
+
+       // Events
+       this.toolbar.connect( this, { 'updateState': 'onUpdateState' } );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-menuToolGroup' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
+
+/* Static Properties */
+
+OO.ui.MenuToolGroup.static.accelTooltips = true;
+
+/* Methods */
+
+/**
+ * Handle the toolbar state being updated.
+ *
+ * When the state changes, the title of each active item in the menu will be joined together and
+ * used as a label for the group. The label will be empty if none of the items are active.
+ *
+ * @method
+ */
+OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
+       var name,
+               labelTexts = [];
+
+       for ( name in this.tools ) {
+               if ( this.tools[name].isActive() ) {
+                       labelTexts.push( this.tools[name].getTitle() );
+               }
+       }
+
+       this.setLabel( labelTexts.join( ', ' ) );
+};
+/**
+ * UserInterface popup tool.
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Tool
+ * @mixins OO.ui.PopuppableElement
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) {
+       // Parent constructor
+       OO.ui.Tool.call( this, toolbar, config );
+
+       // Mixin constructors
+       OO.ui.PopuppableElement.call( this, config );
+
+       // Initialization
+       this.$element
+               .addClass( 'oo-ui-popupTool' )
+               .append( this.popup.$element );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
+
+OO.mixinClass( OO.ui.PopupTool, OO.ui.PopuppableElement );
+
+/* Methods */
+
+/**
+ * Handle the tool being selected.
+ *
+ * @inheritdoc
+ */
+OO.ui.PopupTool.prototype.onSelect = function () {
+       if ( !this.disabled ) {
+               if ( this.popup.isVisible() ) {
+                       this.hidePopup();
+               } else {
+                       this.showPopup();
+               }
+       }
+       this.setActive( false );
+       return false;
+};
+
+/**
+ * Handle the toolbar state being updated.
+ *
+ * @inheritdoc
+ */
+OO.ui.PopupTool.prototype.onUpdateState = function () {
+       this.setActive( false );
+};
+/**
+ * Container for multiple related buttons.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixin OO.ui.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
+       // Parent constructor
+       OO.ui.Widget.call( this, config );
+
+       // Mixin constructors
+       OO.ui.GroupElement.call( this, this.$element, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-buttonGroupWidget' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
+
+OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
+/**
+ * Creates an OO.ui.ButtonWidget object.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.ButtonedElement
+ * @mixins OO.ui.IconedElement
+ * @mixins OO.ui.IndicatedElement
+ * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.TitledElement
+ * @mixins OO.ui.FlaggableElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [title=''] Title text
+ * @cfg {string} [href] Hyperlink to visit when clicked
+ * @cfg {string} [target] Target to open hyperlink in
+ */
+OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
+       // Configuration initialization
+       config = $.extend( { 'target': '_blank' }, config );
+
+       // Parent constructor
+       OO.ui.Widget.call( this, config );
+
+       // Mixin constructors
+       OO.ui.ButtonedElement.call( this, this.$( '<a>' ), config );
+       OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
+       OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
+       OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
+       OO.ui.TitledElement.call( this, this.$button, config );
+       OO.ui.FlaggableElement.call( this, config );
+
+       // Properties
+       this.isHyperlink = typeof config.href === 'string';
+
+       // Events
+       this.$button.on( {
+               'click': OO.ui.bind( this.onClick, this ),
+               'keypress': OO.ui.bind( this.onKeyPress, this )
+       } );
+
+       // Initialization
+       this.$button
+               .append( this.$icon, this.$label, this.$indicator )
+               .attr( { 'href': config.href, 'target': config.target } );
+       this.$element
+               .addClass( 'oo-ui-buttonWidget' )
+               .append( this.$button );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
+
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonedElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatedElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggableElement );
+
+/* Events */
+
+/**
+ * @event click
+ */
+
+/* Methods */
+
+/**
+ * Handles mouse click events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse click event
+ * @fires click
+ */
+OO.ui.ButtonWidget.prototype.onClick = function () {
+       if ( !this.disabled ) {
+               this.emit( 'click' );
+               if ( this.isHyperlink ) {
+                       return true;
+               }
+       }
+       return false;
+};
+
+/**
+ * Handles keypress events.
+ *
+ * @method
+ * @param {jQuery.Event} e Keypress event
+ * @fires click
+ */
+OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
+       if ( !this.disabled && e.which === OO.ui.Keys.SPACE ) {
+               if ( this.isHyperlink ) {
+                       this.onClick();
+                       return true;
+               }
+       }
+       return false;
+};
+/**
+ * Creates an OO.ui.InputWidget object.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [name=''] HTML input name
+ * @cfg {string} [value=''] Input value
+ * @cfg {boolean} [readOnly=false] Prevent changes
+ * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
+ */
+OO.ui.InputWidget = function OoUiInputWidget( config ) {
+       // Config intialization
+       config = $.extend( { 'readOnly': false }, config );
+
+       // Parent constructor
+       OO.ui.Widget.call( this, config );
+
+       // Properties
+       this.$input = this.getInputElement( config );
+       this.value = '';
+       this.readOnly = false;
+       this.inputFilter = config.inputFilter;
+
+       // Events
+       this.$input.on( 'keydown mouseup cut paste change input select', OO.ui.bind( this.onEdit, this ) );
+
+       // Initialization
+       this.$input
+               .attr( 'name', config.name )
+               .prop( 'disabled', this.disabled );
+       this.setReadOnly( config.readOnly );
+       this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input );
+       this.setValue( config.value );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
+
+/* Events */
+
+/**
+ * @event change
+ * @param value
+ */
+
+/* Methods */
+
+/**
+ * Get input element.
+ *
+ * @method
+ * @param {Object} [config] Configuration options
+ * @returns {jQuery} Input element
+ */
+OO.ui.InputWidget.prototype.getInputElement = function () {
+       return this.$( '<input>' );
+};
+
+/**
+ * Handle potentially value-changing events.
+ *
+ * @method
+ * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
+ */
+OO.ui.InputWidget.prototype.onEdit = function () {
+       if ( !this.disabled ) {
+               // Allow the stack to clear so the value will be updated
+               setTimeout( OO.ui.bind( function () {
+                       this.setValue( this.$input.val() );
+               }, this ) );
+       }
+};
+
+/**
+ * Get the value of the input.
+ *
+ * @method
+ * @returns {string} Input value
+ */
+OO.ui.InputWidget.prototype.getValue = function () {
+       return this.value;
+};
+
+/**
+ * Sets the direction of the current input, either RTL or LTR
+ *
+ * @method
+ * @param {boolean} isRTL
+ */
+OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
+       if ( isRTL ) {
+               this.$input.removeClass( 'oo-ui-ltr' );
+               this.$input.addClass( 'oo-ui-rtl' );
+       } else {
+               this.$input.removeClass( 'oo-ui-rtl' );
+               this.$input.addClass( 'oo-ui-ltr' );
+       }
+};
+
+/**
+ * Set the value of the input.
+ *
+ * @method
+ * @param {string} value New value
+ * @fires change
+ * @chainable
+ */
+OO.ui.InputWidget.prototype.setValue = function ( value ) {
+       value = this.sanitizeValue( value );
+       if ( this.value !== value ) {
+               this.value = value;
+               this.emit( 'change', this.value );
+       }
+       // Update the DOM if it has changed. Note that with sanitizeValue, it
+       // is possible for the DOM value to change without this.value changing.
+       if ( this.$input.val() !== this.value ) {
+               this.$input.val( this.value );
+       }
+       return this;
+};
+
+/**
+ * Sanitize incoming value.
+ *
+ * Ensures value is a string, and converts undefined and null to empty strings.
+ *
+ * @method
+ * @param {string} value Original value
+ * @returns {string} Sanitized value
+ */
+OO.ui.InputWidget.prototype.sanitizeValue = function ( value ) {
+       if ( value === undefined || value === null ) {
+               return '';
+       } else if ( this.inputFilter ) {
+               return this.inputFilter( String( value ) );
+       } else {
+               return String( value );
+       }
+};
+
+/**
+ * Check if the widget is read-only.
+ *
+ * @method
+ * @param {boolean} Input is read-only
+ */
+OO.ui.InputWidget.prototype.isReadOnly = function () {
+       return this.readOnly;
+};
+
+/**
+ * Set the read-only state of the widget.
+ *
+ * This should probably change the widgets's appearance and prevent it from being used.
+ *
+ * @method
+ * @param {boolean} state Make input read-only
+ * @chainable
+ */
+OO.ui.InputWidget.prototype.setReadOnly = function ( state ) {
+       this.readOnly = !!state;
+       this.$input.prop( 'readonly', this.readOnly );
+       return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
+       OO.ui.Widget.prototype.setDisabled.call( this, state );
+       if ( this.$input ) {
+               this.$input.prop( 'disabled', this.disabled );
+       }
+       return this;
+};/**
+ * Creates an OO.ui.CheckboxInputWidget object.
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
+       // Parent constructor
+       OO.ui.InputWidget.call( this, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-checkboxInputWidget' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
+
+/* Events */
+
+/* Methods */
+
+/**
+ * Get input element.
+ *
+ * @returns {jQuery} Input element
+ */
+OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
+       return this.$( '<input type="checkbox" />' );
+};
+
+/**
+ * Get checked state of the checkbox
+ *
+ * @returns {boolean} If the checkbox is checked
+ */
+OO.ui.CheckboxInputWidget.prototype.getValue = function () {
+       return this.value;
+};
+
+/**
+ * Set value
+ */
+OO.ui.CheckboxInputWidget.prototype.setValue = function ( value ) {
+       value = !!value;
+       if ( this.value !== value ) {
+               this.value = value;
+               this.$input.prop( 'checked', this.value );
+               this.emit( 'change', this.value );
+       }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
+       if ( !this.disabled ) {
+               // Allow the stack to clear so the value will be updated
+               setTimeout( OO.ui.bind( function () {
+                       this.setValue( this.$input.prop( 'checked' ) );
+               }, this ) );
+       }
+};
+/**
+ * Creates an OO.ui.CheckboxWidget object.
+ *
+ * @class
+ * @extends OO.ui.CheckboxInputWidget
+ * @mixins OO.ui.LabeledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [label=''] Label
+ */
+OO.ui.CheckboxWidget = function OoUiCheckboxWidget( config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.CheckboxInputWidget.call( this, config );
+
+       // Mixin constructors
+       OO.ui.LabeledElement.call( this, this.$( '<span>' ) , config );
+
+       // Initialization
+       this.$element
+               .addClass( 'oo-ui-checkboxWidget' )
+               .append( this.$( '<label>' ).append( this.$input, this.$label ) );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.CheckboxWidget, OO.ui.CheckboxInputWidget );
+
+OO.mixinClass( OO.ui.CheckboxWidget, OO.ui.LabeledElement );
+/**
+ * Creates an OO.ui.InputLabelWidget object.
+ *
+ * CSS classes will be added to the button for each flag, each prefixed with 'oo-ui-InputLabelWidget-'
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.LabeledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {OO.ui.InputWidget|null} [input] Related input widget
+ */
+OO.ui.InputLabelWidget = function OoUiInputLabelWidget( config ) {
+       // Config intialization
+       config = $.extend( { 'input': null }, config );
+
+       // Parent constructor
+       OO.ui.Widget.call( this, config );
+
+       // Mixin constructors
+       OO.ui.LabeledElement.call( this, this.$element, config );
+
+       // Properties
+       this.input = config.input;
+
+       // Events
+       this.$element.on( 'click', OO.ui.bind( this.onClick, this ) );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-inputLabelWidget' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.InputLabelWidget, OO.ui.Widget );
+
+OO.mixinClass( OO.ui.InputLabelWidget, OO.ui.LabeledElement );
+
+/* Static Properties */
+
+OO.ui.InputLabelWidget.static.tagName = 'label';
+
+/* Methods */
+
+/**
+ * Handles mouse click events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse click event
+ */
+OO.ui.InputLabelWidget.prototype.onClick = function () {
+       if ( !this.disabled && this.input ) {
+               this.input.$input.focus();
+       }
+       return false;
+};
+/**
+ * Lookup input widget.
+ *
+ * Mixin that adds a menu showing suggested values to a text input. Subclasses must handle `select`
+ * events on #lookupMenu to make use of selections.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {OO.ui.TextInputWidget} input Input widget
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$overlay=this.$( 'body' )] Overlay layer
+ */
+OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
+       // Config intialization
+       config = config || {};
+
+       // Properties
+       this.lookupInput = input;
+       this.$overlay = config.$overlay || this.$( 'body,.oo-ui-window-overlay' ).last();
+       this.lookupMenu = new OO.ui.TextInputMenuWidget( this, {
+               '$': OO.ui.Element.getJQuery( this.$overlay ),
+               'input': this.lookupInput,
+               '$container': config.$container
+       } );
+       this.lookupCache = {};
+       this.lookupQuery = null;
+       this.lookupRequest = null;
+       this.populating = false;
+
+       // Events
+       this.$overlay.append( this.lookupMenu.$element );
+
+       this.lookupInput.$input.on( {
+               'focus': OO.ui.bind( this.onLookupInputFocus, this ),
+               'blur': OO.ui.bind( this.onLookupInputBlur, this ),
+               'mousedown': OO.ui.bind( this.onLookupInputMouseDown, this )
+       } );
+       this.lookupInput.connect( this, { 'change': 'onLookupInputChange' } );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-lookupWidget' );
+       this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' );
+};
+
+/* Methods */
+
+/**
+ * Handle input focus event.
+ *
+ * @method
+ * @param {jQuery.Event} e Input focus event
+ */
+OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
+       this.openLookupMenu();
+};
+
+/**
+ * Handle input blur event.
+ *
+ * @method
+ * @param {jQuery.Event} e Input blur event
+ */
+OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
+       this.lookupMenu.hide();
+};
+
+/**
+ * Handle input mouse down event.
+ *
+ * @method
+ * @param {jQuery.Event} e Input mouse down event
+ */
+OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
+       this.openLookupMenu();
+};
+
+/**
+ * Handle input change event.
+ *
+ * @method
+ * @param {string} value New input value
+ */
+OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
+       this.openLookupMenu();
+};
+
+/**
+ * Open the menu.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
+       var value = this.lookupInput.getValue();
+
+       if ( this.lookupMenu.$input.is( ':focus' ) && $.trim( value ) !== '' ) {
+               this.populateLookupMenu();
+               if ( !this.lookupMenu.isVisible() ) {
+                       this.lookupMenu.show();
+               }
+       } else {
+               this.lookupMenu.clearItems();
+               this.lookupMenu.hide();
+       }
+
+       return this;
+};
+
+/**
+ * Populate lookup menu with current information.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
+       if ( !this.populating ) {
+               this.populating = true;
+               this.getLookupMenuItems()
+                       .done( OO.ui.bind( function ( items ) {
+                               this.lookupMenu.clearItems();
+                               if ( items.length ) {
+                                       this.lookupMenu.show();
+                                       this.lookupMenu.addItems( items );
+                                       this.initializeLookupMenuSelection();
+                                       this.openLookupMenu();
+                               } else {
+                                       this.lookupMenu.hide();
+                               }
+                               this.populating = false;
+                       }, this ) )
+                       .fail( OO.ui.bind( function () {
+                               this.lookupMenu.clearItems();
+                               this.populating = false;
+                       }, this ) );
+       }
+
+       return this;
+};
+
+/**
+ * Set selection in the lookup menu with current information.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () {
+       if ( !this.lookupMenu.getSelectedItem() ) {
+               this.lookupMenu.intializeSelection( this.lookupMenu.getFirstSelectableItem() );
+       }
+       this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
+};
+
+/**
+ * Get lookup menu items for the current query.
+ *
+ * @method
+ * @returns {jQuery.Promise} Promise object which will be passed menu items as the first argument
+ * of the done event
+ */
+OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
+       var value = this.lookupInput.getValue(),
+               deferred = $.Deferred();
+
+       if ( value && value !== this.lookupQuery ) {
+               // Abort current request if query has changed
+               if ( this.lookupRequest ) {
+                       this.lookupRequest.abort();
+                       this.lookupQuery = null;
+                       this.lookupRequest = null;
+               }
+               if ( value in this.lookupCache ) {
+                       deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
+               } else {
+                       this.lookupQuery = value;
+                       this.lookupRequest = this.getLookupRequest()
+                               .always( OO.ui.bind( function () {
+                                       this.lookupQuery = null;
+                                       this.lookupRequest = null;
+                               }, this ) )
+                               .done( OO.ui.bind( function ( data ) {
+                                       this.lookupCache[value] = this.getLookupCacheItemFromData( data );
+                                       deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
+                               }, this ) )
+                               .fail( function () {
+                                       deferred.reject();
+                               } );
+                       this.pushPending();
+                       this.lookupRequest.always( OO.ui.bind( function () {
+                               this.popPending();
+                       }, this ) );
+               }
+       }
+       return deferred.promise();
+};
+
+/**
+ * Get a new request object of the current lookup query value.
+ *
+ * @method
+ * @abstract
+ * @returns {jqXHR} jQuery AJAX object, or promise object with an .abort() method
+ */
+OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
+       // Stub, implemented in subclass
+       return null;
+};
+
+/**
+ * Handle successful lookup request.
+ *
+ * Overriding methods should call #populateLookupMenu when results are available and cache results
+ * for future lookups in #lookupCache as an array of #OO.ui.MenuItemWidget objects.
+ *
+ * @method
+ * @abstract
+ * @param {Mixed} data Response from server
+ */
+OO.ui.LookupInputWidget.prototype.onLookupRequestDone = function () {
+       // Stub, implemented in subclass
+};
+
+/**
+ * Get a list of menu item widgets from the data stored by the lookup request's done handler.
+ *
+ * @method
+ * @abstract
+ * @param {Mixed} data Cached result data, usually an array
+ * @returns {OO.ui.MenuItemWidget[]} Menu items
+ */
+OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
+       // Stub, implemented in subclass
+       return [];
+};
+/**
+ * Creates an OO.ui.OptionWidget object.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.IconedElement
+ * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.IndicatedElement
+ *
+ * @constructor
+ * @param {Mixed} data Option data
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [selected=false] Select option
+ * @cfg {boolean} [highlighted=false] Highlight option
+ * @cfg {string} [rel] Value for `rel` attribute in DOM, allowing per-option styling
+ */
+OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
+       // Config intialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Widget.call( this, config );
+
+       // Mixin constructors
+       OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
+       OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
+       OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
+
+       // Properties
+       this.data = data;
+       this.selected = false;
+       this.highlighted = false;
+
+       // Initialization
+       this.$element
+               .data( 'oo-ui-optionWidget', this )
+               .attr( 'rel', config.rel )
+               .addClass( 'oo-ui-optionWidget' )
+               .append( this.$label );
+       this.setSelected( config.selected );
+       this.setHighlighted( config.highlighted );
+
+       // Options
+       this.$element
+               .prepend( this.$icon )
+               .append( this.$indicator );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
+
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatedElement );
+
+/* Static Properties */
+
+OO.ui.OptionWidget.static.tagName = 'li';
+
+OO.ui.OptionWidget.static.selectable = true;
+
+OO.ui.OptionWidget.static.highlightable = true;
+
+OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
+
+/* Methods */
+
+/**
+ * Check if option can be selected.
+ *
+ * @method
+ * @returns {boolean} Item is selectable
+ */
+OO.ui.OptionWidget.prototype.isSelectable = function () {
+       return this.constructor.static.selectable && !this.disabled;
+};
+
+/**
+ * Check if option can be highlighted.
+ *
+ * @method
+ * @returns {boolean} Item is highlightable
+ */
+OO.ui.OptionWidget.prototype.isHighlightable = function () {
+       return this.constructor.static.highlightable && !this.disabled;
+};
+
+/**
+ * Check if option is selected.
+ *
+ * @method
+ * @returns {boolean} Item is selected
+ */
+OO.ui.OptionWidget.prototype.isSelected = function () {
+       return this.selected;
+};
+
+/**
+ * Check if option is highlighted.
+ *
+ * @method
+ * @returns {boolean} Item is highlighted
+ */
+OO.ui.OptionWidget.prototype.isHighlighted = function () {
+       return this.highlighted;
+};
+
+/**
+ * Set selected state.
+ *
+ * @method
+ * @param {boolean} [state=false] Select option
+ * @chainable
+ */
+OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
+       if ( !this.disabled && this.constructor.static.selectable ) {
+               this.selected = !!state;
+               if ( this.selected ) {
+                       this.$element.addClass( 'oo-ui-optionWidget-selected' );
+                       if ( this.constructor.static.scrollIntoViewOnSelect ) {
+                               this.scrollElementIntoView();
+                       }
+               } else {
+                       this.$element.removeClass( 'oo-ui-optionWidget-selected' );
+               }
+       }
+       return this;
+};
+
+/**
+ * Set highlighted state.
+ *
+ * @method
+ * @param {boolean} [state=false] Highlight option
+ * @chainable
+ */
+OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
+       if ( !this.disabled && this.constructor.static.highlightable ) {
+               this.highlighted = !!state;
+               if ( this.highlighted ) {
+                       this.$element.addClass( 'oo-ui-optionWidget-highlighted' );
+               } else {
+                       this.$element.removeClass( 'oo-ui-optionWidget-highlighted' );
+               }
+       }
+       return this;
+};
+
+/**
+ * Make the option's highlight flash.
+ *
+ * @method
+ * @param {Function} [done] Callback to execute when flash effect is complete.
+ */
+OO.ui.OptionWidget.prototype.flash = function ( done ) {
+       var $this = this.$element;
+
+       if ( !this.disabled && this.constructor.static.highlightable ) {
+               $this.removeClass( 'oo-ui-optionWidget-highlighted' );
+               setTimeout( OO.ui.bind( function () {
+                       $this.addClass( 'oo-ui-optionWidget-highlighted' );
+                       if ( done ) {
+                               setTimeout( done, 100 );
+                       }
+               }, this ), 100 );
+       }
+};
+
+/**
+ * Get option data.
+ *
+ * @method
+ * @returns {Mixed} Option data
+ */
+OO.ui.OptionWidget.prototype.getData = function () {
+       return this.data;
+};
+/**
+ * Create an OO.ui.SelectWidget object.
+ *
+ * @class
+ * @abstract
+ * @extends OO.ui.Widget
+ * @mixin OO.ui.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
+       // Config intialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Widget.call( this, config );
+
+       // Mixin constructors
+       OO.ui.GroupElement.call( this, this.$element, config );
+
+       // Properties
+       this.pressed = false;
+       this.selecting = null;
+       this.hashes = {};
+
+       // Events
+       this.$element.on( {
+               'mousedown': OO.ui.bind( this.onMouseDown, this ),
+               'mouseup': OO.ui.bind( this.onMouseUp, this ),
+               'mousemove': OO.ui.bind( this.onMouseMove, this ),
+               'mouseover': OO.ui.bind( this.onMouseOver, this ),
+               'mouseleave': OO.ui.bind( this.onMouseLeave, this )
+       } );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-selectWidget' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
+
+OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
+
+/* Events */
+
+/**
+ * @event highlight
+ * @param {OO.ui.OptionWidget|null} item Highlighted item
+ */
+
+/**
+ * @event select
+ * @param {OO.ui.OptionWidget|null} item Selected item
+ */
+
+/**
+ * @event add
+ * @param {OO.ui.OptionWidget[]} items Added items
+ * @param {number} index Index items were added at
+ */
+
+/**
+ * @event remove
+ * @param {OO.ui.OptionWidget[]} items Removed items
+ */
+
+/* Static Properties */
+
+OO.ui.SelectWidget.static.tagName = 'ul';
+
+/* Methods */
+
+/**
+ * Handle mouse down events.
+ *
+ * @method
+ * @private
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
+       var item;
+
+       if ( !this.disabled && e.which === 1 ) {
+               this.pressed = true;
+               item = this.getTargetItem( e );
+               if ( item && item.isSelectable() ) {
+                       this.intializeSelection( item );
+                       this.selecting = item;
+                       this.$( this.$.context ).one( 'mouseup', OO.ui.bind( this.onMouseUp, this ) );
+               }
+       }
+       return false;
+};
+
+/**
+ * Handle mouse up events.
+ *
+ * @method
+ * @private
+ * @param {jQuery.Event} e Mouse up event
+ */
+OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
+       var item;
+       this.pressed = false;
+       if ( !this.selecting ) {
+               item = this.getTargetItem( e );
+               if ( item && item.isSelectable() ) {
+                       this.selecting = item;
+               }
+       }
+       if ( !this.disabled && e.which === 1 && this.selecting ) {
+               this.selectItem( this.selecting );
+               this.selecting = null;
+       }
+       return false;
+};
+
+/**
+ * Handle mouse move events.
+ *
+ * @method
+ * @private
+ * @param {jQuery.Event} e Mouse move event
+ */
+OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
+       var item;
+
+       if ( !this.disabled && this.pressed ) {
+               item = this.getTargetItem( e );
+               if ( item && item !== this.selecting && item.isSelectable() ) {
+                       this.intializeSelection( item );
+                       this.selecting = item;
+               }
+       }
+       return false;
+};
+
+/**
+ * Handle mouse over events.
+ *
+ * @method
+ * @private
+ * @param {jQuery.Event} e Mouse over event
+ */
+OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
+       var item;
+
+       if ( !this.disabled ) {
+               item = this.getTargetItem( e );
+               if ( item && item.isHighlightable() ) {
+                       this.highlightItem( item );
+               }
+       }
+       return false;
+};
+
+/**
+ * Handle mouse leave events.
+ *
+ * @method
+ * @private
+ * @param {jQuery.Event} e Mouse over event
+ */
+OO.ui.SelectWidget.prototype.onMouseLeave = function () {
+       if ( !this.disabled ) {
+               this.highlightItem();
+       }
+       return false;
+};
+
+/**
+ * Get the closest item to a jQuery.Event.
+ *
+ * @method
+ * @private
+ * @param {jQuery.Event} e
+ * @returns {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
+ */
+OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
+       var $item = this.$( e.target ).closest( '.oo-ui-optionWidget' );
+       if ( $item.length ) {
+               return $item.data( 'oo-ui-optionWidget' );
+       }
+       return null;
+};
+
+/**
+ * Get selected item.
+ *
+ * @method
+ * @returns {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
+ */
+OO.ui.SelectWidget.prototype.getSelectedItem = function () {
+       var i, len;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               if ( this.items[i].isSelected() ) {
+                       return this.items[i];
+               }
+       }
+       return null;
+};
+
+/**
+ * Get highlighted item.
+ *
+ * @method
+ * @returns {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
+ */
+OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
+       var i, len;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               if ( this.items[i].isHighlighted() ) {
+                       return this.items[i];
+               }
+       }
+       return null;
+};
+
+/**
+ * Get an existing item with equivilant data.
+ *
+ * @method
+ * @param {Object} data Item data to search for
+ * @returns {OO.ui.OptionWidget|null} Item with equivilent value, `null` if none exists
+ */
+OO.ui.SelectWidget.prototype.getItemFromData = function ( data ) {
+       var hash = OO.getHash( data );
+
+       if ( hash in this.hashes ) {
+               return this.hashes[hash];
+       }
+
+       return null;
+};
+
+/**
+ * Highlight an item.
+ *
+ * Highlighting is mutually exclusive.
+ *
+ * @method
+ * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all
+ * @fires highlight
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
+       var i, len;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               this.items[i].setHighlighted( this.items[i] === item );
+       }
+       this.emit( 'highlight', item );
+
+       return this;
+};
+
+/**
+ * Select an item.
+ *
+ * @method
+ * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
+ * @fires select
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
+       var i, len;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               this.items[i].setSelected( this.items[i] === item );
+       }
+       this.emit( 'select', item );
+
+       return this;
+};
+
+/**
+ * Setup selection and highlighting.
+ *
+ * This should be used to synchronize the UI with the model without emitting events that would in
+ * turn update the model.
+ *
+ * @param {OO.ui.OptionWidget} [item] Item to select
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.intializeSelection = function( item ) {
+       var i, len, selected;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               selected = this.items[i] === item;
+               this.items[i].setSelected( selected );
+               this.items[i].setHighlighted( selected );
+       }
+
+       return this;
+};
+
+/**
+ * Get an item relative to another one.
+ *
+ * @method
+ * @param {OO.ui.OptionWidget} item Item to start at
+ * @param {number} direction Direction to move in
+ * @returns {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
+ */
+OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
+       var inc = direction > 0 ? 1 : -1,
+               len = this.items.length,
+               index = item instanceof OO.ui.OptionWidget ?
+                       this.items.indexOf( item ) : ( inc > 0 ? -1 : 0 ),
+               stopAt = Math.max( Math.min( index, len - 1 ), 0 ),
+               i = inc > 0 ?
+                       // Default to 0 instead of -1, if nothing is selected let's start at the beginning
+                       Math.max( index, -1 ) :
+                       // Default to n-1 instead of -1, if nothing is selected let's start at the end
+                       Math.min( index, len );
+
+       while ( true ) {
+               i = ( i + inc + len ) % len;
+               item = this.items[i];
+               if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
+                       return item;
+               }
+               // Stop iterating when we've looped all the way around
+               if ( i === stopAt ) {
+                       break;
+               }
+       }
+       return null;
+};
+
+/**
+ * Get the next selectable item.
+ *
+ * @method
+ * @returns {OO.ui.OptionWidget|null} Item, `null` if ther aren't any selectable items
+ */
+OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
+       var i, len, item;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               item = this.items[i];
+               if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
+                       return item;
+               }
+       }
+
+       return null;
+};
+
+/**
+ * Add items.
+ *
+ * When items are added with the same values as existing items, the existing items will be
+ * automatically removed before the new items are added.
+ *
+ * @method
+ * @param {OO.ui.OptionWidget[]} items Items to add
+ * @param {number} [index] Index to insert items after
+ * @fires add
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
+       var i, len, item, hash,
+               remove = [];
+
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+               hash = OO.getHash( item.getData() );
+               if ( hash in this.hashes ) {
+                       // Remove item with same value
+                       remove.push( this.hashes[hash] );
+               }
+               this.hashes[hash] = item;
+       }
+       if ( remove.length ) {
+               this.removeItems( remove );
+       }
+
+       OO.ui.GroupElement.prototype.addItems.call( this, items, index );
+
+       // Always provide an index, even if it was omitted
+       this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
+
+       return this;
+};
+
+/**
+ * Remove items.
+ *
+ * Items will be detached, not removed, so they can be used later.
+ *
+ * @method
+ * @param {OO.ui.OptionWidget[]} items Items to remove
+ * @fires remove
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
+       var i, len, item, hash;
+
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+               hash = OO.getHash( item.getData() );
+               if ( hash in this.hashes ) {
+                       // Remove existing item
+                       delete this.hashes[hash];
+               }
+               if ( item.isSelected() ) {
+                       this.selectItem( null );
+               }
+       }
+       OO.ui.GroupElement.prototype.removeItems.call( this, items );
+
+       this.emit( 'remove', items );
+
+       return this;
+};
+
+/**
+ * Clear all items.
+ *
+ * Items will be detached, not removed, so they can be used later.
+ *
+ * @method
+ * @fires remove
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.clearItems = function () {
+       var items = this.items.slice();
+
+       // Clear all items
+       this.hashes = {};
+       OO.ui.GroupElement.prototype.clearItems.call( this );
+       this.selectItem( null );
+
+       this.emit( 'remove', items );
+
+       return this;
+};
+/**
+ * Creates an OO.ui.MenuItemWidget object.
+ *
+ * @class
+ * @extends OO.ui.OptionWidget
+ *
+ * @constructor
+ * @param {Mixed} data Item data
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.MenuItemWidget = function OoUiMenuItemWidget( data, config ) {
+       // Configuration initialization
+       config = $.extend( { 'icon': 'check' }, config );
+
+       // Parent constructor
+       OO.ui.OptionWidget.call( this, data, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-menuItemWidget' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.MenuItemWidget, OO.ui.OptionWidget );
+/**
+ * Create an OO.ui.MenuWidget object.
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ * @mixins OO.ui.ClippableElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {OO.ui.InputWidget} [input] Input to bind keyboard handlers to
+ */
+OO.ui.MenuWidget = function OoUiMenuWidget( config ) {
+       // Config intialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.SelectWidget.call( this, config );
+
+       // Mixin constructors
+       OO.ui.ClippableElement.call( this, this.$group );
+
+       // Properties
+       this.newItems = [];
+       this.$input = config.input ? config.input.$input : null;
+       this.$previousFocus = null;
+       this.isolated = !config.input;
+       this.visible = false;
+       this.onKeyDownHandler = OO.ui.bind( this.onKeyDown, this );
+
+       // Initialization
+       this.$element.hide().addClass( 'oo-ui-menuWidget' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.MenuWidget, OO.ui.SelectWidget );
+
+OO.mixinClass( OO.ui.MenuWidget, OO.ui.ClippableElement );
+
+/* Methods */
+
+/**
+ * Handles key down events.
+ *
+ * @method
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
+       var nextItem,
+               handled = false,
+               highlightItem = this.getHighlightedItem();
+
+       if ( !this.disabled && this.visible ) {
+               if ( !highlightItem ) {
+                       highlightItem = this.getSelectedItem();
+               }
+               switch ( e.keyCode ) {
+                       case OO.ui.Keys.ENTER:
+                               this.selectItem( highlightItem );
+                               handled = true;
+                               break;
+                       case OO.ui.Keys.UP:
+                               nextItem = this.getRelativeSelectableItem( highlightItem, -1 );
+                               handled = true;
+                               break;
+                       case OO.ui.Keys.DOWN:
+                               nextItem = this.getRelativeSelectableItem( highlightItem, 1 );
+                               handled = true;
+                               break;
+                       case OO.ui.Keys.ESCAPE:
+                               if ( highlightItem ) {
+                                       highlightItem.setHighlighted( false );
+                               }
+                               this.hide();
+                               handled = true;
+                               break;
+               }
+
+               if ( nextItem ) {
+                       this.highlightItem( nextItem );
+                       nextItem.scrollElementIntoView();
+               }
+
+               if ( handled ) {
+                       e.preventDefault();
+                       e.stopPropagation();
+                       return false;
+               }
+       }
+};
+
+/**
+ * Check if the menu is visible.
+ *
+ * @method
+ * @returns {boolean} Menu is visible
+ */
+OO.ui.MenuWidget.prototype.isVisible = function () {
+       return this.visible;
+};
+
+/**
+ * Bind key down listener
+ *
+ * @method
+ */
+OO.ui.MenuWidget.prototype.bindKeyDownListener = function () {
+       if ( this.$input ) {
+               this.$input.on( 'keydown', this.onKeyDownHandler );
+       } else {
+               // Capture menu navigation keys
+               this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
+       }
+};
+
+/**
+ * Unbind key down listener
+ *
+ * @method
+ */
+OO.ui.MenuWidget.prototype.unbindKeyDownListener = function () {
+       if ( this.$input ) {
+               this.$input.off( 'keydown' );
+       } else {
+               this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
+       }
+};
+
+/**
+ * Select an item.
+ *
+ * The menu will stay open if an item is silently selected.
+ *
+ * @method
+ * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
+ * @chainable
+ */
+OO.ui.MenuWidget.prototype.selectItem = function ( item ) {
+       // Parent method
+       OO.ui.SelectWidget.prototype.selectItem.call( this, item );
+
+       if ( !this.disabled ) {
+               if ( item ) {
+                       this.disabled = true;
+                       item.flash( OO.ui.bind( function () {
+                               this.hide();
+                               this.disabled = false;
+                       }, this ) );
+               } else {
+                       this.hide();
+               }
+       }
+
+       return this;
+};
+
+/**
+ * Add items.
+ *
+ * Adding an existing item (by value) will move it.
+ *
+ * @method
+ * @param {OO.ui.MenuItemWidget[]} items Items to add
+ * @param {number} [index] Index to insert items after
+ * @chainable
+ */
+OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
+       var i, len, item;
+
+       // Parent method
+       OO.ui.SelectWidget.prototype.addItems.call( this, items, index );
+
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+               if ( this.visible ) {
+                       // Defer fitting label until
+                       item.fitLabel();
+               } else {
+                       this.newItems.push( item );
+               }
+       }
+
+       return this;
+};
+
+/**
+ * Show the menu.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.MenuWidget.prototype.show = function () {
+       var i, len;
+
+       if ( this.items.length ) {
+               this.$element.show();
+               this.visible = true;
+               this.bindKeyDownListener();
+
+               // Change focus to enable keyboard navigation
+               if ( this.isolated && this.$input && !this.$input.is( ':focus' ) ) {
+                       this.$previousFocus = this.$( ':focus' );
+                       this.$input.focus();
+               }
+               if ( this.newItems.length ) {
+                       for ( i = 0, len = this.newItems.length; i < len; i++ ) {
+                               this.newItems[i].fitLabel();
+                       }
+                       this.newItems = [];
+               }
+
+               this.setClipping( true );
+       }
+
+       return this;
+};
+
+/**
+ * Hide the menu.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.MenuWidget.prototype.hide = function () {
+       this.$element.hide();
+       this.visible = false;
+       this.unbindKeyDownListener();
+
+       if ( this.isolated && this.$previousFocus ) {
+               this.$previousFocus.focus();
+               this.$previousFocus = null;
+       }
+
+       this.setClipping( false );
+
+       return this;
+};
+/**
+ * Creates an OO.ui.MenuSectionItemWidget object.
+ *
+ * @class
+ * @extends OO.ui.OptionWidget
+ *
+ * @constructor
+ * @param {Mixed} data Item data
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.MenuSectionItemWidget = function OoUiMenuSectionItemWidget( data, config ) {
+       // Parent constructor
+       OO.ui.OptionWidget.call( this, data, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-menuSectionItemWidget' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.MenuSectionItemWidget, OO.ui.OptionWidget );
+
+OO.ui.MenuSectionItemWidget.static.selectable = false;
+
+OO.ui.MenuSectionItemWidget.static.highlightable = false;
+/**
+ * Create an OO.ui.OutlineWidget object.
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.OutlineWidget = function OoUiOutlineWidget( config ) {
+       // Config intialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.SelectWidget.call( this, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-outlineWidget' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.OutlineWidget, OO.ui.SelectWidget );
+/**
+ * Creates an OO.ui.OutlineControlsWidget object.
+ *
+ * @class
+ *
+ * @constructor
+ * @param {OO.ui.OutlineWidget} outline Outline to control
+ * @param {Object} [config] Configuration options
+ * @cfg {Object[]} [adders] List of icons to show as addable item types, each an object with
+ *  name, title and icon properties
+ */
+OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Widget.call( this, config );
+
+       // Properties
+       this.outline = outline;
+       this.adders = {};
+       this.$adders = this.$( '<div>' );
+       this.$movers = this.$( '<div>' );
+       this.addButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'frameless': true,
+               'icon': 'add-item'
+       } );
+       this.upButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'frameless': true,
+               'icon': 'collapse',
+               'title': OO.ui.msg( 'ooui-outline-control-move-up' )
+       } );
+       this.downButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'frameless': true,
+               'icon': 'expand',
+               'title': OO.ui.msg( 'ooui-outline-control-move-down' )
+       } );
+
+       // Events
+       outline.connect( this, {
+               'select': 'onOutlineChange',
+               'add': 'onOutlineChange',
+               'remove': 'onOutlineChange'
+       } );
+       this.upButton.connect( this, { 'click': ['emit', 'move', -1] } );
+       this.downButton.connect( this, { 'click': ['emit', 'move', 1] } );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-outlineControlsWidget' );
+       this.$adders.addClass( 'oo-ui-outlineControlsWidget-adders' );
+       this.$movers
+               .addClass( 'oo-ui-outlineControlsWidget-movers' )
+               .append( this.upButton.$element, this.downButton.$element );
+       this.$element.append( this.$adders, this.$movers );
+       if ( config.adders && config.adders.length ) {
+               this.setupAdders( config.adders );
+       }
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
+
+/* Events */
+
+/**
+ * @event move
+ * @param {number} places Number of places to move
+ */
+
+/* Methods */
+
+/**
+ * Handle outline change events.
+ *
+ * @method
+ */
+OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
+       var i, len, firstMovable, lastMovable,
+               movable = false,
+               items = this.outline.getItems(),
+               selectedItem = this.outline.getSelectedItem();
+
+       if ( selectedItem && selectedItem.isMovable() ) {
+               movable = true;
+               i = -1;
+               len = items.length;
+               while ( ++i < len ) {
+                       if ( items[i].isMovable() ) {
+                               firstMovable = items[i];
+                               break;
+                       }
+               }
+               i = len;
+               while ( i-- ) {
+                       if ( items[i].isMovable() ) {
+                               lastMovable = items[i];
+                               break;
+                       }
+               }
+       }
+       this.upButton.setDisabled( !movable || selectedItem === firstMovable );
+       this.downButton.setDisabled( !movable || selectedItem === lastMovable );
+};
+
+/**
+ * Setup adders icons.
+ *
+ * @method
+ * @param {Object[]} adders List of configuations for adder buttons, each containing a name, title
+ *  and icon property
+ */
+OO.ui.OutlineControlsWidget.prototype.setupAdders = function ( adders ) {
+       var i, len, addition, button,
+               $buttons = this.$( [] );
+
+       this.$adders.append( this.addButton.$element );
+       for ( i = 0, len = adders.length; i < len; i++ ) {
+               addition = adders[i];
+               button = new OO.ui.ButtonWidget( {
+                       '$': this.$, 'frameless': true, 'icon': addition.icon, 'title': addition.title
+               } );
+               button.connect( this, { 'click': ['emit', 'add', addition.name] } );
+               this.adders[addition.name] = button;
+               this.$adders.append( button.$element );
+               $buttons = $buttons.add( button.$element );
+       }
+};
+/**
+ * Creates an OO.ui.OutlineItemWidget object.
+ *
+ * @class
+ * @extends OO.ui.OptionWidget
+ *
+ * @constructor
+ * @param {Mixed} data Item data
+ * @param {Object} [config] Configuration options
+ * @cfg {number} [level] Indentation level
+ * @cfg {boolean} [movable] Allow modification from outline controls
+ */
+OO.ui.OutlineItemWidget = function OoUiOutlineItemWidget( data, config ) {
+       // Config intialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.OptionWidget.call( this, data, config );
+
+       // Properties
+       this.level = 0;
+       this.movable = !!config.movable;
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-outlineItemWidget' );
+       this.setLevel( config.level );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.OutlineItemWidget, OO.ui.OptionWidget );
+
+/* Static Properties */
+
+OO.ui.OutlineItemWidget.static.highlightable = false;
+
+OO.ui.OutlineItemWidget.static.scrollIntoViewOnSelect = true;
+
+OO.ui.OutlineItemWidget.static.levelClass = 'oo-ui-outlineItemWidget-level-';
+
+OO.ui.OutlineItemWidget.static.levels = 3;
+
+/* Methods */
+
+/**
+ * Check if item is movable.
+ *
+ * Moveablilty is used by outline controls.
+ *
+ * @returns {boolean} Item is movable
+ */
+OO.ui.OutlineItemWidget.prototype.isMovable = function () {
+       return this.movable;
+};
+
+/**
+ * Get indentation level.
+ *
+ * @returns {number} Indentation level
+ */
+OO.ui.OutlineItemWidget.prototype.getLevel = function () {
+       return this.level;
+};
+
+/**
+ * Set indentation level.
+ *
+ * @method
+ * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
+ * @chainable
+ */
+OO.ui.OutlineItemWidget.prototype.setLevel = function ( level ) {
+       var levels = this.constructor.static.levels,
+               levelClass = this.constructor.static.levelClass,
+               i = levels;
+
+       this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
+       while ( i-- ) {
+               if ( this.level === i ) {
+                       this.$element.addClass( levelClass + i );
+               } else {
+                       this.$element.removeClass( levelClass + i );
+               }
+       }
+
+       return this;
+};
+/**
+ * Creates an OO.ui.BookletOutlineItemWidget object.
+ *
+ * @class
+ * @extends OO.ui.OutlineItemWidget
+ *
+ * @constructor
+ * @param {Mixed} data Item data
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.BookletOutlineItemWidget = function OoUiBookletOutlineItemWidget( data, page, config ) {
+       // Configuration intialization
+       config = $.extend( {
+               'label': page.getLabel() || data,
+               'level': page.getLevel(),
+               'icon': page.getIcon(),
+               'indicator': page.getIndicator(),
+               'indicatorTitle': page.getIndicatorTitle(),
+               'movable': page.isMovable()
+       }, config );
+
+       // Parent constructor
+       OO.ui.OutlineItemWidget.call( this, data, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-bookletOutlineItemWidget' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.BookletOutlineItemWidget, OO.ui.OutlineItemWidget );
+/**
+ * Create an OO.ui.ButtonSelect object.
+ *
+ * @class
+ * @extends OO.ui.OptionWidget
+ * @mixins OO.ui.ButtonedElement
+ * @mixins OO.ui.FlaggableElement
+ *
+ * @constructor
+ * @param {Mixed} data Option data
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) {
+       // Parent constructor
+       OO.ui.OptionWidget.call( this, data, config );
+
+       // Mixin constructors
+       OO.ui.ButtonedElement.call( this, this.$( '<a>' ), config );
+       OO.ui.FlaggableElement.call( this, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-buttonOptionWidget' );
+       this.$button.append( this.$element.contents() );
+       this.$element.append( this.$button );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.OptionWidget );
+
+OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonedElement );
+OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.FlaggableElement );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
+       OO.ui.OptionWidget.prototype.setSelected.call( this, state );
+
+       this.setActive( state );
+
+       return this;
+};
+/**
+ * Create an OO.ui.ButtonSelect object.
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
+       // Parent constructor
+       OO.ui.SelectWidget.call( this, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-buttonSelectWidget' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
+/**
+ * Creates an OO.ui.PopupWidget object.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.LabeledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [tail=true] Show tail pointing to origin of popup
+ * @cfg {string} [align='center'] Alignment of popup to origin
+ * @cfg {jQuery} [$container] Container to prevent popup from rendering outside of
+ * @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus
+ * @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked
+ * @cfg {boolean} [head] Show label and close button at the top
+ */
+OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
+       // Config intialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Widget.call( this, config );
+
+       // Mixin constructors
+       OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
+
+       // Properties
+       this.visible = false;
+       this.$popup = this.$( '<div>' );
+       this.$head = this.$( '<div>' );
+       this.$body = this.$( '<div>' );
+       this.$tail = this.$( '<div>' );
+       this.$container = config.$container || this.$( 'body' );
+       this.autoClose = !!config.autoClose;
+       this.$autoCloseIgnore = config.$autoCloseIgnore;
+       this.transitionTimeout = null;
+       this.tail = false;
+       this.align = config.align || 'center';
+       this.closeButton = new OO.ui.ButtonWidget( { '$': this.$, 'frameless': true, 'icon': 'close' } );
+       this.onMouseDownHandler = OO.ui.bind( this.onMouseDown, this );
+
+       // Events
+       this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } );
+
+       // Initialization
+       this.useTail( config.tail !== undefined ? !!config.tail : true );
+       this.$body.addClass( 'oo-ui-popupWidget-body' );
+       this.$tail.addClass( 'oo-ui-popupWidget-tail' );
+       this.$head
+               .addClass( 'oo-ui-popupWidget-head' )
+               .append( this.$label, this.closeButton.$element );
+       if ( !config.head ) {
+               this.$head.hide();
+       }
+       this.$popup
+               .addClass( 'oo-ui-popupWidget-popup' )
+               .append( this.$head, this.$body );
+       this.$element.hide()
+               .addClass( 'oo-ui-popupWidget' )
+               .append( this.$popup, this.$tail );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
+
+OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabeledElement );
+
+/* Events */
+
+/**
+ * @event hide
+ */
+
+/**
+ * @event show
+ */
+
+/* Methods */
+
+/**
+ * Handles mouse down events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
+       if (
+               this.visible &&
+               !$.contains( this.$element[0], e.target ) &&
+               ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
+       ) {
+               this.hide();
+       }
+};
+
+/**
+ * Bind mouse down listener
+ *
+ * @method
+ */
+OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
+       // Capture clicks outside popup
+       this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
+};
+
+/**
+ * Handles close button click events.
+ *
+ * @method
+ */
+OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
+       if ( this.visible ) {
+               this.hide();
+       }
+};
+
+/**
+ * Unbind mouse down listener
+ *
+ * @method
+ */
+OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
+       this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
+};
+
+/**
+ * Check if the popup is visible.
+ *
+ * @method
+ * @returns {boolean} Popup is visible
+ */
+OO.ui.PopupWidget.prototype.isVisible = function () {
+       return this.visible;
+};
+
+/**
+ * Set whether to show a tail.
+ *
+ * @method
+ * @returns {boolean} Make tail visible
+ */
+OO.ui.PopupWidget.prototype.useTail = function ( value ) {
+       value = !!value;
+       if ( this.tail !== value ) {
+               this.tail = value;
+               if ( value ) {
+                       this.$element.addClass( 'oo-ui-popupWidget-tailed' );
+               } else {
+                       this.$element.removeClass( 'oo-ui-popupWidget-tailed' );
+               }
+       }
+};
+
+/**
+ * Check if showing a tail.
+ *
+ * @method
+ * @returns {boolean} tail is visible
+ */
+OO.ui.PopupWidget.prototype.hasTail = function () {
+       return this.tail;
+};
+
+/**
+ * Show the context.
+ *
+ * @method
+ * @fires show
+ * @chainable
+ */
+OO.ui.PopupWidget.prototype.show = function () {
+       if ( !this.visible ) {
+               this.$element.show();
+               this.visible = true;
+               this.emit( 'show' );
+               if ( this.autoClose ) {
+                       this.bindMouseDownListener();
+               }
+       }
+       return this;
+};
+
+/**
+ * Hide the context.
+ *
+ * @method
+ * @fires hide
+ * @chainable
+ */
+OO.ui.PopupWidget.prototype.hide = function () {
+       if ( this.visible ) {
+               this.$element.hide();
+               this.visible = false;
+               this.emit( 'hide' );
+               if ( this.autoClose ) {
+                       this.unbindMouseDownListener();
+               }
+       }
+       return this;
+};
+
+/**
+ * Updates the position and size.
+ *
+ * @method
+ * @param {number} width Width
+ * @param {number} height Height
+ * @param {boolean} [transition=false] Use a smooth transition
+ * @chainable
+ */
+OO.ui.PopupWidget.prototype.display = function ( width, height, transition ) {
+       var padding = 10,
+               originOffset = Math.round( this.$element.offset().left ),
+               containerLeft = Math.round( this.$container.offset().left ),
+               containerWidth = this.$container.innerWidth(),
+               containerRight = containerLeft + containerWidth,
+               popupOffset = width * ( { 'left': 0, 'center': -0.5, 'right': -1 } )[this.align],
+               popupLeft = popupOffset - padding,
+               popupRight = popupOffset + padding + width + padding,
+               overlapLeft = ( originOffset + popupLeft ) - containerLeft,
+               overlapRight = containerRight - ( originOffset + popupRight );
+
+       // Prevent transition from being interrupted
+       clearTimeout( this.transitionTimeout );
+       if ( transition ) {
+               // Enable transition
+               this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
+       }
+
+       if ( overlapRight < 0 ) {
+               popupOffset += overlapRight;
+       } else if ( overlapLeft < 0 ) {
+               popupOffset -= overlapLeft;
+       }
+
+       // Position body relative to anchor and resize
+       this.$popup.css( {
+               'left': popupOffset,
+               'width': width,
+               'height': height === undefined ? 'auto' : height
+       } );
+
+       if ( transition ) {
+               // Prevent transitioning after transition is complete
+               this.transitionTimeout = setTimeout( OO.ui.bind( function () {
+                       this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
+               }, this ), 200 );
+       } else {
+               // Prevent transitioning immediately
+               this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
+       }
+
+       return this;
+};
+/**
+ * Button that shows and hides a popup.
+ *
+ * @class
+ * @extends OO.ui.ButtonWidget
+ * @mixins OO.ui.PopuppableElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
+       // Parent constructor
+       OO.ui.ButtonWidget.call( this, config );
+
+       // Mixin constructors
+       OO.ui.PopuppableElement.call( this, config );
+
+       // Initialization
+       this.$element
+               .addClass( 'oo-ui-popupButtonWidget' )
+               .append( this.popup.$element );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
+
+OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopuppableElement );
+
+/* Methods */
+
+/**
+ * Handles mouse click events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse click event
+ */
+OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) {
+       // Skip clicks within the popup
+       if ( $.contains( this.popup.$element[0], e.target ) ) {
+               return;
+       }
+
+       if ( !this.disabled ) {
+               if ( this.popup.isVisible() ) {
+                       this.hidePopup();
+               } else {
+                       this.showPopup();
+               }
+               OO.ui.ButtonWidget.prototype.onClick.call( this );
+       }
+       return false;
+};
+/**
+ * Creates an OO.ui.SearchWidget object.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string|jQuery} [placeholder] Placeholder text for query input
+ * @cfg {string} [value] Initial query value
+ */
+OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
+       // Configuration intialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Widget.call( this, config );
+
+       // Properties
+       this.query = new OO.ui.TextInputWidget( {
+               '$': this.$,
+               'icon': 'search',
+               'placeholder': config.placeholder,
+               'value': config.value
+       } );
+       this.results = new OO.ui.SelectWidget( { '$': this.$ } );
+       this.$query = this.$( '<div>' );
+       this.$results = this.$( '<div>' );
+
+       // Events
+       this.query.connect( this, {
+               'change': 'onQueryChange',
+               'enter': 'onQueryEnter'
+       } );
+       this.results.connect( this, {
+               'highlight': 'onResultsHighlight',
+               'select': 'onResultsSelect'
+       } );
+       this.query.$input.on( 'keydown', OO.ui.bind( this.onQueryKeydown, this ) );
+
+       // Initialization
+       this.$query
+               .addClass( 'oo-ui-searchWidget-query' )
+               .append( this.query.$element );
+       this.$results
+               .addClass( 'oo-ui-searchWidget-results' )
+               .append( this.results.$element );
+       this.$element
+               .addClass( 'oo-ui-searchWidget' )
+               .append( this.$results, this.$query );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
+
+/* Events */
+
+/**
+ * @event highlight
+ * @param {Object|null} item Item data or null if no item is highlighted
+ */
+
+/**
+ * @event select
+ * @param {Object|null} item Item data or null if no item is selected
+ */
+
+/* Methods */
+
+/**
+ * Handle query key down events.
+ *
+ * @method
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
+       var highlightedItem, nextItem,
+               dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
+
+       if ( dir ) {
+               highlightedItem = this.results.getHighlightedItem();
+               if ( !highlightedItem ) {
+                       highlightedItem = this.results.getSelectedItem();
+               }
+               nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
+               this.results.highlightItem( nextItem );
+               nextItem.scrollElementIntoView();
+       }
+};
+
+/**
+ * Handle select widget select events.
+ *
+ * Clears existing results. Subclasses should repopulate items according to new query.
+ *
+ * @method
+ * @param {string} value New value
+ */
+OO.ui.SearchWidget.prototype.onQueryChange = function () {
+       // Reset
+       this.results.clearItems();
+};
+
+/**
+ * Handle select widget enter key events.
+ *
+ * Selects highlighted item.
+ *
+ * @method
+ * @param {string} value New value
+ */
+OO.ui.SearchWidget.prototype.onQueryEnter = function () {
+       // Reset
+       this.results.selectItem( this.results.getHighlightedItem() );
+};
+
+/**
+ * Handle select widget highlight events.
+ *
+ * @method
+ * @param {OO.ui.OptionWidget} item Highlighted item
+ * @fires highlight
+ */
+OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
+       this.emit( 'highlight', item ? item.getData() : null );
+};
+
+/**
+ * Handle select widget select events.
+ *
+ * @method
+ * @param {OO.ui.OptionWidget} item Selected item
+ * @fires select
+ */
+OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
+       this.emit( 'select', item ? item.getData() : null );
+};
+
+/**
+ * Get the query input.
+ *
+ * @method
+ * @returns {OO.ui.TextInputWidget} Query input
+ */
+OO.ui.SearchWidget.prototype.getQuery = function () {
+       return this.query;
+};
+
+/**
+ * Get the results list.
+ *
+ * @method
+ * @returns {OO.ui.SelectWidget} Select list
+ */
+OO.ui.SearchWidget.prototype.getResults = function () {
+       return this.results;
+};
+/**
+ * Creates an OO.ui.TextInputWidget object.
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [placeholder] Placeholder text
+ * @cfg {string} [icon] Symbolic name of icon
+ * @cfg {boolean} [multiline=false] Allow multiple lines of text
+ */
+OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.InputWidget.call( this, config );
+
+       // Properties
+       this.pending = 0;
+       this.multiline = !!config.multiline;
+
+       // Events
+       this.$input.on( 'keypress', OO.ui.bind( this.onKeyPress, this ) );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-textInputWidget' );
+       if ( config.icon ) {
+               this.$element.addClass( 'oo-ui-textInputWidget-decorated' );
+               this.$element.append(
+                       this.$( '<span>' )
+                               .addClass( 'oo-ui-textInputWidget-icon oo-ui-icon-' + config.icon )
+                               .mousedown( OO.ui.bind( function () {
+                                       this.$input.focus();
+                                       return false;
+                               }, this ) )
+               );
+       }
+       if ( config.placeholder ) {
+               this.$input.attr( 'placeholder', config.placeholder );
+       }
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
+
+/* Events */
+
+/**
+ * User presses enter inside the text box.
+ *
+ * Not called if input is multiline.
+ *
+ * @event enter
+ */
+
+/* Methods */
+
+/**
+ * Handles key press events.
+ *
+ * @param {jQuery.Event} e Key press event
+ * @fires enter If enter key is pressed and input is not multiline
+ */
+OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
+       if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
+               this.emit( 'enter' );
+       }
+};
+
+/**
+ * Get input element.
+ *
+ * @method
+ * @param {Object} [config] Configuration options
+ * @returns {jQuery} Input element
+ */
+OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
+       return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="text" />' );
+};
+
+/* Methods */
+
+/**
+ * Checks if input is pending.
+ *
+ * @method
+ * @returns {boolean} Input is pending
+ */
+OO.ui.TextInputWidget.prototype.isPending = function () {
+       return !!this.pending;
+};
+
+/**
+ * Increases the pending stack.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.pushPending = function () {
+       this.pending++;
+       this.$element.addClass( 'oo-ui-textInputWidget-pending' );
+       this.$input.addClass( 'oo-ui-texture-pending' );
+       return this;
+};
+
+/**
+ * Reduces the pending stack.
+ *
+ * Clamped at zero.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.popPending = function () {
+       this.pending = Math.max( 0, this.pending - 1 );
+       if ( !this.pending ) {
+               this.$element.removeClass( 'oo-ui-textInputWidget-pending' );
+               this.$input.removeClass( 'oo-ui-texture-pending' );
+       }
+       return this;
+};
+/**
+ * Creates an OO.ui.TextInputMenuWidget object.
+ *
+ * @class
+ * @extends OO.ui.MenuWidget
+ *
+ * @constructor
+ * @param {OO.ui.TextInputWidget} input Text input widget to provide menu for
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$container=input.$element] Element to render menu under
+ */
+OO.ui.TextInputMenuWidget = function OoUiTextInputMenuWidget( input, config ) {
+       // Parent constructor
+       OO.ui.MenuWidget.call( this, config );
+
+       // Properties
+       this.input = input;
+       this.$container = config.$container || this.input.$element;
+       this.onWindowResizeHandler = OO.ui.bind( this.onWindowResize, this );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-textInputMenuWidget' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.TextInputMenuWidget, OO.ui.MenuWidget );
+
+/* Methods */
+
+/**
+ * Handle window resize event.
+ *
+ * @method
+ * @param {jQuery.Event} e Window resize event
+ */
+OO.ui.TextInputMenuWidget.prototype.onWindowResize = function () {
+       this.position();
+};
+
+/**
+ * Shows the menu.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.TextInputMenuWidget.prototype.show = function () {
+       // Parent method
+       OO.ui.MenuWidget.prototype.show.call( this );
+
+       this.position();
+       this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
+       return this;
+};
+
+/**
+ * Hides the menu.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.TextInputMenuWidget.prototype.hide = function () {
+       // Parent method
+       OO.ui.MenuWidget.prototype.hide.call( this );
+
+       this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
+       return this;
+};
+
+/**
+ * Positions the menu.
+ *
+ * @method
+ * @chainable
+ */
+OO.ui.TextInputMenuWidget.prototype.position = function () {
+       var frameOffset,
+               $container = this.$container,
+               dimensions = $container.offset();
+
+       // Position under input
+       dimensions.top += $container.height();
+
+       // Compensate for frame position if in a differnt frame
+       if ( this.input.$.frame && this.input.$.context !== this.$element[0].ownerDocument ) {
+               frameOffset = OO.ui.Element.getRelativePosition(
+                       this.input.$.frame.$element, this.$element.offsetParent()
+               );
+               dimensions.left += frameOffset.left;
+               dimensions.top += frameOffset.top;
+       } else {
+               // Fix for RTL (for some reason, no need to fix if the frameoffset is set)
+               if ( this.$element.css( 'direction' ) === 'rtl' ) {
+                       dimensions.right = this.$element.parent().position().left -
+                               dimensions.width - dimensions.left;
+                       // Erase the value for 'left':
+                       delete dimensions.left;
+               }
+       }
+
+       this.$element.css( dimensions );
+       this.setIdealSize( $container.width() );
+       return this;
+};
+/**
+ * Mixin for widgets with a boolean state.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [value=false] Initial value
+ */
+OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Properties
+       this.value = null;
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-toggleWidget' );
+       this.setValue( !!config.value );
+};
+
+/* Events */
+
+/**
+ * @event change
+ * @param {boolean} value Changed value
+ */
+
+/* Methods */
+
+/**
+ * Get the value of the toggle.
+ *
+ * @method
+ * @returns {boolean} Toggle value
+ */
+OO.ui.ToggleWidget.prototype.getValue = function () {
+       return this.value;
+};
+
+/**
+ * Set the value of the toggle.
+ *
+ * @method
+ * @param {boolean} value New value
+ * @fires change
+ * @chainable
+ */
+OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
+       value = !!value;
+       if ( this.value !== value ) {
+               this.value = value;
+               this.emit( 'change', value );
+               this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
+               this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
+       }
+       return this;
+};
+/**
+ * @class
+ * @extends OO.ui.ButtonWidget
+ * @mixins OO.ui.ToggleWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [value=false] Initial value
+ */
+OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.ButtonWidget.call( this, config );
+
+       // Mixin constructors
+       OO.ui.ToggleWidget.call( this, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-toggleButtonWidget' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget );
+
+OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ToggleButtonWidget.prototype.onClick = function () {
+       if ( !this.disabled ) {
+               this.setValue( !this.value );
+       }
+
+       // Parent method
+       return OO.ui.ButtonWidget.prototype.onClick.call( this );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
+       value = !!value;
+       if ( value !== this.value ) {
+               this.setActive( value );
+       }
+
+       // Parent method
+       OO.ui.ToggleWidget.prototype.setValue.call( this, value );
+
+       return this;
+};
+/**
+ * @class
+ * @abstract
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.ToggleWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [value=false] Initial value
+ */
+OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
+       // Parent constructor
+       OO.ui.Widget.call( this, config );
+
+       // Mixin constructors
+       OO.ui.ToggleWidget.call( this, config );
+
+       // Properties
+       this.dragging = false;
+       this.dragStart = null;
+       this.sliding = false;
+       this.$on = this.$( '<span>' );
+       this.$grip = this.$( '<span>' );
+
+       // Events
+       this.$element.on( 'click', OO.ui.bind( this.onClick, this ) );
+
+       // Initialization
+       this.$on.addClass( 'oo-ui-toggleSwitchWidget-on' );
+       this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
+       this.$element
+               .addClass( 'oo-ui-toggleSwitchWidget' )
+               .append( this.$on, this.$grip );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.Widget );
+
+OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
+
+/* Methods */
+
+/**
+ * Handles mouse down events.
+ *
+ * @method
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
+       if ( !this.disabled && e.which === 1 ) {
+               this.setValue( !this.value );
+       }
+};
+}() );
diff --git a/resources/oojs/oojs-ui.svg.css b/resources/oojs/oojs-ui.svg.css
new file mode 100644 (file)
index 0000000..185bcf0
--- /dev/null
@@ -0,0 +1,1880 @@
+/*!
+ * OOjs UI v0.1.0-pre-svg (a290673bbd)
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2014 OOjs Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: Wed Feb 12 2014 13:52:08 GMT-0800 (PST)
+ */
+/*csslint vendor-prefix:false */
+
+/* Textures */
+
+.oo-ui-texture-pending {
+       /* @embed */
+       background-image: url(images/textures/pending.gif);
+}
+
+.oo-ui-texture-transparency {
+       /* @embed */
+       background-image: url(images/textures/transparency.png);
+}
+
+/* Animation */
+
+@-webkit-keyframes oo-ui-zoom-in {
+       from { -webkit-transform: scale(0.5); }
+       to { -webkit-transform: scale(1); }
+}
+
+@-moz-keyframes oo-ui-zoom-in {
+       from { -moz-transform: scale(0.5); }
+       to { -moz-transform: scale(1); }
+}
+
+@-o-keyframes oo-ui-zoom-in {
+       from { -o-transform: scale(0.5); }
+       to { -o-transform: scale(1); }
+}
+
+@keyframes oo-ui-zoom-in {
+       from { transform: scale(0.5); }
+       to { transform: scale(1); }
+}
+
+@-webkit-keyframes oo-ui-fade-in {
+       from { opacity: 0; }
+       to { opacity: 1; }
+}
+
+@-moz-keyframes oo-ui-fade-in {
+       from { opacity: 0; }
+       to { opacity: 1; }
+}
+
+@-o-keyframes oo-ui-fade-in {
+       from { opacity: 0; }
+       to { opacity: 1; }
+}
+
+@keyframes oo-ui-fade-in {
+       from { opacity: 0; }
+       to { opacity: 1; }
+}
+
+/* RTL Definitions */
+
+/* @noflip */
+.oo-ui-rtl {
+       direction: rtl;
+}
+/* @noflip */
+.oo-ui-ltr {
+       direction: ltr;
+}
+.oo-ui-dialog {
+       position: fixed;
+       top: 0;
+       right: 0;
+       bottom: 0;
+       left: 0;
+       padding: 1em;
+       line-height: 1em;
+       background-color: #fff;
+       background-color: rgba(255,255,255,0.5);
+       -webkit-animation: oo-ui-fade-in 250ms ease-in-out 0 1 normal;
+       -moz-animation: oo-ui-fade-in 250ms ease-in-out 0 1 normal;
+       -o-animation: oo-ui-fade-in 250ms ease-in-out 0 1 normal;
+       animation: oo-ui-fade-in 250ms ease-in-out 0 1 normal;
+}
+
+.oo-ui-dialog-closing {
+       -webkit-animation: oo-ui-fade-in 250ms ease-in-out 0 1 reverse;
+       -moz-animation: oo-ui-fade-in 250ms ease-in-out 0 1 reverse;
+       -o-animation: oo-ui-fade-in 250ms ease-in-out 0 1 reverse;
+       animation: oo-ui-fade-in 250ms ease-in-out 0 1 reverse;
+}
+
+.oo-ui-dialog .oo-ui-window-frame {
+       position: fixed;
+       top: 1em;
+       right: 0;
+       bottom: 1em;
+       left: 0;
+       margin: auto;
+       width: 800px;
+       min-height: 12em;
+       max-height: 600px;
+       background-color: #fff;
+       border: solid 1px #ccc;
+       border-radius: 0.5em;
+       box-shadow: 0 0.2em 1em rgba(0, 0, 0, 0.3);
+       overflow: hidden;
+       -webkit-animation: oo-ui-zoom-in 250ms ease-in-out 0 1 normal;
+       -moz-animation: oo-ui-zoom-in 250ms ease-in-out 0 1 normal;
+       -o-animation: oo-ui-zoom-in 250ms ease-in-out 0 1 normal;
+       animation: oo-ui-zoom-in 250ms ease-in-out 0 1 normal;
+}
+
+.oo-ui-dialog .oo-ui-window-frame.oo-ui-window-frame-small {
+       max-width: 600px;
+       max-height: 400px;
+}
+
+.oo-ui-dialog-closing .oo-ui-window-frame {
+       -webkit-animation: oo-ui-zoom-in 250ms ease-in-out 0 1 reverse;
+       -moz-animation: oo-ui-zoom-in 250ms ease-in-out 0 1 reverse;
+       -o-animation: oo-ui-zoom-in 250ms ease-in-out 0 1 reverse;
+       animation: oo-ui-zoom-in 250ms ease-in-out 0 1 reverse;
+}
+
+.oo-ui-dialog .oo-ui-frame {
+       width: 100%;
+       height: 100%;
+}
+
+.oo-ui-dialog-content .oo-ui-window-head,
+.oo-ui-dialog-content .oo-ui-window-body,
+.oo-ui-dialog-content .oo-ui-window-foot {
+       position: absolute;
+       left: 0;
+       right: 0;
+       -webkit-box-sizing: border-box;
+       -moz-box-sizing: border-box;
+       box-sizing: border-box;
+       overflow: hidden;
+}
+
+.oo-ui-dialog-content .oo-ui-window-head {
+       top: 0;
+       height: 3.8em;
+       padding: 0.5em;
+}
+
+.oo-ui-dialog-content .oo-ui-window-foot {
+       bottom: 0;
+       height: 4.8em;
+       padding: 1em;
+}
+
+.oo-ui-dialog-content .oo-ui-window-body {
+       box-shadow: 0 0 0.66em rgba(0,0,0,0.25);
+       top: 3.8em;
+       bottom: 4.8em;
+}
+
+.oo-ui-dialog-content-footless .oo-ui-window-body {
+       bottom: 0;
+}
+
+.oo-ui-dialog-content-footless .oo-ui-window-foot {
+       display: none;
+}
+
+.oo-ui-dialog-content .oo-ui-window-icon {
+       width: 2.4em;
+       height: 2.8em;
+       line-height: 2.8em;
+}
+
+.oo-ui-dialog-content .oo-ui-window-title {
+       line-height: 2.8em;
+}
+
+.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-buttonedElement-framed {
+       float: left;
+       margin: 0.125em 0.25em;
+}
+
+.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary,
+.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive,
+.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-buttonedElement-framed.oo-ui-flaggableElement-destructive {
+       float: right;
+}
+
+.oo-ui-dialog-content .oo-ui-window-closeButton {
+       float: right;
+       margin: 0.25em 0.25em;
+}
+
+/* OO.ui.ButtonedElement */
+
+a.oo-ui-buttonedElement-button {
+       color: #333;
+       cursor: pointer;
+       display: inline-block;
+       vertical-align: middle;
+       -webkit-touch-callout: none;
+       -webkit-user-select: none;
+       -moz-user-select: none;
+       -ms-user-select: none;
+       user-select: none;
+}
+
+.oo-ui-buttonedElement .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator,
+.oo-ui-buttonedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+       display: none;
+}
+
+.oo-ui-buttonedElement.oo-ui-indicatedElement .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator,
+.oo-ui-buttonedElement.oo-ui-iconedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+       opacity: 0.8;
+       display: inline-block;
+       vertical-align: middle;
+       background-position: center center;
+       background-repeat: no-repeat;
+       width: 1.9em;
+       height: 1.9em;
+}
+
+.oo-ui-buttonedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+       margin-left: 0;
+}
+
+.oo-ui-buttonedElement .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator {
+       margin-right: -0.75em;
+}
+
+.oo-ui-buttonedElement-frameless {
+       display: inline-block;
+       position: relative;
+       -webkit-transition: opacity 200ms;
+       -moz-transition: opacity 200ms;
+       -o-transition: opacity 200ms;
+       transition: opacity 200ms;
+}
+
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:hover > .oo-ui-iconedElement-icon,
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:focus > .oo-ui-iconedElement-icon {
+       opacity: 1;
+}
+
+.oo-ui-buttonedElement-frameless.oo-ui-widget-disabled .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+       opacity: 0.2;
+}
+
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+       display: inline-block;
+       vertical-align: middle;
+       margin-left: 0.25em;
+       color: #333;
+}
+
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:hover > .oo-ui-labeledElement-label,
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:focus > .oo-ui-labeledElement-label {
+       color: #000;
+}
+
+.oo-ui-buttonedElement-frameless.oo-ui-widget-disabled .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+       color: #ccc;
+}
+
+/* OO.ui.ButtonWidget */
+
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
+       display: inline-block;
+       font-size: 1em;
+       margin: 0.1em 0;
+       padding: 0.2em 0.8em;
+       border-radius: 0.3em;
+       vertical-align: top;
+       text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
+       box-shadow: 0 0.1em 0.25em rgba(0, 0, 0, 0.1);
+       text-align: center;
+
+       /* Animation */
+       -webkit-transition: border-color 100ms;
+       -moz-transition: border-color 100ms;
+       -o-transition: border-color 100ms;
+       transition: border-color 100ms;
+
+       /* Gray */
+       border: 1px #c9c9c9 solid;
+       background-color: #dddddd;
+       filter: progid:DXImageTransform.Microsoft.gradient(
+               GradientType=0,startColorstr=#ffffff, endColorstr=#dddddd
+       );
+       background-image: -webkit-gradient(
+               linear, right top, right bottom, color-stop(0%,#ffffff), color-stop(100%,#dddddd)
+       );
+       background-image: -webkit-linear-gradient(top, #ffffff 0%, #dddddd 100%);
+       background-image: -moz-linear-gradient(top, #ffffff 0%, #dddddd 100%);
+       background-image: -ms-linear-gradient(top, #ffffff 0%, #dddddd 100%);
+       background-image: -o-linear-gradient(top, #ffffff 0%, #dddddd 100%);
+       background-image: linear-gradient(top, #ffffff 0%, #dddddd 100%);
+}
+
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button:hover,
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button:focus {
+       border-color: #aaa;
+}
+
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button:active,
+.oo-ui-buttonedElement-framed.oo-ui-buttonedElement-active .oo-ui-buttonedElement-button {
+       box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.07);
+       color: black;
+
+       /* Gray */
+       border-color: #c9c9c9;
+       background-color: #dddddd;
+       filter: progid:DXImageTransform.Microsoft.gradient(
+               GradientType=0,startColorstr=#dddddd, endColorstr=#ffffff
+       );
+       background-image: -webkit-gradient(
+               linear, right top, right bottom, color-stop(0%,#dddddd), color-stop(100%,#ffffff)
+       );
+       background-image: -webkit-linear-gradient(top, #dddddd 0%, #ffffff 100%);
+       background-image: -moz-linear-gradient(top, #dddddd 0%, #ffffff 100%);
+       background-image: -ms-linear-gradient(top, #dddddd 0%, #ffffff 100%);
+       background-image: -o-linear-gradient(top, #dddddd 0%, #ffffff 100%);
+       background-image: linear-gradient(top, #dddddd 0%, #ffffff 100%);
+}
+
+.oo-ui-buttonedElement-framed.oo-ui-iconedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+       margin-left: -0.5em;
+       margin-right: -0.5em;
+}
+
+.oo-ui-buttonedElement-framed.oo-ui-iconedElement.oo-ui-labeledElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+       margin-left: -0.5em;
+       margin-right: 0;
+}
+
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+       display: inline-block;
+       vertical-align: middle;
+       line-height: 1.9em;
+}
+
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-destructive .oo-ui-buttonedElement-button {
+       /* Red text */
+       color: #d45353;
+}
+
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button {
+       /* Green */
+       border: solid 1px #b8d892;
+       background-color: #f0fbe1;
+       filter: progid:DXImageTransform.Microsoft.gradient(
+               GradientType=0,startColorstr=#f0fbe1, endColorstr=#c3e59a
+       );
+       background-image: -webkit-gradient(
+               linear, right top, right bottom, color-stop(0%,#f0fbe1), color-stop(100%,#c3e59a)
+       );
+       background-image: -webkit-linear-gradient(top, #f0fbe1 0%, #c3e59a 100%);
+       background-image: -moz-linear-gradient(top, #f0fbe1 0%, #c3e59a 100%);
+       background-image: -ms-linear-gradient(top, #f0fbe1 0%, #c3e59a 100%);
+       background-image: -o-linear-gradient(top, #f0fbe1 0%, #c3e59a 100%);
+       background-image: linear-gradient(top, #f0fbe1 0%, #c3e59a 100%);
+}
+
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button:hover,
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button:focus {
+       border-color: #adcb89;
+}
+
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button:active,
+.oo-ui-buttonedElement-framed.oo-ui-buttonedElement-active.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button {
+       /* Green */
+       border: solid 1px #b8d892;
+       background-color: #c3e59a;
+       filter: progid:DXImageTransform.Microsoft.gradient(
+               GradientType=0,startColorstr=#c3e59a, endColorstr=#f0fbe1
+       );
+       background-image: -webkit-gradient(
+               linear, right top, right bottom, color-stop(0%,#c3e59a), color-stop(100%,#f0fbe1)
+       );
+       background-image: -webkit-linear-gradient(top, #c3e59a 0%, #f0fbe1 100%);
+       background-image: -moz-linear-gradient(top, #c3e59a 0%, #f0fbe1 100%);
+       background-image: -ms-linear-gradient(top, #c3e59a 0%, #f0fbe1 100%);
+       background-image: -o-linear-gradient(top, #c3e59a 0%, #f0fbe1 100%);
+       background-image: linear-gradient(top, #c3e59a 0%, #f0fbe1 100%);
+}
+
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button {
+       /* Blue */
+       border: solid 1px #a6cee1;
+       background-color: #eaf4fa;
+       filter: progid:DXImageTransform.Microsoft.gradient(
+               GradientType=0,startColorstr=#eaf4fa, endColorstr=#b0d9ee
+       );
+       background-image: -webkit-gradient(
+               linear, right top, right bottom, color-stop(0%,#eaf4fa), color-stop(100%,#b0d9ee)
+       );
+       background-image: -webkit-linear-gradient(top, #eaf4fa 0%, #b0d9ee 100%);
+       background-image: -moz-linear-gradient(top, #eaf4fa 0%, #b0d9ee 100%);
+       background-image: -ms-linear-gradient(top, #eaf4fa 0%, #b0d9ee 100%);
+       background-image: -o-linear-gradient(top, #eaf4fa 0%, #b0d9ee 100%);
+       background-image: linear-gradient(top, #eaf4fa 0%, #b0d9ee 100%);
+}
+
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button:hover,
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button:focus {
+       border-color: #9dc2d4;
+}
+
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button:active,
+.oo-ui-buttonedElement-framed.oo-ui-buttonedElement-active.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button {
+       /* Blue */
+       border: solid 1px #a6cee1;
+       background-color: #b0d9ee;
+       filter: progid:DXImageTransform.Microsoft.gradient(
+               GradientType=0,startColorstr=#b0d9ee, endColorstr=#eaf4fa
+       );
+       background-image: -webkit-gradient(
+               linear, right top, right bottom, color-stop(0%,#b0d9ee), color-stop(100%,#eaf4fa)
+       );
+       background-image: -webkit-linear-gradient(top, #b0d9ee 0%, #eaf4fa 100%);
+       background-image: -moz-linear-gradient(top, #b0d9ee 0%, #eaf4fa 100%);
+       background-image: -ms-linear-gradient(top, #b0d9ee 0%, #eaf4fa 100%);
+       background-image: -o-linear-gradient(top, #b0d9ee 0%, #eaf4fa 100%);
+       background-image: linear-gradient(top, #b0d9ee 0%, #eaf4fa 100%);
+}
+
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button:active,
+.oo-ui-buttonedElement-framed.oo-ui-buttonedElement-active.oo-ui-widget-disabled .oo-ui-buttonedElement-button:active {
+       opacity: 0.5;
+       cursor: default;
+       box-shadow: none;
+       color: #333;
+       background: #eee;
+}
+
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button:hover,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button:focus {
+       border-color: #ccc;
+       box-shadow: none;
+}
+
+/* OO.ui.LabeledElement */
+
+.oo-ui-labeledElement-label {
+       display: block;
+}
+
+.oo-ui-clippableElement-clippable {
+       -webkit-box-sizing: border-box;
+       -moz-box-sizing: border-box;
+       box-sizing: border-box;
+}
+.oo-ui-frame {
+       margin: 0;
+       padding: 0;
+}
+
+.oo-ui-frame-body {
+       margin: 0;
+       padding: 0;
+       background: none;
+}
+
+.oo-ui-frame-content {
+       font-family: sans-serif;
+       font-size: 0.8em;
+}
+/* OO.ui.GridLayout */
+/* OO.ui.PanelLayout */
+
+.oo-ui-gridLayout,
+.oo-ui-panelLayout {
+       position: absolute;
+       top: 0;
+       left: 0;
+       right: 0;
+       bottom: 0;
+}
+
+.oo-ui-panelLayout-scrollable {
+       overflow-y: auto;
+}
+
+.oo-ui-panelLayout-padded {
+       padding: 2em;
+}
+
+/* OO.ui.FieldsetLayout */
+
+.oo-ui-fieldsetLayout {
+       border: none;
+       margin: 0;
+       padding: 0;
+}
+
+.oo-ui-fieldsetLayout + .oo-ui-fieldsetLayout {
+       margin-top: 2em;
+}
+
+.oo-ui-fieldsetLayout-labeled {
+       margin-top: -0.75em;
+}
+
+.oo-ui-fieldsetLayout > legend.oo-ui-labeledElement-label {
+       font-size: 1.5em;
+       margin-bottom: 0.5em;
+}
+
+.oo-ui-fieldsetLayout-decorated > legend.oo-ui-labeledElement-label {
+       padding-left: 1.75em;
+       background-position: left center;
+       background-repeat: no-repeat;
+}
+
+/* OO.ui.BookletLayout */
+
+.oo-ui-bookletLayout-stackLayout .oo-ui-panelLayout {
+       padding: 1.5em;
+       width: 100%;
+       -webkit-box-sizing: border-box;
+       -moz-box-sizing: border-box;
+       box-sizing: border-box;
+}
+
+.oo-ui-bookletLayout-stackLayout .oo-ui-panelLayout-scrollable {
+       overflow-y: auto;
+}
+
+.oo-ui-bookletLayout-stackLayout .oo-ui-panelLayout-padded {
+       padding: 2em;
+}
+
+.oo-ui-bookletLayout-outlinePanel {
+       border-right: solid 1px #ddd;
+}
+
+.oo-ui-bookletLayout-outlinePanel-editable .oo-ui-outlineWidget {
+       position: absolute;
+       top: 0;
+       left: 0;
+       right: 0;
+       bottom: 3em;
+       overflow-y: auto;
+}
+
+.oo-ui-bookletLayout-outlinePanel .oo-ui-outlineControlsWidget {
+       position: absolute;
+       bottom: 0;
+       left: 0;
+       right: 0;
+       box-shadow: 0 0 0.25em rgba(0,0,0,0.25);
+}
+
+.oo-ui-stackLayout > .oo-ui-panelLayout {
+       display: none;
+}
+
+.oo-ui-stackLayout-continuous > .oo-ui-panelLayout {
+       display: block;
+       position: relative;
+       margin-bottom: 1em;
+       box-shadow: 0 0 0.5em rgba(0,0,0,0.25);
+}
+
+.oo-ui-stackLayout-continuous > .oo-ui-panelLayout:last-child {
+       margin-bottom: 0;
+}
+/* OO.ui.PopupTool */
+
+.oo-ui-popupTool .oo-ui-popupWidget {
+       margin-left: 1.25em;
+       font-size: 0.8em;
+}
+
+.oo-ui-popupTool .oo-ui-popupWidget-popup,
+.oo-ui-popupTool .oo-ui-popupWidget-tail {
+       z-index: 4;
+}
+.oo-ui-toolbar {
+       clear: both;
+}
+
+.oo-ui-toolbar-bar {
+       border-bottom: solid 1px #ccc;
+       background-color: white;
+       /* @embed */
+       background-image: url(images/fade-up.png);
+       background-position: left bottom;
+       background-repeat: repeat-x;
+       padding-bottom: 1px;
+       line-height: 1em;
+}
+
+.oo-ui-toolbar-bar .oo-ui-toolbar-bar {
+       border: none;
+       background: none;
+}
+
+.oo-ui-toolbar-bottom .oo-ui-toolbar-bar {
+       position: absolute;
+}
+
+.oo-ui-toolbar-actions {
+       float: right;
+}
+
+.oo-ui-toolbar-tools {
+       float: left;
+}
+
+.oo-ui-toolbar-tools,
+.oo-ui-toolbar-actions,
+.oo-ui-toolbar-shadow {
+       -webkit-user-select: none;
+       -moz-user-select: none;
+       -ms-user-select: none;
+       -o-user-select: none;
+       user-select: none;
+}
+
+.oo-ui-toolbar-actions .oo-ui-popupWidget {
+       -webkit-touch-callout: default;
+       -webkit-user-select: all;
+       -moz-user-select: all;
+       -ms-user-select: all;
+       user-select: all;
+}
+
+.oo-ui-toolbar-shadow {
+       /* @embed */
+       background-image: url(images/toolbar-shadow.png);
+       background-position: left top;
+       background-repeat: repeat-x;
+       position: absolute;
+       bottom: -9px;
+       height: 9px;
+       width: 100%;
+       pointer-events: none;
+       -webkit-transition: opacity 500ms ease-in-out;
+       -moz-transition: opacity 500ms ease-in-out;
+       -o-transition: opacity 500ms ease-in-out;
+       transition: opacity 500ms ease-in-out;
+       opacity: 0.125;
+}
+/* OO.ui.ToolGroup */
+
+.oo-ui-toolGroup {
+       display: inline-block;
+       margin: 0.3em;
+       vertical-align: middle;
+       border-radius: 0.25em;
+       border: solid 1px transparent;
+       -webkit-transition: border-color 300ms;
+       -moz-transition: border-color 300ms;
+       -o-transition: border-color 300ms;
+       transition: border-color 300ms;
+}
+
+.oo-ui-toolGroup:hover {
+       border-color: rgba(0,0,0,0.1);
+}
+
+.oo-ui-toolGroup-empty {
+       display: none;
+}
+
+.oo-ui-toolGroup .oo-ui-tool-link .oo-ui-tool-title {
+       color: #000;
+}
+
+.oo-ui-toolGroup .oo-ui-tool-link .oo-ui-iconedElement-icon {
+       background-position: center center;
+       background-repeat: no-repeat;
+}
+
+/* OO.ui.BarToolGroup */
+
+.oo-ui-barToolGroup > .oo-ui-iconedElement-icon,
+.oo-ui-barToolGroup > .oo-ui-iconedElement-label {
+       display: none;
+}
+
+.oo-ui-barToolGroup .oo-ui-tool {
+       display: inline-block;
+       position: relative;
+       vertical-align: top;
+       margin: -1px 0 -1px -1px;
+       border: solid 1px transparent;
+}
+
+.oo-ui-barToolGroup .oo-ui-tool-link {
+       display: block;
+       height: 1.5em;
+       padding: 0.25em;
+       cursor: pointer;
+}
+
+.oo-ui-barToolGroup
+       .oo-ui-tool-active:not(.oo-ui-widget-disabled) +
+       .oo-ui-tool-active:not(.oo-ui-widget-disabled)
+{
+       border-left-color: rgba(0,0,0,0.1);
+}
+
+.oo-ui-barToolGroup .oo-ui-tool:first-child {
+       border-top-left-radius: 0.25em;
+       border-bottom-left-radius: 0.25em;
+}
+
+.oo-ui-barToolGroup .oo-ui-tool:last-child {
+       margin-right: -1px;
+       border-top-right-radius: 0.25em;
+       border-bottom-right-radius: 0.25em;
+}
+
+.oo-ui-barToolGroup .oo-ui-tool:hover:not(.oo-ui-widget-disabled) {
+       border-color: rgba(0,0,0,0.2);
+}
+
+.oo-ui-barToolGroup .oo-ui-tool-active:not(.oo-ui-widget-disabled) {
+       border-color: rgba(0,0,0,0.2);
+}
+
+.oo-ui-barToolGroup .oo-ui-tool-link .oo-ui-iconedElement-icon {
+       display: block;
+       height: 1.5em;
+       width: 1.5em;
+       opacity: 0.8;
+}
+
+.oo-ui-barToolGroup .oo-ui-tool-link .oo-ui-tool-title {
+       display: none;
+}
+
+.oo-ui-barToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link {
+       cursor: default;
+}
+
+.oo-ui-barToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-iconedElement-icon {
+       opacity: 0.2;
+}
+
+.oo-ui-barToolGroup .oo-ui-tool:not(.oo-ui-widget-disabled) .oo-ui-tool-link .oo-ui-iconedElement-icon {
+       opacity: 0.8;
+}
+
+.oo-ui-barToolGroup .oo-ui-tool:hover:not(.oo-ui-widget-disabled) .oo-ui-tool-link .oo-ui-iconedElement-icon {
+       opacity: 1;
+}
+
+.oo-ui-barToolGroup .oo-ui-tool-title {
+       display: none;
+}
+
+/* OO.ui.PopupToolGroup */
+
+.oo-ui-popupToolGroup {
+       position: relative;
+       height: 2em;
+       min-width: 2.5em;
+}
+
+.oo-ui-popupToolGroup.oo-ui-indicatedElement.oo-ui-iconedElement {
+       min-width: 3.5em;
+}
+
+.oo-ui-popupToolGroup-handle {
+       display: block;
+       cursor: pointer;
+}
+
+.oo-ui-popupToolGroup-handle .oo-ui-indicatedElement-indicator,
+.oo-ui-popupToolGroup-handle .oo-ui-iconedElement-icon {
+       position: absolute;
+       top: 0;
+       width: 2em;
+       height: 2em;
+       background-position: center center;
+       background-repeat: no-repeat;
+       opacity: 0.8;
+}
+
+.oo-ui-popupToolGroup-handle .oo-ui-indicatedElement-indicator {
+       right: 0;
+}
+
+.oo-ui-popupToolGroup-handle .oo-ui-iconedElement-icon {
+       left: 0.25em;
+}
+
+.oo-ui-popupToolGroup-handle .oo-ui-labeledElement-label {
+       line-height: 2.6em;
+       font-size: 0.8em;
+       margin: 0 1em;
+}
+
+.oo-ui-popupToolGroup.oo-ui-iconedElement .oo-ui-popupToolGroup-handle .oo-ui-labeledElement-label {
+       margin-left: 3.25em;
+}
+
+.oo-ui-popupToolGroup.oo-ui-indicatedElement .oo-ui-popupToolGroup-handle .oo-ui-labeledElement-label {
+       margin-right: 2.25em;
+}
+
+.oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
+       display: none;
+       position: absolute;
+       top: 2em;
+       left: -1px;
+       z-index: 4;
+       border: solid 1px #ccc;
+       background-color: white;
+       box-shadow: 0 0.25em 1em rgba(0,0,0,0.25);
+}
+
+.oo-ui-popupToolGroup .oo-ui-toolGroup-tools .oo-ui-iconedElement-icon {
+       background-repeat: no-repeat;
+       background-position: center center;
+}
+
+.oo-ui-popupToolGroup-active:not(.oo-ui-widget-disabled) > .oo-ui-toolGroup-tools {
+       display: block;
+}
+
+.oo-ui-popupToolGroup-active:not(.oo-ui-widget-disabled) {
+       border-bottom-left-radius: 0;
+       border-bottom-right-radius: 0;
+}
+
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-iconedElement-icon {
+       display: inline-block;
+       vertical-align: middle;
+       height: 2em;
+       width: 2em;
+       margin-right: 0.5em;
+}
+
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-title {
+       display: inline-block;
+       vertical-align: middle;
+       line-height: 2em;
+       font-size: 0.8em;
+}
+
+.oo-ui-popupToolGroup .oo-ui-tool-accel {
+       display: none;
+}
+
+/* OO.ui.ListToolGroup */
+
+.oo-ui-listToolGroup .oo-ui-toolGroup-tools {
+       padding: 0.25em;
+}
+
+.oo-ui-listToolGroup .oo-ui-tool {
+       display: inline-block;
+       width: 100%;
+       -webkit-box-sizing: border-box;
+       -moz-box-sizing: border-box;
+       box-sizing: border-box;
+       border: solid 1px transparent;
+       margin: -1px 0;
+}
+
+.oo-ui-listToolGroup .oo-ui-tool-link {
+       display: block;
+       cursor: pointer;
+       white-space: nowrap;
+       padding-right: 0.5em;
+}
+
+.oo-ui-listToolGroup.oo-ui-popupToolGroup-active {
+       border-color: rgba(0,0,0,0.2);
+}
+
+.oo-ui-listToolGroup
+       .oo-ui-tool-active:not(.oo-ui-widget-disabled) +
+       .oo-ui-tool-active:not(.oo-ui-widget-disabled)
+{
+       border-top-color: rgba(0,0,0,0.1);
+}
+
+.oo-ui-listToolGroup .oo-ui-tool:hover:not(.oo-ui-widget-disabled) {
+       border-color: rgba(0,0,0,0.2);
+}
+
+.oo-ui-listToolGroup .oo-ui-tool-active:not(.oo-ui-widget-disabled) {
+       border-color: rgba(0,0,0,0.2);
+}
+
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link {
+       cursor: default;
+}
+
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-title {
+       color: #ccc;
+}
+
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-iconedElement-icon {
+       opacity: 0.2;
+}
+
+.oo-ui-listToolGroup .oo-ui-tool:not(.oo-ui-widget-disabled) .oo-ui-tool-link .oo-ui-iconedElement-icon {
+       opacity: 0.8;
+}
+
+.oo-ui-listToolGroup .oo-ui-tool:hover:not(.oo-ui-widget-disabled) .oo-ui-tool-link .oo-ui-iconedElement-icon {
+       opacity: 1;
+}
+
+/* OO.ui.MenuToolGroup */
+
+.oo-ui-menuToolGroup {
+       border-color: rgba(0,0,0,0.1);
+}
+
+.oo-ui-menuToolGroup:hover {
+       border-color: rgba(0,0,0,0.2);
+}
+
+.oo-ui-menuToolGroup.oo-ui-popupToolGroup-active {
+       border-color: rgba(0,0,0,0.25);
+}
+
+.oo-ui-menuToolGroup .oo-ui-popupToolGroup-handle {
+       min-width: 8em;
+}
+
+.oo-ui-menuToolGroup .oo-ui-tool {
+       display: block;
+}
+
+.oo-ui-menuToolGroup .oo-ui-tool-link {
+       display: block;
+       cursor: pointer;
+       white-space: nowrap;
+       padding: 0.25em 1em 0.25em 0.25em;
+}
+
+.oo-ui-menuToolGroup .oo-ui-tool-link .oo-ui-iconedElement-icon {
+       background-image: none;
+}
+
+.oo-ui-menuToolGroup .oo-ui-tool-active .oo-ui-tool-link .oo-ui-iconedElement-icon {
+       /* @embed */
+       background-image: url(images/icons/check.png);
+}
+
+.oo-ui-menuToolGroup .oo-ui-tool:hover {
+       background-color: #e1f3ff;
+}
+
+/* Common */
+
+.oo-ui-barToolGroup .oo-ui-tool-active:not(.oo-ui-widget-disabled),
+.oo-ui-listToolGroup .oo-ui-tool-active:not(.oo-ui-widget-disabled),
+.oo-ui-popupToolGroup-active:not(.oo-ui-widget-disabled) {
+       /* @embed */
+       background-image: url(images/fade-down.png);
+       background-position: left top;
+       background-repeat: repeat-x;
+       box-shadow: inset 0 0.07em 0.07em 0 rgba(0, 0, 0, 0.07);
+}
+/* OO.ui.ButtonWidget */
+
+.oo-ui-buttonWidget {
+       display: inline-block;
+       vertical-align: middle;
+}
+
+/* OO.ui.PopupButtonWidget */
+
+.oo-ui-popupButtonWidget {
+       position: relative;
+}
+
+.oo-ui-popupButtonWidget .oo-ui-popupWidget {
+       position: absolute;
+       left: 1em;
+       cursor: auto;
+}
+
+/* OO.ui.ButtonGroupWidget */
+
+.oo-ui-buttonGroupWidget {
+       display: inline-block;
+       border-radius: 0.3em;
+       box-shadow: 0 0.1em 0.25em rgba(0, 0, 0, 0.1);
+}
+
+.oo-ui-buttonGroupWidget .oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
+       border-radius: 0;
+       margin-bottom: -1px;
+       margin-left: -1px;
+       box-shadow: none;
+}
+
+.oo-ui-buttonGroupWidget .oo-ui-buttonedElement-framed:first-child .oo-ui-buttonedElement-button {
+       border-bottom-left-radius: 0.3em;
+       border-top-left-radius: 0.3em;
+       margin-left: 0;
+}
+
+.oo-ui-buttonGroupWidget .oo-ui-buttonedElement-framed:last-child .oo-ui-buttonedElement-button {
+       border-bottom-right-radius: 0.3em;
+       border-top-right-radius: 0.3em;
+}
+
+/* OO.ui.SelectWidget */
+
+.oo-ui-selectWidget {
+       list-style: none;
+       margin: 0;
+       padding: 0;
+}
+
+/* OO.ui.OptionWidget */
+
+.oo-ui-optionWidget {
+       position: relative;
+       display: block;
+       border: none;
+       list-style: none;
+       margin: 0;
+       padding: 0.5em 2em 0.5em 3em;
+       cursor: pointer;
+}
+
+.oo-ui-optionWidget .oo-ui-labeledElement-label {
+       line-height: 1.5em;
+       white-space: nowrap;
+       text-overflow: ellipsis;
+       overflow: hidden;
+}
+
+.oo-ui-optionWidget-highlighted {
+       background-color: #e1f3ff;
+}
+
+.oo-ui-optionWidget-selected {
+       background-color: #a7dcff;
+}
+
+.oo-ui-optionWidget.oo-ui-widget-disabled {
+       cursor: default;
+}
+
+.oo-ui-optionWidget .oo-ui-iconedElement-icon,
+.oo-ui-optionWidget .oo-ui-indicatedElement-indicator {
+       position: absolute;
+       top: 50%;
+       width: 2em;
+       height: 2em;
+       margin-top: -1em;
+       background-repeat: no-repeat;
+       background-position: center center;
+}
+
+.oo-ui-optionWidget .oo-ui-iconedElement-icon {
+       left: 0.5em;
+}
+
+.oo-ui-optionWidget .oo-ui-indicatedElement-indicator {
+       right: 0.5em;
+}
+
+/* OO.ui.OutlineItemWidget */
+
+.oo-ui-outlineItemWidget {
+       position: relative;
+       padding: 0.75em 0.75em 0.75em 3.5em;
+       -webkit-user-select: none;
+       -moz-user-select: none;
+       -ms-user-select: none;
+       user-select: none;
+       cursor: pointer;
+       font-size: 1.1em;
+}
+
+.oo-ui-outlineItemWidget-level-1 {
+       padding-left: 5em;
+}
+
+.oo-ui-outlineItemWidget-level-2 {
+       padding-left: 6.5em;
+}
+
+.oo-ui-outlineItemWidget.oo-ui-optionWidget-selected {
+       background-color: #a7dcff;
+       text-shadow: 0 1px 1px rgba(255,255,255,0.5);
+}
+
+.oo-ui-outlineItemWidget-level-0 .oo-ui-iconedElement-icon {
+       left: 1em;
+}
+
+.oo-ui-outlineItemWidget-level-1 .oo-ui-iconedElement-icon {
+       left: 2.5em;
+}
+
+.oo-ui-outlineItemWidget-level-2 .oo-ui-iconedElement-icon {
+       left: 4em;
+}
+
+/* OO.ui.OutlineControlsWidget */
+
+.oo-ui-outlineControlsWidget {
+       height: 3em;
+       background-color: #fff;
+}
+
+.oo-ui-outlineControlsWidget-adders,
+.oo-ui-outlineControlsWidget-movers {
+       float: left;
+       -webkit-box-sizing: border-box;
+       -moz-box-sizing: border-box;
+       box-sizing: border-box;
+       height: 3em;
+       padding: 0.5em;
+}
+
+.oo-ui-outlineControlsWidget-adders {
+       float: left;
+}
+.oo-ui-outlineControlsWidget-movers {
+       float: right;
+}
+
+.oo-ui-outlineControlsWidget-adders .oo-ui-buttonWidget {
+       float: left;
+}
+
+.oo-ui-outlineControlsWidget-movers .oo-ui-buttonWidget {
+       float: right;
+}
+
+.oo-ui-outlineControlsWidget-adders .oo-ui-buttonWidget:first-child,
+.oo-ui-outlineControlsWidget-adders .oo-ui-buttonWidget:first-child:hover {
+       opacity: 0.25;
+       cursor: default;
+}
+
+/* OO.ui.InputLabelWidget */
+
+.oo-ui-inputLabelWidget {
+       padding: 0.5em 0;
+}
+
+/* OO.ui.TextInputWidget */
+
+.oo-ui-textInputWidget {
+       -webkit-box-sizing: border-box;
+       -moz-box-sizing: border-box;
+       box-sizing: border-box;
+       width: 20em;
+       position: relative;
+}
+
+.oo-ui-textInputWidget input,
+.oo-ui-textInputWidget input:focus[readonly],
+.oo-ui-widget-disabled.oo-ui-textInputWidget input:focus,
+.oo-ui-textInputWidget textarea,
+.oo-ui-textInputWidget textarea:focus[readonly],
+.oo-ui-widget-disabled.oo-ui-textInputWidget textarea:focus {
+       display: inline-block;
+       font-size: 1em;
+       font-family: sans-serif;
+       background-color: #f7f7f7;
+       border: solid 1px #ccc;
+       box-shadow: 0 0 0 white, inset 0 0.1em 0.2em #ddd;
+       padding: 0.5em;
+       border-radius: 0.25em;
+       -webkit-box-sizing: border-box;
+       -moz-box-sizing: border-box;
+       box-sizing: border-box;
+       width: 100%;
+       resize: none;
+
+       /* Animation */
+       -webkit-transition: border-color 200ms, box-shadow 200ms, background-color 200ms;
+       -moz-transition: border-color 200ms, box-shadow 200ms, background-color 200ms;
+       -o-transition: border-color 200ms, box-shadow 200ms, background-color 200ms;
+       transition: border-color 200ms, box-shadow 200ms, background-color 200ms;
+}
+
+.oo-ui-textInputWidget-pending input,
+.oo-ui-textInputWidget-pending textarea {
+       background-color: transparent;
+}
+
+.oo-ui-textInputWidget input:focus,
+.oo-ui-textInputWidget textarea:focus {
+       outline: none;
+       border-color: #a7dcff;
+       box-shadow: 0 0 0.3em #a7dcff, 0 0 0 white;
+       background-color: #fff;
+}
+
+.oo-ui-textInputWidget input[readonly],
+.oo-ui-textInputWidget textarea[readonly] {
+       color: #777;
+       text-shadow: 0 1px 1px #fff;
+}
+
+.oo-ui-widget-disabled.oo-ui-textInputWidget input,
+.oo-ui-widget-disabled.oo-ui-textInputWidget input:focus,
+.oo-ui-widget-disabled.oo-ui-textInputWidget textarea,
+.oo-ui-widget-disabled.oo-ui-textInputWidget textarea:focus {
+       color: #ccc;
+       text-shadow: 0 1px 1px #fff;
+}
+
+.oo-ui-textInputWidget-decorated input,
+.oo-ui-textInputWidget-decorated textarea {
+       padding-left: 2em;
+}
+
+.oo-ui-textInputWidget-icon {
+       position: absolute;
+       top: 0;
+       left: 0;
+       width: 2em;
+       height: 100%;
+       background-position: right center;
+       background-repeat: no-repeat;
+}
+
+/* OO.ui.CheckboxWidget */
+.oo-ui-checkboxWidget .oo-ui-labeledElement-label {
+       display: inline-block;
+       vertical-align: middle;
+       padding-left: 0.5em;
+}
+
+.oo-ui-checkboxWidget input {
+       vertical-align: middle;
+}
+
+.oo-ui-checkboxWidget.oo-ui-widget-disabled .oo-ui-labeledElement-label {
+       opacity: 0.5;
+}
+
+/* OO.ui.MenuWidget */
+
+.oo-ui-menuWidget {
+       position: absolute;
+       background: #fff;
+       margin-top: -1px;
+       border: solid 1px #ccc;
+       border-radius: 0 0 0.25em 0.25em;
+       box-shadow: 0 0.15em 1em 0 rgba(0, 0, 0, 0.2);
+}
+
+.oo-ui-menuWidget input {
+       position: absolute;
+       width: 0;
+       height: 0;
+       overflow: hidden;
+       opacity: 0;
+}
+
+/* OO.ui.MenuItemWidget */
+
+.oo-ui-menuItemWidget {
+       position: relative;
+}
+
+.oo-ui-menuItemWidget .oo-ui-iconedElement-icon {
+       display: none;
+}
+
+.oo-ui-menuItemWidget.oo-ui-optionWidget-selected .oo-ui-iconedElement-icon {
+       display: block;
+}
+
+.oo-ui-menuItemWidget.oo-ui-optionWidget-selected {
+       background-color: transparent;
+}
+
+.oo-ui-menuItemWidget.oo-ui-optionWidget-highlighted {
+       background-color: #e1f3ff;
+}
+
+/* OO.ui.MenuSectionItemWidget */
+
+.oo-ui-menuSectionItemWidget {
+       padding: 0.33em 0.75em;
+       color: #888;
+       cursor: default;
+}
+
+/* OO.ui.ButtonSelectWidget */
+
+.oo-ui-buttonSelectWidget {
+       display: inline-block;
+       border-radius: 0.3em;
+       box-shadow: 0 0.1em 0.25em rgba(0, 0, 0, 0.1);
+}
+
+.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget .oo-ui-buttonedElement-button {
+       border-radius: 0;
+       margin-left: -1px;
+       box-shadow: none;
+}
+
+.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget:first-child .oo-ui-buttonedElement-button {
+       border-bottom-left-radius: 0.3em;
+       border-top-left-radius: 0.3em;
+       margin-left: 0;
+}
+
+.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget:last-child .oo-ui-buttonedElement-button {
+       border-bottom-right-radius: 0.3em;
+       border-top-right-radius: 0.3em;
+}
+
+/* OO.ui.ButtonOptionWidget */
+
+.oo-ui-buttonOptionWidget {
+       display: inline-block;
+       padding: 0;
+       background-color: transparent;
+}
+
+.oo-ui-buttonOptionWidget .oo-ui-buttonedElement-button {
+       position: relative;
+       height: 1.9em;
+}
+
+.oo-ui-buttonOptionWidget.oo-ui-iconedElement .oo-ui-iconedElement-icon,
+.oo-ui-buttonOptionWidget.oo-ui-indicatedElement .oo-ui-indicatedElement-indicator {
+       position: static;
+       display: inline-block;
+       vertical-align: middle;
+       height: 1.9em;
+       margin-top: 0;
+}
+
+/* OO.ui.PopupWidget */
+
+.oo-ui-popupWidget-popup {
+       position: absolute;
+       overflow: hidden;
+       border: solid 1px #ccc;
+       border-radius: 0.25em;
+       background-color: #fff;
+       box-shadow: 0 0.15em 0.5em 0 rgba(0, 0, 0, 0.2);
+}
+
+.oo-ui-popupWidget-tail {
+       display: none;
+}
+
+.oo-ui-popupWidget-tailed .oo-ui-popupWidget-popup {
+       margin-top: 7px;
+}
+
+.oo-ui-popupWidget-tailed .oo-ui-popupWidget-tail {
+       display: block;
+       position: absolute;
+       /* @embed */
+       background-image: url(images/tail.svg);
+       background-repeat: no-repeat;
+       width: 15px;
+       height: 8px;
+       margin-left: -7px;
+}
+
+.oo-ui-popupWidget-transitioning .oo-ui-popupWidget-popup {
+       -webkit-transition: width 100ms, height 100ms, left 100ms;
+       -moz-transition: width 100ms, height 100ms, left 100ms;
+       -o-transition: width 100ms, height 100ms, left 100ms;
+       transition: width 100ms, height 100ms, left 100ms;
+       -webkit-transition-timing-function: ease-in-out;
+       -moz-transition-timing-function: ease-in-out;
+       -o-transition-timing-function: ease-in-out;
+       transition-timing-function: ease-in-out;
+}
+
+.oo-ui-popupWidget-head {
+       height: 2.5em;
+       -webkit-touch-callout: none;
+       -webkit-user-select: none;
+       -moz-user-select: none;
+       -ms-user-select: none;
+       user-select: none;
+}
+
+.oo-ui-popupWidget-head .oo-ui-buttonWidget {
+       float: right;
+       margin: 0.25em;
+}
+
+.oo-ui-popupWidget-head .oo-ui-labeledElement-label {
+       float: left;
+       margin: 0.75em 1em;
+       cursor: default;
+}
+
+.oo-ui-popupWidget-body {
+       box-shadow: 0 0 0.66em rgba(0,0,0,0.25);
+}
+
+/* OO.ui.SearchWidget */
+
+.oo-ui-searchWidget-query {
+       position: absolute;
+       top: 0;
+       left: 0;
+       right: 0;
+       height: 4em;
+       padding: 0 1em;
+       box-shadow: 0 0 0.5em rgba(0,0,0,0.2);
+}
+
+.oo-ui-searchWidget-query .oo-ui-textInputWidget {
+       width: 100%;
+       margin: 0.75em 0;
+}
+
+.oo-ui-searchWidget-results {
+       position: absolute;
+       top: 4em;
+       bottom: 0;
+       left: 0;
+       right: 0;
+       padding: 1em;
+       overflow-x: hidden;
+       overflow-y: auto;
+       line-height: 0;
+}
+
+/* OO.ui.ToggleSwitchWidget */
+
+.oo-ui-toggleSwitchWidget {
+       position: relative;
+       display: inline-block;
+       vertical-align: middle;
+       height: 2em;
+       width: 3em;
+       border-radius: 1em;
+       overflow: hidden;
+       box-shadow: 0 0 0 white, inset 0 0.1em 0.2em #ddd;
+       border: solid 1px #ccc;
+       cursor: pointer;
+       -webkit-box-sizing: border-box;
+       -moz-box-sizing: border-box;
+       box-sizing: border-box;
+       -webkit-transform: translateZ(0px);
+       -moz-transform: translateZ(0px);
+       -ms-transform: translateZ(0px);
+       -o-transform: translateZ(0px);
+       transform: translateZ(0px);
+
+       /* Gray */
+       background-color: #dddddd;
+       filter: progid:DXImageTransform.Microsoft.gradient(
+               GradientType=0,startColorstr=#dddddd, endColorstr=#ffffff
+       );
+       background-image: -webkit-gradient(
+               linear, right top, right bottom, color-stop(0%,#dddddd), color-stop(100%,#ffffff)
+       );
+       background-image: -webkit-linear-gradient(top, #dddddd 0%, #ffffff 100%);
+       background-image: -moz-linear-gradient(top, #dddddd 0%, #ffffff 100%);
+       background-image: -ms-linear-gradient(top, #dddddd 0%, #ffffff 100%);
+       background-image: -o-linear-gradient(top, #dddddd 0%, #ffffff 100%);
+       background-image: linear-gradient(top, #dddddd 0%, #ffffff 100%);
+}
+
+.oo-ui-toggleSwitchWidget.oo-ui-widget-disabled {
+       opacity: 0.5;
+}
+
+.oo-ui-toggleSwitchWidget-grip {
+       -webkit-transition: left 200ms ease-in-out, margin-left 200ms ease-in-out;
+       -moz-transition: left 200ms ease-in-out, margin-left 200ms ease-in-out;
+       -o-transition: left 200ms ease-in-out, margin-left 200ms ease-in-out;
+       transition: left 200ms ease-in-out, margin-left 200ms ease-in-out;
+}
+
+.oo-ui-toggleSwitchWidget-grip {
+       position: absolute;
+       display: block;
+       top: 0.25em;
+       left: 0.25em;
+       width: 1.5em;
+       height: 1.5em;
+       border-radius: 1em;
+       box-shadow: 0 0.1em 0.25em rgba(0, 0, 0, 0.1);
+       -webkit-box-sizing: border-box;
+       -moz-box-sizing: border-box;
+       box-sizing: border-box;
+
+       /* Gray */
+       border: 1px #c9c9c9 solid;
+       background-color: #ffffff;
+       filter: progid:DXImageTransform.Microsoft.gradient(
+               GradientType=0,startColorstr=#ffffff, endColorstr=#dddddd
+       );
+       background-image: -webkit-gradient(
+               linear, right top, right bottom, color-stop(0%,#ffffff), color-stop(100%,#dddddd)
+       );
+       background-image: -webkit-linear-gradient(top, #ffffff 0%, #dddddd 100%);
+       background-image: -moz-linear-gradient(top, #ffffff 0%, #dddddd 100%);
+       background-image: -ms-linear-gradient(top, #ffffff 0%, #dddddd 100%);
+       background-image: -o-linear-gradient(top, #ffffff 0%, #dddddd 100%);
+       background-image: linear-gradient(top, #ffffff 0%, #dddddd 100%);
+}
+
+.oo-ui-toggleSwitchWidget:not(.oo-ui-widget-disabled):hover,
+.oo-ui-toggleSwitchWidget:not(.oo-ui-widget-disabled):hover .oo-ui-toggleSwitchWidget-grip {
+       border-color: #aaa;
+}
+
+.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-grip {
+       left: 1.25em;
+       margin-left: -2px;
+}
+
+.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-grip {
+       left: 0.25em;
+       margin-left: 0;
+}
+
+.oo-ui-toggleSwitchWidget .oo-ui-toggleSwitchWidget-on {
+       position: absolute;
+       top: 0;
+       bottom: 0;
+       right: 0;
+       left: 0;
+       border-radius: 1em;
+       box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.07);
+       cursor: pointer;
+       -webkit-touch-callout: none;
+       -webkit-user-select: none;
+       -moz-user-select: none;
+       -ms-user-select: none;
+       user-select: none;
+
+       -webkit-transition: opacity 200ms ease-in-out;
+       -moz-transition: opacity 200ms ease-in-out;
+       -o-transition: opacity 200ms ease-in-out;
+       transition: opacity 200ms ease-in-out;
+
+       /* Blue */
+       background-color: #eaf4fa;
+       filter: progid:DXImageTransform.Microsoft.gradient(
+               GradientType=0,startColorstr=#b0d9ee, endColorstr=#eaf4fa
+       );
+       background-image: -webkit-gradient(
+               linear, right top, right bottom, color-stop(0%,#b0d9ee), color-stop(100%,#eaf4fa)
+       );
+       background-image: -webkit-linear-gradient(top, #b0d9ee 0%, #eaf4fa 100%);
+       background-image: -moz-linear-gradient(top, #b0d9ee 0%, #eaf4fa 100%);
+       background-image: -ms-linear-gradient(top, #b0d9ee 0%, #eaf4fa 100%);
+       background-image: -o-linear-gradient(top, #b0d9ee 0%, #eaf4fa 100%);
+       background-image: linear-gradient(top, #b0d9ee 0%, #eaf4fa 100%);
+}
+
+.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-on {
+       opacity: 1;
+}
+
+.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-on {
+       opacity: 0;
+}
+.oo-ui-window-head {
+       -webkit-touch-callout: none;
+       -webkit-user-select: none;
+       -moz-user-select: none;
+       -ms-user-select: none;
+       user-select: none;
+}
+
+.oo-ui-window-body {
+       padding: 0 0.75em;
+}
+
+.oo-ui-window-icon {
+       float: left;
+       width: 2em;
+       height: 2em;
+       line-height: 2em;
+       margin-right: 0.5em;
+       background-position: right center;
+       background-repeat: no-repeat;
+}
+
+.oo-ui-window-title {
+       float: left;
+       line-height: 2em;
+       color: #333;
+       white-space: nowrap;
+       cursor: default;
+}
+
+.oo-ui-window-overlay {
+       font-family: sans-serif;
+       line-height: 1.5em;
+       font-size: 1em;
+       position: absolute;
+       top: 0;
+       left: 0;
+}
+/* Icons */
+
+.oo-ui-icon-add-item {
+       /* @embed */
+       background-image: url(images/icons/add-item.png);
+}
+
+.oo-ui-icon-advanced {
+       /* @embed */
+       background-image: url(images/icons/advanced.png);
+}
+
+.oo-ui-icon-alert {
+       /* @embed */
+       background-image: url(images/icons/alert.png);
+}
+
+.oo-ui-icon-check {
+       /* @embed */
+       background-image: url(images/icons/check.png);
+}
+
+.oo-ui-icon-clear {
+       /* @embed */
+       background-image: url(images/icons/clear.png);
+}
+
+.oo-ui-icon-close {
+       /* @embed */
+       background-image: url(images/icons/close.png);
+}
+
+.oo-ui-icon-code {
+       /* @embed */
+       background-image: url(images/icons/code.png);
+}
+
+.oo-ui-icon-collapse {
+       /* @embed */
+       background-image: url(images/icons/collapse.png);
+}
+
+.oo-ui-icon-comment {
+       /* @embed */
+       background-image: url(images/icons/comment.png);
+}
+
+.oo-ui-icon-expand {
+       /* @embed */
+       background-image: url(images/icons/expand.png);
+}
+
+.oo-ui-icon-help {
+       /* @embed */
+       background-image: url(images/icons/help.png);
+}
+
+.oo-ui-icon-link {
+       /* @embed */
+       background-image: url(images/icons/link.png);
+}
+
+.oo-ui-icon-menu {
+       /* @embed */
+       background-image: url(images/icons/menu.png);
+}
+
+.oo-ui-icon-next {
+       /* @embed */
+       background-image: url(images/icons/move-ltr.png);
+}
+
+.oo-ui-icon-picture {
+       /* @embed */
+       background-image: url(images/icons/picture.png);
+}
+
+.oo-ui-icon-previous {
+       /* @embed */
+       background-image: url(images/icons/move-rtl.png);
+}
+
+.oo-ui-icon-redo {
+       /* @embed */
+       background-image: url(images/icons/arched-arrow-ltr.png);
+}
+
+.oo-ui-icon-remove {
+       /* @embed */
+       background-image: url(images/icons/remove.png);
+}
+
+.oo-ui-icon-search {
+       /* @embed */
+       background-image: url(images/icons/search.png);
+}
+
+.oo-ui-icon-settings {
+       /* @embed */
+       background-image: url(images/icons/settings.png);
+}
+
+.oo-ui-icon-tag {
+       /* @embed */
+       background-image: url(images/icons/tag.png);
+}
+
+.oo-ui-icon-undo {
+       /* @embed */
+       background-image: url(images/icons/arched-arrow-rtl.png);
+}
+
+.oo-ui-icon-window {
+       /* @embed */
+       background-image: url(images/icons/window.png);
+}
+
+/* Indicators */
+
+.oo-ui-indicator-down {
+       /* @embed */
+       background-image: url(images/indicators/down.png);
+}
+
+.oo-ui-indicator-required {
+       /* @embed */
+       background-image: url(images/indicators/required.png);
+}
+
+.oo-ui-indicator-up {
+       /* @embed */
+       background-image: url(images/indicators/up.png);
+}
+/* Icons */
+
+.oo-ui-icon-add-item {
+       /* @embed */
+       background-image: url(images/icons/add-item.svg);
+}
+
+.oo-ui-icon-advanced {
+       /* @embed */
+       background-image: url(images/icons/advanced.svg);
+}
+
+.oo-ui-icon-alert {
+       /* @embed */
+       background-image: url(images/icons/alert.svg);
+}
+
+.oo-ui-icon-check {
+       /* @embed */
+       background-image: url(images/icons/check.svg);
+}
+
+.oo-ui-icon-clear {
+       /* @embed */
+       background-image: url(images/icons/clear.svg);
+}
+
+.oo-ui-icon-close {
+       /* @embed */
+       background-image: url(images/icons/close.svg);
+}
+
+.oo-ui-icon-code {
+       /* @embed */
+       background-image: url(images/icons/code.svg);
+}
+
+.oo-ui-icon-collapse {
+       /* @embed */
+       background-image: url(images/icons/collapse.svg);
+}
+
+.oo-ui-icon-comment {
+       /* @embed */
+       background-image: url(images/icons/comment.svg);
+}
+
+.oo-ui-icon-expand {
+       /* @embed */
+       background-image: url(images/icons/expand.svg);
+}
+
+.oo-ui-icon-help {
+       /* @embed */
+       background-image: url(images/icons/help.svg);
+}
+
+.oo-ui-icon-link {
+       /* @embed */
+       background-image: url(images/icons/link.svg);
+}
+
+.oo-ui-icon-menu {
+       /* @embed */
+       background-image: url(images/icons/menu.svg);
+}
+
+.oo-ui-icon-next {
+       /* @embed */
+       background-image: url(images/icons/move-ltr.svg);
+}
+
+.oo-ui-icon-picture {
+       /* @embed */
+       background-image: url(images/icons/picture.svg);
+}
+
+.oo-ui-icon-previous {
+       /* @embed */
+       background-image: url(images/icons/move-rtl.svg);
+}
+
+.oo-ui-icon-redo {
+       /* @embed */
+       background-image: url(images/icons/arched-arrow-ltr.svg);
+}
+
+.oo-ui-icon-remove {
+       /* @embed */
+       background-image: url(images/icons/remove.svg);
+}
+
+.oo-ui-icon-search {
+       /* @embed */
+       background-image: url(images/icons/search.svg);
+}
+
+.oo-ui-icon-settings {
+       /* @embed */
+       background-image: url(images/icons/settings.svg);
+}
+
+.oo-ui-icon-tag {
+       /* @embed */
+       background-image: url(images/icons/tag.svg);
+}
+
+.oo-ui-icon-undo {
+       /* @embed */
+       background-image: url(images/icons/arched-arrow-rtl.svg);
+}
+
+.oo-ui-icon-window {
+       /* @embed */
+       background-image: url(images/icons/window.svg);
+}
+
+/* Indicators */
+
+.oo-ui-indicator-down {
+       /* @embed */
+       background-image: url(images/indicators/down.svg);
+}
+
+.oo-ui-indicator-required {
+       /* @embed */
+       background-image: url(images/indicators/required.svg);
+}
+
+.oo-ui-indicator-up {
+       /* @embed */
+       background-image: url(images/indicators/up.svg);
+}
diff --git a/resources/sinonjs/sinon-1.8.1.js b/resources/sinonjs/sinon-1.8.1.js
new file mode 100644 (file)
index 0000000..3e9865e
--- /dev/null
@@ -0,0 +1,4721 @@
+/**
+ * Sinon.JS 1.8.1, 2014/02/02
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
+ *
+ * (The BSD License)
+ * 
+ * Copyright (c) 2010-2013, Christian Johansen, christian@cjohansen.no
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ *     * Redistributions of source code must retain the above copyright notice,
+ *       this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright notice,
+ *       this list of conditions and the following disclaimer in the documentation
+ *       and/or other materials provided with the distribution.
+ *     * Neither the name of Christian Johansen nor the names of his contributors
+ *       may be used to endorse or promote products derived from this software
+ *       without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+this.sinon = (function () {
+var samsam, formatio;
+function define(mod, deps, fn) { if (mod == "samsam") { samsam = deps(); } else { formatio = fn(samsam); } }
+define.amd = true;
+((typeof define === "function" && define.amd && function (m) { define("samsam", m); }) ||
+ (typeof module === "object" &&
+      function (m) { module.exports = m(); }) || // Node
+ function (m) { this.samsam = m(); } // Browser globals
+)(function () {
+    var o = Object.prototype;
+    var div = typeof document !== "undefined" && document.createElement("div");
+
+    function isNaN(value) {
+        // Unlike global isNaN, this avoids type coercion
+        // typeof check avoids IE host object issues, hat tip to
+        // lodash
+        var val = value; // JsLint thinks value !== value is "weird"
+        return typeof value === "number" && value !== val;
+    }
+
+    function getClass(value) {
+        // Returns the internal [[Class]] by calling Object.prototype.toString
+        // with the provided value as this. Return value is a string, naming the
+        // internal class, e.g. "Array"
+        return o.toString.call(value).split(/[ \]]/)[1];
+    }
+
+    /**
+     * @name samsam.isArguments
+     * @param Object object
+     *
+     * Returns ``true`` if ``object`` is an ``arguments`` object,
+     * ``false`` otherwise.
+     */
+    function isArguments(object) {
+        if (typeof object !== "object" || typeof object.length !== "number" ||
+                getClass(object) === "Array") {
+            return false;
+        }
+        if (typeof object.callee == "function") { return true; }
+        try {
+            object[object.length] = 6;
+            delete object[object.length];
+        } catch (e) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @name samsam.isElement
+     * @param Object object
+     *
+     * Returns ``true`` if ``object`` is a DOM element node. Unlike
+     * Underscore.js/lodash, this function will return ``false`` if ``object``
+     * is an *element-like* object, i.e. a regular object with a ``nodeType``
+     * property that holds the value ``1``.
+     */
+    function isElement(object) {
+        if (!object || object.nodeType !== 1 || !div) { return false; }
+        try {
+            object.appendChild(div);
+            object.removeChild(div);
+        } catch (e) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * @name samsam.keys
+     * @param Object object
+     *
+     * Return an array of own property names.
+     */
+    function keys(object) {
+        var ks = [], prop;
+        for (prop in object) {
+            if (o.hasOwnProperty.call(object, prop)) { ks.push(prop); }
+        }
+        return ks;
+    }
+
+    /**
+     * @name samsam.isDate
+     * @param Object value
+     *
+     * Returns true if the object is a ``Date``, or *date-like*. Duck typing
+     * of date objects work by checking that the object has a ``getTime``
+     * function whose return value equals the return value from the object's
+     * ``valueOf``.
+     */
+    function isDate(value) {
+        return typeof value.getTime == "function" &&
+            value.getTime() == value.valueOf();
+    }
+
+    /**
+     * @name samsam.isNegZero
+     * @param Object value
+     *
+     * Returns ``true`` if ``value`` is ``-0``.
+     */
+    function isNegZero(value) {
+        return value === 0 && 1 / value === -Infinity;
+    }
+
+    /**
+     * @name samsam.equal
+     * @param Object obj1
+     * @param Object obj2
+     *
+     * Returns ``true`` if two objects are strictly equal. Compared to
+     * ``===`` there are two exceptions:
+     *
+     *   - NaN is considered equal to NaN
+     *   - -0 and +0 are not considered equal
+     */
+    function identical(obj1, obj2) {
+        if (obj1 === obj2 || (isNaN(obj1) && isNaN(obj2))) {
+            return obj1 !== 0 || isNegZero(obj1) === isNegZero(obj2);
+        }
+    }
+
+
+    /**
+     * @name samsam.deepEqual
+     * @param Object obj1
+     * @param Object obj2
+     *
+     * Deep equal comparison. Two values are "deep equal" if:
+     *
+     *   - They are equal, according to samsam.identical
+     *   - They are both date objects representing the same time
+     *   - They are both arrays containing elements that are all deepEqual
+     *   - They are objects with the same set of properties, and each property
+     *     in ``obj1`` is deepEqual to the corresponding property in ``obj2``
+     *
+     * Supports cyclic objects.
+     */
+    function deepEqualCyclic(obj1, obj2) {
+
+        // used for cyclic comparison
+        // contain already visited objects
+        var objects1 = [],
+            objects2 = [],
+        // contain pathes (position in the object structure)
+        // of the already visited objects
+        // indexes same as in objects arrays
+            paths1 = [],
+            paths2 = [],
+        // contains combinations of already compared objects
+        // in the manner: { "$1['ref']$2['ref']": true }
+            compared = {};
+
+        /**
+         * used to check, if the value of a property is an object
+         * (cyclic logic is only needed for objects)
+         * only needed for cyclic logic
+         */
+        function isObject(value) {
+
+            if (typeof value === 'object' && value !== null &&
+                    !(value instanceof Boolean) &&
+                    !(value instanceof Date)    &&
+                    !(value instanceof Number)  &&
+                    !(value instanceof RegExp)  &&
+                    !(value instanceof String)) {
+
+                return true;
+            }
+
+            return false;
+        }
+
+        /**
+         * returns the index of the given object in the
+         * given objects array, -1 if not contained
+         * only needed for cyclic logic
+         */
+        function getIndex(objects, obj) {
+
+            var i;
+            for (i = 0; i < objects.length; i++) {
+                if (objects[i] === obj) {
+                    return i;
+                }
+            }
+
+            return -1;
+        }
+
+        // does the recursion for the deep equal check
+        return (function deepEqual(obj1, obj2, path1, path2) {
+            var type1 = typeof obj1;
+            var type2 = typeof obj2;
+
+            // == null also matches undefined
+            if (obj1 === obj2 ||
+                    isNaN(obj1) || isNaN(obj2) ||
+                    obj1 == null || obj2 == null ||
+                    type1 !== "object" || type2 !== "object") {
+
+                return identical(obj1, obj2);
+            }
+
+            // Elements are only equal if identical(expected, actual)
+            if (isElement(obj1) || isElement(obj2)) { return false; }
+
+            var isDate1 = isDate(obj1), isDate2 = isDate(obj2);
+            if (isDate1 || isDate2) {
+                if (!isDate1 || !isDate2 || obj1.getTime() !== obj2.getTime()) {
+                    return false;
+                }
+            }
+
+            if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
+                if (obj1.toString() !== obj2.toString()) { return false; }
+            }
+
+            var class1 = getClass(obj1);
+            var class2 = getClass(obj2);
+            var keys1 = keys(obj1);
+            var keys2 = keys(obj2);
+
+            if (isArguments(obj1) || isArguments(obj2)) {
+                if (obj1.length !== obj2.length) { return false; }
+            } else {
+                if (type1 !== type2 || class1 !== class2 ||
+                        keys1.length !== keys2.length) {
+                    return false;
+                }
+            }
+
+            var key, i, l,
+                // following vars are used for the cyclic logic
+                value1, value2,
+                isObject1, isObject2,
+                index1, index2,
+                newPath1, newPath2;
+
+            for (i = 0, l = keys1.length; i < l; i++) {
+                key = keys1[i];
+                if (!o.hasOwnProperty.call(obj2, key)) {
+                    return false;
+                }
+
+                // Start of the cyclic logic
+
+                value1 = obj1[key];
+                value2 = obj2[key];
+
+                isObject1 = isObject(value1);
+                isObject2 = isObject(value2);
+
+                // determine, if the objects were already visited
+                // (it's faster to check for isObject first, than to
+                // get -1 from getIndex for non objects)
+                index1 = isObject1 ? getIndex(objects1, value1) : -1;
+                index2 = isObject2 ? getIndex(objects2, value2) : -1;
+
+                // determine the new pathes of the objects
+                // - for non cyclic objects the current path will be extended
+                //   by current property name
+                // - for cyclic objects the stored path is taken
+                newPath1 = index1 !== -1
+                    ? paths1[index1]
+                    : path1 + '[' + JSON.stringify(key) + ']';
+                newPath2 = index2 !== -1
+                    ? paths2[index2]
+                    : path2 + '[' + JSON.stringify(key) + ']';
+
+                // stop recursion if current objects are already compared
+                if (compared[newPath1 + newPath2]) {
+                    return true;
+                }
+
+                // remember the current objects and their pathes
+                if (index1 === -1 && isObject1) {
+                    objects1.push(value1);
+                    paths1.push(newPath1);
+                }
+                if (index2 === -1 && isObject2) {
+                    objects2.push(value2);
+                    paths2.push(newPath2);
+                }
+
+                // remember that the current objects are already compared
+                if (isObject1 && isObject2) {
+                    compared[newPath1 + newPath2] = true;
+                }
+
+                // End of cyclic logic
+
+                // neither value1 nor value2 is a cycle
+                // continue with next level
+                if (!deepEqual(value1, value2, newPath1, newPath2)) {
+                    return false;
+                }
+            }
+
+            return true;
+
+        }(obj1, obj2, '$1', '$2'));
+    }
+
+    var match;
+
+    function arrayContains(array, subset) {
+        if (subset.length === 0) { return true; }
+        var i, l, j, k;
+        for (i = 0, l = array.length; i < l; ++i) {
+            if (match(array[i], subset[0])) {
+                for (j = 0, k = subset.length; j < k; ++j) {
+                    if (!match(array[i + j], subset[j])) { return false; }
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @name samsam.match
+     * @param Object object
+     * @param Object matcher
+     *
+     * Compare arbitrary value ``object`` with matcher.
+     */
+    match = function match(object, matcher) {
+        if (matcher && typeof matcher.test === "function") {
+            return matcher.test(object);
+        }
+
+        if (typeof matcher === "function") {
+            return matcher(object) === true;
+        }
+
+        if (typeof matcher === "string") {
+            matcher = matcher.toLowerCase();
+            var notNull = typeof object === "string" || !!object;
+            return notNull &&
+                (String(object)).toLowerCase().indexOf(matcher) >= 0;
+        }
+
+        if (typeof matcher === "number") {
+            return matcher === object;
+        }
+
+        if (typeof matcher === "boolean") {
+            return matcher === object;
+        }
+
+        if (getClass(object) === "Array" && getClass(matcher) === "Array") {
+            return arrayContains(object, matcher);
+        }
+
+        if (matcher && typeof matcher === "object") {
+            var prop;
+            for (prop in matcher) {
+                if (!match(object[prop], matcher[prop])) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        throw new Error("Matcher was not a string, a number, a " +
+                        "function, a boolean or an object");
+    };
+
+    return {
+        isArguments: isArguments,
+        isElement: isElement,
+        isDate: isDate,
+        isNegZero: isNegZero,
+        identical: identical,
+        deepEqual: deepEqualCyclic,
+        match: match,
+        keys: keys
+    };
+});
+((typeof define === "function" && define.amd && function (m) {
+    define("formatio", ["samsam"], m);
+}) || (typeof module === "object" && function (m) {
+    module.exports = m(require("samsam"));
+}) || function (m) { this.formatio = m(this.samsam); }
+)(function (samsam) {
+    
+    var formatio = {
+        excludeConstructors: ["Object", /^.$/],
+        quoteStrings: true
+    };
+
+    var hasOwn = Object.prototype.hasOwnProperty;
+
+    var specialObjects = [];
+    if (typeof global !== "undefined") {
+        specialObjects.push({ object: global, value: "[object global]" });
+    }
+    if (typeof document !== "undefined") {
+        specialObjects.push({
+            object: document,
+            value: "[object HTMLDocument]"
+        });
+    }
+    if (typeof window !== "undefined") {
+        specialObjects.push({ object: window, value: "[object Window]" });
+    }
+
+    function functionName(func) {
+        if (!func) { return ""; }
+        if (func.displayName) { return func.displayName; }
+        if (func.name) { return func.name; }
+        var matches = func.toString().match(/function\s+([^\(]+)/m);
+        return (matches && matches[1]) || "";
+    }
+
+    function constructorName(f, object) {
+        var name = functionName(object && object.constructor);
+        var excludes = f.excludeConstructors ||
+                formatio.excludeConstructors || [];
+
+        var i, l;
+        for (i = 0, l = excludes.length; i < l; ++i) {
+            if (typeof excludes[i] === "string" && excludes[i] === name) {
+                return "";
+            } else if (excludes[i].test && excludes[i].test(name)) {
+                return "";
+            }
+        }
+
+        return name;
+    }
+
+    function isCircular(object, objects) {
+        if (typeof object !== "object") { return false; }
+        var i, l;
+        for (i = 0, l = objects.length; i < l; ++i) {
+            if (objects[i] === object) { return true; }
+        }
+        return false;
+    }
+
+    function ascii(f, object, processed, indent) {
+        if (typeof object === "string") {
+            var qs = f.quoteStrings;
+            var quote = typeof qs !== "boolean" || qs;
+            return processed || quote ? '"' + object + '"' : object;
+        }
+
+        if (typeof object === "function" && !(object instanceof RegExp)) {
+            return ascii.func(object);
+        }
+
+        processed = processed || [];
+
+        if (isCircular(object, processed)) { return "[Circular]"; }
+
+        if (Object.prototype.toString.call(object) === "[object Array]") {
+            return ascii.array.call(f, object, processed);
+        }
+
+        if (!object) { return String((1/object) === -Infinity ? "-0" : object); }
+        if (samsam.isElement(object)) { return ascii.element(object); }
+
+        if (typeof object.toString === "function" &&
+                object.toString !== Object.prototype.toString) {
+            return object.toString();
+        }
+
+        var i, l;
+        for (i = 0, l = specialObjects.length; i < l; i++) {
+            if (object === specialObjects[i].object) {
+                return specialObjects[i].value;
+            }
+        }
+
+        return ascii.object.call(f, object, processed, indent);
+    }
+
+    ascii.func = function (func) {
+        return "function " + functionName(func) + "() {}";
+    };
+
+    ascii.array = function (array, processed) {
+        processed = processed || [];
+        processed.push(array);
+        var i, l, pieces = [];
+        for (i = 0, l = array.length; i < l; ++i) {
+            pieces.push(ascii(this, array[i], processed));
+        }
+        return "[" + pieces.join(", ") + "]";
+    };
+
+    ascii.object = function (object, processed, indent) {
+        processed = processed || [];
+        processed.push(object);
+        indent = indent || 0;
+        var pieces = [], properties = samsam.keys(object).sort();
+        var length = 3;
+        var prop, str, obj, i, l;
+
+        for (i = 0, l = properties.length; i < l; ++i) {
+            prop = properties[i];
+            obj = object[prop];
+
+            if (isCircular(obj, processed)) {
+                str = "[Circular]";
+            } else {
+                str = ascii(this, obj, processed, indent + 2);
+            }
+
+            str = (/\s/.test(prop) ? '"' + prop + '"' : prop) + ": " + str;
+            length += str.length;
+            pieces.push(str);
+        }
+
+        var cons = constructorName(this, object);
+        var prefix = cons ? "[" + cons + "] " : "";
+        var is = "";
+        for (i = 0, l = indent; i < l; ++i) { is += " "; }
+
+        if (length + indent > 80) {
+            return prefix + "{\n  " + is + pieces.join(",\n  " + is) + "\n" +
+                is + "}";
+        }
+        return prefix + "{ " + pieces.join(", ") + " }";
+    };
+
+    ascii.element = function (element) {
+        var tagName = element.tagName.toLowerCase();
+        var attrs = element.attributes, attr, pairs = [], attrName, i, l, val;
+
+        for (i = 0, l = attrs.length; i < l; ++i) {
+            attr = attrs.item(i);
+            attrName = attr.nodeName.toLowerCase().replace("html:", "");
+            val = attr.nodeValue;
+            if (attrName !== "contenteditable" || val !== "inherit") {
+                if (!!val) { pairs.push(attrName + "=\"" + val + "\""); }
+            }
+        }
+
+        var formatted = "<" + tagName + (pairs.length > 0 ? " " : "");
+        var content = element.innerHTML;
+
+        if (content.length > 20) {
+            content = content.substr(0, 20) + "[...]";
+        }
+
+        var res = formatted + pairs.join(" ") + ">" + content +
+                "</" + tagName + ">";
+
+        return res.replace(/ contentEditable="inherit"/, "");
+    };
+
+    function Formatio(options) {
+        for (var opt in options) {
+            this[opt] = options[opt];
+        }
+    }
+
+    Formatio.prototype = {
+        functionName: functionName,
+
+        configure: function (options) {
+            return new Formatio(options);
+        },
+
+        constructorName: function (object) {
+            return constructorName(this, object);
+        },
+
+        ascii: function (object, processed, indent) {
+            return ascii(this, object, processed, indent);
+        }
+    };
+
+    return Formatio.prototype;
+});
+/*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/
+/*global module, require, __dirname, document*/
+/**
+ * Sinon core utilities. For internal use only.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+var sinon = (function (formatio) {
+    var div = typeof document != "undefined" && document.createElement("div");
+    var hasOwn = Object.prototype.hasOwnProperty;
+
+    function isDOMNode(obj) {
+        var success = false;
+
+        try {
+            obj.appendChild(div);
+            success = div.parentNode == obj;
+        } catch (e) {
+            return false;
+        } finally {
+            try {
+                obj.removeChild(div);
+            } catch (e) {
+                // Remove failed, not much we can do about that
+            }
+        }
+
+        return success;
+    }
+
+    function isElement(obj) {
+        return div && obj && obj.nodeType === 1 && isDOMNode(obj);
+    }
+
+    function isFunction(obj) {
+        return typeof obj === "function" || !!(obj && obj.constructor && obj.call && obj.apply);
+    }
+
+    function mirrorProperties(target, source) {
+        for (var prop in source) {
+            if (!hasOwn.call(target, prop)) {
+                target[prop] = source[prop];
+            }
+        }
+    }
+
+    function isRestorable (obj) {
+        return typeof obj === "function" && typeof obj.restore === "function" && obj.restore.sinon;
+    }
+
+    var sinon = {
+        wrapMethod: function wrapMethod(object, property, method) {
+            if (!object) {
+                throw new TypeError("Should wrap property of object");
+            }
+
+            if (typeof method != "function") {
+                throw new TypeError("Method wrapper should be function");
+            }
+
+            var wrappedMethod = object[property],
+                error;
+
+            if (!isFunction(wrappedMethod)) {
+                error = new TypeError("Attempted to wrap " + (typeof wrappedMethod) + " property " +
+                                    property + " as function");
+            }
+
+            if (wrappedMethod.restore && wrappedMethod.restore.sinon) {
+                error = new TypeError("Attempted to wrap " + property + " which is already wrapped");
+            }
+
+            if (wrappedMethod.calledBefore) {
+                var verb = !!wrappedMethod.returns ? "stubbed" : "spied on";
+                error = new TypeError("Attempted to wrap " + property + " which is already " + verb);
+            }
+
+            if (error) {
+                if (wrappedMethod._stack) {
+                    error.stack += '\n--------------\n' + wrappedMethod._stack;
+                }
+                throw error;
+            }
+
+            // IE 8 does not support hasOwnProperty on the window object and Firefox has a problem
+            // when using hasOwn.call on objects from other frames.
+            var owned = object.hasOwnProperty ? object.hasOwnProperty(property) : hasOwn.call(object, property);
+            object[property] = method;
+            method.displayName = property;
+            // Set up a stack trace which can be used later to find what line of
+            // code the original method was created on.
+            method._stack = (new Error('Stack Trace for original')).stack;
+
+            method.restore = function () {
+                // For prototype properties try to reset by delete first.
+                // If this fails (ex: localStorage on mobile safari) then force a reset
+                // via direct assignment.
+                if (!owned) {
+                    delete object[property];
+                }
+                if (object[property] === method) {
+                    object[property] = wrappedMethod;
+                }
+            };
+
+            method.restore.sinon = true;
+            mirrorProperties(method, wrappedMethod);
+
+            return method;
+        },
+
+        extend: function extend(target) {
+            for (var i = 1, l = arguments.length; i < l; i += 1) {
+                for (var prop in arguments[i]) {
+                    if (arguments[i].hasOwnProperty(prop)) {
+                        target[prop] = arguments[i][prop];
+                    }
+
+                    // DONT ENUM bug, only care about toString
+                    if (arguments[i].hasOwnProperty("toString") &&
+                        arguments[i].toString != target.toString) {
+                        target.toString = arguments[i].toString;
+                    }
+                }
+            }
+
+            return target;
+        },
+
+        create: function create(proto) {
+            var F = function () {};
+            F.prototype = proto;
+            return new F();
+        },
+
+        deepEqual: function deepEqual(a, b) {
+            if (sinon.match && sinon.match.isMatcher(a)) {
+                return a.test(b);
+            }
+            if (typeof a != "object" || typeof b != "object") {
+                return a === b;
+            }
+
+            if (isElement(a) || isElement(b)) {
+                return a === b;
+            }
+
+            if (a === b) {
+                return true;
+            }
+
+            if ((a === null && b !== null) || (a !== null && b === null)) {
+                return false;
+            }
+
+            var aString = Object.prototype.toString.call(a);
+            if (aString != Object.prototype.toString.call(b)) {
+                return false;
+            }
+
+            if (aString == "[object Date]") {
+                return a.valueOf() === b.valueOf();
+            }
+
+            var prop, aLength = 0, bLength = 0;
+
+            if (aString == "[object Array]" && a.length !== b.length) {
+                return false;
+            }
+
+            for (prop in a) {
+                aLength += 1;
+
+                if (!deepEqual(a[prop], b[prop])) {
+                    return false;
+                }
+            }
+
+            for (prop in b) {
+                bLength += 1;
+            }
+
+            return aLength == bLength;
+        },
+
+        functionName: function functionName(func) {
+            var name = func.displayName || func.name;
+
+            // Use function decomposition as a last resort to get function
+            // name. Does not rely on function decomposition to work - if it
+            // doesn't debugging will be slightly less informative
+            // (i.e. toString will say 'spy' rather than 'myFunc').
+            if (!name) {
+                var matches = func.toString().match(/function ([^\s\(]+)/);
+                name = matches && matches[1];
+            }
+
+            return name;
+        },
+
+        functionToString: function toString() {
+            if (this.getCall && this.callCount) {
+                var thisValue, prop, i = this.callCount;
+
+                while (i--) {
+                    thisValue = this.getCall(i).thisValue;
+
+                    for (prop in thisValue) {
+                        if (thisValue[prop] === this) {
+                            return prop;
+                        }
+                    }
+                }
+            }
+
+            return this.displayName || "sinon fake";
+        },
+
+        getConfig: function (custom) {
+            var config = {};
+            custom = custom || {};
+            var defaults = sinon.defaultConfig;
+
+            for (var prop in defaults) {
+                if (defaults.hasOwnProperty(prop)) {
+                    config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop];
+                }
+            }
+
+            return config;
+        },
+
+        format: function (val) {
+            return "" + val;
+        },
+
+        defaultConfig: {
+            injectIntoThis: true,
+            injectInto: null,
+            properties: ["spy", "stub", "mock", "clock", "server", "requests"],
+            useFakeTimers: true,
+            useFakeServer: true
+        },
+
+        timesInWords: function timesInWords(count) {
+            return count == 1 && "once" ||
+                count == 2 && "twice" ||
+                count == 3 && "thrice" ||
+                (count || 0) + " times";
+        },
+
+        calledInOrder: function (spies) {
+            for (var i = 1, l = spies.length; i < l; i++) {
+                if (!spies[i - 1].calledBefore(spies[i]) || !spies[i].called) {
+                    return false;
+                }
+            }
+
+            return true;
+        },
+
+        orderByFirstCall: function (spies) {
+            return spies.sort(function (a, b) {
+                // uuid, won't ever be equal
+                var aCall = a.getCall(0);
+                var bCall = b.getCall(0);
+                var aId = aCall && aCall.callId || -1;
+                var bId = bCall && bCall.callId || -1;
+
+                return aId < bId ? -1 : 1;
+            });
+        },
+
+        log: function () {},
+
+        logError: function (label, err) {
+            var msg = label + " threw exception: ";
+            sinon.log(msg + "[" + err.name + "] " + err.message);
+            if (err.stack) { sinon.log(err.stack); }
+
+            setTimeout(function () {
+                err.message = msg + err.message;
+                throw err;
+            }, 0);
+        },
+
+        typeOf: function (value) {
+            if (value === null) {
+                return "null";
+            }
+            else if (value === undefined) {
+                return "undefined";
+            }
+            var string = Object.prototype.toString.call(value);
+            return string.substring(8, string.length - 1).toLowerCase();
+        },
+
+        createStubInstance: function (constructor) {
+            if (typeof constructor !== "function") {
+                throw new TypeError("The constructor should be a function.");
+            }
+            return sinon.stub(sinon.create(constructor.prototype));
+        },
+
+        restore: function (object) {
+            if (object !== null && typeof object === "object") {
+                for (var prop in object) {
+                    if (isRestorable(object[prop])) {
+                        object[prop].restore();
+                    }
+                }
+            }
+            else if (isRestorable(object)) {
+                object.restore();
+            }
+        }
+    };
+
+    var isNode = typeof module !== "undefined" && module.exports;
+    var isAMD = typeof define === 'function' && typeof define.amd === 'object' && define.amd;
+
+    if (isAMD) {
+        define(function(){
+            return sinon;
+        });
+    } else if (isNode) {
+        try {
+            formatio = require("formatio");
+        } catch (e) {}
+        module.exports = sinon;
+        module.exports.spy = require("./sinon/spy");
+        module.exports.spyCall = require("./sinon/call");
+        module.exports.behavior = require("./sinon/behavior");
+        module.exports.stub = require("./sinon/stub");
+        module.exports.mock = require("./sinon/mock");
+        module.exports.collection = require("./sinon/collection");
+        module.exports.assert = require("./sinon/assert");
+        module.exports.sandbox = require("./sinon/sandbox");
+        module.exports.test = require("./sinon/test");
+        module.exports.testCase = require("./sinon/test_case");
+        module.exports.assert = require("./sinon/assert");
+        module.exports.match = require("./sinon/match");
+    }
+
+    if (formatio) {
+        var formatter = formatio.configure({ quoteStrings: false });
+        sinon.format = function () {
+            return formatter.ascii.apply(formatter, arguments);
+        };
+    } else if (isNode) {
+        try {
+            var util = require("util");
+            sinon.format = function (value) {
+                return typeof value == "object" && value.toString === Object.prototype.toString ? util.inspect(value) : value;
+            };
+        } catch (e) {
+            /* Node, but no util module - would be very old, but better safe than
+             sorry */
+        }
+    }
+
+    return sinon;
+}(typeof formatio == "object" && formatio));
+
+/* @depend ../sinon.js */
+/*jslint eqeqeq: false, onevar: false, plusplus: false*/
+/*global module, require, sinon*/
+/**
+ * Match functions
+ *
+ * @author Maximilian Antoni (mail@maxantoni.de)
+ * @license BSD
+ *
+ * Copyright (c) 2012 Maximilian Antoni
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== 'undefined' && module.exports;
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function assertType(value, type, name) {
+        var actual = sinon.typeOf(value);
+        if (actual !== type) {
+            throw new TypeError("Expected type of " + name + " to be " +
+                type + ", but was " + actual);
+        }
+    }
+
+    var matcher = {
+        toString: function () {
+            return this.message;
+        }
+    };
+
+    function isMatcher(object) {
+        return matcher.isPrototypeOf(object);
+    }
+
+    function matchObject(expectation, actual) {
+        if (actual === null || actual === undefined) {
+            return false;
+        }
+        for (var key in expectation) {
+            if (expectation.hasOwnProperty(key)) {
+                var exp = expectation[key];
+                var act = actual[key];
+                if (match.isMatcher(exp)) {
+                    if (!exp.test(act)) {
+                        return false;
+                    }
+                } else if (sinon.typeOf(exp) === "object") {
+                    if (!matchObject(exp, act)) {
+                        return false;
+                    }
+                } else if (!sinon.deepEqual(exp, act)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    matcher.or = function (m2) {
+        if (!isMatcher(m2)) {
+            throw new TypeError("Matcher expected");
+        }
+        var m1 = this;
+        var or = sinon.create(matcher);
+        or.test = function (actual) {
+            return m1.test(actual) || m2.test(actual);
+        };
+        or.message = m1.message + ".or(" + m2.message + ")";
+        return or;
+    };
+
+    matcher.and = function (m2) {
+        if (!isMatcher(m2)) {
+            throw new TypeError("Matcher expected");
+        }
+        var m1 = this;
+        var and = sinon.create(matcher);
+        and.test = function (actual) {
+            return m1.test(actual) && m2.test(actual);
+        };
+        and.message = m1.message + ".and(" + m2.message + ")";
+        return and;
+    };
+
+    var match = function (expectation, message) {
+        var m = sinon.create(matcher);
+        var type = sinon.typeOf(expectation);
+        switch (type) {
+        case "object":
+            if (typeof expectation.test === "function") {
+                m.test = function (actual) {
+                    return expectation.test(actual) === true;
+                };
+                m.message = "match(" + sinon.functionName(expectation.test) + ")";
+                return m;
+            }
+            var str = [];
+            for (var key in expectation) {
+                if (expectation.hasOwnProperty(key)) {
+                    str.push(key + ": " + expectation[key]);
+                }
+            }
+            m.test = function (actual) {
+                return matchObject(expectation, actual);
+            };
+            m.message = "match(" + str.join(", ") + ")";
+            break;
+        case "number":
+            m.test = function (actual) {
+                return expectation == actual;
+            };
+            break;
+        case "string":
+            m.test = function (actual) {
+                if (typeof actual !== "string") {
+                    return false;
+                }
+                return actual.indexOf(expectation) !== -1;
+            };
+            m.message = "match(\"" + expectation + "\")";
+            break;
+        case "regexp":
+            m.test = function (actual) {
+                if (typeof actual !== "string") {
+                    return false;
+                }
+                return expectation.test(actual);
+            };
+            break;
+        case "function":
+            m.test = expectation;
+            if (message) {
+                m.message = message;
+            } else {
+                m.message = "match(" + sinon.functionName(expectation) + ")";
+            }
+            break;
+        default:
+            m.test = function (actual) {
+              return sinon.deepEqual(expectation, actual);
+            };
+        }
+        if (!m.message) {
+            m.message = "match(" + expectation + ")";
+        }
+        return m;
+    };
+
+    match.isMatcher = isMatcher;
+
+    match.any = match(function () {
+        return true;
+    }, "any");
+
+    match.defined = match(function (actual) {
+        return actual !== null && actual !== undefined;
+    }, "defined");
+
+    match.truthy = match(function (actual) {
+        return !!actual;
+    }, "truthy");
+
+    match.falsy = match(function (actual) {
+        return !actual;
+    }, "falsy");
+
+    match.same = function (expectation) {
+        return match(function (actual) {
+            return expectation === actual;
+        }, "same(" + expectation + ")");
+    };
+
+    match.typeOf = function (type) {
+        assertType(type, "string", "type");
+        return match(function (actual) {
+            return sinon.typeOf(actual) === type;
+        }, "typeOf(\"" + type + "\")");
+    };
+
+    match.instanceOf = function (type) {
+        assertType(type, "function", "type");
+        return match(function (actual) {
+            return actual instanceof type;
+        }, "instanceOf(" + sinon.functionName(type) + ")");
+    };
+
+    function createPropertyMatcher(propertyTest, messagePrefix) {
+        return function (property, value) {
+            assertType(property, "string", "property");
+            var onlyProperty = arguments.length === 1;
+            var message = messagePrefix + "(\"" + property + "\"";
+            if (!onlyProperty) {
+                message += ", " + value;
+            }
+            message += ")";
+            return match(function (actual) {
+                if (actual === undefined || actual === null ||
+                        !propertyTest(actual, property)) {
+                    return false;
+                }
+                return onlyProperty || sinon.deepEqual(value, actual[property]);
+            }, message);
+        };
+    }
+
+    match.has = createPropertyMatcher(function (actual, property) {
+        if (typeof actual === "object") {
+            return property in actual;
+        }
+        return actual[property] !== undefined;
+    }, "has");
+
+    match.hasOwn = createPropertyMatcher(function (actual, property) {
+        return actual.hasOwnProperty(property);
+    }, "hasOwn");
+
+    match.bool = match.typeOf("boolean");
+    match.number = match.typeOf("number");
+    match.string = match.typeOf("string");
+    match.object = match.typeOf("object");
+    match.func = match.typeOf("function");
+    match.array = match.typeOf("array");
+    match.regexp = match.typeOf("regexp");
+    match.date = match.typeOf("date");
+
+    if (commonJSModule) {
+        module.exports = match;
+    } else {
+        sinon.match = match;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+  * @depend ../sinon.js
+  * @depend match.js
+  */
+/*jslint eqeqeq: false, onevar: false, plusplus: false*/
+/*global module, require, sinon*/
+/**
+  * Spy calls
+  *
+  * @author Christian Johansen (christian@cjohansen.no)
+  * @author Maximilian Antoni (mail@maxantoni.de)
+  * @license BSD
+  *
+  * Copyright (c) 2010-2013 Christian Johansen
+  * Copyright (c) 2013 Maximilian Antoni
+  */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== 'undefined' && module.exports;
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function throwYieldError(proxy, text, args) {
+        var msg = sinon.functionName(proxy) + text;
+        if (args.length) {
+            msg += " Received [" + slice.call(args).join(", ") + "]";
+        }
+        throw new Error(msg);
+    }
+
+    var slice = Array.prototype.slice;
+
+    var callProto = {
+        calledOn: function calledOn(thisValue) {
+            if (sinon.match && sinon.match.isMatcher(thisValue)) {
+                return thisValue.test(this.thisValue);
+            }
+            return this.thisValue === thisValue;
+        },
+
+        calledWith: function calledWith() {
+            for (var i = 0, l = arguments.length; i < l; i += 1) {
+                if (!sinon.deepEqual(arguments[i], this.args[i])) {
+                    return false;
+                }
+            }
+
+            return true;
+        },
+
+        calledWithMatch: function calledWithMatch() {
+            for (var i = 0, l = arguments.length; i < l; i += 1) {
+                var actual = this.args[i];
+                var expectation = arguments[i];
+                if (!sinon.match || !sinon.match(expectation).test(actual)) {
+                    return false;
+                }
+            }
+            return true;
+        },
+
+        calledWithExactly: function calledWithExactly() {
+            return arguments.length == this.args.length &&
+                this.calledWith.apply(this, arguments);
+        },
+
+        notCalledWith: function notCalledWith() {
+            return !this.calledWith.apply(this, arguments);
+        },
+
+        notCalledWithMatch: function notCalledWithMatch() {
+            return !this.calledWithMatch.apply(this, arguments);
+        },
+
+        returned: function returned(value) {
+            return sinon.deepEqual(value, this.returnValue);
+        },
+
+        threw: function threw(error) {
+            if (typeof error === "undefined" || !this.exception) {
+                return !!this.exception;
+            }
+
+            return this.exception === error || this.exception.name === error;
+        },
+
+        calledWithNew: function calledWithNew() {
+            return this.thisValue instanceof this.proxy;
+        },
+
+        calledBefore: function (other) {
+            return this.callId < other.callId;
+        },
+
+        calledAfter: function (other) {
+            return this.callId > other.callId;
+        },
+
+        callArg: function (pos) {
+            this.args[pos]();
+        },
+
+        callArgOn: function (pos, thisValue) {
+            this.args[pos].apply(thisValue);
+        },
+
+        callArgWith: function (pos) {
+            this.callArgOnWith.apply(this, [pos, null].concat(slice.call(arguments, 1)));
+        },
+
+        callArgOnWith: function (pos, thisValue) {
+            var args = slice.call(arguments, 2);
+            this.args[pos].apply(thisValue, args);
+        },
+
+        "yield": function () {
+            this.yieldOn.apply(this, [null].concat(slice.call(arguments, 0)));
+        },
+
+        yieldOn: function (thisValue) {
+            var args = this.args;
+            for (var i = 0, l = args.length; i < l; ++i) {
+                if (typeof args[i] === "function") {
+                    args[i].apply(thisValue, slice.call(arguments, 1));
+                    return;
+                }
+            }
+            throwYieldError(this.proxy, " cannot yield since no callback was passed.", args);
+        },
+
+        yieldTo: function (prop) {
+            this.yieldToOn.apply(this, [prop, null].concat(slice.call(arguments, 1)));
+        },
+
+        yieldToOn: function (prop, thisValue) {
+            var args = this.args;
+            for (var i = 0, l = args.length; i < l; ++i) {
+                if (args[i] && typeof args[i][prop] === "function") {
+                    args[i][prop].apply(thisValue, slice.call(arguments, 2));
+                    return;
+                }
+            }
+            throwYieldError(this.proxy, " cannot yield to '" + prop +
+                "' since no callback was passed.", args);
+        },
+
+        toString: function () {
+            var callStr = this.proxy.toString() + "(";
+            var args = [];
+
+            for (var i = 0, l = this.args.length; i < l; ++i) {
+                args.push(sinon.format(this.args[i]));
+            }
+
+            callStr = callStr + args.join(", ") + ")";
+
+            if (typeof this.returnValue != "undefined") {
+                callStr += " => " + sinon.format(this.returnValue);
+            }
+
+            if (this.exception) {
+                callStr += " !" + this.exception.name;
+
+                if (this.exception.message) {
+                    callStr += "(" + this.exception.message + ")";
+                }
+            }
+
+            return callStr;
+        }
+    };
+
+    callProto.invokeCallback = callProto.yield;
+
+    function createSpyCall(spy, thisValue, args, returnValue, exception, id) {
+        if (typeof id !== "number") {
+            throw new TypeError("Call id is not a number");
+        }
+        var proxyCall = sinon.create(callProto);
+        proxyCall.proxy = spy;
+        proxyCall.thisValue = thisValue;
+        proxyCall.args = args;
+        proxyCall.returnValue = returnValue;
+        proxyCall.exception = exception;
+        proxyCall.callId = id;
+
+        return proxyCall;
+    }
+    createSpyCall.toString = callProto.toString; // used by mocks
+
+    if (commonJSModule) {
+        module.exports = createSpyCall;
+    } else {
+        sinon.spyCall = createSpyCall;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+
+/**
+  * @depend ../sinon.js
+  * @depend call.js
+  */
+/*jslint eqeqeq: false, onevar: false, plusplus: false*/
+/*global module, require, sinon*/
+/**
+  * Spy functions
+  *
+  * @author Christian Johansen (christian@cjohansen.no)
+  * @license BSD
+  *
+  * Copyright (c) 2010-2013 Christian Johansen
+  */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== 'undefined' && module.exports;
+    var push = Array.prototype.push;
+    var slice = Array.prototype.slice;
+    var callId = 0;
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function spy(object, property) {
+        if (!property && typeof object == "function") {
+            return spy.create(object);
+        }
+
+        if (!object && !property) {
+            return spy.create(function () { });
+        }
+
+        var method = object[property];
+        return sinon.wrapMethod(object, property, spy.create(method));
+    }
+
+    function matchingFake(fakes, args, strict) {
+        if (!fakes) {
+            return;
+        }
+
+        for (var i = 0, l = fakes.length; i < l; i++) {
+            if (fakes[i].matches(args, strict)) {
+                return fakes[i];
+            }
+        }
+    }
+
+    function incrementCallCount() {
+        this.called = true;
+        this.callCount += 1;
+        this.notCalled = false;
+        this.calledOnce = this.callCount == 1;
+        this.calledTwice = this.callCount == 2;
+        this.calledThrice = this.callCount == 3;
+    }
+
+    function createCallProperties() {
+        this.firstCall = this.getCall(0);
+        this.secondCall = this.getCall(1);
+        this.thirdCall = this.getCall(2);
+        this.lastCall = this.getCall(this.callCount - 1);
+    }
+
+    var vars = "a,b,c,d,e,f,g,h,i,j,k,l";
+    function createProxy(func) {
+        // Retain the function length:
+        var p;
+        if (func.length) {
+            eval("p = (function proxy(" + vars.substring(0, func.length * 2 - 1) +
+                ") { return p.invoke(func, this, slice.call(arguments)); });");
+        }
+        else {
+            p = function proxy() {
+                return p.invoke(func, this, slice.call(arguments));
+            };
+        }
+        return p;
+    }
+
+    var uuid = 0;
+
+    // Public API
+    var spyApi = {
+        reset: function () {
+            this.called = false;
+            this.notCalled = true;
+            this.calledOnce = false;
+            this.calledTwice = false;
+            this.calledThrice = false;
+            this.callCount = 0;
+            this.firstCall = null;
+            this.secondCall = null;
+            this.thirdCall = null;
+            this.lastCall = null;
+            this.args = [];
+            this.returnValues = [];
+            this.thisValues = [];
+            this.exceptions = [];
+            this.callIds = [];
+            if (this.fakes) {
+                for (var i = 0; i < this.fakes.length; i++) {
+                    this.fakes[i].reset();
+                }
+            }
+        },
+
+        create: function create(func) {
+            var name;
+
+            if (typeof func != "function") {
+                func = function () { };
+            } else {
+                name = sinon.functionName(func);
+            }
+
+            var proxy = createProxy(func);
+
+            sinon.extend(proxy, spy);
+            delete proxy.create;
+            sinon.extend(proxy, func);
+
+            proxy.reset();
+            proxy.prototype = func.prototype;
+            proxy.displayName = name || "spy";
+            proxy.toString = sinon.functionToString;
+            proxy._create = sinon.spy.create;
+            proxy.id = "spy#" + uuid++;
+
+            return proxy;
+        },
+
+        invoke: function invoke(func, thisValue, args) {
+            var matching = matchingFake(this.fakes, args);
+            var exception, returnValue;
+
+            incrementCallCount.call(this);
+            push.call(this.thisValues, thisValue);
+            push.call(this.args, args);
+            push.call(this.callIds, callId++);
+
+            try {
+                if (matching) {
+                    returnValue = matching.invoke(func, thisValue, args);
+                } else {
+                    returnValue = (this.func || func).apply(thisValue, args);
+                }
+
+                var thisCall = this.getCall(this.callCount - 1);
+                if (thisCall.calledWithNew() && typeof returnValue !== 'object') {
+                    returnValue = thisValue;
+                }
+            } catch (e) {
+                exception = e;
+            }
+
+            push.call(this.exceptions, exception);
+            push.call(this.returnValues, returnValue);
+
+            createCallProperties.call(this);
+
+            if (exception !== undefined) {
+                throw exception;
+            }
+
+            return returnValue;
+        },
+
+        getCall: function getCall(i) {
+            if (i < 0 || i >= this.callCount) {
+                return null;
+            }
+
+            return sinon.spyCall(this, this.thisValues[i], this.args[i],
+                                    this.returnValues[i], this.exceptions[i],
+                                    this.callIds[i]);
+        },
+
+        getCalls: function () {
+            var calls = [];
+            var i;
+
+            for (i = 0; i < this.callCount; i++) {
+                calls.push(this.getCall(i));
+            }
+
+            return calls;
+        },
+
+        calledBefore: function calledBefore(spyFn) {
+            if (!this.called) {
+                return false;
+            }
+
+            if (!spyFn.called) {
+                return true;
+            }
+
+            return this.callIds[0] < spyFn.callIds[spyFn.callIds.length - 1];
+        },
+
+        calledAfter: function calledAfter(spyFn) {
+            if (!this.called || !spyFn.called) {
+                return false;
+            }
+
+            return this.callIds[this.callCount - 1] > spyFn.callIds[spyFn.callCount - 1];
+        },
+
+        withArgs: function () {
+            var args = slice.call(arguments);
+
+            if (this.fakes) {
+                var match = matchingFake(this.fakes, args, true);
+
+                if (match) {
+                    return match;
+                }
+            } else {
+                this.fakes = [];
+            }
+
+            var original = this;
+            var fake = this._create();
+            fake.matchingAguments = args;
+            fake.parent = this;
+            push.call(this.fakes, fake);
+
+            fake.withArgs = function () {
+                return original.withArgs.apply(original, arguments);
+            };
+
+            for (var i = 0; i < this.args.length; i++) {
+                if (fake.matches(this.args[i])) {
+                    incrementCallCount.call(fake);
+                    push.call(fake.thisValues, this.thisValues[i]);
+                    push.call(fake.args, this.args[i]);
+                    push.call(fake.returnValues, this.returnValues[i]);
+                    push.call(fake.exceptions, this.exceptions[i]);
+                    push.call(fake.callIds, this.callIds[i]);
+                }
+            }
+            createCallProperties.call(fake);
+
+            return fake;
+        },
+
+        matches: function (args, strict) {
+            var margs = this.matchingAguments;
+
+            if (margs.length <= args.length &&
+                sinon.deepEqual(margs, args.slice(0, margs.length))) {
+                return !strict || margs.length == args.length;
+            }
+        },
+
+        printf: function (format) {
+            var spy = this;
+            var args = slice.call(arguments, 1);
+            var formatter;
+
+            return (format || "").replace(/%(.)/g, function (match, specifyer) {
+                formatter = spyApi.formatters[specifyer];
+
+                if (typeof formatter == "function") {
+                    return formatter.call(null, spy, args);
+                } else if (!isNaN(parseInt(specifyer, 10))) {
+                    return sinon.format(args[specifyer - 1]);
+                }
+
+                return "%" + specifyer;
+            });
+        }
+    };
+
+    function delegateToCalls(method, matchAny, actual, notCalled) {
+        spyApi[method] = function () {
+            if (!this.called) {
+                if (notCalled) {
+                    return notCalled.apply(this, arguments);
+                }
+                return false;
+            }
+
+            var currentCall;
+            var matches = 0;
+
+            for (var i = 0, l = this.callCount; i < l; i += 1) {
+                currentCall = this.getCall(i);
+
+                if (currentCall[actual || method].apply(currentCall, arguments)) {
+                    matches += 1;
+
+                    if (matchAny) {
+                        return true;
+                    }
+                }
+            }
+
+            return matches === this.callCount;
+        };
+    }
+
+    delegateToCalls("calledOn", true);
+    delegateToCalls("alwaysCalledOn", false, "calledOn");
+    delegateToCalls("calledWith", true);
+    delegateToCalls("calledWithMatch", true);
+    delegateToCalls("alwaysCalledWith", false, "calledWith");
+    delegateToCalls("alwaysCalledWithMatch", false, "calledWithMatch");
+    delegateToCalls("calledWithExactly", true);
+    delegateToCalls("alwaysCalledWithExactly", false, "calledWithExactly");
+    delegateToCalls("neverCalledWith", false, "notCalledWith",
+        function () { return true; });
+    delegateToCalls("neverCalledWithMatch", false, "notCalledWithMatch",
+        function () { return true; });
+    delegateToCalls("threw", true);
+    delegateToCalls("alwaysThrew", false, "threw");
+    delegateToCalls("returned", true);
+    delegateToCalls("alwaysReturned", false, "returned");
+    delegateToCalls("calledWithNew", true);
+    delegateToCalls("alwaysCalledWithNew", false, "calledWithNew");
+    delegateToCalls("callArg", false, "callArgWith", function () {
+        throw new Error(this.toString() + " cannot call arg since it was not yet invoked.");
+    });
+    spyApi.callArgWith = spyApi.callArg;
+    delegateToCalls("callArgOn", false, "callArgOnWith", function () {
+        throw new Error(this.toString() + " cannot call arg since it was not yet invoked.");
+    });
+    spyApi.callArgOnWith = spyApi.callArgOn;
+    delegateToCalls("yield", false, "yield", function () {
+        throw new Error(this.toString() + " cannot yield since it was not yet invoked.");
+    });
+    // "invokeCallback" is an alias for "yield" since "yield" is invalid in strict mode.
+    spyApi.invokeCallback = spyApi.yield;
+    delegateToCalls("yieldOn", false, "yieldOn", function () {
+        throw new Error(this.toString() + " cannot yield since it was not yet invoked.");
+    });
+    delegateToCalls("yieldTo", false, "yieldTo", function (property) {
+        throw new Error(this.toString() + " cannot yield to '" + property +
+            "' since it was not yet invoked.");
+    });
+    delegateToCalls("yieldToOn", false, "yieldToOn", function (property) {
+        throw new Error(this.toString() + " cannot yield to '" + property +
+            "' since it was not yet invoked.");
+    });
+
+    spyApi.formatters = {
+        "c": function (spy) {
+            return sinon.timesInWords(spy.callCount);
+        },
+
+        "n": function (spy) {
+            return spy.toString();
+        },
+
+        "C": function (spy) {
+            var calls = [];
+
+            for (var i = 0, l = spy.callCount; i < l; ++i) {
+                var stringifiedCall = "    " + spy.getCall(i).toString();
+                if (/\n/.test(calls[i - 1])) {
+                    stringifiedCall = "\n" + stringifiedCall;
+                }
+                push.call(calls, stringifiedCall);
+            }
+
+            return calls.length > 0 ? "\n" + calls.join("\n") : "";
+        },
+
+        "t": function (spy) {
+            var objects = [];
+
+            for (var i = 0, l = spy.callCount; i < l; ++i) {
+                push.call(objects, sinon.format(spy.thisValues[i]));
+            }
+
+            return objects.join(", ");
+        },
+
+        "*": function (spy, args) {
+            var formatted = [];
+
+            for (var i = 0, l = args.length; i < l; ++i) {
+                push.call(formatted, sinon.format(args[i]));
+            }
+
+            return formatted.join(", ");
+        }
+    };
+
+    sinon.extend(spy, spyApi);
+
+    spy.spyCall = sinon.spyCall;
+
+    if (commonJSModule) {
+        module.exports = spy;
+    } else {
+        sinon.spy = spy;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend ../sinon.js
+ */
+/*jslint eqeqeq: false, onevar: false*/
+/*global module, require, sinon, process, setImmediate, setTimeout*/
+/**
+ * Stub behavior
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @author Tim Fischbach (mail@timfischbach.de)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== 'undefined' && module.exports;
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    var slice = Array.prototype.slice;
+    var join = Array.prototype.join;
+    var proto;
+
+    var nextTick = (function () {
+        if (typeof process === "object" && typeof process.nextTick === "function") {
+            return process.nextTick;
+        } else if (typeof setImmediate === "function") {
+            return setImmediate;
+        } else {
+            return function (callback) {
+                setTimeout(callback, 0);
+            };
+        }
+    })();
+
+    function throwsException(error, message) {
+        if (typeof error == "string") {
+            this.exception = new Error(message || "");
+            this.exception.name = error;
+        } else if (!error) {
+            this.exception = new Error("Error");
+        } else {
+            this.exception = error;
+        }
+
+        return this;
+    }
+
+    function getCallback(behavior, args) {
+        var callArgAt = behavior.callArgAt;
+
+        if (callArgAt < 0) {
+            var callArgProp = behavior.callArgProp;
+
+            for (var i = 0, l = args.length; i < l; ++i) {
+                if (!callArgProp && typeof args[i] == "function") {
+                    return args[i];
+                }
+
+                if (callArgProp && args[i] &&
+                    typeof args[i][callArgProp] == "function") {
+                    return args[i][callArgProp];
+                }
+            }
+
+            return null;
+        }
+
+        return args[callArgAt];
+    }
+
+    function getCallbackError(behavior, func, args) {
+        if (behavior.callArgAt < 0) {
+            var msg;
+
+            if (behavior.callArgProp) {
+                msg = sinon.functionName(behavior.stub) +
+                    " expected to yield to '" + behavior.callArgProp +
+                    "', but no object with such a property was passed.";
+            } else {
+                msg = sinon.functionName(behavior.stub) +
+                    " expected to yield, but no callback was passed.";
+            }
+
+            if (args.length > 0) {
+                msg += " Received [" + join.call(args, ", ") + "]";
+            }
+
+            return msg;
+        }
+
+        return "argument at index " + behavior.callArgAt + " is not a function: " + func;
+    }
+
+    function callCallback(behavior, args) {
+        if (typeof behavior.callArgAt == "number") {
+            var func = getCallback(behavior, args);
+
+            if (typeof func != "function") {
+                throw new TypeError(getCallbackError(behavior, func, args));
+            }
+
+            if (behavior.callbackAsync) {
+                nextTick(function() {
+                    func.apply(behavior.callbackContext, behavior.callbackArguments);
+                });
+            } else {
+                func.apply(behavior.callbackContext, behavior.callbackArguments);
+            }
+        }
+    }
+
+    proto = {
+        create: function(stub) {
+            var behavior = sinon.extend({}, sinon.behavior);
+            delete behavior.create;
+            behavior.stub = stub;
+
+            return behavior;
+        },
+
+        isPresent: function() {
+            return (typeof this.callArgAt == 'number' ||
+                    this.exception ||
+                    typeof this.returnArgAt == 'number' ||
+                    this.returnThis ||
+                    this.returnValueDefined);
+        },
+
+        invoke: function(context, args) {
+            callCallback(this, args);
+
+            if (this.exception) {
+                throw this.exception;
+            } else if (typeof this.returnArgAt == 'number') {
+                return args[this.returnArgAt];
+            } else if (this.returnThis) {
+                return context;
+            }
+
+            return this.returnValue;
+        },
+
+        onCall: function(index) {
+            return this.stub.onCall(index);
+        },
+
+        onFirstCall: function() {
+            return this.stub.onFirstCall();
+        },
+
+        onSecondCall: function() {
+            return this.stub.onSecondCall();
+        },
+
+        onThirdCall: function() {
+            return this.stub.onThirdCall();
+        },
+
+        withArgs: function(/* arguments */) {
+            throw new Error('Defining a stub by invoking "stub.onCall(...).withArgs(...)" is not supported. ' +
+                            'Use "stub.withArgs(...).onCall(...)" to define sequential behavior for calls with certain arguments.');
+        },
+
+        callsArg: function callsArg(pos) {
+            if (typeof pos != "number") {
+                throw new TypeError("argument index is not number");
+            }
+
+            this.callArgAt = pos;
+            this.callbackArguments = [];
+            this.callbackContext = undefined;
+            this.callArgProp = undefined;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        callsArgOn: function callsArgOn(pos, context) {
+            if (typeof pos != "number") {
+                throw new TypeError("argument index is not number");
+            }
+            if (typeof context != "object") {
+                throw new TypeError("argument context is not an object");
+            }
+
+            this.callArgAt = pos;
+            this.callbackArguments = [];
+            this.callbackContext = context;
+            this.callArgProp = undefined;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        callsArgWith: function callsArgWith(pos) {
+            if (typeof pos != "number") {
+                throw new TypeError("argument index is not number");
+            }
+
+            this.callArgAt = pos;
+            this.callbackArguments = slice.call(arguments, 1);
+            this.callbackContext = undefined;
+            this.callArgProp = undefined;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        callsArgOnWith: function callsArgWith(pos, context) {
+            if (typeof pos != "number") {
+                throw new TypeError("argument index is not number");
+            }
+            if (typeof context != "object") {
+                throw new TypeError("argument context is not an object");
+            }
+
+            this.callArgAt = pos;
+            this.callbackArguments = slice.call(arguments, 2);
+            this.callbackContext = context;
+            this.callArgProp = undefined;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        yields: function () {
+            this.callArgAt = -1;
+            this.callbackArguments = slice.call(arguments, 0);
+            this.callbackContext = undefined;
+            this.callArgProp = undefined;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        yieldsOn: function (context) {
+            if (typeof context != "object") {
+                throw new TypeError("argument context is not an object");
+            }
+
+            this.callArgAt = -1;
+            this.callbackArguments = slice.call(arguments, 1);
+            this.callbackContext = context;
+            this.callArgProp = undefined;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        yieldsTo: function (prop) {
+            this.callArgAt = -1;
+            this.callbackArguments = slice.call(arguments, 1);
+            this.callbackContext = undefined;
+            this.callArgProp = prop;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+        yieldsToOn: function (prop, context) {
+            if (typeof context != "object") {
+                throw new TypeError("argument context is not an object");
+            }
+
+            this.callArgAt = -1;
+            this.callbackArguments = slice.call(arguments, 2);
+            this.callbackContext = context;
+            this.callArgProp = prop;
+            this.callbackAsync = false;
+
+            return this;
+        },
+
+
+        "throws": throwsException,
+        throwsException: throwsException,
+
+        returns: function returns(value) {
+            this.returnValue = value;
+            this.returnValueDefined = true;
+
+            return this;
+        },
+
+        returnsArg: function returnsArg(pos) {
+            if (typeof pos != "number") {
+                throw new TypeError("argument index is not number");
+            }
+
+            this.returnArgAt = pos;
+
+            return this;
+        },
+
+        returnsThis: function returnsThis() {
+            this.returnThis = true;
+
+            return this;
+        }
+    };
+
+    // create asynchronous versions of callsArg* and yields* methods
+    for (var method in proto) {
+        // need to avoid creating anotherasync versions of the newly added async methods
+        if (proto.hasOwnProperty(method) &&
+            method.match(/^(callsArg|yields)/) &&
+            !method.match(/Async/)) {
+            proto[method + 'Async'] = (function (syncFnName) {
+                return function () {
+                    var result = this[syncFnName].apply(this, arguments);
+                    this.callbackAsync = true;
+                    return result;
+                };
+            })(method);
+        }
+    }
+
+    if (commonJSModule) {
+        module.exports = proto;
+    } else {
+        sinon.behavior = proto;
+    }
+}(typeof sinon == "object" && sinon || null));
+/**
+ * @depend ../sinon.js
+ * @depend spy.js
+ * @depend behavior.js
+ */
+/*jslint eqeqeq: false, onevar: false*/
+/*global module, require, sinon*/
+/**
+ * Stub functions
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== 'undefined' && module.exports;
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function stub(object, property, func) {
+        if (!!func && typeof func != "function") {
+            throw new TypeError("Custom stub should be function");
+        }
+
+        var wrapper;
+
+        if (func) {
+            wrapper = sinon.spy && sinon.spy.create ? sinon.spy.create(func) : func;
+        } else {
+            wrapper = stub.create();
+        }
+
+        if (!object && typeof property === "undefined") {
+            return sinon.stub.create();
+        }
+
+        if (typeof property === "undefined" && typeof object == "object") {
+            for (var prop in object) {
+                if (typeof object[prop] === "function") {
+                    stub(object, prop);
+                }
+            }
+
+            return object;
+        }
+
+        return sinon.wrapMethod(object, property, wrapper);
+    }
+
+    function getDefaultBehavior(stub) {
+        return stub.defaultBehavior || getParentBehaviour(stub) || sinon.behavior.create(stub);
+    }
+
+    function getParentBehaviour(stub) {
+        return (stub.parent && getCurrentBehavior(stub.parent));
+    }
+
+    function getCurrentBehavior(stub) {
+        var behavior = stub.behaviors[stub.callCount - 1];
+        return behavior && behavior.isPresent() ? behavior : getDefaultBehavior(stub);
+    }
+
+    var uuid = 0;
+
+    sinon.extend(stub, (function () {
+        var proto = {
+            create: function create() {
+                var functionStub = function () {
+                    return getCurrentBehavior(functionStub).invoke(this, arguments);
+                };
+
+                functionStub.id = "stub#" + uuid++;
+                var orig = functionStub;
+                functionStub = sinon.spy.create(functionStub);
+                functionStub.func = orig;
+
+                sinon.extend(functionStub, stub);
+                functionStub._create = sinon.stub.create;
+                functionStub.displayName = "stub";
+                functionStub.toString = sinon.functionToString;
+
+                functionStub.defaultBehavior = null;
+                functionStub.behaviors = [];
+
+                return functionStub;
+            },
+
+            resetBehavior: function () {
+                var i;
+
+                this.defaultBehavior = null;
+                this.behaviors = [];
+
+                delete this.returnValue;
+                delete this.returnArgAt;
+                this.returnThis = false;
+
+                if (this.fakes) {
+                    for (i = 0; i < this.fakes.length; i++) {
+                        this.fakes[i].resetBehavior();
+                    }
+                }
+            },
+
+            onCall: function(index) {
+                if (!this.behaviors[index]) {
+                    this.behaviors[index] = sinon.behavior.create(this);
+                }
+
+                return this.behaviors[index];
+            },
+
+            onFirstCall: function() {
+                return this.onCall(0);
+            },
+
+            onSecondCall: function() {
+                return this.onCall(1);
+            },
+
+            onThirdCall: function() {
+                return this.onCall(2);
+            }
+        };
+
+        for (var method in sinon.behavior) {
+            if (sinon.behavior.hasOwnProperty(method) &&
+                !proto.hasOwnProperty(method) &&
+                method != 'create' &&
+                method != 'withArgs' &&
+                method != 'invoke') {
+                proto[method] = (function(behaviorMethod) {
+                    return function() {
+                        this.defaultBehavior = this.defaultBehavior || sinon.behavior.create(this);
+                        this.defaultBehavior[behaviorMethod].apply(this.defaultBehavior, arguments);
+                        return this;
+                    };
+                }(method));
+            }
+        }
+
+        return proto;
+    }()));
+
+    if (commonJSModule) {
+        module.exports = stub;
+    } else {
+        sinon.stub = stub;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend ../sinon.js
+ * @depend stub.js
+ */
+/*jslint eqeqeq: false, onevar: false, nomen: false*/
+/*global module, require, sinon*/
+/**
+ * Mock functions.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== 'undefined' && module.exports;
+    var push = [].push;
+    var match;
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    match = sinon.match;
+
+    if (!match && commonJSModule) {
+        match = require("./match");
+    }
+
+    function mock(object) {
+        if (!object) {
+            return sinon.expectation.create("Anonymous mock");
+        }
+
+        return mock.create(object);
+    }
+
+    sinon.mock = mock;
+
+    sinon.extend(mock, (function () {
+        function each(collection, callback) {
+            if (!collection) {
+                return;
+            }
+
+            for (var i = 0, l = collection.length; i < l; i += 1) {
+                callback(collection[i]);
+            }
+        }
+
+        return {
+            create: function create(object) {
+                if (!object) {
+                    throw new TypeError("object is null");
+                }
+
+                var mockObject = sinon.extend({}, mock);
+                mockObject.object = object;
+                delete mockObject.create;
+
+                return mockObject;
+            },
+
+            expects: function expects(method) {
+                if (!method) {
+                    throw new TypeError("method is falsy");
+                }
+
+                if (!this.expectations) {
+                    this.expectations = {};
+                    this.proxies = [];
+                }
+
+                if (!this.expectations[method]) {
+                    this.expectations[method] = [];
+                    var mockObject = this;
+
+                    sinon.wrapMethod(this.object, method, function () {
+                        return mockObject.invokeMethod(method, this, arguments);
+                    });
+
+                    push.call(this.proxies, method);
+                }
+
+                var expectation = sinon.expectation.create(method);
+                push.call(this.expectations[method], expectation);
+
+                return expectation;
+            },
+
+            restore: function restore() {
+                var object = this.object;
+
+                each(this.proxies, function (proxy) {
+                    if (typeof object[proxy].restore == "function") {
+                        object[proxy].restore();
+                    }
+                });
+            },
+
+            verify: function verify() {
+                var expectations = this.expectations || {};
+                var messages = [], met = [];
+
+                each(this.proxies, function (proxy) {
+                    each(expectations[proxy], function (expectation) {
+                        if (!expectation.met()) {
+                            push.call(messages, expectation.toString());
+                        } else {
+                            push.call(met, expectation.toString());
+                        }
+                    });
+                });
+
+                this.restore();
+
+                if (messages.length > 0) {
+                    sinon.expectation.fail(messages.concat(met).join("\n"));
+                } else {
+                    sinon.expectation.pass(messages.concat(met).join("\n"));
+                }
+
+                return true;
+            },
+
+            invokeMethod: function invokeMethod(method, thisValue, args) {
+                var expectations = this.expectations && this.expectations[method];
+                var length = expectations && expectations.length || 0, i;
+
+                for (i = 0; i < length; i += 1) {
+                    if (!expectations[i].met() &&
+                        expectations[i].allowsCall(thisValue, args)) {
+                        return expectations[i].apply(thisValue, args);
+                    }
+                }
+
+                var messages = [], available, exhausted = 0;
+
+                for (i = 0; i < length; i += 1) {
+                    if (expectations[i].allowsCall(thisValue, args)) {
+                        available = available || expectations[i];
+                    } else {
+                        exhausted += 1;
+                    }
+                    push.call(messages, "    " + expectations[i].toString());
+                }
+
+                if (exhausted === 0) {
+                    return available.apply(thisValue, args);
+                }
+
+                messages.unshift("Unexpected call: " + sinon.spyCall.toString.call({
+                    proxy: method,
+                    args: args
+                }));
+
+                sinon.expectation.fail(messages.join("\n"));
+            }
+        };
+    }()));
+
+    var times = sinon.timesInWords;
+
+    sinon.expectation = (function () {
+        var slice = Array.prototype.slice;
+        var _invoke = sinon.spy.invoke;
+
+        function callCountInWords(callCount) {
+            if (callCount == 0) {
+                return "never called";
+            } else {
+                return "called " + times(callCount);
+            }
+        }
+
+        function expectedCallCountInWords(expectation) {
+            var min = expectation.minCalls;
+            var max = expectation.maxCalls;
+
+            if (typeof min == "number" && typeof max == "number") {
+                var str = times(min);
+
+                if (min != max) {
+                    str = "at least " + str + " and at most " + times(max);
+                }
+
+                return str;
+            }
+
+            if (typeof min == "number") {
+                return "at least " + times(min);
+            }
+
+            return "at most " + times(max);
+        }
+
+        function receivedMinCalls(expectation) {
+            var hasMinLimit = typeof expectation.minCalls == "number";
+            return !hasMinLimit || expectation.callCount >= expectation.minCalls;
+        }
+
+        function receivedMaxCalls(expectation) {
+            if (typeof expectation.maxCalls != "number") {
+                return false;
+            }
+
+            return expectation.callCount == expectation.maxCalls;
+        }
+
+        function verifyMatcher(possibleMatcher, arg){
+            if (match && match.isMatcher(possibleMatcher)) {
+                return possibleMatcher.test(arg);
+            } else {
+                return true;
+            }
+        }
+
+        return {
+            minCalls: 1,
+            maxCalls: 1,
+
+            create: function create(methodName) {
+                var expectation = sinon.extend(sinon.stub.create(), sinon.expectation);
+                delete expectation.create;
+                expectation.method = methodName;
+
+                return expectation;
+            },
+
+            invoke: function invoke(func, thisValue, args) {
+                this.verifyCallAllowed(thisValue, args);
+
+                return _invoke.apply(this, arguments);
+            },
+
+            atLeast: function atLeast(num) {
+                if (typeof num != "number") {
+                    throw new TypeError("'" + num + "' is not number");
+                }
+
+                if (!this.limitsSet) {
+                    this.maxCalls = null;
+                    this.limitsSet = true;
+                }
+
+                this.minCalls = num;
+
+                return this;
+            },
+
+            atMost: function atMost(num) {
+                if (typeof num != "number") {
+                    throw new TypeError("'" + num + "' is not number");
+                }
+
+                if (!this.limitsSet) {
+                    this.minCalls = null;
+                    this.limitsSet = true;
+                }
+
+                this.maxCalls = num;
+
+                return this;
+            },
+
+            never: function never() {
+                return this.exactly(0);
+            },
+
+            once: function once() {
+                return this.exactly(1);
+            },
+
+            twice: function twice() {
+                return this.exactly(2);
+            },
+
+            thrice: function thrice() {
+                return this.exactly(3);
+            },
+
+            exactly: function exactly(num) {
+                if (typeof num != "number") {
+                    throw new TypeError("'" + num + "' is not a number");
+                }
+
+                this.atLeast(num);
+                return this.atMost(num);
+            },
+
+            met: function met() {
+                return !this.failed && receivedMinCalls(this);
+            },
+
+            verifyCallAllowed: function verifyCallAllowed(thisValue, args) {
+                if (receivedMaxCalls(this)) {
+                    this.failed = true;
+                    sinon.expectation.fail(this.method + " already called " + times(this.maxCalls));
+                }
+
+                if ("expectedThis" in this && this.expectedThis !== thisValue) {
+                    sinon.expectation.fail(this.method + " called with " + thisValue + " as thisValue, expected " +
+                        this.expectedThis);
+                }
+
+                if (!("expectedArguments" in this)) {
+                    return;
+                }
+
+                if (!args) {
+                    sinon.expectation.fail(this.method + " received no arguments, expected " +
+                        sinon.format(this.expectedArguments));
+                }
+
+                if (args.length < this.expectedArguments.length) {
+                    sinon.expectation.fail(this.method + " received too few arguments (" + sinon.format(args) +
+                        "), expected " + sinon.format(this.expectedArguments));
+                }
+
+                if (this.expectsExactArgCount &&
+                    args.length != this.expectedArguments.length) {
+                    sinon.expectation.fail(this.method + " received too many arguments (" + sinon.format(args) +
+                        "), expected " + sinon.format(this.expectedArguments));
+                }
+
+                for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) {
+
+                    if (!verifyMatcher(this.expectedArguments[i],args[i])) {
+                        sinon.expectation.fail(this.method + " received wrong arguments " + sinon.format(args) +
+                            ", didn't match " + this.expectedArguments.toString());
+                    }
+
+                    if (!sinon.deepEqual(this.expectedArguments[i], args[i])) {
+                        sinon.expectation.fail(this.method + " received wrong arguments " + sinon.format(args) +
+                            ", expected " + sinon.format(this.expectedArguments));
+                    }
+                }
+            },
+
+            allowsCall: function allowsCall(thisValue, args) {
+                if (this.met() && receivedMaxCalls(this)) {
+                    return false;
+                }
+
+                if ("expectedThis" in this && this.expectedThis !== thisValue) {
+                    return false;
+                }
+
+                if (!("expectedArguments" in this)) {
+                    return true;
+                }
+
+                args = args || [];
+
+                if (args.length < this.expectedArguments.length) {
+                    return false;
+                }
+
+                if (this.expectsExactArgCount &&
+                    args.length != this.expectedArguments.length) {
+                    return false;
+                }
+
+                for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) {
+                    if (!verifyMatcher(this.expectedArguments[i],args[i])) {
+                        return false;
+                    }
+
+                    if (!sinon.deepEqual(this.expectedArguments[i], args[i])) {
+                        return false;
+                    }
+                }
+
+                return true;
+            },
+
+            withArgs: function withArgs() {
+                this.expectedArguments = slice.call(arguments);
+                return this;
+            },
+
+            withExactArgs: function withExactArgs() {
+                this.withArgs.apply(this, arguments);
+                this.expectsExactArgCount = true;
+                return this;
+            },
+
+            on: function on(thisValue) {
+                this.expectedThis = thisValue;
+                return this;
+            },
+
+            toString: function () {
+                var args = (this.expectedArguments || []).slice();
+
+                if (!this.expectsExactArgCount) {
+                    push.call(args, "[...]");
+                }
+
+                var callStr = sinon.spyCall.toString.call({
+                    proxy: this.method || "anonymous mock expectation",
+                    args: args
+                });
+
+                var message = callStr.replace(", [...", "[, ...") + " " +
+                    expectedCallCountInWords(this);
+
+                if (this.met()) {
+                    return "Expectation met: " + message;
+                }
+
+                return "Expected " + message + " (" +
+                    callCountInWords(this.callCount) + ")";
+            },
+
+            verify: function verify() {
+                if (!this.met()) {
+                    sinon.expectation.fail(this.toString());
+                } else {
+                    sinon.expectation.pass(this.toString());
+                }
+
+                return true;
+            },
+
+            pass: function(message) {
+              sinon.assert.pass(message);
+            },
+            fail: function (message) {
+                var exception = new Error(message);
+                exception.name = "ExpectationError";
+
+                throw exception;
+            }
+        };
+    }());
+
+    if (commonJSModule) {
+        module.exports = mock;
+    } else {
+        sinon.mock = mock;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend ../sinon.js
+ * @depend stub.js
+ * @depend mock.js
+ */
+/*jslint eqeqeq: false, onevar: false, forin: true*/
+/*global module, require, sinon*/
+/**
+ * Collections of stubs, spies and mocks.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== 'undefined' && module.exports;
+    var push = [].push;
+    var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function getFakes(fakeCollection) {
+        if (!fakeCollection.fakes) {
+            fakeCollection.fakes = [];
+        }
+
+        return fakeCollection.fakes;
+    }
+
+    function each(fakeCollection, method) {
+        var fakes = getFakes(fakeCollection);
+
+        for (var i = 0, l = fakes.length; i < l; i += 1) {
+            if (typeof fakes[i][method] == "function") {
+                fakes[i][method]();
+            }
+        }
+    }
+
+    function compact(fakeCollection) {
+        var fakes = getFakes(fakeCollection);
+        var i = 0;
+        while (i < fakes.length) {
+          fakes.splice(i, 1);
+        }
+    }
+
+    var collection = {
+        verify: function resolve() {
+            each(this, "verify");
+        },
+
+        restore: function restore() {
+            each(this, "restore");
+            compact(this);
+        },
+
+        verifyAndRestore: function verifyAndRestore() {
+            var exception;
+
+            try {
+                this.verify();
+            } catch (e) {
+                exception = e;
+            }
+
+            this.restore();
+
+            if (exception) {
+                throw exception;
+            }
+        },
+
+        add: function add(fake) {
+            push.call(getFakes(this), fake);
+            return fake;
+        },
+
+        spy: function spy() {
+            return this.add(sinon.spy.apply(sinon, arguments));
+        },
+
+        stub: function stub(object, property, value) {
+            if (property) {
+                var original = object[property];
+
+                if (typeof original != "function") {
+                    if (!hasOwnProperty.call(object, property)) {
+                        throw new TypeError("Cannot stub non-existent own property " + property);
+                    }
+
+                    object[property] = value;
+
+                    return this.add({
+                        restore: function () {
+                            object[property] = original;
+                        }
+                    });
+                }
+            }
+            if (!property && !!object && typeof object == "object") {
+                var stubbedObj = sinon.stub.apply(sinon, arguments);
+
+                for (var prop in stubbedObj) {
+                    if (typeof stubbedObj[prop] === "function") {
+                        this.add(stubbedObj[prop]);
+                    }
+                }
+
+                return stubbedObj;
+            }
+
+            return this.add(sinon.stub.apply(sinon, arguments));
+        },
+
+        mock: function mock() {
+            return this.add(sinon.mock.apply(sinon, arguments));
+        },
+
+        inject: function inject(obj) {
+            var col = this;
+
+            obj.spy = function () {
+                return col.spy.apply(col, arguments);
+            };
+
+            obj.stub = function () {
+                return col.stub.apply(col, arguments);
+            };
+
+            obj.mock = function () {
+                return col.mock.apply(col, arguments);
+            };
+
+            return obj;
+        }
+    };
+
+    if (commonJSModule) {
+        module.exports = collection;
+    } else {
+        sinon.collection = collection;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/
+/*global module, require, window*/
+/**
+ * Fake timer API
+ * setTimeout
+ * setInterval
+ * clearTimeout
+ * clearInterval
+ * tick
+ * reset
+ * Date
+ *
+ * Inspired by jsUnitMockTimeOut from JsUnit
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+if (typeof sinon == "undefined") {
+    var sinon = {};
+}
+
+(function (global) {
+    var id = 1;
+
+    function addTimer(args, recurring) {
+        if (args.length === 0) {
+            throw new Error("Function requires at least 1 parameter");
+        }
+
+        if (typeof args[0] === "undefined") {
+            throw new Error("Callback must be provided to timer calls");
+        }
+
+        var toId = id++;
+        var delay = args[1] || 0;
+
+        if (!this.timeouts) {
+            this.timeouts = {};
+        }
+
+        this.timeouts[toId] = {
+            id: toId,
+            func: args[0],
+            callAt: this.now + delay,
+            invokeArgs: Array.prototype.slice.call(args, 2)
+        };
+
+        if (recurring === true) {
+            this.timeouts[toId].interval = delay;
+        }
+
+        return toId;
+    }
+
+    function parseTime(str) {
+        if (!str) {
+            return 0;
+        }
+
+        var strings = str.split(":");
+        var l = strings.length, i = l;
+        var ms = 0, parsed;
+
+        if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) {
+            throw new Error("tick only understands numbers and 'h:m:s'");
+        }
+
+        while (i--) {
+            parsed = parseInt(strings[i], 10);
+
+            if (parsed >= 60) {
+                throw new Error("Invalid time " + str);
+            }
+
+            ms += parsed * Math.pow(60, (l - i - 1));
+        }
+
+        return ms * 1000;
+    }
+
+    function createObject(object) {
+        var newObject;
+
+        if (Object.create) {
+            newObject = Object.create(object);
+        } else {
+            var F = function () {};
+            F.prototype = object;
+            newObject = new F();
+        }
+
+        newObject.Date.clock = newObject;
+        return newObject;
+    }
+
+    sinon.clock = {
+        now: 0,
+
+        create: function create(now) {
+            var clock = createObject(this);
+
+            if (typeof now == "number") {
+                clock.now = now;
+            }
+
+            if (!!now && typeof now == "object") {
+                throw new TypeError("now should be milliseconds since UNIX epoch");
+            }
+
+            return clock;
+        },
+
+        setTimeout: function setTimeout(callback, timeout) {
+            return addTimer.call(this, arguments, false);
+        },
+
+        clearTimeout: function clearTimeout(timerId) {
+            if (!this.timeouts) {
+                this.timeouts = [];
+            }
+
+            if (timerId in this.timeouts) {
+                delete this.timeouts[timerId];
+            }
+        },
+
+        setInterval: function setInterval(callback, timeout) {
+            return addTimer.call(this, arguments, true);
+        },
+
+        clearInterval: function clearInterval(timerId) {
+            this.clearTimeout(timerId);
+        },
+
+        setImmediate: function setImmediate(callback) {
+            var passThruArgs = Array.prototype.slice.call(arguments, 1);
+
+            return addTimer.call(this, [callback, 0].concat(passThruArgs), false);
+        },
+
+        clearImmediate: function clearImmediate(timerId) {
+            this.clearTimeout(timerId);
+        },
+
+        tick: function tick(ms) {
+            ms = typeof ms == "number" ? ms : parseTime(ms);
+            var tickFrom = this.now, tickTo = this.now + ms, previous = this.now;
+            var timer = this.firstTimerInRange(tickFrom, tickTo);
+
+            var firstException;
+            while (timer && tickFrom <= tickTo) {
+                if (this.timeouts[timer.id]) {
+                    tickFrom = this.now = timer.callAt;
+                    try {
+                      this.callTimer(timer);
+                    } catch (e) {
+                      firstException = firstException || e;
+                    }
+                }
+
+                timer = this.firstTimerInRange(previous, tickTo);
+                previous = tickFrom;
+            }
+
+            this.now = tickTo;
+
+            if (firstException) {
+              throw firstException;
+            }
+
+            return this.now;
+        },
+
+        firstTimerInRange: function (from, to) {
+            var timer, smallest = null, originalTimer;
+
+            for (var id in this.timeouts) {
+                if (this.timeouts.hasOwnProperty(id)) {
+                    if (this.timeouts[id].callAt < from || this.timeouts[id].callAt > to) {
+                        continue;
+                    }
+
+                    if (smallest === null || this.timeouts[id].callAt < smallest) {
+                        originalTimer = this.timeouts[id];
+                        smallest = this.timeouts[id].callAt;
+
+                        timer = {
+                            func: this.timeouts[id].func,
+                            callAt: this.timeouts[id].callAt,
+                            interval: this.timeouts[id].interval,
+                            id: this.timeouts[id].id,
+                            invokeArgs: this.timeouts[id].invokeArgs
+                        };
+                    }
+                }
+            }
+
+            return timer || null;
+        },
+
+        callTimer: function (timer) {
+            if (typeof timer.interval == "number") {
+                this.timeouts[timer.id].callAt += timer.interval;
+            } else {
+                delete this.timeouts[timer.id];
+            }
+
+            try {
+                if (typeof timer.func == "function") {
+                    timer.func.apply(null, timer.invokeArgs);
+                } else {
+                    eval(timer.func);
+                }
+            } catch (e) {
+              var exception = e;
+            }
+
+            if (!this.timeouts[timer.id]) {
+                if (exception) {
+                  throw exception;
+                }
+                return;
+            }
+
+            if (exception) {
+              throw exception;
+            }
+        },
+
+        reset: function reset() {
+            this.timeouts = {};
+        },
+
+        Date: (function () {
+            var NativeDate = Date;
+
+            function ClockDate(year, month, date, hour, minute, second, ms) {
+                // Defensive and verbose to avoid potential harm in passing
+                // explicit undefined when user does not pass argument
+                switch (arguments.length) {
+                case 0:
+                    return new NativeDate(ClockDate.clock.now);
+                case 1:
+                    return new NativeDate(year);
+                case 2:
+                    return new NativeDate(year, month);
+                case 3:
+                    return new NativeDate(year, month, date);
+                case 4:
+                    return new NativeDate(year, month, date, hour);
+                case 5:
+                    return new NativeDate(year, month, date, hour, minute);
+                case 6:
+                    return new NativeDate(year, month, date, hour, minute, second);
+                default:
+                    return new NativeDate(year, month, date, hour, minute, second, ms);
+                }
+            }
+
+            return mirrorDateProperties(ClockDate, NativeDate);
+        }())
+    };
+
+    function mirrorDateProperties(target, source) {
+        if (source.now) {
+            target.now = function now() {
+                return target.clock.now;
+            };
+        } else {
+            delete target.now;
+        }
+
+        if (source.toSource) {
+            target.toSource = function toSource() {
+                return source.toSource();
+            };
+        } else {
+            delete target.toSource;
+        }
+
+        target.toString = function toString() {
+            return source.toString();
+        };
+
+        target.prototype = source.prototype;
+        target.parse = source.parse;
+        target.UTC = source.UTC;
+        target.prototype.toUTCString = source.prototype.toUTCString;
+
+        for (var prop in source) {
+            if (source.hasOwnProperty(prop)) {
+                target[prop] = source[prop];
+            }
+        }
+
+        return target;
+    }
+
+    var methods = ["Date", "setTimeout", "setInterval",
+                   "clearTimeout", "clearInterval"];
+
+    if (typeof global.setImmediate !== "undefined") {
+        methods.push("setImmediate");
+    }
+
+    if (typeof global.clearImmediate !== "undefined") {
+        methods.push("clearImmediate");
+    }
+
+    function restore() {
+        var method;
+
+        for (var i = 0, l = this.methods.length; i < l; i++) {
+            method = this.methods[i];
+
+            if (global[method].hadOwnProperty) {
+                global[method] = this["_" + method];
+            } else {
+                try {
+                    delete global[method];
+                } catch (e) {}
+            }
+        }
+
+        // Prevent multiple executions which will completely remove these props
+        this.methods = [];
+    }
+
+    function stubGlobal(method, clock) {
+        clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call(global, method);
+        clock["_" + method] = global[method];
+
+        if (method == "Date") {
+            var date = mirrorDateProperties(clock[method], global[method]);
+            global[method] = date;
+        } else {
+            global[method] = function () {
+                return clock[method].apply(clock, arguments);
+            };
+
+            for (var prop in clock[method]) {
+                if (clock[method].hasOwnProperty(prop)) {
+                    global[method][prop] = clock[method][prop];
+                }
+            }
+        }
+
+        global[method].clock = clock;
+    }
+
+    sinon.useFakeTimers = function useFakeTimers(now) {
+        var clock = sinon.clock.create(now);
+        clock.restore = restore;
+        clock.methods = Array.prototype.slice.call(arguments,
+                                                   typeof now == "number" ? 1 : 0);
+
+        if (clock.methods.length === 0) {
+            clock.methods = methods;
+        }
+
+        for (var i = 0, l = clock.methods.length; i < l; i++) {
+            stubGlobal(clock.methods[i], clock);
+        }
+
+        return clock;
+    };
+}(typeof global != "undefined" && typeof global !== "function" ? global : this));
+
+sinon.timers = {
+    setTimeout: setTimeout,
+    clearTimeout: clearTimeout,
+    setImmediate: (typeof setImmediate !== "undefined" ? setImmediate : undefined),
+    clearImmediate: (typeof clearImmediate !== "undefined" ? clearImmediate: undefined),
+    setInterval: setInterval,
+    clearInterval: clearInterval,
+    Date: Date
+};
+
+if (typeof module !== 'undefined' && module.exports) {
+    module.exports = sinon;
+}
+
+/*jslint eqeqeq: false, onevar: false*/
+/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/
+/**
+ * Minimal Event interface implementation
+ *
+ * Original implementation by Sven Fuchs: https://gist.github.com/995028
+ * Modifications and tests by Christian Johansen.
+ *
+ * @author Sven Fuchs (svenfuchs@artweb-design.de)
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2011 Sven Fuchs, Christian Johansen
+ */
+
+if (typeof sinon == "undefined") {
+    this.sinon = {};
+}
+
+(function () {
+    var push = [].push;
+
+    sinon.Event = function Event(type, bubbles, cancelable, target) {
+        this.initEvent(type, bubbles, cancelable, target);
+    };
+
+    sinon.Event.prototype = {
+        initEvent: function(type, bubbles, cancelable, target) {
+            this.type = type;
+            this.bubbles = bubbles;
+            this.cancelable = cancelable;
+            this.target = target;
+        },
+
+        stopPropagation: function () {},
+
+        preventDefault: function () {
+            this.defaultPrevented = true;
+        }
+    };
+
+    sinon.EventTarget = {
+        addEventListener: function addEventListener(event, listener) {
+            this.eventListeners = this.eventListeners || {};
+            this.eventListeners[event] = this.eventListeners[event] || [];
+            push.call(this.eventListeners[event], listener);
+        },
+
+        removeEventListener: function removeEventListener(event, listener) {
+            var listeners = this.eventListeners && this.eventListeners[event] || [];
+
+            for (var i = 0, l = listeners.length; i < l; ++i) {
+                if (listeners[i] == listener) {
+                    return listeners.splice(i, 1);
+                }
+            }
+        },
+
+        dispatchEvent: function dispatchEvent(event) {
+            var type = event.type;
+            var listeners = this.eventListeners && this.eventListeners[type] || [];
+
+            for (var i = 0; i < listeners.length; i++) {
+                if (typeof listeners[i] == "function") {
+                    listeners[i].call(this, event);
+                } else {
+                    listeners[i].handleEvent(event);
+                }
+            }
+
+            return !!event.defaultPrevented;
+        }
+    };
+}());
+
+/**
+ * @depend ../../sinon.js
+ * @depend event.js
+ */
+/*jslint eqeqeq: false, onevar: false*/
+/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/
+/**
+ * Fake XMLHttpRequest object
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+// wrapper for global
+(function(global) {
+
+    if (typeof sinon === "undefined") {
+        global.sinon = {};
+    }
+    sinon.xhr = { XMLHttpRequest: global.XMLHttpRequest };
+
+    var xhr = sinon.xhr;
+    xhr.GlobalXMLHttpRequest = global.XMLHttpRequest;
+    xhr.GlobalActiveXObject = global.ActiveXObject;
+    xhr.supportsActiveX = typeof xhr.GlobalActiveXObject != "undefined";
+    xhr.supportsXHR = typeof xhr.GlobalXMLHttpRequest != "undefined";
+    xhr.workingXHR = xhr.supportsXHR ? xhr.GlobalXMLHttpRequest : xhr.supportsActiveX
+                                     ? function() { return new xhr.GlobalActiveXObject("MSXML2.XMLHTTP.3.0") } : false;
+    xhr.supportsCORS = 'withCredentials' in (new sinon.xhr.GlobalXMLHttpRequest());
+
+    /*jsl:ignore*/
+    var unsafeHeaders = {
+        "Accept-Charset": true,
+        "Accept-Encoding": true,
+        "Connection": true,
+        "Content-Length": true,
+        "Cookie": true,
+        "Cookie2": true,
+        "Content-Transfer-Encoding": true,
+        "Date": true,
+        "Expect": true,
+        "Host": true,
+        "Keep-Alive": true,
+        "Referer": true,
+        "TE": true,
+        "Trailer": true,
+        "Transfer-Encoding": true,
+        "Upgrade": true,
+        "User-Agent": true,
+        "Via": true
+    };
+    /*jsl:end*/
+
+    function FakeXMLHttpRequest() {
+        this.readyState = FakeXMLHttpRequest.UNSENT;
+        this.requestHeaders = {};
+        this.requestBody = null;
+        this.status = 0;
+        this.statusText = "";
+        this.upload = new UploadProgress();
+        if (sinon.xhr.supportsCORS) {
+            this.withCredentials = false;
+        }
+
+
+        var xhr = this;
+        var events = ["loadstart", "load", "abort", "loadend"];
+
+        function addEventListener(eventName) {
+            xhr.addEventListener(eventName, function (event) {
+                var listener = xhr["on" + eventName];
+
+                if (listener && typeof listener == "function") {
+                    listener(event);
+                }
+            });
+        }
+
+        for (var i = events.length - 1; i >= 0; i--) {
+            addEventListener(events[i]);
+        }
+
+        if (typeof FakeXMLHttpRequest.onCreate == "function") {
+            FakeXMLHttpRequest.onCreate(this);
+        }
+    }
+
+    // An upload object is created for each
+    // FakeXMLHttpRequest and allows upload
+    // events to be simulated using uploadProgress
+    // and uploadError.
+    function UploadProgress() {
+        this.eventListeners = {
+            "progress": [],
+            "load": [],
+            "abort": [],
+            "error": []
+        }
+    }
+
+    UploadProgress.prototype.addEventListener = function(event, listener) {
+        this.eventListeners[event].push(listener);
+    };
+
+    UploadProgress.prototype.removeEventListener = function(event, listener) {
+        var listeners = this.eventListeners[event] || [];
+
+        for (var i = 0, l = listeners.length; i < l; ++i) {
+            if (listeners[i] == listener) {
+                return listeners.splice(i, 1);
+            }
+        }
+    };
+
+    UploadProgress.prototype.dispatchEvent = function(event) {
+        var listeners = this.eventListeners[event.type] || [];
+
+        for (var i = 0, listener; (listener = listeners[i]) != null; i++) {
+            listener(event);
+        }
+    };
+
+    function verifyState(xhr) {
+        if (xhr.readyState !== FakeXMLHttpRequest.OPENED) {
+            throw new Error("INVALID_STATE_ERR");
+        }
+
+        if (xhr.sendFlag) {
+            throw new Error("INVALID_STATE_ERR");
+        }
+    }
+
+    // filtering to enable a white-list version of Sinon FakeXhr,
+    // where whitelisted requests are passed through to real XHR
+    function each(collection, callback) {
+        if (!collection) return;
+        for (var i = 0, l = collection.length; i < l; i += 1) {
+            callback(collection[i]);
+        }
+    }
+    function some(collection, callback) {
+        for (var index = 0; index < collection.length; index++) {
+            if(callback(collection[index]) === true) return true;
+        }
+        return false;
+    }
+    // largest arity in XHR is 5 - XHR#open
+    var apply = function(obj,method,args) {
+        switch(args.length) {
+        case 0: return obj[method]();
+        case 1: return obj[method](args[0]);
+        case 2: return obj[method](args[0],args[1]);
+        case 3: return obj[method](args[0],args[1],args[2]);
+        case 4: return obj[method](args[0],args[1],args[2],args[3]);
+        case 5: return obj[method](args[0],args[1],args[2],args[3],args[4]);
+        }
+    };
+
+    FakeXMLHttpRequest.filters = [];
+    FakeXMLHttpRequest.addFilter = function(fn) {
+        this.filters.push(fn)
+    };
+    var IE6Re = /MSIE 6/;
+    FakeXMLHttpRequest.defake = function(fakeXhr,xhrArgs) {
+        var xhr = new sinon.xhr.workingXHR();
+        each(["open","setRequestHeader","send","abort","getResponseHeader",
+              "getAllResponseHeaders","addEventListener","overrideMimeType","removeEventListener"],
+             function(method) {
+                 fakeXhr[method] = function() {
+                   return apply(xhr,method,arguments);
+                 };
+             });
+
+        var copyAttrs = function(args) {
+            each(args, function(attr) {
+              try {
+                fakeXhr[attr] = xhr[attr]
+              } catch(e) {
+                if(!IE6Re.test(navigator.userAgent)) throw e;
+              }
+            });
+        };
+
+        var stateChange = function() {
+            fakeXhr.readyState = xhr.readyState;
+            if(xhr.readyState >= FakeXMLHttpRequest.HEADERS_RECEIVED) {
+                copyAttrs(["status","statusText"]);
+            }
+            if(xhr.readyState >= FakeXMLHttpRequest.LOADING) {
+                copyAttrs(["responseText"]);
+            }
+            if(xhr.readyState === FakeXMLHttpRequest.DONE) {
+                copyAttrs(["responseXML"]);
+            }
+            if(fakeXhr.onreadystatechange) fakeXhr.onreadystatechange.call(fakeXhr, { target: fakeXhr });
+        };
+        if(xhr.addEventListener) {
+          for(var event in fakeXhr.eventListeners) {
+              if(fakeXhr.eventListeners.hasOwnProperty(event)) {
+                  each(fakeXhr.eventListeners[event],function(handler) {
+                      xhr.addEventListener(event, handler);
+                  });
+              }
+          }
+          xhr.addEventListener("readystatechange",stateChange);
+        } else {
+          xhr.onreadystatechange = stateChange;
+        }
+        apply(xhr,"open",xhrArgs);
+    };
+    FakeXMLHttpRequest.useFilters = false;
+
+    function verifyRequestSent(xhr) {
+        if (xhr.readyState == FakeXMLHttpRequest.DONE) {
+            throw new Error("Request done");
+        }
+    }
+
+    function verifyHeadersReceived(xhr) {
+        if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) {
+            throw new Error("No headers received");
+        }
+    }
+
+    function verifyResponseBodyType(body) {
+        if (typeof body != "string") {
+            var error = new Error("Attempted to respond to fake XMLHttpRequest with " +
+                                 body + ", which is not a string.");
+            error.name = "InvalidBodyException";
+            throw error;
+        }
+    }
+
+    sinon.extend(FakeXMLHttpRequest.prototype, sinon.EventTarget, {
+        async: true,
+
+        open: function open(method, url, async, username, password) {
+            this.method = method;
+            this.url = url;
+            this.async = typeof async == "boolean" ? async : true;
+            this.username = username;
+            this.password = password;
+            this.responseText = null;
+            this.responseXML = null;
+            this.requestHeaders = {};
+            this.sendFlag = false;
+            if(sinon.FakeXMLHttpRequest.useFilters === true) {
+                var xhrArgs = arguments;
+                var defake = some(FakeXMLHttpRequest.filters,function(filter) {
+                    return filter.apply(this,xhrArgs)
+                });
+                if (defake) {
+                  return sinon.FakeXMLHttpRequest.defake(this,arguments);
+                }
+            }
+            this.readyStateChange(FakeXMLHttpRequest.OPENED);
+        },
+
+        readyStateChange: function readyStateChange(state) {
+            this.readyState = state;
+
+            if (typeof this.onreadystatechange == "function") {
+                try {
+                    this.onreadystatechange();
+                } catch (e) {
+                    sinon.logError("Fake XHR onreadystatechange handler", e);
+                }
+            }
+
+            this.dispatchEvent(new sinon.Event("readystatechange"));
+
+            switch (this.readyState) {
+                case FakeXMLHttpRequest.DONE:
+                    this.dispatchEvent(new sinon.Event("load", false, false, this));
+                    this.dispatchEvent(new sinon.Event("loadend", false, false, this));
+                    this.upload.dispatchEvent(new sinon.Event("load", false, false, this));
+                    this.upload.dispatchEvent(new ProgressEvent("progress", {loaded: 100, total: 100}));
+                    break;
+            }
+        },
+
+        setRequestHeader: function setRequestHeader(header, value) {
+            verifyState(this);
+
+            if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) {
+                throw new Error("Refused to set unsafe header \"" + header + "\"");
+            }
+
+            if (this.requestHeaders[header]) {
+                this.requestHeaders[header] += "," + value;
+            } else {
+                this.requestHeaders[header] = value;
+            }
+        },
+
+        // Helps testing
+        setResponseHeaders: function setResponseHeaders(headers) {
+            this.responseHeaders = {};
+
+            for (var header in headers) {
+                if (headers.hasOwnProperty(header)) {
+                    this.responseHeaders[header] = headers[header];
+                }
+            }
+
+            if (this.async) {
+                this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED);
+            } else {
+                this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED;
+            }
+        },
+
+        // Currently treats ALL data as a DOMString (i.e. no Document)
+        send: function send(data) {
+            verifyState(this);
+
+            if (!/^(get|head)$/i.test(this.method)) {
+                if (this.requestHeaders["Content-Type"]) {
+                    var value = this.requestHeaders["Content-Type"].split(";");
+                    this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8";
+                } else {
+                    this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8";
+                }
+
+                this.requestBody = data;
+            }
+
+            this.errorFlag = false;
+            this.sendFlag = this.async;
+            this.readyStateChange(FakeXMLHttpRequest.OPENED);
+
+            if (typeof this.onSend == "function") {
+                this.onSend(this);
+            }
+
+            this.dispatchEvent(new sinon.Event("loadstart", false, false, this));
+        },
+
+        abort: function abort() {
+            this.aborted = true;
+            this.responseText = null;
+            this.errorFlag = true;
+            this.requestHeaders = {};
+
+            if (this.readyState > sinon.FakeXMLHttpRequest.UNSENT && this.sendFlag) {
+                this.readyStateChange(sinon.FakeXMLHttpRequest.DONE);
+                this.sendFlag = false;
+            }
+
+            this.readyState = sinon.FakeXMLHttpRequest.UNSENT;
+
+            this.dispatchEvent(new sinon.Event("abort", false, false, this));
+
+            this.upload.dispatchEvent(new sinon.Event("abort", false, false, this));
+
+            if (typeof this.onerror === "function") {
+                this.onerror();
+            }
+        },
+
+        getResponseHeader: function getResponseHeader(header) {
+            if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
+                return null;
+            }
+
+            if (/^Set-Cookie2?$/i.test(header)) {
+                return null;
+            }
+
+            header = header.toLowerCase();
+
+            for (var h in this.responseHeaders) {
+                if (h.toLowerCase() == header) {
+                    return this.responseHeaders[h];
+                }
+            }
+
+            return null;
+        },
+
+        getAllResponseHeaders: function getAllResponseHeaders() {
+            if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
+                return "";
+            }
+
+            var headers = "";
+
+            for (var header in this.responseHeaders) {
+                if (this.responseHeaders.hasOwnProperty(header) &&
+                    !/^Set-Cookie2?$/i.test(header)) {
+                    headers += header + ": " + this.responseHeaders[header] + "\r\n";
+                }
+            }
+
+            return headers;
+        },
+
+        setResponseBody: function setResponseBody(body) {
+            verifyRequestSent(this);
+            verifyHeadersReceived(this);
+            verifyResponseBodyType(body);
+
+            var chunkSize = this.chunkSize || 10;
+            var index = 0;
+            this.responseText = "";
+
+            do {
+                if (this.async) {
+                    this.readyStateChange(FakeXMLHttpRequest.LOADING);
+                }
+
+                this.responseText += body.substring(index, index + chunkSize);
+                index += chunkSize;
+            } while (index < body.length);
+
+            var type = this.getResponseHeader("Content-Type");
+
+            if (this.responseText &&
+                (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) {
+                try {
+                    this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText);
+                } catch (e) {
+                    // Unable to parse XML - no biggie
+                }
+            }
+
+            if (this.async) {
+                this.readyStateChange(FakeXMLHttpRequest.DONE);
+            } else {
+                this.readyState = FakeXMLHttpRequest.DONE;
+            }
+        },
+
+        respond: function respond(status, headers, body) {
+            this.setResponseHeaders(headers || {});
+            this.status = typeof status == "number" ? status : 200;
+            this.statusText = FakeXMLHttpRequest.statusCodes[this.status];
+            this.setResponseBody(body || "");
+        },
+
+        uploadProgress: function uploadProgress(progressEventRaw) {
+            this.upload.dispatchEvent(new ProgressEvent("progress", progressEventRaw));
+        },
+
+        uploadError: function uploadError(error) {
+            this.upload.dispatchEvent(new CustomEvent("error", {"detail": error}));
+        }
+    });
+
+    sinon.extend(FakeXMLHttpRequest, {
+        UNSENT: 0,
+        OPENED: 1,
+        HEADERS_RECEIVED: 2,
+        LOADING: 3,
+        DONE: 4
+    });
+
+    // Borrowed from JSpec
+    FakeXMLHttpRequest.parseXML = function parseXML(text) {
+        var xmlDoc;
+
+        if (typeof DOMParser != "undefined") {
+            var parser = new DOMParser();
+            xmlDoc = parser.parseFromString(text, "text/xml");
+        } else {
+            xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
+            xmlDoc.async = "false";
+            xmlDoc.loadXML(text);
+        }
+
+        return xmlDoc;
+    };
+
+    FakeXMLHttpRequest.statusCodes = {
+        100: "Continue",
+        101: "Switching Protocols",
+        200: "OK",
+        201: "Created",
+        202: "Accepted",
+        203: "Non-Authoritative Information",
+        204: "No Content",
+        205: "Reset Content",
+        206: "Partial Content",
+        300: "Multiple Choice",
+        301: "Moved Permanently",
+        302: "Found",
+        303: "See Other",
+        304: "Not Modified",
+        305: "Use Proxy",
+        307: "Temporary Redirect",
+        400: "Bad Request",
+        401: "Unauthorized",
+        402: "Payment Required",
+        403: "Forbidden",
+        404: "Not Found",
+        405: "Method Not Allowed",
+        406: "Not Acceptable",
+        407: "Proxy Authentication Required",
+        408: "Request Timeout",
+        409: "Conflict",
+        410: "Gone",
+        411: "Length Required",
+        412: "Precondition Failed",
+        413: "Request Entity Too Large",
+        414: "Request-URI Too Long",
+        415: "Unsupported Media Type",
+        416: "Requested Range Not Satisfiable",
+        417: "Expectation Failed",
+        422: "Unprocessable Entity",
+        500: "Internal Server Error",
+        501: "Not Implemented",
+        502: "Bad Gateway",
+        503: "Service Unavailable",
+        504: "Gateway Timeout",
+        505: "HTTP Version Not Supported"
+    };
+
+    sinon.useFakeXMLHttpRequest = function () {
+        sinon.FakeXMLHttpRequest.restore = function restore(keepOnCreate) {
+            if (xhr.supportsXHR) {
+                global.XMLHttpRequest = xhr.GlobalXMLHttpRequest;
+            }
+
+            if (xhr.supportsActiveX) {
+                global.ActiveXObject = xhr.GlobalActiveXObject;
+            }
+
+            delete sinon.FakeXMLHttpRequest.restore;
+
+            if (keepOnCreate !== true) {
+                delete sinon.FakeXMLHttpRequest.onCreate;
+            }
+        };
+        if (xhr.supportsXHR) {
+            global.XMLHttpRequest = sinon.FakeXMLHttpRequest;
+        }
+
+        if (xhr.supportsActiveX) {
+            global.ActiveXObject = function ActiveXObject(objId) {
+                if (objId == "Microsoft.XMLHTTP" || /^Msxml2\.XMLHTTP/i.test(objId)) {
+
+                    return new sinon.FakeXMLHttpRequest();
+                }
+
+                return new xhr.GlobalActiveXObject(objId);
+            };
+        }
+
+        return sinon.FakeXMLHttpRequest;
+    };
+
+    sinon.FakeXMLHttpRequest = FakeXMLHttpRequest;
+
+})(typeof global === "object" ? global : this);
+
+if (typeof module !== 'undefined' && module.exports) {
+    module.exports = sinon;
+}
+
+/**
+ * @depend fake_xml_http_request.js
+ */
+/*jslint eqeqeq: false, onevar: false, regexp: false, plusplus: false*/
+/*global module, require, window*/
+/**
+ * The Sinon "server" mimics a web server that receives requests from
+ * sinon.FakeXMLHttpRequest and provides an API to respond to those requests,
+ * both synchronously and asynchronously. To respond synchronuously, canned
+ * answers have to be provided upfront.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+if (typeof sinon == "undefined") {
+    var sinon = {};
+}
+
+sinon.fakeServer = (function () {
+    var push = [].push;
+    function F() {}
+
+    function create(proto) {
+        F.prototype = proto;
+        return new F();
+    }
+
+    function responseArray(handler) {
+        var response = handler;
+
+        if (Object.prototype.toString.call(handler) != "[object Array]") {
+            response = [200, {}, handler];
+        }
+
+        if (typeof response[2] != "string") {
+            throw new TypeError("Fake server response body should be string, but was " +
+                                typeof response[2]);
+        }
+
+        return response;
+    }
+
+    var wloc = typeof window !== "undefined" ? window.location : {};
+    var rCurrLoc = new RegExp("^" + wloc.protocol + "//" + wloc.host);
+
+    function matchOne(response, reqMethod, reqUrl) {
+        var rmeth = response.method;
+        var matchMethod = !rmeth || rmeth.toLowerCase() == reqMethod.toLowerCase();
+        var url = response.url;
+        var matchUrl = !url || url == reqUrl || (typeof url.test == "function" && url.test(reqUrl));
+
+        return matchMethod && matchUrl;
+    }
+
+    function match(response, request) {
+        var requestUrl = request.url;
+
+        if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) {
+            requestUrl = requestUrl.replace(rCurrLoc, "");
+        }
+
+        if (matchOne(response, this.getHTTPMethod(request), requestUrl)) {
+            if (typeof response.response == "function") {
+                var ru = response.url;
+                var args = [request].concat(ru && typeof ru.exec == "function" ? ru.exec(requestUrl).slice(1) : []);
+                return response.response.apply(response, args);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    function log(response, request) {
+        var str;
+
+        str =  "Request:\n"  + sinon.format(request)  + "\n\n";
+        str += "Response:\n" + sinon.format(response) + "\n\n";
+
+        sinon.log(str);
+    }
+
+    return {
+        create: function () {
+            var server = create(this);
+            this.xhr = sinon.useFakeXMLHttpRequest();
+            server.requests = [];
+
+            this.xhr.onCreate = function (xhrObj) {
+                server.addRequest(xhrObj);
+            };
+
+            return server;
+        },
+
+        addRequest: function addRequest(xhrObj) {
+            var server = this;
+            push.call(this.requests, xhrObj);
+
+            xhrObj.onSend = function () {
+                server.handleRequest(this);
+
+                if (server.autoRespond && !server.responding) {
+                    setTimeout(function () {
+                        server.responding = false;
+                        server.respond();
+                    }, server.autoRespondAfter || 10);
+
+                    server.responding = true;
+                }
+            };
+        },
+
+        getHTTPMethod: function getHTTPMethod(request) {
+            if (this.fakeHTTPMethods && /post/i.test(request.method)) {
+                var matches = (request.requestBody || "").match(/_method=([^\b;]+)/);
+                return !!matches ? matches[1] : request.method;
+            }
+
+            return request.method;
+        },
+
+        handleRequest: function handleRequest(xhr) {
+            if (xhr.async) {
+                if (!this.queue) {
+                    this.queue = [];
+                }
+
+                push.call(this.queue, xhr);
+            } else {
+                this.processRequest(xhr);
+            }
+        },
+
+        respondWith: function respondWith(method, url, body) {
+            if (arguments.length == 1 && typeof method != "function") {
+                this.response = responseArray(method);
+                return;
+            }
+
+            if (!this.responses) { this.responses = []; }
+
+            if (arguments.length == 1) {
+                body = method;
+                url = method = null;
+            }
+
+            if (arguments.length == 2) {
+                body = url;
+                url = method;
+                method = null;
+            }
+
+            push.call(this.responses, {
+                method: method,
+                url: url,
+                response: typeof body == "function" ? body : responseArray(body)
+            });
+        },
+
+        respond: function respond() {
+            if (arguments.length > 0) this.respondWith.apply(this, arguments);
+            var queue = this.queue || [];
+            var requests = queue.splice(0);
+            var request;
+
+            while(request = requests.shift()) {
+                this.processRequest(request);
+            }
+        },
+
+        processRequest: function processRequest(request) {
+            try {
+                if (request.aborted) {
+                    return;
+                }
+
+                var response = this.response || [404, {}, ""];
+
+                if (this.responses) {
+                    for (var l = this.responses.length, i = l - 1; i >= 0; i--) {
+                        if (match.call(this, this.responses[i], request)) {
+                            response = this.responses[i].response;
+                            break;
+                        }
+                    }
+                }
+
+                if (request.readyState != 4) {
+                    log(response, request);
+
+                    request.respond(response[0], response[1], response[2]);
+                }
+            } catch (e) {
+                sinon.logError("Fake server request processing", e);
+            }
+        },
+
+        restore: function restore() {
+            return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments);
+        }
+    };
+}());
+
+if (typeof module !== 'undefined' && module.exports) {
+    module.exports = sinon;
+}
+
+/**
+ * @depend fake_server.js
+ * @depend fake_timers.js
+ */
+/*jslint browser: true, eqeqeq: false, onevar: false*/
+/*global sinon*/
+/**
+ * Add-on for sinon.fakeServer that automatically handles a fake timer along with
+ * the FakeXMLHttpRequest. The direct inspiration for this add-on is jQuery
+ * 1.3.x, which does not use xhr object's onreadystatehandler at all - instead,
+ * it polls the object for completion with setInterval. Dispite the direct
+ * motivation, there is nothing jQuery-specific in this file, so it can be used
+ * in any environment where the ajax implementation depends on setInterval or
+ * setTimeout.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function () {
+    function Server() {}
+    Server.prototype = sinon.fakeServer;
+
+    sinon.fakeServerWithClock = new Server();
+
+    sinon.fakeServerWithClock.addRequest = function addRequest(xhr) {
+        if (xhr.async) {
+            if (typeof setTimeout.clock == "object") {
+                this.clock = setTimeout.clock;
+            } else {
+                this.clock = sinon.useFakeTimers();
+                this.resetClock = true;
+            }
+
+            if (!this.longestTimeout) {
+                var clockSetTimeout = this.clock.setTimeout;
+                var clockSetInterval = this.clock.setInterval;
+                var server = this;
+
+                this.clock.setTimeout = function (fn, timeout) {
+                    server.longestTimeout = Math.max(timeout, server.longestTimeout || 0);
+
+                    return clockSetTimeout.apply(this, arguments);
+                };
+
+                this.clock.setInterval = function (fn, timeout) {
+                    server.longestTimeout = Math.max(timeout, server.longestTimeout || 0);
+
+                    return clockSetInterval.apply(this, arguments);
+                };
+            }
+        }
+
+        return sinon.fakeServer.addRequest.call(this, xhr);
+    };
+
+    sinon.fakeServerWithClock.respond = function respond() {
+        var returnVal = sinon.fakeServer.respond.apply(this, arguments);
+
+        if (this.clock) {
+            this.clock.tick(this.longestTimeout || 0);
+            this.longestTimeout = 0;
+
+            if (this.resetClock) {
+                this.clock.restore();
+                this.resetClock = false;
+            }
+        }
+
+        return returnVal;
+    };
+
+    sinon.fakeServerWithClock.restore = function restore() {
+        if (this.clock) {
+            this.clock.restore();
+        }
+
+        return sinon.fakeServer.restore.apply(this, arguments);
+    };
+}());
+
+/**
+ * @depend ../sinon.js
+ * @depend collection.js
+ * @depend util/fake_timers.js
+ * @depend util/fake_server_with_clock.js
+ */
+/*jslint eqeqeq: false, onevar: false, plusplus: false*/
+/*global require, module*/
+/**
+ * Manages fake collections as well as fake utilities such as Sinon's
+ * timers and fake XHR implementation in one convenient object.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+if (typeof module !== 'undefined' && module.exports) {
+    var sinon = require("../sinon");
+    sinon.extend(sinon, require("./util/fake_timers"));
+}
+
+(function () {
+    var push = [].push;
+
+    function exposeValue(sandbox, config, key, value) {
+        if (!value) {
+            return;
+        }
+
+        if (config.injectInto && !(key in config.injectInto) ) {
+            config.injectInto[key] = value;
+        } else {
+            push.call(sandbox.args, value);
+        }
+    }
+
+    function prepareSandboxFromConfig(config) {
+        var sandbox = sinon.create(sinon.sandbox);
+
+        if (config.useFakeServer) {
+            if (typeof config.useFakeServer == "object") {
+                sandbox.serverPrototype = config.useFakeServer;
+            }
+
+            sandbox.useFakeServer();
+        }
+
+        if (config.useFakeTimers) {
+            if (typeof config.useFakeTimers == "object") {
+                sandbox.useFakeTimers.apply(sandbox, config.useFakeTimers);
+            } else {
+                sandbox.useFakeTimers();
+            }
+        }
+
+        return sandbox;
+    }
+
+    sinon.sandbox = sinon.extend(sinon.create(sinon.collection), {
+        useFakeTimers: function useFakeTimers() {
+            this.clock = sinon.useFakeTimers.apply(sinon, arguments);
+
+            return this.add(this.clock);
+        },
+
+        serverPrototype: sinon.fakeServer,
+
+        useFakeServer: function useFakeServer() {
+            var proto = this.serverPrototype || sinon.fakeServer;
+
+            if (!proto || !proto.create) {
+                return null;
+            }
+
+            this.server = proto.create();
+            return this.add(this.server);
+        },
+
+        inject: function (obj) {
+            sinon.collection.inject.call(this, obj);
+
+            if (this.clock) {
+                obj.clock = this.clock;
+            }
+
+            if (this.server) {
+                obj.server = this.server;
+                obj.requests = this.server.requests;
+            }
+
+            return obj;
+        },
+
+        create: function (config) {
+            if (!config) {
+                return sinon.create(sinon.sandbox);
+            }
+
+            var sandbox = prepareSandboxFromConfig(config);
+            sandbox.args = sandbox.args || [];
+            var prop, value, exposed = sandbox.inject({});
+
+            if (config.properties) {
+                for (var i = 0, l = config.properties.length; i < l; i++) {
+                    prop = config.properties[i];
+                    value = exposed[prop] || prop == "sandbox" && sandbox;
+                    exposeValue(sandbox, config, prop, value);
+                }
+            } else {
+                exposeValue(sandbox, config, "sandbox", value);
+            }
+
+            return sandbox;
+        }
+    });
+
+    sinon.sandbox.useFakeXMLHttpRequest = sinon.sandbox.useFakeServer;
+
+    if (typeof module !== 'undefined' && module.exports) {
+        module.exports = sinon.sandbox;
+    }
+}());
+
+/**
+ * @depend ../sinon.js
+ * @depend stub.js
+ * @depend mock.js
+ * @depend sandbox.js
+ */
+/*jslint eqeqeq: false, onevar: false, forin: true, plusplus: false*/
+/*global module, require, sinon*/
+/**
+ * Test function, sandboxes fakes
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== 'undefined' && module.exports;
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function test(callback) {
+        var type = typeof callback;
+
+        if (type != "function") {
+            throw new TypeError("sinon.test needs to wrap a test function, got " + type);
+        }
+
+        return function () {
+            var config = sinon.getConfig(sinon.config);
+            config.injectInto = config.injectIntoThis && this || config.injectInto;
+            var sandbox = sinon.sandbox.create(config);
+            var exception, result;
+            var args = Array.prototype.slice.call(arguments).concat(sandbox.args);
+
+            try {
+                result = callback.apply(this, args);
+            } catch (e) {
+                exception = e;
+            }
+
+            if (typeof exception !== "undefined") {
+                sandbox.restore();
+                throw exception;
+            }
+            else {
+                sandbox.verifyAndRestore();
+            }
+
+            return result;
+        };
+    }
+
+    test.config = {
+        injectIntoThis: true,
+        injectInto: null,
+        properties: ["spy", "stub", "mock", "clock", "server", "requests"],
+        useFakeTimers: true,
+        useFakeServer: true
+    };
+
+    if (commonJSModule) {
+        module.exports = test;
+    } else {
+        sinon.test = test;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend ../sinon.js
+ * @depend test.js
+ */
+/*jslint eqeqeq: false, onevar: false, eqeqeq: false*/
+/*global module, require, sinon*/
+/**
+ * Test case, sandboxes all test functions
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var commonJSModule = typeof module !== 'undefined' && module.exports;
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon || !Object.prototype.hasOwnProperty) {
+        return;
+    }
+
+    function createTest(property, setUp, tearDown) {
+        return function () {
+            if (setUp) {
+                setUp.apply(this, arguments);
+            }
+
+            var exception, result;
+
+            try {
+                result = property.apply(this, arguments);
+            } catch (e) {
+                exception = e;
+            }
+
+            if (tearDown) {
+                tearDown.apply(this, arguments);
+            }
+
+            if (exception) {
+                throw exception;
+            }
+
+            return result;
+        };
+    }
+
+    function testCase(tests, prefix) {
+        /*jsl:ignore*/
+        if (!tests || typeof tests != "object") {
+            throw new TypeError("sinon.testCase needs an object with test functions");
+        }
+        /*jsl:end*/
+
+        prefix = prefix || "test";
+        var rPrefix = new RegExp("^" + prefix);
+        var methods = {}, testName, property, method;
+        var setUp = tests.setUp;
+        var tearDown = tests.tearDown;
+
+        for (testName in tests) {
+            if (tests.hasOwnProperty(testName)) {
+                property = tests[testName];
+
+                if (/^(setUp|tearDown)$/.test(testName)) {
+                    continue;
+                }
+
+                if (typeof property == "function" && rPrefix.test(testName)) {
+                    method = property;
+
+                    if (setUp || tearDown) {
+                        method = createTest(property, setUp, tearDown);
+                    }
+
+                    methods[testName] = sinon.test(method);
+                } else {
+                    methods[testName] = tests[testName];
+                }
+            }
+        }
+
+        return methods;
+    }
+
+    if (commonJSModule) {
+        module.exports = testCase;
+    } else {
+        sinon.testCase = testCase;
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend ../sinon.js
+ * @depend stub.js
+ */
+/*jslint eqeqeq: false, onevar: false, nomen: false, plusplus: false*/
+/*global module, require, sinon*/
+/**
+ * Assertions matching the test spy retrieval interface.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon, global) {
+    var commonJSModule = typeof module !== "undefined" && module.exports;
+    var slice = Array.prototype.slice;
+    var assert;
+
+    if (!sinon && commonJSModule) {
+        sinon = require("../sinon");
+    }
+
+    if (!sinon) {
+        return;
+    }
+
+    function verifyIsStub() {
+        var method;
+
+        for (var i = 0, l = arguments.length; i < l; ++i) {
+            method = arguments[i];
+
+            if (!method) {
+                assert.fail("fake is not a spy");
+            }
+
+            if (typeof method != "function") {
+                assert.fail(method + " is not a function");
+            }
+
+            if (typeof method.getCall != "function") {
+                assert.fail(method + " is not stubbed");
+            }
+        }
+    }
+
+    function failAssertion(object, msg) {
+        object = object || global;
+        var failMethod = object.fail || assert.fail;
+        failMethod.call(object, msg);
+    }
+
+    function mirrorPropAsAssertion(name, method, message) {
+        if (arguments.length == 2) {
+            message = method;
+            method = name;
+        }
+
+        assert[name] = function (fake) {
+            verifyIsStub(fake);
+
+            var args = slice.call(arguments, 1);
+            var failed = false;
+
+            if (typeof method == "function") {
+                failed = !method(fake);
+            } else {
+                failed = typeof fake[method] == "function" ?
+                    !fake[method].apply(fake, args) : !fake[method];
+            }
+
+            if (failed) {
+                failAssertion(this, fake.printf.apply(fake, [message].concat(args)));
+            } else {
+                assert.pass(name);
+            }
+        };
+    }
+
+    function exposedName(prefix, prop) {
+        return !prefix || /^fail/.test(prop) ? prop :
+            prefix + prop.slice(0, 1).toUpperCase() + prop.slice(1);
+    }
+
+    assert = {
+        failException: "AssertError",
+
+        fail: function fail(message) {
+            var error = new Error(message);
+            error.name = this.failException || assert.failException;
+
+            throw error;
+        },
+
+        pass: function pass(assertion) {},
+
+        callOrder: function assertCallOrder() {
+            verifyIsStub.apply(null, arguments);
+            var expected = "", actual = "";
+
+            if (!sinon.calledInOrder(arguments)) {
+                try {
+                    expected = [].join.call(arguments, ", ");
+                    var calls = slice.call(arguments);
+                    var i = calls.length;
+                    while (i) {
+                        if (!calls[--i].called) {
+                            calls.splice(i, 1);
+                        }
+                    }
+                    actual = sinon.orderByFirstCall(calls).join(", ");
+                } catch (e) {
+                    // If this fails, we'll just fall back to the blank string
+                }
+
+                failAssertion(this, "expected " + expected + " to be " +
+                              "called in order but were called as " + actual);
+            } else {
+                assert.pass("callOrder");
+            }
+        },
+
+        callCount: function assertCallCount(method, count) {
+            verifyIsStub(method);
+
+            if (method.callCount != count) {
+                var msg = "expected %n to be called " + sinon.timesInWords(count) +
+                    " but was called %c%C";
+                failAssertion(this, method.printf(msg));
+            } else {
+                assert.pass("callCount");
+            }
+        },
+
+        expose: function expose(target, options) {
+            if (!target) {
+                throw new TypeError("target is null or undefined");
+            }
+
+            var o = options || {};
+            var prefix = typeof o.prefix == "undefined" && "assert" || o.prefix;
+            var includeFail = typeof o.includeFail == "undefined" || !!o.includeFail;
+
+            for (var method in this) {
+                if (method != "export" && (includeFail || !/^(fail)/.test(method))) {
+                    target[exposedName(prefix, method)] = this[method];
+                }
+            }
+
+            return target;
+        }
+    };
+
+    mirrorPropAsAssertion("called", "expected %n to have been called at least once but was never called");
+    mirrorPropAsAssertion("notCalled", function (spy) { return !spy.called; },
+                          "expected %n to not have been called but was called %c%C");
+    mirrorPropAsAssertion("calledOnce", "expected %n to be called once but was called %c%C");
+    mirrorPropAsAssertion("calledTwice", "expected %n to be called twice but was called %c%C");
+    mirrorPropAsAssertion("calledThrice", "expected %n to be called thrice but was called %c%C");
+    mirrorPropAsAssertion("calledOn", "expected %n to be called with %1 as this but was called with %t");
+    mirrorPropAsAssertion("alwaysCalledOn", "expected %n to always be called with %1 as this but was called with %t");
+    mirrorPropAsAssertion("calledWithNew", "expected %n to be called with new");
+    mirrorPropAsAssertion("alwaysCalledWithNew", "expected %n to always be called with new");
+    mirrorPropAsAssertion("calledWith", "expected %n to be called with arguments %*%C");
+    mirrorPropAsAssertion("calledWithMatch", "expected %n to be called with match %*%C");
+    mirrorPropAsAssertion("alwaysCalledWith", "expected %n to always be called with arguments %*%C");
+    mirrorPropAsAssertion("alwaysCalledWithMatch", "expected %n to always be called with match %*%C");
+    mirrorPropAsAssertion("calledWithExactly", "expected %n to be called with exact arguments %*%C");
+    mirrorPropAsAssertion("alwaysCalledWithExactly", "expected %n to always be called with exact arguments %*%C");
+    mirrorPropAsAssertion("neverCalledWith", "expected %n to never be called with arguments %*%C");
+    mirrorPropAsAssertion("neverCalledWithMatch", "expected %n to never be called with match %*%C");
+    mirrorPropAsAssertion("threw", "%n did not throw exception%C");
+    mirrorPropAsAssertion("alwaysThrew", "%n did not always throw exception%C");
+
+    if (commonJSModule) {
+        module.exports = assert;
+    } else {
+        sinon.assert = assert;
+    }
+}(typeof sinon == "object" && sinon || null, typeof window != "undefined" ? window : (typeof self != "undefined") ? self : global));
+
+return sinon;}.call(typeof window != 'undefined' && window || {}));
diff --git a/resources/sinonjs/sinon-ie-1.8.1.js b/resources/sinonjs/sinon-ie-1.8.1.js
new file mode 100644 (file)
index 0000000..f92e9db
--- /dev/null
@@ -0,0 +1,86 @@
+/**
+ * Sinon.JS 1.8.1, 2014/02/02
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
+ *
+ * (The BSD License)
+ * 
+ * Copyright (c) 2010-2013, Christian Johansen, christian@cjohansen.no
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ *     * Redistributions of source code must retain the above copyright notice,
+ *       this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright notice,
+ *       this list of conditions and the following disclaimer in the documentation
+ *       and/or other materials provided with the distribution.
+ *     * Neither the name of Christian Johansen nor the names of his contributors
+ *       may be used to endorse or promote products derived from this software
+ *       without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*global sinon, setTimeout, setInterval, clearTimeout, clearInterval, Date*/
+/**
+ * Helps IE run the fake timers. By defining global functions, IE allows
+ * them to be overwritten at a later point. If these are not defined like
+ * this, overwriting them will result in anything from an exception to browser
+ * crash.
+ *
+ * If you don't require fake timers to work in IE, don't include this file.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+function setTimeout() {}
+function clearTimeout() {}
+function setImmediate() {}
+function clearImmediate() {}
+function setInterval() {}
+function clearInterval() {}
+function Date() {}
+
+// Reassign the original functions. Now their writable attribute
+// should be true. Hackish, I know, but it works.
+setTimeout = sinon.timers.setTimeout;
+clearTimeout = sinon.timers.clearTimeout;
+setImmediate = sinon.timers.setImmediate;
+clearImmediate = sinon.timers.clearImmediate;
+setInterval = sinon.timers.setInterval;
+clearInterval = sinon.timers.clearInterval;
+Date = sinon.timers.Date;
+
+/*global sinon*/
+/**
+ * Helps IE run the fake XMLHttpRequest. By defining global functions, IE allows
+ * them to be overwritten at a later point. If these are not defined like
+ * this, overwriting them will result in anything from an exception to browser
+ * crash.
+ *
+ * If you don't require fake XHR to work in IE, don't include this file.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+function XMLHttpRequest() {}
+
+// Reassign the original function. Now its writable attribute
+// should be true. Hackish, I know, but it works.
+XMLHttpRequest = sinon.xhr.XMLHttpRequest || undefined;
index 43e77cd..596dff5 100644 (file)
@@ -127,6 +127,11 @@ div.vectorMenu {
        background-position: 100% 60%;
        background-repeat: no-repeat;
        cursor: pointer;
+       .transition(background-position 250ms);
+}
+
+div.vectorMenu.menuForceShow {
+       background-position: 100% 100%;
 }
 
 div.vectorMenuFocus {
@@ -212,7 +217,7 @@ x:-moz-any-link {
 
 /* Enable forcing showing of the menu for accessibility */
 div.vectorMenu:hover div.menu,
-div.vectorMenu div.menuForceShow {
+div.vectorMenu.menuForceShow div.menu {
        display: block;
 }
 
index aa64624..8420431 100644 (file)
@@ -9,7 +9,7 @@ jQuery( function ( $ ) {
                        // For accessibility, show the menu when the h3 is clicked (bug 24298/46486)
                        .on( 'click keypress', function ( e ) {
                                if( e.type === 'click' || e.which === 13 ) {
-                                       $el.find( '.menu:first' ).toggleClass( 'menuForceShow' );
+                                       $el.toggleClass( 'menuForceShow' );
                                        e.preventDefault();
                                }
                        } )
index bd240eb..63f610f 100644 (file)
@@ -6,6 +6,14 @@ return array(
 
        /* Utilities */
 
+       'test.sinonjs' => array(
+               'scripts' => array(
+                       'resources/sinonjs/sinon-1.8.1.js',
+                       'resources/sinonjs/sinon-ie-1.8.1.js',
+               ),
+               'targets' => array( 'desktop', 'mobile' ),
+       ),
+
        'test.mediawiki.qunit.testrunner' => array(
                'scripts' => array(
                        'tests/qunit/data/testrunner.js',
@@ -16,6 +24,7 @@ return array(
                        'jquery.qunit.completenessTest',
                        'mediawiki.page.ready',
                        'mediawiki.page.startup',
+                       'test.sinonjs',
                ),
                'position' => 'top',
                'targets' => array( 'desktop', 'mobile' ),
index 50e6014..6930f38 100644 (file)
@@ -1,4 +1,4 @@
-/*global CompletenessTest */
+/*global CompletenessTest, sinon */
 /*jshint evil: true */
 ( function ( $, mw, QUnit, undefined ) {
        'use strict';
                tooltip: 'Run the completeness test'
        } );
 
+       /**
+        * SinonJS
+        *
+        * Glue code for nicer integration with QUnit setup/teardown
+        * Inspired by http://sinonjs.org/releases/sinon-qunit-1.0.0.js
+        * Fixes:
+        * - Work properly with asynchronous QUnit by using module setup/teardown
+        *   instead of synchronously wrapping QUnit.test.
+        */
+       sinon.assert.fail = function ( msg ) {
+               QUnit.assert.ok( false, msg );
+       };
+       sinon.assert.pass = function ( msg ) {
+               QUnit.assert.ok( true, msg );
+       };
+       sinon.config = {
+               injectIntoThis: true,
+               injectInto: null,
+               properties: ['spy', 'stub', 'mock', 'clock', 'sandbox'],
+               // Don't fake timers by default
+               useFakeTimers: false,
+               useFakeServer: false
+       };
+       ( function () {
+               var orgModule = QUnit.module;
+
+               QUnit.module = function ( name, localEnv ) {
+                       localEnv = localEnv || {};
+                       orgModule( name, {
+                               setup: function () {
+                                       var config = sinon.getConfig( sinon.config );
+                                       config.injectInto = this;
+                                       sinon.sandbox.create( config );
+
+                                       if ( localEnv.setup ) {
+                                               localEnv.setup.call( this );
+                                       }
+                               },
+                               teardown: function () {
+                                       this.sandbox.verifyAndRestore();
+
+                                       if ( localEnv.teardown ) {
+                                               localEnv.teardown.call( this );
+                                       }
+                               }
+                       } );
+               };
+       }() );
+
        // Initiate when enabled
        if ( QUnit.urlParams.completenesstest ) {
 
index 9eda75c..c903193 100644 (file)
@@ -1,61 +1,84 @@
 ( function ( mw ) {
-       QUnit.module( 'mediawiki.api', QUnit.newMwEnvironment() );
+       QUnit.module( 'mediawiki.api', QUnit.newMwEnvironment( {
+               setup: function () {
+                       this.clock = this.sandbox.useFakeTimers();
+                       this.server = this.sandbox.useFakeServer();
+               },
+               teardown: function () {
+                       this.clock.tick( 1 );
+               }
+       }) );
 
-       QUnit.asyncTest( 'Basic functionality', function ( assert ) {
-               var api, d1, d2, d3;
-               QUnit.expect( 3 );
+       QUnit.test( 'Basic functionality', function ( assert ) {
+               QUnit.expect( 2 );
 
-               api = new mw.Api();
+               var api = new mw.Api();
 
-               d1 = api.get( {} )
+               api.get( {} )
                        .done( function ( data ) {
                                assert.deepEqual( data, [], 'If request succeeds without errors, resolve deferred' );
                        } );
 
-               d2 = api.get( {
-                       action: 'doesntexist'
-               } )
-                       .fail( function ( errorCode ) {
-                               assert.equal( errorCode, 'unknown_action', 'API error (e.g. "unknown_action") should reject the deferred' );
-                       } );
-
-               d3 = api.post( {} )
+               api.post( {} )
                        .done( function ( data ) {
                                assert.deepEqual( data, [], 'Simple POST request' );
                        } );
 
-               // After all are completed, continue the test suite.
-               QUnit.whenPromisesComplete( d1, d2, d3 ).always( function () {
-                       QUnit.start();
+               this.server.respond( function ( request ) {
+                       request.respond( 200, { 'Content-Type': 'application/json' }, '[]' );
                } );
        } );
 
-       QUnit.asyncTest( 'Deprecated callback methods', function ( assert ) {
-               var api, d1, d2, d3;
+
+       QUnit.test( 'API error', function ( assert ) {
+               QUnit.expect( 1 );
+
+               var api = new mw.Api();
+
+               api.get( { action: 'doesntexist' } )
+                       .fail( function ( errorCode ) {
+                               assert.equal( errorCode, 'unknown_action', 'API error should reject the deferred' );
+                       } );
+
+               this.server.respond( function ( request ) {
+                       request.respond( 200, { 'Content-Type': 'application/json' },
+                               '{ "error": { "code": "unknown_action" } }'
+                       );
+               } );
+       } );
+
+       QUnit.test( 'Deprecated callback methods', function ( assert ) {
                QUnit.expect( 3 );
 
-               api = new mw.Api();
+               var api = new mw.Api();
 
-               d1 = api.get( {}, function () {
+               api.get( {}, function () {
                        assert.ok( true, 'Function argument treated as success callback.' );
                } );
 
-               d2 = api.get( {}, {
+               api.get( {}, {
                        ok: function () {
                                assert.ok( true, '"ok" property treated as success callback.' );
                        }
                } );
 
-               d3 = api.get( {
-                       action: 'doesntexist'
-               }, {
+               api.get( { action: 'doesntexist' }, {
                        err: function () {
                                assert.ok( true, '"err" property treated as error callback.' );
                        }
                } );
 
-               QUnit.whenPromisesComplete( d1, d2, d3 ).always( function () {
-                       QUnit.start();
+               this.server.respondWith( /action=query/, function ( request ) {
+                       request.respond( 200, { 'Content-Type': 'application/json' }, '[]' );
                } );
+
+               this.server.respondWith( /action=doesntexist/, function ( request ) {
+                       request.respond( 200, { 'Content-Type': 'application/json' },
+                               '{ "error": { "code": "unknown_action" } }'
+                       );
+               } );
+
+               this.server.respond();
        } );
+
 }( mediaWiki ) );
index ad5239e..88aecbd 100644 (file)
--- a/thumb.php
+++ b/thumb.php
@@ -307,6 +307,9 @@ function wfStreamThumb( array $params ) {
        if ( $user->pingLimiter( 'renderfile' ) ) {
                wfThumbError( 500, wfMessage( 'actionthrottledtext' ) );
                return;
+       } elseif ( wfThumbIsAttemptThrottled( $img, $thumbName, 5 ) ) {
+               wfThumbError( 500, wfMessage( 'thumbnail_image-failure-limit', 5 ) );
+               return;
        }
 
        // Thumbnail isn't already there, so create the new thumbnail...
@@ -332,6 +335,7 @@ function wfStreamThumb( array $params ) {
        }
 
        if ( $errorMsg !== false ) {
+               wfThumbIncrAttemptFailures( $img, $thumbName );
                wfThumbError( 500, $errorMsg );
        } else {
                // Stream the file if there were no errors
@@ -339,6 +343,45 @@ function wfStreamThumb( array $params ) {
        }
 }
 
+/**
+ * @param File $img
+ * @param string $thumbName
+ * @param int $limit
+ * @return int|bool
+ */
+function wfThumbIsAttemptThrottled( File $img, $thumbName, $limit ) {
+       global $wgMemc;
+
+       return ( $wgMemc->get( wfThumbAttemptKey( $img, $thumbName ) ) >= $limit );
+}
+
+/**
+ * @param File $img
+ * @param string $thumbName
+ */
+function wfThumbIncrAttemptFailures( File $img, $thumbName ) {
+       global $wgMemc;
+
+       $key = wfThumbAttemptKey( $img, $thumbName );
+       if ( !$wgMemc->incr( $key, 1 ) ) {
+               if ( !$wgMemc->add( $key, 1, 3600 ) ) {
+                       $wgMemc->incr( $key, 1 );
+               }
+       }
+}
+
+/**
+ * @param File $img
+ * @param string $thumbName
+ * @return string
+ */
+function wfThumbAttemptKey( File $img, $thumbName ) {
+       global $wgAttemptFailureEpoch;
+
+       return wfMemcKey( 'attempt-failures', $wgAttemptFailureEpoch,
+               $img->getRepo()->getName(), md5( $img->getName() ), md5( $thumbName ) );
+}
+
 /**
  * Convert pathinfo type parameter, into normal request parameters
  *