Merge "Defer addAutopromoteOnceGroups to transaction idle"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 14 Jul 2014 23:21:28 +0000 (23:21 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 14 Jul 2014 23:21:28 +0000 (23:21 +0000)
193 files changed:
RELEASE-NOTES-1.24
docs/hooks.txt
includes/AutoLoader.php
includes/DefaultSettings.php
includes/EditPage.php
includes/Init.php [deleted file]
includes/PrefixSearch.php
includes/Setup.php
includes/Skin.php
includes/Title.php
includes/User.php
includes/UserMailer.php
includes/actions/RevertAction.php
includes/api/ApiBase.php
includes/api/ApiFormatBase.php
includes/api/ApiMain.php
includes/api/ApiQuery.php
includes/api/ApiQueryDeletedrevs.php
includes/api/ApiResult.php
includes/context/DerivativeContext.php
includes/context/RequestContext.php
includes/db/LoadBalancer.php
includes/installer/CliInstaller.php
includes/installer/Installer.php
includes/installer/LocalSettingsGenerator.php
includes/installer/WebInstaller.php
includes/installer/i18n/bto.json [new file with mode: 0644]
includes/installer/i18n/ca.json
includes/installer/i18n/cs.json
includes/installer/i18n/de.json
includes/installer/i18n/eo.json
includes/installer/i18n/es.json
includes/installer/i18n/fa.json
includes/installer/i18n/fr.json
includes/installer/i18n/gl.json
includes/installer/i18n/he.json
includes/installer/i18n/id.json
includes/installer/i18n/ko.json
includes/installer/i18n/mk.json
includes/installer/i18n/nb.json
includes/installer/i18n/pt-br.json
includes/installer/i18n/qqq.json
includes/installer/i18n/ru.json
includes/installer/i18n/sv.json
includes/installer/i18n/tr.json
includes/installer/i18n/zh-hans.json
includes/libs/CSSMin.php
includes/objectcache/MemcachedPeclBagOStuff.php
includes/objectcache/ObjectCache.php
includes/page/Article.php
includes/page/WikiFilePage.php
includes/page/WikiPage.php
includes/parser/CoreParserFunctions.php
includes/resourceloader/ResourceLoaderStartUpModule.php
includes/specials/SpecialUnblock.php
includes/specials/SpecialVersion.php
includes/specials/SpecialWantedfiles.php
includes/templates/Userlogin.php
languages/Language.php
languages/Names.php
languages/i18n/ar.json
languages/i18n/az.json
languages/i18n/bar.json
languages/i18n/bcl.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/bho.json
languages/i18n/bn.json
languages/i18n/bs.json
languages/i18n/ca.json
languages/i18n/ce.json
languages/i18n/cs.json
languages/i18n/cy.json
languages/i18n/da.json
languages/i18n/de.json
languages/i18n/diq.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/es.json
languages/i18n/et.json
languages/i18n/eu.json
languages/i18n/fa.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/gan-hans.json
languages/i18n/gd.json
languages/i18n/gl.json
languages/i18n/gsw.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/hu.json
languages/i18n/ia.json
languages/i18n/id.json
languages/i18n/ilo.json
languages/i18n/is.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/kiu.json
languages/i18n/kk-cyrl.json
languages/i18n/ko.json
languages/i18n/lb.json
languages/i18n/mk.json
languages/i18n/ml.json
languages/i18n/mt.json
languages/i18n/nb.json
languages/i18n/nds-nl.json
languages/i18n/nl.json
languages/i18n/oc.json
languages/i18n/pa.json
languages/i18n/pl.json
languages/i18n/pt-br.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/ro.json
languages/i18n/ru.json
languages/i18n/sl.json
languages/i18n/sq.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/sv.json
languages/i18n/tr.json
languages/i18n/tt-cyrl.json
languages/i18n/uk.json
languages/i18n/vi.json
languages/i18n/wuu.json
languages/i18n/yi.json
languages/i18n/yo.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
languages/messages/MessagesEn.php
maintenance/resources/update-oojs-ui.sh
resources/lib/es5-shim/es5-shim.js
resources/lib/jquery/jquery.cookie.js
resources/lib/oojs-ui/i18n/ca.json
resources/lib/oojs-ui/i18n/de.json
resources/lib/oojs-ui/i18n/en.json
resources/lib/oojs-ui/i18n/es.json
resources/lib/oojs-ui/i18n/fa.json
resources/lib/oojs-ui/i18n/fi.json
resources/lib/oojs-ui/i18n/fr.json
resources/lib/oojs-ui/i18n/gd.json [new file with mode: 0644]
resources/lib/oojs-ui/i18n/gl.json
resources/lib/oojs-ui/i18n/he.json
resources/lib/oojs-ui/i18n/hu.json
resources/lib/oojs-ui/i18n/ia.json
resources/lib/oojs-ui/i18n/lb.json
resources/lib/oojs-ui/i18n/mk.json
resources/lib/oojs-ui/i18n/pl.json
resources/lib/oojs-ui/i18n/pt.json
resources/lib/oojs-ui/i18n/qqq.json
resources/lib/oojs-ui/i18n/ro.json
resources/lib/oojs-ui/i18n/ru.json
resources/lib/oojs-ui/i18n/sq.json
resources/lib/oojs-ui/i18n/uk.json
resources/lib/oojs-ui/i18n/yi.json
resources/lib/oojs-ui/i18n/zh-hans.json
resources/lib/oojs-ui/images/anchor.svg [new file with mode: 0644]
resources/lib/oojs-ui/images/tail.svg [deleted file]
resources/lib/oojs-ui/oojs-ui-agora.css
resources/lib/oojs-ui/oojs-ui-apex.css
resources/lib/oojs-ui/oojs-ui.js
resources/lib/oojs-ui/oojs-ui.svg.css
resources/lib/sinonjs/sinon-1.10.3.js [new file with mode: 0644]
resources/lib/sinonjs/sinon-1.9.0.js [deleted file]
resources/lib/sinonjs/sinon-ie-1.10.3.js [new file with mode: 0644]
resources/lib/sinonjs/sinon-ie-1.9.0.js [deleted file]
resources/src/jquery.json-deprecate.js
resources/src/jquery.ui-themes/vector/jquery.ui.theme.css
resources/src/jquery/jquery.arrowSteps.js
resources/src/mediawiki.api/mediawiki.api.edit.js
resources/src/mediawiki.less/mediawiki.mixins.less
resources/src/mediawiki/mediawiki.feedback.js
resources/src/mediawiki/mediawiki.js
skins/MonoBook/i18n/ca.json
skins/MonoBook/i18n/gl.json
skins/MonoBook/i18n/id.json
skins/MonoBook/i18n/pt.json
skins/MonoBook/i18n/zh-hant.json
skins/Vector/components/notifications.less
skins/Vector/i18n/ca.json
skins/Vector/i18n/gl.json
skins/Vector/i18n/id.json
skins/Vector/i18n/lrc.json
skins/Vector/i18n/uk.json
skins/Vector/i18n/zh-hant.json
tests/parser/parserTests.txt
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/specials/SpecialSearchTest.php
tests/phpunit/maintenance/getSlaveServerTest.php [deleted file]
tests/qunit/QUnitTestResources.php
tests/qunit/data/testrunner.js
thumb.php

index 70f6084..8011044 100644 (file)
@@ -33,6 +33,10 @@ production.
   of search, you should use something like CirrusSearch instead of built in
   search.
 * Users in the 'sysop' group have access to Special:MergeHistory by default.
+* $wgFileStore was removed after having been deprecated in 1.17. Alternative
+  configurations are $wgDeletedDirectory and $wgHashedUploadDirectory.
+* The deprecated $wgUseCommaCount variable has been removed.
+* $wgEnableSorbs and $wgSorbsUrl have been removed.
 
 === New features in 1.24 ===
 * Added a new hook, "WhatLinksHereProps", to allow extensions to annotate
@@ -109,6 +113,9 @@ production.
   permission needs to be set for 'pagelang'.
 * Upgrade Moment.js to v2.7.0.
 * (bug 67042) Added support for the HTML5 <rtc> tag for East Asian typography.
+* Upgrade Sinon.JS to 1.10.3.
+* Added the es5-shim polyfill for older or non-compliant javascript engines.
+* Upgrade jQuery Cookie to v1.2.0.
 
 === Bug fixes in 1.24 ===
 * (bug 49116) Footer copyright notice is now always displayed in user language
@@ -228,6 +235,14 @@ changes to languages because of Bugzilla reports.
 * Removed Title::escapeCanonicalURL(). (deprecated since 1.19)
 * Removed Title::escapeLocalURL(). (deprecated since 1.19)
 * Removed Title::escapeFullURL(). (deprecated since 1.19)
+* Removed User::isValidEmailAddr(). (deprecated since 1.18)
+* Removed Title::getEscapedText(). (deprecated since 1.19)
+* Removed Language::getFallbackLanguageCode(). (deprecated since 1.19)
+* Removed WikiPage::isBigDeletion(). (deprecated since 1.19)
+* Removed MWInit class which contained functions related to a now discontinued
+  PHP compiler called hphpc. (deprecated since 1.22)
+* ApiResult::enableSizeCheck() and disableSizeCheck() are now obsolete.
+* Removed ResourceLoaderGetStartupModules hook. (deprecated since 1.23)
 
 ==== Renamed classes ====
 * CLDRPluralRuleConverter_Expression to CLDRPluralRuleConverterExpression
@@ -271,6 +286,7 @@ changes to languages because of Bugzilla reports.
 * IPBlockForm - Use SpecialBlock directly
 * WatchlistEditor - Use SpecialEditWatchlist directly
 * FormatExif - Use FormatMetadata directly
+* RevertFileAction - Use RevertAction directly
 
 == Compatibility ==
 
index ab23bc0..0bbcd0e 100644 (file)
@@ -2158,12 +2158,6 @@ configuration variables to JavaScript. Things that depend on the current page
 or request state must be added through MakeGlobalVariablesScript instead.
 &$vars: array( variable name => value )
 
-'ResourceLoaderGetStartupModules': DEPRECATED. Run once the startup module is being
-generated. This allows you to add modules to the startup module. This hook
-should be used sparingly since any module added here will be loaded on all
-pages. This hook is useful if you want to make code available to module loader
-scripts.
-
 'ResourceLoaderRegisterModules': Right before modules information is required,
 such as when responding to a resource
 loader request or generating HTML output.
@@ -2920,6 +2914,15 @@ to be switched to HTTPS.
 $user: User in question.
 &$https: Boolean whether $user should be switched to HTTPS.
 
+'UserResetAllOptions': Called in User::resetOptions() when user preferences
+have been requested to be reset. This hook can be used to exclude certain
+options from being reset even when the user has requested all prefs to be reset,
+because certain options might be stored in the user_properties database table
+despite not being visible and editable via Special:Preferences.
+$user: the User (object) whose preferences are being reset
+&$newOptions: array of new (site default) preferences
+$options: array of the user's old preferences
+$resetKinds: array containing the kinds of preferences to reset
 
 'UserRetrieveNewTalks': Called when retrieving "You have new messages!"
 message(s).
@@ -2960,6 +2963,11 @@ invalidated and GetExtendedMetadata hook called again).
 $timestamp: The timestamp metadata was generated
 $file: The file the metadata is for
 
+'UserMailerChangeReturnPath': Called to generate a VERP return address
+when UserMailer sends an email, with a bounce handling extension.
+$to: Array of MailAddress objects for the recipients
+&$returnPath: The return address string
+
 'WantedPages::getQueryInfo': Called in WantedPagesPage::getQueryInfo(), can be
 used to alter the SQL query which gets the list of wanted pages.
 &$wantedPages: WantedPagesPage object
index 67f9a1c..c879258 100644 (file)
@@ -136,7 +136,6 @@ $wgAutoloadLocalClasses = array(
        'MimeMagic' => 'includes/MimeMagic.php',
        'MWHookException' => 'includes/Hooks.php',
        'MWHttpRequest' => 'includes/HttpFunctions.php',
-       'MWInit' => 'includes/Init.php',
        'MWNamespace' => 'includes/Namespace.php',
        'OutputPage' => 'includes/OutputPage.php',
        'Pager' => 'includes/Pager.php',
@@ -227,7 +226,6 @@ $wgAutoloadLocalClasses = array(
        'RawPage' => 'includes/actions/RawAction.php',
        'RenderAction' => 'includes/actions/RenderAction.php',
        'RevertAction' => 'includes/actions/RevertAction.php',
-       'RevertFileAction' => 'includes/actions/RevertAction.php',
        'RevisiondeleteAction' => 'includes/actions/RevisiondeleteAction.php',
        'RollbackAction' => 'includes/actions/RollbackAction.php',
        'SubmitAction' => 'includes/actions/EditAction.php',
index 51ebd57..7c81294 100644 (file)
@@ -355,11 +355,6 @@ $wgEnableAsyncUploads = false;
  */
 $wgIllegalFileChars = ":";
 
-/**
- * @deprecated since 1.17 use $wgDeletedDirectory
- */
-$wgFileStore = array();
-
 /**
  * What directory to place deleted uploads in.
  * Defaults to "{$wgUploadDirectory}/deleted".
@@ -3984,9 +3979,6 @@ $wgTranscludeCacheExpiry = 3600;
  * - 'any': all pages as considered as valid articles
  * - 'comma': the page must contain a comma to be considered valid
  * - 'link': the page must contain a [[wiki link]] to be considered valid
- * - null: the value will be set at run time depending on $wgUseCommaCount:
- *         if $wgUseCommaCount is false, it will be 'link', if it is true
- *         it will be 'comma'
  *
  * See also See https://www.mediawiki.org/wiki/Manual:Article_count
  *
@@ -3994,13 +3986,7 @@ $wgTranscludeCacheExpiry = 3600;
  * to update it, you will need to run the maintenance/updateArticleCount.php
  * script.
  */
-$wgArticleCountMethod = null;
-
-/**
- * Backward compatibility setting, will set $wgArticleCountMethod if it is null.
- * @deprecated since 1.18; use $wgArticleCountMethod instead
- */
-$wgUseCommaCount = false;
+$wgArticleCountMethod = 'link';
 
 /**
  * wgHitcounterUpdateFreq sets how often page counters should be updated, higher
@@ -4712,12 +4698,6 @@ $wgSummarySpamRegex = array();
  */
 $wgEnableDnsBlacklist = false;
 
-/**
- * @deprecated since 1.17 Use $wgEnableDnsBlacklist instead, only kept for
- * backward compatibility.
- */
-$wgEnableSorbs = false;
-
 /**
  * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true.
  *
@@ -4743,12 +4723,6 @@ $wgEnableSorbs = false;
  */
 $wgDnsBlacklistUrls = array( 'http.dnsbl.sorbs.net.' );
 
-/**
- * @deprecated since 1.17 Use $wgDnsBlacklistUrls instead, only kept for
- * backward compatibility.
- */
-$wgSorbsUrl = array();
-
 /**
  * Proxy whitelist, list of addresses that are assumed to be non-proxy despite
  * what the other methods might say.
index 3d57e95..88c49a7 100644 (file)
@@ -1523,6 +1523,37 @@ class EditPage {
                return true;
        }
 
+       /**
+        * Return the summary to be used for a new section.
+        *
+        * @param string $sectionanchor Set to the section anchor text
+        * @return string
+        */
+       private function newSectionSummary( &$sectionanchor = null ) {
+               global $wgParser;
+
+               if ( $this->sectiontitle !== '' ) {
+                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
+                       // If no edit summary was specified, create one automatically from the section
+                       // title and have it link to the new section. Otherwise, respect the summary as
+                       // passed.
+                       if ( $this->summary === '' ) {
+                               $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
+                               return wfMessage( 'newsectionsummary' )
+                                       ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
+                       }
+               } elseif ( $this->summary !== '' ) {
+                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
+                       # This is a new section, so create a link to the new section
+                       # in the revision summary.
+                       $cleanSummary = $wgParser->stripSectionName( $this->summary );
+                       return wfMessage( 'newsectionsummary' )
+                               ->rawParams( $cleanSummary )->inContentLanguage()->text();
+               } else {
+                       return $this->summary;
+               }
+       }
+
        /**
         * Attempt submission (no UI)
         *
@@ -1764,31 +1795,11 @@ class EditPage {
                                if ( $this->sectiontitle !== '' ) {
                                        // Insert the section title above the content.
                                        $content = $content->addSectionHeader( $this->sectiontitle );
-
-                                       // Jump to the new section
-                                       $result['sectionanchor'] =
-                                               $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
-
-                                       // If no edit summary was specified, create one automatically from the section
-                                       // title and have it link to the new section. Otherwise, respect the summary as
-                                       // passed.
-                                       if ( $this->summary === '' ) {
-                                               $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
-                                               $this->summary = wfMessage( 'newsectionsummary' )
-                                                       ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
-                                       }
                                } elseif ( $this->summary !== '' ) {
                                        // Insert the section title above the content.
                                        $content = $content->addSectionHeader( $this->summary );
-
-                                       // Jump to the new section
-                                       $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
-
-                                       // Create a link to the new section from the edit summary.
-                                       $cleanSummary = $wgParser->stripSectionName( $this->summary );
-                                       $this->summary = wfMessage( 'newsectionsummary' )
-                                               ->rawParams( $cleanSummary )->inContentLanguage()->text();
                                }
+                               $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
                        }
 
                        $status->value = self::AS_SUCCESS_NEW_ARTICLE;
@@ -1806,7 +1817,8 @@ class EditPage {
                                $this->isConflict = true;
                                if ( $this->section == 'new' ) {
                                        if ( $this->mArticle->getUserText() == $wgUser->getName() &&
-                                               $this->mArticle->getComment() == $this->summary ) {
+                                               $this->mArticle->getComment() == $this->newSectionSummary()
+                                       ) {
                                                // Probably a duplicate submission of a new comment.
                                                // This can happen when squid resends a request after
                                                // a timeout but the first one actually went through.
@@ -1920,24 +1932,7 @@ class EditPage {
                        wfProfileIn( __METHOD__ . '-sectionanchor' );
                        $sectionanchor = '';
                        if ( $this->section == 'new' ) {
-                               if ( $this->sectiontitle !== '' ) {
-                                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
-                                       // If no edit summary was specified, create one automatically from the section
-                                       // title and have it link to the new section. Otherwise, respect the summary as
-                                       // passed.
-                                       if ( $this->summary === '' ) {
-                                               $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
-                                               $this->summary = wfMessage( 'newsectionsummary' )
-                                                       ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
-                                       }
-                               } elseif ( $this->summary !== '' ) {
-                                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
-                                       # This is a new section, so create a link to the new section
-                                       # in the revision summary.
-                                       $cleanSummary = $wgParser->stripSectionName( $this->summary );
-                                       $this->summary = wfMessage( 'newsectionsummary' )
-                                               ->rawParams( $cleanSummary )->inContentLanguage()->text();
-                               }
+                               $this->summary = $this->newSectionSummary( $sectionanchor );
                        } elseif ( $this->section != '' ) {
                                # Try to get a section anchor from the section source, redirect
                                # to edited section if header found.
diff --git a/includes/Init.php b/includes/Init.php
deleted file mode 100644 (file)
index d9aeb7b..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-<?php
-/**
- * Some functions that are useful during startup.
- *
- * This class previously contained some functionality related to a PHP compiler
- * called hphpc. That compiler has now been discontinued.
- *
- * 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
- */
-
-/**
- * Some functions that are useful during startup.
- *
- * This class previously contained some functionality related to a PHP compiler
- * called hphpc. That compiler has now been discontinued. All methods are now
- * deprecated.
- */
-class MWInit {
-       private static $compilerVersion;
-
-       /**
-        * @deprecated since 1.22
-        */
-       static function getCompilerVersion() {
-               return false;
-       }
-
-       /**
-        * Returns true if we are running under HipHop, whether in compiled or
-        * interpreted mode.
-        *
-        * @deprecated since 1.22
-        * @return bool
-        */
-       static function isHipHop() {
-               return wfIsHHVM();
-       }
-
-       /**
-        * Get a fully-qualified path for a source file relative to $IP.
-        * @deprecated since 1.22
-        *
-        * @param string $file
-        *
-        * @return string
-        */
-       static function interpretedPath( $file ) {
-               global $IP;
-               return "$IP/$file";
-       }
-
-       /**
-        * @deprecated since 1.22
-        * @param string $file
-        * @return string
-        */
-       static function compiledPath( $file ) {
-               global $IP;
-               return "$IP/$file";
-       }
-
-       /**
-        * @deprecated since 1.22
-        * @param string $file
-        * @return string
-        */
-       static function extCompiledPath( $file ) {
-               return false;
-       }
-
-       /**
-        * Deprecated wrapper for class_exists()
-        * @deprecated since 1.22
-        *
-        * @param string $class
-        *
-        * @return bool
-        */
-       static function classExists( $class ) {
-               return class_exists( $class );
-       }
-
-       /**
-        * Deprecated wrapper for method_exists()
-        * @deprecated since 1.22
-        *
-        * @param string $class
-        * @param string $method
-        *
-        * @return bool
-        */
-       static function methodExists( $class, $method ) {
-               return method_exists( $class, $method );
-       }
-
-       /**
-        * Deprecated wrapper for function_exists()
-        * @deprecated since 1.22
-        *
-        * @param string $function
-        *
-        * @return bool
-        */
-       static function functionExists( $function ) {
-               return function_exists( $function );
-       }
-
-       /**
-        * Deprecated wrapper for call_user_func_array()
-        * @deprecated since 1.22
-        *
-        * @param string $className
-        * @param string $methodName
-        * @param array $args
-        *
-        * @return mixed
-        */
-       static function callStaticMethod( $className, $methodName, $args ) {
-               return call_user_func_array( array( $className, $methodName ), $args );
-       }
-}
index 13696ad..f794b2a 100644 (file)
@@ -166,7 +166,9 @@ abstract class PrefixSearch {
        protected function specialSearch( $search, $limit ) {
                global $wgContLang;
 
-               list( $searchKey, $subpageSearch ) = explode( '/', $search, 2 );
+               $searchParts = explode( '/', $search, 2 );
+               $searchKey = $searchParts[0];
+               $subpageSearch = isset( $searchParts[1] ) ? $searchParts[1] : null;
 
                // Handle subpage search separately.
                if ( $subpageSearch !== null ) {
index 145f55a..040aba5 100644 (file)
@@ -110,10 +110,6 @@ if ( $wgGitInfoCacheDirectory === false && $wgCacheDirectory !== false ) {
        $wgGitInfoCacheDirectory = "{$wgCacheDirectory}/gitinfo";
 }
 
-if ( isset( $wgFileStore['deleted']['directory'] ) ) {
-       $wgDeletedDirectory = $wgFileStore['deleted']['directory'];
-}
-
 if ( isset( $wgFooterIcons['copyright'] )
        && isset( $wgFooterIcons['copyright']['copyright'] )
        && $wgFooterIcons['copyright']['copyright'] === array()
@@ -173,11 +169,7 @@ $wgLockManagers[] = array(
  * Initialise $wgLocalFileRepo from backwards-compatible settings
  */
 if ( !$wgLocalFileRepo ) {
-       if ( isset( $wgFileStore['deleted']['hash'] ) ) {
-               $deletedHashLevel = $wgFileStore['deleted']['hash'];
-       } else {
-               $deletedHashLevel = $wgHashedUploadDirectory ? 3 : 0;
-       }
+       $deletedHashLevel = $wgHashedUploadDirectory ? 3 : 0;
        $wgLocalFileRepo = array(
                'class' => 'LocalRepo',
                'name' => 'local',
@@ -378,10 +370,6 @@ if ( !$wgHtml5Version && $wgAllowRdfaAttributes ) {
 // Blacklisted file extensions shouldn't appear on the "allowed" list
 $wgFileExtensions = array_values( array_diff ( $wgFileExtensions, $wgFileBlacklist ) );
 
-if ( $wgArticleCountMethod === null ) {
-       $wgArticleCountMethod = $wgUseCommaCount ? 'comma' : 'link';
-}
-
 if ( $wgInvalidateCacheOnLocalSettingsChange ) {
        // @codingStandardsIgnoreStart Generic.PHP.NoSilencedErrors.Discouraged - No GlobalFunction here yet.
        $wgCacheEpoch = max( $wgCacheEpoch, gmdate( 'YmdHis', @filemtime( "$IP/LocalSettings.php" ) ) );
index bc30eff..35e1ef8 100644 (file)
@@ -155,10 +155,15 @@ abstract class Skin extends ContextSource {
 
                $skinNames = Skin::getSkinNames();
 
+               // Make keys lowercase for case-insensitive matching.
+               $skinNames = array_change_key_case( $skinNames, CASE_LOWER );
+               $key = strtolower( $key );
+               $default = strtolower( $wgDefaultSkin );
+
                if ( $key == '' || $key == 'default' ) {
                        // Don't return the default immediately;
                        // in a misconfiguration we need to fall back.
-                       $key = $wgDefaultSkin;
+                       $key = $default;
                }
 
                if ( isset( $skinNames[$key] ) ) {
@@ -168,7 +173,7 @@ abstract class Skin extends ContextSource {
                // Older versions of the software used a numeric setting
                // in the user preferences.
                $fallback = array(
-                       0 => $wgDefaultSkin,
+                       0 => $default,
                        2 => 'cologneblue'
                );
 
@@ -178,8 +183,8 @@ abstract class Skin extends ContextSource {
 
                if ( isset( $skinNames[$key] ) ) {
                        return $key;
-               } elseif ( isset( $skinNames[$wgDefaultSkin] ) ) {
-                       return $wgDefaultSkin;
+               } elseif ( isset( $skinNames[$default] ) ) {
+                       return $default;
                } else {
                        return 'vector';
                }
index a49d52f..477373a 100644 (file)
@@ -1546,18 +1546,6 @@ class Title {
                return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text );
        }
 
-       /**
-        * Get the HTML-escaped displayable text form.
-        * Used for the title field in <a> tags.
-        *
-        * @return string The text, including any prefixes
-        * @deprecated since 1.19
-        */
-       public function getEscapedText() {
-               wfDeprecated( __METHOD__, '1.19' );
-               return htmlspecialchars( $this->getPrefixedText() );
-       }
-
        /**
         * Get a URL-encoded form of the subpage text
         *
index 5a117bd..fc2b351 100644 (file)
@@ -892,38 +892,6 @@ class User implements IDBAccessObject {
                return $this->mPasswordExpires;
        }
 
-       /**
-        * Does a string look like an e-mail address?
-        *
-        * This validates an email address using an HTML5 specification found at:
-        * http://www.whatwg.org/html/states-of-the-type-attribute.html#valid-e-mail-address
-        * Which as of 2011-01-24 says:
-        *
-        *     A valid e-mail address is a string that matches the ABNF production
-        *   1*( atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined
-        *   in RFC 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section
-        *   3.5.
-        *
-        * This function is an implementation of the specification as requested in
-        * bug 22449.
-        *
-        * Client-side forms will use the same standard validation rules via JS or
-        * HTML 5 validation; additional restrictions can be enforced server-side
-        * by extensions via the 'isValidEmailAddr' hook.
-        *
-        * Note that this validation doesn't 100% match RFC 2822, but is believed
-        * to be liberal enough for wide use. Some invalid addresses will still
-        * pass validation here.
-        *
-        * @param string $addr E-mail address
-        * @return bool
-        * @deprecated since 1.18 call Sanitizer::isValidEmail() directly
-        */
-       public static function isValidEmailAddr( $addr ) {
-               wfDeprecated( __METHOD__, '1.18' );
-               return Sanitizer::validateEmail( $addr );
-       }
-
        /**
         * Given unvalidated user input, return a canonical username, or false if
         * the username is invalid.
@@ -1537,10 +1505,9 @@ class User implements IDBAccessObject {
         * @return bool True if blacklisted.
         */
        public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
-               global $wgEnableSorbs, $wgEnableDnsBlacklist,
-                       $wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist;
+               global $wgEnableDnsBlacklist, $wgDnsBlacklistUrls, $wgProxyWhitelist;
 
-               if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs ) {
+               if ( !$wgEnableDnsBlacklist ) {
                        return false;
                }
 
@@ -1548,8 +1515,7 @@ class User implements IDBAccessObject {
                        return false;
                }
 
-               $urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl );
-               return $this->inDnsBlacklist( $ip, $urls );
+               return $this->inDnsBlacklist( $ip, $wgDnsBlacklistUrls );
        }
 
        /**
@@ -2778,6 +2744,8 @@ class User implements IDBAccessObject {
                        }
                }
 
+               wfRunHooks( 'UserResetAllOptions', array( $this, &$newOptions, $this->mOptions, $resetKinds ) );
+
                $this->mOptions = $newOptions;
                $this->mOptionsLoaded = true;
        }
index d73f6b4..c9db90f 100644 (file)
@@ -239,7 +239,17 @@ class UserMailer {
                # -- hashar 20120218
 
                $headers['From'] = $from->toString();
-               $headers['Return-Path'] = $from->address;
+               $returnPath = $from->address;
+               $extraParams = $wgAdditionalMailParams;
+
+               // Hook to generate custom VERP address for 'Return-Path'
+               wfRunHooks( 'UserMailerChangeReturnPath', array( $to, &$returnPath ) );
+               # Add the envelope sender address using the -f command line option when PHP mail() is used.
+               # Will default to the $from->address when the UserMailerChangeReturnPath hook fails and the
+               # generated VERP address when the hook runs effectively.
+               $extraParams .= ' -f ' . $returnPath;
+
+               $headers['Return-Path'] = $returnPath;
 
                if ( $replyto ) {
                        $headers['Reply-To'] = $replyto->toString();
@@ -371,7 +381,7 @@ class UserMailer {
                                                        self::quotedPrintable( $subject ),
                                                        $body,
                                                        $headers,
-                                                       $wgAdditionalMailParams
+                                                       $extraParams
                                                );
                                        }
                                }
index 92428cf..6481630 100644 (file)
  */
 
 /**
- * Dummy class for pages not in NS_FILE
- *
- * @ingroup Actions
- */
-class RevertAction extends Action {
-
-       public function getName() {
-               return 'revert';
-       }
-
-       public function show() {
-               $this->getOutput()->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
-       }
-
-       public function execute() {
-       }
-}
-
-/**
- * Class for pages in NS_FILE
+ * File reversion user interface
  *
  * @ingroup Actions
  */
-class RevertFileAction extends FormAction {
+class RevertAction extends FormAction {
        /**
         * @var OldLocalFile
         */
@@ -62,6 +43,9 @@ class RevertFileAction extends FormAction {
        }
 
        protected function checkCanExecute( User $user ) {
+               if ( $this->getTitle()->getNamespace() !== NS_FILE ) {
+                       throw new ErrorPageError( $this->msg( 'nosuchaction' ), $this->msg( 'nosuchactiontext' ) );
+               }
                parent::checkCanExecute( $user );
 
                $oldimage = $this->getRequest()->getText( 'oldimage' );
index 4f4d051..7ebd0c3 100644 (file)
@@ -251,10 +251,8 @@ abstract class ApiBase extends ContextSource {
                }
                $msg = array();
                ApiResult::setContent( $msg, $warning );
-               $result->disableSizeCheck();
                $result->addValue( 'warnings', $moduleName,
-                       $msg, ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP );
-               $result->enableSizeCheck();
+                       $msg, ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
        }
 
        /**
index 03a6843..848d129 100644 (file)
@@ -362,10 +362,8 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
                // Disable size checking for this because we can't continue
                // cleanly; size checking would cause more problems than it'd
                // solve
-               $result->disableSizeCheck();
-               $result->addValue( null, '_feed', $feed );
-               $result->addValue( null, '_feeditems', $feedItems );
-               $result->enableSizeCheck();
+               $result->addValue( null, '_feed', $feed, ApiResult::NO_SIZE_CHECK );
+               $result->addValue( null, '_feeditems', $feedItems, ApiResult::NO_SIZE_CHECK );
        }
 
        /**
index 84db9ed..a5873e6 100644 (file)
@@ -699,21 +699,20 @@ class ApiMain extends ApiBase {
                $warnings = isset( $oldResult['warnings'] ) ? $oldResult['warnings'] : null;
 
                $result->reset();
-               $result->disableSizeCheck();
                // Re-add the id
                $requestid = $this->getParameter( 'requestid' );
                if ( !is_null( $requestid ) ) {
-                       $result->addValue( null, 'requestid', $requestid );
+                       $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
                }
                if ( $config->get( 'ShowHostnames' ) ) {
                        // servedby is especially useful when debugging errors
-                       $result->addValue( null, 'servedby', wfHostName() );
+                       $result->addValue( null, 'servedby', wfHostName(), ApiResult::NO_SIZE_CHECK );
                }
                if ( $warnings !== null ) {
-                       $result->addValue( null, 'warnings', $warnings );
+                       $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
                }
 
-               $result->addValue( null, 'error', $errMessage );
+               $result->addValue( null, 'error', $errMessage, ApiResult::NO_SIZE_CHECK );
 
                return $errMessage['code'];
        }
index cd6a840..f4b28fa 100644 (file)
@@ -485,18 +485,16 @@ class ApiQuery extends ApiBase {
                // Don't check the size of exported stuff
                // It's not continuable, so it would cause more
                // problems than it'd solve
-               $result->disableSizeCheck();
                if ( $this->mParams['exportnowrap'] ) {
                        $result->reset();
                        // Raw formatter will handle this
-                       $result->addValue( null, 'text', $exportxml );
-                       $result->addValue( null, 'mime', 'text/xml' );
+                       $result->addValue( null, 'text', $exportxml, ApiResult::NO_SIZE_CHECK );
+                       $result->addValue( null, 'mime', 'text/xml', ApiResult::NO_SIZE_CHECK );
                } else {
                        $r = array();
                        ApiResult::setContent( $r, $exportxml );
-                       $result->addValue( 'query', 'export', $r );
+                       $result->addValue( 'query', 'export', $r, ApiResult::NO_SIZE_CHECK );
                }
-               $result->enableSizeCheck();
        }
 
        public function getAllowedParams( $flags = 0 ) {
index af0d938..6994cd4 100644 (file)
@@ -134,11 +134,17 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                }
 
                if ( $fld_content ) {
+                       // Modern MediaWiki has the content for deleted revs in the 'text'
+                       // table using fields old_text and old_flags. But revisions deleted
+                       // pre-1.5 store the content in the 'archive' table directly using
+                       // fields ar_text and ar_flags, and no corresponding 'text' row. So
+                       // we have to LEFT JOIN and fetch all four fields, plus ar_text_id
+                       // to be able to tell the difference.
                        $this->addTables( 'text' );
                        $this->addJoinConds(
-                               array( 'text' => array( 'INNER JOIN', array( 'ar_text_id=old_id' ) ) )
+                               array( 'text' => array( 'LEFT JOIN', array( 'ar_text_id=old_id' ) ) )
                        );
-                       $this->addFields( array( 'ar_text', 'ar_text_id', 'old_text', 'old_flags' ) );
+                       $this->addFields( array( 'ar_text', 'ar_flags', 'ar_text_id', 'old_text', 'old_flags' ) );
 
                        // This also means stricter restrictions
                        if ( !$user->isAllowedAny( 'undelete', 'deletedtext' ) ) {
@@ -353,7 +359,12 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                                        $anyHidden = true;
                                }
                                if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_TEXT, $user ) ) {
-                                       ApiResult::setContent( $rev, Revision::getRevisionText( $row ) );
+                                       if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
+                                               // Pre-1.5 ar_text row (if condition from Revision::newFromArchiveRow)
+                                               ApiResult::setContent( $rev, Revision::getRevisionText( $row, 'ar_' ) );
+                                       } else {
+                                               ApiResult::setContent( $rev, Revision::getRevisionText( $row ) );
+                                       }
                                }
                        }
 
index b30d9dd..ac64cf0 100644 (file)
@@ -58,6 +58,14 @@ class ApiResult extends ApiBase {
         */
        const ADD_ON_TOP = 2;
 
+       /**
+        * For addValue() and setElement(), do not check size while adding a value
+        * Don't use this unless you REALLY know what you're doing.
+        * Values added while the size checking was disabled will never be counted
+        * @since 1.24
+        */
+       const NO_SIZE_CHECK = 4;
+
        private $mData, $mIsRawMode, $mSize, $mCheckingSize;
 
        private $continueAllModules = array();
@@ -143,6 +151,7 @@ class ApiResult extends ApiBase {
         * Disable size checking in addValue(). Don't use this unless you
         * REALLY know what you're doing. Values added while size checking
         * was disabled will not be counted (ever)
+        * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK
         */
        public function disableSizeCheck() {
                $this->mCheckingSize = false;
@@ -150,6 +159,7 @@ class ApiResult extends ApiBase {
 
        /**
         * Re-enable size checking in addValue()
+        * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK
         */
        public function enableSizeCheck() {
                $this->mCheckingSize = true;
@@ -312,17 +322,16 @@ class ApiResult extends ApiBase {
         * @param array|string|null $path
         * @param string $name
         * @param mixed $value
-        * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. This
-        *    parameter used to be boolean, and the value of OVERRIDE=1 was specifically
-        *    chosen so that it would be backwards compatible with the new method
-        *    signature.
+        * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
+        *   This parameter used to be boolean, and the value of OVERRIDE=1 was specifically
+        *   chosen so that it would be backwards compatible with the new method signature.
         * @return bool True if $value fits in the result, false if not
         *
         * @since 1.21 int $flags replaced boolean $override
         */
        public function addValue( $path, $name, $value, $flags = 0 ) {
                $data = &$this->mData;
-               if ( $this->mCheckingSize ) {
+               if ( $this->mCheckingSize && !( $flags & ApiResult::NO_SIZE_CHECK ) ) {
                        $newsize = $this->mSize + self::size( $value );
                        $maxResultSize = $this->getConfig()->get( 'APIMaxResultSize' );
                        if ( $newsize > $maxResultSize ) {
@@ -616,9 +625,7 @@ class ApiResult extends ApiBase {
                        }
                }
                if ( $data ) {
-                       $this->disableSizeCheck();
-                       $this->addValue( null, $key, $data, ApiResult::ADD_ON_TOP );
-                       $this->enableSizeCheck();
+                       $this->addValue( null, $key, $data, ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
                }
        }
 }
index f550e9e..d78f420 100644 (file)
@@ -126,12 +126,8 @@ class DerivativeContext extends ContextSource {
         * Set the Title object
         *
         * @param Title $t
-        * @throws MWException
         */
-       public function setTitle( $t ) {
-               if ( $t !== null && !$t instanceof Title ) {
-                       throw new MWException( __METHOD__ . " expects an instance of Title" );
-               }
+       public function setTitle( Title $t ) {
                $this->title = $t;
        }
 
index d4bf0b4..efdc6db 100644 (file)
@@ -124,12 +124,8 @@ class RequestContext implements IContextSource {
         * Set the Title object
         *
         * @param Title $t
-        * @throws MWException
         */
-       public function setTitle( $t ) {
-               if ( $t !== null && !$t instanceof Title ) {
-                       throw new MWException( __METHOD__ . " expects an instance of Title" );
-               }
+       public function setTitle( Title $t ) {
                $this->title = $t;
                // Erase the WikiPage so a new one with the new title gets created.
                $this->wikipage = null;
@@ -475,7 +471,8 @@ class RequestContext implements IContextSource {
 
                if ( $params['userId'] ) { // logged-in user
                        $user = User::newFromId( $params['userId'] );
-                       if ( !$user ) {
+                       $user->load();
+                       if ( !$user->getId() ) {
                                throw new MWException( "No user with ID '{$params['userId']}'." );
                        }
                } elseif ( !IP::isValid( $params['ip'] ) ) {
index b3f9210..5353288 100644 (file)
@@ -1048,8 +1048,22 @@ class LoadBalancer {
                $maxLag = -1;
                $host = '';
                $maxIndex = 0;
-               if ( $this->getServerCount() > 1 ) { // no replication = no lag
+
+               if ( $this->getServerCount() <= 1 ) { // no replication = no lag
+                       return array( $host, $maxLag, $maxIndex );
+               }
+
+               // Try to get the max lag info from the server cache
+               $key = 'loadbalancer:maxlag:cluster:' . $this->mServers[0]['host'];
+               $cache = ObjectCache::newAccelerator( array(), 'hash' );
+               $maxLagInfo = $cache->get( $key ); // (host, lag, index)
+
+               // Fallback to connecting to each slave and getting the lag
+               if ( !$maxLagInfo ) {
                        foreach ( $this->mServers as $i => $conn ) {
+                               if ( $i == $this->getWriterIndex() ) {
+                                       continue; // nothing to check
+                               }
                                $conn = false;
                                if ( $wiki === false ) {
                                        $conn = $this->getAnyOpenConnection( $i );
@@ -1067,9 +1081,11 @@ class LoadBalancer {
                                        $maxIndex = $i;
                                }
                        }
+                       $maxLagInfo = array( $host, $maxLag, $maxIndex );
+                       $cache->set( $key, $maxLagInfo, 5 );
                }
 
-               return array( $host, $maxLag, $maxIndex );
+               return $maxLagInfo;
        }
 
        /**
index 1c7762b..c6939a8 100644 (file)
@@ -179,16 +179,11 @@ class CliInstaller extends Installer {
        public function showHelpBox( $msg /*, ... */ ) {
        }
 
+       /**
+        * @param Status $status
+        */
        public function showStatusMessage( Status $status ) {
-               $warnings = array_merge( $status->getWarningsArray(),
-                       $status->getErrorsArray() );
-
-               if ( count( $warnings ) !== 0 ) {
-                       foreach ( $warnings as $w ) {
-                               call_user_func_array( array( $this, 'showMessage' ), $w );
-                       }
-               }
-
+               parent::showStatusMessage( $status );
                if ( !$status->isOk() ) {
                        echo "\n";
                        exit( 1 );
index 4f40693..7c67ee8 100644 (file)
@@ -108,6 +108,10 @@ abstract class Installer {
         * These may output warnings using showMessage(), and/or abort the
         * installation process by returning false.
         *
+        * For the WebInstaller these are only called on the Welcome page,
+        * if these methods have side-effects that should affect later page loads
+        * (as well as the generated stylesheet), use envPreps instead.
+        *
         * @var array
         */
        protected $envChecks = array(
@@ -128,7 +132,6 @@ abstract class Installer {
                'envCheckGit',
                'envCheckServer',
                'envCheckPath',
-               'envCheckExtension',
                'envCheckShellLocale',
                'envCheckUploadsDirectory',
                'envCheckLibicu',
@@ -137,6 +140,17 @@ abstract class Installer {
                'envCheckJSON',
        );
 
+       /**
+        * A list of environment preparation methods called by doEnvironmentPreps().
+        *
+        * @var array
+        */
+       protected $envPreps = array(
+               'envPrepExtension',
+               'envPrepServer',
+               'envPrepPath',
+       );
+
        /**
         * MediaWiki configuration globals that will eventually be passed through
         * to LocalSettings.php. The names only are given here, the defaults
@@ -337,10 +351,17 @@ abstract class Installer {
        abstract public function showError( $msg /*, ... */ );
 
        /**
-        * Show a message to the installing user by using a Status object
+        * Shows messages to the user through a Status object
         * @param Status $status
         */
-       abstract public function showStatusMessage( Status $status );
+       public function showStatusMessage( Status $status ) {
+               $errors = array_merge( $status->getErrorsArray(), $status->getWarningsArray() );
+               if ( $errors ) {
+                       foreach ( $errors as $error ) {
+                               call_user_func( 'showMessage', $error );
+                       }
+               }
+       }
 
        /**
         * Constructor, always call this from child classes.
@@ -377,6 +398,8 @@ abstract class Installer {
                        $this->settings[$var] = $GLOBALS[$var];
                }
 
+               $this->doEnvironmentPreps();
+
                $this->compiledDBs = array();
                foreach ( self::getDBTypes() as $type ) {
                        $installer = $this->getDBInstaller( $type );
@@ -439,6 +462,12 @@ abstract class Installer {
                return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
        }
 
+       public function doEnvironmentPreps() {
+               foreach ( $this->envPreps as $prep ) {
+                       $this->$prep();
+               }
+       }
+
        /**
         * Set a MW configuration variable, or internal installer configuration variable.
         *
@@ -953,55 +982,29 @@ abstract class Installer {
        }
 
        /**
-        * Environment check for the server hostname.
+        * Environment check to inform user which server we've assumed.
+        *
+        * @return bool
         */
        protected function envCheckServer() {
                $server = $this->envGetDefaultServer();
                if ( $server !== null ) {
                        $this->showMessage( 'config-using-server', $server );
-                       $this->setVar( 'wgServer', $server );
                }
-
                return true;
        }
 
        /**
-        * Helper function to be called from envCheckServer()
-        * @return string
-        */
-       abstract protected function envGetDefaultServer();
-
-       /**
-        * Environment check for setting $IP and $wgScriptPath.
+        * Environment check to inform user which paths we've assumed.
+        *
         * @return bool
         */
        protected function envCheckPath() {
-               global $IP;
-               $IP = dirname( dirname( __DIR__ ) );
-               $this->setVar( 'IP', $IP );
-
                $this->showMessage(
                        'config-using-uri',
                        $this->getVar( 'wgServer' ),
                        $this->getVar( 'wgScriptPath' )
                );
-
-               return true;
-       }
-
-       /**
-        * Environment check for setting the preferred PHP file extension.
-        * @return bool
-        */
-       protected function envCheckExtension() {
-               // @todo FIXME: Detect this properly
-               if ( defined( 'MW_INSTALL_PHP5_EXT' ) ) {
-                       $ext = 'php5';
-               } else {
-                       $ext = 'php';
-               }
-               $this->setVar( 'wgScriptExtension', ".$ext" );
-
                return true;
        }
 
@@ -1216,6 +1219,44 @@ abstract class Installer {
                return true;
        }
 
+       /**
+        * Environment prep for the server hostname.
+        */
+       protected function envPrepServer() {
+               $server = $this->envGetDefaultServer();
+               if ( $server !== null ) {
+                       $this->setVar( 'wgServer', $server );
+               }
+       }
+
+       /**
+        * Helper function to be called from envPrepServer()
+        * @return string
+        */
+       abstract protected function envGetDefaultServer();
+
+       /**
+        * Environment prep for setting the preferred PHP file extension.
+        */
+       protected function envPrepExtension() {
+               // @todo FIXME: Detect this properly
+               if ( defined( 'MW_INSTALL_PHP5_EXT' ) ) {
+                       $ext = '.php5';
+               } else {
+                       $ext = '.php';
+               }
+               $this->setVar( 'wgScriptExtension', $ext );
+       }
+
+       /**
+        * Environment prep for setting $IP and $wgScriptPath.
+        */
+       protected function envPrepPath() {
+               global $IP;
+               $IP = dirname( dirname( __DIR__ ) );
+               $this->setVar( 'IP', $IP );
+       }
+
        /**
         * Get an array of likely places we can find executables. Check a bunch
         * of known Unix-like defaults, as well as the PATH environment variable
index 3c8a5b1..100dd10 100644 (file)
@@ -202,7 +202,6 @@ class LocalSettingsGenerator {
                        $locale = '';
                }
 
-               //$rightsUrl = $this->values['wgRightsUrl'] ? '' : '#'; // @todo FIXME: I'm unused!
                $hashedUploads = $this->safeMode ? '' : '#';
                $metaNamespace = '';
                if ( $this->values['wgMetaNamespace'] !== $this->values['wgSitename'] ) {
index 46348f9..ea86231 100644 (file)
@@ -729,16 +729,6 @@ class WebInstaller extends Installer {
                $this->output->addHTML( $html );
        }
 
-       /**
-        * @param Status $status
-        */
-       public function showStatusMessage( Status $status ) {
-               $errors = array_merge( $status->getErrorsArray(), $status->getWarningsArray() );
-               foreach ( $errors as $error ) {
-                       call_user_func_array( array( $this, 'showMessage' ), $error );
-               }
-       }
-
        /**
         * Label a control by wrapping a config-input div around it and putting a
         * label before it.
@@ -1135,8 +1125,18 @@ class WebInstaller extends Installer {
                        $path = $_SERVER['SCRIPT_NAME'];
                }
                if ( $path !== false ) {
-                       $uri = preg_replace( '{^(.*)/(mw-)?config.*$}', '$1', $path );
-                       $this->setVar( 'wgScriptPath', $uri );
+                       $scriptPath = preg_replace( '{^(.*)/(mw-)?config.*$}', '$1', $path );
+                       $scriptExtension = $this->getVar( 'wgScriptExtension' );
+
+                       $this->setVar( 'wgScriptPath', "$scriptPath" );
+                       // Update variables set from Setup.php that are derived from wgScriptPath
+                       $this->setVar( 'wgScript', "$scriptPath/index$scriptExtension" );
+                       $this->setVar( 'wgLoadScript', "$scriptPath/load$scriptExtension" );
+                       $this->setVar( 'wgStylePath', "$scriptPath/skins" );
+                       $this->setVar( 'wgLocalStylePath', "$scriptPath/skins" );
+                       $this->setVar( 'wgExtensionAssetsPath', "$scriptPath/extensions" );
+                       $this->setVar( 'wgUploadPath', "$scriptPath/images" );
+
                } else {
                        $this->showError( 'config-no-uri' );
 
diff --git a/includes/installer/i18n/bto.json b/includes/installer/i18n/bto.json
new file mode 100644 (file)
index 0000000..c1960fb
--- /dev/null
@@ -0,0 +1,17 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Filipinayzd"
+               ]
+       },
+       "config-information": "Impormasyon",
+       "config-page-welcome": "Dagos sa MediaWiki!",
+       "config-page-name": "Ngaran",
+       "config-restart": "Amo, uliton adi",
+       "config-profile-wiki": "Bukas na wiki",
+       "config-profile-private": "Pribadong wiki",
+       "config-logo": "URL ko logo:",
+       "config-cc-again": "Pumili dayday...",
+       "config-install-step-done": "tapus na",
+       "config-help": "tabang"
+}
index 8d678e0..3870ef9 100644 (file)
@@ -4,7 +4,8 @@
                        "Pitort",
                        "පසිඳු කාවින්ද",
                        "Kippelboy",
-                       "Toniher"
+                       "Toniher",
+                       "Fitoschido"
                ]
        },
        "config-desc": "L'instal·lador del MediaWiki",
@@ -45,7 +46,6 @@
        "config-env-good": "S'ha comprovat l'entorn.\nPodeu instal·lar el MediaWiki.",
        "config-env-bad": "S'ha comprovat l'entorn.\nNo podeu instal·lar el MediaWiki.",
        "config-env-php": "El PHP $1 està instal·lat.",
-       "config-env-php-toolow": "El PHP $1 està instal·lat.\nMalauradament, el MediaWiki necessita el PHP $2 o superior.",
        "config-memory-raised": "El <code>memory_limit</code> del PHP és $1 i s'ha aixecat a $2.",
        "config-memory-bad": "<strong>Avís:</strong> El <code>memory_limit</code> del PHP és $1.\nAixò és probablement massa baix.\nLa instal·lació pot fallar!",
        "config-diff3-bad": "No s'ha trobat el GNU diff3.",
@@ -73,6 +73,7 @@
        "config-db-charset": "Joc de caràcters de la base de dades",
        "config-charset-mysql5-binary": "Binari de MySQL 4.1/5.0",
        "config-charset-mysql5": "MySQL 4.1/5.0 UTF-8",
+       "config-mysql-old": "Cal el MySQL $1 o posterior. Teniu el $2.",
        "config-db-port": "Port de la base de dades:",
        "config-db-schema": "Esquema per a MediaWiki:",
        "config-db-schema-help": "Aquest esquema normalment ja serveix.\nNomés canvieu-lo si sabeu què us feu.",
        "config-type-mysql": "MySQL (o compatible)",
        "config-type-mssql": "Microsoft SQL Server",
        "config-header-mysql": "Paràmetres de MySQL",
+       "config-header-postgres": "Paràmetres del PostgreSQL",
+       "config-header-sqlite": "Paràmetres de l'SQLite",
+       "config-header-oracle": "Paràmetres de l'Oracle",
+       "config-header-mssql": "Paràmetres del Microsoft SQL Server",
        "config-invalid-db-type": "Tipus de base de dades no vàlid",
-       "config-missing-db-name": "Heu d'introduir un valor per al «nom de la base de dades»",
-       "config-missing-db-host": "Heu d'introduir un valor per al «servidor de la base de dades»",
+       "config-missing-db-name": "Heu d'introduir un valor per a «{{int:config-db-name}}».",
+       "config-missing-db-host": "Heu d'introduir un valor per a «{{int:config-db-host}}».",
+       "config-missing-db-server-oracle": "Heu d’introduir un valor per a «{{int:config-db-host-oracle}}».",
        "config-sqlite-readonly": "El fitxer <code>$1</code> no es pot escriure.",
        "config-sqlite-cant-create-db": "No s'ha pogut crear el fitxer de base de dades <code>$1</code>.",
        "config-upgrade-done-no-regenerate": "S'ha completat l'actualització.\n\nJa podeu [$1 començar a utilitzar el wiki].",
        "config-install-mainpage-failed": "No s'ha pogut inserir la pàgina principal: $1",
        "config-download-localsettings": "Baixa <code>LocalSettings.php</code>",
        "config-help": "ajuda",
+       "config-help-tooltip": "feu clic per ampliar",
        "config-nofile": "No s'ha pogut trobar el fitxer «$1». S'ha suprimit?",
        "mainpagetext": "'''El MediaWiki s'ha instal·lat correctament.'''",
        "mainpagedocfooter": "Consulteu la [//meta.wikimedia.org/wiki/Help:Contents Guia d'Usuari] per a més informació sobre com utilitzar-lo.\n\n== Per a començar ==\n\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Llista de característiques configurables]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ PMF del MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Llista de correu (''listserv'') per a anuncis del MediaWiki]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Traduïu MediaWiki en la vostra llengua]"
index 59ad98e..307faf3 100644 (file)
@@ -52,7 +52,6 @@
        "config-env-good": "Prostředí bylo zkontrolováno.\nMůžete nainstalovat MediaWiki.",
        "config-env-bad": "Prostředí bylo zkontrolováno.\nMediaWiki nelze nainstalovat.",
        "config-env-php": "Je nainstalováno PHP $1.",
-       "config-env-php-toolow": "Je nainstalováno PHP $1.\nMediaWiki ale vyžaduje PHP $2 nebo vyšší.",
        "config-unicode-using-utf8": "Pro normalizaci Unicode se používá utf8_normalize.so Briona Vibbera.",
        "config-unicode-using-intl": "Pro normalizaci Unicode se používá [http://pecl.php.net/intl PECL rozšíření intl].",
        "config-unicode-pure-php-warning": "'''Upozornění''': Není dostupné [http://pecl.php.net/intl PECL rozšíření intl] pro normalizaci Unicode, bude se využívat pomalá implementace v čistém PHP.\nPokud provozujete wiki s velkou návštěvností, měli byste si přečíst něco o [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizaci Unicode].",
index ac8daaf..f172355 100644 (file)
@@ -58,7 +58,6 @@
        "config-env-good": "Die Installationsumgebung wurde geprüft.\nMediaWiki kann installiert werden.",
        "config-env-bad": "Die Installationsumgebung wurde geprüft.\nMediaWiki kann nicht installiert werden.",
        "config-env-php": "Die Skriptsprache „PHP“ ($1) ist installiert.",
-       "config-env-php-toolow": "PHP $1 ist installiert.\nAllerdings benötigt MediaWiki PHP $2 oder höher.",
        "config-unicode-using-utf8": "Zur Unicode-Normalisierung wird Brion Vibbers <code>utf8_normalize.so</code> eingesetzt.",
        "config-unicode-using-intl": "Zur  Unicode-Normalisierung wird die [http://pecl.php.net/intl PECL-Erweiterung intl] eingesetzt.",
        "config-unicode-pure-php-warning": "'''Warnung:''' Die [http://pecl.php.net/intl PECL-Erweiterung intl] ist für die Unicode-Normalisierung nicht verfügbar, so dass stattdessen die langsame pure-PHP-Implementierung genutzt wird.\nSofern eine Website mit großer Benutzeranzahl betrieben wird, sollten weitere Informationen auf der Webseite [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-Normalisierung (en)] gelesen werden.",
index 71da7f2..250816a 100644 (file)
        "config-env-good": "La medio estis kontrolita.\nVi povas instali MediaWiki.",
        "config-env-bad": "La medio estis kontrolita.\nNe eblas instali MediaWiki.",
        "config-env-php": "PHP $1 estas instalita.",
-       "config-env-php-toolow": "PHP $1 estas instalita.\nTamen, MediaWiki bezonas PHP $2 aŭ pli novan.",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] estas instalita.",
        "config-apc": "[http://www.php.net/apc APC] estas instalita",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] estas instalita",
        "config-diff3-bad": "GNU diff3 ne estis trovita.",
        "config-db-type": "Tipo de datumbazo:",
+       "config-db-wiki-settings": "Identigu ĉi tiun vikion",
+       "config-db-name": "Nomo de datumbazo:",
        "config-charset-mysql5": "MySQL 4.1/5.0 UTF-8",
        "config-type-mysql": "MySQL (aŭ kongrua)",
+       "config-admin-password": "Pasvorto:",
+       "config-admin-password-confirm": "Retajpu pasvorton:",
+       "config-admin-name-blank": "Enigu salutnomon de administranto.",
+       "config-admin-email": "Retpoŝtadreso:",
        "mainpagetext": "'''MediaWiki estis sukcese instalita.'''",
        "mainpagedocfooter": "Konsultu la [//meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide Gvidilon por uzantoj de MediaWiki] por informoj pri uzado de vikia programaro.\n\n==Kiel komenci==\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Listo de konfiguraĵoj] (angle)\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki Oftaj Demandoj] (angle)\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Anonco-dissendolisto pri MediaWiki] (angle)\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Preklad MediaWiki do tvojho jazyka]"
 }
index edb3f27..99555cb 100644 (file)
@@ -67,7 +67,6 @@
        "config-env-good": "El entorno ha sido comprobado.\nPuedes instalar MediaWiki.",
        "config-env-bad": "El entorno ha sido comprobado.\nNo puedes instalar MediaWiki.",
        "config-env-php": "PHP $1 está instalado.",
-       "config-env-php-toolow": "PHP $1 está instalado.\nSin embargo, MediaWiki requiere PHP $2 o superior.",
        "config-unicode-using-utf8": "Usando utf8_normalize.so de Brion Vibber para la normalización Unicode.",
        "config-unicode-using-intl": "Usando la [http://pecl.php.net/intl extensión intl PECL] para la normalización Unicode.",
        "config-unicode-pure-php-warning": "'''Advertencia''': La [http://pecl.php.net/intl extensión intl] no está disponible para efectuar la normalización Unicode. Utilizando la implementación más lenta en PHP.\nSi tu web tiene mucho tráfico, te recomendamos leer acerca de la [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalización Unicode].",
index 23fe457..ccebf08 100644 (file)
@@ -51,7 +51,6 @@
        "config-env-good": "محیط بررسی شده‌است.\nشما می‌توانید مدیاویکی را نصب کنید.",
        "config-env-bad": "محیط بررسی شده‌است.\nشما نمی‌توانید مدیاویکی را نصب کنید.",
        "config-env-php": "پی‌اچ‌پی $1 نصب شده‌است.",
-       "config-env-php-toolow": "پی‌اچ‌پی $1 نصب شده است.\nدر هر صورت، مدیاویکی نیاز به پی‌اچ‌پی نسخهٔ $2 یا بالاتر دارد.",
        "config-unicode-using-utf8": "برای یونیکد عادی از Brion Vibber's utf8_normalize.so استفاده کنید.",
        "config-unicode-using-intl": "برای یونیکد عادی از [http://pecl.php.net/intl intl PECL extension] استفاده کنید.",
        "config-unicode-pure-php-warning": "'''هشدار:''' [http://pecl.php.net/intl intl PECL extension] برای کنترل یونیکد عادی در دسترس نیست،اجرای کاملاً آهسته به تعویق می‌افتد.\nاگر شما یک سایت پر‌ ترافیک را اجرا می‌کنید، باید کمی [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization] را بخوانید.",
@@ -59,7 +58,7 @@
        "config-no-db": "درایور پایگاه اطلاعاتی مناسب پیدا نشد! شما لازم دارید یک درایور پایگاه اطلاعاتی  برای پی‌اچ‌پی نصب کنید.انواع پایگاه اطلاعاتی زیر پشتیبانی شده‌اند:$1.\nاگر شما در گروه اشتراک‌گذاری هستید، از تهیه کنندهٔ گروه خود برای نصب یک درایور پایگاه اطلاعاتی مناسب سوأل کنید.\nاگر خود، پی‌اچ‌پی را تهیه کرده‌اید، با یک پردازشگر فعال دوباره پیکربندی کنید، برای مثال از <code>./configure --with-mysql</code> استفاده کنید.\nاگر پی‌اچ‌پی را از یک بستهٔ دبیان یا آبونتو نصب کرده‌اید، بنابراین لازم دارید بخش php5-mysql را نصب کنید.",
        "config-outdated-sqlite": "''' هشدار:''' شما اس‌کیولایت $1 دارید، که پایین‌تر از حداقل نسخهٔ $2 مورد نیاز است.اس‌کیولایت در دسترس نخواهد بود.",
        "config-no-fts3": "'''هشدار:''' اس‌کیولایت بدون [//sqlite.org/fts3.html FTS3 module] تهیه شده‌است ، جستجوی ویژگی‌ها در این بخش پیشین در دسترس نخواهد‌بود.",
-       "config-register-globals": "'''هشدار:''' گزینهٔ  PHP's <code>[http://php.net/register_globals register_globals]</code> فعال شده‌است.'''\n''' اگر می‌توانید غیر فعالش کنید.'''\nمدیاویکی کار خواهد‌کرد، اما سرور شما در معرض آسیب‌پذیری‌های امنیتی ممکن قرار دارد.",
+       "config-register-globals-error": "<strong>خطا:  پی‌اچ‌پی<code>[http://php.net/register_globals register_globals]</code> گزینه فعال است.\nبرای ادامه نصب باید غیر فعال باشد.</strong>\n[Https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] را برای کمک در مورد نحوه انجام این کار ببینید.",
        "config-magic-quotes-runtime": "'''مخرب: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] فعال است.\nاین گزینه اطلاعات داده شده به رایانه را به طور غیر‌قابل پیش‌بینی از بین می‌برد.\nشما نمی‌توانید مدیاویکی را نصب یا استفاده کنید مگر اینکه این گزینه غیر‌فعال باشد.",
        "config-magic-quotes-sybase": "'''مخرب: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] فعال است.\nاین گزینه اطلاعات داده شده به رایانه را به طور غیر‌قابل پیش‌بینی از بین می‌برد.\nشما نمی‌توانید مدیاویکی را نصب یا استفاده کنید مگر اینکه این گزینه غیر‌فعال باشد.",
        "config-mbstring": "''' مخرب:[http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] فعال است.\nاین گزینه باعث ایجاد خطا می‌شود و ممکن است اطلاعات را به طور غیر‌قابل پیش‌بینی از بین ببرد.\nشما نمی‌توانید مدیاویکی را نصب یا استفاده کنید مگر اینکه این گزینه غیر‌فعال باشد.",
index 308418f..42e5e77 100644 (file)
@@ -66,7 +66,6 @@
        "config-env-good": "L’environnement a été vérifié.\nVous pouvez installer MediaWiki.",
        "config-env-bad": "L’environnement a été vérifié.\nVous ne pouvez pas installer MediaWiki.",
        "config-env-php": "PHP $1 est installé.",
-       "config-env-php-toolow": "PHP $1 est installé.\nCependant, MediaWiki requiert PHP $2 ou supérieur.",
        "config-unicode-using-utf8": "Utilisation de utf8_normalize.so par Brion Vibber pour la normalisation Unicode.",
        "config-unicode-using-intl": "Utilisation de [http://pecl.php.net/intl l'extension PECL intl] pour la normalisation Unicode.",
        "config-unicode-pure-php-warning": "<strong>Attention</strong> : L'[http://pecl.php.net/intl extension PECL intl] n'est pas disponible pour la normalisation d’Unicode, retour à la version lente implémentée en PHP.\nSi votre site web sera très fréquenté, vous devriez lire ceci : [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ''Unicode normalization''] (en anglais).",
index 6d5861b..b809a4c 100644 (file)
@@ -49,7 +49,6 @@
        "config-env-good": "Rematou a comprobación da contorna.\nPode instalar MediaWiki.",
        "config-env-bad": "Rematou a comprobación da contorna.\nNon pode instalar MediaWiki.",
        "config-env-php": "Está instalado o PHP $1.",
-       "config-env-php-toolow": "Está instalado o PHP $1.\nPorén, MediaWiki necesita o PHP $2 ou superior.",
        "config-unicode-using-utf8": "Usando utf8_normalize.so de Brion Vibber para a normalización Unicode.",
        "config-unicode-using-intl": "Usando a [http://pecl.php.net/intl extensión intl PECL] para a normalización Unicode.",
        "config-unicode-pure-php-warning": "'''Atención:''' A [http://pecl.php.net/intl extensión intl PECL] non está dispoñible para manexar a normalización Unicode; volvendo á implementación lenta de PHP puro.\nSe o seu sitio posúe un alto tráfico de visitantes, debería ler un chisco sobre a [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalización Unicode].",
@@ -57,7 +56,7 @@
        "config-no-db": "Non se puido atopar un controlador axeitado para a base de datos! Necesita instalar un controlador de base de datos para PHP.\nOs tipos de base de datos admitidos son os seguintes: $1.\n\nSe compilou o PHP vostede mesmo, reconfigúreo activando un cliente de base de datos, por exemplo, usando <code>./configure --with-mysql</code>.\nSe instalou o PHP desde un paquete Debian ou Ubuntu, entón tamén necesita instalar, por exemplo, o módulo <code>php5-mysql</code>.",
        "config-outdated-sqlite": "'''Atención:''' Ten o SQLite $1, que é inferior á versión mínima necesaria: $2. O SQLite non estará dispoñible.",
        "config-no-fts3": "'''Atención:''' O SQLite está compilado sen o [//sqlite.org/fts3.html módulo FTS3]; as características de procura non estarán dispoñibles nesta instalación.",
-       "config-register-globals": "'''Atención: A opción PHP <code>[http://php.net/register_globals register_globals]</code> está activada.'''\n'''Desactívea se pode.'''\nMediaWiki funcionará, pero o seu servidor está exposto a potenciais vulnerabilidades de seguridade.",
+       "config-register-globals-error": "<strong>Erro: A opción <code>[http://php.net/register_globals register_globals]</code> do PHP está activada.\nCómpre desactivala para continuar a instalación.</strong>\nConsulte o enderezo [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] para obter axuda sobre como facelo.",
        "config-magic-quotes-runtime": "'''Erro fatal: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] está activado!'''\nEsta opción corrompe os datos de entrada de xeito imprevisible.\nNon pode instalar ou empregar MediaWiki a menos que esta opción estea desactivada.",
        "config-magic-quotes-sybase": "'''Erro fatal: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] está activado!'''\nEsta opción corrompe os datos de entrada de xeito imprevisible.\nNon pode instalar ou empregar MediaWiki a menos que esta opción estea desactivada.",
        "config-mbstring": "'''Erro fatal: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] está activado!'''\nEsta opción causa erros e pode corromper os datos de xeito imprevisible.\nNon pode instalar ou empregar MediaWiki a menos que esta opción estea desactivada.",
index 582e463..acede93 100644 (file)
@@ -51,7 +51,6 @@
        "config-env-good": "הסביבה שלכם נבדקה.\nאפשר להתקין מדיה־ויקי.",
        "config-env-bad": "הסביבה שלכם נבדקה.\nאי־אפשר להתקין מדיה־ויקי.",
        "config-env-php": "מותקנת <span dir=\"ltr\">PHP $1</span>.",
-       "config-env-php-toolow": "מותקנת <span dir=\"ltr\">PHP $1</span>.\nלמדיה־ויקי נדרשת <span dir=\"ltr\">PHP $2</span> או גרסה גבוהה יותר.",
        "config-unicode-using-utf8": "משתמש ב־utf8_normalize.so של בריון ויבר לנרמול יוניקוד.",
        "config-unicode-using-intl": "משתמש ב[http://pecl.php.net/intl הרחבת intl PECL] לנרמול יוניקוד.",
        "config-unicode-pure-php-warning": "'''אזהרה''': [http://pecl.php.net/intl הרחבת intl PECL] אינה זמינה לטיפול בנרמול יוניקוד. משתמש ביישום PHP טהור ואטי יותר.\nאם זהו אתר בעל תעבורה גבוהה, כדאי לקרוא את המסמך הבא: [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
index a37ed89..c2411ef 100644 (file)
@@ -53,7 +53,6 @@
        "config-env-good": "Kondisi telah diperiksa.\nAnda dapat menginstal MediaWiki.",
        "config-env-bad": "Kondisi telah diperiksa.\nAnda tidak dapat menginstal MediaWiki.",
        "config-env-php": "PHP $1 diinstal.",
-       "config-env-php-toolow": "PHP $1 telah terinstal.\nNamun, MediaWiki memerlukan PHP $2 atau lebih tinggi.",
        "config-unicode-using-utf8": "Menggunakan utf8_normalize.so Brion Vibber untuk normalisasi Unicode.",
        "config-unicode-using-intl": "Menggunakan [http://pecl.php.net/intl ekstensi PECL intl] untuk normalisasi Unicode.",
        "config-unicode-pure-php-warning": "'''Peringatan''': [http://pecl.php.net/intl Ekstensi intl PECL] untuk menangani normalisasi Unicode tidak tersedia, kembali menggunakan implementasi murni PHP yang lambat.\nJika Anda menjalankan situs berlalu lintas tinggi, Anda harus sedikit membaca [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalisasi Unicode].",
@@ -61,7 +60,7 @@
        "config-no-db": "Pengandar basis data yang sesuai tidak ditemukan! Anda perlu menginstal pengandar basis data untuk PHP.\nJenis basis data yang didukung: $1.\n\nJika Anda mengompilasi sendiri PHP, ubahlah konfigurasinya dengan mengaktifkan klien basis data, misalnya menggunakan <code>./configure --with-mysql</code>.\nJika Anda menginstal PHP dari paket Debian atau Ubuntu, maka Anda juga perlu menginstal modul php5-mysql.",
        "config-outdated-sqlite": "<strong>Peringatan:</strong> Anda menggunakan SQLite $1, yang lebih rendah dari versi minimum yang diperlukan $2. SQLite akan tidak tersedia.",
        "config-no-fts3": "'''Peringatan''': SQLite dikompilasi tanpa [//sqlite.org/fts3.html modul FTS3], fitur pencarian tidak akan tersedia pada konfigurasi ini.",
-       "config-register-globals": "'''Peringatan: Opsi <code>[http://php.net/register_globals register_globals]</code> PHP diaktifkan.'''\n'''Nonaktifkan kalau bisa.'''\nMediaWiki akan bekerja, tetapi server Anda memiliki potensi kerentanan keamanan.",
+       "config-register-globals-error": "<strong>Kesalahan: Pilihan PHP <code>[http://php.net/register_globals register_globals]</code> diaktifkan.\nIni harus dinonaktifkan untuk melanjutkan instalasi.</strong>\nLihat [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] untuk bantuan tentang cara melakukannya.",
        "config-magic-quotes-runtime": "'''Fatal: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] aktif!'''\nPilihan ini dapat merusak masukan data secara tidak terduga.\nAnda tidak dapat menginstal atau menggunakan MediaWiki kecuali pilihan ini dinonaktifkan.",
        "config-magic-quotes-sybase": "'''Fatal: [http://www.php.net/manual/en/ref.info.php#ini.magic_quotes_sybase magic_quotes_sybase] aktif!'''\nPilihan ini dapat merusak masukan data secara tidak terduga.\nAnda tidak dapat menginstal atau menggunakan MediaWiki kecuali pilihan ini dinonaktifkan.",
        "config-mbstring": "'''Fatal: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] aktif!'' '\nPilihan ini dapat menyebabkan kesalahan dan kerusakan data yang tidak terduga.\nAnda tidak dapat menginstal atau menggunakan MediaWiki kecuali pilihan ini dinonaktifkan.",
@@ -72,6 +71,7 @@
        "config-memory-raised": "<code>memory_limit</code> PHP adalah $1, dinaikkan ke $2.",
        "config-memory-bad": "'''Peringatan:''' <code>memory_limit</code> PHP adalah $1.\nIni terlalu rendah.\nInstalasi terancam gagal!",
        "config-ctype": "<strong>Fatal:</strong> PHP harus disusun dengan dukungan untuk [http://www.php.net/manual/en/ctype.installation.php ekstensi Ctype].",
+       "config-json": "<strong>Fatal:</strong> PHP dikompilasi tanpa dukungan JSON.\nAnda harus menginstal salah satu pengaya PHP JSON atau pengaya [http://pecl.php.net/package/jsonc PECL jsonc] sebelum menginstal MediaWiki.\n* Pengaya PHP termasuk dalam Red Hat Enterprise Linux (CentOS) 5 dan 6, meskipun harus diaktifkan pada <code>/etc/php.ini</code> atau <code>/etc/php.d/json.ini</code>.\n* Beberapa distribusi Linux dirilis setelah Mei 2013 menghilangkan pengaya PHP, bukan kemasan pengaya PECL sebagai <code>php5-json</code> atau <code>php-pecl-jsonc</code>.",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] telah diinstal",
        "config-apc": "[http://www.php.net/apc APC] telah diinstal",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] telah diinstal",
@@ -88,6 +88,7 @@
        "config-using-server": "Menggunakan nama server \"<nowiki>$1</nowiki>\".",
        "config-using-uri": "Menggunakan URL server \"<nowiki>$1$2</nowiki>\".",
        "config-uploads-not-safe": "'''Peringatan:''' Direktori bawaan pengunggahan <code>$1</code> Anda rentan terhadap eksekusi skrip yang sewenang-wenang.\nMeskipun MediaWiki memeriksa semua berkas unggahan untuk ancaman keamanan, sangat dianjurkan untuk [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security menutup kerentanan keamanan ini] sebelum mengaktifkan pengunggahan.",
+       "config-no-cli-uploads-check": "<strong>Peringatan:</strong> Direktori default Anda untuk unggahan (<code>$1</code>) tidak diperiksa untuk kerentanan terhadap\neksekusi script sewenang-wenang selama instalasi CLI.",
        "config-brokenlibxml": "Sistem Anda memiliki kombinasi versi PHP dan libxml2 yang memiliki bug dan dapat menyebabkan kerusakan data tersembunyi pada MediaWiki dan aplikasi web lain.\nMutakhirkan ke PHP 5.2.9 atau yang lebih baru dan libxml2 2.7.3 atau yang lebih baru ([https://bugs.php.net/bug.php?id=45996 arsip bug di PHP]).\nInstalasi dibatalkan.",
        "config-suhosin-max-value-length": "Suhosin terpasang dan membatasi parameter GET <code>length</code> sebesar $1 bita. Komponen ResourceLoader MediaWiki akan berjalan dalam batasan ini, tetapi penanganannya akan menurunkan kinerja. Jika memungkinkan, Anda sebaiknya menetapkan nilai <code>suhosin.get.max_value_length</code> menjadi 1024 atau lebih tinggi dalam <code>php.ini</code> dan menyetel <code>$wgResourceLoaderMaxQueryLength</code> dengan nilai yang sama dalam <code>LocalSettings.php</code>.",
        "config-db-type": "Jenis basis data:",
        "config-missing-db-name": "Anda harus memasukkan nilai untuk \"{{int:config-db-name}}\"",
        "config-missing-db-host": "Anda harus memasukkan nilai untuk \"{{int:config-db-host}}\"",
        "config-missing-db-server-oracle": "Anda harus memasukkan nilai untuk \"{{int:config-db-host-oracle}}\"",
-       "config-invalid-db-server-oracle": "TNS basis data \"$1\" tidak sah.\nGunakan hanya huruf ASCII (a-z, A-Z), angka (0-9), garis bawah (_), dan titik (.).",
+       "config-invalid-db-server-oracle": "TNS basis data \"$1\" tidak sah.\nGunakan baik \"Nama TNS\" atau string \"Easy Connect\" ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Metode Penamaan Oracle]).",
        "config-invalid-db-name": "Nama basis data \"$1\" tidak sah.\nGunakan hanya huruf ASCII (a-z, A-Z), angka (0-9), garis bawah (_), dan tanda hubung (-).",
        "config-invalid-db-prefix": "Prefiks basis data \"$1\" tidak sah.\nGunakan hanya huruf ASCII (a-z, A-Z), angka (0-9), garis bawah (_), dan tanda hubung (-).",
        "config-connection-error": "$1.\n\nPeriksa nama inang, pengguna, dan sandi di bawah ini dan coba lagi.",
index 0638452..72619bc 100644 (file)
@@ -50,7 +50,6 @@
        "config-env-good": "환경이 확인되었습니다.\n미디어위키를 설치할 수 있습니다.",
        "config-env-bad": "환경이 확인되었습니다.\n미디어위키를 설치할 수 없습니다.",
        "config-env-php": "PHP $1(이)가 설치되었습니다.",
-       "config-env-php-toolow": "PHP $1(이)가 설치되었습니다.\n하지만 미디어위키는 PHP $2 이상이 필요합니다.",
        "config-unicode-using-utf8": "유니코드 정규화에 Brion Vibber의 utf8_normalize.so를 사용합니다.",
        "config-unicode-using-intl": "유니코드 정규화에 [http://pecl.php.net/intl intl PECL 확장 기능]을 사용합니다.",
        "config-unicode-pure-php-warning": "'''경고''': 유니코드 정규화를 처리할 [http://pecl.php.net/intl intl PECL 확장 기능]을 사용할 수 없기 때문에 느린 pure-PHP 구현을 대신 사용합니다.\n트래픽이 높은 사이트에서 실행하시려면 [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations 유니코드 정규화]를 읽어보시기 바랍니다.",
@@ -58,7 +57,7 @@
        "config-no-db": "적절한 데이터베이스 드라이버를 찾을 수 없습니다! PHP용 데이터베이스 드라이버를 설치해야 합니다.\n다음 데이터베이스 유형을 지원합니다: $1.\n\nPHP를 직접 컴파일했다면, 예를 들어 <code>./configure --with-mysql</code>을 사용하여, 데이터베이스 클라이언트를 활성화하도록 다시 설정하세요.\n데비안이나 우분투 패키지에서 PHP를 설치했다면 <code>php5-mysql</code> 모듈도 설치해야 합니다.",
        "config-outdated-sqlite": "'''경고''': 최소인 $2 버전보다 낮은 SQLite $1(이)가 있습니다. SQLite를 사용할 수 없습니다.",
        "config-no-fts3": "'''경고''': SQLite를 [//sqlite.org/fts3.html FTS3 모듈] 없이 컴파일하며, 검색 기능은 백엔드에 사용할 수 없습니다.",
-       "config-register-globals": "'''경고: PHP의 <code>[http://php.net/register_globals register_globals]</code> 옵션이 활성화되어 있습니다.'''\n'''가능하면 이를 비활성화하십시오.'''\n미디어위키는 작동하지만 서버에 잠재적인 보안 취약점이 노출됩니다.",
+       "config-register-globals-error": "<strong>오류: PHP의 <code>[http://php.net/register_globals register_globals]</code> 옵션이 활성화되어 있습니다.\n설치를 계속하려면 비활성화해야 합니다.</strong>\n어떻게 하는지에 대한 도움말에 대해서는 [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals]를 보세요.",
        "config-magic-quotes-runtime": "'''치명: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime]이 활성화됩니다!'''\n이 옵션은 데이터를 입력하는 데 예기치 않는 손상이 일으킵니다.\n이 옵션을 비활성화하지 않는 한 미디어위키를 설치하고 사용할 수 없습니다.",
        "config-magic-quotes-sybase": "'''치명: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase]이 활성화됩니다!'''\n이 옵션은 데이터를 입력하는 데 예기치 않는 손상을 일으킵니다.\n이 옵션을 비활성화하지 않는 한 미디어위키를 설치하고 사용할 수 없습니다.",
        "config-mbstring": "'''치명: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload]이 활성화됩니다!'''\n이 옵션은 오류가 발생하고 데이터를 입력하는 데 예기치 않는 손상을 일으킬 수 있습니다.\n이 옵션을 비활성화하지 않는 한 미디어위키를 설치하고 사용할 수 없습니다.",
index 87ab22e..b67f62f 100644 (file)
@@ -47,7 +47,6 @@
        "config-env-good": "Околината е проверена.\nМожете да го воспоставите МедијаВики.",
        "config-env-bad": "Околината е проверена.\nНе можете да го воспоставите МедијаВики.",
        "config-env-php": "PHP $1 е воспоставен.",
-       "config-env-php-toolow": "PHP $1 е воспоставен.\nМеѓутоа, МедијаВики бара PHP $2 или поново.",
        "config-unicode-using-utf8": "Со utf8_normalize.so за уникодна нормализација од Брајон Вибер (Brion Vibber).",
        "config-unicode-using-intl": "Со додатокот [http://pecl.php.net/intl intl PECL] за уникодна нормализација.",
        "config-unicode-pure-php-warning": "'''Предупредување''': Додатокот [http://pecl.php.net/intl intl PECL] не е достапен за врши уникодна нормализација, враќајќи се на бавна примена на чист PHP.\n\nАко имате високопрометно мрежно место, тогаш ќе треба да прочитате повеќе за [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations уникодната нормализација].",
index be78096..93ddfa4 100644 (file)
@@ -50,7 +50,6 @@
        "config-env-good": "Miljøet har blitt sjekket.\nDu kan installere MediaWiki.",
        "config-env-bad": "Miljøet har blitt sjekket.\nDu kan installere MediaWiki.",
        "config-env-php": "PHP $1 er innstallert.",
-       "config-env-php-toolow": "PHP $1 er installert.\nMediaWiki krever imidlertid PHP $2 eller høyere.",
        "config-unicode-using-utf8": "Bruker Brion Vibbers utf8_normalize.so for Unicode-normalisering.",
        "config-unicode-using-intl": "Bruker [http://pecl.php.net/intl intl PECL-utvidelsen] for Unicode-normalisering.",
        "config-unicode-pure-php-warning": "'''Advarsel''': [http://pecl.php.net/intl intl PECL-utvidelsen] er ikke tilgjengelig for å håndtere Unicode-normaliseringen, faller tilbake til en langsommere ren-PHP-implementasjon.\nOm du kjører et nettsted med høy trafikk bør du lese litt om [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-normalisering].",
index 79969e8..68a94e8 100644 (file)
@@ -57,7 +57,6 @@
        "config-env-good": "O ambiente foi verificado.\nVocê pode instalar o MediaWiki.",
        "config-env-bad": "O ambiente foi verificado.\nVocê não pode instalar o MediaWiki.",
        "config-env-php": "O PHP $1 está instalado.",
-       "config-env-php-toolow": "PHP $1 está instalado.\nNo entanto, o MediaWiki requer PHP $2 ou superior.",
        "config-unicode-using-utf8": "Usando o utf8_normalize.so, de Brion Vibber, para a normalização Unicode.",
        "config-unicode-using-intl": "Usando a [http://pecl.php.net/intl extensão intl PECL] para a normalização Unicode.",
        "config-unicode-pure-php-warning": "<strong>Aviso</strong>: A [http://pecl.php.net/intl extensão intl PECL] não está disponível para efetuar a normalização Unicode, abortando e passando para a lenta implementação de PHP puro.\nSe o seu site tem um alto volume de tráfego, informe-se sobre a [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalização Unicode].",
index b57ebf9..5e86a92 100644 (file)
@@ -62,7 +62,6 @@
        "config-env-good": "See also:\n* {{msg-mw|Config-env-bad}}",
        "config-env-bad": "See also:\n* {{msg-mw|Config-env-good}}",
        "config-env-php": "Parameters:\n* $1 - the version of PHP that has been installed\nSee also:\n* {{msg-mw|config-env-php-toolow}}",
-       "config-env-php-toolow": "Parameters:\n* $1 - the version of PHP that has been installed\n* $2 - minimum PHP version number\nSee also:\n* {{msg-mw|config-env-php}}",
        "config-unicode-using-utf8": "Status message in the MediaWiki installer environment checks.",
        "config-unicode-using-intl": "Status message in the MediaWiki installer environment checks.",
        "config-unicode-pure-php-warning": "PECL is the name of a group producing standard pieces of software for PHP, and intl is the name of their library handling some aspects of internationalization.",
index 1bc4d08..3203df7 100644 (file)
@@ -62,7 +62,6 @@
        "config-env-good": "Проверка внешней среды была успешно проведена.\nВы можете установить MediaWiki.",
        "config-env-bad": "Была проведена проверка внешней среды.\nВы не можете установить MediaWiki.",
        "config-env-php": "Установленная версия PHP: $1.",
-       "config-env-php-toolow": "Найден PHP $1, тогда как MediaWiki требуется PHP версии $2 или выше.",
        "config-unicode-using-utf8": "Использовать Brion Vibber utf8_normalize.so для нормализации Юникода.",
        "config-unicode-using-intl": "Будет использовано [http://pecl.php.net/intl расширение «intl» для PECL] для нормализации Юникода.",
        "config-unicode-pure-php-warning": "'''Внимание!''': [http://pecl.php.net/intl расширение intl из PECL] недоступно для нормализации Юникода, будет использоваться медленная реализация на чистом PHP.\nЕсли ваш сайт работает под высокой нагрузкой, вам следует больше узнать о [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations нормализации Юникода].",
index 4f1c9c9..4cb5442 100644 (file)
@@ -51,7 +51,6 @@
        "config-env-good": "Miljön har kontrollerats.\nDu kan installera MediaWiki.",
        "config-env-bad": "Miljön har kontrollerats.\nDu kan inte installera MediaWiki.",
        "config-env-php": "PHP $1 är installerat.",
-       "config-env-php-toolow": "PHP $1 är installerat.\nMediaWiki kräver PHP $2 eller högre.",
        "config-unicode-using-utf8": "Använder Brion Vibbers utf8_normalize.so för Unicode-normalisering.",
        "config-unicode-using-intl": "Använder [http://pecl.php.net/intl intl PECL-tillägget] för Unicode-normalisering.",
        "config-unicode-pure-php-warning": "'''Varning:''' [http://pecl.php.net/intl intl PECL-tillägget] är inte tillgängligt för att hantera Unicode-normalisering, faller tillbaka till en långsamt implementering i ren PHP.\nOm du driver en högtrafikerad webbplats bör du läsa lite om [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-normalisering].",
@@ -59,6 +58,7 @@
        "config-no-db": "Kunde inte hitta en lämplig databasdrivrutin! Du måste installera en databasdrivrutin för PHP.\nFöljande databastyper stöds: $1.\n\nI du själv kompilerat din PHP, konfigurera den med en databasklient aktiverad genom att t.ex. använda <code>./configure --with-mysqli</code>.\nOm du installerade PHP från ett Debian- eller Ubuntupaket måste du även installera, t.ex. <code>php5-mysql</code>-paketet.",
        "config-outdated-sqlite": "'''Varning:''' du har SQLite $1, vilket är lägre än minimikravet version $2. SQLite kommer inte att vara tillgänglig.",
        "config-no-fts3": "'''Varning:''' SQLite kompileras utan [//sqlite.org/fts3.html FTS3-modulen], sökfunktioner kommer att vara otillgängliga på denna backend.",
+       "config-register-globals-error": "<strong>Fel: PHP-alternativet <code>[http://php.net/register_globals register_globals]</code> är aktiverad.\nDen måste vara inaktiverad för att fortsätta med installationen.</strong>\nSe [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] för hjälp om hur man gör så.",
        "config-magic-quotes-runtime": "'''Kritiskt: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime magic_quotes_runtime] är aktiv!'''\nDetta alternativ korrumperar inmatad data oförutsägbart.\nDu kan inte installera eller använda MediaWiki om detta alternativ är aktiverat.",
        "config-magic-quotes-sybase": "'''Kritiskt: [http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-sybase magic_quotes_sybase] är aktiv!'''\nDetta alternativ korrumperar inmatad data oförutsägbart.\nDu kan inte installera eller använda MediaWiki om detta alternativ är aktiverat.",
        "config-mbstring": "'''Kritiskt: [http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload mbstring.func_overload] är aktiv!'''\nDetta alternativ orsakar fel och kan korrumpera data oförutsägbart.\nDu kan inte installera eller använda MediaWiki om detta alternativ är aktiverat.",
index 6ec8860..83c51ee 100644 (file)
@@ -18,7 +18,7 @@
        "config-localsettings-badkey": "Sağladığınız anahtar doğru değil.",
        "config-upgrade-key-missing": "Mevcut bir MediaWiki kurulumu algılandı.\nBu kurulumu güncelleştirmek için, lütfen aşağıdaki satırı <code>LocalSettings.php</code> dosyanızın en altına koyun:\n\n$1",
        "config-localsettings-incomplete": "Mevcut <code>LocalSettings.php</code> eksik gibi görünüyor.\n $1  değişkeni ayarlanmamış.\nLütfen <code>LocalSettings.php</code> dosyasını değiştirin bu değişkenleri kuracak, ve tıklayın  \"{{int:Config-cuntinue}}\".",
-       "config-localsettings-connection-error": "<code>LocalSettings.php</code> ya da <code>AdminSettings.php</code> dosyasında belirtilen ayarları kullanarak veritabanına bağlanırken bir hatayla karşılaşıldı. Lütfen bu ayarları düzeltin ve yeniden deneyin.\n\n$1",
+       "config-localsettings-connection-error": "<code>LocalSettings.php</code> içinde belirtilen ayarları kullanarak veritabanına bağlanırken bir hatayla karşılaşıldı. Lütfen bu ayarları düzeltin ve yeniden deneyin.\n\n$1",
        "config-session-error": "Oturum başlatılırken hata: $1",
        "config-session-expired": "Oturum bilgilerinizin süresi bitmiş.\nOturumların süresi $1 kadardır.\nBu süreyi php.ini' deki <code>session.gc_maxlifetime</code> ayarla arttırabilirsiniz.\nKurulum işlemini yeniden başlatın.",
        "config-no-session": "Oturum bilgileriniz silinmiş.\nphp.ini dosyanızı kontrol edin ve <code>session.save_path</code> ayarının uygun bir klasöre yönlendiğinden emin olun.",
        "config-restart": "Evet, yeniden başlat",
        "config-welcome": "===Ortam Kontrolleri===\nOrtamın Mediawiki kurulumuna uygun olup olmadığını anlamak için basit kontroller yapılacak.\nKurulumu nasıl tamamlayacağınız konusunda destek isterken bu bilgileri eklemeyi unutmayın.",
        "config-copyright": "=== Telif Hakları ve Koşulları ===\n\n$1\n\nBu program ücretsiz bir yazılımdır; yeniden dağıtabilir veya Özgür Yazılım Kuruluşu tarafından yayınlanan (GNU) Genel Kamu Lisansı koşulları altında değiştirebilirsiniz; isterseniz ikinci lisans sürümünü veya (sizin seçeneğiniz) herhangi bir sonraki lisans sürümünü kullanabilirsiniz.\n\nBu program, faydalı olacağı umuduyla dağıtılmaktadır, ancak ''' herhangi bir garantisi yoktur '''; ''' uygunluk ''' veya ''' belirli bir amaca uygunluk ''' gibi dolaylı garantileri bile yoktur.\nDaha fazla ayrıntı için (GNU) Genel Kamu Lisansına bakınız.\n\nBu program ile birlikte <doclink href=\"Copying\">bir (GNU) Genel Kamu Lisansının bir kopyasını </doclink> almış olmanız gerekir; bu program (GNU) Genel Kamu Lisansı ile dağıtılmadıysa, Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, ABD adresine yazın veya [http://www.gnu.org/copyleft/gpl.html online olarak okuyun].",
+       "config-sidebar": "* [//www.mediawiki.org MediaWiki ana sayfa]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Kullanıcı Rehberi]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Yetkili Rehberi]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ SSS]\n----\n* <doclink href=Readme>Beni oku</doclink>\n* <doclink href=ReleaseNotes>Sürüm notları</doclink>\n* <doclink href=Copying>Kopyalama</doclink>\n* <doclink href=UpgradeDoc>Yükseltme</doclink>",
        "config-env-good": "Ortam kontrol edildi.\nMediaWiki'yi kurabilirsiniz.",
        "config-env-bad": "Ortam kontrol edildi.\nMediaWiki'yi kuramazsınız.",
        "config-env-php": "PHP $1 kurulu.",
-       "config-env-php-toolow": "PHP $1 kurulu.\nAncak, MediaWiki PHP $2 ya da daha yenisine ihtiyaç duyuyor.",
        "config-unicode-using-utf8": "Unikod normalleştirmesi için Brion Vibber'in utf8_normalize.so kullanılıyor.",
        "config-unicode-using-intl": "Unikod normalleştirmesi için [http://pecl.php.net/intl intl PECL uzantısı] kullanılıyor.",
        "config-xml-bad": "PHP 'nin XML modülü eksik.\nMediaWiki bu modüldeki fonksiyonlara ihtiyaç duyar ve şimdiki kurulumda çalışmayacaktır.\nMandrake kullanıyorsanız php-xml paketini yükleyin.",
        "config-install-mainpage-failed": "Ana sayfa eklenemedi:$1",
        "config-download-localsettings": "İndir <code>LocalSettings.php</code>",
        "config-help": "Yardım",
+       "config-help-tooltip": "genişletmek için tıklayın",
        "config-nofile": "\"$1\" dosyası bulunamadı. Silindi mi?",
        "config-extension-link": "Vikinizin [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions eklentileri] desteklediğini biliyor musunuz?\n\n[//www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category Eklentileri kategorilerine göre] inceleyebilir ya da tüm eklentilerin listesini görmek için [//www.mediawiki.org/wiki/Extension_Matrix Eklenti Matrisine] bakabilirsiniz.",
        "mainpagetext": "'''MediaWiki başarı ile kuruldu.'''",
index 6f67613..9c2d37e 100644 (file)
@@ -65,7 +65,6 @@
        "config-env-good": "环境检查已经完成。您可以安装MediaWiki。",
        "config-env-bad": "环境检查已经完成。您不能安装MediaWiki。",
        "config-env-php": "PHP $1已安装。",
-       "config-env-php-toolow": "已安装PHP $1;但是,MediaWiki需要PHP $2或更高版本。",
        "config-unicode-using-utf8": "使用Brion Vibber的utf8_normalize.so实现Unicode正常化。",
        "config-unicode-using-intl": "使用[http://pecl.php.net/intl intl PECL扩展程序]标准化Unicode。",
        "config-unicode-pure-php-warning": "<strong>警告:</strong>因为尚未安装 [http://pecl.php.net/intl intl PECL 扩展]以处理 Unicode 正常化,故只能退而采用运行较慢的纯 PHP 实现的方法。\n如果您运行着一个高流量的站点,请参阅 [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode 正常化]一文。",
index 6b10ae4..4885ae6 100644 (file)
@@ -163,6 +163,8 @@ class CSSMin {
         * Build a CSS 'url()' value for the given URL, quoting parentheses (and other funny characters)
         * and escaping quotes as necessary.
         *
+        * See http://www.w3.org/TR/css-syntax-3/#consume-a-url-token
+        *
         * @param string $url URL to process
         * @return string 'url()' value, usually just `"url($url)"`, quoted/escaped if necessary
         */
index f7dfe46..8700c8c 100644 (file)
@@ -269,10 +269,4 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                wfProfileOut( __METHOD__ );
                return $this->checkResult( false, $result );
        }
-
-
-       /* NOTE: there is no cas() method here because it is currently not supported
-        * by the BagOStuff interface and other BagOStuff subclasses, such as
-        * SqlBagOStuff.
-        */
 }
index 0009999..633b34a 100644 (file)
@@ -119,11 +119,15 @@ class ObjectCache {
        /**
         * Factory function referenced from DefaultSettings.php for CACHE_ACCEL.
         *
+        * This will look for any APC style server-local cache.
+        * A fallback cache can be specified if none is found.
+        *
         * @param array $params
+        * @param int|string $fallback Fallback cache, e.g. (CACHE_NONE, "hash") (since 1.24)
         * @throws MWException
         * @return BagOStuff
         */
-       static function newAccelerator( $params ) {
+       static function newAccelerator( $params, $fallback = null ) {
                if ( function_exists( 'apc_fetch' ) ) {
                        $id = 'apc';
                } elseif ( function_exists( 'xcache_get' ) && wfIniGetBool( 'xcache.var_size' ) ) {
@@ -131,6 +135,9 @@ class ObjectCache {
                } elseif ( function_exists( 'wincache_ucache_get' ) ) {
                        $id = 'wincache';
                } else {
+                       if ( $fallback ) {
+                               return self::newFromId( $fallback );
+                       }
                        throw new MWException( "CACHE_ACCEL requested but no suitable object " .
                                "cache is present. You may want to install APC." );
                }
index 0e989d3..2f27826 100644 (file)
@@ -480,7 +480,7 @@ class Article implements Page {
         * page of the given title.
         */
        public function view() {
-               global $wgUseFileCache, $wgUseETag, $wgDebugToolbar;
+               global $wgUseFileCache, $wgUseETag, $wgDebugToolbar, $wgMaxRedirects;
 
                wfProfileIn( __METHOD__ );
 
@@ -542,8 +542,31 @@ class Article implements Page {
                                $outputPage->setETag( $parserCache->getETag( $this, $parserOptions ) );
                        }
 
+                       # Use the greatest of the page's timestamp or the timestamp of any
+                       # redirect in the chain (bug 67849)
+                       $timestamp = $this->mPage->getTouched();
+                       if ( isset( $this->mRedirectedFrom ) ) {
+                               $timestamp = max( $timestamp, $this->mRedirectedFrom->getTouched() );
+
+                               # If there can be more than one redirect in the chain, we have
+                               # to go through the whole chain too in case an intermediate
+                               # redirect was changed.
+                               if ( $wgMaxRedirects > 1 ) {
+                                       $titles = Revision::newFromTitle( $this->mRedirectedFrom )
+                                               ->getContent( Revision::FOR_THIS_USER, $user )
+                                               ->getRedirectChain();
+                                       $thisTitle = $this->getTitle();
+                                       foreach ( $titles as $title ) {
+                                               if ( Title::compare( $title, $thisTitle ) === 0 ) {
+                                                       break;
+                                               }
+                                               $timestamp = max( $timestamp, $title->getTouched() );
+                                       }
+                               }
+                       }
+
                        # Is it client cached?
-                       if ( $outputPage->checkLastModified( $this->mPage->getTouched() ) ) {
+                       if ( $outputPage->checkLastModified( $timestamp ) ) {
                                wfDebug( __METHOD__ . ": done 304\n" );
                                wfProfileOut( __METHOD__ );
 
index 34f15c3..87cc7ba 100644 (file)
@@ -40,12 +40,6 @@ class WikiFilePage extends WikiPage {
                $this->mRepo = null;
        }
 
-       public function getActionOverrides() {
-               $overrides = parent::getActionOverrides();
-               $overrides['revert'] = 'RevertFileAction';
-               return $overrides;
-       }
-
        /**
         * @param File $file
         */
index 2ff72fd..31f0ed0 100644 (file)
@@ -3505,17 +3505,6 @@ class WikiPage implements Page, IDBAccessObject {
                return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
        }
 
-       /**
-        * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit
-        *
-        * @deprecated since 1.19; use Title::isBigDeletion() instead.
-        * @return bool
-        */
-       public function isBigDeletion() {
-               wfDeprecated( __METHOD__, '1.19' );
-               return $this->mTitle->isBigDeletion();
-       }
-
        /**
         * Get the  approximate revision count of this page.
         *
index 79c8ade..faff0e7 100644 (file)
@@ -365,9 +365,15 @@ class CoreParserFunctions {
         * @param string $text Desired title text
         * @return string
         */
-       static function displaytitle( $parser, $text = '' ) {
+       static function displaytitle( $parser, $text = '', $uarg = '' ) {
                global $wgRestrictDisplayTitle;
 
+               static $magicWords = null;
+               if ( is_null( $magicWords ) ) {
+                       $magicWords = new MagicWordArray( array( 'displaytitle_noerror', 'displaytitle_noreplace' ) );
+               }
+               $arg = $magicWords->matchStartToEnd( $uarg );
+
                // parse a limited subset of wiki markup (just the single quote items)
                $text = $parser->doQuotes( $text );
 
@@ -413,13 +419,25 @@ class CoreParserFunctions {
                ) );
                $title = Title::newFromText( Sanitizer::stripAllTags( $text ) );
 
-               if ( !$wgRestrictDisplayTitle ) {
-                       $parser->mOutput->setDisplayTitle( $text );
-               } elseif ( $title instanceof Title
+               if ( !$wgRestrictDisplayTitle ||
+                       ( $title instanceof Title
                        && !$title->hasFragment()
-                       && $title->equals( $parser->mTitle )
+                       && $title->equals( $parser->mTitle ) )
                ) {
-                       $parser->mOutput->setDisplayTitle( $text );
+                       $old = $parser->mOutput->getProperty( 'displaytitle' );
+                       if ( $old === false || $arg !== 'displaytitle_noreplace' ) {
+                               $parser->mOutput->setDisplayTitle( $text );
+                       }
+                       if ( $old !== false && $old !== $text && !$arg ) {
+                               $converter = $parser->getConverterLanguage()->getConverter();
+                               return '<span class="error">' .
+                                       wfMessage( 'duplicate-displaytitle',
+                                               // Message should be parsed, but these params should only be escaped.
+                                               $converter->markNoConversion( wfEscapeWikiText( $old ) ),
+                                               $converter->markNoConversion( wfEscapeWikiText( $text ) )
+                                       )->inContentLanguage()->text() .
+                                       '</span>';
+                       }
                }
 
                return '';
index 8a936c6..56eb0a0 100644 (file)
@@ -345,7 +345,6 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
        public static function getStartupModulesUrl( ResourceLoaderContext $context ) {
                // The core modules:
                $moduleNames = array( 'jquery', 'mediawiki' );
-               wfRunHooks( 'ResourceLoaderGetStartupModules', array( &$moduleNames ), '1.23' );
 
                // Get the latest version
                $loader = $context->getResourceLoader();
index 797543f..96e4dbf 100644 (file)
@@ -83,7 +83,7 @@ class SpecialUnblock extends SpecialPage {
                        'Target' => array(
                                'type' => 'text',
                                'label-message' => 'ipaddressorusername',
-                               'tabindex' => '1',
+                               'autofocus' => true,
                                'size' => '45',
                                'required' => true,
                        ),
@@ -131,6 +131,9 @@ class SpecialUnblock extends SpecialPage {
                                                $fields['Target']['default'] = "#{$this->target}";
                                                break;
                                }
+                               // target is hidden, so the reason is the first element
+                               $fields['Target']['autofocus'] = false;
+                               $fields['Reason']['autofocus'] = true;
                        }
                } else {
                        $fields['Target']['default'] = $this->target;
index 576b625..a857da4 100644 (file)
@@ -513,7 +513,7 @@ class SpecialVersion extends SpecialPage {
                        );
 
                        array_walk( $tags, function ( &$value ) {
-                               $value = '&lt;' . htmlentities( $value ) . '&gt;';
+                               $value = '&lt;' . htmlspecialchars( $value ) . '&gt;';
                        } );
                        $out .= $this->listToText( $tags );
                } else {
index 7d439fa..ea526de 100644 (file)
@@ -49,23 +49,44 @@ class WantedFilesPage extends WantedQueryPage {
                        $category = false;
                }
 
+               $noForeign = '';
+               if ( !$this->likelyToHaveFalsePositives() ) {
+                       // Additional messages for grep:
+                       // wantedfiletext-cat-noforeign, wantedfiletext-nocat
+                       $noForeign = '-noforeign';
+               }
+
                if ( $category ) {
                        return $this
-                               ->msg( 'wantedfiletext-cat' )
+                               ->msg( 'wantedfiletext-cat' . $noForeign )
                                ->params( $category->getFullText() )
                                ->parseAsBlock();
                } else {
                        return $this
-                               ->msg( 'wantedfiletext-nocat' )
+                               ->msg( 'wantedfiletext-nocat' . $noForeign )
                                ->parseAsBlock();
                }
        }
 
+       /**
+        * Whether foreign repos are likely to cause false positives
+        *
+        * In its own function to allow subclasses to override.
+        * @see SpecialWantedFilesGUOverride in GlobalUsage extension.
+        * @since 1.24
+        */
+       protected function likelyToHaveFalsePositives() {
+               return RepoGroup::singleton()->hasForeignRepos();
+       }
+
        /**
         * KLUGE: The results may contain false positives for files
         * that exist e.g. in a shared repo.  Setting this at least
         * keeps them from showing up as redlinks in the output, even
         * if it doesn't fix the real problem (bug 6220).
+        *
+        * @note could also have existing links here from broken file
+        * redirects.
         * @return bool
         */
        function forceExistenceCheck() {
index 19ea20b..bab544b 100644 (file)
@@ -29,6 +29,7 @@ class UserloginTemplate extends BaseTemplate {
                $expirationDays = ceil( $wgCookieExpiration / ( 3600 * 24 ) );
 ?>
 <div class="mw-ui-container">
+       <div id="userloginprompt"><?php $this->msgWiki('loginprompt') ?></div>
        <?php if ( $this->haveData( 'languages' ) ) { ?>
                <div id="languagelinks">
                        <p><?php $this->html( 'languages' ); ?></p>
index bf30455..0a19d61 100644 (file)
@@ -443,17 +443,6 @@ class Language {
        function initContLang() {
        }
 
-       /**
-        * Same as getFallbacksFor for current language.
-        * @return array|bool
-        * @deprecated since 1.19
-        */
-       function getFallbackLanguageCode() {
-               wfDeprecated( __METHOD__, '1.19' );
-
-               return self::getFallbackFor( $this->mCode );
-       }
-
        /**
         * @return array
         * @since 1.19
@@ -857,6 +846,11 @@ class Language {
                        include "$IP/languages/Names.php";
                }
 
+               // If passed an invalid language code to use, fallback to en
+               if ( $inLanguage !== null && !Language::isValidCode( $inLanguage ) ) {
+                       $inLanguage = 'en';
+               }
+
                $names = array();
 
                if ( $inLanguage ) {
index c9e75e0..e1b03b5 100644 (file)
        'tw' => 'Twi',                  # Twi, (FIXME!)
        'ty' => 'reo tahiti',   # Tahitian
        'tyv' => 'тыва дыл',     # Tyvan
+       'tzm' => 'ⵜⴰⵎⴰⵣⵉⵖⵜ',    # Tamazight
        'udm' => 'удмурт',        # Udmurt
        'ug' => 'ئۇيغۇرچە / Uyghurche', # Uyghur (multiple scripts - defaults to Arabic)
        'ug-arab' => 'ئۇيغۇرچە', # Uyghur (Arabic script) (default)
index 2ba72df..04ad974 100644 (file)
        "talkpagelinktext": "نقاش",
        "specialpage": "صفحة خاصة",
        "personaltools": "أدوات شخصية",
-       "postcomment": "قسم جديد",
        "articlepage": "اعرض صفحة المحتوى",
        "talk": "نقاش",
        "views": "معاينة",
        "externaldberror": "هناك إما خطأ في دخول قاعدة البيانات الخارجية أو أنه غير مسموح لك بتحديث حسابك الخارجي.",
        "login": "تسجيل الدخول",
        "nav-login-createaccount": "دخول / إنشاء حساب",
-       "loginprompt": "يجب أن تكون الكوكيز لديك مفعلة لتسجل الدخول إلى {{SITENAME}}.",
        "userlogin": "دخول / إنشاء حساب",
        "userloginnocreate": "تسجيل الدخول",
        "logout": "تسجيل الخروج",
        "license-nopreview": "(العرض المسبق غير متوفر)",
        "upload_source_url": "  (مسار صحيح، يمكن الوصول إليه)",
        "upload_source_file": " (ملف على حاسوبك)",
+       "listfiles-delete": "أحذف",
        "listfiles-summary": "هذه الصفحة الخاصة تعرض كل الملفات المرفوعة.",
        "listfiles_search_for": "ابحث عن اسم الميديا:",
        "imgfile": "ملف",
        "expand_templates_remove_nowiki": "أخفِ وسوم <nowiki> في الناتج",
        "expand_templates_generate_xml": "اعرض شجرة XML parse",
        "expand_templates_generate_rawhtml": "أظهر خام HTML",
-       "expand_templates_preview": "عرض مسبق"
+       "expand_templates_preview": "عرض مسبق",
+       "pagelang-name": "صفحة"
 }
index c7d66d8..1669b9d 100644 (file)
        "talkpagelinktext": "Müzakirə",
        "specialpage": "Xüsusi səhifə",
        "personaltools": "Şəxsi alətlər",
-       "postcomment": "Yeni bölmə",
        "articlepage": "Məqaləni nəzərdən keçir",
        "talk": "Müzakirə",
        "views": "Görünüş",
index e2194eb..56162b5 100644 (file)
        "talkpagelinktext": "Dischkrian",
        "specialpage": "Speziaalseiten",
        "personaltools": "Mei Weakzeig",
-       "postcomment": "Neicher Obschnit",
        "articlepage": "Seiteninhoid åzoang",
        "talk": "Dischkrian",
        "views": "Osichtn",
        "externaldberror": "Entweder es ligt a Feeler bai da externen Authentifiziarung vur oder du derfst dai externs Benytzerkonto ned aktualisirn.",
        "login": "Eilogga",
        "nav-login-createaccount": "Eilogga / Konto olegn",
-       "loginprompt": "Zua Omejdung miassen Cookies aktiviat sei.",
        "userlogin": "Eilogga / Konto olegn",
        "userloginnocreate": "Åmöden",
        "logout": "Obmöden",
        "linkstoimage-more": "Es {{PLURAL:$1|valinkt|valinkn}} mea wia {{PLURAL:$1|oa Seitn |$1 Seitn}} auf de Datei.\nDe foignde Listn zaagt netta {{PLURAL:$1|in easten Link|de easten $1 Links}} auf de Datei.\nA [[Special:WhatLinksHere/$2|voiständige Listn]] gibt's aa.",
        "nolinkstoimage": "De Datei wead vo koana Seitn gnutzt.",
        "morelinkstoimage": "[[Special:WhatLinksHere/$1|Weidare Links]] fia de Datei.",
+       "linkstoimage-redirect": "$1 (Dateiweidaloatung) $2",
        "duplicatesoffile": "{{PLURAL:$1|D'foignde Datei is a Duplikat|De foigndn $1 Datein han Duplikate}} vu dea Datei ([[Special:FileDuplicateSearch/$2|weidare Deteus]]):",
        "sharedupload": "De Datei stãmmt aus $1 und deaf bei ãndare Projekte vawendt wean.",
        "sharedupload-desc-there": "De Datei stãmmt aus $1 und deaf bei ãndera Projekte vawendt wean. Schau auf'd [$2 Dateibeschreibungsseitn] fia weidare Infoamazionen.",
        "filedelete-intro": "Du léschst dé Daatei '''„[[Media:$1|$1]]“'''.",
        "mimesearch-summary": "Auf dieser Spezialseite können die Dateien nach dem MIME-Typ gefiltert werden. Die Eingabe muss immer den Medien- und Subtyp beinhalten: <code>image/jpeg</code> (siehe Bildbeschreibungsseite).",
        "download": "Owerlooden",
+       "listredirects": "Weidaloatunga",
        "unusedtemplates": "Net benutzte Vorlagen",
        "unusedtemplateswlh": "Aundre Links",
        "randompage": "Zuafoisseitn",
        "statistics-edits-average": "Beorweitungen pró Seiten im Durchschnit",
        "statistics-views-total": "Seitenaufruaff gsåmmt",
        "statistics-mostpopular": "Dé am moastbsuachten Seiten",
-       "doubleredirects": "Doppede Weiderloatungen",
+       "doubleredirects": "Doppede Weidaloatunga",
+       "doubleredirectstext": "Af dea Seitn stengan Weidaloatunga, de wo af Weidaloatunga zoang.\n\nA <del>duachgstrichane</del> Eidrog is scho repariad worn.",
+       "double-redirect-fixed-maintenance": "Doppede Weidaloatunga vo [[$1]] af [[$2]] mid oana Aktion reparian.",
+       "brokenredirectstext": "Af dera Spezialseitn stengan Weidaloatunga af Artiken, de wo s ned gibt.",
        "brokenredirects-edit": "werkeln",
        "brokenredirects-delete": "léschen",
        "withoutinterwiki": "Seitn ohne Sprochlinks",
        "movepagebtn": "Seitn vaschiam",
        "pagemovedsub": "s'Vaschiam håd highaud",
        "movepage-moved": "'''D'Seitn „$1“ is nåch „$2“ vaschom woan.'''",
-       "movepage-moved-redirect": "Es is a Weiderloatung erstöd worn.",
+       "movepage-moved-redirect": "A Weidaloatung eigricht worn.",
+       "movepage-moved-noredirect": "De Eirichtung vo oana Weidaloatung is vahindat worn.",
        "articleexists": "Unter dém Naum existierd schoh a Seiten. Bittscheh nimm an aundern Naumen her.",
        "movetalk": "Waunns geet, d' Dischkrierseiten aa midvaschiam",
        "movelogpage": "Vaschiabungs-Logbuach",
        "lastmodifiedatby": "Dé Seiten is zletzt am $1 um $2 voh $3 gänderd worn.",
        "othercontribs": "Basiard auf da Orweid voh $1",
        "creditspage": "Seiteninformaziónen",
+       "pageinfo-redirects-name": "Ozoi vo de Weidaloatunga zua dea Seitn",
+       "pageinfo-redirectsto": "Weidaloatunga af",
        "markedaspatrollederrortext": "Du muasst a Seitenänderrung auswön",
        "deletedrevision": "Oide Version $1 glöscht.",
        "filedelete-missing": "De Datei „$1“ ko net glöscht wern, weils es net gibt.",
        "htmlform-submit": "Speichern",
        "htmlform-reset": "Änderrungen ryckgängég mochen",
        "htmlform-selectorother-other": "Åndre",
+       "logentry-move-move_redir": "$1 hod de Seitn $3 af $4 {{GENDER:$2|verschom}} und hod dabei a Weidaloatung ibaschriem",
+       "logentry-move-move_redir-noredirect": "$1 hod de Seitn $3 af $4 {{GENDER:$2|verschom}} und dabei a Weidaloatung ibaschriem, ohne a neiche ozlegn",
        "searchsuggest-search": "Suach",
        "searchsuggest-containing": "Voitextsuach noch ..."
 }
index f5fb077..5030c0d 100644 (file)
@@ -12,9 +12,9 @@
                ]
        },
        "tog-underline": "Linyahan an kilyawan:",
-       "tog-hideminor": "Tagóon an mga saradít na paghirá sa nakakaági pa sanáng pagbabàgo",
+       "tog-hideminor": "Tagoon an saradít na paghira sa nakakaagi pa sanang pagbabàgo",
        "tog-hidepatrolled": "Tagóa an patrolyadong mga paghirá sa nakakaági pa sanáng pagbabàgo",
-       "tog-newpageshidepatrolled": "Tagóa an patrolyadong mga pahina gikan sa listahan kan bàgong pahina",
+       "tog-newpageshidepatrolled": "Tagoon an patrolyadong mga pahina gikan sa listahan kan bàgong pahina",
        "tog-extendwatchlist": "Palakbanga an bantay-listahan (watchlist) na maipahiling an gabos na pinagbago, bako sana an pinakahurihang binago",
        "tog-usenewrc": "Pangrupong mga kaliwatan sa kada pahina kan mga dae pa sana nahaloy na mga kaliwatan asin bantay-listahan",
        "tog-numberheadings": "Tolos-bilang na mga pamayohán",
@@ -27,7 +27,7 @@
        "tog-watchdeletion": "Idagdag an mga pahina asin mga sagunson na ako an nagpura sa sakong bantay-listahan",
        "tog-minordefault": "Markahán gabos na saradit na pagliwat sa paaging panugmad",
        "tog-previewontop": "Ipahilíng an patànaw bàgo an kahon nin paghirá",
-       "tog-previewonfirst": "Ipahilíng an patànaw sa enot na paghirá",
+       "tog-previewonfirst": "Ipahiling an patànaw sa inot na paghira",
        "tog-enotifwatchlistpages": "E-suratan mo ako kunsoarin an sarong pahina o sagunson na yaon sa sakong bantay-listahan pinagliwat",
        "tog-enotifusertalkpages": "E-koreohan ako pag pigribáyan an pahina kan sakóng olay",
        "tog-enotifminoredits": "E-suratan man ako para sa saraditon na mga pagliwat kan mga pahina asin mga sagunson",
@@ -49,8 +49,8 @@
        "tog-norollbackdiff": "Omidohon an diff matapos himoon an pagbalikot",
        "tog-useeditwarning": "Patanidan ako kunsoarin na ako nagbaya sa pahinang pigliliwat na dae naitatagama an mga kaliwatan",
        "tog-prefershttps": "Pirmeng gumamit nin sarong seguradong koneksyon kunsoarin na ika nakalaog na",
-       "underline-always": "Pirmi",
-       "underline-never": "Nungka",
+       "underline-always": "Parati",
+       "underline-never": "Dai lamang",
        "underline-default": "Kublit o kilyaw na panugmad",
        "editfont-style": "Baguhon an estilo nin kalwig sa sinasakupan",
        "editfont-default": "Kilyawang tugmad",
        "june-date": "Hunyo $1",
        "july-date": "Hulyo $1",
        "august-date": "Agosto $1",
-       "september-date": "Septiyembre $1",
-       "october-date": "Oktubre $1",
+       "september-date": "Setyembre $1",
+       "october-date": "Oktobre $1",
        "november-date": "Nobyembre $1",
-       "december-date": "Disyembre $1",
+       "december-date": "Desyembre $1",
        "pagecategories": "{{PLURAL:$1|Kategorya|Mga kategorya}}",
        "category_header": "Mga pahina sa kategoryang \"$1\"",
        "subcategories": "Mga sub-kategorya",
        "category-media-header": "Media sa kategoryang \"$1\"",
-       "category-empty": "''Ining kategorya sa presente mayong laog na mga pahina o media.\"",
+       "category-empty": "''Ining kategorya mayong laog na mga pahina o media sa ngunyan.''",
        "hidden-categories": "{{PLURAL:$1|Nakatagong kategorya|Mga nakatagong kategorya}}",
        "hidden-category-category": "Mga nakatagong kategorya",
        "category-subcat-count": "{{PLURAL:$2|Ining kategorya igwa sana kan minasunod na sub-kategorya.|Ining kategorya igwa kan minasunod {{PLURAL:$1|subcategory|$1 subcategories}}, out of $2 total.}}",
        "category-article-count-limited": "An minasunod na {{PLURAL:$1|pahina|$1 mga pahina}} yaon sa presenteng kategorya.",
        "category-file-count": "{{PLURAL:$2|Ining kategorya naglalaman sana kan minasunod na sagunson.|An minasunod {{PLURAL:$1|sagunson iyo|$1 na mga sagunson iyo}} sa kategoryang ini, na ginahi sa $2 sa kabilogan.}}",
        "category-file-count-limited": "An minasunod {{PLURAL:$1|na sagunson|$1 na mga sagunson}} yaon sa presenteng kategorya.",
-       "listingcontinuesabbrev": "sunód",
-       "index-category": "Hinukdoang mga pahina",
-       "noindex-category": "Bakong hinukdoang mga pahina",
+       "listingcontinuesabbrev": "sunod",
+       "index-category": "Panhinukdong mga pahina",
+       "noindex-category": "Bakong panhinukdong mga pahina",
        "broken-file-category": "Mga pahina na igwang nagkaparasa na sagunsong kilyawan",
        "about": "Manonongod",
        "article": "Laog na pahina",
        "newwindow": "(minabukas sa bàgong bintanà)",
-       "cancel": "Kanselaron",
+       "cancel": "Pondohon",
        "moredotdotdot": "Kadagdagan...",
-       "morenotlisted": "Ining listahan bako pang kumpleto.",
-       "mypage": "An Pahina",
-       "mytalk": "Orolayan",
-       "anontalk": "Olay para kaining IP address",
-       "navigation": "Nabigasyon",
+       "morenotlisted": "Kulang ining listahan.",
+       "mypage": "Pahina",
+       "mytalk": "Mag-ulay",
+       "anontalk": "Urulay para kaining IP address",
+       "navigation": "Paglibotlibot",
        "and": "&#32;asin",
        "qbfind": "Maghanap",
-       "qbbrowse": "Halungkáta",
-       "qbedit": "Liwata",
+       "qbbrowse": "Maghalungkat",
+       "qbedit": "Liwaton",
        "qbpageoptions": "Ining pahina",
        "qbmyoptions": "Sakong mga pahina",
-       "faq": "PPK (Pirmihang Pighahapot na mga kahaputan)",
-       "faqpage": "Project:PPK (Pirmihang Pighahapot na mga Kahaputan)",
-       "actions": "Mga aksyon",
+       "faq": "PH (Parating Hapot)",
+       "faqpage": "Proyekto:PH (Parating Hapot)",
+       "actions": "Mga paghiro",
        "namespaces": "Mga espasyong ngaran",
-       "variants": "Mga pinalaen",
-       "navigation-heading": "Listahan sa Nabigasyon",
+       "variants": "Mga lain pa",
+       "navigation-heading": "Hihilngan nin paglibotlibot",
        "errorpagetitle": "Salâ",
        "returnto": "Magbalik sa $1.",
        "tagline": "Gikan sa {{SITENAME}}",
-       "help": "Katabangan",
+       "help": "Tabang",
        "search": "Maghanap",
        "searchbutton": "Maghanap",
        "go": "Dumani",
        "searcharticle": "Lakaw",
-       "history": "Historiya kan pahina",
-       "history_short": "Historiya",
+       "history": "Uusipon kan pahina",
+       "history_short": "Uusipon",
        "updatedmarker": "dinagdagan poon kan sakong huring pagbisita",
-       "printableversion": "Puwede maimprintang bersyon",
+       "printableversion": "Nalilimbag na bersyon",
        "permalink": "Permanenteng kilyawan",
-       "print": "Imprintaron",
-       "view": "Tanawon",
-       "view-foreign": "Hilngon sa $1",
-       "edit": "Liwatón",
+       "print": "Ilimbag",
+       "view": "Tànawon",
+       "view-foreign": "Hilingon sa $1",
+       "edit": "Liwaton",
        "edit-local": "Liwaton an lokal na deskripsyon",
        "create": "Muknaon",
        "create-local": "Idugang an lokal na deskripsyon",
-       "editthispage": "Liwata ining pahina",
+       "editthispage": "Liwaton ining pahina",
        "create-this-page": "Muknaon ining pahina",
        "delete": "Puraon",
        "deletethispage": "Puraon ining pahina",
        "undeletethispage": "Balikon sa pagkapura ining pahina",
-       "undelete_short": "Bawia an pagpurà kan {{PLURAL:$1|sarong pagliwat|$1 mga pagliwat}}",
-       "viewdeleted_short": "Hilngon {{PLURAL:$1|sarong pinara na pagliwat|$1 mga pinara na pagliwat}}",
+       "undelete_short": "Bawion an {{PLURAL:$1|sarong pagliwat|$1 mga pagliwat}}",
+       "viewdeleted_short": "Hilingon {{PLURAL:$1|sarong pinara na pagliwat|$1 mga pinara na pagliwat}}",
        "protect": "Protektari",
        "protect_change": "Ribayan",
        "protectthispage": "Protektaran ining pahina",
-       "unprotect": "Ribayi an proteksyon",
-       "unprotectthispage": "Ribayi an proteksyon kaining pahina",
-       "newpage": "Bàguhong pahina",
-       "talkpage": "Orolayan ining pahina",
-       "talkpagelinktext": "Olay",
-       "specialpage": "Espesyal na Pahina",
-       "personaltools": "Personal na mga kagamitan",
-       "postcomment": "Baguhong seksyon",
-       "articlepage": "Tanawon an laog kan pahina",
-       "talk": "Orolayan",
-       "views": "Mga Tanawon",
-       "toolbox": "Mga gagamiton:",
-       "userpage": "Tanawon an pahina kan parágamit",
-       "projectpage": "Tanawon an pahina kan proyekto",
-       "imagepage": "Hilngón an pahina nin sagunson (file)",
-       "mediawikipage": "Tanawon an pahina kan mensahe",
-       "templatepage": "Tanawon an pahina kan panguyog",
-       "viewhelppage": "Tanawon an pahina nin katabangan",
-       "categorypage": "Tanawon an pahina nin kategorya",
-       "viewtalkpage": "Tanawon an orolayan",
-       "otherlanguages": "Sa ibang mga lengguwahe",
-       "redirectedfrom": "(Redirektado gikan sa $1)",
-       "redirectpagesub": "Redirektang pahina",
+       "unprotect": "Ribayan an proteksyon",
+       "unprotectthispage": "Ribayan an proteksyon kaining pahina",
+       "newpage": "Bàgong pahina",
+       "talkpage": "Urulayan ining pahina",
+       "talkpagelinktext": "Mag-ulay",
+       "specialpage": "Sadyang Pahina",
+       "personaltools": "Pansadiring mga kagamitan",
+       "articlepage": "Tànawon an laog kan pahina",
+       "talk": "Urulay",
+       "views": "Mga tànaw",
+       "toolbox": "Mga gamit:",
+       "userpage": "Tànawon an pahina kan paragamit",
+       "projectpage": "Tànawon an pahina kan proyekto",
+       "imagepage": "Hilingon an pahina nin sagunson (file)",
+       "mediawikipage": "Tànawon an pahina kan mensahe",
+       "templatepage": "Tànawon an pahina kan panguyog",
+       "viewhelppage": "Tànawon an pahina nin pagtabang",
+       "categorypage": "Tànawon an pahina nin kategorya",
+       "viewtalkpage": "Tànawon an urulay",
+       "otherlanguages": "Sa ibang mga tataramon",
+       "redirectedfrom": "(Inilikay gikan sa $1)",
+       "redirectpagesub": "Likay na pahina",
        "lastmodifiedat": "Ining pahina huring pinagbago kan $1, mga alas $2.",
        "viewcount": "Ining pahina pinaglaog nin {{PLURAL:$1|sarong beses|nin $1 beses}}.",
        "protectedpage": "Protektadong pahina",
-       "jumpto": "Magluksó sa:",
-       "jumptonavigation": "nabigasyon",
+       "jumpto": "Maglukso sa:",
+       "jumptonavigation": "paglibotlibot",
        "jumptosearch": "hanapon",
        "view-pool-error": "Sori tabi, an mga server kargado sa oras na ini.\nGrabe kadakol an mga paragamit na pinagprubaran mahiling an pahinang ini.\nMakihalat tabi nin kadikit na panahon bago ka magprubara na makapaglaog sa pahinang ini.\n\n$1",
        "generic-pool-error": "Sori tabi, an mga serbidor grabe kakargado sa oras na ini. Kadakulon na gayo an mga paragamit na minaprubar na hilngon ining kaggikanan. Tabi pakihalat kadikit bago ka magprubar otro na makapaglaog sa kaggikanang ini.",
        "pool-queuefull": "An grupong panproseso panoon",
        "pool-errorunknown": "Bakong bistadong sala",
        "aboutsite": "Dapít sa {{SITENAME}}",
-       "aboutpage": "Project:Mapanonongód",
+       "aboutpage": "Project:Mapanonongod",
        "copyright": "An kalamnan manunumpungan sa laog kan $1 o baya notado na ining laen.",
        "copyrightpage": "{{ns:project}}:Mga Katanosang pansurat",
        "currentevents": "Sa ngunyan na mga pangyayari",
        "newmessageslinkplural": "{{PLURAL:$1|sarong baguhong mensahe|999=baguhong mga mensahe}}",
        "newmessagesdifflinkplural": "kahurihan na {{PLURAL:$1|kaliwatan|999=mga kaliwatan}}",
        "youhavenewmessagesmulti": "Igwa ka nin mga bàgong mensahe sa $1",
-       "editsection": "liwatón",
-       "editold": "Liwatón",
-       "viewsourceold": "hilingón an ginikánan",
-       "editlink": "liwatón",
-       "viewsourcelink": "tanawon an ginikanan",
-       "editsectionhint": "Liwatón an seksyon: $1",
-       "toc": "Mga laóg",
-       "showtoc": "ipahilíng",
-       "hidetoc": "tagóon",
-       "collapsible-collapse": "Pinahalipot",
-       "collapsible-expand": "Pinahalawig",
-       "thisisdeleted": "Hilingón o isulít an $1?",
-       "viewdeleted": "Hilingón an $1?",
+       "editsection": "liwaton",
+       "editold": "liwaton",
+       "viewsourceold": "hilingon an ginikanan",
+       "editlink": "liwaton",
+       "viewsourcelink": "tànawon an ginikanan",
+       "editsectionhint": "Liwaton an seksyon: $1",
+       "toc": "Mga laog",
+       "showtoc": "ipahiling",
+       "hidetoc": "tagoon",
+       "collapsible-collapse": "Ibagsak",
+       "collapsible-expand": "Ibuka",
+       "thisisdeleted": "Hilingon o isulit an $1?",
+       "viewdeleted": "Hilingon an $1?",
        "restorelink": "{{PLURAL:$1|sarong pinagpurang pagliwat|$1 na pinagpurang mga pagliwat}}",
        "feedlinks": "Hungit:",
        "feed-invalid": "Imbalidong tipo nin hungit sa subkripsyon.",
        "filereadonlyerror": "Dae kinayang baguhon an sagunson (file) \"$1$ nin huli ta an repositoryo kan sagunson \"$2\" yaon sa kamugtakan na basahon sana.\n\nAn administrador na iyo an nagkandado kaini nagpahayag kaining kapaliwanagan: \"$3\".",
        "invalidtitle-knownnamespace": "Imbalidong titulo na igwang espasyadong ngaran na \"$2\" asin teksto na \"$3\"",
        "invalidtitle-unknownnamespace": "Imbalidong titulo na igwang nin bakong bistado na bilang kan espasyadong ngaran na $1 asin teksto na \"$2\"",
-       "exception-nologin": "Dae ka nakalaog",
+       "exception-nologin": "Dai ka nakalaog",
        "exception-nologin-text": "Tabi man [[Special:Userlogin|maglaog]]na tanganing makalangkay sa pahinang ini o aksyon.",
        "exception-nologin-text-manual": "Tabi man $1 na tanganing makalangkay sa pahinang ini o aksyon.",
        "virus-badscanner": "Raot na kasalansanan: Bakong bistadong virus scanner: ''$1''",
        "userlogin-yourname": "Paragamit-na-Ngaran",
        "userlogin-yourname-ph": "Ikaag an saimong paragamit-na-ngaran",
        "createacct-another-username-ph": "Ikaag an paragamit-na-ngaran",
-       "yourpassword": "Pasa-taramon:",
-       "userlogin-yourpassword": "Sikretong panlaog",
+       "yourpassword": "Sekretong Panlaog",
+       "userlogin-yourpassword": "Sekretong Panlaog",
        "userlogin-yourpassword-ph": "Ikaag an saimong sekretong panlaog",
        "createacct-yourpassword-ph": "Ikaag an sekretong panlaog",
        "yourpasswordagain": "Pakilaog giraray kan sekretong panlaog:",
        "externaldberror": "Igwa gayod sala sa arinman kan patunay sa datos-sarayan o ika dae pinagtugutan na bâgohon an saimong panluwas na panindog.",
        "login": "Maglaog",
        "nav-login-createaccount": "Maglaog / magmukna nin panindog",
-       "loginprompt": "Ika kaipong paganahon an mga cookies tanganing makalaog sa {{SITENAME}}.",
        "userlogin": "Maglaog / magmukna nin panindog",
        "userloginnocreate": "Maglaog ka",
        "logout": "Magluwas",
-       "userlogout": "Magluwás",
+       "userlogout": "Magluwas",
        "notloggedin": "Dae ka nakalaog",
        "userlogin-noaccount": "Mayo ka nin panindog?",
        "userlogin-joinproject": "Mag-ayon{{SITENAME}}",
        "resettokens-resetbutton": "Pakibaguha an pinagpiling mga paduos",
        "bold_sample": "Mahìbog na teksto",
        "bold_tip": "Mahìbog na teksto",
-       "italic_sample": "Itálikong teksto",
-       "italic_tip": "Tekstong Itáliko",
+       "italic_sample": "Italikong teksto",
+       "italic_tip": "Tekstong Italiko",
        "link_sample": "Titulo nin sugpon",
        "link_tip": "Panlaog na sugpon",
        "extlink_sample": "http://www.example.com títulong sugpon",
        "editpage-notsupportedcontentformat-text": "An pormat nin kalamnan na $1 bakong suportado kan modelong kalamnan na $2.",
        "content-model-wikitext": "wiki-teksto",
        "content-model-text": "yanong-teksto",
-       "content-model-javascript": "Java-Kurit",
+       "content-model-javascript": "JavaScript",
        "content-model-css": "CSS",
        "expensive-parserfunction-warning": "'''Patanid tabi:''' Ining pahina naglalaman nin grabe kadakulon na ekspensibong programang pambaranga sa punksyon nin mga pag-aapod.\n\nIni dapat magkaigwa nin menos sanang $2 {{PLURAL:$2|apod|mga apod}}, igwa na {{PLURAL:$1|ngunyan nin $1 apod|ngunyan nin $1 mga apod}}.",
        "expensive-parserfunction-category": "Mga pahina na igwa nin grabe kadakulon na mga ekspensibong programang pambaranga sa punksyon nin mga pag-aapod",
        "cantcreateaccount-text": "An pagbukas nin account halì sa IP na ('''$1''') binágat ni [[User:$3|$3]].\n\n''$2'' an rason na pigtao ni $3",
        "viewpagelogs": "\nHilingon an mga katalaanan para sa pahinang ini",
        "nohistory": "Mayong paghirá nin uusipón sa pahinang ini.",
-       "currentrev": "Sa ngonyan na pagpakarháy",
-       "currentrev-asof": "Pinakahuring pagbabago kan $1",
-       "revisionasof": "Rebisyon poon kan $1",
+       "currentrev": "Ppagpakarhay sa ngunyan",
+       "currentrev-asof": "Pinakahuring pagpakarhay kan $1",
+       "revisionasof": "Pagpakarhay poon kan $1",
        "revision-info": "Rebisyon poon kan $1 ni $2",
-       "previousrevision": "←Lumàon na rebisyon",
-       "nextrevision": "Mas bàguhon na rebisyon→",
+       "previousrevision": "← Dating pagpakarhay",
+       "nextrevision": "Bagong pagpakarhay →",
        "currentrevisionlink": "Sa ngunyan na rebisyon",
-       "cur": "sa ngunyán",
+       "cur": "sa ngunyan",
        "next": "sunod",
-       "last": "sa nakaagi",
-       "page_first": "enot",
+       "last": "dati",
+       "page_first": "inot",
        "page_last": "huri",
-       "histlegend": "Kalaenan sa pilian: Markahan an mga kahon nin radyo kan mga rebisyon tanganing komapararon asin pinduta an \"enter\" o an pindutan na yaon sa irarom.<br />\nKabalaynan: '''({{int:cur}})''' = kalaenan sa pinakahuring rebisyon, '''({{int:last}})''' = kalaenan sa sinundan na rebisyon, '''{{int:minoreditletter}}''' = dikiton na pagliwat.",
-       "history-fieldset-title": "Historiya nin kinilyawan",
+       "histlegend": "Lain sa pilian: Markahan an mga kahon nin radyo kan mga pagpakarhay tanganing komapararon asin pinduta an \"enter\" o an pindutan na yaon sa irarom.<br />\nKabalaynan: '''({{int:cur}})''' = kalaenan sa pinakahuring rebisyon, '''({{int:last}})''' = kalaenan sa sinundan na rebisyon, '''{{int:minoreditletter}}''' = dikiton na pagliwat.",
+       "history-fieldset-title": "Uusipon nin paghalungkat",
        "history-show-deleted": "Pinagpura sana",
        "histfirst": "pinakalumaon",
        "histlast": "pinakabaguhon",
        "historysize": "({{PLURAL:$1|sarong byte|$1 mga bytes}})",
        "historyempty": "(mayong laog)",
-       "history-feed-title": "Uusipón kan pagpakaraháy",
-       "history-feed-description": "Uusipón kan pagpakaraháy para sa pahinang ini sa wiki",
+       "history-feed-title": "Uusipon kan pagpakarhay",
+       "history-feed-description": "Uusipon kan pagpakarahay para sa pahinang ini sa wiki",
        "history-feed-item-nocomment": "$1 sa $2",
-       "history-feed-empty": "Mayò man an hinágad na pahina.\nPwedeng pigparà na ini sa wiki, o tinàwan nin bàgong pangaran.\nProbaran tabì an [[Special:Search|pighahanap sa wiki]] para sa mga pahinang dapít.",
+       "history-feed-empty": "Mayò man an hinagad na pahina.\nPwedeng pigparà na ini sa wiki, o tinàwan nin bàgong pangaran.\nProbaran tabì an [[Special:Search|pighahanap sa wiki]] para sa mga pahinang dapít.",
        "rev-deleted-comment": "(pagliwat na sumaryo pinaghale)",
        "rev-deleted-user": "(hinalì an parágamit)",
        "rev-deleted-event": "(talaan kan aksyon pinaghale)",
index f49c47e..b35aa23 100644 (file)
        "talkpagelinktext": "гутаркі",
        "specialpage": "Спэцыяльная старонка",
        "personaltools": "Асабістыя прылады",
-       "postcomment": "Новая сэкцыя",
        "articlepage": "Паказаць старонку зьместу",
        "talk": "Абмеркаваньне",
        "views": "Рэжымы",
        "externaldberror": "Адбылася памылка аўтэнтыфікацыі з дапамогай вонкавай базы зьвестак, ці Вам не дазволена абнаўляць свой рахунак.",
        "login": "Увайсьці",
        "nav-login-createaccount": "Уваход / стварэньне рахунку",
-       "loginprompt": "Вы павінны дазволіць cookie для ўваходу ў {{GRAMMAR:вінавальны|{{SITENAME}}}}.",
        "userlogin": "Увайсьці ў сыстэму",
        "userloginnocreate": "Увайсьці",
        "logout": "Выйсьці",
        "license-nopreview": "(Прагляд недаступны)",
        "upload_source_url": " (слушны, агульнадаступны URL-адрас)",
        "upload_source_file": " (файл на Вашым кампутары)",
+       "listfiles-delete": "выдаліць",
        "listfiles-summary": "На гэтай спэцыяльнай старонцы паказаныя ўсе загружаныя файлы.",
        "listfiles_search_for": "Пошук па назьве файла:",
        "imgfile": "файл",
        "wantedpages-badtitle": "Няслушная назва сярод вынікаў: $1",
        "wantedfiles": "Запатрабаваныя файлы",
        "wantedfiletext-cat": "Наступныя файлы выкарыстоўваюцца, але іх няма. Файлы са зьнешніх сховішчаў могуць знаходзіцца ў сьпісе без уліку іх існаваньня. Любыя такія няслушныя ўваходжаньні будуць <del>выкрасьленыя</del>. Дадаткова, старонкі, якія ўбудоўваюць неіснуючыя файлы прыведзеныя на [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Наступныя файлы ўжваюцца, але не існуюць. Дадаткова, старонкі, у якія ўключаныя няісныя файлы, прыведзеныя ў [[:$1]].",
        "wantedfiletext-nocat": "Наступныя файлы выкарыстоўваюцца, але іх няма. Файлы са зьнешніх сховішчаў могуць знаходзіцца ў сьпісе без уліку іх існаваньня. Любыя такія няслушныя ўваходжаньні будуць <del>выкрасьленыя</del>.",
+       "wantedfiletext-nocat-noforeign": "Наступныя файлы выкарыстоўваюцца, але іх няма.",
        "wantedtemplates": "Запатрабаваныя шаблёны",
        "mostlinked": "Старонкі, на якія найчасьцей спасылаюцца",
        "mostlinkedcategories": "Катэгорыі з найбольшай колькасьцю старонак",
        "timezone-utc": "UTC",
        "unknown_extension_tag": "Невядомы тэг пашырэньня «$1»",
        "duplicate-defaultsort": "Папярэджаньне: Ключ сартыроўкі па змоўчваньні «$2» замяняе папярэдні ключ сартыроўкі па змоўчваньні «$1».",
+       "duplicate-displaytitle": "<strong>Папярэджаньне:</strong> назва для адлюстраваньня «$2» перапісвае ранейшую назву для адлюстраваньня «$1».",
        "version": "Вэрсія",
        "version-extensions": "Усталяваныя пашырэньні",
        "version-skins": "Усталяваныя тэмы афармленьня",
index 58022e7..1d81b60 100644 (file)
        "talkpagelinktext": "Размовы",
        "specialpage": "Адмысловая старонка",
        "personaltools": "Асабістыя прылады",
-       "postcomment": "Новы раздзел",
        "articlepage": "Паказаць старонку змесціва",
        "talk": "Размовы",
        "views": "Віды",
        "externaldberror": "Або памылка вонкавай аўтэнтыкацыі ў базе дадзеных, або вам не дазволена абнаўляць свой вонкавы рахунак.",
        "login": "Увайсці ў сістэму",
        "nav-login-createaccount": "Увайсці ў сістэму / стварыць рахунак",
-       "loginprompt": "Каб уваходзіць у сістэму {{SITENAME}}, трэба дазволіць у браўзеры квіткі (кукі).",
        "userlogin": "Увайсці ў сістэму / стварыць рахунак",
        "userloginnocreate": "Увайсці",
        "logout": "Выйсці з сістэмы",
        "changeemail-submit": "Змяніць адрас электроннай пошты:",
        "changeemail-cancel": "Адмена",
        "changeemail-throttled": "Надта штмат спробаў увайсці пад гэтым рахункам. Пачакайце $1 перад тым, як спрабаваць ізноў.",
+       "resettokens": "Скінуць токены",
+       "resettokens-text": "Вы можаце пераўстанавіць токены, якія дазваляюць атрымліваць доступ да пэўных прыватных звестак, звязаных з вашым уліковым запісам.\n\nВы мусіце скінуць токены, калі выпадкова падзяліліся імі з кім-небудзь, ці ваш уліковы запіс быў скампраметаваны.",
+       "resettokens-no-tokens": "Няма чаго скідваць.",
+       "resettokens-legend": "Скідванне токенаў",
+       "resettokens-tokens": "Токены:",
        "resettokens-token-label": "$1 (актуальнае значэнне: $2)",
+       "resettokens-watchlist-token": "Токен струменя (Atom/RSS) [[Special:Watchlist|зменаў старонак у вашым спісе назірання]]",
+       "resettokens-done": "Токены скінуты.",
+       "resettokens-resetbutton": "Скінуць выбраныя токены",
        "bold_sample": "Цёмны тэкст",
        "bold_tip": "Цёмны тэкст",
        "italic_sample": "Курсіўны тэкст",
        "right-userrights-interwiki": "Правіць дазволы ўдзельнікаў на іншых вікі",
        "right-siteadmin": "Замыкаць і адмыкаць базу даных",
        "right-override-export-depth": "Экспартаваць старонкі, у тым ліку звязаныя, да глыбіні спасылак 5.",
-       "right-sendemail": "Адправіць па электроннай пошце іншым карыстальнікам",
-       "right-passwordreset": "пÑ\80аглÑ\8fд Ñ\8dлекÑ\82Ñ\80оннÑ\8bÑ\85 Ð»Ñ\96Ñ\81Ñ\82оÑ\9e Ñ\81а Ð·Ð¼Ñ\8fненнем пароля",
+       "right-sendemail": "Адпраўляць электронныя лісты іншым удзельнікам",
+       "right-passwordreset": "Ð\91аÑ\87Ñ\8bÑ\86Ñ\8c Ñ\8dлекÑ\82Ñ\80оннÑ\8bÑ\8f Ð»Ñ\96Ñ\81Ñ\82Ñ\8b Ð°Ð± Ð·Ð¼Ñ\8fненнÑ\96 пароля",
        "newuserlogpage": "Журнал рэгістрацыі ўдзельнікаў",
        "newuserlogpagetext": "Гэта журнал рэгістрацыі новых удзельнікаў.",
        "rightslog": "Журнал правоў удзельнікаў",
        "wantedpages-badtitle": "Недапушчальная назва ў выніках: $1",
        "wantedfiles": "Патрэбныя файлы",
        "wantedfiletext-cat": "Наступныя файлы выкарыстоўваюцца, але іх няма. Файлы са знешніх сховішчаў могуць знаходзіцца ў спісе без уліку іх існавання. Любыя такія няслушныя ўваходжанні будуць <del>выкрасленыя</del>. Дадаткова, старонкі, якія ўбудоўваюць неіснуючыя файлы прыведзеныя на [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Наступныя файлы выкарыстоўваюцца, хаця іх няма. Дадаткова, старонкі, што ўключаюць файлы, каторых няма, пералічаны ў [[:$1]].",
        "wantedfiletext-nocat": "Наступныя файлы выкарыстоўваюцца, але іх няма. Файлы са знешніх сховішчаў могуць знаходзіцца ў спісе без уліку іх існавання. Любыя такія няслушныя ўваходжанні будуць <del>выкрасленыя</del>.",
+       "wantedfiletext-nocat-noforeign": "Наступныя файлы выкарыстоўваюцца, хоць іх няма.",
        "wantedtemplates": "Патрэбныя шаблоны",
        "mostlinked": "Старонкі, на якія найчасцей спасылаюцца",
        "mostlinkedcategories": "Катэгорыі з найбольшай колькасцю складнікаў",
        "limitreport-title": "Звесткі прафілявання парсера:",
        "limitreport-cputime": "Выкарыстанне часу ЦП",
        "limitreport-cputime-value": "$1 {{PLURAL:$1|секунда|секунды|секундаў}}",
+       "limitreport-walltime": "Выкарыстанне рэальнага часу",
        "limitreport-walltime-value": "$1 {{PLURAL:$1|секунда|секунды|секундаў}}",
        "limitreport-ppvisitednodes": "Колькасць вузлоў, наведаных прэпрацэсарам",
        "limitreport-ppgeneratednodes": "Колькасць вузлоў, створаных прэпрацэсарам",
index bd3d464..8767a1b 100644 (file)
        "talkpagelinktext": "बात-चीत",
        "specialpage": "ख़ाश पन्ना",
        "personaltools": "ब्यक्तिगत औजार",
-       "postcomment": "नया खण्ड",
        "articlepage": "सामग्री पन्ना देखीं",
        "talk": "बात-चीत",
        "views": "विचारसूची",
        "externaldberror": "या त प्रमाणिकरण डाटाबेस में भइल बा या फिर रउआ के आपन बाह्य खाता अपडेट करे के अनुमति नइखे।",
        "login": "खाता में प्रवेश",
        "nav-login-createaccount": "खाता प्रवेश / खाता बनाईं",
-       "loginprompt": "{{SITENAME}} में प्रवेश खातिर राउर कुकिज चालू होवे के चाहीं",
        "userlogin": "खाता प्रवेश / खाता बनाईं",
        "userloginnocreate": "खाता में प्रवेश",
        "logout": "खाता से बाहर",
        "resetpass-temp-password": "अस्थायी गुप्तशब्द:",
        "resetpass-abort-generic": "कउनो एक्सटेंशन द्वारा गुप्तशब्द में बदलाव रोक दिहल गईल बा।",
        "resetpass-expired": "राउर पासवर्ड की वैधता अवधि समाप्त हो चुकल बा। कृपया लॉग इन करे खातिर एगो नया पासवर्ड सेट करीं।",
+       "resetpass-validity-soft": "राउर पासवर्ड मान्य नईखे: $1 \n\nकृपया अब एक नया पासवर्ड चुनीं, या उ के बाद में पुनर्स्थापित करे खातिर \"{{int:resetpass-submit-cancel}}\" पर क्लिक करीं।",
        "passwordreset": "गुप्तशब्द रिसेट करीं",
        "passwordreset-text-one": "आपन गुप्तशब्द के पुनर्स्थापित करे खातिर इ फॉर्म भरीं।",
-       "passwordreset-text-many": "{{PLURAL:$1|à¤\86पन à¤\97à¥\81पà¥\8dतशबà¥\8dद à¤ªà¥\81नरà¥\8dसà¥\8dथापित à¤\95रे खातिर निम्न में से कउनो एगो स्थान भरीं।}}",
+       "passwordreset-text-many": "{{PLURAL:$1|à¤\88मà¥\87ल à¤¦à¥\8dवारा à¤\85सà¥\8dथाà¤\88 à¤ªà¤¾à¤¸à¤µà¤°à¥\8dड à¤ªà¤¾à¤µे खातिर निम्न में से कउनो एगो स्थान भरीं।}}",
        "passwordreset-legend": "गुप्तशब्द रिसेट करीं",
        "passwordreset-disabled": "इ विकी पर पासवर्ड पुनर्स्थापन अक्षम बा।",
        "passwordreset-emaildisabled": "इ विकि पर ई-मेल सुविधा अक्षम कर दिहल गईल बा।",
        "changeemail-password": "राउर {{SITENAME}} गुप्तशब्द:",
        "changeemail-submit": "ई-मेल बदलीं",
        "changeemail-cancel": "रद्द करीं",
+       "changeemail-throttled": "रउआ हाले में कईयन बार खाता में प्रवेश करे के कोशिश कर चुकल बानी।\nकृपया $1 प्रतिक्षा करला के बाद फिर से प्रयास करब।",
+       "resettokens": "टोकन रीसेट करीं",
+       "resettokens-text": "जौन टोकन राउर खाता से सम्बद्ध कुछ विशिष्ट व्यक्तिगत जानकारी प्रदान करेला, आप उ के अहिजा रीसेट कर सकत बानी।\n\nयदि रउआ ई के गलती से केहू के दिखा देले बानी या फिर राउर खाता हैक हो गईल बा त रउआ ई के रीसेट कर देवे के चाहीं।",
        "bold_sample": "मोट पाठ्य",
        "bold_tip": "मोट पाठ्य",
        "italic_sample": "इटालिक पाठ्य",
index 434cf28..b4be21a 100644 (file)
        "talkpagelinktext": "আলোচনা",
        "specialpage": "বিশেষ পাতা",
        "personaltools": "নিজস্ব সরঞ্জামসমূহ",
-       "postcomment": "নতুন অনুচ্ছেদ",
        "articlepage": "নিবন্ধ দেখুন",
        "talk": "আলোচনা",
        "views": "দৃষ্টিকোণ",
        "externaldberror": "হয় কোন বহিঃস্থ যাচাইকরণ ডাটাবেজ ত্রুটি ঘটেছে অথবা আপনার বহিঃস্থ অ্যাকাউন্ট হালনাগাদ করার অনুমতি নেই।",
        "login": "প্রবেশ",
        "nav-login-createaccount": "প্রবেশ/নতুন অ্যাকাউন্ট",
-       "loginprompt": "{{SITENAME}}-তে প্রবেশ করতে হলে আপনার ব্রাউজারের কুকি অবশ্যই সক্রিয় করতে হবে।",
        "userlogin": "প্রবেশ/নতুন অ্যাকাউন্ট",
        "userloginnocreate": "প্রবেশ",
        "logout": "প্রস্থান করুন",
        "license-nopreview": "(প্রাকদর্শন লভ্য নয়)",
        "upload_source_url": " (একটি বৈধ, উন্মুক্ত URL)",
        "upload_source_file": " (আপনার কম্পিউটারের একটি ফাইল)",
+       "listfiles-delete": "অপসারণ",
        "listfiles-summary": "এই বিশেষ পাতাটি আপলোড করা সকল ফাইল প্রদর্শন করে।",
        "listfiles_search_for": "ছবির নাম অনুসন্ধান:",
        "imgfile": "ফাইল",
index 0c30961..8ec8bb8 100644 (file)
        "talkpagelinktext": "Razgovor",
        "specialpage": "Posebna Stranica",
        "personaltools": "Lični alati",
-       "postcomment": "Nova sekcija",
        "articlepage": "Pogledaj članak",
        "talk": "Razgovor",
        "views": "Pregledi",
        "unexpected": "Neočekivana vrijednost: \"$1\"=\"$2\".",
        "formerror": "Greška: ne može se poslati upitnik",
        "badarticleerror": "Ova akcija ne može biti izvršena na ovoj stranici.",
-       "cannotdelete": "Ne može se obrisati stranica ili datoteka \"$1\".\nMoguće je da ju je neko drugi već obrisao.",
+       "cannotdelete": "Ne može se obrisati stranica ili datoteka \"$1\".\nMoguće je da ju je neko već obrisao.",
        "cannotdelete-title": "Ne mogu izbrisati stranicu \"$1\"",
        "delete-hook-aborted": "Brisanje je prekinuo softverski priključak.\nNije ponuđeno nikakvo objašnjenje.",
        "badtitle": "Loš naslov",
        "externaldberror": "Došlo je do greške pri vanjskoj autorizaciji baze podataka ili vam nije dopušteno osvježavanje Vašeg vanjskog korisničkog računa.",
        "login": "Prijavi se",
        "nav-login-createaccount": "Prijavi se / Registruj se",
-       "loginprompt": "Morate imati kolačiće ('''cookies''') omogućene da biste se prijavili na {{SITENAME}}.",
        "userlogin": "Prijavi se / Registruj se",
        "userloginnocreate": "Prijavi se",
        "logout": "Odjavi me",
        "editingsection": "Uređujete $1 (dio)",
        "editingcomment": "Uređujete $1 (nova sekcija)",
        "editconflict": "Sukobljenje izmjene: $1",
-       "explainconflict": "Neko drugi je promjenio ovu stranicu otkad ste Vi počeli da je mjenjate.\nGornje tekstualno polje sadrži tekst stranice koji trenutno postoji.\nVaše izmjene su prikazane u donjem tekstu.\nMoraćete da unesete svoje promjene u postojeći tekst.\n'''Samo''' tekst u gornjem tekstualnom polju će biti snimljen kad\npritisnete \"{{int:savearticle}}\".",
+       "explainconflict": "Neko drugi je promijenio ovu stranicu otkad ste je Vi počeli mijenjati.\nGornje tekstualno polje sadrži tekst stranice koji trenutno postoji.\nVaše izmjene prikazane su u donjem tekstu.\nMorat ćete unijeti svoje promjene u postojeći tekst.\n'''Samo''' tekst u gornjem tekstualnom polju bit će sačuvan kad\nkliknete \"{{int:savearticle}}\".",
        "yourtext": "Vaš tekst",
        "storedversion": "Uskladištena verzija",
        "nonunicodebrowser": "'''UPOZORENJE: Vaš preglednik ne podržava Unicode zapis znakova.\nMolimo Vas promijenite ga prije sljedećeg uređivanja članaka. Znakovi koji nisu po ASCII standardu će se u prozoru za izmjene pojaviti kao heksadecimalni kodovi.'''",
        "rollbacklinkcount-morethan": "vrati više od $1 {{PLURAL:$1|izmjene|izmjene|izmjena}}",
        "rollbackfailed": "Vraćanje nije uspjelo",
        "cantrollback": "Ne može se vratiti izmjena; posljednji autor je ujedno i jedini.",
-       "alreadyrolled": "Ne može se vratiti posljednja izmjena [[:$1]] od korisnika [[User:$2|$2]] ([[User talk:$2|razgovor]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]); neko drugi je već izmjenio ili vratio članak.\n\nPosljednja izmjena je bila od korisnika [[User:$3|$3]] ([[User talk:$3|razgovor]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
+       "alreadyrolled": "Ne može se vratiti posljednja izmjena [[:$1]] od korisnika [[User:$2|$2]] ([[User talk:$2|razgovor]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]); neko je već izmijenio ili vratio članak na prethodnu provjerenu verziju.\n\nPosljednju izmjenu napravio je korisnik [[User:$3|$3]] ([[User talk:$3|razgovor]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
        "editcomment": "Sažetak izmjene je bio: \"''$1''\".",
        "revertpage": "Vraćene izmjene korisnika [[Special:Contributions/$2|$2]] ([[User talk:$2|razgovor]]) na posljednju izmjenu koju je napravio [[User:$1|$1]]",
        "revertpage-nouser": "Vraćene izmjene skrivenog korisnika na posljednju reviziju, koju je {{GENDER:$1|napravio|napravila}} [[User:$1|$1]]",
        "anonymous": "{{PLURAL:$1|Anonimni korisnik|$1 anonimna korisnika|$1 anonimnih korisnika}} projekta {{SITENAME}}",
        "siteuser": "{{SITENAME}} korisnik $1",
        "anonuser": "{{SITENAME}} anonimni korisnik $1",
-       "lastmodifiedatby": "Ovu stranicu je posljednji put promjenio $3, u $2, $1",
+       "lastmodifiedatby": "Ovu stranicu posljednji je put promijenio $3, u $2, $1",
        "othercontribs": "Bazirano na radu od strane korisnika $1.",
        "others": "ostali",
        "siteusers": "{{SITENAME}} {{PLURAL:$2|korisnik|korisnika}} $1",
index a01a3c1..25ce027 100644 (file)
        "talkpagelinktext": "Discussió",
        "specialpage": "Pàgina especial",
        "personaltools": "Eines de l'usuari",
-       "postcomment": "Nova secció",
        "articlepage": "Mostra la pàgina",
        "talk": "Discussió",
        "views": "Vistes",
        "externaldberror": "Hi ha hagut una fallida en el servidor d'autenticació externa de la base de dades i no teniu permís per a actualitzar el vostre compte d'accès extern.",
        "login": "Inici de sessió",
        "nav-login-createaccount": "Inicia una sessió / crea un compte",
-       "loginprompt": "Heu de tenir les galetes habilitades per a poder iniciar una sessió a {{SITENAME}}.",
        "userlogin": "Inicia una sessió / crea un compte",
        "userloginnocreate": "Inici de sessió",
        "logout": "Finalitza la sessió",
        "currentrev": "Revisió actual",
        "currentrev-asof": "Revisió de $1",
        "revisionasof": "Revisió de $1",
-       "revision-info": "Revisió de $1; $2",
+       "revision-info": "La revisió el $1 per {{GENDER:$6|$2}}$7",
        "previousrevision": "←Versió més antiga",
        "nextrevision": "Versió més nova→",
        "currentrevisionlink": "Versió actual",
        "mergehistory-empty": "No pot fusionar-se cap revisió.",
        "mergehistory-success": "$3 {{PLURAL:$3|revisió|revisions}} de [[:$1]] s'han fusionat amb èxit a [[:$2]].",
        "mergehistory-fail": "No s'ha pogut realitzar la fusió de l'historial, comproveu la pàgina i els paràmetres horaris.",
+       "mergehistory-fail-toobig": "No s'ha pogut realitzar la fusió de l'historial perquè es mourien més del limit de $1 {{PLURAL:$1|revisió|revisions}}.",
        "mergehistory-no-source": "La pàgina d'origen $1 no existeix.",
        "mergehistory-no-destination": "La pàgina de destinació $1 no existeix.",
        "mergehistory-invalid-source": "La pàgina d'origen ha de tenir un títol vàlid.",
        "largefileserver": "Aquest fitxer és més gran del que el servidor permet.",
        "emptyfile": "El fitxer que heu carregat sembla estar buit.\nAçò por ser degut a un mal caràcter en el nom del fitxer.\nComproveu si realment voleu carregar aquest fitxer.",
        "windows-nonascii-filename": "Aquest wiki no permet noms de fitxer amb caràcters especials.",
-       "fileexists": "Ja hi existeix un fitxer amb aquest nom, si us plau, verifiqueu <strong>[[:$1]]</strong> si no esteu segurs de voler substituir-lo.\n[[$1|thumb]]",
+       "fileexists": "Ja existeix un fitxer amb aquest nom. Comproveu <strong>[[:$1]]</strong> si no esteu {{GENDER:|segur|segura}} de voler substituir-lo.\n[[$1|thumb]]",
        "filepageexists": "La pàgina de descripció d'aquest fitxer ja ha estat creada (<strong>[[:$1]]</strong>), però de moment no hi ha cap fitxer amb aquest nom. La descripció que heu posat no apareixerà a la pàgina de descripció. Si voleu que hi aparegui haureu d'editar-la manualment.\n[[$1|thumb]]",
        "fileexists-extension": "Ja existeix un fitxer amb un nom semblant: [[$2|thumb]]\n* Nom del fitxer que es puja: <strong>[[:$1]]</strong>\n* Nom del fitxer existent: <strong>[[:$2]]</strong>\nPotser voleu fer servir un nom més fàcil de distingir?",
        "fileexists-thumbnail-yes": "Aquest fitxer sembla ser una imatge en mida reduïda (<em>miniatura</em>). [[$1|thumb]]\nComproveu si us plau el fitxer <strong>[[:$1]]</strong>.\nSi el fitxer és la mateixa imatge a mida original, no cal carregar cap miniatura més.",
        "license-nopreview": "(Previsualització no disponible)",
        "upload_source_url": " (un URL vàlid i accessible públicament)",
        "upload_source_file": " (un fitxer en el vostre ordinador)",
+       "listfiles-delete": "elimina",
        "listfiles-summary": "Aquesta pàgina especial mostra tots els fitxers carregats.\nSi filtreu per usuari només es mostraran els fitxers la versió més recent dels quals hagi estat carregada per aquell.",
        "listfiles_search_for": "Cerca el nom d'un fitxer de medis:",
        "imgfile": "fitxer",
        "wantedpages-badtitle": "Títol invàlid al conjunt de resultats: $1",
        "wantedfiles": "Fitxers demanats",
        "wantedfiletext-cat": "Els fitxers següents s'utilitzen per no existeixen. Els fitxers de repositoris aliens poden ser llistats encara que existeixin. Aquells que siguin fals positius es <del>tatxaran</del>. A més, les pàgines que tinguin fitxers incrustats que no existeixin es llistaran a [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Els fitxers següents s'utilitzen, però no existeixen. Addicionalment, s'enumeren a [[:$1]] les pàgines que tenen fitxers inserits que no existeixen.",
        "wantedfiletext-nocat": "Els fitxers següents es fan servir però no existeixen. Els fitxers d'un repositori aliè poden ser llistats encara que existeixin. Tots aquells fals positius es <del>tatxaran</del>.",
+       "wantedfiletext-nocat-noforeign": "Els fitxers següents s'utilitzen però no existeixen.",
        "wantedtemplates": "Plantilles demanades",
        "mostlinked": "Pàgines més enllaçades",
        "mostlinkedcategories": "Categories més utilitzades",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|discussió]])",
        "unknown_extension_tag": "Etiqueta d'extensió desconeguda «$1»",
        "duplicate-defaultsort": "Atenció: La clau d'ordenació per defecte \"$2\" invalida l'anterior clau \"$1\".",
+       "duplicate-displaytitle": "<strong>Avís:</strong> El títol a mostrar «$2» sobreescriu l'anterior títol a mostrar «$1».",
        "version": "Versió",
        "version-extensions": "Extensions instaŀlades",
-       "version-skins": "Aparences",
+       "version-skins": "Temes instal·lats",
        "version-specialpages": "Pàgines especials",
        "version-parserhooks": "Extensions de l'analitzador",
        "version-variables": "Variables",
        "pagelang-use-default": "Utilitza l'idioma per defecte",
        "pagelang-select-lang": "Selecciona un idioma",
        "right-pagelang": "Canvia l'idioma de la pàgina",
-       "action-pagelang": "canvia l'idioma de la pàgina"
+       "action-pagelang": "canvia l'idioma de la pàgina",
+       "log-name-pagelang": "Canvia el registre de llengua",
+       "log-description-pagelang": "Aquest és un registre dels canvis en les llengües de les pàgines.",
+       "logentry-pagelang-pagelang": "$1 {{GENDER:$2|ha canviat}} la llengua de la pàgina per a $3 de $4 a $5."
 }
index 2a13114..1f6960d 100644 (file)
        "talkpagelinktext": "Дийцаре",
        "specialpage": "Белха агӀо",
        "personaltools": "Долахь болу гӀирсаш",
-       "postcomment": "Керла дакъа",
        "articlepage": "Хьажа яззаме",
        "talk": "Дийцаре",
        "views": "Хьажарш",
        "externaldberror": "Арахьара хаамийн базан гӀоьнца аутентификаци ечу хенахь гӀалат даьлла я хьа дӀаяздаран хийцам бан бакъонаш яц.",
        "login": "Системин чугӀо",
        "nav-login-createaccount": "Системин чугӀо / дӀаяздар кхолла",
-       "loginprompt": "Ахьа бакъо яла еза оцу «cookies» хьайна системин чохь болхбан лаахь.",
        "userlogin": "Довзийтар я декъашхочун дӀаяздар кхоллар",
        "userloginnocreate": "Довзийта",
        "logout": "Болх дӀаберзор",
        "passwordreset-emailelement": "Декъашхочун цӀе: $1\nХанна йолу пароль: $2",
        "passwordreset-emailsent": "Электронан хаам баийтина кхоьссинчу паролах лаьцна хаам чохь болуш.",
        "passwordreset-emailsent-capture": "Электронан хаам баийтина кхоьссинчу паролах лаьцна хаам чохь болуш. \nцуна йозане хьажа йиш ю лахахь.",
+       "passwordreset-emailerror-capture": "Пароль кхоссаран хаам чохь болуш электронан кехат кхоьллина, цуна йоза хьажа йиш ю лахахь, амма иза {{GENDER:$2|декъашхочунга}} дӀадахьийта тар цаделира бахьнехь: $1",
        "changeemail": "Хийца электронан почта",
        "changeemail-header": "Электронан почтан адрес хийцар",
        "changeemail-text": "Юза хӀара форма хьайн электронан почтан адрес хуьйцуш. Ахьа хийцар бакъдан язъян еза пароль.",
        "nolicense": "Яц",
        "license-nopreview": "(Хьалха муха ю хьажа цало)",
        "upload_source_file": " (файл хьан компьютер чохь ю)",
+       "listfiles-delete": "дӀаяккха",
        "listfiles-summary": "Лахахь гойтуш ю ерриг файлаш.\nДекъашхо къастичи, цун керла файлаш гойту.",
        "listfiles_search_for": "Лаха хIуман цIарца:",
        "imgfile": "файл",
        "mimesearch-summary": "ХӀокху агӀоно йиш хуьлуьйту MIME-тайпан файлаш харжа. Яздеш долу формат: чулацаман тайп/бухара тайп, масала  <code>image/jpeg</code>.",
        "mimetype": "MIME-тайп:",
        "download": "чуяккха",
-       "unwatchedpages": "Цхьамо тергам ца беш йолу агIонаш",
+       "unwatchedpages": "Цхьамо тергам ца беш йолу агӀонаш",
        "listredirects": "ДIасахьажоран могIам",
        "listduplicatedfiles": "Файлийн могӀам дубликатшца",
        "listduplicatedfiles-entry": "Файлан [[:File:$1|$1]] — [[$3|{{PLURAL:$2|дубликат ю}}]].",
        "withoutinterwiki": "Юкъарвики-хьажоргаш йоцу агӀонаш",
        "withoutinterwiki-summary": "Лахара агӀонийн юкъарвики-хьажоргаш яц:",
        "withoutinterwiki-submit": "Гайта",
-       "fewestrevisions": "ЧIогIа кIезиг башхонаш йолу агIонаш",
+       "fewestrevisions": "ЧӀогӀа кӀезиг версеш йолу агӀонаш",
        "nbytes": "$1 {{PLURAL:$1|байт}}",
        "ncategories": "$1 {{PLURAL:$1|категори|категореш}}",
        "ninterwikis": "$1 {{PLURAL:$1|1=юкъарвики-хьажораг|юкъарвики-хьажоргаш}}",
        "nimagelinks": "Лелош ю $1 {{PLURAL:$1|агӀонгахь|агӀонашкахь}}",
        "ntransclusions": "лелош ю $1 {{PLURAL:$1|агӀонгахь|агӀонашкахь}}",
        "specialpage-empty": "Дехаро хӀумма ца елла.",
-       "lonelypages": "Байлахь йисина агIонаш",
+       "lonelypages": "Байлахь йисина агӀонаш",
        "lonelypagestext": "Кхузахь ю {{grammar:genitive|{{SITENAME}}}} кхечу агӀонашкахь тӀе хьажийна хьажоргаш йоцу агӀонаш.",
-       "uncategorizedpages": "Категореш йоцу агIонаш",
+       "uncategorizedpages": "Категореш йоцу агӀонаш",
        "uncategorizedcategories": "Категореш йоцу категореш",
        "uncategorizedimages": "Категореш йоцу файлаш",
        "uncategorizedtemplates": "Категореш йоцу кепаш",
        "unusedimages": "Лелош йоцу файлаш",
        "popularpages": "ГӀараяьлла агӀонаш",
        "wantedcategories": "Оьшуш йолу категореш",
-       "wantedpages": "Оьшуш йолу агIонаш",
+       "wantedpages": "Оьшуш йолу агӀонаш",
        "wantedfiles": "Оьшуш йолу файлаш",
        "wantedfiletext-cat": "Лахара йоцу файлаш лело гӀерта. Оцу могӀам юкъа ца хууш файлаш кхета там бу, кхечу проекташ чохь йолу. Ишта ца хууш юкъа нийса елачарна тӀехула <del>сиз</del> хира ду.\nКхин йоцу файлаш гойту [[:$1]] чохь",
        "wantedfiletext-nocat": "Лахара йоцу файлаш лело гӀерта. Оцу могӀам юкъа ца хууш файлаш кхета там бу, кхечу проекташ чохь йолу. Ишта ца хууш юкъа нийса елачарна тӀехула <del>сиз</del> хира ду.",
        "mostcategories": "Дуккха категореш тӀе тоьхна йолу агӀонаш",
        "mostimages": "Массарел дуккха лелайо файлаш",
        "mostinterwikis": "Дуккха юкъарвики хьажоргаш тӀе тоьхна йолу агӀонаш",
-       "mostrevisions": "Сих сиха нисйина йолу агIонаш",
+       "mostrevisions": "Сих сиха нисйина йолу агӀонаш",
        "prefixindex": "Хьалха агӀонашан цӀераш хӀотто йеза",
        "prefixindex-namespace": "Хьалха агӀонашан цӀераш хӀотто еза («{{ns:$1}}»)",
        "prefixindex-strip": "Хиламийн могӀам чура префикс къайлаяккха",
        "shortpages": "Боца яззамаш",
        "longpages": "Беха яззамаш",
-       "deadendpages": "Дика йоцу агIонаш",
-       "protectedpages": "ГIаролла дина агIонаш",
+       "deadendpages": "Дика йоцу агӀонаш",
+       "protectedpages": "ГIаролла дина агӀонаш",
        "protectedpages-indef": "Хан йоцуш гӀоралла динарш бен",
        "protectedpages-cascade": "Чахчарин гӀоралла бен",
        "protectedpages-noredirect": "Къайлаяха дӀасахьажийнарш",
        "ilsubmit": "Лаха",
        "bydate": "терахьашца",
        "sp-newimages-showfrom": "Гайта керла файлаш $2, $1 тӀера дуьйна",
-       "seconds-abbrev": "$1оцу",
+       "seconds-abbrev": "$1 оцу",
        "minutes-abbrev": "$1 мин",
        "hours-abbrev": "$1 сахь.",
-       "seconds": "{{PLURAL:$1|$1 секунд}}",
-       "minutes": "$1 минут",
-       "hours": "{{PLURAL:$1|сахьат}}",
-       "days": "{{PLURAL:$1|$1 де}}",
+       "seconds": "{{PLURAL:$1|$1 секунд|$1 секунд}}",
+       "minutes": "{{PLURAL:$1|$1 минут|$1 минут}}",
+       "hours": "{{PLURAL:$1|$1 сахьт|$1 сахьт}}",
+       "days": "{{PLURAL:$1|$1 де|$1 де}}",
        "weeks": "{{PLURAL:$1|$1 кӀира}}",
-       "months": "$1 {{PLURAL:$1|бутт}}",
-       "years": "$1 {{PLURAL:$1|шо}}",
+       "months": "{{PLURAL:$1|$1 бутт|$1 бутт}}",
+       "years": "{{PLURAL:$1|$1 шо|$1 шо}}",
        "ago": "$1 хьалха",
        "just-now": "хӀинца",
        "hours-ago": "$1 {{PLURAL:$1|сахьт}}",
        "watchlistedit-clear-submit": "Тергаман могӀам дӀацӀанбан (иза сацадан лурдац)",
        "watchlistedit-clear-done": "Хьан тергаман могӀам дӀацӀанбина",
        "watchlistedit-clear-removed": "{{PLURAL:$1|ДӀаяьккхина|ДӀаяьхна}} $1 {{PLURAL:$1|дӀаяздар|дӀаяздарш}}:",
-       "watchlistedit-too-many": "Ð\9aÑ\85Ñ\83ззаÑ\85Ñ\8c Ð³Ð°Ð¹Ñ\82а Ñ\82Ó\80еÑ\85Ñ\8c Ð´Ñ\83кÑ\85а Ð°Ð³Ó\80онаÑ\88 Ñ\8e.",
+       "watchlistedit-too-many": "Кхузахь гайта тӀехь дукха агӀонаш ю.",
        "watchlisttools-clear": "Тергаман могӀам дӀацӀанбан",
        "watchlisttools-view": "МогӀам чура агӀонашан хийцамаш",
        "watchlisttools-edit": "Хьажа/нисбé могӀам",
index 1478fda..1ec96c9 100644 (file)
        "talkpagelinktext": "diskuse",
        "specialpage": "Speciální stránka",
        "personaltools": "Osobní nástroje",
-       "postcomment": "Nová sekce",
        "articlepage": "Prohlédnout si stránku",
        "talk": "Diskuse",
        "views": "Zobrazení",
        "externaldberror": "Buď nastala chyba externí autentizační databáze, nebo nemáte dovoleno měnit svůj externí účet.",
        "login": "Přihlaste se",
        "nav-login-createaccount": "Přihlášení / vytvoření účtu",
-       "loginprompt": "K přihlášení do {{grammar:2sg|{{SITENAME}}}} musíte mít povoleny cookies.",
        "userlogin": "Přihlášení / vytvoření účtu",
        "userloginnocreate": "Přihlášení",
        "logout": "Odhlásit se",
        "wantedpages-badtitle": "Výsledky obsahují neplatný název: $1",
        "wantedfiles": "Chybějící soubory",
        "wantedfiletext-cat": "Následující soubory se používají, ale neexistují. Soubory ze vzdálených úložišť zde mohou být uvedeny, přestože existují. Taková falešná pozitiva budou zobrazena <del>přeškrtnutě</del>. Stránky, které vkládají neexistující soubory, jsou navíc uvedeny v [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Následující soubory se používají, ale neexistují. Stránky, které používají neexistující soubory, jsou navíc uvedeny v [[:$1]].",
        "wantedfiletext-nocat": "Následující soubory se používají, ale neexistují. Soubory ze vzdálených úložišť zde mohou být uvedeny, přestože existují. Taková falešná pozitiva budou zobrazena <del>přeškrtnutě</del>.",
+       "wantedfiletext-nocat-noforeign": "Následující soubory se používají, ale neexistují.",
        "wantedtemplates": "Chybějící šablony",
        "mostlinked": "Nejodkazovanější stránky",
        "mostlinkedcategories": "Nejpoužívanější kategorie",
index e66b267..fe284ae 100644 (file)
        "talkpagelinktext": "Sgwrs",
        "specialpage": "Tudalen Arbennig",
        "personaltools": "Offer personol",
-       "postcomment": "Adran newydd",
        "articlepage": "Dangos tudalen bwnc",
        "talk": "Sgwrs",
        "views": "Golygon",
index 797b8b6..b505ce4 100644 (file)
        "talkpagelinktext": "diskussion",
        "specialpage": "Speciel side",
        "personaltools": "Personlige værktøjer",
-       "postcomment": "Nyt afsnit",
        "articlepage": "Se artiklen",
        "talk": "Diskussion",
        "views": "Visninger",
        "externaldberror": "Der er opstået en fejl i en ekstern adgangsdatabase, eller du har ikke rettigheder til at opdatere denne.",
        "login": "Log på",
        "nav-login-createaccount": "Opret en konto eller log på",
-       "loginprompt": "Du skal have cookies slået til for at kunne logge på {{SITENAME}}.",
        "userlogin": "Opret en konto eller log på",
        "userloginnocreate": "Log på",
        "logout": "Log af",
        "watchlistedit-raw-done": "Din overvågningsliste blev opdateret.",
        "watchlistedit-raw-added": "{{PLURAL:$1|1 side|$1 sider}} er tilføjet:",
        "watchlistedit-raw-removed": "{{PLURAL:$1|1 side|$1 sider}} er fjernet:",
+       "watchlisttools-clear": "Ryd overvågningsliste",
        "watchlisttools-view": "Se ændrede sider i overvågningslisten",
        "watchlisttools-edit": "Rediger overvågningsliste",
        "watchlisttools-raw": "Rediger rå overvågningsliste",
index b2bf75f..e1d180d 100644 (file)
        "talkpagelinktext": "Diskussion",
        "specialpage": "Spezialseite",
        "personaltools": "Meine Werkzeuge",
-       "postcomment": "Neuer Abschnitt",
        "articlepage": "Inhaltsseite anzeigen",
        "talk": "Diskussion",
        "views": "Ansichten",
        "externaldberror": "Entweder liegt ein Fehler bei der externen Authentifizierung vor oder du darfst dein externes Benutzerkonto nicht aktualisieren.",
        "login": "Anmelden",
        "nav-login-createaccount": "Anmelden / Benutzerkonto erstellen",
-       "loginprompt": "Zur Anmeldung müssen Cookies aktiviert sein.",
        "userlogin": "Anmelden / Benutzerkonto anlegen",
        "userloginnocreate": "Anmelden",
        "logout": "Abmelden",
        "wantedpages-badtitle": "Ungültiger Titel im Ergebnis: $1",
        "wantedfiles": "Gewünschte Dateien",
        "wantedfiletext-cat": "Die folgenden Dateien werden verwendet, sind jedoch nicht vorhanden. Vorhandene Dateien aus fremden Repositorien können dennoch hier aufgelistet sein und werden <del>durchgestrichen</del> dargestellt. Zusätzlich werden Seiten, die nicht vorhandene Dateien enthalten, in die [[:$1]] eingeordnet.",
+       "wantedfiletext-cat-noforeign": "Die folgenden Dateien werden verwendet, sind jedoch nicht vorhanden. Zusätzlich werden Seiten auf [[:$1]] gelistet, die nicht vorhandene Dateien einbetten.",
        "wantedfiletext-nocat": "Die folgenden Dateien werden verwendet, sind jedoch nicht vorhanden. Vorhandene Dateien aus fremden Repositorien können dennoch hier aufgelistet sein und werden <del>durchgestrichen</del> dargestellt.",
+       "wantedfiletext-nocat-noforeign": "Die folgenden Dateien werden verwendet, sind jedoch nicht vorhanden.",
        "wantedtemplates": "Gewünschte Vorlagen",
        "mostlinked": "Seiten mit den meisten Links",
        "mostlinkedcategories": "Meistbenutzte Kategorien",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|Diskussion]])",
        "unknown_extension_tag": "Unbekanntes Parsertag „$1“",
        "duplicate-defaultsort": "Achtung: Der Sortierungsschlüssel „$2“ überschreibt den vorher verwendeten Schlüssel „$1“.",
+       "duplicate-displaytitle": "<strong>Warnung:</strong> Der Anzeigetitel „$2“ überschreibt den früheren Anzeigetitel „$1“.",
        "version": "Version",
        "version-extensions": "Installierte Erweiterungen",
        "version-skins": "Installierte Benutzeroberflächen",
index 4250606..0ed4a54 100644 (file)
        "talkpagelinktext": "Vatenayış",
        "specialpage": "Pela xısusiye",
        "personaltools": "Hacetê şexsiy",
-       "postcomment": "Qısımo newe",
        "articlepage": "Pela zerreki bıvêne",
        "talk": "Werênayış",
        "views": "Asayışi",
        "versionrequired": "No $1 MediaWiki lazımo",
        "versionrequiredtext": "Seba gurenayışê na pele versiyonê MediaWiki $1 lazımo. \n[[Special:Version|Versiyonê pele]] bıvêne.",
        "ok": "Temam",
-       "pagetitle": "\"$1\" adres ra gerya.",
+       "pagetitle": "$1 – {{SITENAME}}",
        "pagetitle-view-mainpage": "{{SITENAME}}",
        "backlinksubtitle": "← $1",
        "retrievedfrom": "\"$1\" ra ard",
        "externaldberror": "Ya database de xeta esta ya zi heqê şıma çino şıma no hesab bıvurni.",
        "login": "Cı kewe",
        "nav-login-createaccount": "Dekew de / hesab vıraze",
-       "loginprompt": "{{SITENAME}} dı ronıştış akerdışi rê ''çerezan'' aktiv kerdış icab keno.",
        "userlogin": "Cı kewe / hesab vıraze",
        "userloginnocreate": "Cı kewe",
        "logout": "Bıveciye",
        "pagemerge-logentry": "[[$1]] u [[$2]] yew kerd (revizyonî heta $3)",
        "revertmerge": "Abırnê",
        "mergelogpagetext": "Cêr de yew liste esta ke mocnena ra, raya tewr peyêne kamci pela tarixi be a bine ra şanawa pê.",
-       "history-title": "Rewizyonê $1:",
+       "history-title": "Tarixê çımraviyarnayışê \"$1\"",
        "difference-title": "Pela \"$1\" ferqê çım ra viyarnayışan",
        "difference-title-multipage": "Ferkê pelan dê \"$1\" u \"$2\"",
        "difference-multipage": "(Ferqê pelan)",
index c7bd536..9b63730 100644 (file)
        "talkpagelinktext": "Συζήτηση",
        "specialpage": "Ειδική σελίδα",
        "personaltools": "Προσωπικά εργαλεία",
-       "postcomment": "Νέα ενότητα",
        "articlepage": "Εμφάνιση σελίδας περιεχομένου",
        "talk": "Συζήτηση",
        "views": "Προβολές",
index ae1abfb..a8c6055 100644 (file)
        "externaldberror": "There was either an authentication database error or you are not allowed to update your external account.",
        "login": "Log in",
        "nav-login-createaccount": "Log in / create account",
-       "loginprompt": "You must have cookies enabled to log in to {{SITENAME}}.",
+       "loginprompt": "",
        "userlogin": "Log in / create account",
        "userloginnocreate": "Log in",
        "logout": "Log out",
        "wantedfiles": "Wanted files",
        "wantedfiles-summary": "",
        "wantedfiletext-cat": "The following files are used but do not exist. Files from foreign repositories may be listed despite existing. Any such false positives will be <del>struck out</del>. Additionally, pages that embed files that do not exist are listed in [[:$1]].",
+       "wantedfiletext-cat-noforeign": "The following files are used but do not exist. Additionally, pages that embed files that do not exist are listed in [[:$1]].",
        "wantedfiletext-nocat": "The following files are used but do not exist. Files from foreign repositories may be listed despite existing. Any such false positives will be <del>struck out</del>.",
+       "wantedfiletext-nocat-noforeign": "The following files are used but do not exist.",
        "wantedtemplates": "Wanted templates",
        "wantedtemplates-summary": "",
        "mostlinked": "Most linked-to pages",
        "timezone-utc": "UTC",
        "unknown_extension_tag": "Unknown extension tag \"$1\"",
        "duplicate-defaultsort": "<strong>Warning:</strong> Default sort key \"$2\" overrides earlier default sort key \"$1\".",
+       "duplicate-displaytitle": "<strong>Warning:</strong> Display title \"$2\" overrides earlier display title \"$1\".",
        "version": "Version",
        "version-summary": "",
        "version-extensions": "Installed extensions",
index 45dc6b7..590d8c2 100644 (file)
        "talkpagelinktext": "Diskuto",
        "specialpage": "Speciala Paĝo",
        "personaltools": "Personaj iloj",
-       "postcomment": "Nova sekcio",
        "articlepage": "Rigardi artikolon",
        "talk": "Diskuto",
        "views": "Vidoj",
        "externaldberror": "Aŭ estis datenbaza eraro rilate al ekstera aŭtentikigado, aŭ vi ne rajtas ĝisdatigi vian eksteran konton.",
        "login": "Ensaluti",
        "nav-login-createaccount": "Ensaluti / Krei novan konton",
-       "loginprompt": "Via foliumilo nepre permesu kuketojn por ensaluti en la {{SITENAME}}.",
        "userlogin": "Ensaluti / Krei novan konton",
        "userloginnocreate": "Ensaluti",
        "logout": "Elsaluti",
        "editundo": "malfari",
        "diff-empty": "(Neniu diferenco)",
        "diff-multi-sameuser": "({{PLURAL:$1|Unu meza versio|$1 mezaj versioj}} de la sama uzanto ne montriĝas)",
-       "diff-multi-otherusers": "({{PLURAL:$1|Unu meza versio|$1 mezaj versioj}} de {{PLURAL:$2|alia uzanto|$2 uzoj}} ne montriĝas)",
+       "diff-multi-otherusers": "({{PLURAL:$1|Unu meza versio|$1 mezaj versioj}} de {{PLURAL:$2|alia uzanto|$2 uzantoj}} ne montriĝas)",
        "diff-multi-manyusers": "({{PLURAL:$1|Unu intermeza versio|$1 intermezaj versioj}} de pli ol {{PLURAL:$2|unu uzanto|$2 uzantoj}} ne estas {{PLURAL:$1|montrata|montrataj}}.)",
        "difference-missing-revision": "{{PLURAL:$2|Unu revizio|$2 revizioj}} de ĉi tiu malsameco ($1) ne {{PLURAL:$2|estis|estis}} trovebla.\n\nLa kutima kaŭzo estas sekvi malaktualan malsamo-ligilon al paĝo forviŝita.\nDetaloj troveblos en la [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registro de forviŝoj].",
        "searchresults": "Serĉrezultoj",
        "license-nopreview": "(Antaŭvido ne montrebla)",
        "upload_source_url": " (valida, publike atingebla URL-o)",
        "upload_source_file": " (dosiero en via komputilo)",
+       "listfiles-delete": "forigi",
        "listfiles-summary": "Ĉi tiu speciala paĝo montras ĉiujn alŝutitajn dosierojn.\nKiam oni filtras ĝin laŭ uzanto, nur la aktuala versio de la dosiero estos montrita.",
        "listfiles_search_for": "Serĉi dosieran nomon:",
        "imgfile": "dosiero",
        "addedwatchtext": "La paĝo \"[[:$1]]\" aldoniĝis al via [[Special:Watchlist|atentaro]]. Estontaj ŝanĝoj de tiu paĝo kaj de ĝia rilata diskutpaĝo aperos tie.",
        "removewatch": "Forigi el atentaro",
        "removedwatchtext": "La paĝo \"[[:$1]]\" estas forigita el via [[Special:Watchlist|atentaro]].",
+       "removedwatchtext-short": "La paĝo \"$1\" estis forigita el via atento-listo.",
        "watch": "Atenti",
        "watchthispage": "Priatenti paĝon",
        "unwatch": "Malatenti",
        "newimages-summary": "Ĉi tiu speciala paĝo montras la lastajn alŝutitajn dosierojn.",
        "newimages-legend": "Dosiernomo",
        "newimages-label": "Dosiernomo (aŭ parto de ĝi):",
+       "newimages-showbots": "Montri alŝutojn per robotoj",
        "noimages": "Nenio videbla.",
        "ilsubmit": "Serĉi",
        "bydate": "laŭ dato",
        "version-hook-name": "Nomo de hoko",
        "version-hook-subscribedby": "Abonita de",
        "version-version": "($1)",
+       "version-no-ext-name": "[sen nomo]",
        "version-license": "Permesilo de MediaWiki",
        "version-ext-license": "Permesilo",
        "version-ext-colheader-name": "Etendilo",
+       "version-skin-colheader-name": "Etoso",
        "version-ext-colheader-version": "Versio",
        "version-ext-colheader-license": "Permesilo",
        "version-ext-colheader-description": "Priskribo",
        "expand_templates_remove_nowiki": "Nuligi <nowiki> etikedojn en rezulto",
        "expand_templates_generate_xml": "Montri XML-sintaksarbon",
        "expand_templates_generate_rawhtml": "Montri krudan HTML-n",
-       "expand_templates_preview": "Antaŭrigardo"
+       "expand_templates_preview": "Antaŭrigardo",
+       "pagelang-name": "Paĝo",
+       "pagelang-language": "Lingvo",
+       "pagelang-use-default": "Uzi defaŭltan lingvon",
+       "pagelang-select-lang": "Elekti la lingvon",
+       "right-pagelang": "Ŝanĝi paĝan lingvon",
+       "action-pagelang": "ŝanĝi la lingvon de la paĝo",
+       "log-name-pagelang": "Ŝanĝi la lingvan protokolon",
+       "log-description-pagelang": "Jen protokolo pri ŝanĝoj de paĝaj lingvoj.",
+       "logentry-pagelang-pagelang": "$1 {{GENDER:$2|ŝanĝis}} la paĝan lingvon por $3 de $4 al $5."
 }
index fa41f55..635f15c 100644 (file)
        "talkpagelinktext": "Discusión",
        "specialpage": "Página especial",
        "personaltools": "Herramientas personales",
-       "postcomment": "Sección nueva",
        "articlepage": "Ver artículo",
        "talk": "Discusión",
        "views": "Vistas",
        "externaldberror": "Hubo un error de autenticación de la base de datos o bien no tienes autorización para actualizar tu cuenta externa.",
        "login": "Iniciar sesión",
        "nav-login-createaccount": "Acceder/crear cuenta",
-       "loginprompt": "Hay que activar las ''cookies'' en el navegador para iniciar sesión en {{SITENAME}}.",
        "userlogin": "Acceder/crear cuenta",
        "userloginnocreate": "Acceder",
        "logout": "Salir",
index 05c0e32..fb3bd43 100644 (file)
        "category-file-count-limited": "{{PLURAL:$1|Järgmine fail|Järgmised $1 faili}} on selles kategoorias.",
        "listingcontinuesabbrev": "jätk",
        "index-category": "Indeksiga leheküljed",
-       "noindex-category": "Indeksita leheküljed",
+       "noindex-category": "Indekseerimata leheküljed",
        "broken-file-category": "Katkiste pildilinkidega leheküljed",
        "about": "Tiitelandmed",
        "article": "artikkel",
        "talkpagelinktext": "arutelu",
        "specialpage": "Erilehekülg",
        "personaltools": "Personaalsed tööriistad",
-       "postcomment": "Uus alaosa",
        "articlepage": "Artiklilehekülg",
        "talk": "Arutelu",
        "views": "vaatamisi",
        "externaldberror": "Esines autentimistõrge või sul pole õigust konto andmeid muuta.",
        "login": "Logi sisse",
        "nav-login-createaccount": "Logi sisse või registreeru kasutajaks",
-       "loginprompt": "Sisselogimiseks peavad küpsised lubatud olema.",
        "userlogin": "Sisselogimine või kasutajakonto loomine",
        "userloginnocreate": "Sisselogimine",
        "logout": "Logi välja",
        "prefs-watchlist-edits-max": "Ülemmäär: 1000",
        "prefs-watchlist-token": "Jälgimisloendi luba:",
        "prefs-misc": "Muu",
-       "prefs-resetpass": "Muuda parooli",
+       "prefs-resetpass": "Muuda parool",
        "prefs-changeemail": "Muuda e-posti aadressi",
        "prefs-setemail": "Määra e-posti aadress",
        "prefs-email": "E-posti sätted",
        "license-nopreview": "(Eelvaade ei ole saadaval)",
        "upload_source_url": "(avalikult ligipääsetav URL)",
        "upload_source_file": "(fail sinu arvutis)",
+       "listfiles-delete": "kustuta",
        "listfiles-summary": "Sellel erileheküljel näidatakse kõiki üles laaditud faile.",
        "listfiles_search_for": "Nimeotsing:",
        "imgfile": "fail",
        "wantedpages-badtitle": "Tulemuste seas on vigane pealkiri: $1",
        "wantedfiles": "Kõige oodatumad failid",
        "wantedfiletext-cat": "Järgmised failid puuduvad, aga on lehekülgedel kasutuses. Siin võivad olla loetletud ka välistes hoidlates asuvad failid, hoolimata sellest, et nad tegelikult olemas on. Loendi sellised valeliikmed on <del>läbi kriipsutatud</del>. Lisaks on puuduvaid faile sisaldavad leheküljed loetletud asukohas [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Järgmised failid puuduvad, aga on kasutuses. Peale selle, leheküljel [[:$1]] on loetletud leheküljed, kus kasutatakse puuduvaid faile.",
        "wantedfiletext-nocat": "Järgmised failid puuduvad, aga on lehekülgedel kasutuses. Siin võivad olla loetletud ka välistes hoidlates asuvad failid, hoolimata sellest, et nad tegelikult olemas on. Loendi sellised valeliikmed on <del>läbi kriipsutatud</del>.",
+       "wantedfiletext-nocat-noforeign": "Järgmised failid puuduvad, aga on kasutuses.",
        "wantedtemplates": "Kõige oodatumad mallid",
        "mostlinked": "Kõige viidatumad leheküljed",
        "mostlinkedcategories": "Kõige viidatumad kategooriad",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|arutelu]])",
        "unknown_extension_tag": "Tundmatu lisa silt \"$1\".",
        "duplicate-defaultsort": "'''Hoiatus:''' Järjestamisvõti \"$2\" tühistab eespool oleva järjestamisvõtme \"$1\".",
+       "duplicate-displaytitle": "<strong>Hoiatus:</strong> Kuvatava pealkirjaga \"$2\" kirjutatakse üle varasem kuvatav pealkiri \"$1\".",
        "version": "Versioon",
        "version-extensions": "Paigaldatud lisad",
        "version-skins": "Paigaldatud kujundused",
index 4f10d30..ab0bb1f 100644 (file)
        "talkpagelinktext": "Eztabaida",
        "specialpage": "Aparteko orrialdea",
        "personaltools": "Tresna pertsonalak",
-       "postcomment": "Atal berria",
        "articlepage": "Artikulua ikusi",
        "talk": "Eztabaida",
        "views": "Ikustaldiak",
        "externaldberror": "Kanpoko datu-base autentifikazio errorea gertatu da edo ez duzu zure kanpo kontua eguneratzeko baimenik.",
        "login": "Saioa hasi",
        "nav-login-createaccount": "Saioa hasi / kontua sortu",
-       "loginprompt": "Cookieak gaituta izatea beharrezkoa da {{SITENAME}}(e)n saioa hasteko.",
        "userlogin": "Saioa hasi / kontua sortu",
        "userloginnocreate": "Saioa hasi",
        "logout": "Saioa itxi",
        "limitreport-expansiondepth": "Gehienezko espantsio sakonera",
        "limitreport-expensivefunctioncount": "Parser funtzio kontaketa garestia",
        "expandtemplates": "Txantiloi ordezkatzailea",
-       "expand_templates_intro": "Aparteko orrialde honek modu errekurtsiboan txantiloiak ordezkatu egiten ditu.\nFuntzioak ere ordezkatu egiten ditu, hala nola\n<code><nowiki>{{</nowiki>#language:…}}</code>, eta\n<code><nowiki>{{</nowiki>CURRENTDAY}}</code> bezalako aldagaiak ere.\nKortxete bikoitzarekin hobeto egiten da lan.",
+       "expand_templates_intro": "Orri berezi honek testua hartu eta txantiloi guztiak modu errekurtsiboan zabaltzen ditu.\nOnartutako funtzio sintaktikoak ere ordezkatzen ditu, hala nola\n<code><nowiki>{{</nowiki>#language:…}}</code>; eta aldagaiak ere, hala nola\n<code><nowiki>{{</nowiki>CURRENTDAY}}</code>.\nIzan ere, kortxete bikoitzen arteko ia edozer zabaltzen du.",
        "expand_templates_title": "Izenburua ({{FULLPAGENAME}} ordezkatzeko, eta abar):",
        "expand_templates_input": "Sarrerako testua:",
        "expand_templates_output": "Emaitza",
index 77c1b16..3531448 100644 (file)
@@ -34,7 +34,8 @@
                        "درفش کاویانی",
                        "محک",
                        "아라",
-                       "Mostafadaneshvar"
+                       "Mostafadaneshvar",
+                       "Pouyana"
                ]
        },
        "tog-underline": "خط کشیدن زیر پیوندها:",
        "talkpagelinktext": "بحث",
        "specialpage": "صفحهٔ ویژه",
        "personaltools": "ابزارهای شخصی",
-       "postcomment": "بخش جدید",
        "articlepage": "نمایش مقاله",
        "talk": "بحث",
        "views": "بازدیدها",
        "externaldberror": "خطایی در ارتباط با پایگاه داده رخ داده‌است یا اینکه شما اجازهٔ به‌روزرسانی حساب خارجی خود را ندارید.",
        "login": "ورود به سامانه",
        "nav-login-createaccount": "ورود به سامانه / ایجاد حساب کاربری",
-       "loginprompt": "برای ورود به {{SITENAME}} باید کوکی‌ها را فعال کنید.",
        "userlogin": "ورود به سامانه / ایجاد حساب کاربری",
        "userloginnocreate": "ورود به سامانه",
        "logout": "خروج از سامانه",
        "license-nopreview": "(پیش‌نمایش وجود ندارد)",
        "upload_source_url": "(یک نشانی اینترنتی معتبر و قابل دسترسی برای عموم)",
        "upload_source_file": "(پرونده‌ای در رایانهٔ شما)",
+       "listfiles-delete": "حذف",
        "listfiles-summary": "این صفحهٔ ویژه تمام پرونده‌های بارگذاری‌شده را نمایش می‌دهد.",
        "listfiles_search_for": "جستجو به دنبال نام پرونده چندرسانه‌ای:",
        "imgfile": "پرونده",
        "wantedpages-badtitle": "عنوان نامجاز در مجموعهٔ نتایج: $1",
        "wantedfiles": "پرونده‌های مورد نیاز",
        "wantedfiletext-cat": "پرونده‌های زیر استفاده می‌شوند اما موجود نیستند. همچنین ممکن است پرونده‌های مخازن خارجی با وجود موجود بودن در اینجا فهرست شوند. هرگونه رتبه مثبت کاذب <del>خط خواهد خورد.</del> علاوه بر این، صفحاتی که پرونده‌هایی ناموجود را در خود جای داده‌اند در [[:$1]] فهرست شده‌اند.",
+       "wantedfiletext-cat-noforeign": "پرونده‌های زیر استفاده می‌شود اما وجود ندارد. علاوه بر این، صفحاتی که پرونده‌ها در آنها وجود دارند فهرست شده‌اند در [[:$1]].",
        "wantedfiletext-nocat": "پرونده‌های زیر استفاده می‌شوند اما موجود نیستند. همچنین ممکن است پرونده‌های مخازن خارجی با وجود موجود بودن در اینجا فهرست شوند. هرگونه رتبهٔ مثبت کاذب <del>خط خواهد خورد.</del>",
+       "wantedfiletext-nocat-noforeign": "پرونده‌های زیر استفاده می‌شوند اما وجود ندارد.",
        "wantedtemplates": "الگوهای مورد نیاز",
        "mostlinked": "صفحه‌هایی که بیشتر از همه به آن‌ها پیوند داده شده‌است",
        "mostlinkedcategories": "رده‌هایی که بیشتر از همه به آن‌ها پیوند داده شده‌است",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|بحث]])",
        "unknown_extension_tag": "برچسب ناشناختهٔ افزونه «$1»",
        "duplicate-defaultsort": "هشدار: ترتیب پیش‌فرض «$2» ترتیب پیش‌فرض قبلی «$1» را باطل می‌کند.",
+       "duplicate-displaytitle": "<strong>هشدار:</strong> نمایش عنوان \" $2 \"باعث ابطال پیش نمایش عنوان\" $1 \" می‌شود.",
        "version": "نسخه",
        "version-extensions": "افزونه‌های نصب‌شده",
        "version-skins": "پوسته‌های نصب شده",
index d554fe6..b62a0d9 100644 (file)
@@ -39,7 +39,8 @@
                        "ZeiP",
                        "לערי ריינהארט",
                        "아라",
-                       "Syreeni"
+                       "Syreeni",
+                       "MrTapsa"
                ]
        },
        "tog-underline": "Linkkien alleviivaus:",
        "talkpagelinktext": "keskustelu",
        "specialpage": "Toimintosivu",
        "personaltools": "Henkilökohtaiset työkalut",
-       "postcomment": "Uusi osio",
        "articlepage": "Näytä varsinainen sivu",
        "talk": "Keskustelu",
        "views": "Näkymät",
        "externaldberror": "Tapahtui virhe ulkoisen autentikointitietokannan käytössä tai sinulla ei ole lupaa päivittää tunnustasi.",
        "login": "Kirjaudu sisään",
        "nav-login-createaccount": "Kirjaudu sisään tai luo tunnus",
-       "loginprompt": "Sinun täytyy sallia evästeet, jotta voit kirjautua sivustolle {{SITENAME}}.",
        "userlogin": "Kirjaudu sisään tai luo tunnus",
        "userloginnocreate": "Kirjaudu sisään",
        "logout": "Kirjaudu ulos",
        "license-nopreview": "(esikatselua ei saatavilla)",
        "upload_source_url": " (julkinen verkko-osoite)",
        "upload_source_file": " (tiedosto tietokoneella)",
+       "listfiles-delete": "poista",
        "listfiles-summary": "Tämä toimintosivu näyttää kaikki tallennetut tiedostot.",
        "listfiles_search_for": "Nimihaku",
        "imgfile": "tiedosto",
        "wantedpages-badtitle": "Virheellinen otsikko tuloksissa: $1",
        "wantedfiles": "Halutut tiedostot",
        "wantedfiletext-cat": "Seuraavia tiedostoja käytetään, mutta niitä ei ole olemassa. Ulkopuolissa mediavarastoissa olevat tiedostot voivat näkyä tällä listalla, vaikka ne ovat olemassa. Tällaiset väärät merkinnät on <del>yliviivattu</del>. Lisäksi sellaiset sivut, joihin on sisällytetty tiedostoja, jotka eivät ole olemassa, on luetteloitu [[:$1|täällä]].",
+       "wantedfiletext-cat-noforeign": "Seuraavat tiedostot ovat käytössä vaikka niitä ei ole olemassa. Luettelo sellaisista sivuista, joihin on upotettu olemattomia tiedostoja, on [[:$1]].",
        "wantedfiletext-nocat": "Seuraavia tiedostoja käytetään, mutta niitä ei ole olemassa. Ulkopuolissa mediavarastoissa olevat tiedostot voivat näkyä tällä listalla, vaikka ne ovat olemassa. Tällaiset väärät merkinnät on <del>yliviivattu</del.>",
+       "wantedfiletext-nocat-noforeign": "Seuraavia tiedostoja käytetään vaikka niitä ei ole olemassa.",
        "wantedtemplates": "Halutut mallineet",
        "mostlinked": "Viitatuimmat sivut",
        "mostlinkedcategories": "Viitatuimmat luokat",
        "ipbemailban": "Estä käyttäjää lähettämästä sähköpostia",
        "ipbenableautoblock": "Estä automaattisesti viimeisin IP-osoite, josta käyttäjä on muokannut, sekä ne osoitteet, joista hän jatkossa yrittää muokata.",
        "ipbsubmit": "Estä",
-       "ipbother": "Muu kesto",
+       "ipbother": "Muu aikamääre:",
        "ipboptions": "2 tuntia:2 hours,1 päivä:1 day,3 päivää:3 days,1 viikko:1 week,2 viikkoa:2 weeks,1 kuukausi:1 month,3 kuukautta:3 months,6 kuukautta:6 months,1 vuosi:1 year,ikuinen:infinite",
        "ipbhidename": "Piilota tunnus muokkauksista ja listauksista",
        "ipbwatchuser": "Tarkkaile tämän käyttäjän käyttäjä- ja keskustelusivua",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|keskustelu]])",
        "unknown_extension_tag": "Tuntematon laajennuskoodi ”$1”.",
        "duplicate-defaultsort": "'''Varoitus:''' Oletuslajitteluavain ”$2” korvaa aiemman oletuslajitteluavaimen ”$1”.",
+       "duplicate-displaytitle": "<strong>Varoitus:</strong> Näytettävä otsikko \"$2\" päällekirjoittaa edellisen otsikon \"$1\".",
        "version": "Versio",
        "version-extensions": "Asennetut laajennukset",
        "version-skins": "Asennetut ulkoasut",
        "pagelang-language": "Kieli",
        "pagelang-use-default": "Käytä oletuskieltä",
        "pagelang-select-lang": "Valitse kieli",
-       "right-pagelang": "Vaihda sivun kieli",
+       "right-pagelang": "Vaihtaa sivun kieli",
        "action-pagelang": "muuttaa sivun kieliasetuksia",
        "log-name-pagelang": "Kielenvaihtoloki",
        "log-description-pagelang": "Tämä on loki, johon merkitään muutokset sivujen kieliasetuksissa.",
index 5f57d75..2140121 100644 (file)
                        "Scoopfinder",
                        "Akeron",
                        "Linedwell",
-                       "Yona b"
+                       "Yona b",
+                       "SnowedEarth"
                ]
        },
        "tog-underline": "Souligner les liens :",
        "tog-minordefault": "Marquer toutes mes modifications comme mineures par défaut",
        "tog-previewontop": "Afficher la prévisualisation au-dessus de la zone de modification",
        "tog-previewonfirst": "Afficher la prévisualisation lors de la première modification",
-       "tog-enotifwatchlistpages": "M'avertir par courriel lorsqu'une page ou un fichier de ma liste de suivi est modifiée",
+       "tog-enotifwatchlistpages": "M'avertir par courriel lorsqu'une page ou un fichier de ma liste de suivi est modifié",
        "tog-enotifusertalkpages": "M'avertir par courriel si ma page de discussion est modifiée",
        "tog-enotifminoredits": "M'avertir par courriel également lors des modifications mineures de pages ou de fichiers",
        "tog-enotifrevealaddr": "Afficher mon adresse de courriel dans les courriels de notification",
        "talkpagelinktext": "discuter",
        "specialpage": "Page spéciale",
        "personaltools": "Outils personnels",
-       "postcomment": "Nouvelle section",
        "articlepage": "Voir la page de contenu",
        "talk": "Discussion",
        "views": "Affichages",
        "externaldberror": "Une erreur s'est produite avec la base de données d'authentification externe, ou bien vous n'êtes pas autorisé{{GENDER:||e|(e)}} à mettre à jour votre compte externe.",
        "login": "Connexion",
        "nav-login-createaccount": "Créer un compte ou se connecter",
-       "loginprompt": "Vous devez activer les cookies pour vous connecter à {{SITENAME}}.",
        "userlogin": "Créer un compte ou se connecter",
        "userloginnocreate": "Connexion",
        "logout": "Se déconnecter",
        "wantedpages-badtitle": "Titre invalide dans les résultats : $1",
        "wantedfiles": "Fichiers les plus demandés",
        "wantedfiletext-cat": "Les fichiers suivants sont utilisés, mais n'existent pas localement. S'ils se trouvent sur un dépôt partagé, ils peuvent être listés ici, bien qu'ils soient, de fait, déjà disponibles. Tous ces faux positifs seront <del>barrés</del>. En outre, les pages qui intègrent des fichiers qui n'existent pas sont répertoriées dans [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Les fichiers suivants sont utilisés mais n'existent pas. De plus, les pages qui intègrent les fichiers qui n'existent pas sont listés dans [[:$1]].",
        "wantedfiletext-nocat": "Les fichiers suivants sont utilisés, mais n'existent pas localement. S'ils se trouvent sur un dépôt partagé, ils peuvent être listés ici, bien qu'ils soient, de fait, déjà disponibles. Tous ces faux positifs seront <del>barrés</del>.",
+       "wantedfiletext-nocat-noforeign": "Les fichiers suivants sont utilisés mais n'existent pas.",
        "wantedtemplates": "Modèles demandés",
        "mostlinked": "Pages les plus liées",
        "mostlinkedcategories": "Catégories les plus utilisées",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|discussion]])",
        "unknown_extension_tag": "Balise d’extension « $1 » inconnue",
        "duplicate-defaultsort": "Attention : la clé de tri par défaut « $2 » écrase la précédente clé « $1 ».",
+       "duplicate-displaytitle": "<strong>Attention :</strong> Le titre d'affichage «$2» remplace l'ancien titre d'affichage «$1».",
        "version": "Version",
        "version-extensions": "Extensions installées",
        "version-skins": "Habillages installés",
index b15deea..b343117 100644 (file)
        "talkpagelinktext": "谈詑",
        "specialpage": "特殊页",
        "personaltools": "个人工具",
-       "postcomment": "话滴想法",
        "articlepage": "看吖文章",
        "talk": "谈詑",
        "views": "眵",
index 8e18888..ee29121 100644 (file)
        "talkpagelinktext": "Deasbaireachd",
        "specialpage": "Duilleag shònraichte",
        "personaltools": "Innealan pearsanta",
-       "postcomment": "Earrann ùr",
        "articlepage": "Seall duilleag na susbainte",
        "talk": "Deasbaireachd",
        "views": "Tadhalan",
        "userlogin-yourpassword-ph": "Cuir a-steach am facal-faire agad",
        "createacct-yourpassword-ph": "Cuir a-steach facal-faire",
        "yourpasswordagain": "Ath-sgrìobh facal-faire",
-       "createacct-yourpasswordagain": "Dearbh am facal-faire",
+       "createacct-yourpasswordagain": "Dearbhaich am facal-faire",
        "createacct-yourpasswordagain-ph": "Cuir a-steach am facal-faire a-rithist",
        "remembermypassword": "Cuimhnich gu bheil mi air logadh a-steach air a' choimpiutair seo (suas gu $1 {{PLURAL:$1|latha|latha|làithean|latha}})",
        "userlogin-remembermypassword": "Cum air logadh a-steach mi",
        "externaldberror": "Thachair mearachd le dearbhadh an stòir-dhàta air neo chan eil cead agad an cunntas agad air an taobh a-muigh ùrachadh.",
        "login": "Log a-steach",
        "nav-login-createaccount": "Log a-steach / cruthaich cunntas",
-       "loginprompt": "Feumaidh briosgaidean a bhith ceadaichte mus dèan thu logadh a-steach do {{SITENAME}}.",
        "userlogin": "Log a-steach / cruthaich cunntas",
        "userloginnocreate": "Log a-steach",
        "logout": "Log a-mach",
        "emailauthenticated": "Chaidh an seòladh puist-d agad a dhearbhadh $2 aig $3.",
        "emailnotauthenticated": "Cha deach am post-d agad a dhearbhadh fhathast.\nCha dèid post-d a chur airson gin dhe na feartan a leanas.",
        "noemailprefs": "Sònraich post-d sna roghainnean agad gus na feartan seo a chur an comas.",
-       "emailconfirmlink": "Dearbh an seòladh puist-dhealain agad",
+       "emailconfirmlink": "Dearbhaich an seòladh puist-dhealain agad",
        "invalidemailaddress": "Chan urrainn dhuinn gabhail ris an t-seòladh seo a chionn 's gu bheil coltas cearr air.\nCuir a-steach seòladh san fhòrmat cheart no falamhaich an raon sin.",
        "cannotchangeemail": "Cha ghabh na puist-d a tha co-cheangailte ri cunntas atharrachadh air an uicipeid seo.",
        "emaildisabled": "Chan urrainn dhut puist-d a chur air an làrach seo.",
        "parser-unstrip-loop-warning": "Mhothaich sinn do lùb unstrip",
        "parser-unstrip-recursion-limit": "Chaidheas thairis air crìoch unstrip recursion ($1)",
        "converter-manual-rule-error": "Mhothaich sinn do mhearachd san riaghailt iompachadh làimhe airson cànan",
-       "undo-success": "Gabhaidh an deasachadh seo a neo-dhèanamh.\nThoir sùil air a' choimeas gu h-ìosal is dearbh gur e sin a tha fa-near dhut agus sàbhail na h-atharraichean gu h-ìosal gus neo-dhèanamh an deasachaidh a choileanadh.",
+       "undo-success": "Gabhaidh an deasachadh seo a neo-dhèanamh.\nThoir sùil air a' choimeas gu h-ìosal is dearbhaich gur e sin a tha fa-near dhut agus sàbhail na h-atharraichean gu h-ìosal gus neo-dhèanamh an deasachaidh a choileanadh.",
        "undo-failure": "Cha b' urrainn dhuinn an deasachadh a neo-dhèanamh air sgàth 's gun robh deasachaidhean eile sa mheadhan.",
        "undo-norev": "Cha b' urrainn dhuinn an deasachadh a neo-dhèanamh a chionn 's nach robh e ann no gun deach a sguabadh às.",
        "undo-nochange": "Tha coltas gun deach am mùthadh seo a neo-dhèanamh mu thràth.",
        "revdelete-text-file": "Nochdaidh tionndaidhean dhen fhaidhle a chaidh a sguabadh às ann an eachdraidh na duilleige fhathast ach chan fhaic buill a' phobaill cuid dhen t-susbaint aca.",
        "logdelete-text": "Nochdaidh tachartasan san loga a chaidh a sguabadh às ann an eachdraidh na duilleige fhathast ach chan fhaic buill a' phobaill cuid dhen t-susbaint aca.",
        "revdelete-text-others": "Gheibh rianairean eile air {{SITENAME}} cothrom air an t-susbaint fhalaichte fhathast agus is urrainn dhaibh an sguabadh às a neo-dhèanamh san dearbh eadar-aghaidh mur an deach cuingeachaidhean a bharrachd a chur orra.",
-       "revdelete-confirm": "Dearbh gu bheil thu airson seo a dhèanamh, gu bheil thu a' tuigsinn na thachras ri linn agus gu bheil thu a' dèanamh seo a-rèir [[{{MediaWiki:Policy-url}}|a' phoileasaidh]].",
+       "revdelete-confirm": "Dearbhaich gu bheil thu airson seo a dhèanamh, gu bheil thu a' tuigsinn na thachras ri linn agus gu bheil thu a' dèanamh seo a-rèir [[{{MediaWiki:Policy-url}}|a' phoileasaidh]].",
        "revdelete-suppress-text": "Cha bu chòir dhut mùchadh a chleachdadh <strong>ach</strong> ann an suidheachaidhean mar seo:\n* Fiosrachadh a dh'fhaodadh a bhith dìteachail\n* Fiosrachadh pearsanta a tha cearr\n*: <em>seòladh taighe, àireamhan fòn, àireamhan NI is msaa.</em>",
        "revdelete-legend": "Suidhich cuingeachaidhean na faicsinneachd",
        "revdelete-hide-text": "Teacsa a' mhùthaidh",
        "watchlistall2": "na h-uile",
        "namespacesall": "na h-uile",
        "monthsall": "na h-uile",
-       "confirmemail": "Dearbh an seòladh puist-dhealain",
+       "confirmemail": "Dearbhaich an seòladh puist-dhealain",
        "confirmemail_noemail": "Cha dug thu seachad seòladh puist-d dligheach ann an [[Special:Preferences|roghainnean a' chleachdaiche]] agad.",
-       "confirmemail_text": "Iarraidh {{SITENAME}} ort gun dearbh thu an seòladh puist-d agad mus cleachd thu feartan puist-d.\nCleachd am putan gu h-ìosal gus post-d dearbhaidh a chur dhan t-seòladh agad.\nBidh ceangal le còd sa phost-d ud;\nluchdaich an ceangal sa bhrabhsair agad airson dearbhadh gu bheil an seòladh puist-d agad dligheach.",
+       "confirmemail_text": "Iarraidh {{SITENAME}} ort gun dearbhaich thu an seòladh puist-d agad mus cleachd thu feartan puist-d.\nCleachd am putan gu h-ìosal gus post-d dearbhaidh a chur dhan t-seòladh agad.\nBidh ceangal le còd sa phost-d ud;\nluchdaich an ceangal sa bhrabhsair agad airson dearbhadh gu bheil an seòladh puist-d agad dligheach.",
        "confirmemail_pending": "Chaidh còd dearbhaidh a chur thugad air a' phost-d mar-thà;\nma tha thu air a' chunntas agad a chruthachadh o chionn goirid, 's math dh'fhaoidte gum b' feairrde thu feitheamh mionaid no dhà ach an ruig e thu mus iarr thu còd ùr.",
        "confirmemail_send": "Cuir còd dearbhaidh thugam",
        "confirmemail_sent": "Chaidh post-d dearbhaidh a chur.",
        "feedback-error3": "Mearachd: Cha d' fhuair sinn freagairt on API",
        "feedback-thanks": "Mòran taing! Chaidh do bheachd a phostadh air an duilleag \"[$2 $1]\".",
        "feedback-close": "Dèanta",
-       "feedback-bugcheck": "Taghta! Dearbh nach eil e air [$1 liosta nam bugaichean air a bheil sinn eòlach] mar-thà.",
+       "feedback-bugcheck": "Taghta! Dearbhaich nach eil e air [$1 liosta nam bugaichean air a bheil sinn eòlach] mar-thà.",
        "feedback-bugnew": "Dhearbh mi seo. Dèan aithris air buga ur",
        "searchsuggest-search": "Lorg",
        "searchsuggest-containing": "anns a bheil...",
index d72e97a..ddda55f 100644 (file)
        "talkpagelinktext": "Conversa",
        "specialpage": "Páxina especial",
        "personaltools": "Ferramentas persoais",
-       "postcomment": "Nova sección",
        "articlepage": "Ver a páxina de contido",
        "talk": "Conversa",
        "views": "Vistas",
        "externaldberror": "Ou ben se produciu un erro da base de datos na autenticación externa ou ben non se lle permite actualizar a súa conta externa.",
        "login": "Acceder ao sistema",
        "nav-login-createaccount": "Rexistro",
-       "loginprompt": "Debe habilitar as cookies para acceder a {{SITENAME}}.",
        "userlogin": "Rexistro",
        "userloginnocreate": "Rexistro",
        "logout": "Saír ao anonimato",
        "revdelete-offender": "Autor da revisión:",
        "suppressionlog": "Rexistro de supresións",
        "suppressionlogtext": "A continuación móstrase unha lista coas eliminacións e cos bloqueos recentes, que inclúen contido oculto dos administradores.\nOlle a [[Special:BlockList|lista de bloqueos]] para comprobar os bloqueos vixentes.",
-       "mergehistory": "Fusionar os historiais das páxinas",
-       "mergehistory-header": "Esta páxina permítelle fusionar revisións dos historiais da páxina de orixe nunha nova páxina.\nAsegúrese de que esta modificación da páxina mantén a continuidade histórica.",
+       "mergehistory": "Fusionar os historiais de páxinas",
+       "mergehistory-header": "Esta páxina permítelle fusionar as revisións dos historiais da páxina de orixe nunha nova páxina.\nAsegúrese de que esta modificación mantén a continuidade histórica da páxina.",
        "mergehistory-box": "Fusionar as revisións de dúas páxinas:",
        "mergehistory-from": "Páxina de orixe:",
        "mergehistory-into": "Páxina de destino:",
-       "mergehistory-list": "Historial de edicións fusionables",
-       "mergehistory-merge": "As revisións seguintes de [[:$1]] pódense fusionar con [[:$2]]. Use a columna de botóns de selección para fusionar só as revisións creadasen e antes da hora indicada. Teña en conta que se usa as ligazóns de navegación a columna limparase.",
+       "mergehistory-list": "Historial de edicións que se pode fusionar",
+       "mergehistory-merge": "As seguintes revisións de \"[[:$1]]\" pódense fusionar con \"[[:$2]]\".\nUtilice a columna de caixas de selección para fusionar só as revisións creadas ata a hora indicada, esta incluída.\nTeña en conta que o uso das ligazóns de navegación ha borrar a selección da columna.",
        "mergehistory-go": "Mostrar as edicións que se poden fusionar",
        "mergehistory-submit": "Fusionar as revisións",
        "mergehistory-empty": "Non hai revisións que se poidan fusionar.",
-       "mergehistory-success": "{{PLURAL:$3|Unha revisión|$3 revisións}} de [[:$1]] {{PLURAL:$3|fusionouse|fusionáronse}} sen problemas en [[:$2]].",
-       "mergehistory-fail": "Non se puido fusionar o historial; comprobe outra vez os parámetros de páxina e hora.",
+       "mergehistory-success": "{{PLURAL:$3|Unha revisión|$3 revisións}} de \"[[:$1]]\" {{PLURAL:$3|fusionouse|fusionáronse}} sen problemas con \"[[:$2]]\".",
+       "mergehistory-fail": "Non se puido fusionar o historial; comprobe outra vez os parámetros de páxina e data.",
+       "mergehistory-fail-toobig": "Non se puido fusionar o historial, xa que supón trasladar máis revisións que o límite de $1 {{PLURAL:$1|revisión|revisións}}.",
        "mergehistory-no-source": "Non existe a páxina de orixe \"$1\".",
        "mergehistory-no-destination": "Non existe a páxina de destino \"$1\".",
        "mergehistory-invalid-source": "A páxina de orixe ten que ter un título válido.",
        "mergehistory-invalid-destination": "A páxina de destino ten que ter un título válido.",
-       "mergehistory-autocomment": "\"[[:$1]]\" fusionouse en \"[[:$2]]\"",
-       "mergehistory-comment": "\"[[:$1]]\" fusionouse en \"[[:$2]]\": $3",
-       "mergehistory-same-destination": "A orixe das páxinas e o seu destino non poden ser os mesmos",
+       "mergehistory-autocomment": "\"[[:$1]]\" fusionouse con \"[[:$2]]\"",
+       "mergehistory-comment": "\"[[:$1]]\" fusionouse con \"[[:$2]]\": $3",
+       "mergehistory-same-destination": "A páxina de orixe e a páxina de destino non pode ser a mesma",
        "mergehistory-reason": "Motivo:",
        "mergelog": "Rexistro de fusións",
-       "pagemerge-logentry": "fusionou \"[[$1]]\" con \"[[$2]]\" (revisións até o $3)",
+       "pagemerge-logentry": "fusionou \"[[$1]]\" con \"[[$2]]\" (revisións ata o $3)",
        "revertmerge": "Desfacer a fusión",
        "mergelogpagetext": "A continuación hai unha lista coas fusións máis recentes do historial dunha páxina co doutra.",
        "history-title": "Historial de revisións de \"$1\"",
        "largefileserver": "Este ficheiro é de maior tamaño ca o permitido pola configuración do servidor.",
        "emptyfile": "O ficheiro que cargou semella estar baleiro.\nIsto pode deberse a un erro ortográfico no seu nome.\nPor favor, verifique se realmente quere cargar este ficheiro.",
        "windows-nonascii-filename": "Este wiki non soporta os nomes de ficheiros con caracteres especiais.",
-       "fileexists": "Xa existe un ficheiro con ese nome. Por favor, comprobe \"<strong>[[:$1]]</strong>\" se non está seguro de querer cambialo.\n[[$1|thumb]]",
+       "fileexists": "Xa existe un ficheiro con ese nome. Por favor, comprobe \"<strong>[[:$1]]</strong>\" se non está {{GENDER:|seguro|segura}} de querer cambialo.\n[[$1|thumb]]",
        "filepageexists": "A páxina de descrición deste ficheiro xa foi creada en <strong>[[:$1]]</strong>, pero polo de agora non existe ningún ficheiro con este nome.\nO resumo que escribiu non aparecerá na páxina de descrición.\nPara facer que o resumo apareza alí, necesitará editar a páxina manualmente.\n[[$1|thumb]]",
        "fileexists-extension": "Xa existe un ficheiro cun nome semellante: [[$2|thumb]]\n* Nome do ficheiro que intenta cargar: <strong>[[:$1]]</strong>\n* Nome de ficheiro existente: <strong>[[:$2]]</strong>\nPor favor, escolla un nome diferente.",
        "fileexists-thumbnail-yes": "Semella que o ficheiro é unha imaxe de tamaño reducido ''(miniatura)''.\n[[$1|thumb]]\nPor favor, comprobe o ficheiro <strong>[[:$1]]</strong>.\nSe o ficheiro seleccionado é a mesma imaxe en tamaño orixinal non é preciso enviar unha miniatura adicional.",
        "license-nopreview": "(A vista previa non está dispoñible)",
        "upload_source_url": "  (un URL válido e accesible publicamente)",
        "upload_source_file": "  (un ficheiro no seu ordenador)",
+       "listfiles-delete": "borrar",
        "listfiles-summary": "Esta páxina especial mostra todos os ficheiros cargados.",
        "listfiles_search_for": "Buscar polo nome do ficheiro multimedia:",
        "imgfile": "ficheiro",
        "filedelete-maintenance": "Os borrados e restauracións de ficheiros están desactivados temporalmente durante o mantemento.",
        "filedelete-maintenance-title": "Non se pode borrar o ficheiro",
        "mimesearch": "Busca MIME",
-       "mimesearch-summary": "Esta páxina permite filtrar os ficheiros segundo o seu tipo MIME.\nEntrada: tipodecontido/subtipo, por exemplo <code>image/jpeg</code>.",
+       "mimesearch-summary": "Esta páxina permite filtrar os ficheiros segundo o seu tipo MIME.\nEntrada: tipodecontido/subtipo ou tipodecontido/*; por exemplo, <code>image/jpeg</code>.",
        "mimetype": "Tipo MIME:",
        "download": "descargar",
        "unwatchedpages": "Páxinas non vixiadas",
        "wantedpages-badtitle": "Título inválido fixado nos resultados: $1",
        "wantedfiles": "Ficheiros requiridos",
        "wantedfiletext-cat": "Os seguintes ficheiros están en uso, pero non existen. É posible que aparezan ficheiros de repositoroios externos, malia que existan. Calquera falso positivo estará <del>riscado</del>. Ademais, as páxinas que inclúen ficheiros que non existen están listadas en [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Os seguintes ficheiros están en uso, pero non existen. Ademais, as páxinas que inclúen ficheiros que non existen están listadas en [[:$1]].",
        "wantedfiletext-nocat": "Os seguintes ficheiros están en uso, pero non existen. É posible que aparezan ficheiros de repositoroios externos, malia que existan. Calquera falso positivo estará <del>riscado</del>.",
+       "wantedfiletext-nocat-noforeign": "Os seguintes ficheiros están en uso, pero non existen.",
        "wantedtemplates": "Modelos requiridos",
        "mostlinked": "Páxinas máis ligadas",
        "mostlinkedcategories": "Categorías máis ligadas",
-       "mostlinkedtemplates": "Modelos máis ligados",
+       "mostlinkedtemplates": "Modelos máis transcluídos",
        "mostcategories": "Páxinas con máis categorías",
        "mostimages": "Ficheiros máis usados",
        "mostinterwikis": "Páxinas con máis interwikis",
        "markedaspatrollederror-noautopatrol": "Non está permitido que un mesmo marque as propias edicións como revisadas.",
        "markedaspatrollednotify": "A modificación feita en \"$1\" marcouse como revisada.",
        "markedaspatrollederrornotify": "Erro ao marcar como revisada.",
-       "patrol-log-page": "Rexistro de revisións",
+       "patrol-log-page": "Rexistro de revisións patrulladas",
        "patrol-log-header": "Este é un rexistro das revisións patrulladas.",
        "log-show-hide-patrol": "$1 o rexistro de patrullas",
        "deletedrevision": "A revisión vella $1 foi borrada.",
        "watchlisttools-raw": "Editar a lista de vixilancia simple",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|conversa]])",
        "unknown_extension_tag": "Etiqueta de extensión descoñecida \"$1\"",
-       "duplicate-defaultsort": "'''Aviso:''' A clave de ordenación por defecto \"$2\" anula a clave de ordenación anterior por defecto \"$1\".",
+       "duplicate-defaultsort": "<strong>Aviso:</strong> A clave de ordenación por defecto \"$2\" anula a clave de ordenación anterior por defecto \"$1\".",
+       "duplicate-displaytitle": "'''Aviso:''' O título mostrado \"$2\" anula o título anterior \"$1\".",
        "version": "Versión",
        "version-extensions": "Extensións instaladas",
+       "version-skins": "Aparencias instaladas",
        "version-specialpages": "Páxinas especiais",
        "version-parserhooks": "Asociadores analíticos",
        "version-variables": "Variables",
        "version-antispam": "Prevención contra spam",
-       "version-skins": "Aparencias",
        "version-other": "Outros",
        "version-mediahandlers": "Executadores de multimedia",
        "version-hooks": "Asociadores",
        "version-hook-name": "Nome do asociador",
        "version-hook-subscribedby": "Subscrito por",
        "version-version": "($1)",
+       "version-no-ext-name": "[sen nome]",
        "version-license": "Licenza de MediaWiki",
        "version-ext-license": "Licenza",
        "version-ext-colheader-name": "Extensión",
+       "version-skin-colheader-name": "Aparencia",
        "version-ext-colheader-version": "Versión",
        "version-ext-colheader-license": "Licenza",
        "version-ext-colheader-description": "Descrición",
        "expand_templates_remove_nowiki": "Suprimir as etiquetas <nowiki> no resultado",
        "expand_templates_generate_xml": "Mostrar as árbores de análise XML",
        "expand_templates_generate_rawhtml": "Mostrar o HTML en bruto",
-       "expand_templates_preview": "Vista previa"
+       "expand_templates_preview": "Vista previa",
+       "pagelanguage": "Selector de lingua da páxina",
+       "pagelang-name": "Páxina",
+       "pagelang-language": "Lingua",
+       "pagelang-use-default": "Utilizar a lingua por defecto",
+       "pagelang-select-lang": "Seleccionar a lingua",
+       "right-pagelang": "Cambiar a lingua da páxina",
+       "action-pagelang": "cambiar a lingua da páxina",
+       "log-name-pagelang": "Rexistro de cambios de lingua",
+       "log-description-pagelang": "Este é un rexistro dos cambios na lingua das páxinas.",
+       "logentry-pagelang-pagelang": "$1 {{GENDER:$2|cambiou}} a lingua da páxina \"$3\" do $4 ao $5."
 }
index d47eab5..579df7a 100644 (file)
        "permalink": "Bschtändigi URL",
        "print": "Drucke",
        "view": "Aaluege",
+       "view-foreign": "Uf $1 aaluege",
        "edit": "Ändere",
+       "edit-local": "Lokali Bschrybig bearbeite",
        "create": "Erstelle",
+       "create-local": "Lokali Bschrybig zuefiege",
        "editthispage": "Syte bearbeite",
        "create-this-page": "Die Syte afange",
        "delete": "Lesche",
        "talkpagelinktext": "Diskussion",
        "specialpage": "Spezialsyte",
        "personaltools": "Persönlichi Wärkzüg",
-       "postcomment": "Neje Abschnitt",
        "articlepage": "Syte",
        "talk": "Diskussion",
        "views": "Wievylmol agluegt",
        "jumptonavigation": "Navigation",
        "jumptosearch": "Suech",
        "view-pool-error": "Excusez, d Server sin zur Zyt iberlaschtet.\nS versueche grad zvyl Benutzer die Syte aazluege.\nBitte wart e paar Minute, voreb Du s nomol versuechsch.\n\n$1",
+       "generic-pool-error": "Excusez, d Server sin zur Zyt iberlaschtet.\nS versueche grad zvyl Benutzer die Ressource aazluege.\nBitte wart e paar Momänt, voreb Du s nomol versuechsch.",
        "pool-timeout": "\nDi maximal Wartezyt fir e Lock isch umme",
        "pool-queuefull": "D Warteschlang isch voll",
        "pool-errorunknown": "Nit bekannte Fähler",
+       "pool-servererror": "Dr Poolzellerdienscht isch nit verfiegbar ($1).",
        "aboutsite": "Über {{GRAMMAR:akkusativ|{{SITENAME}}}}",
        "aboutpage": "Project:Über {{UCFIRST:{{GRAMMAR:akkusativ|{{SITENAME}}}}}}",
        "copyright": "Dr Inhalt vu dere Syte stoht unter dr Lizänz $1, wänn s nit andersch aagee isch.",
        "externaldberror": "Entwäder s lit e Fähler bi dr externe Authentifizierung vor, oder Du derfsch Dyy extern Benutzerkonto nid aktualisiere.",
        "login": "Aamälde",
        "nav-login-createaccount": "Aamälde / Konto aalege",
-       "loginprompt": "<small>Für di bir {{SITENAME}} aazmälde, muesch Cookies erloube!</small>",
        "userlogin": "Aamälde/Konto aalege",
        "userloginnocreate": "Aamälde",
        "logout": "Abmälde",
        "gotaccountlink": "»Login fir Benutzer, wu scho aagmäldet sin«",
        "userlogin-resetlink": "Hesch Dyy Aamäldedate vergässe?",
        "userlogin-resetpassword-link": "Passwort vergässe?",
+       "userlogin-helplink2": "Hilf bim Aamälde",
        "userlogin-loggedin": "Du bisch scho as {{GENDER:$1|$1}} aagmäldet.\nBruuch s Formular unte go Di unter eme andere Benutzername aamälde.",
        "userlogin-createanother": "En ander Benutzerkonto aalege",
        "createacct-emailrequired": "E-Mail-Adräss",
        "user-mail-no-addy": "Es isch versuecht worde e E-Mail ohni Angab vunere E-Mail-Adräss z verschigge.",
        "user-mail-no-body": "S isch versuecht wore, ne E-Mail mit eme lääre oder z churze Tekscht z verschicke.",
        "changepassword": "Passwort ändere",
-       "resetpass_announce": "Aamäldig mit em Code, wu per Mail zuegschickt woren isch. Zum d Aamäldig abzschliesse, muesch jetz e nej Passwort wehle.",
+       "resetpass_announce": "Go d Aamäldig abzschließe, muesch e nej Passwort feschtlege.",
        "resetpass_text": "<!-- Tue do dr Text ergänze -->",
        "resetpass_header": "Passwort zrucksetze",
        "oldpassword": "Alts Passwort",
        "retypenew": "Nöis Passwort (es zwöits Mal)",
        "resetpass_submit": "Passwort ibermittle un aamälde",
        "changepassword-success": "Dyy Passwort isch erfolgryych gänderet wore.",
+       "changepassword-throttled": "Du hesch z vilmol versuecht Di aazmälde. Bitte wart $1, voreb Du s non emol versuechsch.",
        "resetpass_forbidden": "S Passwort cha nid gänderet wäre.",
        "resetpass-no-info": "Du muesch Di aamälde zum uf die Syte diräkt zuegryfe z chenne.",
        "resetpass-submit-loggedin": "Passwort ändere",
        "resetpass-submit-cancel": "Abbräche",
        "resetpass-wrong-oldpass": "S temporär oder aktuäll Passwort isch nimi giltig.\nVillicht hesch Dyy Passwort scho gänderet oder e nej temporär Passwort aagforderet.",
+       "resetpass-recycled": "Bitte due Dy Passwort ändere.",
+       "resetpass-temp-emailed": "Du hesch Di mit eme temporäre E-Mail-Code aagmäldet.\nGo d Aamäldig abzschließe, muesch e jetz no ne nej Passwort feschtlege:",
        "resetpass-temp-password": "Temporär Passwort:",
        "resetpass-abort-generic": "D Passwortänderig isch dur e Erwyterig abbroche wore.",
+       "resetpass-expired": "Dy Passwort isch abglofe. Bitte leg e nej Passwort fir d Aamäldig fescht.",
        "passwordreset": "Passwort zruggsetze",
        "passwordreset-text-one": "Fill des Formular uus go Dy Passwort zrucksetze.",
        "passwordreset-text-many": "{{PLURAL:$1|Fill eis vu dr Fälder uus go Dy Passwort zrucksetze.}}",
        "duplicate-defaultsort": "Obacht: Dr Sortierigsschlüssel „$2“ iberschrybt dr vorig brucht Schlüssel „$1“.",
        "version": "Version",
        "version-extensions": "Installierti Erwyterige",
+       "version-skins": "Benutzeroberflechine",
        "version-specialpages": "Spezialsyte",
        "version-parserhooks": "Parser-Schnittstelle",
        "version-variables": "Variable",
        "version-antispam": "Spamschutz",
-       "version-skins": "Benutzeroberflechine",
        "version-other": "Anders",
        "version-mediahandlers": "Medie-Handler",
        "version-hooks": "Schnittstelle ''(Hook)''",
index 6116884..ce59917 100644 (file)
        "talkpagelinktext": "שיחה",
        "specialpage": "דף מיוחד",
        "personaltools": "כלים אישיים",
-       "postcomment": "פסקה חדשה",
        "articlepage": "צפייה בדף התוכן",
        "talk": "שיחה",
        "views": "צפיות",
        "externaldberror": "הייתה שגיאה בבסיס הנתונים של ההזדהות, או שאינכם רשאים לעדכן את חשבונכם החיצוני.",
        "login": "כניסה לחשבון",
        "nav-login-createaccount": "כניסה לחשבון / הרשמה",
-       "loginprompt": "לפני הכניסה לחשבון ב{{grammar:תחילית|{{SITENAME}}}}, עליכם לוודא כי ה\"עוגיות\" (Cookies) מופעלות.",
        "userlogin": "כניסה לחשבון / הרשמה",
        "userloginnocreate": "כניסה לחשבון",
        "logout": "יציאה מהחשבון",
        "wantedpages-badtitle": "כותרת בלתי תקינה ברשימת התוצאות: $1",
        "wantedfiles": "קבצים מבוקשים",
        "wantedfiletext-cat": "הקבצים הבאים נמצאים בשימוש, אך אינם קיימים. ייתכן שקבצים ממאגרים חיצוניים יהיו רשומים אף על פי שהם קיימים, אך שגיאות כאלה יהיו <del>מחוקות</del>. בנוסף, דפים שמשתמשים בקבצים שאינם קיימים רשומים בדף [[:$1]].",
+       "wantedfiletext-cat-noforeign": "הקבצים הבאים נמצאים בשימוש, אבל אינם קיימים. כמו־כן, דפים שמשתמשים בקבצים שאינם קיימים רשומים בדף [[:$1]].",
        "wantedfiletext-nocat": "הקבצים הבאים נמצאים בשימוש, אך אינם קיימים. ייתכן שקבצים ממאגרים חיצוניים יהיו רשומים אף על פי שהם קיימים, אך שגיאות כאלה יהיו <del>מחוקות</del>.",
+       "wantedfiletext-nocat-noforeign": "הקבצים הבאים נמצאים בשימוש, אבל אינם קיימים.",
        "wantedtemplates": "תבניות מבוקשות",
        "mostlinked": "הדפים המקושרים ביותר",
        "mostlinkedcategories": "הקטגוריות המקושרות ביותר",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|שיחה]])",
        "unknown_extension_tag": "תגית בלתי ידועה: \"$1\"",
        "duplicate-defaultsort": "'''אזהרה:''' המיון הרגיל \"$2\" דורס את המיון הרגיל המוקדם ממנו \"$1\".",
+       "duplicate-displaytitle": "<strong>אזהרה:</strong> כותרת התצוגה \"$2\" דורסת את כותרת התצוגה הקודמת \"$1\".",
        "version": "גרסת התוכנה",
        "version-extensions": "הרחבות מותקנות",
        "version-skins": "עיצובים מותקנים",
index f2cf581..609b3de 100644 (file)
        "talkpagelinktext": "razgovor",
        "specialpage": "Posebna stranica",
        "personaltools": "Osobni alati",
-       "postcomment": "Novi odlomak",
        "articlepage": "Vidi članak",
        "talk": "Razgovor",
        "views": "Pogledi",
        "externaldberror": "Došlo je do pogreške s vanjskom autorizacijom ili Vam nije dopušteno osvježavanje vanjskog suradničkog računa.",
        "login": "Prijavi se",
        "nav-login-createaccount": "Prijavi se",
-       "loginprompt": "Za prijavu na sustav {{SITENAME}} morate u pregledniku uključiti kolačiće (cookies).",
        "userlogin": "Prijavi se / stvori račun",
        "userloginnocreate": "Prijavi se",
        "logout": "Odjavi se",
index 6f410c1..c56948a 100644 (file)
        "talkpagelinktext": "vitalap",
        "specialpage": "Speciális lap",
        "personaltools": "Személyes eszközök",
-       "postcomment": "Új szakasz",
        "articlepage": "Szócikk megtekintése",
        "talk": "Vitalap",
        "views": "Nézetek",
        "externaldberror": "Hiba történt a külső adatbázis hitelesítése közben, vagy nem vagy jogosult a külső fiókod frissítésére.",
        "login": "Bejelentkezés",
        "nav-login-createaccount": "Bejelentkezés / fiók létrehozása",
-       "loginprompt": "Engedélyezned kell a sütiket (''cookie''), hogy bejelentkezhess a(z) {{SITENAME}} wikibe.",
        "userlogin": "Bejelentkezés / fiók létrehozása",
        "userloginnocreate": "Bejelentkezés",
        "logout": "Kijelentkezés",
        "sp-contributions-search": "Közreműködések szűrése",
        "sp-contributions-username": "IP-cím vagy felhasználónév:",
        "sp-contributions-toponly": "Csak a jelenleg utolsónak számító változtatásokat mutassa",
+       "sp-contributions-newonly": "Csak az új oldalt létrehozó szerkesztéseket mutassa.",
        "sp-contributions-submit": "Keresés",
        "whatlinkshere": "Mi hivatkozik erre",
        "whatlinkshere-title": "A(z) „$1” lapra hivatkozó lapok",
index ca955f2..f6e3c30 100644 (file)
        "talkpagelinktext": "Discussion",
        "specialpage": "Pagina special",
        "personaltools": "Instrumentos personal",
-       "postcomment": "Nove section",
        "articlepage": "Vider pagina de contento",
        "talk": "Discussion",
        "views": "Representationes",
        "externaldberror": "O il occurreva un error in le base de datos de authentication, o tu non ha le autorisation de actualisar tu conto externe.",
        "login": "Aperir session",
        "nav-login-createaccount": "Aperir session / crear conto",
-       "loginprompt": "Tu debe haber activate le cookies pro poter aperir un session in {{SITENAME}}.",
        "userlogin": "Aperir session / crear conto",
        "userloginnocreate": "Aperir session",
        "logout": "Clauder session",
        "license-nopreview": "(Previsualisation non disponibile)",
        "upload_source_url": " (un adresse URL valide e publicamente accessibile)",
        "upload_source_file": " (un file in tu computator)",
+       "listfiles-delete": "deler",
        "listfiles-summary": "Iste pagina special monstra tote le files incargate.",
        "listfiles_search_for": "Cercar un nomine de media:",
        "imgfile": "file",
        "wantedpages-badtitle": "Titulo invalide in le gruppo de resultatos: $1",
        "wantedfiles": "Files desirate",
        "wantedfiletext-cat": "Le sequente files es usate ma non existe. Le files ab repositorios externe pote esser listate malgrado que illos existe. Omne tal false positivos essera <del>cancellate</del>. In addition, paginas que incorpora files que non existe es listate in [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Le sequente files es usate ma non existe. In addition, paginas que incorpora files que non existe es listate in [[:$1]].",
        "wantedfiletext-nocat": "Le sequente files es usate ma non existe. Files ab repositorios externe pote esser listate malgrado que illos existe. Omne tal false positivos essera <del>cancellate</del>.",
+       "wantedfiletext-nocat-noforeign": "Le sequente files es usate ma non existe.",
        "wantedtemplates": "Patronos desirate",
        "mostlinked": "Paginas le plus ligate",
        "mostlinkedcategories": "Categorias le plus ligate",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|discussion]])",
        "unknown_extension_tag": "Etiquetta de extension incognite \"$1\"",
        "duplicate-defaultsort": "Attention: Le clave de ordination predefinite \"$2\" supplanta le anterior clave de ordination predefinite \"$1\".",
+       "duplicate-displaytitle": "<strong>Attention:</strong> Le titulo a monstrar \"$2\" supplanta le ancian titulo a monstrar \"$1\".",
        "version": "Version",
        "version-extensions": "Extensiones installate",
        "version-skins": "Apparentias installate",
        "pagelang-use-default": "Usar lingua predefinite",
        "pagelang-select-lang": "Selige lingua",
        "right-pagelang": "Cambiar lingua del pagina",
-       "action-pagelang": "Cambiar lingua del pagina",
+       "action-pagelang": "cambiar le lingua del pagina",
        "log-name-pagelang": "Registro de cambios de lingua",
        "log-description-pagelang": "Isto es un registro de cambios de lingua in paginas.",
        "logentry-pagelang-pagelang": "$1 {{GENDER:$2|cambiava}} le lingua del pagina $3 de $4 a $5."
index b23e77e..87985cc 100644 (file)
        "talkpagelinktext": "bicara",
        "specialpage": "Halaman istimewa",
        "personaltools": "Peralatan pribadi",
-       "postcomment": "Bagian baru",
        "articlepage": "Lihat halaman isi",
        "talk": "Pembicaraan",
        "views": "Tampilan",
        "externaldberror": "Telah terjadi kesalahan otentikasi basis data eksternal atau Anda tidak diizinkan melakukan kemaskini terhadap akun eksternal Anda.",
        "login": "Masuk log",
        "nav-login-createaccount": "Masuk log / buat akun",
-       "loginprompt": "Anda harus mengaktifkan kuki untuk dapat masuk log ke {{SITENAME}}.",
        "userlogin": "Masuk log / buat akun",
        "userloginnocreate": "Masuk log",
        "logout": "Keluar log",
        "mergehistory-empty": "Tidak ada revisi yang dapat digabung.",
        "mergehistory-success": "$3 {{PLURAL:$3|revisi|revisi}} dari [[:$1]] berhasil digabungkan ke [[:$2]].",
        "mergehistory-fail": "Tidak dapat melakukan penggabungan, harap periksa kembali halaman dan parameter waktu.",
+       "mergehistory-fail-toobig": "Tidak dapat melakukan penggabungan sebagai lebih dari batas dari $1 {{PLURAL:$1|revisi|revisi}} akan dipindahkan.",
        "mergehistory-no-source": "Halaman sumber $1 tidak ada.",
        "mergehistory-no-destination": "Halaman tujuan $1 tidak ada.",
        "mergehistory-invalid-source": "Judul halaman sumber haruslah judul yang berlaku.",
        "largefileserver": "Berkas ini lebih besar dari pada yang diizinkan server.",
        "emptyfile": "Berkas yang Anda muatkan kelihatannya kosong. Hal ini mungkin disebabkan karena adanya kesalahan ketik pada nama berkas. Silakan pastikan apakah Anda benar-benar ingin memuatkan berkas ini.",
        "windows-nonascii-filename": "Wiki ini tidak mendukung nama berkas dengan karakter istimewa.",
-       "fileexists": "Suatu berkas dengan nama tersebut telah ada, harap periksa <strong>[[:$1]]</strong> jika Anda tidak yakin untuk mengubahnya.\n[[$1|thumb]]",
+       "fileexists": "Suatu berkas dengan nama tersebut telah ada, harap periksa <strong>[[:$1]]</strong> jika {{GENDER:|Anda}} tidak yakin untuk mengubahnya.\n[[$1|thumb]]",
        "filepageexists": "Halaman deskripsi untuk berkas ini telah dibuat di <strong>[[:$1]]</strong>, tapi saat ini tak ditemukan berkas dengan nama tersebut. Ringkasan yang Anda masukkan tidak akan tampil pada halaman deskripsi. Untuk memunculkannya, Anda perlu untuk menyuntingnya secara manual.\n[[$1|thumb]]",
-       "fileexists-extension": "Berkas dengan nama serupa telah ada: [[$2|thumb]]\n* Nama berkas yang akan dimuat: <strong>[[:$1]]</strong>\n* Nama berkas yang telah ada: <strong>[[:$2]]</strong>\nMohon gunakan nama yang berbeda.",
+       "fileexists-extension": "Berkas dengan nama serupa telah ada: [[$2|thumb]]\n* Nama berkas yang akan dimuat: <strong>[[:$1]]</strong>\n* Nama berkas yang telah ada: <strong>[[:$2]]</strong>\nApakah Anda mungkin ingin menggunakan nama yang lebih khas?",
        "fileexists-thumbnail-yes": "Berkas ini tampaknya merupakan gambar yang ukurannya diperkecil ''(miniatur)''. [[$1|thumb]]\nHarap periksa berkas <strong>[[:$1]]</strong> tersebut.\nJika berkas tersebut memang merupakan gambar dalam ukuran aslinya, Anda tidak perlu untuk memuat kembali miniatur lainnya.",
        "file-thumbnail-no": "Nama berkas dimulai dengan <strong>$1</strong>.\nTampaknya berkas ini merupakan gambar dengan ukuran diperkecil ''(miniatur)''.\nJika Anda memiliki versi resolusi penuh dari gambar ini, harap muatkan berkas tersebut. Jika tidak, harap ubah nama berkas ini.",
        "fileexists-forbidden": "Suatu berkas dengan nama ini telah ada dan tak dapat ditimpa.\nJika Anda masih ingin memuat berkas Anda, silakan kembali dan gunakan nama baru. [[File:$1|thumb|center|$1]]",
        "license-nopreview": "(Pratayang tak tersedia)",
        "upload_source_url": " (suatu URL valid yang dapat diakses publik)",
        "upload_source_file": " (suatu berkas di komputer Anda)",
+       "listfiles-delete": "hapus",
        "listfiles-summary": "Halaman istimewa ini menampilkan semua berkas yang telah diunggah.\nKetika disaring oleh pengguna, hanya versi berkas terbaru dari berkas yang diunggah oleh pengguna tersebut yang ditampilkan.",
        "listfiles_search_for": "Cari nama berkas:",
        "imgfile": "berkas",
        "filedelete-maintenance": "Penghapusan dan pengembalian berkas sementara dinonaktifkan selama perawatan.",
        "filedelete-maintenance-title": "Tidak dapat menghapus berkas",
        "mimesearch": "Pencarian MIME",
-       "mimesearch-summary": "Halaman ini menyediakan fasilitas menyaring berkas berdasarkan tipe MIME-nya. Masukkan: contenttype/subtype, misalnya <code>image/jpeg</code>.",
+       "mimesearch-summary": "Halaman ini menyediakan fasilitas menyaring berkas berdasarkan tipe MIME-nya. Masukkan: contenttype/subtype atau contenttype/*, misalnya <code>image/jpeg</code>.",
        "mimetype": "Tipe MIME:",
        "download": "unduh",
        "unwatchedpages": "Halaman yang tak dipantau",
        "wantedpages-badtitle": "Judul tak valid dalam himpunan hasil: $1",
        "wantedfiles": "Berkas yang diinginkan",
        "wantedfiletext-cat": "Berkas-berkas berikut digunakan tetapi tidak ada. Berkas dari repositori asing mungkin tercantum meskipun ada. Setiap \"false positive\" akan <del>dicoret</del>. Selain itu, halaman yang menggunakan berkas yang tidak ada akan dicantumkan dalam [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Berkas berikut ini digunakan tetapi tidak ada. Selain itu, halaman yang menanamkan berkas yang tidak ada tercantum dalam [[:$1]].",
        "wantedfiletext-nocat": "Berkas-berkas berikut digunakan tetapi tidak ada. Berkas dari repositori asing mungkin tercantum meskipun ada. Setiap \"false positive\" akan <del>dicoret</del>.",
+       "wantedfiletext-nocat-noforeign": "Berkas berikut ini digunakan tetapi tidak ada.",
        "wantedtemplates": "Templat yang diinginkan",
        "mostlinked": "Halaman yang tersering dituju",
        "mostlinkedcategories": "Kategori yang tersering digunakan",
-       "mostlinkedtemplates": "Templat yang tersering digunakan",
+       "mostlinkedtemplates": "Halaman yang paling ditransklusikan",
        "mostcategories": "Halaman dengan kategori terbanyak",
        "mostimages": "Berkas yang tersering digunakan",
        "mostinterwikis": "Halaman dengan interwiki terbanyak",
        "timezone-utc": "UTC",
        "unknown_extension_tag": "Tag ekstensi tidak dikenal \"$1\"",
        "duplicate-defaultsort": "Peringatan: Kunci pengurutan baku \"$2\" mengabaikan kunci pengurutan baku \"$1\" sebelumnya.",
+       "duplicate-displaytitle": "<strong>Peringatan:</strong> Menampilkan judul \"$2\" menimpa judul tampilan \"$1\" sebelumnya.",
        "version": "Versi",
        "version-extensions": "Ekstensi terinstal",
-       "version-skins": "Kulit",
+       "version-skins": "Kulit yang diinstal",
        "version-specialpages": "Halaman istimewa",
        "version-parserhooks": "Kait parser",
        "version-variables": "Variabel",
        "version-hook-name": "Nama kait",
        "version-hook-subscribedby": "Dilanggani oleh",
        "version-version": "(Versi $1)",
+       "version-no-ext-name": "[tanpa nama]",
        "version-svn-revision": "(r$2)",
        "version-license": "Lisensi MediaWiki",
        "version-ext-license": "Lisensi",
        "version-ext-colheader-name": "Ekstensi",
+       "version-skin-colheader-name": "Kulit",
        "version-ext-colheader-version": "Versi",
        "version-ext-colheader-license": "Lisensi",
        "version-ext-colheader-description": "Deskripsi",
        "expand_templates_remove_nowiki": "Tidak menampilkan tag <nowiki> pada hasilnya",
        "expand_templates_generate_xml": "Tampilkan pohon parser XML",
        "expand_templates_generate_rawhtml": "Tampilkan HTML mentah",
-       "expand_templates_preview": "Pratayang"
+       "expand_templates_preview": "Pratayang",
+       "pagelanguage": "Pemilih bahasa halaman",
+       "pagelang-name": "Halaman",
+       "pagelang-language": "Bahasa",
+       "pagelang-use-default": "Gunakan bahasa default",
+       "pagelang-select-lang": "Pilih bahasa",
+       "right-pagelang": "Ubah bahasa halaman",
+       "action-pagelang": "mengubah bahasa halaman",
+       "log-name-pagelang": "Ubah bahasa log",
+       "log-description-pagelang": "Ini adalah log perubahan dalam bahasa halaman.",
+       "logentry-pagelang-pagelang": "$1 {{GENDER:$2|mengubah}} bahasa halaman $3 dari $4 menjadi $5."
 }
index 617110a..7bea4ef 100644 (file)
@@ -56,7 +56,7 @@
        "editfont-monospace": "Monospaced a kita ti letra",
        "editfont-sansserif": "Sans-serif a kita ti letra",
        "editfont-serif": "Serif a kita ti letra",
-       "sunday": "Dominggo",
+       "sunday": "Domingo",
        "monday": "Lunes",
        "tuesday": "Martes",
        "wednesday": "Mierkoles",
@@ -66,9 +66,9 @@
        "sun": "Dom",
        "mon": "Lun",
        "tue": "Mar",
-       "wed": "Mie",
+       "wed": "Mier",
        "thu": "Hue",
-       "fri": "Bie",
+       "fri": "Bier",
        "sat": "Sab",
        "january": "Enero",
        "february": "Pebrero",
        "talkpagelinktext": "Tungtungan",
        "specialpage": "Espesial a panid",
        "personaltools": "Bukod a ram-ramit",
-       "postcomment": "Baro a paset",
        "articlepage": "Kitaen ti naglaon a panid",
        "talk": "Pagtungtungan",
        "views": "Dagiti pangkitaan",
        "externaldberror": "Adda biddut idi ti panakapasingked ti database wenno saanmo a mabalin ti agpabaro ti bukodmo nga akin-ruar a pakabilangan.",
        "login": "Sumrek",
        "nav-login-createaccount": "Sumrek / agaramid ti pakabilangan",
-       "loginprompt": "Nasken a napakabaelam dagiti \"galietas\" tapno makastrekka iti {{SITENAME}}.",
        "userlogin": "Sumrek / agaramid ti pakabilangan",
        "userloginnocreate": "Sumrek",
        "logout": "Rummuar",
        "mergehistory-empty": "Awan dagiti mabalin nga itipon ti panagbalbaliw.",
        "mergehistory-success": "$3 {{PLURAL:$3|a binaliwan|dagiti binaliwan}} ti [[:$1]] balligi ti panagitipon idiay [[:$2]].",
        "mergehistory-fail": "Saan a nakaaramid ti panagtipon ti pakasaritaan, pangngaasi ta kitaen ti panid ken parametro ti oras.",
+       "mergehistory-fail-toobig": "Di naaramid ti panagtipon ti pakasaritaan gapu ta ad-adu ti patingga ti $1 {{PLURAL:$1|a rebision|kadagiti rebision}} ti maiyalisto.",
        "mergehistory-no-source": "Awan ti taudan ti panid a $1.",
        "mergehistory-no-destination": "Awan ti papanan ti panid a $1.",
        "mergehistory-invalid-source": "Masapul nga adda ti umisu a titulo ti taudan ti panid.",
        "largefileserver": "Daytoy a papeles ket dakdakel ngem ti naaramid a mabalin para iti server.",
        "emptyfile": "Ti papeles nga ipanmo ket kasla awan ti nagyan na.\nBaka daytoy ket gapu ti kamali ti inkabil a nagan ti papeles.\nPangngaasi ta kitaem no kayatmo latta nga ipapan daytoy a papeles.",
        "windows-nonascii-filename": "Daytoy a wiki ket saanna a suportaran dagiti nagan ti papeles nga addaan kadagiti espesial a karakter.",
-       "fileexists": "Ti papeles nga agnagan ti kastoy ket addan, pangngaasi a kitaem ti <strong>[[:$1]]</strong> no saanka a sigurado no kayatmo a sukatan.\n[[$1|thumb]]",
+       "fileexists": "Ti papeles nga agnagan ti kastoy ket addan, pangngaasi a kitaem ti <strong>[[:$1]]</strong> no {{GENDER:|saanka}} a sigurado no kayatmo a sukatan.\n[[$1|thumb]]",
        "filepageexists": "Ti panangipalpalawag a panid para iti daytoy a papeles ket naaramiden idiay <strong>[[:$1]]</strong>, ngem awan ti agdama nga agnagan ti kastoy a papeles.\nTi pakabuklan nga inkabilmo ket saan nga agparang idiay deskripsion ti panid.\nTapno agparang ti pakabuklan idiay, masapul a manual a baliwam.\n[[$1|thumb]]",
-       "fileexists-extension": "Adda papeles nga agnagan ti kastoy: [[$2|thumb]]\n* Nagan ti naipapan a papeles: <strong>[[:$1]]</strong>\n* Nagan ti adda a papeles: <strong>[[:$2]]</strong>\nPangngaasi nga agpili ti sabali a nagan.",
+       "fileexists-extension": "Adda papeles nga agnagan ti kastoy: [[$2|thumb]]\n* Nagan ti naipapan a papeles: <strong>[[:$1]]</strong>\n* Nagan ti adda a papeles: <strong>[[:$2]]</strong>\nKayatmo kadi ti agusar ti naisangsangayan a nagan?",
        "fileexists-thumbnail-yes": "Daytoy a papeles ket kasla ladawan a napabassit ''(thumbnail)''.\n[[$1|thumb]]\nPangngaasi a kitaem ti papeles a <strong>[[:$1]]</strong>.\nNo ti nakitam a papeles ket isu ti ladawan iti dati a kadakkel saanen a nasken ti agipan ti maysa a napabassit a ladawan.",
        "file-thumbnail-no": "Ti nagan ti papeles ket mangrugi iti <strong>$1</strong>.\nKasla ladawan a napabassit ''(thumbnail)''.\nNo addaanka ti napno a resolusion ipanmo daytoy, no saan pangngaasi a sukatam ti nagan ti papeles.",
        "fileexists-forbidden": "Daytoy a nagan ti papeles ket adda dita, ken saan a mabalin a masuratan manen.\nNo kayatmo pay latta nga ipan ti papeles, pangngaasi nga agsublika ken usarem ti baro a nagan.\n[[File:$1|thumb|center|$1]]",
        "license-nopreview": "(Saan a mabalin nga ipadas)",
        "upload_source_url": " (maysa nga umisu, ken maserrekan ti publiko nga URL)",
        "upload_source_file": "(papeles iti kompiutermo)",
+       "listfiles-delete": "ikkaten",
        "listfiles-summary": "Daytoy nga espesial a panid ket agiparang kadagiti amin a naipan a papeles.",
        "listfiles_search_for": "Agsapul para iti nagan ti midia:",
        "imgfile": "papeles",
        "filedelete-maintenance": "Ti panagikkat ken panagisubli kadagiti papaeles ket nabaldado iti las-ud ti panagtartaripato.",
        "filedelete-maintenance-title": "Saan a maikkat daytoy a papeles",
        "mimesearch": "Pagbiruk ti MIME",
-       "mimesearch-summary": "Daytoy a panid ket pakabaelanna ti panagsagat ti papeles iti MIME a kitada.\nIkabil: kita ti nagyan/subtipo, a kas ti <code>image/jpeg</code>.",
+       "mimesearch-summary": "Daytoy a panid ket pakabaelanna ti panagsagat ti papeles iti MIME a kitada.\nIkabil: kita ti nagyan/subtipo wenno kita ti linaon/*, a kas ti <code>image/jpeg</code>.",
        "mimetype": "Kita ti MIME:",
        "download": "ikarga",
        "unwatchedpages": "Di mabambantayan a pampanid",
        "wantedpages-badtitle": "Saan nga umisu a titulo idiay naikabil a pagbanagan: $1",
        "wantedfiles": "Dagiti makiddaw a papeles",
        "wantedfiletext-cat": "Dagiti sumaganad a papeles ket maus-usar ngem awanda met. Dagiti papeles a naggapu kadagiti ganganaet a repositorio ket mailista uray pay no addaan da. No adda dagiti kasla adda dagitoy ket <del>maikkat</del> to. A maipanayon pay, dagiti pampanid nga agisengngat kadagiti papeles nga awan ket nailista idiay [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Dagiti sumaganad a papeles ket naus-usar ngem awanda met. Iti pay maipatinayon, dagiti panid a mangipenpen kadagiti papeles ket nailista idiay [[:$1]].",
        "wantedfiletext-nocat": "Dagiti sumaganad a papeles ket maus-usar ngem awanda met. Dagiti papeles a naggapu kadagiti ganganaet a repositorio ket mailista uray pay no addaan da. No adda dagiti kasla adda dagitoy ket <del>maikkat</del> to.",
+       "wantedfiletext-nocat-noforeign": "Dagiti sumaganad a papeles ket naus-usar ngem awanda met",
        "wantedtemplates": "Dagiti makiddaw a plantilia",
        "mostlinked": "Dagiti panid a kaaduan iti nakasilpo",
        "mostlinkedcategories": "Dagiti kategoria a kaaduan iti nakasilpo",
-       "mostlinkedtemplates": "Dagiti plantilia a kaaduan iti nakasilpo",
+       "mostlinkedtemplates": "Kaaduan a nailak-am a pampanid",
        "mostcategories": "Dagiti panid a kaaduan kadagiti kategoria",
        "mostimages": "Dagiti papeles a kaaduan iti nakasilpo",
        "mostinterwikis": "Dagiti panid a kaaduan kadagiti interwiki",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|tungtungan]])",
        "unknown_extension_tag": "Di ammo a pagpaatiddog nga etiketa \"$1\"",
        "duplicate-defaultsort": "'''Ballaag:''' Kinasigud a panagilasin ti \"$2\" ket sukatanna ti immuna a kinasigud a panagilasin \"$1\".",
+       "duplicate-displaytitle": "<strong>Ballaag:</strong> Ti maiparang a titulo ti \"$2\" ket tuonanna ti immmuna a maiparang a titulo ti \"$1\".",
        "version": "Bersion",
        "version-extensions": "Dagiti naisaad a pagpaatiddog",
+       "version-skins": "Naisaad a kudkudil",
        "version-specialpages": "Espesial a pampanid",
        "version-parserhooks": "Dagiti parser a kawit",
        "version-variables": "Nadumaduma a kita",
        "version-antispam": "Pawilan ti spam",
-       "version-skins": "Dagiti Kudil",
        "version-other": "Sabali",
        "version-mediahandlers": "Agtengtengngel kadagiti midia",
        "version-hooks": "Dagiti kawit",
        "version-hook-name": "Nagan ti kawit",
        "version-hook-subscribedby": "Umanamong babaen ti",
        "version-version": "($1)",
+       "version-no-ext-name": "[awan nagan]",
        "version-license": "Lisensia ti MediaWiki",
        "version-ext-license": "Lisensia",
        "version-ext-colheader-name": "Pagpaatiddog",
+       "version-skin-colheader-name": "Kudil",
        "version-ext-colheader-version": "Bersion",
        "version-ext-colheader-license": "Lisensia",
        "version-ext-colheader-description": "Deskripsion",
        "expand_templates_remove_nowiki": "Parmeken dagiti <nowiki> nga etiketa kadagiti nagbanagan",
        "expand_templates_generate_xml": "Iparang ti XML parse a kayo",
        "expand_templates_generate_rawhtml": "Ipakita ti naata a HTML",
-       "expand_templates_preview": "Pamadasan"
+       "expand_templates_preview": "Pamadasan",
+       "pagelanguage": "Pagpilian ti pagsasao ti panid",
+       "pagelang-name": "Panid",
+       "pagelang-language": "Pagsasao",
+       "pagelang-use-default": "Usaren ti kasisigud a pagsasao",
+       "pagelang-select-lang": "Agpili iti pagsasao",
+       "right-pagelang": "Baliwan ti pagsasao ti panid",
+       "action-pagelang": "baliwan ti pagsasao ti panid",
+       "log-name-pagelang": "Baliwan ti pagsasao ti listaan",
+       "log-description-pagelang": "Daytoy ket listaan dagiti binaliwan kadagiti pagsasao ti panid.",
+       "logentry-pagelang-pagelang": "NI $1 ket {{GENDER:$2|binaliwanna}} ti pagsasao ti panid para iti $3 manipud ti $4 iti $5."
 }
index 2837738..68d3030 100644 (file)
        "talkpagelinktext": "Spjall",
        "specialpage": "Kerfissíða",
        "personaltools": "Tenglar",
-       "postcomment": "Nýr hluti",
        "articlepage": "Sýna núverandi síðu",
        "talk": "Spjall",
        "views": "Sýn",
        "externaldberror": "Uppfærsla mistókst. Annaðhvort varð villa í gagnasafninu eða að þér sé óheimilt að uppfæra aðra aðganga.",
        "login": "Innskrá",
        "nav-login-createaccount": "Innskrá / Búa til aðgang",
-       "loginprompt": "Þú verður að leyfa vefkökur til þess að geta skráð þig inn á {{SITENAME}}.",
        "userlogin": "Innskrá / Búa til aðgang",
        "userloginnocreate": "Innskrá",
        "logout": "Útskráning",
index 7172181..b933c9d 100644 (file)
        "talkpagelinktext": "Discussione",
        "specialpage": "Pagina speciale",
        "personaltools": "Strumenti personali",
-       "postcomment": "Nuova sezione",
        "articlepage": "Visualizza la voce",
        "talk": "Discussione",
        "views": "Visite",
        "externaldberror": "Si è verificato un errore con il server di autenticazione esterno, oppure non si dispone delle autorizzazioni necessarie per aggiornare il proprio accesso esterno.",
        "login": "Entra",
        "nav-login-createaccount": "Entra / registrati",
-       "loginprompt": "Per accedere a {{SITENAME}} è necessario abilitare i cookie.",
        "userlogin": "Entra / registrati",
        "userloginnocreate": "Entra",
        "logout": "Esci",
        "wantedpages": "Pagine più richieste",
        "wantedpages-badtitle": "Titolo non valido nel gruppo di risultati: $1",
        "wantedfiles": "File richiesti",
-       "wantedfiletext-cat": "I seguenti file sono richiamati da wikilink, ma non esistono. I file ospitati su repository esterni potrebbero essere elencati anche se di fatto esistenti. Questi falsi positivi saranno <del>barrati</del>. Le pagine che incorporano i file che non esistono sono elencate in [[:$1]].",
+       "wantedfiletext-cat": "I seguenti file sono utilizzati, ma non esistono. I file ospitati su repository esterni potrebbero essere elencati anche se di fatto esistenti. Questi falsi positivi saranno <del>barrati</del>. Le pagine che incorporano i file che non esistono sono elencate in [[:$1]].",
+       "wantedfiletext-cat-noforeign": "I seguenti file sono utilizzati, ma non esistono. Inoltre, le pagine che incorporano questi file sono elencate nella [[:$1]].",
        "wantedfiletext-nocat": "I seguenti file sono richiamati da wikilink, ma non esistono. I file ospitati su repository esterni potrebbero essere elencati anche se di fatto esistenti. Questi falsi positivi saranno <del>barrati</del>.",
+       "wantedfiletext-nocat-noforeign": "I seguenti file sono utilizzati, ma non esistono.",
        "wantedtemplates": "Template richiesti",
        "mostlinked": "Pagine più richiamate",
        "mostlinkedcategories": "Categorie più richiamate",
-       "mostlinkedtemplates": "Pagine più inlcuse",
+       "mostlinkedtemplates": "Pagine più incluse",
        "mostcategories": "Pagine con più categorie",
        "mostimages": "File più richiamati",
        "mostinterwikis": "Pagine con più interwiki",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|discussioni]])",
        "unknown_extension_tag": "Tag estensione sconosciuto: \"$1\"",
        "duplicate-defaultsort": "Attenzione: la chiave di ordinamento predefinita \"$2\" sostituisce la precedente \"$1\".",
+       "duplicate-displaytitle": "<strong>Attenzione:</strong> il titolo visualizzato \"$2\" sostituisce il precedente titolo \"$1\".",
        "version": "Versione",
        "version-extensions": "Estensioni installate",
        "version-skins": "Skin installate",
index 4457755..049f394 100644 (file)
        "newwindow": "(新しいウィンドウで開きます)",
        "cancel": "中止",
        "moredotdotdot": "続き...",
-       "morenotlisted": "ã\81\93ã\81®ä¸\80覧ã\81®ç¶\9aã\81\8d",
+       "morenotlisted": "ã\81\93ã\81®ä¸\80覧ã\81¯å®\8cå\85¨ã\81§ã\81¯ã\81\82ã\82\8aã\81¾ã\81\9bã\82\93ã\80\82",
        "mypage": "ページ",
        "mytalk": "トーク",
        "anontalk": "このIPアドレスのトーク",
        "talkpagelinktext": "トーク",
        "specialpage": "特別ページ",
        "personaltools": "個人用ツール",
-       "postcomment": "新しい節",
        "articlepage": "本文を表示",
        "talk": "議論",
        "views": "表示",
        "externaldberror": "認証データベースでエラーが発生した、または外部アカウントの更新が許可されていません。",
        "login": "ログイン",
        "nav-login-createaccount": "ログインまたはアカウント作成",
-       "loginprompt": "{{SITENAME}}にログインするにはCookieを有効にする必要があります。",
        "userlogin": "ログインまたはアカウント作成",
        "userloginnocreate": "ログイン",
        "logout": "ログアウト",
index c5d401d..7006a1e 100644 (file)
        "talkpagelinktext": "Hurênayis",
        "specialpage": "Pela xısusiye",
        "personaltools": "Hacetê keşi",
-       "postcomment": "Qısımo newe",
        "articlepage": "Pela zerreki bıvêne",
        "talk": "Hurênais",
        "views": "Asaişi",
        "externaldberror": "Cıfeteliyaisê naskerdene de ya xeta esta ya ki tebera vırastena hesabê sıma rê destur çino.",
        "login": "Cı kuye",
        "nav-login-createaccount": "Cı kuye / hesab vıraze",
-       "loginprompt": "Cıkotena {{SITENAME}} rê gunê ''cookies'' akerdey bê.",
        "userlogin": "Cı kuye / hesab vıraze",
        "userloginnocreate": "Cı kuye",
        "logout": "Veciye",
        "mergehistory-into": "Pela hedefi:",
        "mergehistory-reason": "Sebeb:",
        "revertmerge": "Cia ke",
-       "history-title": "Rewizyonê $1:",
+       "history-title": "Tarixê çımraviyarnayişê \"$1\"",
        "lineno": "Rêza $1i:",
        "compareselectedversions": "Varyantunê weçinıtun têver sane",
        "editundo": "peyser bia",
index f962fa6..1bb1d5d 100644 (file)
        "talkpagelinktext": "Талқылауы",
        "specialpage": "Арнайы бет",
        "personaltools": "Жеке құралдар",
-       "postcomment": "Жаңа бөлім",
        "articlepage": "Мәлімет бетін қарау",
        "talk": "Талқылау",
        "views": "Көрініс",
        "externaldberror": "Осы арада не шеттік растау дерекқорында қате болды, немесе шеттік тіркелгіңізді жаңалау рұқсаты жоқ.",
        "login": "Кіру",
        "nav-login-createaccount": "Кіру / Тіркелу",
-       "loginprompt": "{{SITENAME}} торабына кіруіңіз үшін «cookies» қосылуы керек.",
        "userlogin": "Кіру / Тіркелу",
        "userloginnocreate": "Кіру",
        "logout": "Шығу",
index 159b32b..996ae86 100644 (file)
        "talkpagelinktext": "토론",
        "specialpage": "특수 문서",
        "personaltools": "개인 도구",
-       "postcomment": "새 문단",
        "articlepage": "문서 보기",
        "talk": "토론",
        "views": "보기",
        "externaldberror": "바깥 인증 데이터베이스에 오류가 있거나 바깥 계정을 새로 고칠 권한이 없습니다.",
        "login": "로그인",
        "nav-login-createaccount": "로그인 / 계정 만들기",
-       "loginprompt": "{{SITENAME}}에 로그인하려면 쿠키를 사용할 수 있어야 합니다.",
        "userlogin": "로그인 / 계정 만들기",
        "userloginnocreate": "로그인",
        "logout": "로그아웃",
        "nologin": "계정이 없나요? $1.",
        "nologinlink": "계정을 만드세요",
        "createaccount": "계정 만들기",
-       "gotaccount": "계정이 이미 있다면, $1.",
+       "gotaccount": "계정이 이미 있습니까? $1.",
        "gotaccountlink": "로그인하세요",
        "userlogin-resetlink": "로그인 정보를 잊으셨나요?",
        "userlogin-resetpassword-link": "비밀번호를 잊으셨나요?",
        "userlogin-helplink2": "로그인에 대한 도움말",
-       "userlogin-loggedin": "이미 $1로 로그인되어 있습니다. 아래의 양식을 사용하여 다른 계정으로 로그인하세요.",
+       "userlogin-loggedin": "이미 {{GENDER:$1|$1}} 사용자로 로그인되어 있습니다.\n다른 사용자로 로그인하려면 아래의 양식을 사용하세요.",
        "userlogin-createanother": "다른 계정 만들기",
        "createacct-emailrequired": "이메일 주소",
        "createacct-emailoptional": "이메일 주소 (선택 사항)",
        "prefs-dateformat": "날짜 형식",
        "prefs-timeoffset": "시차 설정",
        "prefs-advancedediting": "일반 설정",
-       "prefs-editor": "편집",
+       "prefs-editor": "편집",
        "prefs-preview": "미리 보기",
        "prefs-advancedrc": "고급 설정",
        "prefs-advancedrendering": "고급 설정",
        "rcshowhidemine-show": "보이기",
        "rcshowhidemine-hide": "숨기기",
        "rclinks": "최근 $2일간의 $1개 바뀐 문서 보기<br />$3",
-       "diff": "비교",
+       "diff": "차이",
        "hist": "역사",
        "hide": "숨기기",
        "show": "보이기",
        "license-nopreview": "(미리 보기 불가능)",
        "upload_source_url": "(올바르고, 공개적으로 접근할 수 있는 URL)",
        "upload_source_file": " (당신의 컴퓨터에 있는 파일)",
+       "listfiles-delete": "삭제",
        "listfiles-summary": "이 특수 문서는 모든 올려진 파일을 보여줍니다.",
        "listfiles_search_for": "다음 미디어 이름 검색:",
        "imgfile": "파일",
        "wantedpages-badtitle": "문서 제목이 잘못되었습니다: $1",
        "wantedfiles": "필요한 파일 목록",
        "wantedfiletext-cat": "다음 파일은 쓰이고는 있지만 없는 파일입니다. 바깥 저장소에 있는 파일은 실제로는 있지만 여기 올라 있을 수 있습니다. 그런 오류는 <del>삭제선</del>이 그어질 것입니다. 또한 없는 파일을 포함하고 있는 문서는 [[:$1]]에 올라 있습니다.",
-       "wantedfiletext-nocat": "다음 파일은 쓰이고는 있지만 없는 파일입니다. 바깥 저장소에 있는 파일은 실제로는 있지만 여기 올라 있을 수 있습니다. 그런 오류는 <del>삭제선</del>이 그어질 것입니다.",
+       "wantedfiletext-cat-noforeign": "다음 파일은 쓰이고 있지만 존재하지 않습니다. 또한, 존재하지 않는 파일이 포함된 문서가 [[:$1]]에 나열되어 있습니다.",
+       "wantedfiletext-nocat": "다음 파일은 쓰이고 있지만 존재하지 않습니다. 바깥 저장소에 있는 파일은 실제로는 있지만 여기 올라 있을 수 있습니다. 그런 오류는 <del>삭제선</del>이 그어질 것입니다.",
+       "wantedfiletext-nocat-noforeign": "다음 파일은 쓰이고 있지만 존재하지 않습니다.",
        "wantedtemplates": "필요한 틀 목록",
        "mostlinked": "가장 많이 연결된 문서 목록",
        "mostlinkedcategories": "가장 많이 연결된 분류 목록",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|토론]])",
        "unknown_extension_tag": "알 수 없는 확장 기능 태그 \"$1\"",
        "duplicate-defaultsort": "'''경고:''' 기본 정렬 키 \"$2\"가 이전의 기본 정렬 키 \"$1\"를 덮어쓰고 있습니다.",
+       "duplicate-displaytitle": "<strong>경고:</strong> \"$2\" 제목 표시는 기존의 표시되는 제목 \"$1\"을 덮어씁니다.",
        "version": "버전",
        "version-extensions": "설치된 확장 기능",
        "version-skins": "설치된 스킨",
        "expand_templates_remove_nowiki": "결과에서 <nowiki> 태그를 숨기기",
        "expand_templates_generate_xml": "XML 구문 트리 보기",
        "expand_templates_generate_rawhtml": "원본 HTML 보이기",
-       "expand_templates_preview": "미리 보기"
+       "expand_templates_preview": "미리 보기",
+       "pagelanguage": "문서 언어 선택기",
+       "pagelang-name": "문서",
+       "pagelang-language": "언어",
+       "pagelang-use-default": "기본 언어 사용",
+       "pagelang-select-lang": "언어 선택",
+       "right-pagelang": "문서 언어 바꾸기",
+       "action-pagelang": "문서 언어 바꾸기",
+       "log-name-pagelang": "언어 바꾸기 기록",
+       "log-description-pagelang": "문서 언어를 바꾼 기록입니다.",
+       "logentry-pagelang-pagelang": "$1 사용자가 $3의 문서 언어를 $4에서 $5로 {{GENDER:$2|바꾸었습니다}}."
 }
index f261368..c89dcd0 100644 (file)
        "talkpagelinktext": "Diskussioun",
        "specialpage": "Spezialsäit",
        "personaltools": "Perséinlech Tools",
-       "postcomment": "Neien Abschnitt",
        "articlepage": "Säit",
        "talk": "Diskussioun",
        "views": "Affichagen",
        "externaldberror": "Entweder ass e Feeler bei der externer Authentifizéierung geschitt, oder Dir däerft Ären externe Benotzerkont net aktualiséieren.",
        "login": "Aloggen",
        "nav-login-createaccount": "Aloggen / Benotzerkont uleeën",
-       "loginprompt": "Fir sech op {{SITENAME}} aloggen ze kënnen, mussen d'Cookien aktivéiert sinn.",
        "userlogin": "Aloggen / Benotzerkont uleeën",
        "userloginnocreate": "Umellen",
        "logout": "Ofmellen",
index 684b2a0..a198956 100644 (file)
        "talkpagelinktext": "Разговор",
        "specialpage": "Специјална страница",
        "personaltools": "Лични алатки",
-       "postcomment": "Ново заглавие",
        "articlepage": "Преглед на содржината",
        "talk": "Разговор",
        "views": "Посети",
        "externaldberror": "Настана грешка при надворешното најавување на базата или пак немате дозвола да ја подновите вашата надворешна сметка.",
        "login": "Најава",
        "nav-login-createaccount": "Најава / регистрација",
-       "loginprompt": "За да се најавите на {{SITENAME}} мора да користите колачиња.",
        "userlogin": "Најава / регистрација",
        "userloginnocreate": "Најава",
        "logout": "Одјава",
        "wantedpages-badtitle": "Невалиден наслов во резултатите: $1",
        "wantedfiles": "Потребни податотеки",
        "wantedfiletext-cat": "Следниве податотеки се користат, но не постојат. Податотеките од други складишта може да се наведени дури и ако постојат. Таквите ќе бидат <del>поништени</del> од списокот. Покрај ова, страниците што содржат податотеки кои не постојат се наведени на [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Следниве податотеки се користат, но не постојат. Покрај ова, страниците што ги содржат непостоечките податотеки се наведени во [[:$1]].",
        "wantedfiletext-nocat": "Следниве податотеки се користат, но не постојат. Податотеките од други складишта може да се наведени дури и ако постојат. Таквите ќе бидат <del>поништени</del> од списокот.",
+       "wantedfiletext-nocat-noforeign": "Следниве податотеки се користат, но не постојат.",
        "wantedtemplates": "Потребни шаблони",
        "mostlinked": "Најмногу врски до страници",
        "mostlinkedcategories": "Најмногу врски до категории",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|разговор]])",
        "unknown_extension_tag": "Непозната ознака на додатокот „$1“",
        "duplicate-defaultsort": "Предупредување: Основниот клуч за подредување „$2“ го поништува претходниот основен клуч за подредување „$1“.",
+       "duplicate-displaytitle": "<strong>Предупредување:</strong> Приказниот наслов „$2“ го заменува претходнито приказен наслов „$1“.",
        "version": "Верзија",
        "version-extensions": "Воспоставени додатоци",
        "version-skins": "Воспоставени рува",
index 56c62e6..6c146cf 100644 (file)
        "talkpagelinktext": "സംവാദം",
        "specialpage": "പ്രത്യേക താൾ",
        "personaltools": "സ്വകാര്യതാളുകൾ",
-       "postcomment": "അഭിപ്രായം ചേർക്കുക",
        "articlepage": "ലേഖനം കാണുക",
        "talk": "സംവാദം",
        "views": "ദർശനീയത",
        "externaldberror": "ഒന്നുകിൽ ഡേറ്റാബേസ് സാധൂകരണത്തിൽ പ്രശ്നം ഉണ്ടായിരുന്നു അല്ലെങ്കിൽ നവീകരിക്കുവാൻ താങ്കളുടെ ബാഹ്യ അംഗത്വം താങ്കളെ അനുവദിക്കുന്നില്ല.",
        "login": "പ്രവേശിക്കുക",
        "nav-login-createaccount": "പ്രവേശിക്കുക / അംഗത്വമെടുക്കുക",
-       "loginprompt": "{{SITENAME}} സംരംഭത്തിൽ ലോഗിൻ ചെയ്യാൻ താങ്കൾ കുക്കികൾ (Cookies) സജ്ജമാക്കിയിരിക്കണം.",
        "userlogin": "പ്രവേശിക്കുക / അംഗത്വമെടുക്കുക",
        "userloginnocreate": "പ്രവേശിക്കുക",
        "logout": "ലോഗൗട്ട്",
        "license-nopreview": "(പ്രിവ്യൂ ലഭ്യമല്ല)",
        "upload_source_url": "(സാധുവായ, ആർക്കും ഉപയോഗിക്കാവുന്ന യൂ.ആർ.എൽ.)",
        "upload_source_file": "(താങ്കളുടെ കമ്പ്യൂട്ടറിലുള്ള ഒരു പ്രമാണം)",
+       "listfiles-delete": "മായ്ക്കുക",
        "listfiles-summary": "അപ്‌ലോഡ് ചെയ്തിട്ടുള്ള എല്ലാ പ്രമാണങ്ങളും ഈ പ്രത്യേക താളിൽ കാണാവുന്നതാണ്.",
        "listfiles_search_for": "മീഡിയ പ്രമാണം തിരയുക:",
        "imgfile": "പ്രമാണം",
index d707821..48493dc 100644 (file)
        "talkpagelinktext": "Diskussjoni",
        "specialpage": "Paġna speċjali",
        "personaltools": "Għodda personali",
-       "postcomment": "Sezzjoni ġdida",
        "articlepage": "Ara l-artiklu",
        "talk": "Diskussjoni",
        "views": "Veduti",
        "namespaceprotected": "Inti m'għandhekx il-permess li timodifika paġni fin-''namespace'' '''$1''.",
        "customcssprotected": "M'għandekx il-permessi neċessarji sabiex timmodifika din il-paġna tas-CSS, minħabba li għandha tqegħid personali ta' utent ieħor.",
        "customjsprotected": "M'għandekx il-permessi neċessarji sabiex timmodifika din il-paġna tal-JavaScript, minħabba li għandha tqegħid personali ta' utent ieħor.",
+       "mycustomjsprotected": "Ma għandekx permess li teditja din il-paġna JavaScript.",
+       "myprivateinfoprotected": "Ma għandekx permess li teditja l-informazzjoni privata tiegħek.",
+       "mypreferencesprotected": "Ma għandekx permess li teditja l-preferenzi tiegħek.",
        "ns-specialprotected": "Il-paġni speċjali ma jistgħux jiġu mmodifikati.",
        "titleprotected": "Dan it-titlu ġie protett mill-ħolqien minn [[User:$1|$1]].\nIr-raġuni li ġiet mogħtija kienet ''$2''.",
+       "filereadonlyerror": "L-amministratur li sakkar offra din l-ispjegazzjoni: \"$3\".",
+       "invalidtitle-knownnamespace": "Titolu validu bin-namespace \"$2\" u t-test\"$3\"",
+       "invalidtitle-unknownnamespace": "Titolu validu b'numru tan-namespace mhux magħruf  $1 u t-test \"$2\"",
+       "exception-nologin": "Mhux qiegħed fil-kont",
+       "exception-nologin-text": "Jekk jogħġbok [[Special:Userlogin|idħol fil-kont tiegħek]] biex tkun tista' taċċessa din il-paġna jew din l-azzjoni.",
+       "exception-nologin-text-manual": "Jekk jogħġbok $1 sabiex tkuu tista' taċċessa din il-paġna jew din l-azzjoni.",
        "virus-badscanner": "Problema fil-konfigurazzjoni: antivirus mhux magħruf: ''$1''",
        "virus-scanfailed": "Tfittxija falliet (kodiċi $1)",
        "virus-unknownscanner": "antivirus mhux magħruf:",
-       "logouttext": "'''Bħalissa tinsab barra mill-kont tiegħek.'''\n\nTista' tkompli tuża' {{SITENAME}} bħala utent anonimu, jew tista' terġa <span class='plainlinks'>[$1 tidħol]</span> bħala l-istess utent jew wieħed differenti.\nKun af li ċerti paġni jistgħu jkomplu jidhru bħallikieku l-illogjar 'l barra mill-kont qatt ma seħħ, sakemm ma tħassarx il-cache tal-browser.",
+       "logouttext": "<strong>Bħalissa mhux qiegħed fil-kont tiegħek</strong>\n\nJista' jkun li xi paġni jibqgħu jidhru bħalli kieku qiegħed fil-kont, sakemm ma tneddafx il-cache tan-navigatur tiegħek.",
+       "welcomeuser": "Merħba, $1!",
+       "welcomecreation-msg": "Il-kont tiegħek inħoloq.\nJekk trid tista' tibdel il- [[Special:Preferences|preferenzi tas-]]{{SITENAME}}.",
        "yourname": "Isem l-utent:",
        "userlogin-yourname": "Isem l-utent",
        "userlogin-yourname-ph": "Daħħal isem l-utent tiegħek",
+       "createacct-another-username-ph": "Daħħal l-isem tiegħek ta' utent",
        "yourpassword": "Password:",
        "userlogin-yourpassword": "Password",
        "userlogin-yourpassword-ph": "Daħħal il-password tiegħek",
        "userlogin-remembermypassword": "Żommni fil-kont",
        "userlogin-signwithsecure": "Uża konnessjoni sigura",
        "yourdomainname": "Id-dominju tiegħek:",
+       "password-change-forbidden": "Fuq din il-wiki ma tistax tibdel il-kliem tad-dħul (passwords).",
        "externaldberror": "Kien hemm problema esterna ta' awtentiċitá jew m'għandhekx permess neċċessarju sabiex tagħmel aġġornamenti fuq l-aċċess estern.",
        "login": "Idħol",
        "nav-login-createaccount": "Idħol / Oħloq kont",
-       "loginprompt": "Irid ikollok il-cookies mixgħula biex tkun tista' tidħol fuq {{SITENAME}}.",
        "userlogin": "Idħol jew oħloq kont ġdid",
        "userloginnocreate": "Idħol",
        "logout": "Oħroġ",
        "gotaccount": "Diġa għandhek kont? '''$1'''.",
        "gotaccountlink": "Idħol",
        "userlogin-resetlink": "Insejt kif tidħol fil-kont tiegħek?",
-       "userlogin-resetpassword-link": "Irrisettja l-password",
+       "userlogin-resetpassword-link": "Insejt il-kelma tad-dħul (password)?",
+       "userlogin-helplink2": "Għajnuna biex tidħol fil-kont",
+       "userlogin-loggedin": "Diġà dħalt fil-kont bħala {{GENDER:$1|$1}}.\nUża l-formola t'hawn taħt biex tidħol bħala utent ieħor",
+       "userlogin-createanother": "Oħloq kont ieħor",
        "createacct-emailrequired": "Indirizz elettroniku",
        "createacct-emailoptional": "Indirizz elettroniku (mhux obbligatorju)",
        "createacct-email-ph": "Daħħal l-indirizz elettroniku tiegħek",
-       "createaccountmail": "Uża password każwali temporanja u ibgħatha fuq l-indirizz elettroniku mniżżel hawn taħt",
+       "createacct-another-email-ph": "Daħħal l-indirizz elettroniku",
+       "createaccountmail": "Uża kelma tad-dħul temporanja li tkun u ibgħatha lill-indirizz elettroniku speċifikat",
        "createacct-realname": "Isem proprju (fakultattiv)",
        "createaccountreason": "Raġuni:",
        "createacct-reason": "Raġuni",
        "createacct-captcha": "Kontroll tas-sigurtà",
        "createacct-imgcaptcha-ph": "Daħħal it-test li qed tara hawn fuq",
        "createacct-submit": "Oħloq il-kont",
+       "createacct-another-submit": "Oħloq kont ieħor",
        "createacct-benefit-heading": "{{SITENAME}} hi magħmula minn persuni bħalek.",
        "createacct-benefit-body1": "{{PLURAL:$1|modifika|modifiki}}",
-       "createacct-benefit-body2": "paġna",
+       "createacct-benefit-body2": "{{PLURAL:$1|paġna|paġni}}",
        "createacct-benefit-body3": "{{PLURAL:$1|kontributur|kontributuri}} riċenti",
        "badretype": "Il-passwords li daħħalt ma jaqblux.",
        "userexists": "L-isem l-utent li daħħalt diġà meħud. Jekk jogħġbok, agħżel isem differenti.",
        "passwordtooshort": "Il-password trid tkun mill-inqas {{PLURAL:$1|karattru|$1 karattri}} twila u differenti mill-isem tal-utent.",
        "password-name-match": "Il-password trid tkun differenti mill-isem tal-utent tiegħek.",
        "password-login-forbidden": "L-użu ta' dan l-isem tal-utent u l-password huwa projbit.",
-       "mailmypassword": "Ibgħatli password ġdida",
+       "mailmypassword": "Erġa' waqqaf kelma tad-dħul",
        "passwordremindertitle": "Password temporanju ġdid għal {{SITENAME}}",
        "passwordremindertext": "Xi ħadd (probabbilment int, mill-indirizz IP $1) għamel rikjesta għal password ġdida għal {{SITENAME}} ($4). Password temporanja għall-utent \"$2\" ġiet maħluqa u din hi \"$3\".\nHuwa opportun li inti tidħol issa u tbiddel immedjatament il-password tiegħek. Din il-password il-ġdida se tiskadi fi żmien {{PLURAL:$5|ġurnata|$5 ijiem}}.\n\nJekk xi ħadd ieħor għamel din ir-rikjesta jew jekk int ftakart il-password tiegħek u issa ma tridx tbiddilha, int tista' ma tagħtix każ dan il-messaġġ u tkompli tuża' l-password l-antika.",
        "noemail": "M'hemm l-ebda indirizz ta' posta elettronika għall-utent \"$1\".",
        "noemailcreate": "Huwa neċessarju li tipprovdi indirizz elettroniku validu",
        "passwordsent": "Il-password il-ġdida ntbagħtet fl-indirizz tal-posta elettronika ta' \"$1\".\nJekk jogħġbok, għamel aċċess wara li tasallek.",
        "blocked-mailpassword": "L-indirizz tal-IP tiegħek huwa bblokkjat u miżmum milli jwettaq modifiki. Għaldaqstant, mhuwiex possibli għalik li tuża l-funzjoni sabiex iġġib lura l-password, u dan sabiex ma jkunx hemm abbużi.",
-       "eauthentsent": "Intbagħat messaġġ ta' konferma b'permezz tal-posta elettronika lejn l-indirizz indikat.<br />\nQabel xi posta elettronika oħra tiġi mibgħuta fuq il-kont, trid qabel xejn tesegwixxi l-istruzzjonijiet kif inhuma indikati, sabiex tikkonferma li l-kont huwa tassew tiegħek.",
+       "eauthentsent": "Intbagħtetlek konferma b'permezz ta' messaġġ elettroniku fl-indirizz speċifikat.\nQabel ma tinbagħat xi posta elettronika oħra fuq il-kont, trid issegwi l-istruzzjonijiet indikati fil-messaġġ, sabiex tikkonferma li l-kont huwa tassew tiegħek.",
        "throttled-mailpassword": "Posta elettronika sabiex tfakrek il-password ġiet postjata, fl-aħħar {{PLURAL:$1|siegħa|$1 siegħat}}.\nSabiex jitnaqqas l-abbuż, waħda biss tista' tiġi postjata f'kull {{PLURAL:$1|siegħa|$1 siegħat}}.",
        "mailerror": "Problema bil-postar tal-messaġġ: $1",
        "acct_creation_throttle_hit": "L-utenti ta' din il-wiki li jużaw l-indirizz IP tiegħek ħolqu {{PLURAL:$1|kont|$1 kontijiet}} fl-aħħar ġurnata, li hu n-numru massimu permess f'dan il-perjodu ta' żmien.\nBħala riżultat, il-viżitaturi li jużaw dan l-IP ma jistgħux għall-mument, joħoloqu aktar kontijiet.",
index 51f192a..76f2580 100644 (file)
        "talkpagelinktext": "diskusjon",
        "specialpage": "Spesialside",
        "personaltools": "Personlige verktøy",
-       "postcomment": "Ny seksjon",
        "articlepage": "Vis innholdsside",
        "talk": "Diskusjon",
        "views": "Visninger",
        "externaldberror": "Det var en ekstern autentifiseringsfeil, eller du kan ikke oppdatere din eksterne konto.",
        "login": "Logg inn",
        "nav-login-createaccount": "Logg inn eller opprett en konto",
-       "loginprompt": "Du må ha slått på informasjonskapsler for å logge in på {{SITENAME}}.",
        "userlogin": "Logg inn eller opprett en konto",
        "userloginnocreate": "Logg inn",
        "logout": "Logg ut",
index d3c08bb..0ba4a58 100644 (file)
        "talkpagelinktext": "Overleg",
        "specialpage": "Spesiale zied",
        "personaltools": "Persoonlike instellingen",
-       "postcomment": "Niej onderwarp",
        "articlepage": "Artikel",
        "talk": "Overleg",
        "views": "Weergaven",
        "externaldberror": "Der gung iets fout bie de externe authentisering, of je maggen je gebrukersprofiel niet bewarken.",
        "login": "Anmelden",
        "nav-login-createaccount": "Anmelden",
-       "loginprompt": "Je mutten scheumbestaanden (cookies) an hebben staon um an te kunnen melden bie {{SITENAME}}.",
        "userlogin": "Anmelden / inschrieven",
        "userloginnocreate": "Anmelden",
        "logout": "Aofmelden",
index fc941fa..780e8dc 100644 (file)
        "talkpagelinktext": "Overleg",
        "specialpage": "Speciale pagina",
        "personaltools": "Persoonlijke instellingen",
-       "postcomment": "Nieuw kopje",
        "articlepage": "Pagina bekijken",
        "talk": "Overleg",
        "views": "Weergaven",
        "externaldberror": "Er is een fout opgetreden bij het aanmelden bij de database of u hebt geen toestemming uw externe account bij te werken.",
        "login": "Aanmelden",
        "nav-login-createaccount": "Aanmelden / registreren",
-       "loginprompt": "U moet cookies ingeschakeld hebben om u te kunnen aanmelden bij {{SITENAME}}.",
        "userlogin": "Aanmelden / registreren",
        "userloginnocreate": "Aanmelden",
        "logout": "Afmelden",
index 1d2cd01..800f321 100644 (file)
        "talkpagelinktext": "Discussion",
        "specialpage": "Pagina especiala",
        "personaltools": "Aisinas personalas",
-       "postcomment": "Seccion novèla",
        "articlepage": "Vejatz l'article",
        "talk": "Discussion",
        "views": "Afichatges",
        "externaldberror": "Siá una error s’es producha amb la banca de donadas d’autentificacion extèrna, siá sètz pas autorizat a metre a jorn vòstre compte extèrne.",
        "login": "Identificacion",
        "nav-login-createaccount": "Crear un compte o se connectar",
-       "loginprompt": "Vos cal activar los cookies per vos connectar a {{SITENAME}}.",
        "userlogin": "Crear un compte o se connectar",
        "userloginnocreate": "Connexion",
        "logout": "Se desconnectar",
        "currentrev": "Version actuala",
        "currentrev-asof": "Version actuala en data del $1",
        "revisionasof": "Version del $1",
-       "revision-info": "Version del $1 per $2",
+       "revision-info": "Version del $1 per {{GENDER:$6|$2}}$7",
        "previousrevision": "← Version precedenta",
        "nextrevision": "Version seguenta →",
        "currentrevisionlink": "vejatz la version correnta",
        "largefileserver": "La talha d'aqueste fichièr es superiora al maximum autorizat.",
        "emptyfile": "Lo fichièr que volètz importar sembla void. Aquò pòt èsser degut a una error dins lo nom del fichièr. Verificatz que desiratz vertadièrament copiar aqueste fichièr.",
        "windows-nonascii-filename": "Aqueste wiki supòrta pas los noms de fichièrs amb de caractèrs especials.",
-       "fileexists": "Un fichièr amb aqueste nom existís ja.\nMercé de verificar <strong>[[:$1]]</strong>.\nSètz segur de voler modificar aqueste fichièr ? [[$1|thumb]]",
+       "fileexists": "Un fichièr amb aqueste nom existís ja.\nMercé de verificar <strong>[[:$1]]</strong>\nse sètz pas segur{{GENDER:||a|}} que o volètz remplaçar. [[$1|thumb]]",
        "filepageexists": "La pagina de descripcion per aqueste fichièr ja es estada creada aicí <strong>[[:$1]]</strong>, mas cap de fichièr existís pas actualament jos aqueste nom.\nLo resumit qu'anatz especificar apareisserà pas sus la pagina de descripcion.\nPer o far, vos caldrà modificar la pagina manualament. [[$1|vinheta]]",
-       "fileexists-extension": "Un fichièr amb un nom pròchi existís ja : [[$2|thumb]]\n* Nom del fichièr d'importar : <strong>[[:$1]]</strong>\n* Nom del fichièr existent : <strong>[[:$2]]</strong>\nCausissètz-ne un autre.",
+       "fileexists-extension": "Un fichièr amb un nom pròchi existís ja : [[$2|thumb]]\n* Nom del fichièr d'importar : <strong>[[:$1]]</strong>\n* Nom del fichièr existent : <strong>[[:$2]]</strong>\nBenlèu que podètz utilizar un nom mai explicit ?",
        "fileexists-thumbnail-yes": "Lo fichièr sembla èsser un imatge en talha reducha ''(thumbnail)''. [[$1|thumb]]\nVerificatz lo fichièr <strong>[[:$1]]</strong>.\nSe lo fichièr verificat es lo meteis imatge (dins una resolucion melhora), es pas de besonh d’importar una version reducha.",
        "file-thumbnail-no": "Lo nom del fichièr comença per <strong>$1</strong>.\nEs possible que s’agisca d’una version reducha ''(miniatura)''.\nSe dispausatz del fichièr en resolucion nauta, importatz-lo, si que non cambiatz lo nom del fichièr.",
        "fileexists-forbidden": "Un fichièr amb aqueste nom existís ja e pòt pas èsser espotit.\nSe volètz totjorn importar aquel fichièr, mercé de tornar en arrièr e d'utilizar un nom novèl. [[File:$1|thumb|center|$1]]",
        "filedelete-maintenance": "La supression e lo restabliment de fichièrs es temporàriament desactivada pendent la mantenença.",
        "filedelete-maintenance-title": "Impossible de suprimir lo fichièr",
        "mimesearch": "Recèrca per tipe MIME",
-       "mimesearch-summary": "Aquesta pagina especiala permet de cercar de fichièrs en foncion de lor tipe MIME. Entrada : tipe/sostipe, per exemple <code>image/jpeg</code>.",
+       "mimesearch-summary": "Aquesta pagina vos permet de filtrar los fichièrs en foncion de lor tipe MIME. Entrada : tipe_de_contengut/sostipe o tipe_de_contengut/*, per exemple <code>image/jpeg</code>.",
        "mimetype": "Tipe MIME :",
        "download": "telecargament",
        "unwatchedpages": "Paginas pas seguidas",
        "nmemberschanged": "$1 → $2 {{PLURAL:$2|membre|membres}}",
        "nrevisions": "$1 {{PLURAL:$1|revision|revisions}}",
        "nviews": "$1 {{PLURAL:$1|consultacion|consultacions}}",
-       "nimagelinks": "Utilisat sus $1 {{PLURAL:$1|pagina|paginas}}",
-       "ntransclusions": "Utilisat sus $1 {{PLURAL:$1|pagina|paginas}}",
+       "nimagelinks": "Utilizat sus $1 {{PLURAL:$1|pagina|paginas}}",
+       "ntransclusions": "Utilizat sus $1 {{PLURAL:$1|pagina|paginas}}",
        "specialpage-empty": "Aquesta pagina es voida.",
        "lonelypages": "Paginas orfanèlas",
        "lonelypagestext": "Las paginas seguentas son pas ligadas o enclusas a partir d’autras paginas de {{SITENAME}}.",
        "wantedtemplates": "Modèls demandats",
        "mostlinked": "Paginas mai ligadas",
        "mostlinkedcategories": "Categorias mai utilizadas",
-       "mostlinkedtemplates": "Modèls mai utilizats",
+       "mostlinkedtemplates": "Paginas las mai inclusas",
        "mostcategories": "Articles utilizant mai de categorias",
        "mostimages": "Fichièrs mai utilizats",
        "mostinterwikis": "Paginas amb lo mai d'interwikis",
        "nowatchlist": "Vòstra lista de seguiment conten pas cap d'article.",
        "watchlistanontext": "Per poder afichar o editar los elements de vòstra lista de seguiment, vos cal vos $1.",
        "watchnologin": "Vos sètz pas identificat(ada)",
-       "addwatch": "Ajustar a la lista de seguiment",
+       "addwatch": "Apondre a la lista de seguiment",
        "addedwatchtext": "La pagina « [[:$1]] » es estada aponduda a vòstra [[Special:Watchlist|lista de seguiment]]. Las modificacions venentas d'aquesta pagina e de la pagina de discussion associada i seràn repertoriadas.",
        "removewatch": "Suprimir de la lista de seguiment",
        "removedwatchtext": "La pagina « [[:$1]] » es estada levada de vòstra [[Special:Watchlist|lista de seguiment]].",
        "revertpage": "Anullacion de las modificacions de [[Special:Contributions/$2|$2]] ([[User talk:$2|Discussion]]) cap a la darrièra version de [[User:$1|$1]]",
        "revertpage-nouser": "Revocacion de las modificacions per un utilizaire amagat a la darrièra version per {{GENDER:$1|[[User:$1|$1]]}}",
        "rollback-success": "Anullacion de las modificacions de $1 ; retorn a la version de $2.",
-       "sessionfailure-title": "La session capitèt mal",
+       "sessionfailure-title": "La sesilha a fracassat",
        "sessionfailure": "Vòstra sesilha de connexion sembla aver de problèmas ;\naquesta accion es estada anullada en prevencion d’un piratatge de sesilha.\nClicatz sus « Precedent » e tornatz cargar la pagina d’ont venètz, puèi ensajatz tornarmai.",
        "protectlogpage": "Istoric de las proteccions",
        "protectlogtext": "Aquí una lista de las modificacions de las proteccions de paginas.\nConsultatz la [[Special:ProtectedPages|lista de las paginas protegidas]] per la lista de las proteccions actualament operacionalas.",
        "duplicate-defaultsort": "Atencion : La clau de triada per defaut « $2 » espotís la mai recenta « $1 ».",
        "version": "Version",
        "version-extensions": "Extensions installadas",
+       "version-skins": "Abilhatges installats",
        "version-specialpages": "Paginas especialas",
        "version-parserhooks": "Extensions del parser",
        "version-variables": "Variablas",
        "version-antispam": "Prevencion del spam",
-       "version-skins": "Abilhatges",
        "version-other": "Divèrs",
        "version-mediahandlers": "Supòrts mèdia",
        "version-hooks": "Croquets",
index d66eb12..ec41ac6 100644 (file)
        "talkpagelinktext": "ਗੱਲ-ਬਾਤ",
        "specialpage": "ਖ਼ਾਸ ਸਫ਼ਾ",
        "personaltools": "ਨਿੱਜੀ ਸੰਦ",
-       "postcomment": "ਨਵਾਂ ਭਾਗ",
        "articlepage": "ਸਮੱਗਰੀ ਸਫ਼ਾ ਵੇਖੋ",
        "talk": "ਚਰਚਾ",
        "views": "ਵਿਊ",
        "externaldberror": "ਜਾਂ ਤਾਂ ਪ੍ਰਮਾਣਕੀ ਡਾਟਾਬੇਸ ਦੋਸ਼ ਆਇਆ ਹੈ ਜਾਂ ਤੁਹਾਨੂੰ ਆਪਣੇ ਬਾਹਰੀ ਖਾਤੇ ਨੂੰ ਅੱਪਡੇਟ ਕਰਨ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ।",
        "login": "ਲਾਗਇਨ",
        "nav-login-createaccount": "ਲਾਗਇਨ/ਖਾਤਾ ਬਣਾਓ",
-       "loginprompt": "ਤੁਹਾਨੂੰ {{SITENAME}} ’ਤੇ ਲਾਗਇਨ ਕਰਨ ਲਈ ਕੂਕੀਸ ਯੋਗ ਕਰਨੇ ਜ਼ਰੂਰੀ ਹਨ।",
        "userlogin": "ਲਾਗਇਨ/ਖਾਤਾ ਬਣਾਓ",
        "userloginnocreate": "ਲਾਗਇਨ",
        "logout": "ਲਾਗ ਆਉਟ",
index 776d7bc..a75d85e 100644 (file)
        "talkpagelinktext": "dyskusja",
        "specialpage": "Strona specjalna",
        "personaltools": "Osobiste",
-       "postcomment": "Nowa sekcja",
        "articlepage": "Artykuł",
        "talk": "Dyskusja",
        "views": "Widok",
        "externaldberror": "Wystąpił błąd zewnętrznej bazy autentyfikacyjnej lub nie posiadasz uprawnień koniecznych do aktualizacji zewnętrznego konta.",
        "login": "Zaloguj się",
        "nav-login-createaccount": "Logowanie i rejestracja",
-       "loginprompt": "Musisz mieć włączoną w przeglądarce obsługę ciasteczek, by móc się zalogować do {{GRAMMAR:D.lp|{{SITENAME}}}}.",
        "userlogin": "Logowanie i rejestracja",
        "userloginnocreate": "Zaloguj się",
        "logout": "Wyloguj",
        "license-nopreview": "(Podgląd niedostępny)",
        "upload_source_url": " (poprawny, publicznie dostępny adres URL)",
        "upload_source_file": " (plik na twoim komputerze)",
+       "listfiles-delete": "usuń",
        "listfiles-summary": "Na tej stronie specjalnej prezentowane są wszystkie przesłane pliki.",
        "listfiles_search_for": "Szukaj pliku o nazwie",
        "imgfile": "plik",
        "wantedpages-badtitle": "Nieprawidłowy tytuł wśród wyników – $1",
        "wantedfiles": "Potrzebne pliki",
        "wantedfiletext-cat": "Następujące pliki są używane, ale nie istnieją. Pliki z obcych repozytoriów mogą być wymienione pomimo istnienia. Takie fałszywe wyniki zostaną <del>przekreślone</del>. Ponadto strony, które osadzają pliki, które nie istnieją, są wymienione w [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Następujące pliki są używane, ale nie istnieją. Dodatkowo strony, które zawierają nieistniejące pliki, są wymienione w [[:$1]].",
        "wantedfiletext-nocat": "Następujące pliki są używane, ale nie istnieją. Pliki z obcych repozytoriów mogą być wymienione pomimo istnienia. Takie fałszywe wyniki zostaną <del>przekreślone</del>.",
+       "wantedfiletext-nocat-noforeign": "Następujące pliki są używane, ale nie istnieją.",
        "wantedtemplates": "Potrzebne szablony",
        "mostlinked": "Najczęściej linkowane strony",
        "mostlinkedcategories": "Kategorie o największej liczbie stron",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|dyskusja]])",
        "unknown_extension_tag": "Nieznany znacznik rozszerzenia „$1”",
        "duplicate-defaultsort": "Uwaga: Domyślnym kluczem sortowania będzie „$2” i zastąpi on wcześniej wykorzystywany klucz „$1”.",
+       "duplicate-displaytitle": "<strong>Uwaga:</strong> Wyświetlenie tytułu „$2” powoduje nadpisanie wcześniej wyświetlanego tytułu „$1”.",
        "version": "Wersja oprogramowania",
        "version-extensions": "Zainstalowane rozszerzenia",
        "version-skins": "Zainstalowane skórki",
        "specialpages-note": "* Normalne strony specjalne.\n* <span class=\"mw-specialpagerestricted\">Zastrzeżone strony specjalne.</span>",
        "specialpages-group-maintenance": "Raporty konserwacyjne",
        "specialpages-group-other": "Inne strony specjalne",
-       "specialpages-group-login": "Zaloguj się / utwórz konto",
+       "specialpages-group-login": "Logowanie / rejestracja",
        "specialpages-group-changes": "Ostatnie zmiany i rejestry",
        "specialpages-group-media": "Pliki",
        "specialpages-group-users": "Użytkownicy i uprawnienia",
index ff4c931..1b41713 100644 (file)
        "talkpagelinktext": "discussão",
        "specialpage": "Página especial",
        "personaltools": "Ferramentas pessoais",
-       "postcomment": "Nova seção",
        "articlepage": "Ver página de conteúdo",
        "talk": "Discussão",
        "views": "Visualizações",
        "externaldberror": "Ocorreu ou um erro no banco de dados durante a autenticação ou não lhe é permitido atualizar a sua conta externa.",
        "login": "Autenticar-se",
        "nav-login-createaccount": "Entrar / criar conta",
-       "loginprompt": "É necessário estar com cookies ativados para poder autenticar-se no wiki {{SITENAME}}.",
        "userlogin": "Entrar / criar conta",
        "userloginnocreate": "Entrar",
        "logout": "Sair",
        "revdelete-offender": "Autor da revisão:",
        "suppressionlog": "Registro de supressões",
        "suppressionlogtext": "Abaixo está uma lista das eliminações e bloqueios envolvendo conteúdo ocultado por administradores.\nVeja a [[Special:BlockList|lista de bloqueios]] para uma lista de banimentos e bloqueios em efeito neste momento.",
-       "mergehistory": "Fundir histórico de páginas",
+       "mergehistory": "Fundir históricos das páginas",
        "mergehistory-header": "A partir desta página é possível fundir históricos de edições de uma página em outra.\nCertifique-se de que tal alteração manterá a continuidade das ações.",
        "mergehistory-box": "Fundir revisões de duas páginas:",
        "mergehistory-from": "Página de origem:",
        "mergehistory-list": "Histórico de edições habilitadas para fusão",
        "mergehistory-merge": "As edições de [[:$1]] a seguir poderão ser fundidas em [[:$2]]. Utilize a coluna de botões de opção para fundir apenas as edições feitas entre o intervalo de tempo especificado. Note que ao utilizar os links de navegação esta coluna será retornada a seus valores padrão.",
        "mergehistory-go": "Exibir edições habilitadas a serem fundidas",
-       "mergehistory-submit": "Fundir edições",
+       "mergehistory-submit": "Fundir revisões",
        "mergehistory-empty": "Não existem edições habilitadas a serem fundidas.",
        "mergehistory-success": "$3 {{PLURAL:$3|revisão|revisões}} de [[:$1]] fundidas em [[:$2]] com sucesso.",
        "mergehistory-fail": "Não foi possível fundir os históricos; por gentileza, verifique a página e os parâmetros de tempo.",
        "action-patrol": "marcar as edições de outros usuários como patrulhadas",
        "action-autopatrol": "ter suas edições marcadas como patrulhadas",
        "action-unwatchedpages": "ver a lista de páginas não-vigiadas",
-       "action-mergehistory": "fundir o histórico de edições desta página",
+       "action-mergehistory": "fundir o histórico desta página",
        "action-userrights": "editar todos os privilégios de usuário",
        "action-userrights-interwiki": "editar privilégios de usuários de outros wikis",
        "action-siteadmin": "bloquear ou desbloquear o banco de dados",
index 8d8abfa..ff1e8e3 100644 (file)
        "unprotectthispage": "Alterar a proteção desta página",
        "newpage": "Página nova",
        "talkpage": "Discutir esta página",
-       "talkpagelinktext": "discussão",
+       "talkpagelinktext": "Discussão",
        "specialpage": "Página especial",
        "personaltools": "Ferramentas pessoais",
-       "postcomment": "Seção nova",
        "articlepage": "Ver página de conteúdo",
        "talk": "Discussão",
        "views": "Vistas",
        "externaldberror": "Ocorreu um erro externo à base de dados durante a autenticação ou não lhe é permitido atualizar a sua conta externa.",
        "login": "Entrar",
        "nav-login-createaccount": "Entrar / criar conta",
-       "loginprompt": "É necessário ter os ''cookies'' ativados no seu navegador para poder autenticar-se em {{SITENAME}}.",
        "userlogin": "Criar uma conta ou entrar",
        "userloginnocreate": "Entrar",
        "logout": "Sair",
        "delete-warning-toobig": "Esta página tem um histórico de edições longo, com mais de $1 {{PLURAL:$1|edição|edições}}.\nEliminá-la poderá causar problemas na base de dados da {{SITENAME}};\nprossiga com precaução.",
        "deleting-backlinks-warning": "'''Aviso:''' Há [[Special:WhatLinksHere/{{FULLPAGENAME}}|páginas]] que contêm ligações para a página que está prestes a eliminar ou que a transcluem.",
        "rollback": "Reverter edições",
-       "rollback_short": "Desfazer",
-       "rollbacklink": "desfazer",
-       "rollbacklinkcount": "desfazer $1 {{PLURAL:$1|edição|edições}}",
-       "rollbacklinkcount-morethan": "desfazer mais do que $1 {{PLURAL:$1|edição|edições}}",
+       "rollback_short": "Reverter",
+       "rollbacklink": "reverter",
+       "rollbacklinkcount": "reverter $1 {{PLURAL:$1|edição|edições}}",
+       "rollbacklinkcount-morethan": "reverter mais do que $1 {{PLURAL:$1|edição|edições}}",
        "rollbackfailed": "A reversão falhou",
        "cantrollback": "Não foi possível reverter a edição; o último contribuidor é o único autor desta página",
        "alreadyrolled": "Não foi possível reverter as edições de [[:$1]] por [[User:$2|$2]] ([[User talk:$2|discussão]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]);\nalguém editou ou já reverteu a página.\n\nA última edição foi de [[User:$3|$3]] ([[User talk:$3|discussão]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
index fc600ab..d7dfd87 100644 (file)
        "talkpagelinktext": "Used as name of links going to talk page in some places, like in [[Special:RecentChanges]], [[Special:Allmessages]], [[Special:Logs]], and [[Special:Watchlist/edit]].\n\n{{Identical|Talk}}",
        "specialpage": "{{Identical|Special page}}",
        "personaltools": "Heading for a group of links to your user page, talk page, preferences, watchlist, and contributions. This heading is visible in the sidebar in some skins. For an example, see [{{canonicalurl:Main_Page|useskin=simple}} Main Page using simple skin].",
-       "postcomment": "Used as link text.\n\nThe link points to the talk page and has the parameters \"action=edit&section=new\".",
        "addsection": "{{notranslate}}\nText of the new section tab (the one next to the \"edit\" tab on non-Vector skins).\n\nSee also:\n* {{msg-mw|Addsection}}\n* {{msg-mw|Accesskey-ca-addsection}}\n* {{msg-mw|Tooltip-ca-addsection}}",
        "articlepage": "'Content page' is used for NS_MAIN and any other non-standard namespaces. Only used in the Cologne Blue skin in the bottomLinks part.\n\n{{Identical|Content page}}",
        "talk": "Used as display name for the tab to all {{msg-mw|Talk}} pages. These pages accompany all content pages and can be used for discussing the content page. Example: [[Talk:Example]].\n\nSee also:\n* {{msg-mw|Talk}}\n* {{msg-mw|Accesskey-ca-talk}}\n* {{msg-mw|Tooltip-ca-talk}}\n{{Identical|Discussion}}",
        "externaldberror": "This message is thrown when a valid attempt to change the wiki password for a user fails because of a database error or an error from an external system.",
        "login": "{{Doc-special|UserLogin|unlisted=1}}\n{{Identical|Log in}}",
        "nav-login-createaccount": "Shown to anonymous users in the upper right corner of the page. When you can't create an account, the message {{msg-mw|login}} is shown.\n{{Identical|Log in / create account}}",
-       "loginprompt": "A small notice in the log in form.",
+       "loginprompt": "{{ignored}}",
        "userlogin": "Since 1.22 no longer used in core, but may still be used by extensions. DEPRECATED\n\n{{Identical|Log in / create account}}",
        "userloginnocreate": "Since 1.22 no longer used in core, but may still be used by some extensions. A variant of {{msg-mw|Userlogin}} when the user is not allowed to create a new account. DEPRECATED\n\n{{Identical|Log in}}",
        "logout": "Used as link text in your personal toolbox (upper right side).\n\nSee also:\n* {{msg-mw|Logout}}\n* {{msg-mw|Accesskey-pt-logout}}\n* {{msg-mw|Tooltip-pt-logout}}\n{{Identical|Log out}}",
        "wantedpages-badtitle": "Error message shown when [[Special:WantedPages]] is listing a page with a title that shouldn't exist.\n\nParameters:\n* $1 - a page title",
        "wantedfiles": "{{doc-special|WantedFiles}}",
        "wantedfiles-summary": "{{doc-specialpagesummary|wantedfiles}}",
-       "wantedfiletext-cat": "Message displayed at top of [[special:WantedFiles]]. $1 contains the name of the tracking category for broken files (Including Category prefix). {{msg-mw|wantedfiletext-nocat}} is used if the tracking category is disabled.",
-       "wantedfiletext-nocat": "Message displayed at top of [[special:WantedFiles]] when broken file tracking category is disabled. See {{msg-mw|wantedfiletext-cat}}.",
+       "wantedfiletext-cat": "Message displayed at top of [[special:WantedFiles]] when false positives from foreign file repositories (like commons) are likely. $1 contains the name of the tracking category for broken files (Including Category prefix). {{msg-mw|wantedfiletext-nocat}} is used if the tracking category is disabled.\n\nSee also: {{msg-mw|wantedfiletext-cat-noforeign}}, {{msg-mw|wantedfiletext-nocat}}",
+       "wantedfiletext-cat-noforeign": "Message displayed at top of [[Special:WantedFiles]] when the wiki has no foreign repositories. See also {{msg-mw|wantedfilestext-cat}}. $1 contains the name of the tracking category for broken files (Including Category prefix). {{msg-mw|wantedfiletext-nocat}} is used if the tracking category is disabled.",
+       "wantedfiletext-nocat": "Message displayed at top of [[special:WantedFiles]] when broken file tracking category is disabled and false positives from foreign file repositories (like commons) are likely. See {{msg-mw|wantedfiletext-cat}}.",
+       "wantedfiletext-nocat-noforeign": "Message displayed at top of [[special:WantedFiles]] when broken file tracking category is disabled and their are no foreign file repositories enabled on the wiki. See {{msg-mw|wantedfiletext-cat}}, {{msg-mw|wantedfiletext-nocat}}.",
        "wantedtemplates": "{{doc-special|WantedTemplates}}",
        "wantedtemplates-summary": "{{doc-specialpagesummary|wantedtemplates}}",
        "mostlinked": "{{doc-special|MostLinked}}",
        "timezone-utc": "{{optional}}",
        "unknown_extension_tag": "This is an error shown when you use an unknown extension tag name.\n\nThis feature allows tags like <code><nowiki><pre></nowiki></code> to be called with a parser like <code><nowiki>{{#tag:pre}}</nowiki></code>.\n\nParameters:\n* $1 - the unknown extension tag name",
        "duplicate-defaultsort": "See definition of [[w:Sorting|sort key]] on Wikipedia. Parameters:\n* $1 - old default sort key\n* $2 - new default sort key",
+       "duplicate-displaytitle": "Warning shown when a page has its display title set multiple times. Parameters:\n* $1 - old display title\n* $2 - new display title",
        "version": "{{doc-special|Version}}\n{{Identical|Version}}",
        "version-summary": "{{doc-specialpagesummary|version}}",
        "version-extensions": "Header on [[Special:Version]].",
index 1b49b95..8dbbff0 100644 (file)
        "talkpagelinktext": "Discuție",
        "specialpage": "Pagină specială",
        "personaltools": "Unelte personale",
-       "postcomment": "Secțiune nouă",
        "articlepage": "Vedeți articolul",
        "talk": "Discuție",
        "views": "Vizualizări",
        "externaldberror": "A fost fie o eroare de bază de date pentru o autentificare extenă sau nu aveți permisiunea să actualizați contul extern.",
        "login": "Autentificare",
        "nav-login-createaccount": "Creare cont / Autentificare",
-       "loginprompt": "Trebuie să ai modulele cookie activate pentru a te autentifica la {{SITENAME}}.",
        "userlogin": "Creare cont / Autentificare",
        "userloginnocreate": "Autentificare",
        "logout": "Închidere sesiune",
        "wantedpages-badtitle": "Titlu invalid în rezultatele : $1",
        "wantedfiles": "Fișiere dorite",
        "wantedfiletext-cat": "Următoarele fișiere sunt utilizate, dar nu există. Fișierele provenind din depozite externe pot apărea listate, în ciuda faptului că ele nu există. Orice astfel de pozitive false vor fi <del>tăiate</del>. În plus, paginile care încorporează astfel de fișiere inexistente sunt listate la [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Următoarele fișiere sunt utilizate, dar nu există. În plus, paginile care încorporează astfel de fișiere inexistente sunt listate la [[:$1]].",
        "wantedfiletext-nocat": "Următoarele fișiere sunt utilizate, dar nu există. Fișierele provenind din depozite externe pot apărea listate, în ciuda faptului că ele nu există. Orice astfel de pozitive false vor fi <del>tăiate</del>.",
+       "wantedfiletext-nocat-noforeign": "Următoarele fișiere sunt utilizate, dar nu există.",
        "wantedtemplates": "Formate dorite",
        "mostlinked": "Cele mai căutate articole",
        "mostlinkedcategories": "Cele mai căutate categorii",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|discuție]])",
        "unknown_extension_tag": "Extensie etichetă necunoscută „$1”",
        "duplicate-defaultsort": "'''Atenție:''' Cheia de sortare implicită („$2”) o înlocuiește pe precedenta („$1”).",
+       "duplicate-displaytitle": "<strong>Atenție:</strong> Titlul afișat „$2” înlocuieşte titlul afișat anterior, „$1”.",
        "version": "Versiune",
        "version-extensions": "Extensii instalate",
        "version-skins": "Aspecte instalate",
index 45846c3..f236963 100644 (file)
        "talkpagelinktext": "обсуждение",
        "specialpage": "Служебная страница",
        "personaltools": "Персональные инструменты",
-       "postcomment": "Новый раздел",
        "articlepage": "Просмотреть статью",
        "talk": "Обсуждение",
        "views": "Просмотры",
        "externaldberror": "Произошла ошибка при аутентификации с помощью внешней базы данных или у вас недостаточно прав для внесения изменений в свою внешнюю учётную запись.",
        "login": "Представиться системе",
        "nav-login-createaccount": "Представиться / зарегистрироваться",
-       "loginprompt": "Вы должны разрешить «cookies», чтобы представиться системе.",
        "userlogin": "Представиться или зарегистрироваться",
        "userloginnocreate": "Представиться",
        "logout": "Завершение сеанса",
        "wantedpages-badtitle": "Ошибочный заголовок в результатах запроса: $1",
        "wantedfiles": "Требуемые файлы",
        "wantedfiletext-cat": "Следующие файлы пытаются использовать, хотя их не существует. В этот список могут ошибочно попасть файлы, находящиеся во внешних хранилищах. Подобные ложные срабатывания будут отмечены <del>зачёркиванием</del>. Кроме того, страницы, содержащие несуществующие файлы, перечислены в [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Следующие файлы используются, но не существуют. Кроме того, страницы, которые ссылаются на эти файлы, не существуют и перечислены на странице [[:$1]].",
        "wantedfiletext-nocat": "Следующие файлы пытаются использовать, хотя их не существует. В этот список могут ошибочно попасть файлы, находящиеся во внешних хранилищах. Подобные ложные срабатывания будут отмечены <del>зачёркиванием</del>.",
+       "wantedfiletext-nocat-noforeign": "Следующие файлы используются, но не существуют.",
        "wantedtemplates": "Требуемые шаблоны",
        "mostlinked": "Страницы, на которые больше всего ссылок",
        "mostlinkedcategories": "Категории, на которые больше всего ссылок",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|обсуждение]])",
        "unknown_extension_tag": "Неизвестный тег расширения «$1»",
        "duplicate-defaultsort": "Внимание. Ключ сортировки по умолчанию «$2» переопределяет прежний ключ сортировки по умолчанию «$1».",
+       "duplicate-displaytitle": "<strong>Внимание:</strong> Отображаемое название «$2» переопределяет ранее заданное отображаемое название «$1».",
        "version": "Версия",
        "version-extensions": "Установленные расширения",
        "version-skins": "Установленные темы оформления",
index 719ce57..2d6a3a4 100644 (file)
        "talkpagelinktext": "Pogovor",
        "specialpage": "Posebna stran",
        "personaltools": "Osebna orodja",
-       "postcomment": "Nov razdelek",
        "articlepage": "Prikaže članek",
        "talk": "Pogovor",
        "views": "Pogled",
        "externaldberror": "Pri potrjevanju istovetnosti je prišlo do notranje napake ali pa za osveževanje zunanjega računa nimate dovoljenja.",
        "login": "Prijava",
        "nav-login-createaccount": "Prijavite se / registrirajte se",
-       "loginprompt": "Za prijavo v {{GRAMMAR:tožilnik|{{SITENAME}}}} morate imeti omogočene piškotke.",
        "userlogin": "Prijavite se / registrirajte se",
        "userloginnocreate": "Prijava",
        "logout": "Odjava",
        "wantedpages-badtitle": "Neveljaven naslov v končnem nizu: $1",
        "wantedfiles": "Želene datoteke",
        "wantedfiletext-cat": "Naslednje datoteke so uporabljene, vendar ne obstajajo. Navedene so morda tudi datoteke iz zunanjih hramb, čeprav obstajajo. Vsi takšni lažni pozitivi bodo <del>prečrtani</del>. Poleg tega so strani, ki vključujejo neobstoječe datoteke, navedene na [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Spodnje datoteke so nekje uporabljene, vendar ne obstajajo. Dodatno lahko strani, ki vključujejo neobstoječe datoteke, najdete na [[:$1]].",
        "wantedfiletext-nocat": "Naslednje datoteke so uporabljene, vendar ne obstajajo. Navedene so morda tudi datoteke iz zunanjih hramb, čeprav obstajajo. Vsi takšni lažni pozitivi bodo <del>prečrtani</del>.",
+       "wantedfiletext-nocat-noforeign": "Spodnje datoteke so nekje uporabljene, vendar ne obstajajo.",
        "wantedtemplates": "Želene predloge",
        "mostlinked": "Strani, na katere se največ povezuje",
        "mostlinkedcategories": "Kategorije z največ elementi",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|pogovor]])",
        "unknown_extension_tag": "Neznana razširitvena etiketa »$1«",
        "duplicate-defaultsort": "'''Opozorilo:''' Privzeti ključ razvrščanja »$2« prepiše prejšnji privzeti ključ razvrščanja »$1«.",
+       "duplicate-displaytitle": "<strong>Opozorilo:</strong> Prikazni naslov »$2« prepiše prejšnji prikazni naslov »$1«.",
        "version": "Različica",
        "version-extensions": "Nameščene razširitve",
        "version-skins": "Nameščene kože",
index 384a630..31a2dba 100644 (file)
        "talkpagelinktext": "Diskuto",
        "specialpage": "Faqe speciale",
        "personaltools": "Mjetet e mia",
-       "postcomment": "Seksion i ri",
        "articlepage": "Shiko faqen me përmbajtje",
        "talk": "Diskutimet",
        "views": "Shikime",
        "externaldberror": "Ose kishte një gabim tek regjistri i identifikimit të jashtëm, ose nuk ju lejohet të përtërini llogarinë tuaje të jashtme.",
        "login": "Hyni",
        "nav-login-createaccount": "Hyni ose hapni një llogari",
-       "loginprompt": "Ju duhet të mundësoni lejimin e \"cookies\" për të hyrë brënda në {{SITENAME}}.",
        "userlogin": "Hyni / hapni llogari",
        "userloginnocreate": "Hyni",
        "logout": "Dalje",
index 83ee30b..e2f8066 100644 (file)
@@ -24,7 +24,8 @@
                        "Милан Јелисавчић",
                        "Михајло Анђелковић",
                        "לערי ריינהארט",
-                       "아라"
+                       "아라",
+                       "Nemo bis"
                ]
        },
        "tog-underline": "Подвлачење веза:",
        "talkpagelinktext": "разговор",
        "specialpage": "Посебна страница",
        "personaltools": "Личне алатке",
-       "postcomment": "Нови одељак",
        "articlepage": "Погледај страницу са садржајем",
        "talk": "Разговор",
        "views": "Прегледи",
        "externaldberror": "Дошло је до грешке при препознавању базе података или немате овлашћења да ажурирате свој спољни налог.",
        "login": "Пријави ме",
        "nav-login-createaccount": "Пријава/регистрација",
-       "loginprompt": "Омогућите колачиће да бисте се пријавили на овај вики.",
        "userlogin": "Пријава/регистрација",
        "userloginnocreate": "Пријава",
        "logout": "Одјава",
        "uploadnewversion-linktext": "Пошаљи нову верзију ове датотеке",
        "shared-repo-from": "из $1",
        "shared-repo": "заједничко складиште",
-       "shared-repo-name-wikimediacommons": "{{#SWITCH:{{{1|}}}\n|#default=Викимедијина остава\n|dat=Викимедијиној остави\n}}",
+       "shared-repo-name-wikimediacommons": "Викимедијина остава",
        "filepage.css": "/* CSS који је постављен овде се налази на страницама за опис датотека, као и на страним викијима */",
        "upload-disallowed-here": "Не можете да замените ову датотеку.",
        "filerevert": "Врати $1",
        "wantedfiles": "Тражене датотеке",
        "wantedfiletext-cat": "Следеће датотеке се користе, али не постоје. Датотеке из других ризница могу бити наведене иако не постоје. Такве датотеке ће бити <del>поништене</del> са списка. Поред тога, странице које садрже непостојеће датотеке се налазе [[:$1|овде]].",
        "wantedfiletext-nocat": "Следеће датотеке се користе, али не постоје. Датотеке из других ризница могу бити наведене иако не постоје. Такве датотеке ће бити <del>поништене</del> са списка.",
+       "wantedfiletext-nocat-noforeign": "Следеће датотеке се користе, али не постоје.",
        "wantedtemplates": "Тражени шаблони",
        "mostlinked": "Странице с највише веза",
        "mostlinkedcategories": "Категорије с највише веза",
index 799eaea..aa575c1 100644 (file)
@@ -16,7 +16,8 @@
                        "Жељко Тодоровић",
                        "Михајло Анђелковић",
                        "לערי ריינהארט",
-                       "아라"
+                       "아라",
+                       "Nemo bis"
                ]
        },
        "tog-underline": "Podvlačenje veza:",
        "talkpagelinktext": "razgovor",
        "specialpage": "Posebna stranica",
        "personaltools": "Lične alatke",
-       "postcomment": "Novi odeljak",
        "articlepage": "Pogledaj stranicu sa sadržajem",
        "talk": "Razgovor",
        "views": "Pregledi",
        "externaldberror": "Došlo je do greške pri prepoznavanju baze podataka ili nemate ovlašćenja da ažurirate svoj spoljni nalog.",
        "login": "Prijavi me",
        "nav-login-createaccount": "Prijava/registracija",
-       "loginprompt": "Omogućite kolačiće da biste se prijavili na ovaj viki.",
        "userlogin": "Prijava/registracija",
        "userloginnocreate": "Prijava",
        "logout": "Odjava",
        "uploadnewversion-linktext": "Pošalji novo izdanje ove datoteke",
        "shared-repo-from": "iz $1",
        "shared-repo": "zajedničko skladište",
-       "shared-repo-name-wikimediacommons": "{{#SWITCH:{{{1|}}}\n|#default=Vikimedijina ostava\n|dat=Vikimedijinoj ostavi\n}}",
+       "shared-repo-name-wikimediacommons": "Vikimedijina ostava",
        "filepage.css": "/* CSS koji je postavljen ovde se nalazi na stranicama za opis datoteka, kao i na stranim vikijima */",
        "upload-disallowed-here": "Ne možete da zamenite ovu datoteku.",
        "filerevert": "Vrati $1",
index 6dc5f90..4223180 100644 (file)
        "talkpagelinktext": "Diskussion",
        "specialpage": "Specialsida",
        "personaltools": "Personliga verktyg",
-       "postcomment": "Nytt avsnitt",
        "articlepage": "Visa innehållssida",
        "talk": "Diskussion",
        "views": "Visningar",
        "externaldberror": "Antingen inträffade autentiseringsproblem med en extern databas, eller så får du inte uppdatera ditt externa konto.",
        "login": "Logga in",
        "nav-login-createaccount": "Logga in / skapa konto",
-       "loginprompt": "Du måste tillåta kakor för att logga in på {{SITENAME}}.",
        "userlogin": "Logga in / skapa konto",
        "userloginnocreate": "Logga in",
        "logout": "Logga ut",
        "license-nopreview": "(Förhandsvisning är inte tillgänglig)",
        "upload_source_url": " (en giltig URL som är allmänt åtkomlig)",
        "upload_source_file": " (en fil på din dator)",
+       "listfiles-delete": "radera",
        "listfiles-summary": "Den här specialsidan visar alla filer som laddats upp.",
        "listfiles_search_for": "Sök efter filnamn:",
        "imgfile": "fil",
        "wantedpages-badtitle": "Ogiltig titel bland resultaten: $1",
        "wantedfiles": "Önskade filer",
        "wantedfiletext-cat": "Följande filer används men finns inte. Filer från utländska databaser kan vara listade trots att de inte finns. Sådana falska realiteter kommer att <del>tas bort</del>. Sidor som bäddar in filer som inte finns listas upp på [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Följande filer används men finns inte. Sidor som bäddar in filer som inte finns listas upp i [[:$1]].",
        "wantedfiletext-nocat": "Följande filer används men finns inte. Filer från utländska databaser kan vara listade trots att de inte finns. Sådana falska realiteter kommer att <del>tas bort</del>.",
+       "wantedfiletext-nocat-noforeign": "Följande filer används men finns inte.",
        "wantedtemplates": "Önskade mallar",
        "mostlinked": "Sidor med flest länkar till sig",
        "mostlinkedcategories": "Kategorier med flest länkar till sig",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|diskussion]])",
        "unknown_extension_tag": "Okänd tagg \"$1\"",
        "duplicate-defaultsort": "'''Varning:''' Standardsorteringsnyckeln \"$2\" tar över från den tidigare standardsorteringsnyckeln \"$1\".",
+       "duplicate-displaytitle": "<strong>Varning:</strong> Visningstiteln \"$2\" skriver över den tidigare visningstiteln \"$1\".",
        "version": "Version",
        "version-extensions": "Installerade programtillägg",
        "version-skins": "Installerade utseenden",
index 00479fe..d8d1b2d 100644 (file)
        "talkpagelinktext": "Mesaj",
        "specialpage": "Özel sayfa",
        "personaltools": "Kişisel araçlar",
-       "postcomment": "Yeni bölüm",
        "articlepage": "İçerik sayfasını gör",
        "talk": "Tartışma",
        "views": "Görünümler",
        "externaldberror": "Ya doğrulama veritabanı hatası var ya da kullanıcı hesabınızı güncellemeye yetkiniz yok.",
        "login": "Oturum aç",
        "nav-login-createaccount": "Oturum aç / hesap oluştur",
-       "loginprompt": "{{SITENAME}} sitesinde oturum açabilmek için çerezleri etkinleştirmeniz gerekmektedir.",
        "userlogin": "Oturum aç / hesap oluştur",
        "userloginnocreate": "Giriş yap",
        "logout": "Oturumu kapat",
        "resetpass-submit-cancel": "İptal",
        "resetpass-wrong-oldpass": "Geçersiz geçici veya güncel şifre.\nŞifrenizi zaten başarıyla değiştirdiniz ya da yeni bir geçici şifre istediniz.",
        "resetpass-recycled": "Lütfen parolanızı eski parolanızdan farklı olarak değiştirin.",
+       "resetpass-temp-emailed": "E-postayla gönderilmiş geçici kodla giriş yaptınız. Oturum açmayı tamamlamak için yeni bir şifre belirlemeniz gerekiyor:",
        "resetpass-temp-password": "Geçici parola:",
        "resetpass-abort-generic": "Parola değişikliği bir uzantı tarafından iptal edildi.",
        "resetpass-expired": "Parolanızın süresi bitti. Lütfen, giriş için yeni bir parola oluşturun.",
        "resetpass-expired-soft": "Parolanızın süresi bitti ve değiştirilmesi gerekiyor. Lütfen, yeni bir parola seçin veya daha sonra oluşturmak için \"{{int:resetpass-submit-cancel}}\" butonuna tıklayın.",
+       "resetpass-validity-soft": "Parolanız geçerli değiş: $1\n\nLütfen yeni bir şifre belirleyin veya daha sonra sıfırlamak için \"{{int:resetpass-submit-cancel}}\" bağlantısını tıklayın.",
        "passwordreset": "Parola sıfırlama",
        "passwordreset-text-one": "Parolanızı sıfırlamak için bu formu doldurun.",
        "passwordreset-text-many": "{{PLURAL:$1|E-posta ile geçici bir parola almak için alanlardan birini doldurun.}}",
        "revdelete-show-file-confirm": "\"<nowiki>$1</nowiki>\" dosyasının $2 $3 tarihli silinmiş bir revizyonunu görmek istediğinize emin misiniz?",
        "revdelete-show-file-submit": "Evet",
        "logdelete-selected": "{{PLURAL:$1|Seçili kayıt olayı|Seçili kayıt olayları}}:",
+       "revdelete-text-text": "Silinen sürümler sayfa geçmişinde yer almaya devam edecek ancak okuyucular tarafından içeriklerine erişilemeyecektir.",
+       "revdelete-text-file": "Silinen dosya sürümleri dosya geçmişinde yer almaya devam edecek ancak okuyucular tarafından içerik bölümlerine erişilemeyecektir.",
        "revdelete-confirm": "Lütfen, bunu yapmak istediğinizi , sonuçlarını anladığınızı, ve bunu [[{{MediaWiki:Policy-url}}|ilkelere]] göre yapıyor olduğunuzu onaylayın.",
        "revdelete-suppress-text": "Saklama '''sadece''' aşağıdaki durumlarda kullanılmalıdır:\n* Muhtemel iftira niteliğindeki bilgi\n* Uygunsuz kişisel bilgi\n*: ''ev adresleri ve telefon numaraları, sosyal güvenlik numaraları, vs.''",
        "revdelete-legend": "Görünürlük kısıtlamaları ayarla",
        "license-nopreview": "(Önizleme etkin değil)",
        "upload_source_url": " (geçerli, herkesin ulaşabileceği bir URL)",
        "upload_source_file": " (bilgisayarınızdaki bir dosya)",
+       "listfiles-delete": "sil",
        "listfiles-summary": "Bu özel sayfa yüklenen tüm dosyaları gösterir.",
        "listfiles_search_for": "Medya adı ara:",
        "imgfile": "dosya",
        "filedelete-maintenance": "Dosyaların silinmesi ve geri getirilmesi bakım süresince geçici olarak devre dışı bırakıldı.",
        "filedelete-maintenance-title": "Dosya silinemiyor",
        "mimesearch": "MIME araması",
-       "mimesearch-summary": "Bu sayfa, MIME türü dosyaların süzülmesini sağlar. Girdi: içeriktürü/alttürü, e.g. <code>resim/jpeg</code>.",
+       "mimesearch-summary": "Bu sayfa, dosyaların MIME türlerine göre filtrelenmesini sağlar. Girdi: içerik_türü/alt_tür veya içerik_türü/*, örn. <code>image/jpeg</code>.",
        "mimetype": "MIME türü:",
        "download": "yükle",
        "unwatchedpages": "İzlenmeyen sayfalar",
        "pageswithprop-prophidden-binary": "ikili özellik değeri gizlendi ($1)",
        "doubleredirects": "Çift yönlendirmeler",
        "doubleredirectstext": "Bu sayfa diğer yönlendirme sayfalarına yönlendirme yapan sayfaları listeler.\nHer satırın içerdiği bağlantılar; birinci ve ikinci yönlendirme, ayrıca ikinci yönlendirmenin hedefi, ki bu genelde birinci yönlendirmenin göstermesi gereken \"gerçek\" hedef sayfasıdır.\n<del>Üstü çizili</del> girdiler çözülmüştür.",
-       "double-redirect-fixed-move": "[[$1]] taşındı, artık [[$2]] sayfasına yönlendiriyor",
+       "double-redirect-fixed-move": "[[$1]] taşındı.\nYönlendirme otomatik olarak güncellendi ve [[$2]] sayfasına yönlendirildi.",
        "double-redirect-fixed-maintenance": "[[$1]] - [[$2]] yapılan çift yönlendirme düzeltiliyor.",
        "double-redirect-fixer": "Yönlendirme tamircisi",
        "brokenredirects": "Boş yönlendirmeler",
        "movenotallowedfile": "Sayfaları taşımaya izniniz yok.",
        "cant-move-user-page": "Kullanıcı sayfalarını taşımaya izniniz yok (altsayfalardan başka).",
        "cant-move-to-user-page": "Bir sayfayı, bir kullanıcı sayfasına taşımaya izniniz yok (bir kullanıcı altsayfası dışında).",
+       "cant-move-category-page": "Kategori sayfalarını taşıma yetkiniz yok.",
        "cant-move-to-category-page": "Bir sayfayı, bir kategoriye taşımaya izniniz yok.",
        "newtitle": "Yeni isim",
        "move-watch": "Bu sayfayı izle",
        "pagelang-language": "Dil",
        "pagelang-use-default": "Varsayılan dili kullan",
        "pagelang-select-lang": "Dil seçin",
-       "right-pagelang": "Sayfa dilini değiştir"
+       "right-pagelang": "Sayfa dilini değiştir",
+       "action-pagelang": "sayfa dilini değiştir"
 }
index 8cc9001..0fa99e0 100644 (file)
        "talkpagelinktext": "Бәхәс",
        "specialpage": "Махсус бит",
        "personaltools": "Шәхси кораллар",
-       "postcomment": "Яңа бүлек",
        "articlepage": "Мәкаләне карау",
        "talk": "Бәхәс",
        "views": "Караулар",
        "externaldberror": "Тышкы мәгълүмат базасы ярдәмендә аутентификация үткәндә хата чыкты, яисә тышкы хисап язмагызга үзгәрешләр кертү хокукыгыз юк.",
        "login": "Керү",
        "nav-login-createaccount": "Керү / теркәлү",
-       "loginprompt": "{{SITENAME}} проектына керү өчен «cookies» рөхсәт ителгән булырга тиеш.",
        "userlogin": "Керү / теркәлү",
        "userloginnocreate": "Керү",
        "logout": "Чыгу",
        "newimages": "Яңа сүрәтләр җыелмасы",
        "newimages-legend": "Фильтр",
        "ilsubmit": "Эзләү",
+       "hours": "{{PLURAL:$1|$1 cәгать|$1 cәгать}}",
+       "hours-ago": "$1 cәгать элек",
+       "minutes-ago": "$1 минут элек",
        "bad_image_list": "Киләчәк рәвеш кирәк:\n\nИсемлек кисәкләре генә (* символыннан башланучы юллар) саналырлар.\nЮлның беренче сылтамасы куйма өчен тыелган рәсемгә сылтама булырга тиеш.\nШул ук юлның киләчәк сылтамалары чыгармалар, рәсемгә тыелмаган битләре, саналырлар.",
        "metadata": "Мета мәгълүматлар",
        "metadata-help": "Бу файлда гадәттә санлы камера яки сканер тарафыннан өстәлгән мәгълүмат бар. Әгәр бу файл төзү вакытыннан соң үзгәртелгән булса, аның кайбер параметрлары дөрес булмаска мөмкин.",
index 95d9f26..b1a48d9 100644 (file)
        "talkpagelinktext": "обговорення",
        "specialpage": "Спеціальна сторінка",
        "personaltools": "Особисті інструменти",
-       "postcomment": "Новий розділ",
        "articlepage": "Переглянути статтю",
        "talk": "Обговорення",
        "views": "Перегляди",
        "externaldberror": "Сталася помилка при автентифікації за допомогою зовнішньої бази даних, або у вас недостатньо прав для внесення змін до свого зовнішнього облікового запису.",
        "login": "Вхід до системи",
        "nav-login-createaccount": "Вхід / реєстрація",
-       "loginprompt": "Ви повинні активувати куки (cookies) для входу до {{GRAMMAR:genitive|{{SITENAME}}}}.",
        "userlogin": "Вхід / реєстрація",
        "userloginnocreate": "Увійти",
        "logout": "Вихід із системи",
index a262641..63be321 100644 (file)
        "talkpagelinktext": "Thảo luận",
        "specialpage": "Trang đặc biệt",
        "personaltools": "Công cụ cá nhân",
-       "postcomment": "Đề mục mới",
        "articlepage": "Xem trang nội dung",
        "talk": "Thảo luận",
        "views": "Các hiển thị",
        "externaldberror": "Có lỗi khi xác nhận cơ sở dữ liệu bên ngoài hoặc bạn không được phép cập nhật tài khoản bên ngoài.",
        "login": "Đăng nhập",
        "nav-login-createaccount": "Đăng nhập / Mở tài khoản",
-       "loginprompt": "Bạn cần bật cookie để đăng nhập vào {{SITENAME}}.",
        "userlogin": "Đăng nhập / Mở tài khoản",
        "userloginnocreate": "Đăng nhập",
        "logout": "Đăng xuất",
        "noemailprefs": "Hãy ghi một địa chỉ thư điện tử trong tùy chọn cá nhân để có thể sử dụng tính năng này.",
        "emailconfirmlink": "Xác nhận địa chỉ thư điện tử",
        "invalidemailaddress": "Địa chỉ thư điện tử không được chấp nhận vì định dạng thư có vẻ sai.\nHãy nhập một địa chỉ có định dạng đúng hoặc bỏ trống ô đó.",
-       "cannotchangeemail": "Không có thể thay đổi địa chỉ thư điện tử của các tài khoản trên wiki này.",
+       "cannotchangeemail": "Không thể thay đổi địa chỉ thư điện tử của các tài khoản trên wiki này.",
        "emaildisabled": "Website này không thể gửi thư điện tử.",
        "accountcreated": "Mở tài khoản thành công",
        "accountcreatedtext": "Tài khoản thành viên cho [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|thảo luận]]) đã được mở.",
        "prefs-custom-css": "sửa CSS",
        "prefs-custom-js": "sửa JS",
        "prefs-common-css-js": "CSS/JS chung cho mọi giao diện:",
-       "prefs-reset-intro": "Có thể mặc định lại toàn bộ tùy chọn dùng trang này.\nKhông có thể lùi lại tác động này.",
+       "prefs-reset-intro": "Có thể mặc định lại toàn bộ tùy chọn dùng trang này. Điều này không thể hoàn tác.",
        "prefs-emailconfirm-label": "Xác nhận thư điện tử:",
        "youremail": "Thư điện tử:",
        "username": "{{GENDER:$1}}Tên người dùng:",
        "ignorewarnings": "Bỏ qua cảnh báo",
        "minlength1": "Tên tập tin phải có ít nhất một ký tự.",
        "illegalfilename": "Tên tập tin “$1” có chứa ký tự không được phép dùng cho tựa trang. Xin hãy đổi tên và tải lên lại.",
-       "filename-toolong": "Tên tập tin không có thể dài quá 240 byte.",
+       "filename-toolong": "Tên tập tin không thể dài quá 240 byte.",
        "badfilename": "Tên tập tin đã được đổi thành “$1”.",
        "filetype-mime-mismatch": "Phần mở rộng của tập tin (“.$1”) không phù hợp kiểu MIME được nhận ra ($2).",
        "filetype-badmime": "Không thể tải lên các tập tin có kiểu MIME “$1”.",
        "license-nopreview": "(Không xem trước được)",
        "upload_source_url": " (địa chỉ URL đúng, có thể truy cập)",
        "upload_source_file": " (tập tin trên máy của bạn)",
+       "listfiles-delete": "xóa",
        "listfiles-summary": "Trang đặc biệt này liệt kê các tập tin được tải lên.",
        "listfiles_search_for": "Tìm kiếm theo tên tập tin:",
        "imgfile": "tập tin",
        "shared-repo-from": "tại $1",
        "shared-repo": "kho lưu trữ dùng chung",
        "filepage.css": "/* Mã CSS tại đây sẽ ảnh hướng đến trang miêu tả tập tin, cũng như các wiki khách bên ngoài dựa trên wiki này */",
-       "upload-disallowed-here": "Bạn không có thể ghi đè lên tập tin này.",
+       "upload-disallowed-here": "Bạn không thể ghi đè lên tập tin này.",
        "filerevert": "Lùi lại phiên bản của $1",
        "filerevert-legend": "Lùi lại tập tin",
        "filerevert-intro": "Bạn đang lùi '''[[Media:$1|$1]]''' về [$4 phiên bản lúc $3, $2].",
        "wantedpages-badtitle": "Tiêu đề không hợp lệ trong tập kết quả: $1",
        "wantedfiles": "Tập tin cần thiết",
        "wantedfiletext-cat": "Các tập tin sau được nhúng nhưng không tồn tại. Các tập tin từ kho dùng chung có thể được liệt kê trong khi tồn tại; các trường hợp này được <del>gạch bỏ</del>. Ngoài ra, các trang nhúng tập tin không tồn tại được liệt kê tại [[:$1]].",
+       "wantedfiletext-cat-noforeign": "Các tập tin bên dưới được sử dụng nhưng không tồn tại. Các trang nhúng những tập tin không tồn tại cũng được xếp trong [[:$1]].",
        "wantedfiletext-nocat": "Các tập tin sau được nhúng nhưng không tồn tại. Các tập tin từ kho dùng chung có thể được liệt kê trong khi tồn tại; các trường hợp này được <del>gạch bỏ</del>.",
+       "wantedfiletext-nocat-noforeign": "Các tập tin bên dưới được sử dụng nhưng không tồn tại.",
        "wantedtemplates": "Bản mẫu cần viết nhất",
        "mostlinked": "Trang được liên kết đến nhiều nhất",
        "mostlinkedcategories": "Thể loại có nhiều trang nhất",
        "imported-log-entries": "Đã nhập {{PLURAL:$1|mục nhật trình|$1 mục nhật trình}}.",
        "importfailed": "Không nhập được: $1",
        "importunknownsource": "Không hiểu nguồn trang để nhập vào",
-       "importcantopen": "Không có thể mở tập tin để nhập vào",
+       "importcantopen": "Không thể mở tập tin để nhập vào",
        "importbadinterwiki": "Liên kết liên wiki sai",
        "importsuccess": "Nhập thành công!",
        "importnosources": "Không có nguồn nhập giữa wiki và việc nhập lịch sử bị tắt.",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|thảo luận]])",
        "unknown_extension_tag": "Không hiểu thẻ mở rộng “$1”",
        "duplicate-defaultsort": "Cảnh báo: Từ khóa xếp mặc định “$2” ghi đè từ khóa trước, “$1”.",
+       "duplicate-displaytitle": "<strong>Cảnh báo:</strong> Tên hiển thị “$2” ghi đè tên hiển thị “$1” bên trên.",
        "version": "Phiên bản",
        "version-extensions": "Các phần mở rộng được cài đặt",
        "version-skins": "Giao diện đã cài đặt",
index 5778823..62b2328 100644 (file)
        "talkpagelinktext": "討論",
        "specialpage": "特別頁",
        "personaltools": "私人家伙",
-       "postcomment": "新段",
        "articlepage": "望內容頁",
        "talk": "探讨",
        "views": "望",
index 816bf32..4376f87 100644 (file)
        "talkpagelinktext": "שמועס",
        "specialpage": "ספעציעלער בלאט",
        "personaltools": "פערזענלעכע געצייג",
-       "postcomment": "נייע אפטיילונג",
        "articlepage": "זען אינהאַלט בלאַט",
        "talk": "שמועס",
        "views": "קוקן",
index a67e3d9..0c53e99 100644 (file)
@@ -45,6 +45,7 @@
        "tog-showhiddencats": "Ṣ'àfihàn àwọn ẹ̀ka pípamọ́",
        "tog-norollbackdiff": "Fo ìyàtọ̀ lẹ́yín síṣe ìyísẹ́yìn",
        "tog-useeditwarning": "Kìlọ̀ fún mi tí mo bá únkúrò ní ojúewé àtúnṣe láì tíì mupamọ́",
+       "tog-prefershttps": "Lo ìjáwọlé oníàbò ní gbogbo ìgbà",
        "underline-always": "Nígbà gbogbo",
        "underline-never": "Rárá",
        "underline-default": "Ti àwọ tàbí ẹrọ́ ìtọ́kùn",
        "permalink": "Ìjápọ̀ tíkòníyípadà",
        "print": "Ìtẹ̀síìwé",
        "view": "Ìwòran",
+       "view-foreign": "Ìgbéwò lórí $1",
        "edit": "Àtúnṣe",
+       "edit-local": "Àtúnṣe ìjúwe ìhàhín",
        "create": "Ṣèdá",
+       "create-local": "Ìfikún ìjúwe ìhàhín",
        "editthispage": "S'àtúnṣe ojúewé yi",
        "create-this-page": "Ṣè'dá ojúewé yìí",
        "delete": "Ìparẹ́",
        "talkpagelinktext": "Ọ̀rọ̀",
        "specialpage": "Ojúewé Pàtàkì",
        "personaltools": "Àwọn irinṣẹ́ àdáni",
-       "postcomment": "Abala tuntun",
        "articlepage": "Ìfihàn àkóónú ojúewé",
        "talk": "Ìfọ̀rọ̀wérọ̀",
        "views": "Àwọn ìwò",
        "externaldberror": "Bóyá àsìṣe ìfidájú ibùdó dátà ló ṣẹlẹ̀ tàbí ẹ kò jẹ́ gbígbà ní ààyè láti sọ àpamọ́ òde yín di ọ̀tun.",
        "login": "Ìjáwọlé",
        "nav-login-createaccount": "Ìwọlé / Ìforúkọ sílẹ̀",
-       "loginprompt": "Ẹ gbọ́dọ̀ jọ̀wọ́ cookies láti wọlé sí {{SITENAME}}.",
        "userlogin": "Ìwọlé / ìforúkọ sílẹ̀",
        "userloginnocreate": "Ìjáwọlé",
        "logout": "Ìjáde",
        "duplicate-defaultsort": "'''Ìkìlọ̀:''' Bọ́tìnì ìtò àkọ́kọ́ṣe \"$2\" dípò Bọ́tìnì ìtò àkọ́kọ́ṣe \"$1\" tẹ́lẹ̀.",
        "version": "Àtẹ̀jáde",
        "version-extensions": "Àwọn ìfàgùn kíkànsínú",
+       "version-skins": "Skin (Àwọ̀)",
        "version-specialpages": "Àwọn ojúewé pàtàkì",
        "version-variables": "Ayàtọ̀",
        "version-antispam": "Ìdínà spam",
-       "version-skins": "Skin (Àwọ̀)",
        "version-other": "Òmíràn",
        "version-hooks": "Àwọn hook",
        "version-hook-name": "Orúkọ hook",
index 8139cf8..54cc41c 100644 (file)
        "talkpagelinktext": "讨论",
        "specialpage": "特殊页面",
        "personaltools": "个人工具",
-       "postcomment": "新段落",
        "articlepage": "查看内容页面",
        "talk": "讨论",
        "views": "查看",
        "externaldberror": "验证数据库出错或您被禁止更新您的外部账号。",
        "login": "登录",
        "nav-login-createaccount": "登录/创建账户",
-       "loginprompt": "你必须启用Cookie才能登录{{SITENAME}}。",
        "userlogin": "登录/创建账户",
        "userloginnocreate": "登录",
        "logout": "退出",
        "wantedpages-badtitle": "在结果组上的无效标题:$1",
        "wantedfiles": "需要的文件",
        "wantedfiletext-cat": "以下文件被使用,但并不存在。来自外部库的文件即使存在也可能被列出。任何这类误报会用<del>删除线</del>标记。另外,插入不存在的文件的页面列于[[:$1]]。",
+       "wantedfiletext-cat-noforeign": "以下文件被使用但尚不存在。此外嵌入不存在文件的页面在[[:$1]]列出。",
        "wantedfiletext-nocat": "以下文件被使用,但并不存在。来自外部库的文件即使存在也可能被列出。任何这类误报会用<del>删除线</del>标记。",
+       "wantedfiletext-nocat-noforeign": "以下文件被使用但尚不存在。",
        "wantedtemplates": "需要的模板",
        "mostlinked": "最多链接页面",
        "mostlinkedcategories": "最多链接分类",
        "signature": "[[{{ns:user}}:$1|$2]]([[{{ns:user_talk}}:$1|讨论]])",
        "unknown_extension_tag": "未知扩展标签“$1”",
        "duplicate-defaultsort": "'''警告:'''默认排序关键词“$2”覆盖了之前的默认排序关键词“$1”。",
+       "duplicate-displaytitle": "<strong>警告:</strong>显示的标题“$2”重写了此前显示的标题“$1”。",
        "version": "版本",
        "version-extensions": "安装的扩展程序",
        "version-skins": "已安装皮肤",
index 2dbb891..341bf45 100644 (file)
        "talkpagelinktext": "對話",
        "specialpage": "特殊頁面",
        "personaltools": "個人工具",
-       "postcomment": "新章節",
        "articlepage": "檢視內容頁面",
        "talk": "討論",
        "views": "檢視",
        "externaldberror": "這可能是由於資料庫驗證錯誤,或是不允許您更新外部帳號。",
        "login": "登入",
        "nav-login-createaccount": "登入/建立帳號",
-       "loginprompt": "您必須允許瀏覽器紀錄 Cookie 才能成功登入 {{SITENAME}}。",
        "userlogin": "登入/建立帳號",
        "userloginnocreate": "登入",
        "logout": "登出",
        "license-nopreview": "(不可預覽)",
        "upload_source_url": "(有效,可公開存取的 URL)",
        "upload_source_file": "(在您電腦上的檔案)",
+       "listfiles-delete": "刪除",
        "listfiles-summary": "此特殊頁面顯示所有上傳過的檔案。",
        "listfiles_search_for": "搜尋媒體名稱:",
        "imgfile": "檔案",
        "wantedpages-badtitle": "在結果組上的無效標題: $1",
        "wantedfiles": "需要的檔案",
        "wantedfiletext-cat": "以下檔案被使用,但不存在。外部儲存庫的文件儘管現有,但可能會在此列出,任何此類的誤報將被<del>剔除</del>。此外,內嵌了不存在的檔案的網頁將在[[:$1]]列出。",
+       "wantedfiletext-cat-noforeign": "下列檔案已被使用但不存在。 除此之外,頁面已內嵌但不存在的檔案列於 [[:$1]]。",
        "wantedfiletext-nocat": "以下檔案被使用,但不存在。外部儲存庫的文件儘管現有,但可能會在此列出,任何此類的誤報將被<del>剔除</del>。",
+       "wantedfiletext-nocat-noforeign": "下列檔案已被使用但不存在。",
        "wantedtemplates": "需要的樣版",
        "mostlinked": "最多連結頁面",
        "mostlinkedcategories": "最多連結分類",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|對話]])",
        "unknown_extension_tag": "不明的擴充標籤 \"$1\"",
        "duplicate-defaultsort": "<strong>警告:</strong>預設的排序鍵 \"$2\" 會覆蓋先前預設的排序鍵 \"$1\"。",
+       "duplicate-displaytitle": "<strong>警告:</strong> 顯示標題 \"$2\" 覆蓋之前的顯示標題 \"$1\"。",
        "version": "版本",
        "version-extensions": "已安裝的擴充套件",
-       "version-skins": "已外觀",
+       "version-skins": "已安裝的外觀",
        "version-specialpages": "特殊頁面",
        "version-parserhooks": "語法連結(Hook)",
        "version-variables": "變數",
        "version-entrypoints": "入口 URL",
        "version-entrypoints-header-entrypoint": "入口",
        "version-entrypoints-header-url": "URL",
-       "version-entrypoints-articlepath": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgArticlePath 條目路徑]",
+       "version-entrypoints-articlepath": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgArticlePath 文章路徑]",
+       "version-entrypoints-scriptpath": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgScriptPath Script 路徑]",
        "redirect": "重新導向至檔案、使用者、頁面或修訂 ID",
        "redirect-legend": "重新導向至檔案或頁面",
        "redirect-summary": "此特殊頁面可用來重新導向至檔案 (指定檔案名稱)、頁面 (指定修訂 ID 或頁面 ID) 或使用者頁面 (指定使用者 ID)。用法:[[{{#Special:Redirect}}/file/Example.jpg]]、[[{{#Special:Redirect}}/page/64308]]、[[{{#Special:Redirect}}/revision/328429]] 或 [[{{#Special:Redirect}}/user/101]]。",
index 6900aeb..8167b19 100644 (file)
@@ -365,6 +365,8 @@ $magicWords = array(
        'url_query'               => array( 0,    'QUERY' ),
        'defaultsort_noerror'     => array( 0,    'noerror' ),
        'defaultsort_noreplace'   => array( 0,    'noreplace' ),
+       'displaytitle_noerror'    => array( 0,    'noerror' ),
+       'displaytitle_noreplace'  => array( 0,    'noreplace' ),
        'pagesincategory_all'     => array( 0,    'all' ),
        'pagesincategory_pages'   => array( 0,    'pages' ),
        'pagesincategory_subcats' => array( 0,    'subcats' ),
index ca9572f..e1ccb46 100755 (executable)
@@ -72,7 +72,7 @@ NEWCHANGESDISPLAY=$(git log $OLDHASH.. --oneline --no-merges --reverse --color=a
 
 # Copy files
 # - Exclude the default non-svg stylesheet
-rsync --recursive --delete --force --exclude 'oojs-ui.css' ./dist/ $TARGET_REPO/$TARGET_DIR || exit 1
+rsync --recursive --delete --force --exclude 'oojs-ui.css' --exclude 'oojs-ui*.rtl.css' ./dist/ $TARGET_REPO/$TARGET_DIR || exit 1
 
 # Read the new version
 NEWVERSION=$(oojsuiversion)
index 02048c0..4595994 100644 (file)
@@ -366,7 +366,6 @@ var spliceWorksWithEmptyObject = (function () {
     ArrayPrototype.splice.call(obj, 0, 0, 1);
     return obj.length === 1;
 }());
-var omittingSecondSpliceArgIsNoop = [1].splice(0).length === 0;
 defineProperties(ArrayPrototype, {
     splice: function splice(start, deleteCount) {
         if (arguments.length === 0) { return []; }
@@ -375,14 +374,14 @@ defineProperties(ArrayPrototype, {
         if (arguments.length > 0 && typeof deleteCount !== 'number') {
             args = _Array_slice_.call(arguments);
             if (args.length < 2) {
-                args.push(toInteger(deleteCount));
+                args.push(this.length - start);
             } else {
                 args[1] = toInteger(deleteCount);
             }
         }
         return array_splice.apply(this, args);
     }
-}, !omittingSecondSpliceArgIsNoop || !spliceWorksWithEmptyObject);
+}, !spliceWorksWithEmptyObject);
 
 // ES5 15.4.4.12
 // http://es5.github.com/#x15.4.4.13
@@ -674,7 +673,7 @@ defineProperties(ArrayPrototype, {
 // ES5 15.4.4.14
 // http://es5.github.com/#x15.4.4.14
 // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf
-var hasFirefox2IndexOfBug = [0, 1].indexOf(1, 2) !== -1;
+var hasFirefox2IndexOfBug = Array.prototype.indexOf && [0, 1].indexOf(1, 2) !== -1;
 defineProperties(ArrayPrototype, {
     indexOf: function indexOf(sought /*, fromIndex */ ) {
         var self = splitString && isString(this) ? this.split('') : toObject(this),
@@ -703,7 +702,7 @@ defineProperties(ArrayPrototype, {
 // ES5 15.4.4.15
 // http://es5.github.com/#x15.4.4.15
 // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/lastIndexOf
-var hasFirefox2LastIndexOfBug = [0, 1].lastIndexOf(0, -3) !== -1;
+var hasFirefox2LastIndexOfBug = Array.prototype.lastIndexOf && [0, 1].lastIndexOf(0, -3) !== -1;
 defineProperties(ArrayPrototype, {
     lastIndexOf: function lastIndexOf(sought /*, fromIndex */) {
         var self = splitString && isString(this) ? this.split('') : toObject(this),
@@ -1082,7 +1081,7 @@ if (!Date.now) {
 // http://es5.github.com/#x15.7.4.5
 var hasToFixedBugs = NumberPrototype.toFixed && (
   (0.00008).toFixed(3) !== '0.000'
-  || (0.9).toFixed(0) === '0'
+  || (0.9).toFixed(0) !== '1'
   || (1.255).toFixed(2) !== '1.25'
   || (1000000000000000128).toFixed(0) !== "1000000000000000128"
 );
index 6d5974a..95001fb 100644 (file)
@@ -1,5 +1,6 @@
+/*jshint eqnull:true */
 /*!
- * jQuery Cookie Plugin
+ * jQuery Cookie Plugin v1.2
  * https://github.com/carhartl/jquery-cookie
  *
  * Copyright 2011, Klaus Hartl
@@ -7,41 +8,65 @@
  * http://www.opensource.org/licenses/mit-license.php
  * http://www.opensource.org/licenses/GPL-2.0
  */
-(function($) {
-    $.cookie = function(key, value, options) {
-
-        // key and at least value given, set cookie...
-        if (arguments.length > 1 && (!/Object/.test(Object.prototype.toString.call(value)) || value === null || value === undefined)) {
-            options = $.extend({}, options);
-
-            if (value === null || value === undefined) {
-                options.expires = -1;
-            }
-
-            if (typeof options.expires === 'number') {
-                var days = options.expires, t = options.expires = new Date();
-                t.setDate(t.getDate() + days);
-            }
-
-            value = String(value);
-
-            return (document.cookie = [
-                encodeURIComponent(key), '=', options.raw ? value : encodeURIComponent(value),
-                options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
-                options.path    ? '; path=' + options.path : '',
-                options.domain  ? '; domain=' + options.domain : '',
-                options.secure  ? '; secure' : ''
-            ].join(''));
-        }
-
-        // key and possibly options given, get cookie...
-        options = value || {};
-        var decode = options.raw ? function(s) { return s; } : decodeURIComponent;
-
-        var pairs = document.cookie.split('; ');
-        for (var i = 0, pair; pair = pairs[i] && pairs[i].split('='); i++) {
-            if (decode(pair[0]) === key) return decode(pair[1] || ''); // IE saves cookies with empty string as "c; ", e.g. without "=" as opposed to EOMB, thus pair[1] may be undefined
-        }
-        return null;
-    };
-})(jQuery);
+(function ($, document, undefined) {
+
+       var pluses = /\+/g;
+
+       function raw(s) {
+               return s;
+       }
+
+       function decoded(s) {
+               return decodeURIComponent(s.replace(pluses, ' '));
+       }
+
+       $.cookie = function (key, value, options) {
+
+               // key and at least value given, set cookie...
+               if (value !== undefined && !/Object/.test(Object.prototype.toString.call(value))) {
+                       options = $.extend({}, $.cookie.defaults, options);
+
+                       if (value === null) {
+                               options.expires = -1;
+                       }
+
+                       if (typeof options.expires === 'number') {
+                               var days = options.expires, t = options.expires = new Date();
+                               t.setDate(t.getDate() + days);
+                       }
+
+                       value = String(value);
+
+                       return (document.cookie = [
+                               encodeURIComponent(key), '=', options.raw ? value : encodeURIComponent(value),
+                               options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
+                               options.path    ? '; path=' + options.path : '',
+                               options.domain  ? '; domain=' + options.domain : '',
+                               options.secure  ? '; secure' : ''
+                       ].join(''));
+               }
+
+               // key and possibly options given, get cookie...
+               options = value || $.cookie.defaults || {};
+               var decode = options.raw ? raw : decoded;
+               var cookies = document.cookie.split('; ');
+               for (var i = 0, parts; (parts = cookies[i] && cookies[i].split('=')); i++) {
+                       if (decode(parts.shift()) === key) {
+                               return decode(parts.join('='));
+                       }
+               }
+
+               return null;
+       };
+
+       $.cookie.defaults = {};
+
+       $.removeCookie = function (key, options) {
+               if ($.cookie(key, options) !== null) {
+                       $.cookie(key, null, options);
+                       return true;
+               }
+               return false;
+       };
+
+})(jQuery, document);
index 3ff9763..4555c11 100644 (file)
@@ -7,11 +7,12 @@
                        "Pginer",
                        "QuimGil",
                        "SMP",
-                       "Vriullop"
+                       "Vriullop",
+                       "Toniher"
                ]
        },
-       "ooui-dialog-action-close": "Tanca",
        "ooui-outline-control-move-down": "Baixa element",
        "ooui-outline-control-move-up": "Puja element",
-       "ooui-toolbar-more": "Més"
+       "ooui-toolbar-more": "Més",
+       "ooui-dialog-process-dismiss": "Descarta"
 }
index 97ed48c..546689b 100644 (file)
                        "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-outline-control-remove": "Element entfernen",
        "ooui-toolbar-more": "Mehr",
-       "ooui-dialog-confirm-title": "Bestätigen",
-       "ooui-dialog-confirm-default-prompt": "Bist du sicher?",
-       "ooui-dialog-confirm-default-ok": "Okay",
-       "ooui-dialog-confirm-default-cancel": "Abbrechen"
+       "ooui-dialog-message-accept": "Okay",
+       "ooui-dialog-message-reject": "Abbrechen",
+       "ooui-dialog-process-error": "Etwas ist schief gelaufen",
+       "ooui-dialog-process-dismiss": "Ausblenden",
+       "ooui-dialog-process-retry": "Erneut versuchen"
 }
index 2498a76..602efc8 100644 (file)
             "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-outline-control-remove": "Remove item",
     "ooui-toolbar-more": "More",
-    "ooui-dialog-confirm-title": "Confirm",
-    "ooui-dialog-confirm-default-prompt": "Are you sure?",
-    "ooui-dialog-confirm-default-ok": "OK",
-    "ooui-dialog-confirm-default-cancel": "Cancel"
+    "ooui-dialog-message-accept": "OK",
+    "ooui-dialog-message-reject": "Cancel",
+    "ooui-dialog-process-error": "Something went wrong",
+    "ooui-dialog-process-dismiss": "Dismiss",
+    "ooui-dialog-process-retry": "Try again"
 }
index 76485ea..805897d 100644 (file)
                        "Gloria sah"
                ]
        },
-       "ooui-dialog-action-close": "Cerrar",
        "ooui-outline-control-move-down": "Bajar elemento",
        "ooui-outline-control-move-up": "Subir elemento",
        "ooui-outline-control-remove": "Eliminar elemento",
        "ooui-toolbar-more": "Más",
-       "ooui-dialog-confirm-title": "Confirmar",
-       "ooui-dialog-confirm-default-prompt": "¿Está seguro?",
-       "ooui-dialog-confirm-default-ok": "Aceptar",
-       "ooui-dialog-confirm-default-cancel": "Cancelar"
+       "ooui-dialog-message-accept": "Aceptar",
+       "ooui-dialog-message-reject": "Cancelar",
+       "ooui-dialog-process-retry": "Intentar de nuevo"
 }
index ec051ac..b0ec803 100644 (file)
                        "Armin1392"
                ]
        },
-       "ooui-dialog-action-close": "بستن",
        "ooui-outline-control-move-down": "انتقال مورد به پایین",
        "ooui-outline-control-move-up": "انتقال مورد به بالا",
        "ooui-outline-control-remove": "حذف مورد",
        "ooui-toolbar-more": "بیشتر",
-       "ooui-dialog-confirm-title": "تأیید",
-       "ooui-dialog-confirm-default-prompt": "آیا مطمئن هستید؟",
-       "ooui-dialog-confirm-default-ok": "تأیید",
-       "ooui-dialog-confirm-default-cancel": "لغو"
+       "ooui-dialog-message-accept": "تأیید",
+       "ooui-dialog-message-reject": "لغو",
+       "ooui-dialog-process-error": "مشکلی وجود دارد",
+       "ooui-dialog-process-dismiss": "نپذیرفتن",
+       "ooui-dialog-process-retry": "دوباره امتحان کن"
 }
index 8e8b81e..efaabed 100644 (file)
                        "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-outline-control-remove": "Poista kohde",
        "ooui-toolbar-more": "Lisää",
-       "ooui-dialog-confirm-title": "Vahvista",
-       "ooui-dialog-confirm-default-prompt": "Oletko varma?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Peruuta"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Peruuta",
+       "ooui-dialog-process-error": "Jokin meni pieleen",
+       "ooui-dialog-process-dismiss": "Hylkää",
+       "ooui-dialog-process-retry": "Yritä uudelleen"
 }
index 6b8871a..8ff5475 100644 (file)
                        "Trizek",
                        "Urhixidur",
                        "Verdy p",
-                       "Wyz"
+                       "Wyz",
+                       "SnowedEarth"
                ]
        },
-       "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-outline-control-remove": "Supprimer l’élément",
        "ooui-toolbar-more": "Plus",
-       "ooui-dialog-confirm-title": "Confirmer",
-       "ooui-dialog-confirm-default-prompt": "Êtes-vous sûr ?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Annuler"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Annuler",
+       "ooui-dialog-process-error": "Quelque chose a mal tourné",
+       "ooui-dialog-process-dismiss": "Rejeter",
+       "ooui-dialog-process-retry": "Réessayez"
 }
diff --git a/resources/lib/oojs-ui/i18n/gd.json b/resources/lib/oojs-ui/i18n/gd.json
new file mode 100644 (file)
index 0000000..6a83c9c
--- /dev/null
@@ -0,0 +1,13 @@
+{
+       "@metadata": {
+               "authors": [
+                       "GunChleoc"
+               ]
+       },
+       "ooui-outline-control-move-down": "Gluais nì sìos",
+       "ooui-outline-control-move-up": "Gluais nì suas",
+       "ooui-outline-control-remove": "Thoir air falbh an nì",
+       "ooui-toolbar-more": "Barrachd",
+       "ooui-dialog-message-accept": "Ceart ma-thà",
+       "ooui-dialog-message-reject": "Sguir dheth"
+}
index a4b6787..eac992f 100644 (file)
@@ -6,13 +6,13 @@
                        "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-outline-control-remove": "Eliminar o elemento",
        "ooui-toolbar-more": "Máis",
-       "ooui-dialog-confirm-title": "Confirmar",
-       "ooui-dialog-confirm-default-prompt": "Está seguro?",
-       "ooui-dialog-confirm-default-ok": "Aceptar",
-       "ooui-dialog-confirm-default-cancel": "Cancelar"
+       "ooui-dialog-message-accept": "Aceptar",
+       "ooui-dialog-message-reject": "Cancelar",
+       "ooui-dialog-process-error": "Algo foi mal",
+       "ooui-dialog-process-dismiss": "Agochar",
+       "ooui-dialog-process-retry": "Inténteo de novo"
 }
index 26660f9..bbaf4c1 100644 (file)
                        "קיפודנחש"
                ]
        },
-       "ooui-dialog-action-close": "סגירה",
        "ooui-outline-control-move-down": "להזיז את הפריט מטה",
        "ooui-outline-control-move-up": "להזיז את הפריט מעלה",
        "ooui-outline-control-remove": "להסיר את הפריט",
        "ooui-toolbar-more": "עוד",
-       "ooui-dialog-confirm-title": "אישור",
-       "ooui-dialog-confirm-default-prompt": "באמת?",
-       "ooui-dialog-confirm-default-ok": "אישור",
-       "ooui-dialog-confirm-default-cancel": "ביטול"
+       "ooui-dialog-message-accept": "אישור",
+       "ooui-dialog-message-reject": "ביטול",
+       "ooui-dialog-process-error": "משהו השתבש",
+       "ooui-dialog-process-dismiss": "לוותר",
+       "ooui-dialog-process-retry": "לנסות שוב"
 }
index 0f423b3..6069625 100644 (file)
@@ -5,15 +5,14 @@
                        "Einstein2",
                        "Misibacsi",
                        "ViDam",
-                       "Tacsipacsi"
+                       "Tacsipacsi",
+                       "Csega"
                ]
        },
-       "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-outline-control-remove": "Elem eltávolítása",
        "ooui-toolbar-more": "Tovább...",
-       "ooui-dialog-confirm-title": "Megerősítés",
-       "ooui-dialog-confirm-default-prompt": "Biztos vagy benne?",
-       "ooui-dialog-confirm-default-cancel": "Mégse"
+       "ooui-dialog-message-reject": "Mégse",
+       "ooui-dialog-process-retry": "Próbáld újra"
 }
index f1c9ced..b374b6f 100644 (file)
@@ -4,13 +4,13 @@
                        "McDutchie"
                ]
        },
-       "ooui-dialog-action-close": "Clauder",
        "ooui-outline-control-move-down": "Displaciar elemento in basso",
        "ooui-outline-control-move-up": "Displaciar elemento in alto",
        "ooui-outline-control-remove": "Remover elemento",
        "ooui-toolbar-more": "Plus",
-       "ooui-dialog-confirm-title": "Confirmation",
-       "ooui-dialog-confirm-default-prompt": "Es tu secur?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Cancellar"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Cancellar",
+       "ooui-dialog-process-error": "Qualcosa ha vadite mal",
+       "ooui-dialog-process-dismiss": "Clauder",
+       "ooui-dialog-process-retry": "Reprobar"
 }
index e2e12ab..1cbcb8a 100644 (file)
                        "Викиней"
                ]
        },
-       "ooui-dialog-action-close": "Zoumaachen",
        "ooui-outline-control-move-down": "Element erof réckelen",
        "ooui-outline-control-move-up": "Element erop réckelen",
        "ooui-outline-control-remove": "Element ewechhuelen",
        "ooui-toolbar-more": "Méi",
-       "ooui-dialog-confirm-title": "Confirméieren",
-       "ooui-dialog-confirm-default-prompt": "Sidd Dir sécher?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Ofbriechen"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Ofbriechen",
+       "ooui-dialog-process-error": "Et ass eppes schif gaang",
+       "ooui-dialog-process-dismiss": "Verwerfen",
+       "ooui-dialog-process-retry": "Nach eng Kéier probéieren"
 }
index 90685ea..d628034 100644 (file)
@@ -6,13 +6,13 @@
                        "Iwan Novirion"
                ]
        },
-       "ooui-dialog-action-close": "Затвори",
        "ooui-outline-control-move-down": "Помести надолу",
        "ooui-outline-control-move-up": "Помести нагоре",
        "ooui-outline-control-remove": "Отстрани ставка",
        "ooui-toolbar-more": "Повеќе",
-       "ooui-dialog-confirm-title": "Потврди",
-       "ooui-dialog-confirm-default-prompt": "Дали сте сигурни?",
-       "ooui-dialog-confirm-default-ok": "ОК",
-       "ooui-dialog-confirm-default-cancel": "Откажи"
+       "ooui-dialog-message-accept": "ОК",
+       "ooui-dialog-message-reject": "Откажи",
+       "ooui-dialog-process-error": "Нешто не е во ред",
+       "ooui-dialog-process-dismiss": "Тргни",
+       "ooui-dialog-process-retry": "Обиди се пак"
 }
index bea0c3a..2431096 100644 (file)
                        "Andrzej aa"
                ]
        },
-       "ooui-dialog-action-close": "Zamknij",
        "ooui-outline-control-move-down": "Przenieś niżej",
        "ooui-outline-control-move-up": "Przenieś wyżej",
        "ooui-outline-control-remove": "Usuń element",
        "ooui-toolbar-more": "Więcej",
-       "ooui-dialog-confirm-title": "Potwierdź",
-       "ooui-dialog-confirm-default-prompt": "Jesteś pewien?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Anuluj"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Anuluj",
+       "ooui-dialog-process-error": "Coś poszło nie tak",
+       "ooui-dialog-process-retry": "Spróbuj ponownie"
 }
index e9ad6de..5cb3e3d 100644 (file)
                        "SandroHc"
                ]
        },
-       "ooui-dialog-action-close": "Fechar",
        "ooui-outline-control-move-down": "Mover item para baixo",
        "ooui-outline-control-move-up": "Mover item para cima",
        "ooui-outline-control-remove": "Remover elemento",
        "ooui-toolbar-more": "Mais",
-       "ooui-dialog-confirm-title": "Confirmar",
-       "ooui-dialog-confirm-default-prompt": "Tem a certeza?",
-       "ooui-dialog-confirm-default-ok": "Aceitar",
-       "ooui-dialog-confirm-default-cancel": "Cancelar"
+       "ooui-dialog-message-accept": "Aceitar",
+       "ooui-dialog-message-reject": "Cancelar",
+       "ooui-dialog-process-error": "Algo correu mal",
+       "ooui-dialog-process-dismiss": "Ignorar",
+       "ooui-dialog-process-retry": "Tentar novamente"
 }
index 87198e5..e8ab9f9 100644 (file)
                        "Sayak Sarkar",
                        "Shirayuki",
                        "Siebrand",
-                       "Trevor Parscal"
+                       "Trevor Parscal",
+                       "Liuxinyu970226"
                ]
        },
-       "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-outline-control-remove": "Tool tip for a button that removes items from a list.\n{{Identical|Remove item}}",
        "ooui-toolbar-more": "Label for the toolbar group that contains a list of all other available tools.\n{{Identical|More}}",
-       "ooui-dialog-confirm-title": "Title of the generic dialog used to confirm things.\n{{Identical|Confirm}}",
-       "ooui-dialog-confirm-default-prompt": "The default prompt of a confirmation dialog.\n{{Identical|Are you sure?}}",
-       "ooui-dialog-confirm-default-ok": "The default OK button text on a confirmation dialog.\n{{Identical|OK}}",
-       "ooui-dialog-confirm-default-cancel": "The default cancel button text on a confirmation dialog.\n{{Identical|Cancel}}"
+       "ooui-dialog-message-accept": "Default label for the accept button of a message dialog",
+       "ooui-dialog-message-reject": "Default label for the reject button of a message dialog",
+       "ooui-dialog-process-error": "Title for process dialog error description",
+       "ooui-dialog-process-dismiss": "Label for process dialog dismiss error button, visible when describing errors\n{{Identical|Dismiss}}",
+       "ooui-dialog-process-retry": "Label for process dialog retry action button, visible when describing recoverable errors\n{{Identical|Try again}}"
 }
index 0181514..06e0f1d 100644 (file)
@@ -8,13 +8,13 @@
                        "Gloria sah"
                ]
        },
-       "ooui-dialog-action-close": "Închide",
        "ooui-outline-control-move-down": "Mută elementul mai jos",
        "ooui-outline-control-move-up": "Mută elementul mai sus",
        "ooui-outline-control-remove": "Elimină elementul",
        "ooui-toolbar-more": "Mai mult",
-       "ooui-dialog-confirm-title": "Confirmare",
-       "ooui-dialog-confirm-default-prompt": "Sunteți sigur(ă)?",
-       "ooui-dialog-confirm-default-ok": "OK",
-       "ooui-dialog-confirm-default-cancel": "Revocare"
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Revocare",
+       "ooui-dialog-process-error": "Ceva nu a funcționat",
+       "ooui-dialog-process-dismiss": "Renunțare",
+       "ooui-dialog-process-retry": "Reîncearcă"
 }
index 435f20c..efd1062 100644 (file)
                        "Умар"
                ]
        },
-       "ooui-dialog-action-close": "Закрыть",
        "ooui-outline-control-move-down": "Переместить элемент вниз",
        "ooui-outline-control-move-up": "Переместить элемент вверх",
        "ooui-outline-control-remove": "Удалить пункт",
        "ooui-toolbar-more": "Ещё",
-       "ooui-dialog-confirm-title": "Подтвердить",
-       "ooui-dialog-confirm-default-prompt": "Вы уверены?",
-       "ooui-dialog-confirm-default-ok": "ОК",
-       "ooui-dialog-confirm-default-cancel": "Отмена"
+       "ooui-dialog-message-accept": "ОК",
+       "ooui-dialog-message-reject": "Отмена",
+       "ooui-dialog-process-error": "Что-то пошло не так",
+       "ooui-dialog-process-dismiss": "Закрыть",
+       "ooui-dialog-process-retry": "Попробовать ещё раз"
 }
index 44dfd60..ec18019 100644 (file)
@@ -4,16 +4,16 @@
                        "Euriditi",
                        "Kushtrim",
                        "Elioqoshi",
-                       "GretaDoci"
+                       "GretaDoci",
+                       "Gertakapllani"
                ]
        },
-       "ooui-dialog-action-close": "Mbylle",
        "ooui-outline-control-move-down": "Zhvendose artikullin më poshtë",
        "ooui-outline-control-move-up": "Zhvendose artikullin më lart",
        "ooui-outline-control-remove": "Hiq artikullin",
        "ooui-toolbar-more": "Më tepër...",
-       "ooui-dialog-confirm-title": "Konfirmo",
-       "ooui-dialog-confirm-default-prompt": "A jeni i sigurt?",
-       "ooui-dialog-confirm-default-ok": "Në rregull",
-       "ooui-dialog-confirm-default-cancel": "Anullo"
+       "ooui-dialog-message-accept": "Në rregull",
+       "ooui-dialog-message-reject": "Anullo",
+       "ooui-dialog-process-error": "Diçka shkoi keq",
+       "ooui-dialog-process-retry": "Provo përsëri"
 }
index 2bdac54..11aeed4 100644 (file)
@@ -21,5 +21,9 @@
        "ooui-outline-control-move-down": "Перемістити елемент униз",
        "ooui-outline-control-move-up": "Перемістити елемент вгору",
        "ooui-outline-control-remove": "Видалити елемент",
-       "ooui-toolbar-more": "Більше"
+       "ooui-toolbar-more": "Більше",
+       "ooui-dialog-confirm-title": "Підтвердити",
+       "ooui-dialog-confirm-default-prompt": "Ви впевнені?",
+       "ooui-dialog-confirm-default-ok": "Готово",
+       "ooui-dialog-confirm-default-cancel": "Скасувати"
 }
index 01a22d1..e26af70 100644 (file)
@@ -6,13 +6,12 @@
                        "十弌"
                ]
        },
-       "ooui-dialog-action-close": "שליסן",
        "ooui-outline-control-move-down": "רוקן עלעמענט אראפ",
        "ooui-outline-control-move-up": "רוקן עלעמענט ארויף",
        "ooui-outline-control-remove": "אַראָפנעמען איינס",
        "ooui-toolbar-more": "נאך",
-       "ooui-dialog-confirm-title": "באַשטעטיקן",
-       "ooui-dialog-confirm-default-prompt": "איר זענט זיכער?",
-       "ooui-dialog-confirm-default-ok": "יאָ",
-       "ooui-dialog-confirm-default-cancel": "אַנולירן"
+       "ooui-dialog-message-accept": "יאָ",
+       "ooui-dialog-message-reject": "אַנולירן",
+       "ooui-dialog-process-error": "עפעס איז דורכגעפאלן",
+       "ooui-dialog-process-retry": "פרובירט נאכאמאל"
 }
index 8d1c09f..50df67a 100644 (file)
                        "乌拉跨氪"
                ]
        },
-       "ooui-dialog-action-close": "关闭",
        "ooui-outline-control-move-down": "下移项",
        "ooui-outline-control-move-up": "上移项",
        "ooui-outline-control-remove": "删除项",
        "ooui-toolbar-more": "更多",
-       "ooui-dialog-confirm-title": "确认",
-       "ooui-dialog-confirm-default-prompt": "您确定吗?",
-       "ooui-dialog-confirm-default-ok": "好",
-       "ooui-dialog-confirm-default-cancel": "取消"
+       "ooui-dialog-message-accept": "好",
+       "ooui-dialog-message-reject": "取消",
+       "ooui-dialog-process-error": "发生一些错误",
+       "ooui-dialog-process-dismiss": "解除",
+       "ooui-dialog-process-retry": "重试"
 }
diff --git a/resources/lib/oojs-ui/images/anchor.svg b/resources/lib/oojs-ui/images/anchor.svg
new file mode 100644 (file)
index 0000000..417bc96
--- /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="anchor">
+       <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/lib/oojs-ui/images/tail.svg b/resources/lib/oojs-ui/images/tail.svg
deleted file mode 100644 (file)
index 4df8bb2..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<?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>
index dc999cd..5b35674 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.1.0-pre (85cfc2e735)
+ * OOjs UI v0.1.0-pre (d2451ac748)
  * 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: 2014-07-03T02:33:09Z
+ * Date: 2014-07-14T16:49:51Z
  */
 .oo-ui-dialog-content .oo-ui-window-closeButton {
   position: absolute;
index 7018b52..aeba582 100644 (file)
@@ -1,53 +1,16 @@
 /*!
- * OOjs UI v0.1.0-pre (85cfc2e735)
+ * OOjs UI v0.1.0-pre (d2451ac748)
  * 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: 2014-07-03T02:33:09Z
+ * Date: 2014-07-14T16:49:51Z
  */
-.oo-ui-dialog {
-  background-color: #fff;
-  background-color: rgba(255, 255, 255, 0.5);
-  /* Opening and closing animation */
-
-  opacity: 0;
-}
-
-.oo-ui-dialog > .oo-ui-window-frame {
-  -webkit-transform: scale(0.5);
-     -moz-transform: scale(0.5);
-      -ms-transform: scale(0.5);
-       -o-transform: scale(0.5);
-          transform: scale(0.5);
-}
-
-.oo-ui-dialog.oo-ui-window-setup,
-.oo-ui-dialog.oo-ui-window-setup > .oo-ui-window-frame {
-  -webkit-transition: all 250ms ease-in-out;
-     -moz-transition: all 250ms ease-in-out;
-      -ms-transition: all 250ms ease-in-out;
-       -o-transition: all 250ms ease-in-out;
-          transition: all 250ms ease-in-out;
-}
-
-.oo-ui-dialog.oo-ui-window-ready {
-  opacity: 1;
-}
-
-.oo-ui-dialog.oo-ui-window-ready > .oo-ui-window-frame {
-  -webkit-transform: scale(1);
-     -moz-transform: scale(1);
-      -ms-transform: scale(1);
-       -o-transform: scale(1);
-          transform: scale(1);
-}
-
-.oo-ui-dialog-content .oo-ui-window-head,
-.oo-ui-dialog-content .oo-ui-window-body,
-.oo-ui-dialog-content .oo-ui-window-foot {
+.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;
   right: 0;
   left: 0;
           box-sizing: border-box;
 }
 
-.oo-ui-dialog-content .oo-ui-window-head {
+.oo-ui-dialog-content .oo-ui-window-head {
   top: 0;
-  height: 3.8em;
-  padding: 0.5em;
-}
-
-.oo-ui-dialog-content .oo-ui-window-title {
-  line-height: 2.8em;
-}
-
-.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-closeButton {
-  float: right;
-  margin: 0.25em 0.25em;
-}
-
-.oo-ui-dialog-content .oo-ui-window-body {
-  top: 3.8em;
-  bottom: 4.8em;
-}
-
-.oo-ui-dialog-content-footless .oo-ui-window-body {
-  bottom: 0;
-}
-
-.oo-ui-dialog > .oo-ui-window-frame {
-  top: 1em;
-  bottom: 1em;
-  background-color: #fff;
-  border: solid 1px #ccc;
-  border-radius: 0.5em;
-  box-shadow: 0 0.2em 1em rgba(0, 0, 0, 0.3);
-}
-
-.oo-ui-dialog-small > .oo-ui-window-frame {
-  width: 400px;
-  max-height: 230px;
-}
-
-.oo-ui-dialog-medium > .oo-ui-window-frame {
-  width: 600px;
-  max-height: 460px;
-}
-
-.oo-ui-dialog-large > .oo-ui-window-frame {
-  width: 800px;
-  max-height: 690px;
-}
-
-.oo-ui-dialog-content .oo-ui-window-head,
-.oo-ui-dialog-content .oo-ui-window-foot {
   z-index: 1;
 }
 
-.oo-ui-dialog-content .oo-ui-window-body {
+.oo-ui-dialog-content > .oo-ui-window-body {
+  top: 0;
+  bottom: 0;
   z-index: 2;
   box-shadow: 0 0 0.66em rgba(0, 0, 0, 0.25);
 }
 
-.oo-ui-dialog-content .oo-ui-window-foot {
+.oo-ui-dialog-content .oo-ui-window-foot {
   bottom: 0;
-  height: 4.8em;
-  padding: 1em;
-}
-
-.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-buttonedElement-framed {
-  margin: 0.125em 0.25em;
+  z-index: 1;
 }
 
-.oo-ui-dialog-content .oo-ui-window-overlay {
+.oo-ui-dialog-content .oo-ui-window-overlay {
   z-index: 3;
 }
 
   color: #000;
 }
 
-.oo-ui-window-body {
-  padding: 0 0.75em;
-}
-
-.oo-ui-window-icon {
-  width: 2em;
-  height: 2em;
-  margin-right: 0.5em;
-  line-height: 2em;
+.oo-ui-window > .oo-ui-window-frame {
+  -webkit-box-sizing: border-box;
+     -moz-box-sizing: border-box;
+          box-sizing: border-box;
 }
 
-.oo-ui-window-title {
-  line-height: 2em;
-  color: #333;
+.oo-ui-window-content {
+  background: transparent;
 }
 
 .oo-ui-window-overlay {
   line-height: 1.5em;
 }
 
-.oo-ui-buttonedElement .oo-ui-buttonedElement-button {
+.oo-ui-windowManager-modal > .oo-ui-dialog {
+  background-color: #fff;
+  background-color: rgba(255, 255, 255, 0.5);
+  opacity: 0;
+  -webkit-transition: opacity 250ms ease-in-out;
+     -moz-transition: opacity 250ms ease-in-out;
+      -ms-transition: opacity 250ms ease-in-out;
+       -o-transition: opacity 250ms ease-in-out;
+          transition: opacity 250ms ease-in-out;
+}
+
+.oo-ui-windowManager-modal > .oo-ui-dialog > .oo-ui-window-frame {
+  top: 1em;
+  bottom: 1em;
+  background-color: #fff;
+  border: solid 1px #ccc;
+  border-radius: 0.5em;
+  -webkit-transform: scale(0.5);
+     -moz-transform: scale(0.5);
+      -ms-transform: scale(0.5);
+       -o-transform: scale(0.5);
+          transform: scale(0.5);
+  box-shadow: 0 0.2em 1em rgba(0, 0, 0, 0.3);
+  -webkit-transition: all 250ms ease-in-out;
+     -moz-transition: all 250ms ease-in-out;
+      -ms-transition: all 250ms ease-in-out;
+       -o-transition: all 250ms ease-in-out;
+          transition: all 250ms ease-in-out;
+}
+
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-ready {
+  opacity: 1;
+}
+
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-ready > .oo-ui-window-frame {
+  -webkit-transform: scale(1);
+     -moz-transform: scale(1);
+      -ms-transform: scale(1);
+       -o-transform: scale(1);
+          transform: scale(1);
+}
+
+.oo-ui-windowManager-fullscreen > .oo-ui-dialog > .oo-ui-window-frame {
+  top: 0;
+  bottom: 0;
+  width: 100%;
+  height: 100%;
+  border: none;
+  border-radius: 0;
+  box-shadow: none;
+}
+
+.oo-ui-messageDialog-text.oo-ui-panelLayout {
+  bottom: auto;
+}
+
+.oo-ui-messageDialog-title {
+  display: block;
+  padding-top: 0;
+  font-size: 1.5em;
+  color: #000;
+  text-align: center;
+}
+
+.oo-ui-messageDialog-message {
+  display: block;
+  font-size: 0.9em;
+  line-height: 1.25em;
+  color: #666;
+  text-align: center;
+}
+
+.oo-ui-messageDialog-message-verbose {
+  font-size: 1.1em;
+  line-height: 1.5em;
+  text-align: left;
+}
+
+.oo-ui-messageDialog-content > .oo-ui-window-body {
+  bottom: 3.4em;
+  box-shadow: 0 0 0.66em rgba(0, 0, 0, 0.25);
+}
+
+.oo-ui-messageDialog-content > .oo-ui-window-foot {
+  min-height: 3.4em;
+}
+
+.oo-ui-messageDialog-actions-horizontal {
+  display: table;
+  width: 100%;
+  table-layout: fixed;
+}
+
+.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget {
+  display: table-cell;
+  width: 1%;
+  border-right: solid 1px #e5e5e5;
+}
+
+.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget:last-child {
+  border-right-width: 0;
+}
+
+.oo-ui-messageDialog-actions-vertical {
+  display: block;
+}
+
+.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget {
+  display: block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  border-bottom: solid 1px #e5e5e5;
+}
+
+.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget:last-child {
+  border-bottom-width: 0;
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget {
+  position: relative;
+  height: 3.4em;
+  padding: 0;
+  text-align: center;
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget:hover {
+  background-color: rgba(0, 0, 0, 0.05);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget:active {
+  background-color: rgba(0, 0, 0, 0.1);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-primary:hover {
+  background-color: rgba(8, 126, 204, 0.05);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-primary:active {
+  background-color: rgba(8, 126, 204, 0.1);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-primary .oo-ui-labeledElement-label {
+  font-weight: bold;
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:hover {
+  background-color: rgba(118, 171, 54, 0.05);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:active {
+  background-color: rgba(118, 171, 54, 0.1);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:hover {
+  background-color: rgba(212, 83, 83, 0.05);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:active {
+  background-color: rgba(212, 83, 83, 0.1);
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-buttonedElement-button {
+  display: block;
+}
+
+.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-labeledElement-label {
+  position: relative;
+  top: auto;
+  bottom: auto;
+  display: inline;
+  padding: 0 2em;
+  line-height: 3.4em;
+  white-space: nowrap;
+}
+
+.oo-ui-processDialog-content > .oo-ui-window-head {
+  height: 3.4em;
+}
+
+.oo-ui-processDialog-content > .oo-ui-window-body {
+  top: 3.4em;
+  box-shadow: 0 0 0.66em rgba(0, 0, 0, 0.25);
+}
+
+.oo-ui-processDialog-navigation {
+  position: relative;
+  height: 3.4em;
+  padding: 0 1em;
+  -webkit-user-select: none;
+     -moz-user-select: none;
+      -ms-user-select: none;
+          user-select: none;
+  -webkit-touch-callout: none;
+}
+
+.oo-ui-processDialog-location {
+  height: 1.9em;
+  padding: 0.75em 0;
+  overflow: hidden;
+  text-align: center;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  cursor: default;
+}
+
+.oo-ui-processDialog-location .oo-ui-labelWidget {
+  display: inline;
+}
+
+.oo-ui-processDialog-title {
+  font-weight: bold;
+  line-height: 1.9em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget {
+  white-space: nowrap;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-buttonedElement-button {
+  min-width: 1.9em;
+  min-height: 1.9em;
+  padding-top: 0.75em;
+  padding-bottom: 0.75em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-labeledElement-label,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-labeledElement-label,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-labeledElement-label {
+  padding: 0 1em;
+  line-height: 1.9em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-iconedElement-icon,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-iconedElement-icon,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-iconedElement-icon {
+  position: absolute;
+  margin-top: -0.125em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
+  padding: 0;
+  vertical-align: middle;
+}
+
+.oo-ui-processDialog-actions-safe,
+.oo-ui-processDialog-actions-primary {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+}
+
+.oo-ui-processDialog-actions-safe.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button,
+.oo-ui-processDialog-actions-primary.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
+  margin: 0.75em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget:hover {
+  background-color: rgba(0, 0, 0, 0.05);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget:active {
+  background-color: rgba(0, 0, 0, 0.1);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-primary:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-primary:hover {
+  background-color: rgba(8, 126, 204, 0.05);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-primary:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-primary:active {
+  background-color: rgba(8, 126, 204, 0.1);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-primary .oo-ui-labeledElement-label,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-primary .oo-ui-labeledElement-label {
+  font-weight: bold;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:hover {
+  background-color: rgba(118, 171, 54, 0.05);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-constructive:active {
+  background-color: rgba(118, 171, 54, 0.1);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:hover {
+  background-color: rgba(212, 83, 83, 0.05);
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggableElement-destructive:active {
+  background-color: rgba(212, 83, 83, 0.1);
+}
+
+.oo-ui-processDialog-actions-safe {
+  left: 0;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-iconedElement-icon {
+  left: 0.5em;
+}
+
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-labeledElement-label {
+  padding-left: 2.25em;
+}
+
+.oo-ui-processDialog-actions-primary {
+  right: 0;
+}
+
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-iconedElement-icon {
+  right: 0.5em;
+}
+
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-iconedElement .oo-ui-labeledElement-label {
+  padding-right: 2.25em;
+}
+
+.oo-ui-processDialog-actions-other:not(:empty) {
+  padding: 0.75em;
+}
+
+.oo-ui-processDialog-actions-other:not(:empty) .oo-ui-actionWidget {
+  margin: 0 0.75em 0 0;
+}
+
+.oo-ui-processDialog > .oo-ui-window-frame {
+  min-height: 5em;
+}
+
+.oo-ui-processDialog-errors {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 2;
+  display: none;
+  padding: 3em 3em 1.5em 3em;
+  overflow-x: hidden;
+  overflow-y: auto;
+  text-align: center;
+  background-color: rgba(255, 255, 255, 0.9);
+}
+
+.oo-ui-processDialog-errors .oo-ui-buttonWidget {
+  margin: 2em 1em 2em 1em;
+}
+
+.oo-ui-processDialog-errors-title {
+  margin-bottom: 2em;
+  font-size: 1.5em;
+  color: #000;
+}
+
+.oo-ui-processDialog-error {
+  padding: 1em;
+  margin: 1em;
+  text-align: left;
+  background-color: #fff7f7;
+  border: solid 1px #ff9e9e;
+  border-radius: 0.25em;
+}
+
+.oo-ui-buttonedElement > .oo-ui-buttonedElement-button {
   color: #333;
 }
 
-.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 {
+.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 {
   width: 1.9em;
   height: 1.9em;
   opacity: 0.8;
 }
 
-.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
   /* Don't animate opacities for now, causes wiggling in Chrome (bug 63020) */
 
   /*.oo-ui-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 {
+.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-buttonedElement-button:hover > .oo-ui-labeledElement-label,
-.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button:focus > .oo-ui-labeledElement-label {
+.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-buttonedElement-button > .oo-ui-labeledElement-label {
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
   color: #333;
 }
 
-.oo-ui-buttonedElement-frameless.oo-ui-widget-disabled .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+.oo-ui-buttonedElement-frameless.oo-ui-flaggableElement-primary > .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+  color: #087ecc;
+}
+
+.oo-ui-buttonedElement-frameless.oo-ui-flaggableElement-constructive > .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+  color: #76ab36;
+}
+
+.oo-ui-buttonedElement-frameless.oo-ui-flaggableElement-destructive > .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+  color: #d45353;
+}
+
+.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-widget-disabled .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+.oo-ui-buttonedElement-frameless.oo-ui-widget-disabled .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
   color: #ccc;
 }
 
-.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
   padding: 0.2em 0.8em;
   margin: 0.1em 0;
   text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
           transition: border-color 100ms ease-in-out;
 }
 
-.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button:hover,
-.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button:focus {
+.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.oo-ui-buttonedElement-active,
-.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
   color: black;
   background: #eeeeee;
   background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0%, #dddddd), color-stop(100%, #ffffff));
   box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.07);
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-iconedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+.oo-ui-buttonedElement-framed.oo-ui-iconedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
   margin-right: -0.5em;
   margin-left: -0.5em;
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-iconedElement.oo-ui-labeledElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+.oo-ui-buttonedElement-framed.oo-ui-iconedElement.oo-ui-labeledElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
   margin-right: 0.3em;
   margin-left: -0.5em;
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button {
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button {
   background: #cde7f4;
   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%);
   filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#eaf4fa', endColorstr='#b0d9ee');
 }
 
-.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 {
+.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.oo-ui-buttonedElement-active,
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-primary .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
   background: #cde7f4;
   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%);
   filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#b0d9ee', endColorstr='#eaf4fa');
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button {
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button {
   background: #daf0be;
   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%);
   filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#f0fbe1', endColorstr='#c3e59a');
 }
 
-.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 {
+.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.oo-ui-buttonedElement-active,
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-constructive .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
   background: #daf0be;
   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%);
   filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#c3e59a', endColorstr='#f0fbe1');
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-destructive .oo-ui-buttonedElement-button {
+.oo-ui-buttonedElement-framed.oo-ui-flaggableElement-destructive .oo-ui-buttonedElement-button {
   color: #d45353;
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
   color: #333;
   background: #eee;
   border-color: #ccc;
   box-shadow: none;
 }
 
-.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.oo-ui-buttonedElement-active:hover,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed:hover,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button:focus,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active:focus,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed:focus {
+.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.oo-ui-buttonedElement-active:hover,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed:hover,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button:focus,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active:focus,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed:focus {
   border-color: #ccc;
   box-shadow: none;
 }
 }
 
 .oo-ui-panelLayout-padded {
-  padding: 2em;
+  padding: 1.25em;
 }
 
 .oo-ui-barToolGroup .oo-ui-tool {
   box-shadow: 0 0.15em 0.5em 0 rgba(0, 0, 0, 0.2);
 }
 
-.oo-ui-popupWidget-tailed .oo-ui-popupWidget-tail {
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor {
   width: 15px;
   height: 8px;
   margin-left: -7px;
-  background-image: /* @embed */ url(images/tail.svg);
+  background-image: /* @embed */ url(images/anchor.svg);
 }
 
 .oo-ui-popupWidget-transitioning .oo-ui-popupWidget-popup {
index f2e3202..68d4be6 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.1.0-pre (85cfc2e735)
+ * OOjs UI v0.1.0-pre (d2451ac748)
  * 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: 2014-07-03T02:33:09Z
+ * Date: 2014-07-14T16:49:51Z
  */
 ( function ( OO ) {
 
@@ -94,7 +94,6 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) {
 };
 
 ( function () {
-
        /**
         * Message store for the default implementation of OO.ui.msg
         *
@@ -104,8 +103,6 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) {
         * @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
@@ -114,15 +111,16 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) {
                'ooui-outline-control-remove': 'Remove item',
                // Label for the toolbar group that contains a list of all other available tools
                'ooui-toolbar-more': 'More',
-
-               // Label for the generic dialog used to confirm things
-               'ooui-dialog-confirm-title': 'Confirm',
-               // The default prompt of a confirmation dialog
-               'ooui-dialog-confirm-default-prompt': 'Are you sure?',
-               // The default OK button text on a confirmation dialog
-               'ooui-dialog-confirm-default-ok': 'OK',
-               // The default cancel button text on a confirmation dialog
-               'ooui-dialog-confirm-default-cancel': 'Cancel'
+               // Default label for the accept button of a confirmation dialog
+               'ooui-dialog-message-accept': 'OK',
+               // Default label for the reject button of a confirmation dialog
+               'ooui-dialog-message-reject': 'Cancel',
+               // Title for process dialog error description
+               'ooui-dialog-process-error': 'Something went wrong',
+               // Label for process dialog dismiss error button, visible when describing errors
+               'ooui-dialog-process-dismiss': 'Dismiss',
+               // Label for process dialog retry action button, visible when describing recoverable errors
+               'ooui-dialog-process-retry': 'Try again'
        };
 
        /**
@@ -157,14 +155,30 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) {
                return message;
        };
 
-       /** */
-       OO.ui.deferMsg = function ( key ) {
+       /**
+        * Package a message and arguments for deferred resolution.
+        *
+        * Use this when you are statically specifying a message and the message may not yet be present.
+        *
+        * @param {string} key Message key
+        * @param {Mixed...} [params] Message parameters
+        * @return {Function} Function that returns the resolved message when executed
+        */
+       OO.ui.deferMsg = function () {
+               var args = arguments;
                return function () {
-                       return OO.ui.msg( key );
+                       return OO.ui.msg.apply( OO.ui, args );
                };
        };
 
-       /** */
+       /**
+        * Resolve a message.
+        *
+        * If the message is a function it will be executed, otherwise it will pass through directly.
+        *
+        * @param {Function|string} msg Deferred message, or message text
+        * @return {string} Resolved message
+        */
        OO.ui.resolveMsg = function ( msg ) {
                if ( $.isFunction( msg ) ) {
                        return msg();
@@ -174,6 +188,414 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) {
 
 } )();
 
+/**
+ * List of actions.
+ *
+ * @abstract
+ * @class
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.ActionSet = function OoUiActionSet( config ) {
+       // Configuration intialization
+       config = config || {};
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+
+       // Properties
+       this.list = [];
+       this.categories = {
+               'actions': 'getAction',
+               'flags': 'getFlags',
+               'modes': 'getModes'
+       };
+       this.categorized = {};
+       this.special = {};
+       this.others = [];
+       this.organized = false;
+       this.changing = false;
+       this.changed = false;
+};
+
+/* Setup */
+
+OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
+
+/* Static Properties */
+
+/**
+ * Symbolic name of dialog.
+ *
+ * @abstract
+ * @static
+ * @inheritable
+ * @property {string}
+ */
+OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
+
+/* Events */
+
+/**
+ * @event click
+ * @param {OO.ui.ActionWidget} action Action that was clicked
+ */
+
+/**
+ * @event resize
+ * @param {OO.ui.ActionWidget} action Action that was resized
+ */
+
+/**
+ * @event add
+ * @param {OO.ui.ActionWidget[]} added Actions added
+ */
+
+/**
+ * @event remove
+ * @param {OO.ui.ActionWidget[]} added Actions removed
+ */
+
+/**
+ * @event change
+ */
+
+/* Methods */
+
+/**
+ * Handle action change events.
+ *
+ * @fires change
+ */
+OO.ui.ActionSet.prototype.onActionChange = function () {
+       this.organized = false;
+       if ( this.changing ) {
+               this.changed = true;
+       } else {
+               this.emit( 'change' );
+       }
+};
+
+/**
+ * Check if a action is one of the special actions.
+ *
+ * @param {OO.ui.ActionWidget} action Action to check
+ * @return {boolean} Action is special
+ */
+OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
+       var flag;
+
+       for ( flag in this.special ) {
+               if ( action === this.special[flag] ) {
+                       return true;
+               }
+       }
+
+       return false;
+};
+
+/**
+ * Get actions.
+ *
+ * @param {Object} [filters] Filters to use, omit to get all actions
+ * @param {string|string[]} [filters.actions] Actions that actions must have
+ * @param {string|string[]} [filters.flags] Flags that actions must have
+ * @param {string|string[]} [filters.modes] Modes that actions must have
+ * @param {boolean} [filters.visible] Actions must be visible
+ * @param {boolean} [filters.disabled] Actions must be disabled
+ * @return {OO.ui.ActionWidget[]} Actions matching all criteria
+ */
+OO.ui.ActionSet.prototype.get = function ( filters ) {
+       var i, len, list, category, actions, index, match, matches;
+
+       if ( filters ) {
+               this.organize();
+
+               // Collect category candidates
+               matches = [];
+               for ( category in this.categorized ) {
+                       list = filters[category];
+                       if ( list ) {
+                               if ( !Array.isArray( list ) ) {
+                                       list = [ list ];
+                               }
+                               for ( i = 0, len = list.length; i < len; i++ ) {
+                                       actions = this.categorized[category][list[i]];
+                                       if ( Array.isArray( actions ) ) {
+                                               matches.push.apply( matches, actions );
+                                       }
+                               }
+                       }
+               }
+               // Remove by boolean filters
+               for ( i = 0, len = matches.length; i < len; i++ ) {
+                       match = matches[i];
+                       if (
+                               ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
+                               ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
+                       ) {
+                               matches.splice( i, 1 );
+                               len--;
+                               i--;
+                       }
+               }
+               // Remove duplicates
+               for ( i = 0, len = matches.length; i < len; i++ ) {
+                       match = matches[i];
+                       index = matches.lastIndexOf( match );
+                       while ( index !== i ) {
+                               matches.splice( index, 1 );
+                               len--;
+                               index = matches.lastIndexOf( match );
+                       }
+               }
+               return matches;
+       }
+       return this.list.slice();
+};
+
+/**
+ * Get special actions.
+ *
+ * Special actions are the first visible actions with special flags, such as 'safe' and 'primary'.
+ * Special flags can be configured by changing #static-specialFlags in a subclass.
+ *
+ * @return {OO.ui.ActionWidget|null} Safe action
+ */
+OO.ui.ActionSet.prototype.getSpecial = function () {
+       this.organize();
+       return $.extend( {}, this.special );
+};
+
+/**
+ * Get other actions.
+ *
+ * Other actions include all non-special visible actions.
+ *
+ * @return {OO.ui.ActionWidget[]} Other actions
+ */
+OO.ui.ActionSet.prototype.getOthers = function () {
+       this.organize();
+       return this.others.slice();
+};
+
+/**
+ * Toggle actions based on their modes.
+ *
+ * Unlike calling toggle on actions with matching flags, this will enforce mutually exclusive
+ * visibility; matching actions will be shown, non-matching actions will be hidden.
+ *
+ * @param {string} mode Mode actions must have
+ * @chainable
+ * @fires toggle
+ * @fires change
+ */
+OO.ui.ActionSet.prototype.setMode = function ( mode ) {
+       var i, len, action;
+
+       this.changing = true;
+       for ( i = 0, len = this.list.length; i < len; i++ ) {
+               action = this.list[i];
+               action.toggle( action.hasMode( mode ) );
+       }
+
+       this.organized = false;
+       this.changing = false;
+       this.emit( 'change' );
+
+       return this;
+};
+
+/**
+ * Change which actions are able to be performed.
+ *
+ * Actions with matching actions will be disabled/enabled. Other actions will not be changed.
+ *
+ * @param {Object.<string,boolean>} actions List of abilities, keyed by action name, values
+ *   indicate actions are able to be performed
+ * @chainable
+ */
+OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
+       var i, len, action, item;
+
+       for ( i = 0, len = this.list.length; i < len; i++ ) {
+               item = this.list[i];
+               action = item.getAction();
+               if ( actions[action] !== undefined ) {
+                       item.setDisabled( !actions[action] );
+               }
+       }
+
+       return this;
+};
+
+/**
+ * Executes a function once per action.
+ *
+ * When making changes to multiple actions, use this method instead of iterating over the actions
+ * manually to defer emitting a change event until after all actions have been changed.
+ *
+ * @param {Object|null} actions Filters to use for which actions to iterate over; see #get
+ * @param {Function} callback Callback to run for each action; callback is invoked with three
+ *   arguments: the action, the action's index, the list of actions being iterated over
+ * @chainable
+ */
+OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
+       this.changed = false;
+       this.changing = true;
+       this.get( filter ).forEach( callback );
+       this.changing = false;
+       if ( this.changed ) {
+               this.emit( 'change' );
+       }
+
+       return this;
+};
+
+/**
+ * Add actions.
+ *
+ * @param {OO.ui.ActionWidget[]} actions Actions to add
+ * @chainable
+ * @fires add
+ * @fires change
+ */
+OO.ui.ActionSet.prototype.add = function ( actions ) {
+       var i, len, action;
+
+       this.changing = true;
+       for ( i = 0, len = actions.length; i < len; i++ ) {
+               action = actions[i];
+               action.connect( this, {
+                       'click': [ 'emit', 'click', action ],
+                       'resize': [ 'emit', 'resize', action ],
+                       'toggle': [ 'onActionChange' ]
+               } );
+               this.list.push( action );
+       }
+       this.organized = false;
+       this.emit( 'add', actions );
+       this.changing = false;
+       this.emit( 'change' );
+
+       return this;
+};
+
+/**
+ * Remove actions.
+ *
+ * @param {OO.ui.ActionWidget[]} actions Actions to remove
+ * @chainable
+ * @fires remove
+ * @fires change
+ */
+OO.ui.ActionSet.prototype.remove = function ( actions ) {
+       var i, len, index, action;
+
+       this.changing = true;
+       for ( i = 0, len = actions.length; i < len; i++ ) {
+               action = actions[i];
+               index = this.list.indexOf( action );
+               if ( index !== -1 ) {
+                       action.disconnect( this );
+                       this.list.splice( index, 1 );
+               }
+       }
+       this.organized = false;
+       this.emit( 'remove', actions );
+       this.changing = false;
+       this.emit( 'change' );
+
+       return this;
+};
+
+/**
+ * Remove all actions.
+ *
+ * @chainable
+ * @fires remove
+ * @fires change
+ */
+OO.ui.ActionSet.prototype.clear = function () {
+       var i, len, action,
+               removed = this.list.slice();
+
+       this.changing = true;
+       for ( i = 0, len = this.list.length; i < len; i++ ) {
+               action = this.list[i];
+               action.disconnect( this );
+       }
+
+       this.list = [];
+
+       this.organized = false;
+       this.emit( 'remove', removed );
+       this.changing = false;
+       this.emit( 'change' );
+
+       return this;
+};
+
+/**
+ * Organize actions.
+ *
+ * This is called whenver organized information is requested. It will only reorganize the actions
+ * if something has changed since the last time it ran.
+ *
+ * @private
+ * @chainable
+ */
+OO.ui.ActionSet.prototype.organize = function () {
+       var i, iLen, j, jLen, flag, action, category, list, item, special,
+               specialFlags = this.constructor.static.specialFlags;
+
+       if ( !this.organized ) {
+               this.categorized = {};
+               this.special = {};
+               this.others = [];
+               for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
+                       action = this.list[i];
+                       if ( action.isVisible() ) {
+                               // Populate catgeories
+                               for ( category in this.categories ) {
+                                       if ( !this.categorized[category] ) {
+                                               this.categorized[category] = {};
+                                       }
+                                       list = action[this.categories[category]]();
+                                       if ( !Array.isArray( list ) ) {
+                                               list = [ list ];
+                                       }
+                                       for ( j = 0, jLen = list.length; j < jLen; j++ ) {
+                                               item = list[j];
+                                               if ( !this.categorized[category][item] ) {
+                                                       this.categorized[category][item] = [];
+                                               }
+                                               this.categorized[category][item].push( action );
+                                       }
+                               }
+                               // Populate special/others
+                               special = false;
+                               for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
+                                       flag = specialFlags[j];
+                                       if ( !this.special[flag] && action.hasFlag( flag ) ) {
+                                               this.special[flag] = action;
+                                               special = true;
+                                               break;
+                                       }
+                               }
+                               if ( !special ) {
+                                       this.others.push( action );
+                               }
+                       }
+               }
+               this.organized = true;
+       }
+
+       return this;
+};
+
 /**
  * DOM element abstraction.
  *
@@ -851,7 +1273,8 @@ OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, timeout )
  * @fires load
  */
 OO.ui.Frame.prototype.load = function () {
-       var win, doc;
+       var win, doc,
+               frame = this;
 
        // Return existing promise if already loading or loaded
        if ( this.loading ) {
@@ -872,8 +1295,7 @@ OO.ui.Frame.prototype.load = function () {
        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 class="oo-ui-frame-content oo-ui-' + this.dir + '" style="direction:' + this.dir + ';" dir="' + this.dir + '">' +
                        '</body>' +
                '</html>'
        );
@@ -886,10 +1308,10 @@ OO.ui.Frame.prototype.load = function () {
 
        // Initialization
        this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] )
-               .always( OO.ui.bind( function () {
-                       this.emit( 'load' );
-                       this.loading.resolve();
-               }, this ) );
+               .always( function () {
+                       frame.emit( 'load' );
+                       frame.loading.resolve();
+               } );
 
        return this.loading.promise();
 };
@@ -907,10 +1329,7 @@ OO.ui.Frame.prototype.setSize = function ( width, height ) {
 };
 
 /**
- * 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.
+ * Container for elements.
  *
  * @abstract
  * @class
@@ -919,305 +1338,369 @@ OO.ui.Frame.prototype.setSize = function ( width, height ) {
  *
  * @constructor
  * @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( config ) {
-       var element = this;
+OO.ui.Layout = function OoUiLayout( config ) {
+       // Initialize config
+       config = config || {};
+
        // Parent constructor
-       OO.ui.Window.super.call( this, config );
+       OO.ui.Layout.super.call( this, config );
 
        // Mixin constructors
        OO.EventEmitter.call( this );
 
-       // Properties
-       this.visible = false;
-       this.opening = null;
-       this.closing = null;
-       this.opened = null;
-       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.on( 'load', function () {
-               element.initialize();
-               // Undo the visibility: hidden; hack and apply display: none;
-               // We can do this safely now that the iframe has initialized
-               // (don't do this from within #initialize because it has to happen
-               // after the all subclasses have been handled as well).
-               element.$element.hide().css( 'visibility', '' );
-       } );
+       this.$element.addClass( 'oo-ui-layout' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.Window, OO.ui.Element );
-OO.mixinClass( OO.ui.Window, OO.EventEmitter );
-
-/* Events */
+OO.inheritClass( OO.ui.Layout, OO.ui.Element );
+OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
 
 /**
- * Window is setup.
+ * User interface control.
  *
- * Fired after the setup process has been executed.
+ * @abstract
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
  *
- * @event setup
- * @param {Object} data Window opening data
+ * @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.Widget.super.call( this, config );
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+
+       // Properties
+       this.visible = true;
+       this.disabled = null;
+       this.wasDisabled = null;
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-widget' );
+       this.setDisabled( !!config.disabled );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.Widget, OO.ui.Element );
+OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
+
+/* Events */
 
 /**
- * Window is ready.
- *
- * Fired after the ready process has been executed.
- *
- * @event ready
- * @param {Object} data Window opening data
+ * @event disable
+ * @param {boolean} disabled Widget is disabled
  */
 
 /**
- * Window is torn down
- *
- * Fired after the teardown process has been executed.
- *
- * @event teardown
- * @param {Object} data Window closing data
+ * @event toggle
+ * @param {boolean} visible Widget is visible
  */
 
-/* Static Properties */
+/* Methods */
 
 /**
- * Symbolic name of icon.
+ * Check if the widget is disabled.
  *
- * @static
- * @inheritable
- * @property {string}
+ * @param {boolean} Button is disabled
  */
-OO.ui.Window.static.icon = 'window';
+OO.ui.Widget.prototype.isDisabled = function () {
+       return this.disabled;
+};
 
 /**
- * Window title.
+ * Check if widget is visible.
  *
- * Subclasses must implement this property before instantiating the window.
- * Alternatively, override #getTitle with an alternative implementation.
+ * @return {boolean} Widget is visible
+ */
+OO.ui.Widget.prototype.isVisible = function () {
+       return this.visible;
+};
+
+/**
+ * Set the disabled state of the widget.
  *
- * @static
- * @abstract
- * @inheritable
- * @property {string|Function} Title string or function that returns a string
+ * This should probably change the widgets' appearance and prevent it from being used.
+ *
+ * @param {boolean} disabled Disable widget
+ * @chainable
  */
-OO.ui.Window.static.title = null;
+OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
+       var isDisabled;
 
-/* Methods */
+       this.disabled = !!disabled;
+       isDisabled = this.isDisabled();
+       if ( isDisabled !== this.wasDisabled ) {
+               this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
+               this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
+               this.emit( 'disable', isDisabled );
+       }
+       this.wasDisabled = isDisabled;
+
+       return this;
+};
 
 /**
- * Check if window is visible.
+ * Toggle visibility of widget.
  *
- * @return {boolean} Window is visible
+ * @param {boolean} [show] Make widget visible, omit to toggle visibility
+ * @fires visible
+ * @chainable
  */
-OO.ui.Window.prototype.isVisible = function () {
-       return this.visible;
+OO.ui.Widget.prototype.toggle = function ( show ) {
+       show = show === undefined ? !this.visible : !!show;
+
+       if ( show !== this.isVisible() ) {
+               this.visible = show;
+               this.$element.toggle( show );
+               this.emit( 'toggle', show );
+       }
+
+       return this;
 };
 
 /**
- * Check if window is opening.
+ * Update the disabled state, in case of changes in parent widget.
  *
- * @return {boolean} Window is opening
+ * @chainable
  */
-OO.ui.Window.prototype.isOpening = function () {
-       return !!this.opening && this.opening.state() === 'pending';
+OO.ui.Widget.prototype.updateDisabled = function () {
+       this.setDisabled( this.disabled );
+       return this;
 };
 
 /**
- * Check if window is closing.
+ * Container for elements in a child frame.
  *
- * @return {boolean} Window is closing
+ * Use together with OO.ui.WindowManager.
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
+ *
+ * When a window is opened, the setup and ready processes are executed. Similarly, the hold and
+ * teardown processes are executed when the window is closed.
+ *
+ * - {@link OO.ui.WindowManager#openWindow} or {@link #open} methods are used to start opening
+ * - Window manager begins opening window
+ * - {@link #getSetupProcess} method is called and its result executed
+ * - {@link #getReadyProcess} method is called and its result executed
+ * - Window is now open
+ *
+ * - {@link OO.ui.WindowManager#closeWindow} or {@link #close} methods are used to start closing
+ * - Window manager begins closing window
+ * - {@link #getHoldProcess} method is called and its result executed
+ * - {@link #getTeardownProcess} method is called and its result executed
+ * - Window is now closed
+ *
+ * Each process (setup, ready, hold and teardown) can be extended in subclasses by overriding
+ * {@link #getSetupProcess}, {@link #getReadyProcess}, {@link #getHoldProcess} and
+ * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchonous
+ * processing can complete. Always assume window processes are executed asychronously. See
+ * OO.ui.Process for more details about how to work with processes. Some events, as well as the
+ * #open and #close methods, provide promises which are resolved when the window enters a new state.
+ *
+ * Sizing of windows is specified using symbolic names which are interpreted by the window manager.
+ * If the requested size is not recognized, the window manager will choose a sensible fallback.
+ *
+ * @constructor
+ * @param {OO.ui.WindowManager} manager Manager of window
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large` or `full`; omit to
+ *   use #static-size
+ * @fires initialize
  */
-OO.ui.Window.prototype.isClosing = function () {
-       return !!this.closing && this.closing.state() === 'pending';
+OO.ui.Window = function OoUiWindow( manager, config ) {
+       var win = this;
+
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.Window.super.call( this, config );
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+
+       if ( !( manager instanceof OO.ui.WindowManager ) ) {
+               throw new Error( 'Cannot construct window: window must have a manager' );
+       }
+
+       // Properties
+       this.manager = manager;
+       this.initialized = false;
+       this.visible = false;
+       this.opening = null;
+       this.closing = null;
+       this.opened = null;
+       this.timing = null;
+       this.size = config.size || this.constructor.static.size;
+       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.on( 'load', function () {
+               win.initialize();
+               win.initialized = true;
+               // Undo the visibility: hidden; hack and apply display: none;
+               // We can do this safely now that the iframe has initialized
+               // (don't do this from within #initialize because it has to happen
+               // after the all subclasses have been handled as well).
+               win.$element.hide().css( 'visibility', '' );
+       } );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.Window, OO.ui.Element );
+OO.mixinClass( OO.ui.Window, OO.EventEmitter );
+
+/* Events */
+
 /**
- * Check if window is opened.
+ * @event resize
+ * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
+ */
+
+/* Static Properties */
+
+/**
+ * Symbolic name of size.
  *
- * @return {boolean} Window is opened
+ * Size is used if no size is configured during construction.
+ *
+ * @static
+ * @inheritable
+ * @property {string}
  */
-OO.ui.Window.prototype.isOpened = function () {
-       return !!this.opened && this.opened.state() === 'pending';
-};
+OO.ui.Window.static.size = 'medium';
+
+/* Methods */
 
 /**
- * Get the window frame.
+ * Check if window has been initialized.
  *
- * @return {OO.ui.Frame} Frame of window
+ * @return {boolean} Window has been initialized
  */
-OO.ui.Window.prototype.getFrame = function () {
-       return this.frame;
+OO.ui.Window.prototype.isInitialized = function () {
+       return this.initialized;
 };
 
 /**
- * Get the title of the window.
+ * Check if window is visible.
  *
- * @return {string} Title text
+ * @return {boolean} Window is visible
  */
-OO.ui.Window.prototype.getTitle = function () {
-       return this.title;
+OO.ui.Window.prototype.isVisible = function () {
+       return this.visible;
 };
 
 /**
- * Get the window icon.
+ * Check if window is opening.
+ *
+ * This is a wrapper around OO.ui.WindowManager#isOpening.
  *
- * @return {string} Symbolic name of icon
+ * @return {boolean} Window is opening
  */
-OO.ui.Window.prototype.getIcon = function () {
-       return this.icon;
+OO.ui.Window.prototype.isOpening = function () {
+       return this.manager.isOpening( this );
 };
 
 /**
- * Set the size of window frame.
+ * Check if window is closing.
  *
- * @param {number} [width=auto] Custom width
- * @param {number} [height=auto] Custom height
- * @chainable
+ * This is a wrapper around OO.ui.WindowManager#isClosing.
+ *
+ * @return {boolean} Window is closing
  */
-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;
+OO.ui.Window.prototype.isClosing = function () {
+       return this.manager.isClosing( this );
 };
 
 /**
- * Set the title of the window.
+ * Check if window is opened.
  *
- * @param {string|Function} title Title text or a function that returns text
- * @chainable
+ * This is a wrapper around OO.ui.WindowManager#isOpened.
+ *
+ * @return {boolean} Window is opened
  */
-OO.ui.Window.prototype.setTitle = function ( title ) {
-       this.title = OO.ui.resolveMsg( title );
-       if ( this.$title ) {
-               this.$title.text( title );
-       }
-       return this;
+OO.ui.Window.prototype.isOpened = function () {
+       return this.manager.isOpened( this );
 };
 
 /**
- * Set the icon of the window.
+ * Get the window manager.
  *
- * @param {string} icon Symbolic name of icon
- * @chainable
+ * @return {OO.ui.WindowManager} Manager of window
  */
-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;
+OO.ui.Window.prototype.getManager = function () {
+       return this.manager;
 };
 
 /**
- * Set the position of window to fit with contents.
+ * Get the window frame.
  *
- * @param {string} left Left offset
- * @param {string} top Top offset
- * @chainable
+ * @return {OO.ui.Frame} Frame of window
  */
-OO.ui.Window.prototype.setPosition = function ( left, top ) {
-       this.$element.css( { 'left': left, 'top': top } );
-       return this;
+OO.ui.Window.prototype.getFrame = function () {
+       return this.frame;
 };
 
 /**
- * Set the height of window to fit with contents.
+ * Get the window size.
  *
- * @param {number} [min=0] Min height
- * @param {number} [max] Max height (defaults to content's outer height)
- * @chainable
+ * @return {string} Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
  */
-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;
+OO.ui.Window.prototype.getSize = function () {
+       return this.size;
 };
 
 /**
- * Set the width of window to fit with contents.
+ * Get the height of the dialog contents.
  *
- * @param {number} [min=0] Min height
- * @param {number} [max] Max height (defaults to content's outer width)
- * @chainable
+ * @return {number} Content height
  */
-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 ) )
+OO.ui.Window.prototype.getContentHeight = function () {
+       return Math.round(
+               // Add buffer for border
+               ( ( this.$frame.outerHeight() - this.$frame.innerHeight() ) * 2 ) +
+               // Height of contents
+               ( this.$head.outerHeight( true ) + this.getBodyHeight() + this.$foot.outerHeight( true ) )
        );
-
-       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.
+ * Get the height of the dialog contents.
  *
- * @chainable
+ * @return {number} Height of content
  */
-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
-       );
-
-       return this;
+OO.ui.Window.prototype.getBodyHeight = function () {
+       return this.$body[0].scrollHeight;
 };
 
 /**
@@ -1255,322 +1738,298 @@ OO.ui.Window.prototype.getReadyProcess = function () {
 };
 
 /**
- * Get a process for tearing down a window after use.
+ * Get a process for holding a window from use.
  *
- * Each time the window is closed this process will tear it down and do something with the user's
- * interactions within the window, based on the `data` argument.
+ * Each time the window is closed, this process will hold it from use in a particular context, based
+ * on the `data` argument.
  *
- * When you override this method, you can add additional teardown steps to the process the parent
+ * When you override this method, you can add additional setup steps to the process the parent
  * method provides using the 'first' and 'next' methods.
  *
  * @abstract
  * @param {Object} [data] Window closing data
- * @return {OO.ui.Process} Teardown process
+ * @return {OO.ui.Process} Hold process
  */
-OO.ui.Window.prototype.getTeardownProcess = function () {
+OO.ui.Window.prototype.getHoldProcess = function () {
        return new OO.ui.Process();
 };
 
 /**
- * Open window.
+ * Get a process for tearing down a window after use.
  *
- * Do not override this method. Use #getSetupProcess to do something each time the window closes.
+ * Each time the window is closed this process will tear it down and do something with the user's
+ * interactions within the window, based on the `data` argument.
  *
- * @param {Object} [data] Window opening data
- * @fires initialize
- * @fires opening
- * @fires open
- * @fires ready
- * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
- *   first argument will be a promise which will be resolved when the window begins closing
+ * When you override this method, you can add additional teardown steps to the process the parent
+ * method provides using the 'first' and 'next' methods.
+ *
+ * @abstract
+ * @param {Object} [data] Window closing data
+ * @return {OO.ui.Process} Teardown process
  */
-OO.ui.Window.prototype.open = function ( data ) {
-       // Return existing promise if already opening or open
-       if ( this.opening ) {
-               return this.opening.promise();
-       }
-
-       // Open the window
-       this.opening = $.Deferred();
-
-       this.$ariaHidden = $( 'body' ).children().not( this.$element.parentsUntil( 'body' ).last() )
-               .attr( 'aria-hidden', '' );
-
-       this.frame.load().done( OO.ui.bind( function () {
-               this.$element.show();
-               this.visible = true;
-               this.getSetupProcess( data ).execute().done( OO.ui.bind( function () {
-                       this.$element.addClass( 'oo-ui-window-setup' );
-                       this.emit( 'setup', data );
-                       setTimeout( OO.ui.bind( function () {
-                               this.frame.$content.focus();
-                               this.getReadyProcess( data ).execute().done( OO.ui.bind( function () {
-                                       this.$element.addClass( 'oo-ui-window-ready' );
-                                       this.emit( 'ready', data );
-                                       this.opened = $.Deferred();
-                                       // Now that we are totally done opening, it's safe to allow closing
-                                       this.closing = null;
-                                       this.opening.resolve( this.opened.promise() );
-                               }, this ) );
-                       }, this ) );
-               }, this ) );
-       }, this ) );
-
-       return this.opening.promise();
+OO.ui.Window.prototype.getTeardownProcess = function () {
+       return new OO.ui.Process();
 };
 
 /**
- * Close window.
- *
- * Do not override this method. Use #getTeardownProcess to do something each time the window closes.
+ * Set the window size.
  *
- * @param {Object} [data] Window closing data
- * @fires closing
- * @fires close
- * @return {jQuery.Promise} Promise resolved when window is closed
+ * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
+ * @chainable
  */
-OO.ui.Window.prototype.close = function ( data ) {
-       var close;
-
-       // Return existing promise if already closing or closed
-       if ( this.closing ) {
-               return this.closing.promise();
-       }
-
-       // Close after opening is done if opening is in progress
-       if ( this.opening && this.opening.state() === 'pending' ) {
-               close = OO.ui.bind( function () {
-                       return this.close( data );
-               }, this );
-               return this.opening.then( close, close );
-       }
-
-       // Close the window
-       // This.closing needs to exist before we emit the closing event so that handlers can call
-       // window.close() and trigger the safety check above
-       this.closing = $.Deferred();
-       this.frame.$content.find( ':focus' ).blur();
-       this.$element.removeClass( 'oo-ui-window-ready' );
-       this.getTeardownProcess( data ).execute().done( OO.ui.bind( function () {
-               this.$element.removeClass( 'oo-ui-window-setup' );
-               this.emit( 'teardown', data );
-               // To do something different with #opened, resolve/reject #opened in the teardown process
-               if ( this.opened && this.opened.state() === 'pending' ) {
-                       this.opened.resolve();
-               }
-               this.$element.hide();
-               if ( this.$ariaHidden ) {
-                       this.$ariaHidden.removeAttr( 'aria-hidden' );
-                       this.$ariaHidden = undefined;
-               }
-               this.visible = false;
-               this.closing.resolve();
-               // Now that we are totally done closing, it's safe to allow opening
-               this.opening = null;
-       }, this ) );
-
-       return this.closing.promise();
+OO.ui.Window.prototype.setSize = function ( size ) {
+       this.size = size;
+       this.manager.updateWindowSize( this );
+       return this;
 };
 
 /**
- * Set of mutually exclusive windows.
+ * Set window dimensions.
  *
- * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
+ * Properties are applied to the frame container.
  *
- * @constructor
- * @param {OO.Factory} factory Window factory
- * @param {Object} [config] Configuration options
+ * @param {Object} dim CSS dimension properties
+ * @param {string|number} [dim.width] Width
+ * @param {string|number} [dim.minWidth] Minimum width
+ * @param {string|number} [dim.maxWidth] Maximum width
+ * @param {string|number} [dim.width] Height, omit to set based on height of contents
+ * @param {string|number} [dim.minWidth] Minimum height
+ * @param {string|number} [dim.maxWidth] Maximum height
+ * @chainable
  */
-OO.ui.WindowSet = function OoUiWindowSet( factory, config ) {
-       // Parent constructor
-       OO.ui.WindowSet.super.call( this, config );
-
-       // Mixin constructors
-       OO.EventEmitter.call( this );
-
-       // Properties
-       this.factory = factory;
-
-       /**
-        * List of all windows associated with this window set.
-        *
-        * @property {OO.ui.Window[]}
-        */
-       this.windowList = [];
-
-       /**
-        * Mapping of OO.ui.Window objects created by name from the #factory.
-        *
-        * @property {Object}
-        */
-       this.windows = {};
-       this.currentWindow = null;
-
-       // Initialization
-       this.$element.addClass( 'oo-ui-windowSet' );
+OO.ui.Window.prototype.setDimensions = function ( dim ) {
+       // Apply width before height so height is not based on wrapping content using the wrong width
+       this.$frame.css( {
+               'width': dim.width || '',
+               'min-width': dim.minWidth || '',
+               'max-width': dim.maxWidth || ''
+       } );
+       this.$frame.css( {
+               'height': ( dim.height !== undefined ? dim.height : this.getContentHeight() ) || '',
+               'min-height': dim.minHeight || '',
+               'max-height': dim.maxHeight || ''
+       } );
+       return this;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.WindowSet, OO.ui.Element );
-OO.mixinClass( OO.ui.WindowSet, OO.EventEmitter );
-
-/* Events */
-
-/**
- * @event setup
- * @param {OO.ui.Window} win Window that's been setup
- * @param {Object} config Window opening information
- */
-
 /**
- * @event ready
- * @param {OO.ui.Window} win Window that's ready
- * @param {Object} config Window opening information
+ * Initialize window contents.
+ *
+ * The first time the window is opened, #initialize is called when it's safe to begin populating
+ * its contents. See #getSetupProcess 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.
+ *
+ * @chainable
  */
+OO.ui.Window.prototype.initialize = function () {
+       // Properties
+       this.$ = this.frame.$;
+       this.$head = this.$( '<div>' );
+       this.$body = this.$( '<div>' );
+       this.$foot = this.$( '<div>' );
+       this.$overlay = this.$( '<div>' );
 
-/**
- * @event teardown
- * @param {OO.ui.Window} win Window that's been torn down
- * @param {Object} config Window closing information
- */
+       // Initialization
+       this.$head.addClass( 'oo-ui-window-head' );
+       this.$body.addClass( 'oo-ui-window-body' );
+       this.$foot.addClass( 'oo-ui-window-foot' );
+       this.$overlay.addClass( 'oo-ui-window-overlay' );
+       this.frame.$content
+               .addClass( 'oo-ui-window-content' )
+               .append( this.$head, this.$body, this.$foot, this.$overlay );
 
-/* Methods */
+       return this;
+};
 
 /**
- * Handle a window setup event.
+ * Open window.
  *
- * @param {OO.ui.Window} win Window that's been setup
- * @param {Object} [config] Window opening information
- * @fires setup
+ * This is a wrapper around calling {@link OO.ui.WindowManager#openWindow} on the window manager.
+ * To do something each time the window opens, use #getSetupProcess or #getReadyProcess.
+ *
+ * @param {Object} [data] Window opening data
+ * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
+ *   first argument will be a promise which will be resolved when the window begins closing
  */
-OO.ui.WindowSet.prototype.onWindowSetup = function ( win, config ) {
-       if ( this.currentWindow && this.currentWindow !== win ) {
-               this.currentWindow.close();
-       }
-       this.currentWindow = win;
-       this.emit( 'setup', win, config );
+OO.ui.Window.prototype.open = function ( data ) {
+       return this.manager.openWindow( this, data );
 };
 
 /**
- * Handle a window ready event.
+ * Close window.
+ *
+ * This is a wrapper around calling OO.ui.WindowManager#closeWindow on the window manager.
+ * To do something each time the window closes, use #getHoldProcess or #getTeardownProcess.
  *
- * @param {OO.ui.Window} win Window that's ready
- * @param {Object} [config] Window opening information
- * @fires ready
+ * @param {Object} [data] Window closing data
+ * @return {jQuery.Promise} Promise resolved when window is closed
  */
-OO.ui.WindowSet.prototype.onWindowReady = function ( win, config ) {
-       this.emit( 'ready', win, config );
+OO.ui.Window.prototype.close = function ( data ) {
+       return this.manager.closeWindow( this, data );
 };
 
 /**
- * Handle a window teardown event.
+ * Load window.
+ *
+ * This is called by OO.ui.WindowManager durring window adding, and should not be called directly
+ * by other systems.
  *
- * @param {OO.ui.Window} win Window that's been torn down
- * @param {Object} [config] Window closing information
- * @fires teardown
+ * @return {jQuery.Promise} Promise resolved when window is loaded
  */
-OO.ui.WindowSet.prototype.onWindowTeardown = function ( win, config ) {
-       this.currentWindow = null;
-       this.emit( 'teardown', win, config );
+OO.ui.Window.prototype.load = function () {
+       return this.frame.load();
 };
 
 /**
- * Get the current window.
+ * Setup window.
+ *
+ * This is called by OO.ui.WindowManager durring window opening, and should not be called directly
+ * by other systems.
  *
- * @return {OO.ui.Window|null} Current window or null if none open
+ * @param {Object} [data] Window opening data
+ * @return {jQuery.Promise} Promise resolved when window is setup
  */
-OO.ui.WindowSet.prototype.getCurrentWindow = function () {
-       return this.currentWindow;
+OO.ui.Window.prototype.setup = function ( data ) {
+       var win = this,
+               deferred = $.Deferred();
+
+       this.$element.show();
+       this.visible = true;
+       this.getSetupProcess( data ).execute().done( function () {
+               win.manager.updateWindowSize( win );
+               // Force redraw by asking the browser to measure the elements' widths
+               win.$element.addClass( 'oo-ui-window-setup' ).width();
+               win.frame.$content.addClass( 'oo-ui-window-content-setup' ).width();
+               deferred.resolve();
+       } );
+
+       return deferred.promise();
 };
 
 /**
- * Return a given window.
+ * Ready window.
  *
- * @param {string} name Symbolic name of window
- * @return {OO.ui.Window} Window with specified name
+ * This is called by OO.ui.WindowManager durring window opening, and should not be called directly
+ * by other systems.
+ *
+ * @param {Object} [data] Window opening data
+ * @return {jQuery.Promise} Promise resolved when window is ready
  */
-OO.ui.WindowSet.prototype.getWindow = function ( name ) {
-       var win;
+OO.ui.Window.prototype.ready = function ( data ) {
+       var win = this,
+               deferred = $.Deferred();
 
-       if ( !this.factory.lookup( name ) ) {
-               throw new Error( 'Unknown window: ' + name );
-       }
-       if ( !( name in this.windows ) ) {
-               win = this.windows[name] = this.createWindow( name );
-               this.addWindow( win );
-       }
-       return this.windows[name];
+       this.frame.$content.focus();
+       this.getReadyProcess( data ).execute().done( function () {
+               // Force redraw by asking the browser to measure the elements' widths
+               win.$element.addClass( 'oo-ui-window-ready' ).width();
+               win.frame.$content.addClass( 'oo-ui-window-content-ready' ).width();
+               deferred.resolve();
+       } );
+
+       return deferred.promise();
 };
 
 /**
- * Create a window for use in this window set.
+ * Hold window.
+ *
+ * This is called by OO.ui.WindowManager durring window closing, and should not be called directly
+ * by other systems.
  *
- * @param {string} name Symbolic name of window
- * @return {OO.ui.Window} Window with specified name
+ * @param {Object} [data] Window closing data
+ * @return {jQuery.Promise} Promise resolved when window is held
  */
-OO.ui.WindowSet.prototype.createWindow = function ( name ) {
-       return this.factory.create( name, { '$': this.$ } );
+OO.ui.Window.prototype.hold = function ( data ) {
+       var win = this,
+               deferred = $.Deferred();
+
+       this.getHoldProcess( data ).execute().done( function () {
+               win.frame.$content.find( ':focus' ).blur();
+               // Force redraw by asking the browser to measure the elements' widths
+               win.$element.removeClass( 'oo-ui-window-ready' ).width();
+               win.frame.$content.removeClass( 'oo-ui-window-content-ready' ).width();
+               deferred.resolve();
+       } );
+
+       return deferred.promise();
 };
 
 /**
- * Add a given window to this window set.
+ * Teardown window.
  *
- * Connects event handlers and attaches it to the DOM. Calling
- * OO.ui.Window#open will not work until the window is added to the set.
+ * This is called by OO.ui.WindowManager durring window closing, and should not be called directly
+ * by other systems.
  *
- * @param {OO.ui.Window} win Window to add
+ * @param {Object} [data] Window closing data
+ * @return {jQuery.Promise} Promise resolved when window is torn down
  */
-OO.ui.WindowSet.prototype.addWindow = function ( win ) {
-       if ( this.windowList.indexOf( win ) !== -1 ) {
-               // Already set up
-               return;
-       }
-       this.windowList.push( win );
+OO.ui.Window.prototype.teardown = function ( data ) {
+       var win = this,
+               deferred = $.Deferred();
 
-       win.connect( this, {
-               'setup': [ 'onWindowSetup', win ],
-               'ready': [ 'onWindowReady', win ],
-               'teardown': [ 'onWindowTeardown', win ]
+       this.getTeardownProcess( data ).execute().done( function () {
+               // Force redraw by asking the browser to measure the elements' widths
+               win.$element.removeClass( 'oo-ui-window-setup' ).width();
+               win.frame.$content.removeClass( 'oo-ui-window-content-setup' ).width();
+               win.$element.hide();
+               win.visible = false;
+               deferred.resolve();
        } );
-       this.$element.append( win.$element );
+
+       return deferred.promise();
 };
 
 /**
- * Modal dialog window.
+ * Base class for all dialogs.
+ *
+ * Logic:
+ * - Manage the window (open and close, etc.).
+ * - Store the internal name and display title.
+ * - A stack to track one or more pending actions.
+ * - Manage a set of actions that can be performed.
+ * - Configure and create action widgets.
+ *
+ * User interface:
+ * - Close the dialog with Escape key.
+ * - Visually lock the dialog while an action is in
+ *   progress (aka "pending").
+ *
+ * Subclass responsibilities:
+ * - Display the title somewhere.
+ * - Add content to the dialog.
+ * - Provide a UI to close the dialog.
+ * - Display the action widgets somewhere.
  *
  * @abstract
  * @class
  * @extends OO.ui.Window
+ * @mixins OO.ui.LabeledElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {boolean} [footless] Hide foot
- * @cfg {string} [size='large'] Symbolic name of dialog size, `small`, `medium` or `large`
  */
-OO.ui.Dialog = function OoUiDialog( config ) {
-       // Configuration initialization
-       config = $.extend( { 'size': 'large' }, config );
-
+OO.ui.Dialog = function OoUiDialog( manager, config ) {
        // Parent constructor
-       OO.ui.Dialog.super.call( this, config );
+       OO.ui.Dialog.super.call( this, manager, config );
 
        // Properties
-       this.visible = false;
-       this.footless = !!config.footless;
-       this.size = null;
+       this.actions = new OO.ui.ActionSet();
+       this.attachedActions = [];
+       this.currentAction = null;
        this.pending = 0;
-       this.onWindowMouseWheelHandler = OO.ui.bind( this.onWindowMouseWheel, this );
-       this.onDocumentKeyDownHandler = OO.ui.bind( this.onDocumentKeyDown, this );
 
        // Events
-       this.$element.on( 'mousedown', false );
+       this.actions.connect( this, {
+               'click': 'onActionClick',
+               'resize': 'onActionResize',
+               'change': 'onActionsChange'
+       } );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-dialog' ).attr( 'role', 'dialog' );
-       this.setSize( config.size );
+       this.$element
+               .addClass( 'oo-ui-dialog' )
+               .attr( 'role', 'dialog' );
 };
 
 /* Setup */
@@ -1590,55 +2049,35 @@ OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
 OO.ui.Dialog.static.name = '';
 
 /**
- * Map of symbolic size names and CSS classes.
+ * Dialog title.
  *
+ * @abstract
  * @static
  * @inheritable
- * @property {Object}
- */
-OO.ui.Dialog.static.sizeCssClasses = {
-       'small': 'oo-ui-dialog-small',
-       'medium': 'oo-ui-dialog-medium',
-       'large': 'oo-ui-dialog-large'
-};
-
-/* Methods */
-
-/**
- * Handle close button click events.
+ * @property {jQuery|string|Function} Label nodes, text or a function that returns nodes or text
  */
-OO.ui.Dialog.prototype.onCloseButtonClick = function () {
-       this.close( { 'action': 'cancel' } );
-};
+OO.ui.Dialog.static.title = '';
 
 /**
- * Handle window mouse wheel events.
+ * List of OO.ui.ActionWidget configuration options.
  *
- * @param {jQuery.Event} e Mouse wheel event
+ * @static
+ * inheritable
+ * @property {Object[]}
  */
-OO.ui.Dialog.prototype.onWindowMouseWheel = function () {
-       return false;
-};
+OO.ui.Dialog.static.actions = [];
 
 /**
- * Handle document key down events.
+ * Close dialog when the escape key is pressed.
  *
- * @param {jQuery.Event} e Key down event
+ * @static
+ * @abstract
+ * @inheritable
+ * @property {boolean}
  */
-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;
-       }
-};
+OO.ui.Dialog.static.escapable = true;
+
+/* Methods */
 
 /**
  * Handle frame document key down events.
@@ -1647,68 +2086,109 @@ OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
  */
 OO.ui.Dialog.prototype.onFrameDocumentKeyDown = function ( e ) {
        if ( e.which === OO.ui.Keys.ESCAPE ) {
-               this.close( { 'action': 'cancel' } );
+               this.close();
                return false;
        }
 };
 
 /**
- * Set dialog size.
+ * Handle action resized events.
  *
- * @param {string} [size='large'] Symbolic name of dialog size, `small`, `medium` or `large`
+ * @param {OO.ui.ActionWidget} action Action that was resized
  */
-OO.ui.Dialog.prototype.setSize = function ( size ) {
-       var name, state, cssClass,
-               sizeCssClasses = OO.ui.Dialog.static.sizeCssClasses;
+OO.ui.Dialog.prototype.onActionResize = function () {
+       // Override in subclass
+};
 
-       if ( !sizeCssClasses[size] ) {
-               size = 'large';
-       }
-       this.size = size;
-       for ( name in sizeCssClasses ) {
-               state = name === size;
-               cssClass = sizeCssClasses[name];
-               this.$element.toggleClass( cssClass, state );
+/**
+ * Handle action click events.
+ *
+ * @param {OO.ui.ActionWidget} action Action that was clicked
+ */
+OO.ui.Dialog.prototype.onActionClick = function ( action ) {
+       if ( !this.isPending() ) {
+               this.currentAction = action;
+               this.executeAction( action.getAction() );
        }
 };
 
 /**
- * @inheritdoc
+ * Handle actions change event.
  */
-OO.ui.Dialog.prototype.initialize = function () {
-       // Parent method
-       OO.ui.Dialog.super.prototype.initialize.call( this );
+OO.ui.Dialog.prototype.onActionsChange = function () {
+       this.detachActions();
+       if ( !this.isClosing() ) {
+               this.attachActions();
+       }
+};
 
-       // Properties
-       this.closeButton = new OO.ui.ButtonWidget( {
-               '$': this.$,
-               'frameless': true,
-               'icon': 'close',
-               'title': OO.ui.msg( 'ooui-dialog-action-close' )
-       } );
+/**
+ * Check if input is pending.
+ *
+ * @return {boolean}
+ */
+OO.ui.Dialog.prototype.isPending = function () {
+       return !!this.pending;
+};
 
-       // Events
-       this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } );
-       this.frame.$document.on( 'keydown', OO.ui.bind( this.onFrameDocumentKeyDown, this ) );
+/**
+ * Get set of actions.
+ *
+ * @return {OO.ui.ActionSet}
+ */
+OO.ui.Dialog.prototype.getActions = function () {
+       return this.actions;
+};
 
-       // Initialization
-       this.frame.$content.addClass( 'oo-ui-dialog-content' );
-       if ( this.footless ) {
-               this.frame.$content.addClass( 'oo-ui-dialog-content-footless' );
-       }
-       this.closeButton.$element.addClass( 'oo-ui-window-closeButton' );
-       this.$head.append( this.closeButton.$element );
+/**
+ * Get a process for taking action.
+ *
+ * When you override this method, you can add additional accept steps to the process the parent
+ * method provides using the 'first' and 'next' methods.
+ *
+ * @abstract
+ * @param {string} [action] Symbolic name of action
+ * @return {OO.ui.Process} Action process
+ */
+OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
+       return new OO.ui.Process()
+               .next( function () {
+                       if ( !action ) {
+                               // An empty action always closes the dialog without data, which should always be
+                               // safe and make no changes
+                               this.close();
+                       }
+               }, this );
 };
 
 /**
  * @inheritdoc
+ *
+ * @param {Object} [data] Dialog opening data
+ * @param {jQuery|string|Function|null} [data.label] Dialog label, omit to use #static-label
+ * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each
+ *   action item, omit to use #static-actions
  */
 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
+       data = data || {};
+
+       // Parent method
        return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
                .next( function () {
-                       // Prevent scrolling in top-level window
-                       this.$( window ).on( 'mousewheel', this.onWindowMouseWheelHandler );
-                       this.$( document ).on( 'keydown', this.onDocumentKeyDownHandler );
+                       var i, len,
+                               items = [],
+                               config = this.constructor.static,
+                               actions = data.actions !== undefined ? data.actions : config.actions;
+
+                       this.title.setLabel(
+                               data.title !== undefined ? data.title : this.constructor.static.title
+                       );
+                       for ( i = 0, len = actions.length; i < len; i++ ) {
+                               items.push(
+                                       new OO.ui.ActionWidget( $.extend( { '$': this.$ }, actions[i] ) )
+                               );
+                       }
+                       this.actions.add( items );
                }, this );
 };
 
@@ -1716,37 +2196,77 @@ OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
  * @inheritdoc
  */
 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
+       // Parent method
        return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
                .first( function () {
-                       // Wait for closing transition
-                       return OO.ui.Process.static.delay( 250 );
-               }, this )
-               .next( function () {
-                       // Allow scrolling in top-level window
-                       this.$( window ).off( 'mousewheel', this.onWindowMouseWheelHandler );
-                       this.$( document ).off( 'keydown', this.onDocumentKeyDownHandler );
+                       this.actions.clear();
+                       this.currentAction = null;
                }, this );
 };
 
 /**
- * Check if input is pending.
- *
- * @return {boolean}
+ * @inheritdoc
  */
-OO.ui.Dialog.prototype.isPending = function () {
-       return !!this.pending;
+OO.ui.Dialog.prototype.initialize = function () {
+       // Parent method
+       OO.ui.Dialog.super.prototype.initialize.call( this );
+
+       // Properties
+       this.title = new OO.ui.LabelWidget( { '$': this.$ } );
+
+       // Events
+       if ( this.constructor.static.escapable ) {
+               this.frame.$document.on( 'keydown', OO.ui.bind( this.onFrameDocumentKeyDown, this ) );
+       }
+
+       // Initialization
+       this.frame.$content.addClass( 'oo-ui-dialog-content' );
 };
 
 /**
- * Increase the pending stack.
+ * Attach action actions.
+ */
+OO.ui.Dialog.prototype.attachActions = function () {
+       // Remember the list of potentially attached actions
+       this.attachedActions = this.actions.get();
+};
+
+/**
+ * Detach action actions.
+ *
+ * @chainable
+ */
+OO.ui.Dialog.prototype.detachActions = function () {
+       var i, len;
+
+       // Detach all actions that may have been previously attached
+       for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
+               this.attachedActions[i].$element.detach();
+       }
+       this.attachedActions = [];
+};
+
+/**
+ * Execute an action.
+ *
+ * @param {string} action Symbolic name of action to execute
+ * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
+ */
+OO.ui.Dialog.prototype.executeAction = function ( action ) {
+       this.pushPending();
+       return this.getActionProcess( action ).execute()
+               .always( OO.ui.bind( this.popPending, this ) );
+};
+
+/**
+ * Increase the pending stack.
  *
  * @chainable
  */
 OO.ui.Dialog.prototype.pushPending = function () {
        if ( this.pending === 0 ) {
-               this.frame.$content.addClass( 'oo-ui-dialog-pending' );
+               this.frame.$content.addClass( 'oo-ui-actionDialog-content-pending' );
                this.$head.addClass( 'oo-ui-texture-pending' );
-               this.$foot.addClass( 'oo-ui-texture-pending' );
        }
        this.pending++;
 
@@ -1762,9 +2282,8 @@ OO.ui.Dialog.prototype.pushPending = function () {
  */
 OO.ui.Dialog.prototype.popPending = function () {
        if ( this.pending === 1 ) {
-               this.frame.$content.removeClass( 'oo-ui-dialog-pending' );
+               this.frame.$content.removeClass( 'oo-ui-actionDialog-content-pending' );
                this.$head.removeClass( 'oo-ui-texture-pending' );
-               this.$foot.removeClass( 'oo-ui-texture-pending' );
        }
        this.pending = Math.max( 0, this.pending - 1 );
 
@@ -1772,2032 +2291,2617 @@ OO.ui.Dialog.prototype.popPending = function () {
 };
 
 /**
- * Container for elements.
+ * Collection of windows.
  *
- * @abstract
  * @class
  * @extends OO.ui.Element
  * @mixins OO.EventEmitter
  *
+ * Managed windows are mutually exclusive. If a window is opened while there is a current window
+ * already opening or opened, the current window will be closed without data. Empty closing data
+ * should always result in the window being closed without causing constructive or destructive
+ * action.
+ *
+ * As a window is opened and closed, it passes through several stages and the manager emits several
+ * corresponding events.
+ *
+ * - {@link #openWindow} or {@link OO.ui.Window#open} methods are used to start opening
+ * - {@link #event-opening} is emitted with `opening` promise
+ * - {@link #getSetupDelay} is called the returned value is used to time a pause in execution
+ * - {@link OO.ui.Window#getSetupProcess} method is called on the window and its result executed
+ * - `setup` progress notification is emitted from opening promise
+ * - {@link #getReadyDelay} is called the returned value is used to time a pause in execution
+ * - {@link OO.ui.Window#getReadyProcess} method is called on the window and its result executed
+ * - `ready` progress notification is emitted from opening promise
+ * - `opening` promise is resolved with `opened` promise
+ * - Window is now open
+ *
+ * - {@link #closeWindow} or {@link OO.ui.Window#close} methods are used to start closing
+ * - `opened` promise is resolved with `closing` promise
+ * - {@link #event-opening} is emitted with `closing` promise
+ * - {@link #getHoldDelay} is called the returned value is used to time a pause in execution
+ * - {@link OO.ui.Window#getHoldProcess} method is called on the window and its result executed
+ * - `hold` progress notification is emitted from opening promise
+ * - {@link #getTeardownDelay} is called the returned value is used to time a pause in execution
+ * - {@link OO.ui.Window#getTeardownProcess} method is called on the window and its result executed
+ * - `teardown` progress notification is emitted from opening promise
+ * - Closing promise is resolved
+ * - Window is now closed
+ *
  * @constructor
  * @param {Object} [config] Configuration options
+ * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
+ * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
  */
-OO.ui.Layout = function OoUiLayout( config ) {
-       // Initialize config
+OO.ui.WindowManager = function OoUiWindowManager( config ) {
+       // Configuration initialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.Layout.super.call( this, config );
+       OO.ui.WindowManager.super.call( this, config );
 
        // Mixin constructors
        OO.EventEmitter.call( this );
 
+       // Properties
+       this.factory = config.factory;
+       this.modal = config.modal === undefined ? true : !!config.modal;
+       this.windows = {};
+       this.opening = null;
+       this.opened = null;
+       this.closing = null;
+       this.size = null;
+       this.currentWindow = null;
+       this.$ariaHidden = null;
+       this.requestedSize = null;
+       this.onWindowResizeTimeout = null;
+       this.onWindowResizeHandler = OO.ui.bind( this.onWindowResize, this );
+       this.afterWindowResizeHandler = OO.ui.bind( this.afterWindowResize, this );
+       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-layout' );
+       this.$element
+               .addClass( 'oo-ui-windowManager' )
+               .toggleClass( 'oo-ui-windowManager-modal', this.modal );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.Layout, OO.ui.Element );
-OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
+OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
+OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
+
+/* Events */
 
 /**
- * User interface control.
+ * Window is opening.
  *
- * @abstract
- * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
+ * Fired when the window begins to be opened.
  *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [disabled=false] Disable
+ * @event opening
+ * @param {OO.ui.Window} win Window that's being opened
+ * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is
+ *   resolved the first argument will be a promise which will be resolved when the window begins
+ *   closing, the second argument will be the opening data; progress notifications will be fired on
+ *   the promise for `setup` and `ready` when those processes are completed respectively.
+ * @param {Object} data Window opening data
  */
-OO.ui.Widget = function OoUiWidget( config ) {
-       // Initialize config
-       config = $.extend( { 'disabled': false }, config );
-
-       // Parent constructor
-       OO.ui.Widget.super.call( this, config );
 
-       // Mixin constructors
-       OO.EventEmitter.call( this );
+/**
+ * Window is closing.
+ *
+ * Fired when the window begins to be closed.
+ *
+ * @event closing
+ * @param {OO.ui.Window} win Window that's being closed
+ * @param {jQuery.Promise} opening Promise resolved when window is closed; when the promise
+ *   is resolved the first argument will be a the closing data; progress notifications will be fired
+ *   on the promise for `hold` and `teardown` when those processes are completed respectively.
+ * @param {Object} data Window closing data
+ */
 
-       // Properties
-       this.disabled = null;
-       this.wasDisabled = null;
+/* Static Properties */
 
-       // Initialization
-       this.$element.addClass( 'oo-ui-widget' );
-       this.setDisabled( !!config.disabled );
+/**
+ * Map of symbolic size names and CSS properties.
+ *
+ * @static
+ * @inheritable
+ * @property {Object}
+ */
+OO.ui.WindowManager.static.sizes = {
+       'small': {
+               'width': 300
+       },
+       'medium': {
+               'width': 500
+       },
+       'large': {
+               'width': 700
+       },
+       'full': {
+               // These can be non-numeric because they are never used in calculations
+               'width': '100%',
+               'height': '100%'
+       }
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.Widget, OO.ui.Element );
-OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
-
-/* Events */
-
 /**
- * @event disable
- * @param {boolean} disabled Widget is disabled
+ * Symbolic name of default size.
+ *
+ * Default size is used if the window's requested size is not recognized.
+ *
+ * @static
+ * @inheritable
+ * @property {string}
  */
+OO.ui.WindowManager.static.defaultSize = 'medium';
 
 /* Methods */
 
 /**
- * Check if the widget is disabled.
+ * Handle window resize events.
  *
- * @param {boolean} Button is disabled
+ * @param {jQuery.Event} e Window resize event
  */
-OO.ui.Widget.prototype.isDisabled = function () {
-       return this.disabled;
+OO.ui.WindowManager.prototype.onWindowResize = function () {
+       clearTimeout( this.onWindowResizeTimeout );
+       this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
 };
 
 /**
- * Update the disabled state, in case of changes in parent widget.
+ * Handle window resize events.
  *
- * @chainable
+ * @param {jQuery.Event} e Window resize event
  */
-OO.ui.Widget.prototype.updateDisabled = function () {
-       this.setDisabled( this.disabled );
-       return this;
+OO.ui.WindowManager.prototype.afterWindowResize = function () {
+       if ( this.currentWindow ) {
+               this.updateWindowSize( this.currentWindow );
+       }
 };
 
 /**
- * Set the disabled state of the widget.
- *
- * This should probably change the widgets' appearance and prevent it from being used.
+ * Handle window mouse wheel events.
  *
- * @param {boolean} disabled Disable widget
- * @chainable
+ * @param {jQuery.Event} e Mouse wheel event
  */
-OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
-       var isDisabled;
+OO.ui.WindowManager.prototype.onWindowMouseWheel = function () {
+       return false;
+};
 
-       this.disabled = !!disabled;
-       isDisabled = this.isDisabled();
-       if ( isDisabled !== this.wasDisabled ) {
-               this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
-               this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
-               this.emit( 'disable', isDisabled );
+/**
+ * Handle document key down events.
+ *
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.WindowManager.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;
        }
-       this.wasDisabled = isDisabled;
-       return this;
 };
 
 /**
- * A list of functions, called in sequence.
+ * Check if window is opening.
  *
- * If a function added to a process returns boolean false the process will stop; if it returns an
- * object with a `promise` method the process will use the promise to either continue to the next
- * step when the promise is resolved or stop when the promise is rejected.
+ * @return {boolean} Window is opening
+ */
+OO.ui.WindowManager.prototype.isOpening = function ( win ) {
+       return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
+};
+
+/**
+ * Check if window is closing.
  *
- * @class
+ * @return {boolean} Window is closing
+ */
+OO.ui.WindowManager.prototype.isClosing = function ( win ) {
+       return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
+};
+
+/**
+ * Check if window is opened.
  *
- * @constructor
+ * @return {boolean} Window is opened
  */
-OO.ui.Process = function () {
-       // Properties
-       this.steps = [];
+OO.ui.WindowManager.prototype.isOpened = function ( win ) {
+       return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
 };
 
-/* Setup */
+/**
+ * Check if a window is being managed.
+ *
+ * @param {OO.ui.Window} win Window to check
+ * @return {boolean} Window is being managed
+ */
+OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
+       var name;
 
-OO.initClass( OO.ui.Process );
+       for ( name in this.windows ) {
+               if ( this.windows[name] === win ) {
+                       return true;
+               }
+       }
 
-/* Static Methods */
+       return false;
+};
 
 /**
- * Generate a promise which is resolved after a set amount of time.
+ * Get the number of milliseconds to wait between beginning opening and executing setup process.
  *
- * @param {number} length Number of milliseconds before resolving the promise
- * @return {jQuery.Promise} Promise that will be resolved after a set amount of time
+ * @param {OO.ui.Window} win Window being opened
+ * @param {Object} [data] Window opening data
+ * @return {number} Milliseconds to wait
  */
-OO.ui.Process.static.delay = function ( length ) {
-       var deferred = $.Deferred();
-
-       setTimeout( function () {
-               deferred.resolve();
-       }, length );
+OO.ui.WindowManager.prototype.getSetupDelay = function () {
+       return 0;
+};
 
-       return deferred.promise();
+/**
+ * Get the number of milliseconds to wait between finishing setup and executing ready process.
+ *
+ * @param {OO.ui.Window} win Window being opened
+ * @param {Object} [data] Window opening data
+ * @return {number} Milliseconds to wait
+ */
+OO.ui.WindowManager.prototype.getReadyDelay = function () {
+       return 0;
 };
 
-/* Methods */
+/**
+ * Get the number of milliseconds to wait between beginning closing and executing hold process.
+ *
+ * @param {OO.ui.Window} win Window being closed
+ * @param {Object} [data] Window closing data
+ * @return {number} Milliseconds to wait
+ */
+OO.ui.WindowManager.prototype.getHoldDelay = function () {
+       return 0;
+};
 
 /**
- * Start the process.
+ * Get the number of milliseconds to wait between finishing hold and executing teardown process.
  *
- * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
- *   any of the steps return boolean false or a promise which gets rejected; upon stopping the
- *   process, the remaining steps will not be taken
+ * @param {OO.ui.Window} win Window being closed
+ * @param {Object} [data] Window closing data
+ * @return {number} Milliseconds to wait
  */
-OO.ui.Process.prototype.execute = function () {
-       var i, len, promise;
+OO.ui.WindowManager.prototype.getTeardownDelay = function () {
+       return this.modal ? 250 : 0;
+};
 
-       /**
-        * Continue execution.
-        *
-        * @ignore
-        * @param {Array} step A function and the context it should be called in
-        * @return {Function} Function that continues the process
-        */
-       function proceed( step ) {
-               return function () {
-                       // Execute step in the correct context
-                       var result = step[0].call( step[1] );
+/**
+ * Get managed window by symbolic name.
+ *
+ * If window is not yet instantiated, it will be instantiated and added automatically.
+ *
+ * @param {string} name Symbolic window name
+ * @return {jQuery.Promise} Promise resolved when window is ready to be accessed; when resolved the
+ *   first argument is an OO.ui.Window; when rejected the first argument is an OO.ui.Error
+ * @throws {Error} If the symbolic name is unrecognized by the factory
+ * @throws {Error} If the symbolic name unrecognized as a managed window
+ */
+OO.ui.WindowManager.prototype.getWindow = function ( name ) {
+       var deferred = $.Deferred(),
+               win = this.windows[name];
 
-                       if ( result === false ) {
-                               // Use rejected promise for boolean false results
-                               return $.Deferred().reject().promise();
-                       }
-                       // Duck-type the object to see if it can produce a promise
-                       if ( result && $.isFunction( result.promise ) ) {
-                               // Use a promise generated from the result
-                               return result.promise();
+       if ( !( win instanceof OO.ui.Window ) ) {
+               if ( this.factory ) {
+                       if ( !this.factory.lookup( name ) ) {
+                               deferred.reject( new OO.ui.Error(
+                                       'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
+                               ) );
+                       } else {
+                               win = this.factory.create( name, this, { '$': this.$ } );
+                               this.addWindows( [ win ] ).then(
+                                       OO.ui.bind( deferred.resolve, deferred, win ),
+                                       deferred.reject
+                               );
                        }
-                       // Use resolved promise for other results
-                       return $.Deferred().resolve().promise();
-               };
-       }
-
-       if ( this.steps.length ) {
-               // Generate a chain reaction of promises
-               promise = proceed( this.steps[0] )();
-               for ( i = 1, len = this.steps.length; i < len; i++ ) {
-                       promise = promise.then( proceed( this.steps[i] ) );
+               } else {
+                       deferred.reject( new OO.ui.Error(
+                               'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
+                       ) );
                }
        } else {
-               promise = $.Deferred().resolve().promise();
+               deferred.resolve( win );
        }
 
-       return promise;
+       return deferred.promise();
 };
 
 /**
- * Add step to the beginning of the process.
+ * Get current window.
  *
- * @param {Function} step Function to execute; if it returns boolean false the process will stop; if
- *   it returns an object with a `promise` method the process will use the promise to either
- *   continue to the next step when the promise is resolved or stop when the promise is rejected
- * @param {Object} [context=null] Context to call the step function in
- * @chainable
+ * @return {OO.ui.Window|null} Currently opening/opened/closing window
  */
-OO.ui.Process.prototype.first = function ( step, context ) {
-       this.steps.unshift( [ step, context || null ] );
-       return this;
+OO.ui.WindowManager.prototype.getCurrentWindow = function () {
+       return this.currentWindow;
 };
 
 /**
- * Add step to the end of the process.
+ * Open a window.
  *
- * @param {Function} step Function to execute; if it returns boolean false the process will stop; if
- *   it returns an object with a `promise` method the process will use the promise to either
- *   continue to the next step when the promise is resolved or stop when the promise is rejected
- * @param {Object} [context=null] Context to call the step function in
- * @chainable
+ * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
+ * @param {Object} [data] Window opening data
+ * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening}
+ *   for more details about the `opening` promise
+ * @fires opening
  */
-OO.ui.Process.prototype.next = function ( step, context ) {
-       this.steps.push( [ step, context || null ] );
-       return this;
+OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
+       var manager = this,
+               preparing = [],
+               opening = $.Deferred();
+
+       // Argument handling
+       if ( typeof win === 'string' ) {
+               return this.getWindow( win ).then( function ( win ) {
+                       return manager.openWindow( win, data );
+               } );
+       }
+
+       // Error handling
+       if ( !this.hasWindow( win ) ) {
+               opening.reject( new OO.ui.Error(
+                       'Cannot open window: window is not attached to manager'
+               ) );
+       }
+
+       // Window opening
+       if ( opening.state() !== 'rejected' ) {
+               // Begin loading the window if it's not loaded already - may take noticable time and we want
+               // too do this in paralell with any preparatory actions
+               preparing.push( win.load() );
+
+               if ( this.opening || this.opened ) {
+                       // If a window is currently opening or opened, close it first
+                       preparing.push( this.closeWindow( this.currentWindow ) );
+               } else if ( this.closing ) {
+                       // If a window is currently closing, wait for it to complete
+                       preparing.push( this.closing );
+               }
+
+               $.when.apply( $, preparing ).done( function () {
+                       if ( manager.modal ) {
+                               manager.$( manager.getElementDocument() ).on( {
+                                       // Prevent scrolling by keys in top-level window
+                                       'keydown': manager.onDocumentKeyDownHandler
+                               } );
+                               manager.$( manager.getElementWindow() ).on( {
+                                       // Prevent scrolling by wheel in top-level window
+                                       'mousewheel': manager.onWindowMouseWheelHandler,
+                                       // Start listening for top-level window dimension changes
+                                       'orientationchange resize': manager.onWindowResizeHandler
+                               } );
+                               // Hide other content from screen readers
+                               manager.$ariaHidden = $( 'body' )
+                                       .children()
+                                       .not( manager.$element.parentsUntil( 'body' ).last() )
+                                       .attr( 'aria-hidden', '' );
+                       }
+                       manager.currentWindow = win;
+                       manager.opening = opening;
+                       manager.emit( 'opening', win, opening, data );
+                       manager.updateWindowSize( win );
+                       setTimeout( function () {
+                               win.setup( data ).then( function () {
+                                       manager.opening.notify( { 'state': 'setup' } );
+                                       setTimeout( function () {
+                                               win.ready( data ).then( function () {
+                                                       manager.opening.notify( { 'state': 'ready' } );
+                                                       manager.opening = null;
+                                                       manager.opened = $.Deferred();
+                                                       opening.resolve( manager.opened.promise(), data );
+                                               } );
+                                       }, manager.getReadyDelay() );
+                               } );
+                       }, manager.getSetupDelay() );
+               } );
+       }
+
+       return opening;
 };
 
 /**
- * Dialog for showing a confirmation/warning message.
+ * Close a window.
  *
- * @class
- * @extends OO.ui.Dialog
- *
- * @constructor
- * @param {Object} [config] Configuration options
+ * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
+ * @param {Object} [data] Window closing data
+ * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-closing}
+ *   for more details about the `closing` promise
+ * @throws {Error} If no window by that name is being managed
+ * @fires closing
  */
-OO.ui.ConfirmationDialog = function OoUiConfirmationDialog( config ) {
-       // Configuration initialization
-       config = $.extend( { 'size': 'small' }, config );
-
-       // Parent constructor
-       OO.ui.Dialog.call( this, config );
-};
+OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
+       var manager = this,
+               preparing = [],
+               closing = $.Deferred(),
+               opened = this.opened;
+
+       // Argument handling
+       if ( typeof win === 'string' ) {
+               win = this.windows[win];
+       } else if ( !this.hasWindow( win ) ) {
+               win = null;
+       }
+
+       // Error handling
+       if ( !win ) {
+               closing.reject( new OO.ui.Error(
+                       'Cannot close window: window is not attached to manager'
+               ) );
+       } else if ( win !== this.currentWindow ) {
+               closing.reject( new OO.ui.Error(
+                       'Cannot close window: window already closed with different data'
+               ) );
+       } else if ( this.closing ) {
+               closing.reject( new OO.ui.Error(
+                       'Cannot close window: window already closing with different data'
+               ) );
+       }
+
+       // Window closing
+       if ( closing.state() !== 'rejected' ) {
+               if ( this.opening ) {
+                       // If the window is currently opening, close it when it's done
+                       preparing.push( this.opening );
+               }
 
-/* Inheritance */
+               // Close the window
+               $.when.apply( $, preparing ).done( function () {
+                       manager.closing = closing;
+                       manager.emit( 'closing', win, closing, data );
+                       manager.opened = null;
+                       opened.resolve( closing.promise(), data );
+                       setTimeout( function () {
+                               win.hold( data ).then( function () {
+                                       closing.notify( { 'state': 'hold' } );
+                                       setTimeout( function () {
+                                               win.teardown( data ).then( function () {
+                                                       closing.notify( { 'state': 'teardown' } );
+                                                       if ( manager.modal ) {
+                                                               manager.$( manager.getElementDocument() ).off( {
+                                                                       // Allow scrolling by keys in top-level window
+                                                                       'keydown': manager.onDocumentKeyDownHandler
+                                                               } );
+                                                               manager.$( manager.getElementWindow() ).off( {
+                                                                       // Allow scrolling by wheel in top-level window
+                                                                       'mousewheel': manager.onWindowMouseWheelHandler,
+                                                                       // Stop listening for top-level window dimension changes
+                                                                       'orientationchange resize': manager.onWindowResizeHandler
+                                                               } );
+                                                       }
+                                                       // Restore screen reader visiblity
+                                                       if ( manager.$ariaHidden ) {
+                                                               manager.$ariaHidden.removeAttr( 'aria-hidden' );
+                                                               manager.$ariaHidden = null;
+                                                       }
+                                                       manager.closing = null;
+                                                       manager.currentWindow = null;
+                                                       closing.resolve( data );
+                                               } );
+                                       }, manager.getTeardownDelay() );
+                               } );
+                       }, manager.getHoldDelay() );
+               } );
+       }
 
-OO.inheritClass( OO.ui.ConfirmationDialog, OO.ui.Dialog );
+       return closing;
+};
 
-/* Static Properties */
+/**
+ * Add windows.
+ *
+ * If the window manager is attached to the DOM then windows will be automatically loaded as they
+ * are added.
+ *
+ * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add
+ * @return {jQuery.Promise} Promise resolved when all windows are added
+ * @throws {Error} If one of the windows being added without an explicit symbolic name does not have
+ *   a statically configured symbolic name
+ */
+OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
+       var i, len, win, name, list,
+               promises = [];
 
-OO.ui.ConfirmationDialog.static.name = 'confirm';
+       if ( $.isArray( windows ) ) {
+               // Convert to map of windows by looking up symbolic names from static configuration
+               list = {};
+               for ( i = 0, len = windows.length; i < len; i++ ) {
+                       name = windows[i].constructor.static.name;
+                       if ( typeof name !== 'string' ) {
+                               throw new Error( 'Cannot add window' );
+                       }
+                       list[name] = windows[i];
+               }
+       } else if ( $.isPlainObject( windows ) ) {
+               list = windows;
+       }
 
-OO.ui.ConfirmationDialog.static.icon = 'help';
+       // Add windows
+       for ( name in list ) {
+               win = list[name];
+               this.windows[name] = win;
+               this.$element.append( win.$element );
 
-OO.ui.ConfirmationDialog.static.title = OO.ui.deferMsg( 'ooui-dialog-confirm-title' );
+               if ( this.isElementAttached() ) {
+                       promises.push( win.load() );
+               }
+       }
 
-/* Methods */
+       return $.when.apply( $, promises );
+};
 
 /**
- * @inheritdoc
+ * Remove windows.
+ *
+ * Windows will be closed before they are removed.
+ *
+ * @param {string} name Symbolic name of window to remove
+ * @return {jQuery.Promise} Promise resolved when window is closed and removed
+ * @throws {Error} If windows being removed are not being managed
  */
-OO.ui.ConfirmationDialog.prototype.initialize = function () {
-       // Parent method
-       OO.ui.Dialog.prototype.initialize.call( this );
-
-       // Set up the layout
-       var contentLayout = new OO.ui.PanelLayout( {
-               '$': this.$,
-               'padded': true
-       } );
-
-       this.$promptContainer = this.$( '<div>' ).addClass( 'oo-ui-dialog-confirm-promptContainer' );
-
-       this.cancelButton = new OO.ui.ButtonWidget();
-       this.cancelButton.connect( this, { 'click': [ 'close', 'cancel' ] } );
-
-       this.okButton = new OO.ui.ButtonWidget();
-       this.okButton.connect( this, { 'click': [ 'close', 'ok' ] } );
+OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
+       var i, len, win, name,
+               manager = this,
+               promises = [],
+               cleanup = function ( name, win ) {
+                       delete manager.windows[name];
+                       win.$element.detach();
+               };
 
-       // Make the buttons
-       contentLayout.$element.append( this.$promptContainer );
-       this.$body.append( contentLayout.$element );
+       for ( i = 0, len = names.length; i < len; i++ ) {
+               name = names[i];
+               win = this.windows[name];
+               if ( !win ) {
+                       throw new Error( 'Cannot remove window' );
+               }
+               promises.push( this.closeWindow( name ).then( OO.ui.bind( cleanup, null, name, win ) ) );
+       }
 
-       this.$foot.append(
-               this.okButton.$element,
-               this.cancelButton.$element
-       );
+       return $.when.apply( $, promises );
 };
 
-/*
- * Setup a confirmation dialog.
+/**
+ * Remove all windows.
  *
- * @param {Object} [data] Window opening data including text of the dialog and text for the buttons
- * @param {jQuery|string} [data.prompt] Text to display or list of nodes to use as content of the dialog.
- * @param {jQuery|string|Function|null} [data.okLabel] Label of the OK button
- * @param {jQuery|string|Function|null} [data.cancelLabel] Label of the cancel button
- * @param {string|string[]} [data.okFlags="constructive"] Flags for the OK button
- * @param {string|string[]} [data.cancelFlags="destructive"] Flags for the cancel button
- * @return {OO.ui.Process} Setup process
+ * Windows will be closed before they are removed.
+ *
+ * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
  */
-OO.ui.ConfirmationDialog.prototype.getSetupProcess = function ( data ) {
-       // Parent method
-       return OO.ui.ConfirmationDialog.super.prototype.getSetupProcess.call( this, data )
-               .next( function () {
-                       var prompt = data.prompt || OO.ui.deferMsg( 'ooui-dialog-confirm-default-prompt' ),
-                               okLabel = data.okLabel || OO.ui.deferMsg( 'ooui-dialog-confirm-default-ok' ),
-                               cancelLabel = data.cancelLabel || OO.ui.deferMsg( 'ooui-dialog-confirm-default-cancel' ),
-                               okFlags = data.okFlags || 'constructive',
-                               cancelFlags = data.cancelFlags || 'destructive';
-
-                       if ( typeof prompt === 'string' ) {
-                               this.$promptContainer.text( prompt );
-                       } else {
-                               this.$promptContainer.empty().append( prompt );
-                       }
-
-                       this.okButton.setLabel( okLabel ).clearFlags().setFlags( okFlags );
-                       this.cancelButton.setLabel( cancelLabel ).clearFlags().setFlags( cancelFlags );
-               }, this );
+OO.ui.WindowManager.prototype.clearWindows = function () {
+       return this.removeWindows( Object.keys( this.windows ) );
 };
 
 /**
- * @inheritdoc
+ * Set dialog size.
+ *
+ * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
+ *
+ * @chainable
  */
-OO.ui.ConfirmationDialog.prototype.getTeardownProcess = function ( data ) {
-       // Parent method
-       return OO.ui.ConfirmationDialog.super.prototype.getTeardownProcess.call( this, data )
-               .first( function () {
-                       if ( data === 'ok' ) {
-                               this.opened.resolve();
-                       } else { // data === 'cancel', or no data
-                               this.opened.reject();
-                       }
-               }, this );
+OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
+       // Bypass for non-current, and thus invisible, windows
+       if ( win !== this.currentWindow ) {
+               return;
+       }
+
+       var viewport = OO.ui.Element.getDimensions( win.getElementWindow() ),
+               sizes = this.constructor.static.sizes,
+               size = win.getSize();
+
+       if ( !sizes[size] ) {
+               size = this.constructor.static.defaultSize;
+       }
+       if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[size].width ) {
+               size = 'full';
+       }
+
+       this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
+       win.setDimensions( sizes[size] );
+
+       return this;
 };
 
 /**
- * Element with a button.
+ * Process error.
  *
  * @abstract
  * @class
  *
  * @constructor
- * @param {jQuery} $button Button node, assigned to #$button
+ * @param {string|jQuery} message Description of error
  * @param {Object} [config] Configuration options
- * @cfg {boolean} [frameless] Render button without a frame
- * @cfg {number} [tabIndex=0] Button's tab index, use -1 to prevent tab focusing
+ * @cfg {boolean} [recoverable=true] Error is recoverable
  */
-OO.ui.ButtonedElement = function OoUiButtonedElement( $button, config ) {
+OO.ui.Error = function OoUiElement( message, 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' )
-               .prop( 'tabIndex', config.tabIndex || 0 );
-       this.$button
-               .addClass( 'oo-ui-buttonedElement-button' )
-               .attr( 'role', 'button' );
-       if ( config.frameless ) {
-               this.$element.addClass( 'oo-ui-buttonedElement-frameless' );
-       } else {
-               this.$element.addClass( 'oo-ui-buttonedElement-framed' );
-       }
+       this.message = message instanceof jQuery ? message : String( message );
+       this.recoverable = config.recoverable === undefined ? true : !!config.recoverable;
 };
 
 /* Setup */
 
-OO.initClass( OO.ui.ButtonedElement );
-
-/* Static Properties */
-
-/**
- * Cancel mouse down events.
- *
- * @static
- * @inheritable
- * @property {boolean}
- */
-OO.ui.ButtonedElement.static.cancelButtonMouseDownEvents = true;
+OO.initClass( OO.ui.Error );
 
 /* Methods */
 
 /**
- * Handles mouse down events.
+ * Check if error can be recovered from.
  *
- * @param {jQuery.Event} e Mouse down event
+ * @return {boolean} Error is recoverable
  */
-OO.ui.ButtonedElement.prototype.onMouseDown = function ( e ) {
-       if ( this.isDisabled() || e.which !== 1 ) {
-               return false;
-       }
-       // tabIndex should generally be interacted with via the property, but it's not possible to
-       // reliably unset a tabIndex via a property so we use the (lowercase) "tabindex" attribute
-       this.tabIndex = this.$button.attr( 'tabindex' );
-       this.$button
-               // Remove the tab-index while the button is down to prevent the button from stealing focus
-               .removeAttr( 'tabindex' )
-               .addClass( 'oo-ui-buttonedElement-pressed' );
-       // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
-       // reliably reapply the tabindex and remove the pressed class
-       this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
-       // Prevent change of focus unless specifically configured otherwise
-       if ( this.constructor.static.cancelButtonMouseDownEvents ) {
-               return false;
-       }
+OO.ui.Error.prototype.isRecoverable = function () {
+       return this.recoverable;
 };
 
 /**
- * Handles mouse up events.
+ * Get error message as DOM nodes.
  *
- * @param {jQuery.Event} e Mouse up event
+ * @return {jQuery} Error message in DOM nodes
  */
-OO.ui.ButtonedElement.prototype.onMouseUp = function ( e ) {
-       if ( this.isDisabled() || e.which !== 1 ) {
-               return false;
-       }
-       this.$button
-               // Restore the tab-index after the button is up to restore the button's accesssibility
-               .attr( 'tabindex', this.tabIndex )
-               .removeClass( 'oo-ui-buttonedElement-pressed' );
-       // Stop listening for mouseup, since we only needed this once
-       this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
+OO.ui.Error.prototype.getMessage = function () {
+       return this.message instanceof jQuery ?
+               this.message.clone() :
+               $( '<div>' ).text( this.message ).contents();
 };
 
 /**
- * Set active state.
+ * Get error message as text.
  *
- * @param {boolean} [value] Make button active
- * @chainable
+ * @return {string} Error message
  */
-OO.ui.ButtonedElement.prototype.setActive = function ( value ) {
-       this.$button.toggleClass( 'oo-ui-buttonedElement-active', !!value );
-       return this;
+OO.ui.Error.prototype.getMessageText = function () {
+       return this.message instanceof jQuery ? this.message.text() : this.message;
 };
 
 /**
- * Element that can be automatically clipped to visible boundaies.
+ * A list of functions, called in sequence.
+ *
+ * If a function added to a process returns boolean false the process will stop; if it returns an
+ * object with a `promise` method the process will use the promise to either continue to the next
+ * step when the promise is resolved or stop when the promise is rejected.
  *
- * @abstract
  * @class
  *
  * @constructor
- * @param {jQuery} $clippable Nodes to clip, assigned to #$clippable
- * @param {Object} [config] Configuration options
+ * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to
+ *   call, see #createStep for more information
+ * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
+ *   or a promise
+ * @return {Object} Step object, with `callback` and `context` properties
  */
-OO.ui.ClippableElement = function OoUiClippableElement( $clippable, config ) {
-       // Configuration initialization
-       config = config || {};
-
+OO.ui.Process = function ( step, context ) {
        // 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 );
+       this.steps = [];
 
        // Initialization
-       this.$clippable.addClass( 'oo-ui-clippableElement-clippable' );
+       if ( step !== undefined ) {
+               this.next( step, context );
+       }
 };
 
+/* Setup */
+
+OO.initClass( OO.ui.Process );
+
 /* Methods */
 
 /**
- * Set clipping.
+ * Start the process.
  *
- * @param {boolean} value Enable clipping
- * @chainable
+ * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
+ *   any of the steps return boolean false or a promise which gets rejected; upon stopping the
+ *   process, the remaining steps will not be taken
  */
-OO.ui.ClippableElement.prototype.setClipping = function ( value ) {
-       value = !!value;
+OO.ui.Process.prototype.execute = function () {
+       var i, len, promise;
 
-       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;
+       /**
+        * Continue execution.
+        *
+        * @ignore
+        * @param {Array} step A function and the context it should be called in
+        * @return {Function} Function that continues the process
+        */
+       function proceed( step ) {
+               return function () {
+                       // Execute step in the correct context
+                       var deferred,
+                               result = step.callback.call( step.context );
+
+                       if ( result === false ) {
+                               // Use rejected promise for boolean false results
+                               return $.Deferred().reject( [] ).promise();
+                       }
+                       if ( typeof result === 'number' ) {
+                               if ( result < 0 ) {
+                                       throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
+                               }
+                               // Use a delayed promise for numbers, expecting them to be in milliseconds
+                               deferred = $.Deferred();
+                               setTimeout( deferred.resolve, result );
+                               return deferred.promise();
+                       }
+                       if ( result instanceof OO.ui.Error ) {
+                               // Use rejected promise for error
+                               return $.Deferred().reject( [ result ] ).promise();
+                       }
+                       if ( $.isArray( result ) && result.length && result[0] instanceof OO.ui.Error ) {
+                               // Use rejected promise for list of errors
+                               return $.Deferred().reject( result ).promise();
+                       }
+                       // Duck-type the object to see if it can produce a promise
+                       if ( result && $.isFunction( result.promise ) ) {
+                               // Use a promise generated from the result
+                               return result.promise();
+                       }
+                       // Use resolved promise for other results
+                       return $.Deferred().resolve().promise();
+               };
+       }
+
+       if ( this.steps.length ) {
+               // Generate a chain reaction of promises
+               promise = proceed( this.steps[0] )();
+               for ( i = 1, len = this.steps.length; i < len; i++ ) {
+                       promise = promise.then( proceed( this.steps[i] ) );
                }
+       } else {
+               promise = $.Deferred().resolve().promise();
        }
 
-       return this;
+       return promise;
 };
 
 /**
- * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
+ * Create a process step.
  *
- * @return {boolean} Element will be clipped to the visible area
- */
-OO.ui.ClippableElement.prototype.isClipping = function () {
-       return this.clipping;
+ * @private
+ * @param {number|jQuery.Promise|Function} step
+ *
+ * - Number of milliseconds to wait; or
+ * - Promise to wait to be resolved; or
+ * - Function to execute
+ *   - If it returns boolean false the process will stop
+ *   - If it returns an object with a `promise` method the process will use the promise to either
+ *     continue to the next step when the promise is resolved or stop when the promise is rejected
+ *   - If it returns a number, the process will wait for that number of milliseconds before
+ *     proceeding
+ * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
+ *   or a promise
+ * @return {Object} Step object, with `callback` and `context` properties
+ */
+OO.ui.Process.prototype.createStep = function ( step, context ) {
+       if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
+               return {
+                       'callback': function () {
+                               return step;
+                       },
+                       'context': null
+               };
+       }
+       if ( $.isFunction( step ) ) {
+               return {
+                       'callback': step,
+                       'context': context
+               };
+       }
+       throw new Error( 'Cannot create process step: number, promise or function expected' );
 };
 
 /**
- * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
+ * Add step to the beginning of the process.
  *
- * @return {boolean} Part of the element is being clipped
+ * @inheritdoc #createStep
+ * @return {OO.ui.Process} this
+ * @chainable
  */
-OO.ui.ClippableElement.prototype.isClipped = function () {
-       return this.clipped;
-};
-
-/**
- * Set the ideal size.
- *
- * @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;
+OO.ui.Process.prototype.first = function ( step, context ) {
+       this.steps.unshift( this.createStep( step, context ) );
+       return this;
 };
 
 /**
- * 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.
+ * Add step to the end of the process.
  *
+ * @inheritdoc #createStep
+ * @return {OO.ui.Process} this
  * @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( 'width', this.idealWidth || '' );
-               this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
-               this.$clippable.css( 'overflow-x', '' );
-       }
-       if ( clipHeight ) {
-               this.$clippable.css( { 'overflow-y': 'auto', 'height': desiredHeight } );
-       } else {
-               this.$clippable.css( 'height', this.idealHeight || '' );
-               this.$clippable.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
-               this.$clippable.css( 'overflow-y', '' );
-       }
-
-       this.clipped = clipWidth || clipHeight;
-
+OO.ui.Process.prototype.next = function ( step, context ) {
+       this.steps.push( this.createStep( step, context ) );
        return this;
 };
 
 /**
- * Element with named flags that can be added, removed, listed and checked.
- *
- * A flag, when set, adds a CSS class on the `$element` by combing `oo-ui-flaggableElement-` with
- * the flag name. Flags are primarily useful for styling.
+ * Factory for tools.
  *
- * @abstract
  * @class
- *
+ * @extends OO.Factory
  * @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 || {};
+OO.ui.ToolFactory = function OoUiToolFactory() {
+       // Parent constructor
+       OO.ui.ToolFactory.super.call( this );
+};
 
-       // Properties
-       this.flags = {};
+/* Setup */
 
-       // Initialization
-       this.setFlags( config.flags );
-};
+OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
 
 /* Methods */
 
-/**
- * Check if a flag is set.
- *
- * @param {string} flag Name of flag
- * @return {boolean} Has flag
- */
-OO.ui.FlaggableElement.prototype.hasFlag = function ( flag ) {
-       return flag in this.flags;
-};
+/** */
+OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
+       var i, len, included, promoted, demoted,
+               auto = [],
+               used = {};
 
-/**
- * Get the names of all flags set.
- *
- * @return {string[]} flags Flag names
- */
-OO.ui.FlaggableElement.prototype.getFlags = function () {
-       return Object.keys( this.flags );
-};
+       // Collect included and not excluded tools
+       included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
 
-/**
- * Clear all flags.
- *
- * @chainable
- */
-OO.ui.FlaggableElement.prototype.clearFlags = function () {
-       var flag,
-               classPrefix = 'oo-ui-flaggableElement-';
+       // Promotion
+       promoted = this.extract( promote, used );
+       demoted = this.extract( demote, used );
 
-       for ( flag in this.flags ) {
-               delete this.flags[flag];
-               this.$element.removeClass( classPrefix + flag );
+       // Auto
+       for ( i = 0, len = included.length; i < len; i++ ) {
+               if ( !used[included[i]] ) {
+                       auto.push( included[i] );
+               }
        }
 
-       return this;
+       return promoted.concat( auto ).concat( demoted );
 };
 
 /**
- * Add one or more flags.
+ * Get a flat list of names from a list of names or groups.
  *
- * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
- *  keyed by flag name containing boolean set/remove instructions.
- * @chainable
+ * 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.FlaggableElement.prototype.setFlags = function ( flags ) {
-       var i, len, flag,
-               classPrefix = 'oo-ui-flaggableElement-';
+OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
+       var i, len, item, name, tool,
+               names = [];
 
-       if ( typeof flags === 'string' ) {
-               // Set
-               this.flags[flags] = true;
-               this.$element.addClass( classPrefix + flags );
-       } else if ( $.isArray( flags ) ) {
-               for ( i = 0, len = flags.length; i < len; i++ ) {
-                       flag = flags[i];
-                       // Set
-                       this.flags[flag] = true;
-                       this.$element.addClass( classPrefix + flag );
+       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.autoAddToCatchall &&
+                               // Exclude already used tools
+                               ( !used || !used[name] )
+                       ) {
+                               names.push( name );
+                               if ( used ) {
+                                       used[name] = true;
+                               }
+                       }
                }
-       } else if ( OO.isPlainObject( flags ) ) {
-               for ( flag in flags ) {
-                       if ( flags[flag] ) {
-                               // Set
-                               this.flags[flag] = true;
-                               this.$element.addClass( classPrefix + flag );
-                       } else {
-                               // Remove
-                               delete this.flags[flag];
-                               this.$element.removeClass( classPrefix + flag );
+       } else if ( $.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.autoAddToGroup &&
+                                                       // 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 this;
+       return names;
 };
 
 /**
- * Element containing a sequence of child elements.
+ * Factory for tool groups.
  *
- * @abstract
  * @class
- *
+ * @extends OO.Factory
  * @constructor
- * @param {jQuery} $group Container node, assigned to #$group
- * @param {Object} [config] Configuration options
  */
-OO.ui.GroupElement = function OoUiGroupElement( $group, config ) {
-       // Configuration
-       config = config || {};
+OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
+       // Parent constructor
+       OO.Factory.call( this );
 
-       // Properties
-       this.$group = $group;
-       this.items = [];
-       this.aggregateItemEvents = {};
+       var i, l,
+               defaultClasses = this.constructor.static.getDefaultClasses();
+
+       // Register default toolgroups
+       for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
+               this.register( defaultClasses[i] );
+       }
 };
 
-/* Methods */
+/* Setup */
 
-/**
- * Get items.
- *
- * @return {OO.ui.Element[]} Items
- */
-OO.ui.GroupElement.prototype.getItems = function () {
-       return this.items.slice( 0 );
-};
+OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
+
+/* Static Methods */
 
 /**
- * Add an aggregate item event.
+ * Get a default set of classes to be registered on construction
  *
- * Aggregated events are listened to on each item and then emitted by the group under a new name,
- * and with an additional leading parameter containing the item that emitted the original event.
- * Other arguments that were emitted from the original event are passed through.
+ * @return {Function[]} Default classes
+ */
+OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
+       return [
+               OO.ui.BarToolGroup,
+               OO.ui.ListToolGroup,
+               OO.ui.MenuToolGroup
+       ];
+};
+
+/**
+ * Element with a button.
  *
- * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
- *   event, use null value to remove aggregation
- * @throws {Error} If aggregation already exists
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {jQuery} $button Button node, assigned to #$button
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [framed=true] Render button with a frame
+ * @cfg {number} [tabIndex=0] Button's tab index, use null to have no tabIndex
+ * @cfg {string} [accessKey] Button's access key
  */
-OO.ui.GroupElement.prototype.aggregate = function ( events ) {
-       var i, len, item, add, remove, itemEvent, groupEvent;
+OO.ui.ButtonedElement = function OoUiButtonedElement( $button, config ) {
+       // Configuration initialization
+       config = config || {};
 
-       for ( itemEvent in events ) {
-               groupEvent = events[itemEvent];
+       // Properties
+       this.$button = $button;
+       this.tabIndex = null;
+       this.framed = null;
+       this.active = false;
+       this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
 
-               // Remove existing aggregated event
-               if ( itemEvent in this.aggregateItemEvents ) {
-                       // Don't allow duplicate aggregations
-                       if ( groupEvent ) {
-                               throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
-                       }
-                       // Remove event aggregation from existing items
-                       for ( i = 0, len = this.items.length; i < len; i++ ) {
-                               item = this.items[i];
-                               if ( item.connect && item.disconnect ) {
-                                       remove = {};
-                                       remove[itemEvent] = [ 'emit', groupEvent, item ];
-                                       item.disconnect( this, remove );
-                               }
-                       }
-                       // Prevent future items from aggregating event
-                       delete this.aggregateItemEvents[itemEvent];
-               }
+       // Events
+       this.$button.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
 
-               // Add new aggregate event
-               if ( groupEvent ) {
-                       // Make future items aggregate event
-                       this.aggregateItemEvents[itemEvent] = groupEvent;
-                       // Add event aggregation to existing items
-                       for ( i = 0, len = this.items.length; i < len; i++ ) {
-                               item = this.items[i];
-                               if ( item.connect && item.disconnect ) {
-                                       add = {};
-                                       add[itemEvent] = [ 'emit', groupEvent, item ];
-                                       item.connect( this, add );
-                               }
-                       }
-               }
-       }
+       // Initialization
+       this.$element.addClass( 'oo-ui-buttonedElement' );
+       this.$button
+               .addClass( 'oo-ui-buttonedElement-button' )
+               .attr( 'role', 'button' );
+       this.setTabIndex( config.tabIndex || 0 );
+       this.setAccessKey( config.accessKey );
+       this.toggleFramed( config.framed === undefined || config.framed );
 };
 
+/* Setup */
+
+OO.initClass( OO.ui.ButtonedElement );
+
+/* Static Properties */
+
 /**
- * Add items.
+ * Cancel mouse down events.
  *
- * @param {OO.ui.Element[]} items Item
- * @param {number} [index] Index to insert items at
- * @chainable
+ * @static
+ * @inheritable
+ * @property {boolean}
  */
-OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
-       var i, len, item, event, events, currentIndex,
-               itemElements = [];
+OO.ui.ButtonedElement.static.cancelButtonMouseDownEvents = true;
 
-       for ( i = 0, len = items.length; i < len; i++ ) {
-               item = items[i];
+/* Methods */
 
-               // Check if item exists then remove it first, effectively "moving" it
-               currentIndex = $.inArray( item, this.items );
-               if ( currentIndex >= 0 ) {
-                       this.removeItems( [ item ] );
-                       // Adjust index to compensate for removal
-                       if ( currentIndex < index ) {
-                               index--;
-                       }
-               }
-               // Add the item
-               if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
-                       events = {};
-                       for ( event in this.aggregateItemEvents ) {
-                               events[event] = [ 'emit', this.aggregateItemEvents[event], item ];
-                       }
-                       item.connect( this, events );
-               }
-               item.setElementGroup( this );
-               itemElements.push( item.$element.get( 0 ) );
+/**
+ * Handles mouse down events.
+ *
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.ButtonedElement.prototype.onMouseDown = function ( e ) {
+       if ( this.isDisabled() || e.which !== 1 ) {
+               return false;
        }
-
-       if ( index === undefined || index < 0 || index >= this.items.length ) {
-               this.$group.append( itemElements );
-               this.items.push.apply( this.items, items );
-       } else if ( index === 0 ) {
-               this.$group.prepend( itemElements );
-               this.items.unshift.apply( this.items, items );
-       } else {
-               this.items[index].$element.before( itemElements );
-               this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
+       // tabIndex should generally be interacted with via the property, but it's not possible to
+       // reliably unset a tabIndex via a property so we use the (lowercase) "tabindex" attribute
+       this.tabIndex = this.$button.attr( 'tabindex' );
+       this.$button
+               // Remove the tab-index while the button is down to prevent the button from stealing focus
+               .removeAttr( 'tabindex' )
+               .addClass( 'oo-ui-buttonedElement-pressed' );
+       // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
+       // reliably reapply the tabindex and remove the pressed class
+       this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
+       // Prevent change of focus unless specifically configured otherwise
+       if ( this.constructor.static.cancelButtonMouseDownEvents ) {
+               return false;
        }
-
-       return this;
 };
 
 /**
- * Remove items.
+ * Handles mouse up events.
  *
- * Items will be detached, not removed, so they can be used later.
+ * @param {jQuery.Event} e Mouse up event
+ */
+OO.ui.ButtonedElement.prototype.onMouseUp = function ( e ) {
+       if ( this.isDisabled() || e.which !== 1 ) {
+               return false;
+       }
+       this.$button
+               // Restore the tab-index after the button is up to restore the button's accesssibility
+               .attr( 'tabindex', this.tabIndex )
+               .removeClass( 'oo-ui-buttonedElement-pressed' );
+       // Stop listening for mouseup, since we only needed this once
+       this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
+};
+
+/**
+ * Toggle frame.
  *
- * @param {OO.ui.Element[]} items Items to remove
+ * @param {boolean} [framed] Make button framed, omit to toggle
  * @chainable
  */
-OO.ui.GroupElement.prototype.removeItems = function ( items ) {
-       var i, len, item, index, remove, itemEvent;
-
-       // Remove specific items
-       for ( i = 0, len = items.length; i < len; i++ ) {
-               item = items[i];
-               index = $.inArray( item, this.items );
-               if ( index !== -1 ) {
-                       if (
-                               item.connect && item.disconnect &&
-                               !$.isEmptyObject( this.aggregateItemEvents )
-                       ) {
-                               remove = {};
-                               if ( itemEvent in this.aggregateItemEvents ) {
-                                       remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
-                               }
-                               item.disconnect( this, remove );
-                       }
-                       item.setElementGroup( null );
-                       this.items.splice( index, 1 );
-                       item.$element.detach();
-               }
+OO.ui.ButtonedElement.prototype.toggleFramed = function ( framed ) {
+       framed = framed === undefined ? !this.framed : !!framed;
+       if ( framed !== this.framed ) {
+               this.framed = framed;
+               this.$element
+                       .toggleClass( 'oo-ui-buttonedElement-frameless', !framed )
+                       .toggleClass( 'oo-ui-buttonedElement-framed', framed );
        }
 
        return this;
 };
 
 /**
- * Clear all items.
- *
- * Items will be detached, not removed, so they can be used later.
+ * Set tab index.
  *
+ * @param {number|null} tabIndex Button's tab index, use null to remove
  * @chainable
  */
-OO.ui.GroupElement.prototype.clearItems = function () {
-       var i, len, item, remove, itemEvent;
+OO.ui.ButtonedElement.prototype.setTabIndex = function ( tabIndex ) {
+       if ( typeof tabIndex === 'number' && tabIndex >= 0 ) {
+               this.$button.attr( 'tabindex', tabIndex );
+       } else {
+               this.$button.removeAttr( 'tabindex' );
+       }
+       return this;
+};
 
-       // Remove all items
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               item = this.items[i];
-               if (
-                       item.connect && item.disconnect &&
-                       !$.isEmptyObject( this.aggregateItemEvents )
-               ) {
-                       remove = {};
-                       if ( itemEvent in this.aggregateItemEvents ) {
-                               remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
-                       }
-                       item.disconnect( this, remove );
-               }
-               item.setElementGroup( null );
-               item.$element.detach();
+/**
+ * Set access key
+ *
+ * @param {string} accessKey Button's access key, use empty string to remove
+ * @chainable
+ */
+OO.ui.ButtonedElement.prototype.setAccessKey = function ( accessKey ) {
+       if ( typeof accessKey === 'string' && accessKey.length ) {
+               this.$button.attr( 'accesskey', accessKey );
+       } else {
+               this.$button.removeAttr( 'accesskey' );
        }
+       return this;
+};
 
-       this.items = [];
+/**
+ * Set active state.
+ *
+ * @param {boolean} [value] Make button active
+ * @chainable
+ */
+OO.ui.ButtonedElement.prototype.setActive = function ( value ) {
+       this.$button.toggleClass( 'oo-ui-buttonedElement-active', !!value );
        return this;
 };
 
 /**
- * Element containing an icon.
+ * Element that can be automatically clipped to visible boundaies.
  *
  * @abstract
  * @class
  *
  * @constructor
- * @param {jQuery} $icon Icon node, assigned to #$icon
+ * @param {jQuery} $clippable Nodes to clip, assigned to #$clippable
  * @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
+OO.ui.ClippableElement = function OoUiClippableElement( $clippable, config ) {
+       // Configuration initialization
        config = config || {};
 
        // Properties
-       this.$icon = $icon;
-       this.icon = null;
+       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.$icon.addClass( 'oo-ui-iconedElement-icon' );
-       this.setIcon( config.icon || this.constructor.static.icon );
+       this.$clippable.addClass( 'oo-ui-clippableElement-clippable' );
 };
 
-/* Setup */
+/* Methods */
 
-OO.initClass( OO.ui.IconedElement );
+/**
+ * Set clipping.
+ *
+ * @param {boolean} value Enable clipping
+ * @chainable
+ */
+OO.ui.ClippableElement.prototype.setClipping = function ( value ) {
+       value = !!value;
 
-/* Static Properties */
+       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;
+};
 
 /**
- * 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.
+ * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
  *
- * Example of i18n icon definition:
- *     { 'default': 'bold-a', 'en': 'bold-b', 'de': 'bold-f' }
+ * @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.
  *
- * @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
+ * @return {boolean} Part of the element is being clipped
  */
-OO.ui.IconedElement.static.icon = null;
+OO.ui.ClippableElement.prototype.isClipped = function () {
+       return this.clipped;
+};
 
-/* Methods */
+/**
+ * Set the ideal size.
+ *
+ * @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;
+};
 
 /**
- * Set icon.
+ * 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.
  *
- * @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;
+OO.ui.ClippableElement.prototype.clip = function () {
+       if ( !this.clipping ) {
+               // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
+               return this;
+       }
 
-       if ( this.icon ) {
-               this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
+       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( 'width', this.idealWidth || '' );
+               this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
+               this.$clippable.css( 'overflow-x', '' );
        }
-       if ( typeof icon === 'string' ) {
-               icon = icon.trim();
-               if ( icon.length ) {
-                       this.$icon.addClass( 'oo-ui-icon-' + icon );
-                       this.icon = icon;
-               }
+       if ( clipHeight ) {
+               this.$clippable.css( { 'overflow-y': 'auto', 'height': desiredHeight } );
+       } else {
+               this.$clippable.css( 'height', this.idealHeight || '' );
+               this.$clippable.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
+               this.$clippable.css( 'overflow-y', '' );
        }
-       this.$element.toggleClass( 'oo-ui-iconedElement', !!this.icon );
+
+       this.clipped = clipWidth || clipHeight;
 
        return this;
 };
 
 /**
- * Get icon.
+ * Element with named flags that can be added, removed, listed and checked.
  *
- * @return {string} Icon
- */
-OO.ui.IconedElement.prototype.getIcon = function () {
-       return this.icon;
-};
-
-/**
- * Element containing an indicator.
+ * A flag, when set, adds a CSS class on the `$element` by combing `oo-ui-flaggableElement-` with
+ * the flag name. Flags are primarily useful for styling.
  *
  * @abstract
  * @class
  *
  * @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
+ * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive'
  */
-OO.ui.IndicatedElement = function OoUiIndicatedElement( $indicator, config ) {
-       // Config intialization
+OO.ui.FlaggableElement = function OoUiFlaggableElement( config ) {
+       // Config initialization
        config = config || {};
 
        // Properties
-       this.$indicator = $indicator;
-       this.indicator = null;
-       this.indicatorLabel = null;
+       this.flags = {};
 
        // Initialization
-       this.$indicator.addClass( 'oo-ui-indicatedElement-indicator' );
-       this.setIndicator( config.indicator || this.constructor.static.indicator );
-       this.setIndicatorTitle( config.indicatorTitle  || this.constructor.static.indicatorTitle );
+       this.setFlags( config.flags );
 };
 
-/* Setup */
+/* Events */
 
-OO.initClass( OO.ui.IndicatedElement );
+/**
+ * @event flag
+ * @param {Object.<string,boolean>} changes Object keyed by flag name containing boolean
+ *   added/removed properties
+ */
 
-/* Static Properties */
+/* Methods */
 
 /**
- * indicator.
+ * Check if a flag is set.
  *
- * @static
- * @inheritable
- * @property {string|null} Symbolic indicator name or null for no indicator
+ * @param {string} flag Name of flag
+ * @return {boolean} Has flag
  */
-OO.ui.IndicatedElement.static.indicator = null;
+OO.ui.FlaggableElement.prototype.hasFlag = function ( flag ) {
+       return flag in this.flags;
+};
 
 /**
- * Indicator title.
+ * Get the names of all flags set.
  *
- * @static
- * @inheritable
- * @property {string|Function|null} Indicator title text, a function that return text or null for no
- *  indicator title
+ * @return {string[]} flags Flag names
  */
-OO.ui.IndicatedElement.static.indicatorTitle = null;
-
-/* Methods */
+OO.ui.FlaggableElement.prototype.getFlags = function () {
+       return Object.keys( this.flags );
+};
 
 /**
- * Set indicator.
+ * Clear all flags.
  *
- * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
  * @chainable
+ * @fires flag
  */
-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;
-               }
+OO.ui.FlaggableElement.prototype.clearFlags = function () {
+       var flag,
+               changes = {},
+               classPrefix = 'oo-ui-flaggableElement-';
+
+       for ( flag in this.flags ) {
+               changes[flag] = false;
+               delete this.flags[flag];
+               this.$element.removeClass( classPrefix + flag );
        }
-       this.$element.toggleClass( 'oo-ui-indicatedElement', !!this.indicator );
+
+       this.emit( 'flag', changes );
 
        return this;
 };
 
 /**
- * Set indicator label.
+ * Add one or more flags.
  *
- * @param {string|Function|null} indicator Indicator title text, a function that return text or null
- *  for no indicator title
+ * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
+ *  keyed by flag name containing boolean set/remove instructions.
  * @chainable
+ * @fires flag
  */
-OO.ui.IndicatedElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
-       this.indicatorTitle = indicatorTitle = OO.ui.resolveMsg( indicatorTitle );
+OO.ui.FlaggableElement.prototype.setFlags = function ( flags ) {
+       var i, len, flag,
+               changes = {},
+               classPrefix = 'oo-ui-flaggableElement-';
 
-       if ( typeof indicatorTitle === 'string' && indicatorTitle.length ) {
-               this.$indicator.attr( 'title', indicatorTitle );
-       } else {
-               this.$indicator.removeAttr( 'title' );
+       if ( typeof flags === 'string' ) {
+               // Set
+               this.flags[flags] = true;
+               this.$element.addClass( classPrefix + flags );
+       } else if ( $.isArray( flags ) ) {
+               for ( i = 0, len = flags.length; i < len; i++ ) {
+                       flag = flags[i];
+                       // Set
+                       changes[flag] = true;
+                       this.flags[flag] = true;
+                       this.$element.addClass( classPrefix + flag );
+               }
+       } else if ( OO.isPlainObject( flags ) ) {
+               for ( flag in flags ) {
+                       if ( flags[flag] ) {
+                               // Set
+                               changes[flag] = true;
+                               this.flags[flag] = true;
+                               this.$element.addClass( classPrefix + flag );
+                       } else {
+                               // Remove
+                               changes[flag] = false;
+                               delete this.flags[flag];
+                               this.$element.removeClass( classPrefix + flag );
+                       }
+               }
        }
 
+       this.emit( 'flag', changes );
+
        return this;
 };
 
 /**
- * Get indicator.
+ * Element containing a sequence of child elements.
  *
- * @return {string} title Symbolic name of indicator
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {jQuery} $group Container node, assigned to #$group
+ * @param {Object} [config] Configuration options
  */
-OO.ui.IndicatedElement.prototype.getIndicator = function () {
-       return this.indicator;
+OO.ui.GroupElement = function OoUiGroupElement( $group, config ) {
+       // Configuration
+       config = config || {};
+
+       // Properties
+       this.$group = $group;
+       this.items = [];
+       this.aggregateItemEvents = {};
 };
 
+/* Methods */
+
 /**
- * Get indicator title.
+ * Get items.
  *
- * @return {string} Indicator title text
+ * @return {OO.ui.Element[]} Items
  */
-OO.ui.IndicatedElement.prototype.getIndicatorTitle = function () {
-       return this.indicatorTitle;
+OO.ui.GroupElement.prototype.getItems = function () {
+       return this.items.slice( 0 );
 };
 
 /**
- * Element containing a label.
+ * Add an aggregate item event.
  *
- * @abstract
- * @class
+ * Aggregated events are listened to on each item and then emitted by the group under a new name,
+ * and with an additional leading parameter containing the item that emitted the original event.
+ * Other arguments that were emitted from the original event are passed through.
  *
- * @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
- * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
+ * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
+ *   event, use null value to remove aggregation
+ * @throws {Error} If aggregation already exists
  */
-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 );
-       this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
-};
+OO.ui.GroupElement.prototype.aggregate = function ( events ) {
+       var i, len, item, add, remove, itemEvent, groupEvent;
 
-/* Setup */
+       for ( itemEvent in events ) {
+               groupEvent = events[itemEvent];
 
-OO.initClass( OO.ui.LabeledElement );
+               // Remove existing aggregated event
+               if ( itemEvent in this.aggregateItemEvents ) {
+                       // Don't allow duplicate aggregations
+                       if ( groupEvent ) {
+                               throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
+                       }
+                       // Remove event aggregation from existing items
+                       for ( i = 0, len = this.items.length; i < len; i++ ) {
+                               item = this.items[i];
+                               if ( item.connect && item.disconnect ) {
+                                       remove = {};
+                                       remove[itemEvent] = [ 'emit', groupEvent, item ];
+                                       item.disconnect( this, remove );
+                               }
+                       }
+                       // Prevent future items from aggregating event
+                       delete this.aggregateItemEvents[itemEvent];
+               }
 
-/* Static Properties */
+               // Add new aggregate event
+               if ( groupEvent ) {
+                       // Make future items aggregate event
+                       this.aggregateItemEvents[itemEvent] = groupEvent;
+                       // Add event aggregation to existing items
+                       for ( i = 0, len = this.items.length; i < len; i++ ) {
+                               item = this.items[i];
+                               if ( item.connect && item.disconnect ) {
+                                       add = {};
+                                       add[itemEvent] = [ 'emit', groupEvent, item ];
+                                       item.connect( this, add );
+                               }
+                       }
+               }
+       }
+};
 
 /**
- * Label.
+ * Add items.
  *
- * @static
- * @inheritable
- * @property {string|Function|null} Label text; a function that returns a nodes or text; or null for
- *  no label
+ * @param {OO.ui.Element[]} items Item
+ * @param {number} [index] Index to insert items at
+ * @chainable
  */
-OO.ui.LabeledElement.static.label = null;
+OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
+       var i, len, item, event, events, currentIndex,
+               itemElements = [];
 
-/* Methods */
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+
+               // Check if item exists then remove it first, effectively "moving" it
+               currentIndex = $.inArray( item, this.items );
+               if ( currentIndex >= 0 ) {
+                       this.removeItems( [ item ] );
+                       // Adjust index to compensate for removal
+                       if ( currentIndex < index ) {
+                               index--;
+                       }
+               }
+               // Add the item
+               if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
+                       events = {};
+                       for ( event in this.aggregateItemEvents ) {
+                               events[event] = [ 'emit', this.aggregateItemEvents[event], item ];
+                       }
+                       item.connect( this, events );
+               }
+               item.setElementGroup( this );
+               itemElements.push( item.$element.get( 0 ) );
+       }
+
+       if ( index === undefined || index < 0 || index >= this.items.length ) {
+               this.$group.append( itemElements );
+               this.items.push.apply( this.items, items );
+       } else if ( index === 0 ) {
+               this.$group.prepend( itemElements );
+               this.items.unshift.apply( this.items, items );
+       } else {
+               this.items[index].$element.before( itemElements );
+               this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
+       }
+
+       return this;
+};
 
 /**
- * Set the label.
+ * Remove items.
  *
- * An empty string will result in the label being hidden. A string containing only whitespace will
- * be converted to a single &nbsp;
+ * Items will be detached, not removed, so they can be used later.
  *
- * @param {jQuery|string|Function|null} label Label nodes; text; a function that retuns nodes or
- *  text; or null for no label
+ * @param {OO.ui.Element[]} items Items to remove
  * @chainable
  */
-OO.ui.LabeledElement.prototype.setLabel = function ( label ) {
-       var empty = false;
+OO.ui.GroupElement.prototype.removeItems = function ( items ) {
+       var i, len, item, index, remove, itemEvent;
 
-       this.label = label = OO.ui.resolveMsg( label ) || null;
-       if ( typeof label === 'string' && label.length ) {
-               if ( label.match( /^\s*$/ ) ) {
-                       // Convert whitespace only string to a single non-breaking space
-                       this.$label.html( '&nbsp;' );
-               } else {
-                       this.$label.text( label );
+       // Remove specific items
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+               index = $.inArray( item, this.items );
+               if ( index !== -1 ) {
+                       if (
+                               item.connect && item.disconnect &&
+                               !$.isEmptyObject( this.aggregateItemEvents )
+                       ) {
+                               remove = {};
+                               if ( itemEvent in this.aggregateItemEvents ) {
+                                       remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
+                               }
+                               item.disconnect( this, remove );
+                       }
+                       item.setElementGroup( null );
+                       this.items.splice( index, 1 );
+                       item.$element.detach();
                }
-       } 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.
+ * Clear all items.
  *
- * @return {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.
+ * Items will be detached, not removed, so they can be used later.
  *
  * @chainable
  */
-OO.ui.LabeledElement.prototype.fitLabel = function () {
-       if ( this.$label.autoEllipsis && this.autoFitLabel ) {
-               this.$label.autoEllipsis( { 'hasSpan': false, 'tooltip': true } );
+OO.ui.GroupElement.prototype.clearItems = function () {
+       var i, len, item, remove, itemEvent;
+
+       // Remove all items
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               item = this.items[i];
+               if (
+                       item.connect && item.disconnect &&
+                       !$.isEmptyObject( this.aggregateItemEvents )
+               ) {
+                       remove = {};
+                       if ( itemEvent in this.aggregateItemEvents ) {
+                               remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
+                       }
+                       item.disconnect( this, remove );
+               }
+               item.setElementGroup( null );
+               item.$element.detach();
        }
+
+       this.items = [];
        return this;
 };
 
 /**
- * Popuppable element.
+ * Element containing an icon.
  *
  * @abstract
  * @class
  *
  * @constructor
+ * @param {jQuery} $icon Icon node, assigned to #$icon
  * @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
+ * @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.PopuppableElement = function OoUiPopuppableElement( config ) {
-       // Configuration initialization
-       config = $.extend( { 'popupWidth': 320 }, config );
+OO.ui.IconedElement = function OoUiIconedElement( $icon, config ) {
+       // Config intialization
+       config = 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;
+       this.$icon = $icon;
+       this.icon = null;
+
+       // Initialization
+       this.$icon.addClass( 'oo-ui-iconedElement-icon' );
+       this.setIcon( config.icon || this.constructor.static.icon );
 };
 
-/* Methods */
+/* Setup */
 
-/**
- * Get popup.
- *
- * @return {OO.ui.PopupWidget} Popup widget
- */
-OO.ui.PopuppableElement.prototype.getPopup = function () {
-       return this.popup;
-};
+OO.initClass( OO.ui.IconedElement );
 
-/**
- * Show popup.
- */
-OO.ui.PopuppableElement.prototype.showPopup = function () {
-       this.popup.show().display( this.popupWidth, this.popupHeight );
-};
+/* Static Properties */
 
 /**
- * Hide popup.
- */
-OO.ui.PopuppableElement.prototype.hidePopup = function () {
-       this.popup.hide();
-};
-
-/**
- * Element with a title.
+ * Icon.
  *
- * @abstract
- * @class
+ * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
  *
- * @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 );
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.TitledElement );
-
-/* Static Properties */
-
-/**
- * Title.
+ * 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 {string|Function} Title text or a function that returns text
+ * @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.TitledElement.static.title = null;
+OO.ui.IconedElement.static.icon = null;
 
 /* Methods */
 
 /**
- * Set title.
+ * Set icon.
  *
- * @param {string|Function|null} title Title text, a function that returns text or null for no title
+ * @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.TitledElement.prototype.setTitle = function ( title ) {
-       this.title = title = OO.ui.resolveMsg( title ) || null;
+OO.ui.IconedElement.prototype.setIcon = function ( icon ) {
+       icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
 
-       if ( typeof title === 'string' && title.length ) {
-               this.$titled.attr( 'title', title );
-       } else {
-               this.$titled.removeAttr( 'title' );
+       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 title.
+ * Get icon.
  *
- * @return {string} Title string
+ * @return {string} Icon
  */
-OO.ui.TitledElement.prototype.getTitle = function () {
-       return this.title;
+OO.ui.IconedElement.prototype.getIcon = function () {
+       return this.icon;
 };
 
 /**
- * Generic toolbar tool.
+ * Element containing an indicator.
  *
  * @abstract
  * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.IconedElement
  *
  * @constructor
- * @param {OO.ui.ToolGroup} toolGroup
+ * @param {jQuery} $indicator Indicator node, assigned to #$indicator
  * @param {Object} [config] Configuration options
- * @cfg {string|Function} [title] Title text or a function that returns text
+ * @cfg {string} [indicator] Symbolic indicator name
+ * @cfg {string} [indicatorTitle] Indicator title text or a function that return text
  */
-OO.ui.Tool = function OoUiTool( toolGroup, config ) {
+OO.ui.IndicatedElement = function OoUiIndicatedElement( $indicator, config ) {
        // Config intialization
        config = config || {};
 
-       // Parent constructor
-       OO.ui.Tool.super.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' } );
+       this.$indicator = $indicator;
+       this.indicator = null;
+       this.indicatorLabel = null;
 
        // Initialization
-       this.$title.addClass( 'oo-ui-tool-title' );
-       this.$link
-               .addClass( 'oo-ui-tool-link' )
-               .append( this.$icon, this.$title )
-               .prop( 'tabIndex', 0 )
-               .attr( 'role', 'button' );
-       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 );
+       this.$indicator.addClass( 'oo-ui-indicatedElement-indicator' );
+       this.setIndicator( config.indicator || this.constructor.static.indicator );
+       this.setIndicatorTitle( config.indicatorTitle  || this.constructor.static.indicatorTitle );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
-OO.mixinClass( OO.ui.Tool, OO.ui.IconedElement );
-
-/* Events */
-
-/**
- * @event select
- */
+OO.initClass( OO.ui.IndicatedElement );
 
 /* Static Properties */
 
 /**
- * @static
- * @inheritdoc
- */
-OO.ui.Tool.static.tagName = 'span';
-
-/**
- * Symbolic name of tool.
+ * indicator.
  *
- * @abstract
  * @static
  * @inheritable
- * @property {string}
+ * @property {string|null} Symbolic indicator name or null for no indicator
  */
-OO.ui.Tool.static.name = '';
+OO.ui.IndicatedElement.static.indicator = null;
 
 /**
- * Tool group.
+ * Indicator title.
  *
- * @abstract
  * @static
  * @inheritable
- * @property {string}
+ * @property {string|Function|null} Indicator title text, a function that return text or null for no
+ *  indicator title
  */
-OO.ui.Tool.static.group = '';
+OO.ui.IndicatedElement.static.indicatorTitle = null;
 
-/**
- * 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
- * @inheritable
- * @property {string|Function} Title text or a function that returns text
- */
-OO.ui.Tool.static.title = '';
+/* Methods */
 
 /**
- * Tool can be automatically added to catch-all groups.
+ * Set indicator.
  *
- * @static
- * @inheritable
- * @property {boolean}
+ * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
+ * @chainable
  */
-OO.ui.Tool.static.autoAddToCatchall = true;
+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 );
 
-/**
- * Tool can be automatically added to named groups.
- *
- * @static
- * @property {boolean}
- * @inheritable
- */
-OO.ui.Tool.static.autoAddToGroup = true;
+       return this;
+};
 
 /**
- * Check if this tool is compatible with given data.
+ * Set indicator label.
  *
- * @static
- * @inheritable
- * @param {Mixed} data Data to check
- * @return {boolean} Tool can be used with data
+ * @param {string|Function|null} indicator Indicator title text, a function that return text or null
+ *  for no indicator title
+ * @chainable
  */
-OO.ui.Tool.static.isCompatibleWith = function () {
-       return false;
-};
+OO.ui.IndicatedElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
+       this.indicatorTitle = indicatorTitle = OO.ui.resolveMsg( indicatorTitle );
 
-/* Methods */
+       if ( typeof indicatorTitle === 'string' && indicatorTitle.length ) {
+               this.$indicator.attr( 'title', indicatorTitle );
+       } else {
+               this.$indicator.removeAttr( 'title' );
+       }
 
-/**
- * Handle the toolbar state being updated.
- *
- * This is an abstract method that must be overridden in a concrete subclass.
- *
- * @abstract
- */
-OO.ui.Tool.prototype.onUpdateState = function () {
-       throw new Error(
-               'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
-       );
+       return this;
 };
 
 /**
- * Handle the tool being selected.
- *
- * This is an abstract method that must be overridden in a concrete subclass.
+ * Get indicator.
  *
- * @abstract
+ * @return {string} title Symbolic name of indicator
  */
-OO.ui.Tool.prototype.onSelect = function () {
-       throw new Error(
-               'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
-       );
+OO.ui.IndicatedElement.prototype.getIndicator = function () {
+       return this.indicator;
 };
 
 /**
- * Check if the button is active.
+ * Get indicator title.
  *
- * @param {boolean} Button is active
+ * @return {string} Indicator title text
  */
-OO.ui.Tool.prototype.isActive = function () {
-       return this.active;
+OO.ui.IndicatedElement.prototype.getIndicatorTitle = function () {
+       return this.indicatorTitle;
 };
 
 /**
- * Make the button appear active or inactive.
+ * Element containing a label.
  *
- * @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' );
-       }
+ * @abstract
+ * @class
+ *
+ * @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
+ * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
+ */
+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 );
+       this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
 };
 
+/* Setup */
+
+OO.initClass( OO.ui.LabeledElement );
+
+/* Static Properties */
+
 /**
- * Get the tool title.
+ * Label.
  *
- * @param {string|Function} title Title text or a function that returns text
+ * @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.
+ *
+ * An empty string will result in the label being hidden. A string containing only whitespace will
+ * be converted to a single &nbsp;
+ *
+ * @param {jQuery|string|Function|null} label Label nodes; text; a function that retuns nodes or
+ *  text; or null for no label
  * @chainable
  */
-OO.ui.Tool.prototype.setTitle = function ( title ) {
-       this.title = OO.ui.resolveMsg( title );
-       this.updateTitle();
+OO.ui.LabeledElement.prototype.setLabel = function ( label ) {
+       var empty = false;
+
+       this.label = label = OO.ui.resolveMsg( label ) || null;
+       if ( typeof label === 'string' && label.length ) {
+               if ( label.match( /^\s*$/ ) ) {
+                       // Convert whitespace only string to a single non-breaking space
+                       this.$label.html( '&nbsp;' );
+               } else {
+                       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 tool title.
+ * Get the label.
  *
- * @return {string} Title text
+ * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
+ *  text; or null for no label
  */
-OO.ui.Tool.prototype.getTitle = function () {
-       return this.title;
+OO.ui.LabeledElement.prototype.getLabel = function () {
+       return this.label;
 };
 
 /**
- * Get the tool's symbolic name.
+ * Fit the label.
  *
- * @return {string} Symbolic name of tool
+ * @chainable
  */
-OO.ui.Tool.prototype.getName = function () {
-       return this.constructor.static.name;
+OO.ui.LabeledElement.prototype.fitLabel = function () {
+       if ( this.$label.autoEllipsis && this.autoFitLabel ) {
+               this.$label.autoEllipsis( { 'hasSpan': false, 'tooltip': true } );
+       }
+       return this;
 };
 
 /**
- * Update the title.
+ * Popuppable element.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object} [popup] Configuration to pass to popup
+ * @cfg {boolean} [autoClose=true] Popup auto-closes when it loses focus
  */
-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 )
-               );
+OO.ui.PopuppableElement = function OoUiPopuppableElement( config ) {
+       // Configuration initialization
+       config = config || {};
 
-       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' );
-       }
+       // Properties
+       this.popup = new OO.ui.PopupWidget( $.extend(
+               { 'autoClose': true },
+               config.popup,
+               { '$': this.$, '$autoCloseIgnore': this.$element }
+       ) );
 };
 
+/* Methods */
+
 /**
- * Destroy tool.
+ * Get popup.
+ *
+ * @return {OO.ui.PopupWidget} Popup widget
  */
-OO.ui.Tool.prototype.destroy = function () {
-       this.toolbar.disconnect( this );
-       this.$element.remove();
+OO.ui.PopuppableElement.prototype.getPopup = function () {
+       return this.popup;
 };
 
 /**
- * Collection of tool groups.
+ * Element with a title.
  *
+ * @abstract
  * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
- * @mixins OO.ui.GroupElement
  *
  * @constructor
- * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
- * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
+ * @param {jQuery} $label Titled node, assigned to #$titled
  * @param {Object} [config] Configuration options
- * @cfg {boolean} [actions] Add an actions section opposite to the tools
- * @cfg {boolean} [shadow] Add a shadow below the toolbar
+ * @cfg {string|Function} [title] Title text or a function that returns text
  */
-OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
-       // Configuration initialization
+OO.ui.TitledElement = function OoUiTitledElement( $titled, config ) {
+       // Config intialization
        config = config || {};
 
-       // Parent constructor
-       OO.ui.Toolbar.super.call( this, config );
-
-       // Mixin constructors
-       OO.EventEmitter.call( this );
-       OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
-
        // Properties
-       this.toolFactory = toolFactory;
-       this.toolGroupFactory = toolGroupFactory;
-       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 ) );
+       this.$titled = $titled;
+       this.title = null;
 
        // Initialization
-       this.$group.addClass( 'oo-ui-toolbar-tools' );
-       this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group );
-       if ( config.actions ) {
-               this.$actions.addClass( 'oo-ui-toolbar-actions' );
-               this.$bar.append( this.$actions );
-       }
-       this.$bar.append( '<div style="clear:both"></div>' );
-       if ( config.shadow ) {
-               this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
-       }
-       this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
+       this.setTitle( config.title || this.constructor.static.title );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
-OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
-OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
+OO.initClass( OO.ui.TitledElement );
 
-/* Methods */
+/* Static Properties */
 
 /**
- * Get the tool factory.
+ * Title.
  *
- * @return {OO.ui.ToolFactory} Tool factory
+ * @static
+ * @inheritable
+ * @property {string|Function} Title text or a function that returns text
  */
-OO.ui.Toolbar.prototype.getToolFactory = function () {
-       return this.toolFactory;
-};
+OO.ui.TitledElement.static.title = null;
 
-/**
- * Get the tool group factory.
- *
- * @return {OO.Factory} Tool group factory
- */
-OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
-       return this.toolGroupFactory;
-};
+/* Methods */
 
 /**
- * Handles mouse down events.
+ * Set title.
  *
- * @param {jQuery.Event} e Mouse down event
+ * @param {string|Function|null} title Title text, a function that returns text or null for no title
+ * @chainable
  */
-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;
+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;
 };
 
 /**
- * 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.
+ * Get title.
+ *
+ * @return {string} Title string
  */
-OO.ui.Toolbar.prototype.initialize = function () {
-       this.initialized = true;
+OO.ui.TitledElement.prototype.getTitle = function () {
+       return this.title;
 };
 
 /**
- * Setup toolbar.
+ * Generic toolbar tool.
  *
- * Tools can be specified in the following ways:
+ * @abstract
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.IconedElement
  *
- * - 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
- *
- * @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
+ * @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.Toolbar.prototype.setup = function ( groups ) {
-       var i, len, type, group,
-               items = [],
-               defaultType = 'bar';
+OO.ui.Tool = function OoUiTool( toolGroup, config ) {
+       // Config intialization
+       config = config || {};
 
-       // Cleanup previous groups
-       this.reset();
+       // Parent constructor
+       OO.ui.Tool.super.call( this, config );
 
-       // 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';
-                       }
-               }
-               // Check type has been registered
-               type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
-               items.push(
-                       this.getToolGroupFactory().create( type, this, $.extend( { '$': this.$ }, group ) )
-               );
-       }
-       this.addItems( items );
+       // 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 )
+               .prop( 'tabIndex', 0 )
+               .attr( 'role', 'button' );
+       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 );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
+OO.mixinClass( OO.ui.Tool, OO.ui.IconedElement );
+
+/* Events */
+
 /**
- * Remove all tools and groups from the toolbar.
+ * @event select
  */
-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();
-};
+/* Static Properties */
 
 /**
- * Destroys toolbar, removing event handlers and DOM elements.
- *
- * Call this whenever you are done using a toolbar.
+ * @static
+ * @inheritdoc
  */
-OO.ui.Toolbar.prototype.destroy = function () {
-       this.reset();
-       this.$element.remove();
-};
+OO.ui.Tool.static.tagName = 'span';
 
 /**
- * Check if tool has not been used yet.
+ * Symbolic name of tool.
  *
- * @param {string} name Symbolic name of tool
- * @return {boolean} Tool is available
+ * @abstract
+ * @static
+ * @inheritable
+ * @property {string}
  */
-OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
-       return !this.tools[name];
-};
+OO.ui.Tool.static.name = '';
 
 /**
- * Prevent tool from being used again.
+ * Tool group.
  *
- * @param {OO.ui.Tool} tool Tool to reserve
+ * @abstract
+ * @static
+ * @inheritable
+ * @property {string}
  */
-OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
-       this.tools[tool.getName()] = tool;
-};
+OO.ui.Tool.static.group = '';
 
 /**
- * Allow tool to be used again.
+ * Tool title.
  *
- * @param {OO.ui.Tool} tool Tool to release
+ * 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
+ * @inheritable
+ * @property {string|Function} Title text or a function that returns text
  */
-OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
-       delete this.tools[tool.getName()];
-};
+OO.ui.Tool.static.title = '';
 
 /**
- * Get accelerator label for tool.
+ * Tool can be automatically added to catch-all groups.
  *
- * This is a stub that should be overridden to provide access to accelerator information.
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.Tool.static.autoAddToCatchall = true;
+
+/**
+ * Tool can be automatically added to named groups.
  *
- * @param {string} name Symbolic name of tool
- * @return {string|undefined} Tool accelerator label if available
+ * @static
+ * @property {boolean}
+ * @inheritable
  */
-OO.ui.Toolbar.prototype.getToolAccelerator = function () {
-       return undefined;
-};
+OO.ui.Tool.static.autoAddToGroup = true;
 
 /**
- * Factory for tools.
+ * Check if this tool is compatible with given data.
  *
- * @class
- * @extends OO.Factory
- * @constructor
+ * @static
+ * @inheritable
+ * @param {Mixed} data Data to check
+ * @return {boolean} Tool can be used with data
  */
-OO.ui.ToolFactory = function OoUiToolFactory() {
-       // Parent constructor
-       OO.ui.ToolFactory.super.call( this );
+OO.ui.Tool.static.isCompatibleWith = function () {
+       return false;
 };
 
-/* Setup */
-
-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 = {};
+/**
+ * Handle the toolbar state being updated.
+ *
+ * This is an abstract method that must be overridden in a concrete subclass.
+ *
+ * @abstract
+ */
+OO.ui.Tool.prototype.onUpdateState = function () {
+       throw new Error(
+               'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
+       );
+};
 
-       // Collect included and not excluded tools
-       included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
+/**
+ * Handle the tool being selected.
+ *
+ * This is an abstract method that must be overridden in a concrete subclass.
+ *
+ * @abstract
+ */
+OO.ui.Tool.prototype.onSelect = function () {
+       throw new Error(
+               'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
+       );
+};
 
-       // Promotion
-       promoted = this.extract( promote, used );
-       demoted = this.extract( demote, used );
+/**
+ * Check if the button is active.
+ *
+ * @param {boolean} Button is active
+ */
+OO.ui.Tool.prototype.isActive = function () {
+       return this.active;
+};
 
-       // Auto
-       for ( i = 0, len = included.length; i < len; i++ ) {
-               if ( !used[included[i]] ) {
-                       auto.push( included[i] );
-               }
+/**
+ * Make the button appear active or inactive.
+ *
+ * @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' );
        }
-
-       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: `'*'`
+ * Get the tool title.
  *
- * @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
+ * @param {string|Function} title Title text or a function that returns text
+ * @chainable
  */
-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.autoAddToCatchall &&
-                               // Exclude already used tools
-                               ( !used || !used[name] )
-                       ) {
-                               names.push( name );
-                               if ( used ) {
-                                       used[name] = true;
-                               }
-                       }
-               }
-       } else if ( $.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.autoAddToGroup &&
-                                                       // 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;
+OO.ui.Tool.prototype.setTitle = function ( title ) {
+       this.title = OO.ui.resolveMsg( title );
+       this.updateTitle();
+       return this;
 };
 
 /**
- * Collection of tools.
+ * Get the tool title.
  *
- * Tools can be specified in the following ways:
+ * @return {string} Title text
+ */
+OO.ui.Tool.prototype.getTitle = function () {
+       return this.title;
+};
+
+/**
+ * Get the tool's symbolic name.
  *
- * - A specific tool: `{ 'name': 'tool-name' }` or `'tool-name'`
- * - All tools in a group: `{ 'group': 'group-name' }`
- * - All tools: `'*'`
+ * @return {string} Symbolic name of tool
+ */
+OO.ui.Tool.prototype.getName = function () {
+       return this.constructor.static.name;
+};
+
+/**
+ * Update the title.
+ */
+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.
+ */
+OO.ui.Tool.prototype.destroy = function () {
+       this.toolbar.disconnect( this );
+       this.$element.remove();
+};
+
+/**
+ * Collection of tool groups.
  *
- * @abstract
  * @class
- * @extends OO.ui.Widget
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
  * @mixins OO.ui.GroupElement
  *
  * @constructor
- * @param {OO.ui.Toolbar} toolbar
+ * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
+ * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
  * @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
+ * @cfg {boolean} [actions] Add an actions section opposite to the tools
+ * @cfg {boolean} [shadow] Add a shadow below the toolbar
  */
-OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
+OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
        // Configuration initialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.ToolGroup.super.call( this, config );
+       OO.ui.Toolbar.super.call( this, config );
 
        // Mixin constructors
+       OO.EventEmitter.call( this );
        OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
 
        // Properties
-       this.toolbar = toolbar;
+       this.toolFactory = toolFactory;
+       this.toolGroupFactory = toolGroupFactory;
+       this.groups = [];
        this.tools = {};
-       this.pressed = null;
-       this.autoDisabled = false;
-       this.include = config.include || [];
-       this.exclude = config.exclude || [];
-       this.promote = config.promote || [];
-       this.demote = config.demote || [];
-       this.onCapturedMouseUpHandler = OO.ui.bind( this.onCapturedMouseUp, this );
+       this.$bar = this.$( '<div>' );
+       this.$actions = this.$( '<div>' );
+       this.initialized = false;
 
        // 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' } );
-       this.aggregate( { 'disable': 'itemDisable' } );
-       this.connect( this, { 'itemDisable': 'updateDisabled' } );
+       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-toolGroup-tools' );
-       this.$element
-               .addClass( 'oo-ui-toolGroup' )
-               .append( this.$group );
-       this.populate();
+       this.$group.addClass( 'oo-ui-toolbar-tools' );
+       this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group );
+       if ( config.actions ) {
+               this.$actions.addClass( 'oo-ui-toolbar-actions' );
+               this.$bar.append( this.$actions );
+       }
+       this.$bar.append( '<div style="clear:both"></div>' );
+       if ( config.shadow ) {
+               this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
+       }
+       this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
-OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
-
-/* Events */
-
-/**
- * @event update
- */
+OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
+OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
+OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
 
-/* Static Properties */
+/* Methods */
 
 /**
- * Show labels in tooltips.
+ * Get the tool factory.
  *
- * @static
- * @inheritable
- * @property {boolean}
+ * @return {OO.ui.ToolFactory} Tool factory
  */
-OO.ui.ToolGroup.static.titleTooltips = false;
+OO.ui.Toolbar.prototype.getToolFactory = function () {
+       return this.toolFactory;
+};
 
 /**
- * Show acceleration labels in tooltips.
+ * Get the tool group factory.
  *
- * @static
- * @inheritable
- * @property {boolean}
+ * @return {OO.Factory} Tool group factory
  */
-OO.ui.ToolGroup.static.accelTooltips = false;
+OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
+       return this.toolGroupFactory;
+};
 
 /**
- * Automatically disable the toolgroup when all tools are disabled
+ * Handles mouse down events.
  *
- * @static
- * @inheritable
- * @property {boolean}
- */
-OO.ui.ToolGroup.static.autoDisable = true;
-
-/* Methods */
-
-/**
- * @inheritdoc
+ * @param {jQuery.Event} e Mouse down event
  */
-OO.ui.ToolGroup.prototype.isDisabled = function () {
-       return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
+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;
+       }
 };
 
 /**
- * @inheritdoc
+ * 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.ToolGroup.prototype.updateDisabled = function () {
-       var i, item, allDisabled = true;
-
-       if ( this.constructor.static.autoDisable ) {
-               for ( i = this.items.length - 1; i >= 0; i-- ) {
-                       item = this.items[i];
-                       if ( !item.isDisabled() ) {
-                               allDisabled = false;
-                               break;
-                       }
-               }
-               this.autoDisabled = allDisabled;
-       }
-       OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
+OO.ui.Toolbar.prototype.initialize = function () {
+       this.initialized = true;
 };
 
 /**
- * Handle mouse down events.
+ * Setup toolbar.
  *
- * @param {jQuery.Event} e Mouse down event
- */
-OO.ui.ToolGroup.prototype.onMouseDown = function ( e ) {
-       if ( !this.isDisabled() && e.which === 1 ) {
-               this.pressed = this.getTargetTool( e );
+ * 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
+ *
+ * @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 = [],
+               defaultType = 'bar';
+
+       // 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';
+                       }
+               }
+               // Check type has been registered
+               type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
+               items.push(
+                       this.getToolGroupFactory().create( 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
+ * @return {string|undefined} Tool accelerator label if available
+ */
+OO.ui.Toolbar.prototype.getToolAccelerator = function () {
+       return undefined;
+};
+
+/**
+ * Collection of tools.
+ *
+ * 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: `'*'`
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.GroupElement
+ *
+ * @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.ToolGroup.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
+
+       // Properties
+       this.toolbar = toolbar;
+       this.tools = {};
+       this.pressed = null;
+       this.autoDisabled = false;
+       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' } );
+       this.aggregate( { 'disable': 'itemDisable' } );
+       this.connect( this, { 'itemDisable': 'updateDisabled' } );
+
+       // Initialization
+       this.$group.addClass( 'oo-ui-toolGroup-tools' );
+       this.$element
+               .addClass( 'oo-ui-toolGroup' )
+               .append( this.$group );
+       this.populate();
+};
+
+/* Setup */
+
+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
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.ToolGroup.static.titleTooltips = false;
+
+/**
+ * Show acceleration labels in tooltips.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.ToolGroup.static.accelTooltips = false;
+
+/**
+ * Automatically disable the toolgroup when all tools are disabled
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.ToolGroup.static.autoDisable = true;
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ToolGroup.prototype.isDisabled = function () {
+       return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ToolGroup.prototype.updateDisabled = function () {
+       var i, item, allDisabled = true;
+
+       if ( this.constructor.static.autoDisable ) {
+               for ( i = this.items.length - 1; i >= 0; i-- ) {
+                       item = this.items[i];
+                       if ( !item.isDisabled() ) {
+                               allDisabled = false;
+                               break;
+                       }
+               }
+               this.autoDisabled = allDisabled;
+       }
+       OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
+};
+
+/**
+ * Handle mouse down events.
+ *
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.ToolGroup.prototype.onMouseDown = function ( e ) {
+       if ( !this.isDisabled() && e.which === 1 ) {
+               this.pressed = this.getTargetTool( e );
                if ( this.pressed ) {
                        this.pressed.setActive( true );
                        this.getElementDocument().addEventListener(
@@ -3980,367 +5084,440 @@ OO.ui.ToolGroup.prototype.destroy = function () {
 };
 
 /**
- * Factory for tool groups.
+ * Dialog for showing a message.
+ *
+ * User interface:
+ * - Registers two actions by default (safe and primary).
+ * - Renders action widgets in the footer.
  *
  * @class
- * @extends OO.Factory
+ * @extends OO.ui.Dialog
+ *
  * @constructor
+ * @param {Object} [config] Configuration options
  */
-OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
+OO.ui.MessageDialog = function OoUiMessageDialog( manager, config ) {
        // Parent constructor
-       OO.Factory.call( this );
+       OO.ui.MessageDialog.super.call( this, manager, config );
 
-       var i, l,
-               defaultClasses = this.constructor.static.getDefaultClasses();
+       // Properties
+       this.verticalActionLayout = null;
 
-       // Register default toolgroups
-       for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
-               this.register( defaultClasses[i] );
-       }
+       // Initialization
+       this.$element.addClass( 'oo-ui-messageDialog' );
 };
 
-/* Setup */
+/* Inheritance */
 
-OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
+OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
 
-/* Static Methods */
+/* Static Properties */
+
+OO.ui.MessageDialog.static.name = 'message';
+
+OO.ui.MessageDialog.static.size = 'small';
+
+OO.ui.MessageDialog.static.verbose = false;
 
 /**
- * Get a default set of classes to be registered on construction
+ * Dialog title.
  *
- * @return {Function[]} Default classes
+ * A confirmation dialog's title should describe what the progressive action will do. An alert
+ * dialog's title should describe what event occured.
+ *
+ * @static
+ * inheritable
+ * @property {jQuery|string|Function|null}
  */
-OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
-       return [
-               OO.ui.BarToolGroup,
-               OO.ui.ListToolGroup,
-               OO.ui.MenuToolGroup
-       ];
-};
+OO.ui.MessageDialog.static.title = null;
 
 /**
- * Layout made of a fieldset and optional legend.
- *
- * Just add OO.ui.FieldLayout items.
- *
- * @class
- * @extends OO.ui.Layout
- * @mixins OO.ui.LabeledElement
- * @mixins OO.ui.IconedElement
- * @mixins OO.ui.GroupElement
+ * A confirmation dialog's message should describe the consequences of the progressive action. An
+ * alert dialog's message should describe why the event occured.
  *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string} [icon] Symbolic icon name
- * @cfg {OO.ui.FieldLayout[]} [items] Items to add
+ * @static
+ * inheritable
+ * @property {jQuery|string|Function|null}
  */
-OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
-       // Config initialization
-       config = config || {};
+OO.ui.MessageDialog.static.message = null;
 
-       // Parent constructor
-       OO.ui.FieldsetLayout.super.call( this, config );
+OO.ui.MessageDialog.static.actions = [
+       { 'label': OO.ui.deferMsg( 'ooui-dialog-message-accept' ), 'flags': 'primary' },
+       { 'label': OO.ui.deferMsg( 'ooui-dialog-message-reject' ), 'flags': 'safe' }
+];
 
-       // Mixin constructors
-       OO.ui.IconedElement.call( this, this.$( '<div>' ), config );
-       OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
-       OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
+/* Methods */
 
-       // Initialization
-       this.$element
-               .addClass( 'oo-ui-fieldsetLayout' )
-               .prepend( this.$icon, this.$label, this.$group );
-       if ( $.isArray( config.items ) ) {
-               this.addItems( config.items );
-       }
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
+       this.fitActions();
+       return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
-OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabeledElement );
-OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
-
-/* Static Properties */
-
-OO.ui.FieldsetLayout.static.tagName = 'div';
-
 /**
- * Layout made of a field and optional label.
- *
- * @class
- * @extends OO.ui.Layout
- * @mixins OO.ui.LabeledElement
- *
- * Available label alignment modes include:
- *  - 'left': Label is before the field and aligned away from it, best for when the user will be
- *    scanning for a specific label in a form with many fields
- *  - 'right': Label is before the field and aligned toward it, best for forms the user is very
- *    familiar with and will tab through field checking quickly to verify which field they are in
- *  - 'top': Label is before the field and above it, best for when the use will need to fill out all
- *    fields from top to bottom in a form with few fields
- *  - 'inline': Label is after the field and aligned toward it, best for small boolean fields like
- *    checkboxes or radio buttons
+ * Toggle action layout between vertical and horizontal.
  *
- * @constructor
- * @param {OO.ui.Widget} field Field widget
- * @param {Object} [config] Configuration options
- * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
+ * @param {boolean} [value] Layout actions vertically, omit to toggle
+ * @chainable
  */
-OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) {
-       // Config initialization
-       config = $.extend( { 'align': 'left' }, config );
-
-       // Parent constructor
-       OO.ui.FieldLayout.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.LabeledElement.call( this, this.$( '<label>' ), config );
+OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
+       value = value === undefined ? !this.verticalActionLayout : !!value;
 
-       // Properties
-       this.$field = this.$( '<div>' );
-       this.field = field;
-       this.align = null;
-
-       // Events
-       if ( this.field instanceof OO.ui.InputWidget ) {
-               this.$label.on( 'click', OO.ui.bind( this.onLabelClick, this ) );
+       if ( value !== this.verticalActionLayout ) {
+               this.verticalActionLayout = value;
+               this.$actions
+                       .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
+                       .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
        }
-       this.field.connect( this, { 'disable': 'onFieldDisable' } );
 
-       // Initialization
-       this.$element.addClass( 'oo-ui-fieldLayout' );
-       this.$field
-               .addClass( 'oo-ui-fieldLayout-field' )
-               .toggleClass( 'oo-ui-fieldLayout-disable', this.field.isDisabled() )
-               .append( this.field.$element );
-       this.setAlignment( config.align );
+       return this;
 };
 
-/* Setup */
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
+       if ( action ) {
+               return new OO.ui.Process( function () {
+                       this.close( { 'action': action } );
+               }, this );
+       }
+       return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action );
+};
 
-OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
-OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabeledElement );
+/**
+ * @inheritdoc
+ *
+ * @param {Object} [data] Dialog opening data
+ * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
+ * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
+ * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
+ * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
+ *   action item
+ */
+OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
+       data = data || {};
 
-/* Methods */
+       // Parent method
+       return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data )
+               .next( function () {
+                       this.title.setLabel(
+                               data.title !== undefined ? data.title : this.constructor.static.title
+                       );
+                       this.message.setLabel(
+                               data.message !== undefined ? data.message : this.constructor.static.message
+                       );
+                       this.message.$element.toggleClass(
+                               'oo-ui-messageDialog-message-verbose',
+                               data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
+                       );
+               }, this );
+};
 
 /**
- * Handle field disable events.
- *
- * @param {boolean} value Field is disabled
+ * @inheritdoc
  */
-OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
-       this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
+OO.ui.MessageDialog.prototype.getBodyHeight = function () {
+       return Math.round( this.text.$element.outerHeight( true ) );
 };
 
 /**
- * Handle label mouse click events.
- *
- * @param {jQuery.Event} e Mouse click event
+ * @inheritdoc
  */
-OO.ui.FieldLayout.prototype.onLabelClick = function () {
-       this.field.simulateLabelClick();
-       return false;
+OO.ui.MessageDialog.prototype.initialize = function () {
+       // Parent method
+       OO.ui.MessageDialog.super.prototype.initialize.call( this );
+
+       // Properties
+       this.$actions = this.$( '<div>' );
+       this.container = new OO.ui.PanelLayout( {
+               '$': this.$, 'scrollable': true, 'classes': [ 'oo-ui-messageDialog-container' ]
+       } );
+       this.text = new OO.ui.PanelLayout( {
+               '$': this.$, 'padded': true, 'classes': [ 'oo-ui-messageDialog-text' ]
+       } );
+       this.message = new OO.ui.LabelWidget( {
+               '$': this.$, 'classes': [ 'oo-ui-messageDialog-message' ]
+       } );
+
+       // Initialization
+       this.title.$element.addClass( 'oo-ui-messageDialog-title' );
+       this.frame.$content.addClass( 'oo-ui-messageDialog-content' );
+       this.container.$element.append( this.text.$element );
+       this.text.$element.append( this.title.$element, this.message.$element );
+       this.$body.append( this.container.$element );
+       this.$actions.addClass( 'oo-ui-messageDialog-actions' );
+       this.$foot.append( this.$actions );
 };
 
 /**
- * Get the field.
- *
- * @return {OO.ui.Widget} Field widget
+ * @inheritdoc
  */
-OO.ui.FieldLayout.prototype.getField = function () {
-       return this.field;
+OO.ui.MessageDialog.prototype.attachActions = function () {
+       var i, len, other, special, others;
+
+       // Parent method
+       OO.ui.MessageDialog.super.prototype.attachActions.call( this );
+
+       special = this.actions.getSpecial();
+       others = this.actions.getOthers();
+       if ( special.safe ) {
+               this.$actions.append( special.safe.$element );
+               special.safe.toggleFramed( false );
+       }
+       if ( others.length ) {
+               for ( i = 0, len = others.length; i < len; i++ ) {
+                       other = others[i];
+                       this.$actions.append( other.$element );
+                       other.toggleFramed( false );
+               }
+       }
+       if ( special.primary ) {
+               this.$actions.append( special.primary.$element );
+               special.primary.toggleFramed( false );
+       }
+
+       this.fitActions();
+       if ( !this.isOpening() ) {
+               this.manager.updateWindowSize( this );
+       }
+       this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
 };
 
 /**
- * Set the field alignment mode.
+ * Fit action actions into columns or rows.
  *
- * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
- * @chainable
+ * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
  */
-OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
-       if ( value !== this.align ) {
-               // Default to 'left'
-               if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
-                       value = 'left';
-               }
-               // Reorder elements
-               if ( value === 'inline' ) {
-                       this.$element.append( this.$field, this.$label );
-               } else {
-                       this.$element.append( this.$label, this.$field );
-               }
-               // Set classes
-               if ( this.align ) {
-                       this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
+OO.ui.MessageDialog.prototype.fitActions = function () {
+       var i, len, action,
+               actions = this.actions.get();
+
+       // Detect clipping
+       this.toggleVerticalActionLayout( false );
+       for ( i = 0, len = actions.length; i < len; i++ ) {
+               action = actions[i];
+               if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
+                       this.toggleVerticalActionLayout( true );
+                       break;
                }
-               this.align = value;
-               this.$element.addClass( 'oo-ui-fieldLayout-align-' + this.align );
        }
-
-       return this;
 };
 
 /**
- * Layout made of proportionally sized columns and rows.
+ * Navigation dialog window.
+ *
+ * Logic:
+ * - Show and hide errors.
+ * - Retry an action.
+ *
+ * User interface:
+ * - Renders header with dialog title and one action widget on either side
+ *   (a 'safe' button on the left, and a 'primary' button on the right, both of
+ *   which close the dialog).
+ * - Displays any action widgets in the footer (none by default).
+ * - Ability to dismiss errors.
  *
+ * Subclass responsibilities:
+ * - Register a 'safe' action.
+ * - Register a 'primary' action.
+ * - Add content to the dialog.
+ *
+ * @abstract
  * @class
- * @extends OO.ui.Layout
+ * @extends OO.ui.Dialog
  *
  * @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 || {};
-
+OO.ui.ProcessDialog = function OoUiProcessDialog( manager, config ) {
        // Parent constructor
-       OO.ui.GridLayout.super.call( this, config );
-
-       // Properties
-       this.panels = [];
-       this.widths = [];
-       this.heights = [];
+       OO.ui.ProcessDialog.super.call( this, manager, config );
 
        // 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 ] );
-       }
+       this.$element.addClass( 'oo-ui-processDialog' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
+OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
 
-/* Events */
+/* Methods */
 
 /**
- * @event layout
+ * Handle dismiss button click events.
+ *
+ * Hides errors.
  */
+OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
+       this.hideErrors();
+};
 
 /**
- * @event update
+ * Handle retry button click events.
+ *
+ * Hides errors and then tries again.
  */
+OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
+       this.hideErrors();
+       this.executeAction( this.currentAction.getAction() );
+};
 
-/* Static Properties */
-
-OO.ui.GridLayout.static.tagName = 'div';
-
-/* Methods */
+/**
+ * @inheritdoc
+ */
+OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
+       if ( this.actions.isSpecial( action ) ) {
+               this.fitLabel();
+       }
+       return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
+};
 
 /**
- * Set grid dimensions.
- *
- * @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
+ * @inheritdoc
  */
-OO.ui.GridLayout.prototype.layout = function ( widths, heights ) {
-       var x, y,
-               xd = 0,
-               yd = 0,
-               cols = widths.length,
-               rows = heights.length;
+OO.ui.ProcessDialog.prototype.initialize = function () {
+       // Parent method
+       OO.ui.ProcessDialog.super.prototype.initialize.call( this );
 
-       // 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' );
-       }
+       // Properties
+       this.$navigation = this.$( '<div>' );
+       this.$location = this.$( '<div>' );
+       this.$safeActions = this.$( '<div>' );
+       this.$primaryActions = this.$( '<div>' );
+       this.$otherActions = this.$( '<div>' );
+       this.dismissButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'label': OO.ui.msg( 'ooui-dialog-process-dismiss' )
+       } );
+       this.retryButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'label': OO.ui.msg( 'ooui-dialog-process-retry' )
+       } );
+       this.$errors = this.$( '<div>' );
+       this.$errorsTitle = this.$( '<div>' );
 
-       // 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' );
+       // Events
+       this.dismissButton.connect( this, { 'click': 'onDismissErrorButtonClick' } );
+       this.retryButton.connect( this, { 'click': 'onRetryButtonClick' } );
+
+       // Initialization
+       this.title.$element.addClass( 'oo-ui-processDialog-title' );
+       this.$location
+               .append( this.title.$element )
+               .addClass( 'oo-ui-processDialog-location' );
+       this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
+       this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
+       this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
+       this.$errorsTitle
+               .addClass( 'oo-ui-processDialog-errors-title' )
+               .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
+       this.$errors
+               .addClass( 'oo-ui-processDialog-errors' )
+               .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
+       this.frame.$content
+               .addClass( 'oo-ui-processDialog-content' )
+               .append( this.$errors );
+       this.$navigation
+               .addClass( 'oo-ui-processDialog-navigation' )
+               .append( this.$safeActions, this.$location, this.$primaryActions );
+       this.$head.append( this.$navigation );
+       this.$foot.append( this.$otherActions );
 };
 
 /**
- * Update panel positions and sizes.
- *
- * @fires update
+ * @inheritdoc
  */
-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;
+OO.ui.ProcessDialog.prototype.attachActions = function () {
+       var i, len, other, special, others;
 
-       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;
+       // Parent method
+       OO.ui.ProcessDialog.super.prototype.attachActions.call( this );
+
+       special = this.actions.getSpecial();
+       others = this.actions.getOthers();
+       if ( special.primary ) {
+               this.$primaryActions.append( special.primary.$element );
+               special.primary.toggleFramed( false );
+       }
+       if ( others.length ) {
+               for ( i = 0, len = others.length; i < len; i++ ) {
+                       other = others[i];
+                       this.$otherActions.append( other.$element );
+                       other.toggleFramed( true );
                }
-               top += height;
-               left = 0;
+       }
+       if ( special.safe ) {
+               this.$safeActions.append( special.safe.$element );
+               special.safe.toggleFramed( false );
        }
 
-       this.emit( 'update' );
+       this.fitLabel();
+       this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
 };
 
 /**
- * Get a panel at a given position.
+ * @inheritdoc
+ */
+OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
+       OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action )
+               .fail( OO.ui.bind( this.showErrors, this ) );
+};
+
+/**
+ * Fit label between actions.
  *
- * The x and y position is affected by the current grid layout.
+ * @chainable
+ */
+OO.ui.ProcessDialog.prototype.fitLabel = function () {
+       var width = Math.max(
+               this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0,
+               this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0
+       );
+       this.$location.css( { 'padding-left': width, 'padding-right': width } );
+
+       return this;
+};
+
+/**
+ * Handle errors that occured durring accept or reject processes.
  *
- * @param {number} x Horizontal position
- * @param {number} y Vertical position
- * @return {OO.ui.PanelLayout} The panel at the given postion
+ * @param {OO.ui.Error[]} errors Errors to be handled
  */
-OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
-       return this.panels[( x * this.widths.length ) + y];
+OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
+       var i, len, $item,
+               items = [],
+               recoverable = true;
+
+       for ( i = 0, len = errors.length; i < len; i++ ) {
+               if ( !errors[i].isRecoverable() ) {
+                       recoverable = false;
+               }
+               $item = this.$( '<div>' )
+                       .addClass( 'oo-ui-processDialog-error' )
+                       .append( errors[i].getMessage() );
+               items.push( $item[0] );
+       }
+       this.$errorItems = this.$( items );
+       if ( recoverable ) {
+               this.retryButton.clearFlags().setFlags( this.currentAction.getFlags() );
+       } else {
+               this.currentAction.setDisabled( true );
+       }
+       this.retryButton.toggle( recoverable );
+       this.$errorsTitle.after( this.$errorItems );
+       this.$errors.show().scrollTop( 0 );
+};
+
+/**
+ * Hide errors.
+ */
+OO.ui.ProcessDialog.prototype.hideErrors = function () {
+       this.$errors.hide();
+       this.$errorItems.remove();
+       this.$errorItems = null;
 };
 
 /**
@@ -4420,346 +5597,724 @@ OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
 
 OO.inheritClass( OO.ui.BookletLayout, OO.ui.Layout );
 
-/* Events */
+/* 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.
+ *
+ * @param {jQuery.Event} e Focusin event
+ */
+OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
+       var name, $target;
+
+       // Find the page that an element was focused within
+       $target = $( e.target ).closest( '.oo-ui-pageLayout' );
+       for ( name in this.pages ) {
+               // Check for page match, exclude current page to find only page changes
+               if ( this.pages[name].$element[0] === $target[0] && name !== this.currentPageName ) {
+                       this.setPage( name );
+                       break;
+               }
+       }
+};
+
+/**
+ * Handle stack layout set events.
+ *
+ * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
+ */
+OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
+       var layout = this;
+       if ( page ) {
+               page.scrollElementIntoView( { 'complete': function () {
+                       if ( layout.autoFocus ) {
+                               // Set focus to the first input if nothing on the page is focused yet
+                               if ( !page.$element.find( ':focus' ).length ) {
+                                       page.$element.find( ':input:first' ).focus();
+                               }
+                       }
+               } } );
+       }
+};
+
+/**
+ * Handle outline widget select events.
+ *
+ * @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.
+ *
+ * @return {boolean}
+ */
+OO.ui.BookletLayout.prototype.isOutlined = function () {
+       return this.outlined;
+};
+
+/**
+ * Check if booklet has editing controls.
+ *
+ * @return {boolean}
+ */
+OO.ui.BookletLayout.prototype.isEditable = function () {
+       return this.editable;
+};
+
+/**
+ * Check if booklet has a visible outline.
+ *
+ * @return {boolean}
+ */
+OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
+       return this.outlined && this.outlineVisible;
+};
+
+/**
+ * Hide or show the outline.
+ *
+ * @param {boolean} [show] Show outline, omit to invert current state
+ * @chainable
+ */
+OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
+       if ( this.outlined ) {
+               show = show === undefined ? !this.outlineVisible : !!show;
+               this.outlineVisible = show;
+               this.gridLayout.layout( show ? [ 1, 2 ] : [ 0, 1 ], [ 1 ] );
+       }
+
+       return this;
+};
+
+/**
+ * Get the outline widget.
+ *
+ * @param {OO.ui.PageLayout} page Page to be selected
+ * @return {OO.ui.PageLayout|null} Closest page to another
+ */
+OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
+       var next, prev, level,
+               pages = this.stackLayout.getItems(),
+               index = $.inArray( page, pages );
+
+       if ( index !== -1 ) {
+               next = pages[index + 1];
+               prev = pages[index - 1];
+               // Prefer adjacent pages at the same level
+               if ( this.outlined ) {
+                       level = this.outlineWidget.getItemFromData( page.getName() ).getLevel();
+                       if (
+                               prev &&
+                               level === this.outlineWidget.getItemFromData( prev.getName() ).getLevel()
+                       ) {
+                               return prev;
+                       }
+                       if (
+                               next &&
+                               level === this.outlineWidget.getItemFromData( next.getName() ).getLevel()
+                       ) {
+                               return next;
+                       }
+               }
+       }
+       return prev || next || null;
+};
+
+/**
+ * Get the outline widget.
+ *
+ * @return {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.
+ *
+ * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
+ */
+OO.ui.BookletLayout.prototype.getOutlineControls = function () {
+       return this.outlineControlsWidget;
+};
+
+/**
+ * Get a page by name.
+ *
+ * @param {string} name Symbolic name of page
+ * @return {OO.ui.PageLayout|undefined} Page, if found
+ */
+OO.ui.BookletLayout.prototype.getPage = function ( name ) {
+       return this.pages[name];
+};
+
+/**
+ * Get the current page name.
+ *
+ * @return {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.
+ *
+ * @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, item, currentIndex,
+               stackLayoutPages = this.stackLayout.getItems(),
+               remove = [],
+               items = [];
+
+       // Remove pages with same names
+       for ( i = 0, len = pages.length; i < len; i++ ) {
+               page = pages[i];
+               name = page.getName();
+
+               if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
+                       // Correct the insertion index
+                       currentIndex = $.inArray( this.pages[name], stackLayoutPages );
+                       if ( currentIndex !== -1 && currentIndex + 1 < index ) {
+                               index--;
+                       }
+                       remove.push( this.pages[name] );
+               }
+       }
+       if ( remove.length ) {
+               this.removePages( remove );
+       }
+
+       // Add new pages
+       for ( i = 0, len = pages.length; i < len; i++ ) {
+               page = pages[i];
+               name = page.getName();
+               this.pages[page.getName()] = page;
+               if ( this.outlined ) {
+                       item = new OO.ui.OutlineItemWidget( name, page, { '$': this.$ } );
+                       page.setOutlineItem( item );
+                       items.push( item );
+               }
+       }
+
+       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.
+ *
+ * @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 ) );
+                       page.setOutlineItem( null );
+               }
+       }
+       if ( this.outlined && items.length ) {
+               this.outlineWidget.removeItems( items );
+               this.updateOutlineWidget();
+       }
+       this.stackLayout.removeItems( pages );
+       this.emit( 'remove', pages );
+
+       return this;
+};
 
 /**
- * @event set
- * @param {OO.ui.PageLayout} page Current page
+ * Clear all pages from the layout.
+ *
+ * @fires remove
+ * @chainable
  */
+OO.ui.BookletLayout.prototype.clearPages = function () {
+       var i, len,
+               pages = this.stackLayout.getItems();
 
-/**
- * @event add
- * @param {OO.ui.PageLayout[]} page Added pages
- * @param {number} index Index pages were added at
- */
+       this.pages = {};
+       this.currentPageName = null;
+       if ( this.outlined ) {
+               this.outlineWidget.clearItems();
+               for ( i = 0, len = pages.length; i < len; i++ ) {
+                       pages[i].setOutlineItem( null );
+               }
+       }
+       this.stackLayout.clearItems();
 
-/**
- * @event remove
- * @param {OO.ui.PageLayout[]} pages Removed pages
- */
+       this.emit( 'remove', pages );
 
-/* Methods */
+       return this;
+};
 
 /**
- * Handle stack layout focus.
+ * Set the current page by name.
  *
- * @param {jQuery.Event} e Focusin event
+ * @fires set
+ * @param {string} name Symbolic name of page
  */
-OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
-       var name, $target;
+OO.ui.BookletLayout.prototype.setPage = function ( name ) {
+       var selectedItem,
+               page = this.pages[name];
 
-       // Find the page that an element was focused within
-       $target = $( e.target ).closest( '.oo-ui-pageLayout' );
-       for ( name in this.pages ) {
-               // Check for page match, exclude current page to find only page changes
-               if ( this.pages[name].$element[0] === $target[0] && name !== this.currentPageName ) {
-                       this.setPage( name );
-                       break;
+       if ( name !== this.currentPageName ) {
+               if ( this.outlined ) {
+                       selectedItem = this.outlineWidget.getSelectedItem();
+                       if ( selectedItem && selectedItem.getData() !== name ) {
+                               this.outlineWidget.selectItem( this.outlineWidget.getItemFromData( name ) );
+                       }
+               }
+               if ( page ) {
+                       if ( this.currentPageName && this.pages[this.currentPageName] ) {
+                               this.pages[this.currentPageName].setActive( false );
+                               // Blur anything focused if the next page doesn't have anything focusable - this
+                               // is not needed if the next page has something focusable because once it is focused
+                               // this blur happens automatically
+                               if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
+                                       this.pages[this.currentPageName].$element.find( ':focus' ).blur();
+                               }
+                       }
+                       this.currentPageName = name;
+                       this.stackLayout.setItem( page );
+                       page.setActive( true );
+                       this.emit( 'set', page );
                }
        }
 };
 
 /**
- * Handle stack layout set events.
+ * Call this after adding or removing items from the OutlineWidget.
  *
- * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
+ * @chainable
  */
-OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
-       if ( page ) {
-               page.scrollElementIntoView( { 'complete': OO.ui.bind( function () {
-                       if ( this.autoFocus ) {
-                               // Set focus to the first input if nothing on the page is focused yet
-                               if ( !page.$element.find( ':focus' ).length ) {
-                                       page.$element.find( ':input:first' ).focus();
-                               }
-                       }
-               }, this ) } );
+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;
 };
 
 /**
- * Handle outline widget select events.
+ * Layout made of a field and optional label.
  *
- * @param {OO.ui.OptionWidget|null} item Selected item
+ * @class
+ * @extends OO.ui.Layout
+ * @mixins OO.ui.LabeledElement
+ *
+ * Available label alignment modes include:
+ *  - 'left': Label is before the field and aligned away from it, best for when the user will be
+ *    scanning for a specific label in a form with many fields
+ *  - 'right': Label is before the field and aligned toward it, best for forms the user is very
+ *    familiar with and will tab through field checking quickly to verify which field they are in
+ *  - 'top': Label is before the field and above it, best for when the use will need to fill out all
+ *    fields from top to bottom in a form with few fields
+ *  - 'inline': Label is after the field and aligned toward it, best for small boolean fields like
+ *    checkboxes or radio buttons
+ *
+ * @constructor
+ * @param {OO.ui.Widget} field Field widget
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
  */
-OO.ui.BookletLayout.prototype.onOutlineWidgetSelect = function ( item ) {
-       if ( item ) {
-               this.setPage( item.getData() );
+OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) {
+       // Config initialization
+       config = $.extend( { 'align': 'left' }, config );
+
+       // Parent constructor
+       OO.ui.FieldLayout.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.LabeledElement.call( this, this.$( '<label>' ), config );
+
+       // Properties
+       this.$field = this.$( '<div>' );
+       this.field = field;
+       this.align = null;
+
+       // Events
+       if ( this.field instanceof OO.ui.InputWidget ) {
+               this.$label.on( 'click', OO.ui.bind( this.onLabelClick, this ) );
        }
+       this.field.connect( this, { 'disable': 'onFieldDisable' } );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-fieldLayout' );
+       this.$field
+               .addClass( 'oo-ui-fieldLayout-field' )
+               .toggleClass( 'oo-ui-fieldLayout-disable', this.field.isDisabled() )
+               .append( this.field.$element );
+       this.setAlignment( config.align );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
+OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabeledElement );
+
+/* Methods */
+
 /**
- * Check if booklet has an outline.
+ * Handle field disable events.
  *
- * @return {boolean}
+ * @param {boolean} value Field is disabled
  */
-OO.ui.BookletLayout.prototype.isOutlined = function () {
-       return this.outlined;
+OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
+       this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
 };
 
 /**
- * Check if booklet has editing controls.
+ * Handle label mouse click events.
  *
- * @return {boolean}
+ * @param {jQuery.Event} e Mouse click event
  */
-OO.ui.BookletLayout.prototype.isEditable = function () {
-       return this.editable;
+OO.ui.FieldLayout.prototype.onLabelClick = function () {
+       this.field.simulateLabelClick();
+       return false;
 };
 
 /**
- * Check if booklet has a visible outline.
+ * Get the field.
  *
- * @return {boolean}
+ * @return {OO.ui.Widget} Field widget
  */
-OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
-       return this.outlined && this.outlineVisible;
+OO.ui.FieldLayout.prototype.getField = function () {
+       return this.field;
 };
 
 /**
- * Hide or show the outline.
+ * Set the field alignment mode.
  *
- * @param {boolean} [show] Show outline, omit to invert current state
+ * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
  * @chainable
  */
-OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
-       if ( this.outlined ) {
-               show = show === undefined ? !this.outlineVisible : !!show;
-               this.outlineVisible = show;
-               this.gridLayout.layout( show ? [ 1, 2 ] : [ 0, 1 ], [ 1 ] );
+OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
+       if ( value !== this.align ) {
+               // Default to 'left'
+               if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
+                       value = 'left';
+               }
+               // Reorder elements
+               if ( value === 'inline' ) {
+                       this.$element.append( this.$field, this.$label );
+               } else {
+                       this.$element.append( this.$label, this.$field );
+               }
+               // Set classes
+               if ( this.align ) {
+                       this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
+               }
+               this.align = value;
+               this.$element.addClass( 'oo-ui-fieldLayout-align-' + this.align );
        }
 
        return this;
 };
 
 /**
- * Get the outline widget.
+ * Layout made of a fieldset and optional legend.
  *
- * @param {OO.ui.PageLayout} page Page to be selected
- * @return {OO.ui.PageLayout|null} Closest page to another
+ * Just add OO.ui.FieldLayout items.
+ *
+ * @class
+ * @extends OO.ui.Layout
+ * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.IconedElement
+ * @mixins OO.ui.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [icon] Symbolic icon name
+ * @cfg {OO.ui.FieldLayout[]} [items] Items to add
  */
-OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
-       var next, prev, level,
-               pages = this.stackLayout.getItems(),
-               index = $.inArray( page, pages );
+OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
+       // Config initialization
+       config = config || {};
 
-       if ( index !== -1 ) {
-               next = pages[index + 1];
-               prev = pages[index - 1];
-               // Prefer adjacent pages at the same level
-               if ( this.outlined ) {
-                       level = this.outlineWidget.getItemFromData( page.getName() ).getLevel();
-                       if (
-                               prev &&
-                               level === this.outlineWidget.getItemFromData( prev.getName() ).getLevel()
-                       ) {
-                               return prev;
-                       }
-                       if (
-                               next &&
-                               level === this.outlineWidget.getItemFromData( next.getName() ).getLevel()
-                       ) {
-                               return next;
-                       }
-               }
+       // Parent constructor
+       OO.ui.FieldsetLayout.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.IconedElement.call( this, this.$( '<div>' ), config );
+       OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
+       OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
+
+       // Initialization
+       this.$element
+               .addClass( 'oo-ui-fieldsetLayout' )
+               .prepend( this.$icon, this.$label, this.$group );
+       if ( $.isArray( config.items ) ) {
+               this.addItems( config.items );
        }
-       return prev || next || null;
 };
 
-/**
- * Get the outline widget.
- *
- * @return {OO.ui.OutlineWidget|null} Outline widget, or null if boolet has no outline
- */
-OO.ui.BookletLayout.prototype.getOutline = function () {
-       return this.outlineWidget;
-};
+/* Setup */
+
+OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
+
+/* Static Properties */
+
+OO.ui.FieldsetLayout.static.tagName = 'div';
 
 /**
- * Get the outline controls widget. If the outline is not editable, null is returned.
+ * Form layout.
  *
- * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
+ * @class
+ * @extends OO.ui.Layout
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
  */
-OO.ui.BookletLayout.prototype.getOutlineControls = function () {
-       return this.outlineControlsWidget;
+OO.ui.FormLayout = function OoUiFormLayout( config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.FormLayout.super.call( this, config );
+
+       // Events
+       this.$element.on( 'submit', OO.ui.bind( this.onFormSubmit, this ) );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-formLayout' );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
+
+/* Events */
+
 /**
- * Get a page by name.
- *
- * @param {string} name Symbolic name of page
- * @return {OO.ui.PageLayout|undefined} Page, if found
+ * @event submit
  */
-OO.ui.BookletLayout.prototype.getPage = function ( name ) {
-       return this.pages[name];
-};
+
+/* Static Properties */
+
+OO.ui.FormLayout.static.tagName = 'form';
+
+/* Methods */
 
 /**
- * Get the current page name.
+ * Handle form submit events.
  *
- * @return {string|null} Current page name
+ * @param {jQuery.Event} e Submit event
+ * @fires submit
  */
-OO.ui.BookletLayout.prototype.getPageName = function () {
-       return this.currentPageName;
+OO.ui.FormLayout.prototype.onFormSubmit = function () {
+       this.emit( 'submit' );
+       return false;
 };
 
 /**
- * Add a page to the layout.
+ * Layout made of proportionally sized columns and rows.
  *
- * When pages are added with the same names as existing pages, the existing pages will be
- * automatically removed before the new pages are added.
+ * @class
+ * @extends OO.ui.Layout
  *
- * @param {OO.ui.PageLayout[]} pages Pages to add
- * @param {number} index Index to insert pages after
- * @fires add
- * @chainable
+ * @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.BookletLayout.prototype.addPages = function ( pages, index ) {
-       var i, len, name, page, item, currentIndex,
-               stackLayoutPages = this.stackLayout.getItems(),
-               remove = [],
-               items = [];
+OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
+       var i, len, widths;
 
-       // Remove pages with same names
-       for ( i = 0, len = pages.length; i < len; i++ ) {
-               page = pages[i];
-               name = page.getName();
+       // Config initialization
+       config = config || {};
 
-               if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
-                       // Correct the insertion index
-                       currentIndex = $.inArray( this.pages[name], stackLayoutPages );
-                       if ( currentIndex !== -1 && currentIndex + 1 < index ) {
-                               index--;
-                       }
-                       remove.push( this.pages[name] );
-               }
-       }
-       if ( remove.length ) {
-               this.removePages( remove );
-       }
+       // Parent constructor
+       OO.ui.GridLayout.super.call( this, config );
 
-       // Add new pages
-       for ( i = 0, len = pages.length; i < len; i++ ) {
-               page = pages[i];
-               name = page.getName();
-               this.pages[page.getName()] = page;
-               if ( this.outlined ) {
-                       item = new OO.ui.OutlineItemWidget( name, page, { '$': this.$ } );
-                       page.setOutlineItem( item );
-                       items.push( item );
+       // 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 ] );
        }
+};
 
-       if ( this.outlined && items.length ) {
-               this.outlineWidget.addItems( items, index );
-               this.updateOutlineWidget();
-       }
-       this.stackLayout.addItems( pages, index );
-       this.emit( 'add', pages, index );
+/* Setup */
 
-       return this;
-};
+OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
+
+/* Events */
 
 /**
- * Remove a page from the layout.
- *
- * @fires remove
- * @chainable
+ * @event layout
  */
-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 ) );
-                       page.setOutlineItem( null );
-               }
-       }
-       if ( this.outlined && items.length ) {
-               this.outlineWidget.removeItems( items );
-               this.updateOutlineWidget();
-       }
-       this.stackLayout.removeItems( pages );
-       this.emit( 'remove', pages );
+/**
+ * @event update
+ */
 
-       return this;
-};
+/* Static Properties */
+
+OO.ui.GridLayout.static.tagName = 'div';
+
+/* Methods */
 
 /**
- * Clear all pages from the layout.
+ * Set grid dimensions.
  *
- * @fires remove
- * @chainable
+ * @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.BookletLayout.prototype.clearPages = function () {
-       var i, len,
-               pages = this.stackLayout.getItems();
+OO.ui.GridLayout.prototype.layout = function ( widths, heights ) {
+       var x, y,
+               xd = 0,
+               yd = 0,
+               cols = widths.length,
+               rows = heights.length;
 
-       this.pages = {};
-       this.currentPageName = null;
-       if ( this.outlined ) {
-               this.outlineWidget.clearItems();
-               for ( i = 0, len = pages.length; i < len; i++ ) {
-                       pages[i].setOutlineItem( null );
-               }
+       // 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' );
        }
-       this.stackLayout.clearItems();
 
-       this.emit( 'remove', pages );
-
-       return this;
+       // 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' );
 };
 
 /**
- * Set the current page by name.
+ * Update panel positions and sizes.
  *
- * @fires set
- * @param {string} name Symbolic name of page
+ * @fires update
  */
-OO.ui.BookletLayout.prototype.setPage = function ( name ) {
-       var selectedItem,
-               page = this.pages[name];
-
-       if ( name !== this.currentPageName ) {
-               if ( this.outlined ) {
-                       selectedItem = this.outlineWidget.getSelectedItem();
-                       if ( selectedItem && selectedItem.getData() !== name ) {
-                               this.outlineWidget.selectItem( this.outlineWidget.getItemFromData( name ) );
-                       }
-               }
-               if ( page ) {
-                       if ( this.currentPageName && this.pages[this.currentPageName] ) {
-                               this.pages[this.currentPageName].setActive( false );
-                               // Blur anything focused if the next page doesn't have anything focusable - this
-                               // is not needed if the next page has something focusable because once it is focused
-                               // this blur happens automatically
-                               if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
-                                       this.pages[this.currentPageName].$element.find( ':focus' ).blur();
-                               }
+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++ ) {
+               height = this.heights[y];
+               for ( x = 0; x < cols; x++ ) {
+                       panel = this.panels[i];
+                       width = this.widths[x];
+                       dimensions = {
+                               'width': Math.round( width * 100 ) + '%',
+                               'height': Math.round( height * 100 ) + '%',
+                               'top': Math.round( top * 100 ) + '%',
+                               // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero
+                               'visibility': width === 0 || height === 0 ? 'hidden' : ''
+                       };
+                       // If RTL, reverse:
+                       if ( OO.ui.Element.getDir( this.$.context ) === 'rtl' ) {
+                               dimensions.right = Math.round( left * 100 ) + '%';
+                       } else {
+                               dimensions.left = Math.round( left * 100 ) + '%';
                        }
-                       this.currentPageName = name;
-                       this.stackLayout.setItem( page );
-                       page.setActive( true );
-                       this.emit( 'set', page );
+                       panel.$element.css( dimensions );
+                       i++;
+                       left += width;
                }
+               top += height;
+               left = 0;
        }
+
+       this.emit( 'update' );
 };
 
 /**
- * Call this after adding or removing items from the OutlineWidget.
+ * Get a panel at a given position.
  *
- * @chainable
+ * The x and y position is affected by the current grid layout.
+ *
+ * @param {number} x Horizontal position
+ * @param {number} y Vertical position
+ * @return {OO.ui.PanelLayout} The panel at the given postion
  */
-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;
+OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
+       return this.panels[( x * this.widths.length ) + y];
 };
 
 /**
@@ -4863,13 +6418,33 @@ OO.ui.PageLayout.prototype.getOutlineItem = function () {
 };
 
 /**
- * Get outline item.
+ * Set outline item.
+ *
+ * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
+ *   outline item as desired; this method is called for setting (with an object) and unsetting
+ *   (with null) and overriding methods would have to check the value of `outlineItem` to avoid
+ *   operating on null instead of an OO.ui.OutlineItemWidget object.
  *
  * @param {OO.ui.OutlineItemWidget|null} outlineItem Outline item widget, null to clear
  * @chainable
  */
 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
-       this.outlineItem = outlineItem;
+       this.outlineItem = outlineItem || null;
+       if ( outlineItem ) {
+               this.setupOutlineItem();
+       }
+       return this;
+};
+
+/**
+ * Setup outline item.
+ *
+ * @localdoc Subclasses should override this method to adjust the outline item as desired.
+ *
+ * @param {OO.ui.OutlineItemWidget} outlineItem Outline item widget to setup
+ * @chainable
+ */
+OO.ui.PageLayout.prototype.setupOutlineItem = function () {
        return this;
 };
 
@@ -5154,403 +6729,738 @@ OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
 
 /* Setup */
 
-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 */
+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 */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.PopupToolGroup.prototype.setDisabled = function () {
+       // Parent method
+       OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
+
+       if ( this.isDisabled() && this.isElementAttached() ) {
+               this.setActive( false );
+       }
+};
+
+/**
+ * Handle focus being lost.
+ *
+ * The event is actually generated from a mouseup, so it is not a normal blur event object.
+ *
+ * @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 ) {
+       if ( !this.isDisabled() && e.which === 1 ) {
+               this.setActive( false );
+       }
+       return OO.ui.PopupToolGroup.super.prototype.onMouseUp.call( this, e );
+};
+
+/**
+ * Handle mouse up events.
+ *
+ * @param {jQuery.Event} e Mouse up event
+ */
+OO.ui.PopupToolGroup.prototype.onHandleMouseUp = function () {
+       return false;
+};
+
+/**
+ * Handle mouse down events.
+ *
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.PopupToolGroup.prototype.onHandleMouseDown = function ( e ) {
+       if ( !this.isDisabled() && e.which === 1 ) {
+               this.setActive( !this.active );
+       }
+       return false;
+};
+
+/**
+ * Switch into active mode.
+ *
+ * When active, mouseup events anywhere in the document will trigger deactivation.
+ */
+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
+ * @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.ListToolGroup.super.call( this, toolbar, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-listToolGroup' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
+
+/* Static Properties */
+
+OO.ui.ListToolGroup.static.accelTooltips = true;
+
+OO.ui.ListToolGroup.static.name = 'list';
+
+/**
+ * Drop down menu layout of tools as selectable menu items.
+ *
+ * @class
+ * @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.MenuToolGroup.super.call( this, toolbar, config );
+
+       // Events
+       this.toolbar.connect( this, { 'updateState': 'onUpdateState' } );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-menuToolGroup' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
+
+/* Static Properties */
+
+OO.ui.MenuToolGroup.static.accelTooltips = true;
+
+OO.ui.MenuToolGroup.static.name = 'menu';
+
+/* 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.
+ */
+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( ', ' ) || ' ' );
+};
+
+/**
+ * Tool that shows a popup when selected.
+ *
+ * @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.PopupTool.super.call( this, toolbar, config );
+
+       // Mixin constructors
+       OO.ui.PopuppableElement.call( this, config );
+
+       // Initialization
+       this.$element
+               .addClass( 'oo-ui-popupTool' )
+               .append( this.popup.$element );
+};
+
+/* Setup */
+
+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.PopupToolGroup.prototype.setDisabled = function () {
-       // Parent method
-       OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
-
-       if ( this.isDisabled() && this.isElementAttached() ) {
-               this.setActive( false );
+OO.ui.PopupTool.prototype.onSelect = function () {
+       if ( !this.isDisabled() ) {
+               this.popup.toggle();
        }
+       this.setActive( false );
+       return false;
 };
 
 /**
- * Handle focus being lost.
- *
- * The event is actually generated from a mouseup, so it is not a normal blur event object.
+ * Handle the toolbar state being updated.
  *
- * @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 ) {
-       if ( !this.isDisabled() && e.which === 1 ) {
-               this.setActive( false );
-       }
-       return OO.ui.PopupToolGroup.super.prototype.onMouseUp.call( this, e );
+OO.ui.PopupTool.prototype.onUpdateState = function () {
+       this.setActive( false );
 };
 
 /**
- * Handle mouse up events.
+ * Group widget.
  *
- * @param {jQuery.Event} e Mouse up event
- */
-OO.ui.PopupToolGroup.prototype.onHandleMouseUp = function () {
-       return false;
-};
-
-/**
- * Handle mouse down events.
+ * Mixin for OO.ui.Widget subclasses.
  *
- * @param {jQuery.Event} e Mouse down event
+ * Use together with OO.ui.ItemWidget to make disabled state inheritable.
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.GroupElement
+ *
+ * @constructor
+ * @param {jQuery} $group Container node, assigned to #$group
+ * @param {Object} [config] Configuration options
  */
-OO.ui.PopupToolGroup.prototype.onHandleMouseDown = function ( e ) {
-       if ( !this.isDisabled() && e.which === 1 ) {
-               this.setActive( !this.active );
-       }
-       return false;
+OO.ui.GroupWidget = function OoUiGroupWidget( $element, config ) {
+       // Parent constructor
+       OO.ui.GroupWidget.super.call( this, $element, config );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
+
+/* Methods */
+
 /**
- * Switch into active mode.
+ * Set the disabled state of the widget.
  *
- * When active, mouseup events anywhere in the document will trigger deactivation.
+ * This will also update the disabled state of child widgets.
+ *
+ * @param {boolean} disabled Disable widget
+ * @chainable
  */
-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 );
+OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
+       var i, len;
+
+       // Parent method
+       // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
+       OO.ui.Widget.prototype.setDisabled.call( this, disabled );
+
+       // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
+       if ( this.items ) {
+               for ( i = 0, len = this.items.length; i < len; i++ ) {
+                       this.items[i].updateDisabled();
                }
        }
+
+       return this;
 };
 
 /**
- * Drop down list layout of tools as labeled icon buttons.
+ * Item widget.
+ *
+ * Use together with OO.ui.GroupWidget to make disabled state inheritable.
  *
+ * @abstract
  * @class
- * @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.ListToolGroup.super.call( this, toolbar, config );
-
-       // Initialization
-       this.$element.addClass( 'oo-ui-listToolGroup' );
+OO.ui.ItemWidget = function OoUiItemWidget() {
+       //
 };
 
-/* Setup */
+/* Methods */
 
-OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
+/**
+ * Check if widget is disabled.
+ *
+ * Checks parent if present, making disabled state inheritable.
+ *
+ * @return {boolean} Widget is disabled
+ */
+OO.ui.ItemWidget.prototype.isDisabled = function () {
+       return this.disabled ||
+               ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
+};
 
-/* Static Properties */
+/**
+ * Set group element is in.
+ *
+ * @param {OO.ui.GroupElement|null} group Group element, null if none
+ * @chainable
+ */
+OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
+       // Parent method
+       // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
+       OO.ui.Element.prototype.setElementGroup.call( this, group );
 
-OO.ui.ListToolGroup.static.accelTooltips = true;
+       // Initialize item disabled states
+       this.updateDisabled();
 
-OO.ui.ListToolGroup.static.name = 'list';
+       return this;
+};
 
 /**
- * Drop down menu layout of tools as selectable menu items.
+ * Lookup input widget.
+ *
+ * Mixin that adds a menu showing suggested values to a text input. Subclasses must handle `select`
+ * and `choose` events on #lookupMenu to make use of selections.
  *
  * @class
- * @extends OO.ui.PopupToolGroup
+ * @abstract
  *
  * @constructor
- * @param {OO.ui.Toolbar} toolbar
+ * @param {OO.ui.TextInputWidget} input Input widget
  * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$overlay=this.$( 'body' )] Overlay layer
  */
-OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
-       // Configuration initialization
+OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
+       // Config intialization
        config = config || {};
 
-       // Parent constructor
-       OO.ui.MenuToolGroup.super.call( this, toolbar, 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.toolbar.connect( this, { 'updateState': 'onUpdateState' } );
+       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-menuToolGroup' );
+       this.$element.addClass( 'oo-ui-lookupWidget' );
+       this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' );
 };
 
-/* Setup */
+/* Methods */
 
-OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
+/**
+ * Handle input focus event.
+ *
+ * @param {jQuery.Event} e Input focus event
+ */
+OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
+       this.openLookupMenu();
+};
 
-/* Static Properties */
+/**
+ * Handle input blur event.
+ *
+ * @param {jQuery.Event} e Input blur event
+ */
+OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
+       this.lookupMenu.toggle( false );
+};
 
-OO.ui.MenuToolGroup.static.accelTooltips = true;
+/**
+ * Handle input mouse down event.
+ *
+ * @param {jQuery.Event} e Input mouse down event
+ */
+OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
+       this.openLookupMenu();
+};
 
-OO.ui.MenuToolGroup.static.name = 'menu';
+/**
+ * Handle input change event.
+ *
+ * @param {string} value New input value
+ */
+OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
+       this.openLookupMenu();
+};
+
+/**
+ * Get lookup menu.
+ *
+ * @return {OO.ui.TextInputMenuWidget}
+ */
+OO.ui.LookupInputWidget.prototype.getLookupMenu = function () {
+       return this.lookupMenu;
+};
+
+/**
+ * Open the menu.
+ *
+ * @chainable
+ */
+OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
+       var value = this.lookupInput.getValue();
+
+       if ( this.lookupMenu.$input.is( ':focus' ) && $.trim( value ) !== '' ) {
+               this.populateLookupMenu();
+               this.lookupMenu.toggle( true );
+       } else {
+               this.lookupMenu
+                       .clearItems()
+                       .toggle( false );
+       }
 
-/* Methods */
+       return this;
+};
 
 /**
- * Handle the toolbar state being updated.
+ * Populate lookup menu with current information.
  *
- * 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.
+ * @chainable
  */
-OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
-       var name,
-               labelTexts = [];
+OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
+       var widget = this;
 
-       for ( name in this.tools ) {
-               if ( this.tools[name].isActive() ) {
-                       labelTexts.push( this.tools[name].getTitle() );
-               }
+       if ( !this.populating ) {
+               this.populating = true;
+               this.getLookupMenuItems()
+                       .done( function ( items ) {
+                               widget.lookupMenu.clearItems();
+                               if ( items.length ) {
+                                       widget.lookupMenu
+                                               .addItems( items )
+                                               .toggle( true );
+                                       widget.initializeLookupMenuSelection();
+                                       widget.openLookupMenu();
+                               } else {
+                                       widget.lookupMenu.toggle( true );
+                               }
+                               widget.populating = false;
+                       } )
+                       .fail( function () {
+                               widget.lookupMenu.clearItems();
+                               widget.populating = false;
+                       } );
        }
 
-       this.setLabel( labelTexts.join( ', ' ) || ' ' );
+       return this;
 };
 
 /**
- * Tool that shows a popup when selected.
- *
- * @abstract
- * @class
- * @extends OO.ui.Tool
- * @mixins OO.ui.PopuppableElement
+ * Set selection in the lookup menu with current information.
  *
- * @constructor
- * @param {OO.ui.Toolbar} toolbar
- * @param {Object} [config] Configuration options
+ * @chainable
  */
-OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) {
-       // Parent constructor
-       OO.ui.PopupTool.super.call( this, toolbar, config );
-
-       // Mixin constructors
-       OO.ui.PopuppableElement.call( this, config );
-
-       // Initialization
-       this.$element
-               .addClass( 'oo-ui-popupTool' )
-               .append( this.popup.$element );
+OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () {
+       if ( !this.lookupMenu.getSelectedItem() ) {
+               this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
+       }
+       this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
-OO.mixinClass( OO.ui.PopupTool, OO.ui.PopuppableElement );
-
-/* Methods */
-
 /**
- * Handle the tool being selected.
+ * Get lookup menu items for the current query.
  *
- * @inheritdoc
+ * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument
+ * of the done event
  */
-OO.ui.PopupTool.prototype.onSelect = function () {
-       if ( !this.isDisabled() ) {
-               if ( this.popup.isVisible() ) {
-                       this.hidePopup();
+OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
+       var widget = this,
+               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.showPopup();
+                       this.lookupQuery = value;
+                       this.lookupRequest = this.getLookupRequest()
+                               .always( function () {
+                                       widget.lookupQuery = null;
+                                       widget.lookupRequest = null;
+                               } )
+                               .done( function ( data ) {
+                                       widget.lookupCache[value] = widget.getLookupCacheItemFromData( data );
+                                       deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[value] ) );
+                               } )
+                               .fail( function () {
+                                       deferred.reject();
+                               } );
+                       this.pushPending();
+                       this.lookupRequest.always( function () {
+                               widget.popPending();
+                       } );
                }
        }
-       this.setActive( false );
-       return false;
+       return deferred.promise();
 };
 
 /**
- * Handle the toolbar state being updated.
+ * Get a new request object of the current lookup query value.
  *
- * @inheritdoc
+ * @abstract
+ * @return {jqXHR} jQuery AJAX object, or promise object with an .abort() method
  */
-OO.ui.PopupTool.prototype.onUpdateState = function () {
-       this.setActive( false );
+OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
+       // Stub, implemented in subclass
+       return null;
 };
 
 /**
- * Group widget.
- *
- * Mixin for OO.ui.Widget subclasses.
+ * Handle successful lookup request.
  *
- * Use together with OO.ui.ItemWidget to make disabled state inheritable.
+ * 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.
  *
  * @abstract
- * @class
- * @extends OO.ui.GroupElement
- *
- * @constructor
- * @param {jQuery} $group Container node, assigned to #$group
- * @param {Object} [config] Configuration options
+ * @param {Mixed} data Response from server
  */
-OO.ui.GroupWidget = function OoUiGroupWidget( $element, config ) {
-       // Parent constructor
-       OO.ui.GroupWidget.super.call( this, $element, config );
+OO.ui.LookupInputWidget.prototype.onLookupRequestDone = function () {
+       // Stub, implemented in subclass
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
-
-/* Methods */
-
 /**
- * Set the disabled state of the widget.
- *
- * This will also update the disabled state of child widgets.
+ * Get a list of menu item widgets from the data stored by the lookup request's done handler.
  *
- * @param {boolean} disabled Disable widget
- * @chainable
+ * @abstract
+ * @param {Mixed} data Cached result data, usually an array
+ * @return {OO.ui.MenuItemWidget[]} Menu items
  */
-OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
-       var i, len;
-
-       // Parent method
-       // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
-       OO.ui.Widget.prototype.setDisabled.call( this, disabled );
-
-       // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
-       if ( this.items ) {
-               for ( i = 0, len = this.items.length; i < len; i++ ) {
-                       this.items[i].updateDisabled();
-               }
-       }
-
-       return this;
+OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
+       // Stub, implemented in subclass
+       return [];
 };
 
 /**
- * Item widget.
+ * Creates an OO.ui.OutlineControlsWidget object.
  *
- * Use together with OO.ui.GroupWidget to make disabled state inheritable.
+ * Use together with OO.ui.OutlineWidget.js
  *
- * @abstract
  * @class
  *
  * @constructor
+ * @param {OO.ui.OutlineWidget} outline Outline to control
+ * @param {Object} [config] Configuration options
  */
-OO.ui.ItemWidget = function OoUiItemWidget() {
-       //
+OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
+       // Configuration initialization
+       config = $.extend( { 'icon': 'add-item' }, config );
+
+       // Parent constructor
+       OO.ui.OutlineControlsWidget.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
+       OO.ui.IconedElement.call( this, this.$( '<div>' ), config );
+
+       // Properties
+       this.outline = outline;
+       this.$movers = this.$( '<div>' );
+       this.upButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'framed': false,
+               'icon': 'collapse',
+               'title': OO.ui.msg( 'ooui-outline-control-move-up' )
+       } );
+       this.downButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'framed': false,
+               'icon': 'expand',
+               'title': OO.ui.msg( 'ooui-outline-control-move-down' )
+       } );
+       this.removeButton = new OO.ui.ButtonWidget( {
+               '$': this.$,
+               'framed': false,
+               'icon': 'remove',
+               'title': OO.ui.msg( 'ooui-outline-control-remove' )
+       } );
+
+       // 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 ] } );
+       this.removeButton.connect( this, { 'click': [ 'emit', 'remove' ] } );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-outlineControlsWidget' );
+       this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
+       this.$movers
+               .addClass( 'oo-ui-outlineControlsWidget-movers' )
+               .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
+       this.$element.append( this.$icon, this.$group, this.$movers );
 };
 
-/* Methods */
+/* Setup */
+
+OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
+OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconedElement );
+
+/* Events */
 
 /**
- * Check if widget is disabled.
- *
- * Checks parent if present, making disabled state inheritable.
- *
- * @return {boolean} Widget is disabled
+ * @event move
+ * @param {number} places Number of places to move
  */
-OO.ui.ItemWidget.prototype.isDisabled = function () {
-       return this.disabled ||
-               ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
-};
 
 /**
- * Set group element is in.
- *
- * @param {OO.ui.GroupElement|null} group Group element, null if none
- * @chainable
+ * @event remove
  */
-OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
-       // Parent method
-       // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
-       OO.ui.Element.prototype.setElementGroup.call( this, group );
 
-       // Initialize item disabled states
-       this.updateDisabled();
+/* Methods */
 
-       return this;
+/**
+ * Handle outline change events.
+ */
+OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
+       var i, len, firstMovable, lastMovable,
+               items = this.outline.getItems(),
+               selectedItem = this.outline.getSelectedItem(),
+               movable = selectedItem && selectedItem.isMovable(),
+               removable = selectedItem && selectedItem.isRemovable();
+
+       if ( movable ) {
+               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 );
+       this.removeButton.setDisabled( !removable );
 };
 
 /**
- * Icon widget.
+ * Width with on and off states.
+ *
+ * Mixin for widgets with a boolean state.
  *
+ * @abstract
  * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.IconedElement
- * @mixins OO.ui.TitledElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
+ * @cfg {boolean} [value=false] Initial value
  */
-OO.ui.IconWidget = function OoUiIconWidget( config ) {
-       // Config intialization
+OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
+       // Configuration initialization
        config = config || {};
 
-       // Parent constructor
-       OO.ui.IconWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.IconedElement.call( this, this.$element, config );
-       OO.ui.TitledElement.call( this, this.$element, config );
+       // Properties
+       this.value = null;
 
        // Initialization
-       this.$element.addClass( 'oo-ui-iconWidget' );
+       this.$element.addClass( 'oo-ui-toggleWidget' );
+       this.setValue( !!config.value );
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.IconWidget, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
+/* Events */
 
-/* Static Properties */
+/**
+ * @event change
+ * @param {boolean} value Changed value
+ */
 
-OO.ui.IconWidget.static.tagName = 'span';
+/* Methods */
 
 /**
- * Indicator widget.
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.IndicatedElement
- * @mixins OO.ui.TitledElement
+ * Get the value of the toggle.
  *
- * @constructor
- * @param {Object} [config] Configuration options
+ * @return {boolean}
  */
-OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
-       // Config intialization
-       config = config || {};
-
-       // Parent constructor
-       OO.ui.IndicatorWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.IndicatedElement.call( this, this.$element, config );
-       OO.ui.TitledElement.call( this, this.$element, config );
-
-       // Initialization
-       this.$element.addClass( 'oo-ui-indicatorWidget' );
+OO.ui.ToggleWidget.prototype.getValue = function () {
+       return this.value;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatedElement );
-OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
-
-/* Static Properties */
-
-OO.ui.IndicatorWidget.static.tagName = 'span';
+/**
+ * Set the value of the toggle.
+ *
+ * @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;
+};
 
 /**
  * Container for multiple related buttons.
@@ -5598,7 +7508,6 @@ OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
  *
  * @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
  */
@@ -5618,7 +7527,9 @@ OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
        OO.ui.FlaggableElement.call( this, config );
 
        // Properties
-       this.isHyperlink = typeof config.href === 'string';
+       this.href = null;
+       this.target = null;
+       this.isHyperlink = false;
 
        // Events
        this.$button.on( {
@@ -5627,12 +7538,12 @@ OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
        } );
 
        // Initialization
-       this.$button
-               .append( this.$icon, this.$label, this.$indicator )
-               .attr( { 'href': config.href, 'target': config.target } );
+       this.$button.append( this.$icon, this.$label, this.$indicator );
        this.$element
                .addClass( 'oo-ui-buttonWidget' )
                .append( this.$button );
+       this.setHref( config.href );
+       this.setTarget( config.target );
 };
 
 /* Setup */
@@ -5656,1877 +7567,1418 @@ OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggableElement );
 /**
  * Handles mouse click events.
  *
- * @param {jQuery.Event} e Mouse click event
- * @fires click
- */
-OO.ui.ButtonWidget.prototype.onClick = function () {
-       if ( !this.isDisabled() ) {
-               this.emit( 'click' );
-               if ( this.isHyperlink ) {
-                       return true;
-               }
-       }
-       return false;
-};
-
-/**
- * Handles keypress events.
- *
- * @param {jQuery.Event} e Keypress event
- * @fires click
- */
-OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
-       if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
-               this.onClick();
-               if ( this.isHyperlink ) {
-                       return true;
-               }
-       }
-       return false;
-};
-
-/**
- * Input widget.
- *
- * @abstract
- * @class
- * @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.InputWidget.super.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.isDisabled() );
-       this.setReadOnly( config.readOnly );
-       this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input );
-       this.setValue( config.value );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
-
-/* Events */
-
-/**
- * @event change
- * @param value
- */
-
-/* Methods */
-
-/**
- * Get input element.
- *
- * @param {Object} [config] Configuration options
- * @return {jQuery} Input element
- */
-OO.ui.InputWidget.prototype.getInputElement = function () {
-       return this.$( '<input>' );
-};
-
-/**
- * Handle potentially value-changing events.
- *
- * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
- */
-OO.ui.InputWidget.prototype.onEdit = function () {
-       if ( !this.isDisabled() ) {
-               // 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.
- *
- * @return {string} Input value
- */
-OO.ui.InputWidget.prototype.getValue = function () {
-       return this.value;
-};
-
-/**
- * Sets the direction of the current input, either RTL or LTR
- *
- * @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.
- *
- * @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.
- *
- * @param {string} value Original value
- * @return {string} Sanitized value
+ * @param {jQuery.Event} e Mouse click event
+ * @fires click
  */
-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 );
+OO.ui.ButtonWidget.prototype.onClick = function () {
+       if ( !this.isDisabled() ) {
+               this.emit( 'click' );
+               if ( this.isHyperlink ) {
+                       return true;
+               }
        }
+       return false;
 };
 
 /**
- * Simulate the behavior of clicking on a label bound to this input.
+ * Handles keypress events.
+ *
+ * @param {jQuery.Event} e Keypress event
+ * @fires click
  */
-OO.ui.InputWidget.prototype.simulateLabelClick = function () {
-       if ( !this.isDisabled() ) {
-               if ( this.$input.is( ':checkbox,:radio' ) ) {
-                       this.$input.click();
-               } else if ( this.$input.is( ':input' ) ) {
-                       this.$input.focus();
+OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
+       if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
+               this.onClick();
+               if ( this.isHyperlink ) {
+                       return true;
                }
        }
+       return false;
 };
 
 /**
- * Check if the widget is read-only.
+ * Get hyperlink location.
  *
- * @return {boolean}
+ * @return {string} Hyperlink location
  */
-OO.ui.InputWidget.prototype.isReadOnly = function () {
-       return this.readOnly;
+OO.ui.ButtonWidget.prototype.getHref = function () {
+       return this.href;
 };
 
 /**
- * Set the read-only state of the widget.
- *
- * This should probably change the widgets's appearance and prevent it from being used.
+ * Get hyperlink target.
  *
- * @param {boolean} state Make input read-only
- * @chainable
+ * @return {string} Hyperlink target
  */
-OO.ui.InputWidget.prototype.setReadOnly = function ( state ) {
-       this.readOnly = !!state;
-       this.$input.prop( 'readOnly', this.readOnly );
-       return this;
+OO.ui.ButtonWidget.prototype.getTarget = function () {
+       return this.target;
 };
 
 /**
- * @inheritdoc
+ * Set hyperlink location.
+ *
+ * @param {string|null} href Hyperlink location, null to remove
  */
-OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
-       OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
-       if ( this.$input ) {
-               this.$input.prop( 'disabled', this.isDisabled() );
+OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
+       href = typeof href === 'string' ? href : null;
+
+       if ( href !== this.href ) {
+               this.href = href;
+               if ( href !== null ) {
+                       this.$button.attr( 'href', href );
+                       this.isHyperlink = true;
+               } else {
+                       this.$button.removeAttr( 'href' );
+                       this.isHyperlink = false;
+               }
        }
+
        return this;
 };
 
 /**
- * Focus the input.
+ * Set hyperlink target.
  *
- * @chainable
+ * @param {string|null} target Hyperlink target, null to remove
  */
-OO.ui.InputWidget.prototype.focus = function () {
-       this.$input.focus();
+OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
+       target = typeof target === 'string' ? target : null;
+
+       if ( target !== this.target ) {
+               this.target = target;
+               if ( target !== null ) {
+                       this.$button.attr( 'target', target );
+               } else {
+                       this.$button.removeAttr( 'target' );
+               }
+       }
+
        return this;
 };
 
 /**
- * Checkbox widget.
+ * ActionButton widget.
  *
  * @class
- * @extends OO.ui.InputWidget
+ * @extends OO.ui.ButtonWidget
  *
  * @constructor
  * @param {Object} [config] Configuration options
+ * @cfg {string} [action] Symbolic action name
+ * @cfg {string[]} [modes] Symbolic mode names
  */
-OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
+OO.ui.ActionWidget = function OoUiActionWidget( config ) {
+       // Config intialization
+       config = $.extend( { 'framed': false }, config );
+
        // Parent constructor
-       OO.ui.CheckboxInputWidget.super.call( this, config );
+       OO.ui.ActionWidget.super.call( this, config );
+
+       // Properties
+       this.action = config.action || '';
+       this.modes = config.modes || [];
+       this.width = 0;
+       this.height = 0;
 
        // Initialization
-       this.$element.addClass( 'oo-ui-checkboxInputWidget' );
+       this.$element.addClass( 'oo-ui-actionWidget' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
+OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
 
 /* Events */
 
+/**
+ * @event resize
+ */
+
 /* Methods */
 
 /**
- * Get input element.
+ * Check if action is available in a certain mode.
  *
- * @return {jQuery} Input element
+ * @param {string} mode Name of mode
+ * @return {boolean} Has mode
  */
-OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
-       return this.$( '<input type="checkbox" />' );
+OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
+       return this.modes.indexOf( mode ) !== -1;
 };
 
 /**
- * Get checked state of the checkbox
+ * Get symbolic action name.
  *
- * @return {boolean} If the checkbox is checked
+ * @return {string}
  */
-OO.ui.CheckboxInputWidget.prototype.getValue = function () {
-       return this.value;
+OO.ui.ActionWidget.prototype.getAction = function () {
+       return this.action;
 };
 
 /**
- * Set value
+ * Get symbolic action name.
+ *
+ * @return {string}
  */
-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 );
-       }
+OO.ui.ActionWidget.prototype.getModes = function () {
+       return this.modes.slice();
 };
 
 /**
- * @inheritdoc
+ * Emit a resize event if the size has changed.
+ *
+ * @chainable
  */
-OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
-       if ( !this.isDisabled() ) {
-               // Allow the stack to clear so the value will be updated
-               setTimeout( OO.ui.bind( function () {
-                       this.setValue( this.$input.prop( 'checked' ) );
-               }, this ) );
+OO.ui.ActionWidget.prototype.propagateResize = function () {
+       var width, height;
+
+       if ( this.isElementAttached() ) {
+               width = this.$element.width();
+               height = this.$element.height();
+
+               if ( width !== this.width || height !== this.height ) {
+                       this.width = width;
+                       this.height = height;
+                       this.emit( 'resize' );
+               }
        }
+
+       return this;
 };
 
 /**
- * Label widget.
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.LabeledElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
+ * @inheritdoc
  */
-OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
-       // Config intialization
-       config = config || {};
-
-       // Parent constructor
-       OO.ui.LabelWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.LabeledElement.call( this, this.$element, config );
+OO.ui.ActionWidget.prototype.setIcon = function () {
+       // Mixin method
+       OO.ui.IconedElement.prototype.setIcon.apply( this, arguments );
+       this.propagateResize();
 
-       // Properties
-       this.input = config.input;
+       return this;
+};
 
-       // Events
-       if ( this.input instanceof OO.ui.InputWidget ) {
-               this.$element.on( 'click', OO.ui.bind( this.onClick, this ) );
-       }
+/**
+ * @inheritdoc
+ */
+OO.ui.ActionWidget.prototype.setLabel = function () {
+       // Mixin method
+       OO.ui.LabeledElement.prototype.setLabel.apply( this, arguments );
+       this.propagateResize();
 
-       // Initialization
-       this.$element.addClass( 'oo-ui-labelWidget' );
+       return this;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabeledElement );
+/**
+ * @inheritdoc
+ */
+OO.ui.ActionWidget.prototype.setFlags = function () {
+       // Mixin method
+       OO.ui.FlaggableElement.prototype.setFlags.apply( this, arguments );
+       this.propagateResize();
 
-/* Static Properties */
+       return this;
+};
 
-OO.ui.LabelWidget.static.tagName = 'label';
+/**
+ * @inheritdoc
+ */
+OO.ui.ActionWidget.prototype.clearFlags = function () {
+       // Mixin method
+       OO.ui.FlaggableElement.prototype.clearFlags.apply( this, arguments );
+       this.propagateResize();
 
-/* Methods */
+       return this;
+};
 
 /**
- * Handles label mouse click events.
+ * Toggle visibility of button.
  *
- * @param {jQuery.Event} e Mouse click event
+ * @param {boolean} [show] Show button, omit to toggle visibility
+ * @chainable
  */
-OO.ui.LabelWidget.prototype.onClick = function () {
-       this.input.simulateLabelClick();
-       return false;
+OO.ui.ActionWidget.prototype.toggle = function () {
+       // Parent method
+       OO.ui.ActionWidget.super.prototype.toggle.apply( this, arguments );
+       this.propagateResize();
+
+       return this;
 };
 
 /**
- * Lookup input widget.
- *
- * Mixin that adds a menu showing suggested values to a text input. Subclasses must handle `select`
- * and `choose` events on #lookupMenu to make use of selections.
+ * Button that shows and hides a popup.
  *
  * @class
- * @abstract
+ * @extends OO.ui.ButtonWidget
+ * @mixins OO.ui.PopuppableElement
  *
  * @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 );
+OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
+       // Parent constructor
+       OO.ui.PopupButtonWidget.super.call( this, config );
 
-       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' } );
+       // Mixin constructors
+       OO.ui.PopuppableElement.call( this, config );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-lookupWidget' );
-       this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' );
+       this.$element
+               .addClass( 'oo-ui-popupButtonWidget' )
+               .append( this.popup.$element );
 };
 
-/* Methods */
+/* Setup */
 
-/**
- * Handle input focus event.
- *
- * @param {jQuery.Event} e Input focus event
- */
-OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
-       this.openLookupMenu();
-};
+OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
+OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopuppableElement );
 
-/**
- * Handle input blur event.
- *
- * @param {jQuery.Event} e Input blur event
- */
-OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
-       this.lookupMenu.hide();
-};
+/* Methods */
 
 /**
- * Handle input mouse down event.
+ * Handles mouse click events.
  *
- * @param {jQuery.Event} e Input mouse down event
+ * @param {jQuery.Event} e Mouse click event
  */
-OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
-       this.openLookupMenu();
-};
+OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) {
+       // Skip clicks within the popup
+       if ( $.contains( this.popup.$element[0], e.target ) ) {
+               return;
+       }
 
-/**
- * Handle input change event.
- *
- * @param {string} value New input value
- */
-OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
-       this.openLookupMenu();
+       if ( !this.isDisabled() ) {
+               this.popup.toggle();
+               // Parent method
+               OO.ui.PopupButtonWidget.super.prototype.onClick.call( this );
+       }
+       return false;
 };
 
 /**
- * Get lookup menu.
+ * Button that toggles on and off.
  *
- * @return {OO.ui.TextInputMenuWidget}
- */
-OO.ui.LookupInputWidget.prototype.getLookupMenu = function () {
-       return this.lookupMenu;
-};
-
-/**
- * Open the menu.
+ * @class
+ * @extends OO.ui.ButtonWidget
+ * @mixins OO.ui.ToggleWidget
  *
- * @chainable
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [value=false] Initial value
  */
-OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
-       var value = this.lookupInput.getValue();
+OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
+       // Configuration initialization
+       config = config || {};
 
-       if ( this.lookupMenu.$input.is( ':focus' ) && $.trim( value ) !== '' ) {
-               this.populateLookupMenu();
-               if ( !this.lookupMenu.isVisible() ) {
-                       this.lookupMenu.show();
-               }
-       } else {
-               this.lookupMenu.clearItems();
-               this.lookupMenu.hide();
-       }
+       // Parent constructor
+       OO.ui.ToggleButtonWidget.super.call( this, config );
 
-       return this;
+       // Mixin constructors
+       OO.ui.ToggleWidget.call( this, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-toggleButtonWidget' );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget );
+OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
+
+/* Methods */
+
 /**
- * Populate lookup menu with current information.
- *
- * @chainable
+ * @inheritdoc
  */
-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 ) );
+OO.ui.ToggleButtonWidget.prototype.onClick = function () {
+       if ( !this.isDisabled() ) {
+               this.setValue( !this.value );
        }
 
-       return this;
+       // Parent method
+       return OO.ui.ToggleButtonWidget.super.prototype.onClick.call( this );
 };
 
 /**
- * Set selection in the lookup menu with current information.
- *
- * @chainable
+ * @inheritdoc
  */
-OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () {
-       if ( !this.lookupMenu.getSelectedItem() ) {
-               this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
+OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
+       value = !!value;
+       if ( value !== this.value ) {
+               this.setActive( value );
        }
-       this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
-};
 
-/**
- * Get lookup menu items for the current query.
- *
- * @return {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();
+       // Parent method (from mixin)
+       OO.ui.ToggleWidget.prototype.setValue.call( this, value );
 
-       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();
+       return this;
 };
 
 /**
- * Get a new request object of the current lookup query value.
+ * Icon widget.
  *
- * @abstract
- * @return {jqXHR} jQuery AJAX object, or promise object with an .abort() method
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.IconedElement
+ * @mixins OO.ui.TitledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
  */
-OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
-       // Stub, implemented in subclass
-       return null;
+OO.ui.IconWidget = function OoUiIconWidget( config ) {
+       // Config intialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.IconWidget.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.IconedElement.call( this, this.$element, config );
+       OO.ui.TitledElement.call( this, this.$element, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-iconWidget' );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.IconWidget, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
+
+/* Static Properties */
+
+OO.ui.IconWidget.static.tagName = 'span';
+
 /**
- * Handle successful lookup request.
+ * Indicator widget.
  *
- * 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.
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.IndicatedElement
+ * @mixins OO.ui.TitledElement
  *
- * @abstract
- * @param {Mixed} data Response from server
+ * @constructor
+ * @param {Object} [config] Configuration options
  */
-OO.ui.LookupInputWidget.prototype.onLookupRequestDone = function () {
-       // Stub, implemented in subclass
-};
+OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
+       // Config intialization
+       config = config || {};
 
-/**
- * Get a list of menu item widgets from the data stored by the lookup request's done handler.
- *
- * @abstract
- * @param {Mixed} data Cached result data, usually an array
- * @return {OO.ui.MenuItemWidget[]} Menu items
- */
-OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
-       // Stub, implemented in subclass
-       return [];
+       // Parent constructor
+       OO.ui.IndicatorWidget.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.IndicatedElement.call( this, this.$element, config );
+       OO.ui.TitledElement.call( this, this.$element, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-indicatorWidget' );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatedElement );
+OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
+
+/* Static Properties */
+
+OO.ui.IndicatorWidget.static.tagName = 'span';
+
 /**
- * Option widget.
+ * Inline menu of options.
  *
- * Use with OO.ui.SelectWidget.
+ * Use with OO.ui.MenuOptionWidget.
  *
  * @class
  * @extends OO.ui.Widget
  * @mixins OO.ui.IconedElement
- * @mixins OO.ui.LabeledElement
  * @mixins OO.ui.IndicatedElement
- * @mixins OO.ui.FlaggableElement
+ * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.TitledElement
  *
  * @constructor
- * @param {Mixed} data Option data
  * @param {Object} [config] Configuration options
- * @cfg {string} [rel] Value for `rel` attribute in DOM, allowing per-option styling
+ * @cfg {Object} [menu] Configuration options to pass to menu widget
  */
-OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
-       // Config intialization
-       config = config || {};
+OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) {
+       // Configuration initialization
+       config = $.extend( { 'indicator': 'down' }, config );
 
        // Parent constructor
-       OO.ui.OptionWidget.super.call( this, config );
+       OO.ui.InlineMenuWidget.super.call( this, config );
 
        // Mixin constructors
-       OO.ui.ItemWidget.call( this );
        OO.ui.IconedElement.call( this, this.$( '<span>' ), config );
-       OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
        OO.ui.IndicatedElement.call( this, this.$( '<span>' ), config );
-       OO.ui.FlaggableElement.call( this, config );
+       OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
+       OO.ui.TitledElement.call( this, this.$label, config );
 
        // Properties
-       this.data = data;
-       this.selected = false;
-       this.highlighted = false;
-       this.pressed = false;
+       this.menu = new OO.ui.MenuWidget( $.extend( { '$': this.$ }, config.menu ) );
+       this.$handle = this.$( '<span>' );
+
+       // Events
+       this.$element.on( { 'click': OO.ui.bind( this.onClick, this ) } );
+       this.menu.connect( this, { 'select': 'onMenuSelect' } );
 
        // Initialization
+       this.$handle
+               .addClass( 'oo-ui-inlineMenuWidget-handle' )
+               .append( this.$icon, this.$label, this.$indicator );
        this.$element
-               .data( 'oo-ui-optionWidget', this )
-               .attr( 'rel', config.rel )
-               .addClass( 'oo-ui-optionWidget' )
-               .append( this.$label );
-       this.$element
-               .prepend( this.$icon )
-               .append( this.$indicator );
+               .addClass( 'oo-ui-inlineMenuWidget' )
+               .append( this.$handle, this.menu.$element );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabeledElement );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatedElement );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggableElement );
-
-/* Static Properties */
-
-OO.ui.OptionWidget.static.tagName = 'li';
-
-OO.ui.OptionWidget.static.selectable = true;
-
-OO.ui.OptionWidget.static.highlightable = true;
-
-OO.ui.OptionWidget.static.pressable = true;
-
-OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
+OO.inheritClass( OO.ui.InlineMenuWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IndicatedElement );
+OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.TitledElement );
 
 /* Methods */
 
 /**
- * Check if option can be selected.
+ * Get the menu.
  *
- * @return {boolean} Item is selectable
+ * @return {OO.ui.MenuWidget} Menu of widget
  */
-OO.ui.OptionWidget.prototype.isSelectable = function () {
-       return this.constructor.static.selectable && !this.isDisabled();
+OO.ui.InlineMenuWidget.prototype.getMenu = function () {
+       return this.menu;
 };
 
 /**
- * Check if option can be highlighted.
+ * Handles menu select events.
  *
- * @return {boolean} Item is highlightable
+ * @param {OO.ui.MenuItemWidget} item Selected menu item
  */
-OO.ui.OptionWidget.prototype.isHighlightable = function () {
-       return this.constructor.static.highlightable && !this.isDisabled();
-};
+OO.ui.InlineMenuWidget.prototype.onMenuSelect = function ( item ) {
+       var selectedLabel;
 
-/**
- * Check if option can be pressed.
- *
- * @return {boolean} Item is pressable
- */
-OO.ui.OptionWidget.prototype.isPressable = function () {
-       return this.constructor.static.pressable && !this.isDisabled();
+       if ( !item ) {
+               return;
+       }
+
+       selectedLabel = item.getLabel();
+
+       // If the label is a DOM element, clone it, because setLabel will append() it
+       if ( selectedLabel instanceof jQuery ) {
+               selectedLabel = selectedLabel.clone();
+       }
+
+       this.setLabel( selectedLabel );
 };
 
 /**
- * Check if option is selected.
+ * Handles mouse click events.
  *
- * @return {boolean} Item is selected
+ * @param {jQuery.Event} e Mouse click event
  */
-OO.ui.OptionWidget.prototype.isSelected = function () {
-       return this.selected;
+OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) {
+       // Skip clicks within the menu
+       if ( $.contains( this.menu.$element[0], e.target ) ) {
+               return;
+       }
+
+       if ( !this.isDisabled() ) {
+               if ( this.menu.isVisible() ) {
+                       this.menu.toggle( false );
+               } else {
+                       this.menu.toggle( true );
+               }
+       }
+       return false;
 };
 
 /**
- * Check if option is highlighted.
+ * Input widget.
  *
- * @return {boolean} Item is highlighted
+ * @abstract
+ * @class
+ * @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.OptionWidget.prototype.isHighlighted = function () {
-       return this.highlighted;
+OO.ui.InputWidget = function OoUiInputWidget( config ) {
+       // Config intialization
+       config = $.extend( { 'readOnly': false }, config );
+
+       // Parent constructor
+       OO.ui.InputWidget.super.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.isDisabled() );
+       this.setReadOnly( config.readOnly );
+       this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input );
+       this.setValue( config.value );
 };
 
+/* Setup */
+
+OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
+
+/* Events */
+
 /**
- * Check if option is pressed.
- *
- * @return {boolean} Item is pressed
+ * @event change
+ * @param value
  */
-OO.ui.OptionWidget.prototype.isPressed = function () {
-       return this.pressed;
-};
+
+/* Methods */
 
 /**
- * Set selected state.
+ * Get input element.
  *
- * @param {boolean} [state=false] Select option
- * @chainable
+ * @param {Object} [config] Configuration options
+ * @return {jQuery} Input element
  */
-OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
-       if ( 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;
+OO.ui.InputWidget.prototype.getInputElement = function () {
+       return this.$( '<input>' );
 };
 
 /**
- * Set highlighted state.
+ * Handle potentially value-changing events.
  *
- * @param {boolean} [state=false] Highlight option
- * @chainable
+ * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
  */
-OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
-       if ( 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' );
-               }
+OO.ui.InputWidget.prototype.onEdit = function () {
+       var widget = this;
+       if ( !this.isDisabled() ) {
+               // Allow the stack to clear so the value will be updated
+               setTimeout( function () {
+                       widget.setValue( widget.$input.val() );
+               } );
        }
-       return this;
 };
 
 /**
- * Set pressed state.
+ * Get the value of the input.
  *
- * @param {boolean} [state=false] Press option
- * @chainable
+ * @return {string} Input value
  */
-OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
-       if ( this.constructor.static.pressable ) {
-               this.pressed = !!state;
-               if ( this.pressed ) {
-                       this.$element.addClass( 'oo-ui-optionWidget-pressed' );
-               } else {
-                       this.$element.removeClass( 'oo-ui-optionWidget-pressed' );
-               }
-       }
-       return this;
+OO.ui.InputWidget.prototype.getValue = function () {
+       return this.value;
 };
 
 /**
- * Make the option's highlight flash.
- *
- * While flashing, the visual style of the pressed state is removed if present.
+ * Sets the direction of the current input, either RTL or LTR
  *
- * @return {jQuery.Promise} Promise resolved when flashing is done
+ * @param {boolean} isRTL
  */
-OO.ui.OptionWidget.prototype.flash = function () {
-       var $this = this.$element,
-               deferred = $.Deferred();
-
-       if ( !this.isDisabled() && this.constructor.static.pressable ) {
-               $this.removeClass( 'oo-ui-optionWidget-highlighted oo-ui-optionWidget-pressed' );
-               setTimeout( OO.ui.bind( function () {
-                       // Restore original classes
-                       $this
-                               .toggleClass( 'oo-ui-optionWidget-highlighted', this.highlighted )
-                               .toggleClass( 'oo-ui-optionWidget-pressed', this.pressed );
-                       setTimeout( function () {
-                               deferred.resolve();
-                       }, 100 );
-               }, this ), 100 );
+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' );
        }
-
-       return deferred.promise();
 };
 
 /**
- * Get option data.
+ * Set the value of the input.
  *
- * @return {Mixed} Option data
+ * @param {string} value New value
+ * @fires change
+ * @chainable
  */
-OO.ui.OptionWidget.prototype.getData = function () {
-       return this.data;
+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;
 };
 
 /**
- * Selection of options.
- *
- * Use together with OO.ui.OptionWidget.
+ * Sanitize incoming value.
  *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.GroupElement
+ * Ensures value is a string, and converts undefined and null to empty strings.
  *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {OO.ui.OptionWidget[]} [items] Options to add
+ * @param {string} value Original value
+ * @return {string} Sanitized value
  */
-OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
-       // Config intialization
-       config = config || {};
-
-       // Parent constructor
-       OO.ui.SelectWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.GroupWidget.call( this, this.$element, config );
-
-       // Properties
-       this.pressed = false;
-       this.selecting = null;
-       this.hashes = {};
-       this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
-       this.onMouseMoveHandler = OO.ui.bind( this.onMouseMove, this );
-
-       // Events
-       this.$element.on( {
-               'mousedown': OO.ui.bind( this.onMouseDown, this ),
-               'mouseover': OO.ui.bind( this.onMouseOver, this ),
-               'mouseleave': OO.ui.bind( this.onMouseLeave, this )
-       } );
-
-       // Initialization
-       this.$element.addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' );
-       if ( $.isArray( config.items ) ) {
-               this.addItems( config.items );
+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 );
        }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
-
-// Need to mixin base class as well
-OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
-OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
-
-/* Events */
+};
 
 /**
- * @event highlight
- * @param {OO.ui.OptionWidget|null} item Highlighted item
+ * Simulate the behavior of clicking on a label bound to this input.
  */
+OO.ui.InputWidget.prototype.simulateLabelClick = function () {
+       if ( !this.isDisabled() ) {
+               if ( this.$input.is( ':checkbox,:radio' ) ) {
+                       this.$input.click();
+               } else if ( this.$input.is( ':input' ) ) {
+                       this.$input.focus();
+               }
+       }
+};
 
 /**
- * @event press
- * @param {OO.ui.OptionWidget|null} item Pressed item
+ * Check if the widget is read-only.
+ *
+ * @return {boolean}
  */
+OO.ui.InputWidget.prototype.isReadOnly = function () {
+       return this.readOnly;
+};
 
 /**
- * @event select
- * @param {OO.ui.OptionWidget|null} item Selected item
+ * Set the read-only state of the widget.
+ *
+ * This should probably change the widgets's appearance and prevent it from being used.
+ *
+ * @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;
+};
 
 /**
- * @event choose
- * @param {OO.ui.OptionWidget|null} item Chosen item
+ * @inheritdoc
  */
+OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
+       OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
+       if ( this.$input ) {
+               this.$input.prop( 'disabled', this.isDisabled() );
+       }
+       return this;
+};
 
 /**
- * @event add
- * @param {OO.ui.OptionWidget[]} items Added items
- * @param {number} index Index items were added at
+ * Focus the input.
+ *
+ * @chainable
  */
+OO.ui.InputWidget.prototype.focus = function () {
+       this.$input.focus();
+       return this;
+};
 
 /**
- * @event remove
- * @param {OO.ui.OptionWidget[]} items Removed items
+ * Blur the input.
+ *
+ * @chainable
  */
-
-/* Static Properties */
-
-OO.ui.SelectWidget.static.tagName = 'ul';
-
-/* Methods */
+OO.ui.InputWidget.prototype.blur = function () {
+       this.$input.blur();
+       return this;
+};
 
 /**
- * Handle mouse down events.
+ * Checkbox widget.
  *
- * @private
- * @param {jQuery.Event} e Mouse down event
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
  */
-OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
-       var item;
+OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
+       // Parent constructor
+       OO.ui.CheckboxInputWidget.super.call( this, config );
 
-       if ( !this.isDisabled() && e.which === 1 ) {
-               this.togglePressed( true );
-               item = this.getTargetItem( e );
-               if ( item && item.isSelectable() ) {
-                       this.pressItem( item );
-                       this.selecting = item;
-                       this.getElementDocument().addEventListener(
-                               'mouseup', this.onMouseUpHandler, true
-                       );
-                       this.getElementDocument().addEventListener(
-                               'mousemove', this.onMouseMoveHandler, true
-                       );
-               }
-       }
-       return false;
+       // Initialization
+       this.$element.addClass( 'oo-ui-checkboxInputWidget' );
 };
 
-/**
- * Handle mouse up events.
- *
- * @private
- * @param {jQuery.Event} e Mouse up event
- */
-OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
-       var item;
+/* Setup */
 
-       this.togglePressed( false );
-       if ( !this.selecting ) {
-               item = this.getTargetItem( e );
-               if ( item && item.isSelectable() ) {
-                       this.selecting = item;
-               }
-       }
-       if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
-               this.pressItem( null );
-               this.chooseItem( this.selecting );
-               this.selecting = null;
-       }
+OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
 
-       this.getElementDocument().removeEventListener(
-               'mouseup', this.onMouseUpHandler, true
-       );
-       this.getElementDocument().removeEventListener(
-               'mousemove', this.onMouseMoveHandler, true
-       );
+/* Events */
 
-       return false;
-};
+/* Methods */
 
 /**
- * Handle mouse move events.
+ * Get input element.
  *
- * @private
- * @param {jQuery.Event} e Mouse move event
+ * @return {jQuery} Input element
  */
-OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
-       var item;
-
-       if ( !this.isDisabled() && this.pressed ) {
-               item = this.getTargetItem( e );
-               if ( item && item !== this.selecting && item.isSelectable() ) {
-                       this.pressItem( item );
-                       this.selecting = item;
-               }
-       }
-       return false;
+OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
+       return this.$( '<input type="checkbox" />' );
 };
 
 /**
- * Handle mouse over events.
+ * Get checked state of the checkbox
  *
- * @private
- * @param {jQuery.Event} e Mouse over event
+ * @return {boolean} If the checkbox is checked
  */
-OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
-       var item;
-
-       if ( !this.isDisabled() ) {
-               item = this.getTargetItem( e );
-               this.highlightItem( item && item.isHighlightable() ? item : null );
-       }
-       return false;
+OO.ui.CheckboxInputWidget.prototype.getValue = function () {
+       return this.value;
 };
 
 /**
- * Handle mouse leave events.
- *
- * @private
- * @param {jQuery.Event} e Mouse over event
+ * Set value
  */
-OO.ui.SelectWidget.prototype.onMouseLeave = function () {
-       if ( !this.isDisabled() ) {
-               this.highlightItem( null );
+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 );
        }
-       return false;
 };
 
 /**
- * Get the closest item to a jQuery.Event.
- *
- * @private
- * @param {jQuery.Event} e
- * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
+ * @inheritdoc
  */
-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' );
+OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
+       var widget = this;
+       if ( !this.isDisabled() ) {
+               // Allow the stack to clear so the value will be updated
+               setTimeout( function () {
+                       widget.setValue( widget.$input.prop( 'checked' ) );
+               } );
        }
-       return null;
 };
 
 /**
- * Get selected item.
+ * Text input widget.
  *
- * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
+ * @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
+ * @cfg {boolean} [autosize=false] Automatically resize to fit content
+ * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
  */
-OO.ui.SelectWidget.prototype.getSelectedItem = function () {
-       var i, len;
+OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
+       var widget = this;
+       config = $.extend( { 'maxRows': 10 }, config );
 
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               if ( this.items[i].isSelected() ) {
-                       return this.items[i];
-               }
-       }
-       return null;
-};
+       // Parent constructor
+       OO.ui.TextInputWidget.super.call( this, config );
 
-/**
- * Get highlighted item.
- *
- * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
- */
-OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
-       var i, len;
+       // Properties
+       this.pending = 0;
+       this.multiline = !!config.multiline;
+       this.autosize = !!config.autosize;
+       this.maxRows = config.maxRows;
 
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               if ( this.items[i].isHighlighted() ) {
-                       return this.items[i];
-               }
+       // Events
+       this.$input.on( 'keypress', OO.ui.bind( this.onKeyPress, this ) );
+       this.$element.on( 'DOMNodeInsertedIntoDocument', OO.ui.bind( this.onElementAttach, 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( function () {
+                                       widget.$input.focus();
+                                       return false;
+                               } )
+               );
+       }
+       if ( config.placeholder ) {
+               this.$input.attr( 'placeholder', config.placeholder );
        }
-       return null;
 };
 
-/**
- * Get an existing item with equivilant data.
- *
- * @param {Object} data Item data to search for
- * @return {OO.ui.OptionWidget|null} Item with equivilent value, `null` if none exists
- */
-OO.ui.SelectWidget.prototype.getItemFromData = function ( data ) {
-       var hash = OO.getHash( data );
+/* Setup */
 
-       if ( hash in this.hashes ) {
-               return this.hashes[hash];
-       }
+OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
 
-       return null;
-};
+/* Events */
 
 /**
- * Toggle pressed state.
+ * User presses enter inside the text box.
  *
- * @param {boolean} pressed An option is being pressed
+ * Not called if input is multiline.
+ *
+ * @event enter
  */
-OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
-       if ( pressed === undefined ) {
-               pressed = !this.pressed;
-       }
-       if ( pressed !== this.pressed ) {
-               this.$element.toggleClass( 'oo-ui-selectWidget-pressed', pressed );
-               this.$element.toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
-               this.pressed = pressed;
-       }
-};
+
+/* Methods */
 
 /**
- * Highlight an item.
- *
- * Highlighting is mutually exclusive.
+ * Handle key press events.
  *
- * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all
- * @fires highlight
- * @chainable
+ * @param {jQuery.Event} e Key press event
+ * @fires enter If enter key is pressed and input is not multiline
  */
-OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
-       var i, len, highlighted,
-               changed = false;
-
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               highlighted = this.items[i] === item;
-               if ( this.items[i].isHighlighted() !== highlighted ) {
-                       this.items[i].setHighlighted( highlighted );
-                       changed = true;
-               }
-       }
-       if ( changed ) {
-               this.emit( 'highlight', item );
+OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
+       if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
+               this.emit( 'enter' );
        }
-
-       return this;
 };
 
 /**
- * Select an item.
+ * Handle element attach events.
  *
- * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
- * @fires select
- * @chainable
+ * @param {jQuery.Event} e Element attach event
  */
-OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
-       var i, len, selected,
-               changed = false;
+OO.ui.TextInputWidget.prototype.onElementAttach = function () {
+       this.adjustSize();
+};
 
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               selected = this.items[i] === item;
-               if ( this.items[i].isSelected() !== selected ) {
-                       this.items[i].setSelected( selected );
-                       changed = true;
-               }
-       }
-       if ( changed ) {
-               this.emit( 'select', item );
-       }
+/**
+ * @inheritdoc
+ */
+OO.ui.TextInputWidget.prototype.onEdit = function () {
+       this.adjustSize();
 
-       return this;
+       // Parent method
+       return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
 };
 
 /**
- * Press an item.
+ * Automatically adjust the size of the text input.
+ *
+ * This only affects multi-line inputs that are auto-sized.
  *
- * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
- * @fires press
  * @chainable
  */
-OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
-       var i, len, pressed,
-               changed = false;
+OO.ui.TextInputWidget.prototype.adjustSize = function () {
+       var $clone, scrollHeight, innerHeight, outerHeight, maxInnerHeight, idealHeight;
 
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               pressed = this.items[i] === item;
-               if ( this.items[i].isPressed() !== pressed ) {
-                       this.items[i].setPressed( pressed );
-                       changed = true;
-               }
-       }
-       if ( changed ) {
-               this.emit( 'press', item );
+       if ( this.multiline && this.autosize ) {
+               $clone = this.$input.clone()
+                       .val( this.$input.val() )
+                       .css( { 'height': 0 } )
+                       .insertAfter( this.$input );
+               // Set inline height property to 0 to measure scroll height
+               scrollHeight = $clone[0].scrollHeight;
+               // Remove inline height property to measure natural heights
+               $clone.css( 'height', '' );
+               innerHeight = $clone.innerHeight();
+               outerHeight = $clone.outerHeight();
+               // Measure max rows height
+               $clone.attr( 'rows', this.maxRows ).css( 'height', 'auto' );
+               maxInnerHeight = $clone.innerHeight();
+               $clone.removeAttr( 'rows' ).css( 'height', '' );
+               $clone.remove();
+               idealHeight = Math.min( maxInnerHeight, scrollHeight );
+               // Only apply inline height when expansion beyond natural height is needed
+               this.$input.css(
+                       'height',
+                       // Use the difference between the inner and outer height as a buffer
+                       idealHeight > outerHeight ? idealHeight + ( outerHeight - innerHeight ) : ''
+               );
        }
-
        return this;
 };
 
 /**
- * Choose an item.
- *
- * Identical to #selectItem, but may vary in subclasses that want to take additional action when
- * an item is selected using the keyboard or mouse.
+ * Get input element.
  *
- * @param {OO.ui.OptionWidget} item Item to choose
- * @fires choose
- * @chainable
+ * @param {Object} [config] Configuration options
+ * @return {jQuery} Input element
  */
-OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
-       this.selectItem( item );
-       this.emit( 'choose', item );
-
-       return this;
+OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
+       return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="text" />' );
 };
 
+/* Methods */
+
 /**
- * Get an item relative to another one.
+ * Check if input supports multiple lines.
  *
- * @param {OO.ui.OptionWidget} item Item to start at
- * @param {number} direction Direction to move in
- * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
+ * @return {boolean}
  */
-OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
-       var inc = direction > 0 ? 1 : -1,
-               len = this.items.length,
-               index = item instanceof OO.ui.OptionWidget ?
-                       $.inArray( item, this.items ) : ( 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;
+OO.ui.TextInputWidget.prototype.isMultiline = function () {
+       return !!this.multiline;
 };
 
 /**
- * Get the next selectable item.
+ * Check if input automatically adjusts its size.
  *
- * @return {OO.ui.OptionWidget|null} Item, `null` if ther aren't any selectable items
+ * @return {boolean}
  */
-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;
+OO.ui.TextInputWidget.prototype.isAutosizing = function () {
+       return !!this.autosize;
 };
 
 /**
- * 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.
+ * Check if input is pending.
  *
- * @param {OO.ui.OptionWidget[]} items Items to add
- * @param {number} [index] Index to insert items after
- * @fires add
- * @chainable
+ * @return {boolean}
  */
-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 );
-       }
-
-       // Mixin method
-       OO.ui.GroupWidget.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;
+OO.ui.TextInputWidget.prototype.isPending = function () {
+       return !!this.pending;
 };
 
 /**
- * Remove items.
- *
- * Items will be detached, not removed, so they can be used later.
+ * Increase the pending stack.
  *
- * @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 );
-               }
-       }
-
-       // Mixin method
-       OO.ui.GroupWidget.prototype.removeItems.call( this, items );
-
-       this.emit( 'remove', items );
+OO.ui.TextInputWidget.prototype.pushPending = function () {
+       if ( this.pending === 0 ) {
+               this.$element.addClass( 'oo-ui-textInputWidget-pending' );
+               this.$input.addClass( 'oo-ui-texture-pending' );
+       }
+       this.pending++;
 
        return this;
 };
 
 /**
- * Clear all items.
+ * Reduce the pending stack.
  *
- * Items will be detached, not removed, so they can be used later.
+ * Clamped at zero.
  *
- * @fires remove
  * @chainable
  */
-OO.ui.SelectWidget.prototype.clearItems = function () {
-       var items = this.items.slice();
-
-       // Clear all items
-       this.hashes = {};
-       // Mixin method
-       OO.ui.GroupWidget.prototype.clearItems.call( this );
-       this.selectItem( null );
-
-       this.emit( 'remove', items );
+OO.ui.TextInputWidget.prototype.popPending = function () {
+       if ( this.pending === 1 ) {
+               this.$element.removeClass( 'oo-ui-textInputWidget-pending' );
+               this.$input.removeClass( 'oo-ui-texture-pending' );
+       }
+       this.pending = Math.max( 0, this.pending - 1 );
 
        return this;
 };
 
 /**
- * Menu item widget.
+ * Select the contents of the input.
  *
- * Use with OO.ui.MenuWidget.
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.select = function () {
+       this.$input.select();
+       return this;
+};
+
+/**
+ * Label widget.
  *
  * @class
- * @extends OO.ui.OptionWidget
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.LabeledElement
  *
  * @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 );
+OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
+       // Config intialization
+       config = config || {};
 
        // Parent constructor
-       OO.ui.MenuItemWidget.super.call( this, data, config );
+       OO.ui.LabelWidget.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.LabeledElement.call( this, this.$element, config );
+
+       // Properties
+       this.input = config.input;
+
+       // Events
+       if ( this.input instanceof OO.ui.InputWidget ) {
+               this.$element.on( 'click', OO.ui.bind( this.onClick, this ) );
+       }
 
        // Initialization
-       this.$element.addClass( 'oo-ui-menuItemWidget' );
+       this.$element.addClass( 'oo-ui-labelWidget' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.MenuItemWidget, OO.ui.OptionWidget );
+OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabeledElement );
+
+/* Static Properties */
+
+OO.ui.LabelWidget.static.tagName = 'label';
+
+/* Methods */
 
 /**
- * Menu widget.
+ * Handles label mouse click events.
  *
- * Use together with OO.ui.MenuItemWidget.
+ * @param {jQuery.Event} e Mouse click event
+ */
+OO.ui.LabelWidget.prototype.onClick = function () {
+       this.input.simulateLabelClick();
+       return false;
+};
+
+/**
+ * Option widget.
+ *
+ * Use with OO.ui.SelectWidget.
  *
  * @class
- * @extends OO.ui.SelectWidget
- * @mixins OO.ui.ClippableElement
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.FlaggableElement
  *
  * @constructor
+ * @param {Mixed} data Option data
  * @param {Object} [config] Configuration options
- * @cfg {OO.ui.InputWidget} [input] Input to bind keyboard handlers to
- * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
+ * @cfg {string} [rel] Value for `rel` attribute in DOM, allowing per-option styling
  */
-OO.ui.MenuWidget = function OoUiMenuWidget( config ) {
+OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
        // Config intialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.MenuWidget.super.call( this, config );
+       OO.ui.OptionWidget.super.call( this, config );
 
        // Mixin constructors
-       OO.ui.ClippableElement.call( this, this.$group, config );
+       OO.ui.ItemWidget.call( this );
+       OO.ui.LabeledElement.call( this, this.$( '<span>' ), config );
+       OO.ui.FlaggableElement.call( this, config );
 
        // Properties
-       this.autoHide = config.autoHide === undefined || !!config.autoHide;
-       this.newItems = null;
-       this.$input = config.input ? config.input.$input : null;
-       this.$previousFocus = null;
-       this.isolated = !config.input;
-       this.visible = false;
-       this.flashing = false;
-       this.onKeyDownHandler = OO.ui.bind( this.onKeyDown, this );
-       this.onDocumentMouseDownHandler = OO.ui.bind( this.onDocumentMouseDown, this );
+       this.data = data;
+       this.selected = false;
+       this.highlighted = false;
+       this.pressed = false;
 
        // Initialization
-       this.$element.hide().addClass( 'oo-ui-menuWidget' );
+       this.$element
+               .data( 'oo-ui-optionWidget', this )
+               .attr( 'rel', config.rel )
+               .addClass( 'oo-ui-optionWidget' )
+               .append( this.$label );
+       this.$element
+               .prepend( this.$icon )
+               .append( this.$indicator );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.MenuWidget, OO.ui.SelectWidget );
-OO.mixinClass( OO.ui.MenuWidget, OO.ui.ClippableElement );
+OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggableElement );
+
+/* Static Properties */
+
+OO.ui.OptionWidget.static.tagName = 'li';
+
+OO.ui.OptionWidget.static.selectable = true;
+
+OO.ui.OptionWidget.static.highlightable = true;
+
+OO.ui.OptionWidget.static.pressable = true;
+
+OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
 
 /* Methods */
 
 /**
- * Handles document mouse down events.
+ * Check if option can be selected.
  *
- * @param {jQuery.Event} e Key down event
+ * @return {boolean} Item is selectable
  */
-OO.ui.MenuWidget.prototype.onDocumentMouseDown = function ( e ) {
-       if ( !$.contains( this.$element[0], e.target ) ) {
-               this.hide();
-       }
+OO.ui.OptionWidget.prototype.isSelectable = function () {
+       return this.constructor.static.selectable && !this.isDisabled();
 };
 
 /**
- * Handles key down events.
+ * Check if option can be highlighted.
  *
- * @param {jQuery.Event} e Key down event
+ * @return {boolean} Item is highlightable
  */
-OO.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
-       var nextItem,
-               handled = false,
-               highlightItem = this.getHighlightedItem();
-
-       if ( !this.isDisabled() && this.visible ) {
-               if ( !highlightItem ) {
-                       highlightItem = this.getSelectedItem();
-               }
-               switch ( e.keyCode ) {
-                       case OO.ui.Keys.ENTER:
-                               this.chooseItem( 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;
-               }
+OO.ui.OptionWidget.prototype.isHighlightable = function () {
+       return this.constructor.static.highlightable && !this.isDisabled();
+};
 
-               if ( nextItem ) {
-                       this.highlightItem( nextItem );
-                       nextItem.scrollElementIntoView();
-               }
+/**
+ * Check if option can be pressed.
+ *
+ * @return {boolean} Item is pressable
+ */
+OO.ui.OptionWidget.prototype.isPressable = function () {
+       return this.constructor.static.pressable && !this.isDisabled();
+};
 
-               if ( handled ) {
-                       e.preventDefault();
-                       e.stopPropagation();
-                       return false;
-               }
-       }
+/**
+ * Check if option is selected.
+ *
+ * @return {boolean} Item is selected
+ */
+OO.ui.OptionWidget.prototype.isSelected = function () {
+       return this.selected;
 };
 
 /**
- * Check if the menu is visible.
+ * Check if option is highlighted.
  *
- * @return {boolean} Menu is visible
+ * @return {boolean} Item is highlighted
  */
-OO.ui.MenuWidget.prototype.isVisible = function () {
-       return this.visible;
+OO.ui.OptionWidget.prototype.isHighlighted = function () {
+       return this.highlighted;
 };
 
 /**
- * Bind key down listener.
+ * Check if option is pressed.
+ *
+ * @return {boolean} Item is pressed
  */
-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 );
-       }
+OO.ui.OptionWidget.prototype.isPressed = function () {
+       return this.pressed;
 };
 
 /**
- * Unbind key down listener.
+ * Set selected state.
+ *
+ * @param {boolean} [state=false] Select option
+ * @chainable
  */
-OO.ui.MenuWidget.prototype.unbindKeyDownListener = function () {
-       if ( this.$input ) {
-               this.$input.off( 'keydown' );
-       } else {
-               this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
+OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
+       if ( this.constructor.static.selectable ) {
+               this.selected = !!state;
+               this.$element.toggleClass( 'oo-ui-optionWidget-selected', state );
+               if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
+                       this.scrollElementIntoView();
+               }
        }
+       return this;
 };
 
 /**
- * Choose an item.
- *
- * This will close the menu when done, unlike selectItem which only changes selection.
+ * Set highlighted state.
  *
- * @param {OO.ui.OptionWidget} item Item to choose
+ * @param {boolean} [state=false] Highlight option
  * @chainable
  */
-OO.ui.MenuWidget.prototype.chooseItem = function ( item ) {
-       // Parent method
-       OO.ui.MenuWidget.super.prototype.chooseItem.call( this, item );
-
-       if ( item && !this.flashing ) {
-               this.flashing = true;
-               item.flash().done( OO.ui.bind( function () {
-                       this.hide();
-                       this.flashing = false;
-               }, this ) );
-       } else {
-               this.hide();
+OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
+       if ( this.constructor.static.highlightable ) {
+               this.highlighted = !!state;
+               this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
        }
-
        return this;
 };
 
 /**
- * Add items.
- *
- * Adding an existing item (by value) will move it.
+ * Set pressed state.
  *
- * @param {OO.ui.MenuItemWidget[]} items Items to add
- * @param {number} [index] Index to insert items after
+ * @param {boolean} [state=false] Press option
  * @chainable
  */
-OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
-       var i, len, item;
-
-       // Parent method
-       OO.ui.MenuWidget.super.prototype.addItems.call( this, items, index );
-
-       // Auto-initialize
-       if ( !this.newItems ) {
-               this.newItems = [];
-       }
-
-       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 );
-               }
+OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
+       if ( this.constructor.static.pressable ) {
+               this.pressed = !!state;
+               this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
        }
-
        return this;
 };
 
 /**
- * Show the menu.
+ * Make the option's highlight flash.
  *
- * @chainable
+ * While flashing, the visual style of the pressed state is removed if present.
+ *
+ * @return {jQuery.Promise} Promise resolved when flashing is done
  */
-OO.ui.MenuWidget.prototype.show = function () {
-       var i, len;
-
-       if ( this.items.length ) {
-               this.$element.show();
-               this.visible = true;
-               this.bindKeyDownListener();
+OO.ui.OptionWidget.prototype.flash = function () {
+       var widget = this,
+               $element = this.$element,
+               deferred = $.Deferred();
 
-               // 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 && this.newItems.length ) {
-                       for ( i = 0, len = this.newItems.length; i < len; i++ ) {
-                               this.newItems[i].fitLabel();
-                       }
-                       this.newItems = null;
-               }
+       if ( !this.isDisabled() && this.constructor.static.pressable ) {
+               $element.removeClass( 'oo-ui-optionWidget-highlighted oo-ui-optionWidget-pressed' );
+               setTimeout( function () {
+                       // Restore original classes
+                       $element
+                               .toggleClass( 'oo-ui-optionWidget-highlighted', widget.highlighted )
+                               .toggleClass( 'oo-ui-optionWidget-pressed', widget.pressed );
 
-               this.setClipping( true );
+                       setTimeout( function () {
+                               deferred.resolve();
+                       }, 100 );
 
-               // Auto-hide
-               if ( this.autoHide ) {
-                       this.getElementDocument().addEventListener(
-                               'mousedown', this.onDocumentMouseDownHandler, true
-                       );
-               }
+               }, 100 );
        }
 
-       return this;
+       return deferred.promise();
 };
 
 /**
- * Hide the menu.
+ * Get option data.
  *
- * @chainable
+ * @return {Mixed} Option data
  */
-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.getElementDocument().removeEventListener(
-               'mousedown', this.onDocumentMouseDownHandler, true
-       );
-
-       this.setClipping( false );
-
-       return this;
+OO.ui.OptionWidget.prototype.getData = function () {
+       return this.data;
 };
 
 /**
- * Inline menu of options.
+ * Option with an option icon and indicator.
  *
- * Use with OO.ui.MenuOptionWidget.
+ * Use together with OO.ui.SelectWidget.
  *
  * @class
- * @extends OO.ui.Widget
+ * @extends OO.ui.OptionWidget
  * @mixins OO.ui.IconedElement
  * @mixins OO.ui.IndicatedElement
- * @mixins OO.ui.LabeledElement
- * @mixins OO.ui.TitledElement
  *
  * @constructor
+ * @param {Mixed} data Option data
  * @param {Object} [config] Configuration options
- * @cfg {Object} [menu] Configuration options to pass to menu widget
  */
-OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) {
-       // Configuration initialization
-       config = $.extend( { 'indicator': 'down' }, config );
-
+OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( data, config ) {
        // Parent constructor
-       OO.ui.InlineMenuWidget.super.call( this, config );
+       OO.ui.DecoratedOptionWidget.super.call( this, data, 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.$label, config );
-
-       // Properties
-       this.menu = new OO.ui.MenuWidget( $.extend( { '$': this.$ }, config.menu ) );
-       this.$handle = this.$( '<span>' );
-
-       // Events
-       this.$element.on( { 'click': OO.ui.bind( this.onClick, this ) } );
-       this.menu.connect( this, { 'select': 'onMenuSelect' } );
 
        // Initialization
-       this.$handle
-               .addClass( 'oo-ui-inlineMenuWidget-handle' )
-               .append( this.$icon, this.$label, this.$indicator );
        this.$element
-               .addClass( 'oo-ui-inlineMenuWidget' )
-               .append( this.$handle, this.menu.$element );
+               .addClass( 'oo-ui-decoratedOptionWidget' )
+               .prepend( this.$icon )
+               .append( this.$indicator );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.InlineMenuWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IconedElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IndicatedElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.LabeledElement );
-OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.TitledElement );
-
-/* Methods */
-
-/**
- * Get the menu.
- *
- * @return {OO.ui.MenuWidget} Menu of widget
- */
-OO.ui.InlineMenuWidget.prototype.getMenu = function () {
-       return this.menu;
-};
-
-/**
- * Handles menu select events.
- *
- * @param {OO.ui.MenuItemWidget} item Selected menu item
- */
-OO.ui.InlineMenuWidget.prototype.onMenuSelect = function ( item ) {
-       var selectedLabel;
-
-       if ( !item ) {
-               return;
-       }
-
-       selectedLabel = item.getLabel();
-
-       // If the label is a DOM element, clone it, because setLabel will append() it
-       if ( selectedLabel instanceof jQuery ) {
-               selectedLabel = selectedLabel.clone();
-       }
-
-       this.setLabel( selectedLabel );
-};
-
-/**
- * Handles mouse click events.
- *
- * @param {jQuery.Event} e Mouse click event
- */
-OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) {
-       // Skip clicks within the menu
-       if ( $.contains( this.menu.$element[0], e.target ) ) {
-               return;
-       }
-
-       if ( !this.isDisabled() ) {
-               if ( this.menu.isVisible() ) {
-                       this.menu.hide();
-               } else {
-                       this.menu.show();
-               }
-       }
-       return false;
-};
+OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconedElement );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatedElement );
 
 /**
- * Menu section item widget.
+ * Option widget that looks like a button.
  *
- * Use with OO.ui.MenuWidget.
+ * Use together with OO.ui.ButtonSelectWidget.
  *
  * @class
- * @extends OO.ui.OptionWidget
+ * @extends OO.ui.DecoratedOptionWidget
+ * @mixins OO.ui.ButtonedElement
  *
  * @constructor
- * @param {Mixed} data Item data
+ * @param {Mixed} data Option data
  * @param {Object} [config] Configuration options
  */
-OO.ui.MenuSectionItemWidget = function OoUiMenuSectionItemWidget( data, config ) {
+OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) {
        // Parent constructor
-       OO.ui.MenuSectionItemWidget.super.call( this, data, config );
+       OO.ui.ButtonOptionWidget.super.call( this, data, config );
+
+       // Mixin constructors
+       OO.ui.ButtonedElement.call( this, this.$( '<a>' ), config );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-menuSectionItemWidget' );
+       this.$element.addClass( 'oo-ui-buttonOptionWidget' );
+       this.$button.append( this.$element.contents() );
+       this.$element.append( this.$button );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.MenuSectionItemWidget, OO.ui.OptionWidget );
+OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
+OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonedElement );
 
 /* Static Properties */
 
-OO.ui.MenuSectionItemWidget.static.selectable = false;
+// Allow button mouse down events to pass through so they can be handled by the parent select widget
+OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
 
-OO.ui.MenuSectionItemWidget.static.highlightable = false;
+/* Methods */
 
 /**
- * Create an OO.ui.OutlineWidget object.
- *
- * Use with OO.ui.OutlineItemWidget.
- *
- * @class
- * @extends OO.ui.SelectWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
+ * @inheritdoc
  */
-OO.ui.OutlineWidget = function OoUiOutlineWidget( config ) {
-       // Config intialization
-       config = config || {};
+OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
+       OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
 
-       // Parent constructor
-       OO.ui.OutlineWidget.super.call( this, config );
+       if ( this.constructor.static.selectable ) {
+               this.setActive( state );
+       }
 
-       // Initialization
-       this.$element.addClass( 'oo-ui-outlineWidget' );
+       return this;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.OutlineWidget, OO.ui.SelectWidget );
-
 /**
- * Creates an OO.ui.OutlineControlsWidget object.
+ * Menu item widget.
  *
- * Use together with OO.ui.OutlineWidget.js
+ * Use with OO.ui.MenuWidget.
  *
  * @class
+ * @extends OO.ui.DecoratedOptionWidget
  *
  * @constructor
- * @param {OO.ui.OutlineWidget} outline Outline to control
+ * @param {Mixed} data Item data
  * @param {Object} [config] Configuration options
  */
-OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
+OO.ui.MenuItemWidget = function OoUiMenuItemWidget( data, config ) {
        // Configuration initialization
-       config = $.extend( { 'icon': 'add-item' }, config );
-
-       // Parent constructor
-       OO.ui.OutlineControlsWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.GroupElement.call( this, this.$( '<div>' ), config );
-       OO.ui.IconedElement.call( this, this.$( '<div>' ), config );
-
-       // Properties
-       this.outline = outline;
-       this.$movers = this.$( '<div>' );
-       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' )
-       } );
-       this.removeButton = new OO.ui.ButtonWidget( {
-               '$': this.$,
-               'frameless': true,
-               'icon': 'remove',
-               'title': OO.ui.msg( 'ooui-outline-control-remove' )
-       } );
+       config = $.extend( { 'icon': 'check' }, config );
 
-       // 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 ] } );
-       this.removeButton.connect( this, { 'click': [ 'emit', 'remove' ] } );
+       // Parent constructor
+       OO.ui.MenuItemWidget.super.call( this, data, config );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-outlineControlsWidget' );
-       this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
-       this.$movers
-               .addClass( 'oo-ui-outlineControlsWidget-movers' )
-               .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
-       this.$element.append( this.$icon, this.$group, this.$movers );
+       this.$element.addClass( 'oo-ui-menuItemWidget' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
-OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconedElement );
-
-/* Events */
+OO.inheritClass( OO.ui.MenuItemWidget, OO.ui.DecoratedOptionWidget );
 
 /**
- * @event move
- * @param {number} places Number of places to move
+ * Menu section item widget.
+ *
+ * Use with OO.ui.MenuWidget.
+ *
+ * @class
+ * @extends OO.ui.DecoratedOptionWidget
+ *
+ * @constructor
+ * @param {Mixed} data Item data
+ * @param {Object} [config] Configuration options
  */
+OO.ui.MenuSectionItemWidget = function OoUiMenuSectionItemWidget( data, config ) {
+       // Parent constructor
+       OO.ui.MenuSectionItemWidget.super.call( this, data, config );
 
-/**
- * @event remove
- */
+       // Initialization
+       this.$element.addClass( 'oo-ui-menuSectionItemWidget' );
+};
 
-/* Methods */
+/* Setup */
 
-/**
- * Handle outline change events.
- */
-OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
-       var i, len, firstMovable, lastMovable,
-               items = this.outline.getItems(),
-               selectedItem = this.outline.getSelectedItem(),
-               movable = selectedItem && selectedItem.isMovable(),
-               removable = selectedItem && selectedItem.isRemovable();
+OO.inheritClass( OO.ui.MenuSectionItemWidget, OO.ui.DecoratedOptionWidget );
 
-       if ( movable ) {
-               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 );
-       this.removeButton.setDisabled( !removable );
-};
+/* Static Properties */
+
+OO.ui.MenuSectionItemWidget.static.selectable = false;
+
+OO.ui.MenuSectionItemWidget.static.highlightable = false;
 
 /**
  * Creates an OO.ui.OutlineItemWidget object.
@@ -7534,7 +8986,7 @@ OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
  * Use with OO.ui.OutlineWidget.
  *
  * @class
- * @extends OO.ui.OptionWidget
+ * @extends OO.ui.DecoratedOptionWidget
  *
  * @constructor
  * @param {Mixed} data Item data
@@ -7561,7 +9013,7 @@ OO.ui.OutlineItemWidget = function OoUiOutlineItemWidget( data, config ) {
 
 /* Setup */
 
-OO.inheritClass( OO.ui.OutlineItemWidget, OO.ui.OptionWidget );
+OO.inheritClass( OO.ui.OutlineItemWidget, OO.ui.DecoratedOptionWidget );
 
 /* Static Properties */
 
@@ -7576,824 +9028,1293 @@ OO.ui.OutlineItemWidget.static.levels = 3;
 /* Methods */
 
 /**
- * Check if item is movable.
- *
- * Movablilty is used by outline controls.
- *
- * @return {boolean} Item is movable
+ * Check if item is movable.
+ *
+ * Movablilty is used by outline controls.
+ *
+ * @return {boolean} Item is movable
+ */
+OO.ui.OutlineItemWidget.prototype.isMovable = function () {
+       return this.movable;
+};
+
+/**
+ * Check if item is removable.
+ *
+ * Removablilty is used by outline controls.
+ *
+ * @return {boolean} Item is removable
+ */
+OO.ui.OutlineItemWidget.prototype.isRemovable = function () {
+       return this.removable;
+};
+
+/**
+ * Get indentation level.
+ *
+ * @return {number} Indentation level
+ */
+OO.ui.OutlineItemWidget.prototype.getLevel = function () {
+       return this.level;
+};
+
+/**
+ * Set movability.
+ *
+ * Movablilty is used by outline controls.
+ *
+ * @param {boolean} movable Item is movable
+ * @chainable
+ */
+OO.ui.OutlineItemWidget.prototype.setMovable = function ( movable ) {
+       this.movable = !!movable;
+       return this;
+};
+
+/**
+ * Set removability.
+ *
+ * Removablilty is used by outline controls.
+ *
+ * @param {boolean} movable Item is removable
+ * @chainable
+ */
+OO.ui.OutlineItemWidget.prototype.setRemovable = function ( removable ) {
+       this.removable = !!removable;
+       return this;
+};
+
+/**
+ * Set indentation level.
+ *
+ * @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;
+};
+
+/**
+ * Container for content that is overlaid and positioned absolutely.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.LabeledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {number} [width=320] Width of popup in pixels
+ * @cfg {number} [height] Height of popup, omit to use automatic height
+ * @cfg {boolean} [anchor=true] Show anchor 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 {jQuery} [$content] Content to append to the popup's body
+ * @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
+ * @cfg {boolean} [padded] Add padding to the body
+ */
+OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
+       // Config intialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.PopupWidget.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
+       OO.ui.ClippableElement.call( this, this.$( '<div>' ), config );
+
+       // Properties
+       this.visible = false;
+       this.$popup = this.$( '<div>' );
+       this.$head = this.$( '<div>' );
+       this.$body = this.$clippable;
+       this.$anchor = this.$( '<div>' );
+       this.$container = config.$container || this.$( 'body' );
+       this.autoClose = !!config.autoClose;
+       this.$autoCloseIgnore = config.$autoCloseIgnore;
+       this.transitionTimeout = null;
+       this.anchor = false;
+       this.width = config.width !== undefined ? config.width : 320;
+       this.height = config.height !== undefined ? config.height : null;
+       this.align = config.align || 'center';
+       this.closeButton = new OO.ui.ButtonWidget( { '$': this.$, 'framed': false, 'icon': 'close' } );
+       this.onMouseDownHandler = OO.ui.bind( this.onMouseDown, this );
+
+       // Events
+       this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } );
+
+       // Initialization
+       this.toggleAnchor( config.anchor !== undefined ? !!config.anchor : true );
+       this.$body.addClass( 'oo-ui-popupWidget-body' );
+       this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
+       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.$anchor );
+       // Move content, which was added to #$element by OO.ui.Widget, to the body
+       if ( config.$content instanceof jQuery ) {
+               this.$body.append( config.$content );
+       }
+       if ( config.padded ) {
+               this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
+       }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabeledElement );
+OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
+
+/* Events */
+
+/**
+ * @event hide
+ */
+
+/**
+ * @event show
+ */
+
+/* Methods */
+
+/**
+ * Handles mouse down events.
+ *
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
+       if (
+               this.isVisible() &&
+               !$.contains( this.$element[0], e.target ) &&
+               ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
+       ) {
+               this.toggle( false );
+       }
+};
+
+/**
+ * Bind mouse down listener.
+ */
+OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
+       // Capture clicks outside popup
+       this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
+};
+
+/**
+ * Handles close button click events.
+ */
+OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
+       if ( this.isVisible() ) {
+               this.toggle( false );
+       }
+};
+
+/**
+ * Unbind mouse down listener.
  */
-OO.ui.OutlineItemWidget.prototype.isMovable = function () {
-       return this.movable;
+OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
+       this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
 };
 
 /**
- * Check if item is removable.
+ * Set whether to show a anchor.
  *
- * Removablilty is used by outline controls.
- *
- * @return {boolean} Item is removable
+ * @param {boolean} [show] Show anchor, omit to toggle
  */
-OO.ui.OutlineItemWidget.prototype.isRemovable = function () {
-       return this.removable;
+OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
+       show = show === undefined ? !this.anchored : !!show;
+
+       if ( this.anchored !== show ) {
+               if ( show ) {
+                       this.$element.addClass( 'oo-ui-popupWidget-anchored' );
+               } else {
+                       this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
+               }
+               this.anchored = show;
+       }
 };
 
 /**
- * Get indentation level.
+ * Check if showing a anchor.
  *
- * @return {number} Indentation level
+ * @return {boolean} anchor is visible
  */
-OO.ui.OutlineItemWidget.prototype.getLevel = function () {
-       return this.level;
+OO.ui.PopupWidget.prototype.hasAnchor = function () {
+       return this.anchor;
 };
 
 /**
- * Set movability.
- *
- * Movablilty is used by outline controls.
- *
- * @param {boolean} movable Item is movable
- * @chainable
+ * @inheritdoc
  */
-OO.ui.OutlineItemWidget.prototype.setMovable = function ( movable ) {
-       this.movable = !!movable;
+OO.ui.PopupWidget.prototype.toggle = function ( show ) {
+       show = show === undefined ? !this.isVisible() : !!show;
+
+       var change = show !== this.isVisible();
+
+       // Parent method
+       OO.ui.PopupWidget.super.prototype.toggle.call( this, show );
+
+       if ( change ) {
+               if ( show ) {
+                       this.setClipping( true );
+                       if ( this.autoClose ) {
+                               this.bindMouseDownListener();
+                       }
+                       this.updateDimensions();
+               } else {
+                       this.setClipping( false );
+                       if ( this.autoClose ) {
+                               this.unbindMouseDownListener();
+                       }
+               }
+       }
+
        return this;
 };
 
 /**
- * Set removability.
+ * Set the size of the popup.
  *
- * Removablilty is used by outline controls.
+ * Changing the size may also change the popup's position depending on the alignment.
  *
- * @param {boolean} movable Item is removable
+ * @param {number} width Width
+ * @param {number} height Height
+ * @param {boolean} [transition=false] Use a smooth transition
  * @chainable
  */
-OO.ui.OutlineItemWidget.prototype.setRemovable = function ( removable ) {
-       this.removable = !!removable;
-       return this;
+OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
+       this.width = width;
+       this.height = height !== undefined ? height : null;
+       if ( this.isVisible() ) {
+               this.updateDimensions( transition );
+       }
 };
 
 /**
- * Set indentation level.
+ * Update the size and position.
  *
- * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
+ * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
+ * be called automatically.
+ *
+ * @param {boolean} [transition=false] Use a smooth transition
  * @chainable
  */
-OO.ui.OutlineItemWidget.prototype.setLevel = function ( level ) {
-       var levels = this.constructor.static.levels,
-               levelClass = this.constructor.static.levelClass,
-               i = levels;
+OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
+       var widget = this,
+               padding = 10,
+               originOffset = Math.round( this.$element.offset().left ),
+               containerLeft = Math.round( this.$container.offset().left ),
+               containerWidth = this.$container.innerWidth(),
+               containerRight = containerLeft + containerWidth,
+               popupOffset = this.width * ( { 'left': 0, 'center': -0.5, 'right': -1 } )[this.align],
+               anchorWidth = this.$anchor.width(),
+               popupLeft = popupOffset - padding,
+               popupRight = popupOffset + padding + this.width + padding,
+               overlapLeft = ( originOffset + popupLeft ) - containerLeft,
+               overlapRight = containerRight - ( originOffset + popupRight );
 
-       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 );
-               }
+       // 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;
+       }
+
+       // Adjust offset to avoid anchor being rendered too close to the edge
+       if ( this.align === 'right' ) {
+               popupOffset += anchorWidth;
+       } else if ( this.align === 'left' ) {
+               popupOffset -= anchorWidth;
+       }
+
+       // Position body relative to anchor and resize
+       this.$popup.css( {
+               'left': popupOffset,
+               'width': this.width,
+               'height': this.height !== null ? this.height : 'auto'
+       } );
+
+       if ( transition ) {
+               // Prevent transitioning after transition is complete
+               this.transitionTimeout = setTimeout( function () {
+                       widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
+               }, 200 );
+       } else {
+               // Prevent transitioning immediately
+               this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
        }
 
        return this;
 };
 
 /**
- * Option widget that looks like a button.
+ * Search widget.
  *
- * Use together with OO.ui.ButtonSelectWidget.
+ * Combines query and results selection widgets.
  *
  * @class
- * @extends OO.ui.OptionWidget
- * @mixins OO.ui.ButtonedElement
- * @mixins OO.ui.FlaggableElement
+ * @extends OO.ui.Widget
  *
  * @constructor
- * @param {Mixed} data Option data
  * @param {Object} [config] Configuration options
+ * @cfg {string|jQuery} [placeholder] Placeholder text for query input
+ * @cfg {string} [value] Initial query value
  */
-OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) {
+OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
+       // Configuration intialization
+       config = config || {};
+
        // Parent constructor
-       OO.ui.ButtonOptionWidget.super.call( this, data, config );
+       OO.ui.SearchWidget.super.call( this, config );
 
-       // Mixin constructors
-       OO.ui.ButtonedElement.call( this, this.$( '<a>' ), config );
-       OO.ui.FlaggableElement.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.$element.addClass( 'oo-ui-buttonOptionWidget' );
-       this.$button.append( this.$element.contents() );
-       this.$element.append( this.$button );
+       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 );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.OptionWidget );
-OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonedElement );
-OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.FlaggableElement );
+OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
 
-/* Static Properties */
+/* Events */
 
-// Allow button mouse down events to pass through so they can be handled by the parent select widget
-OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
+/**
+ * @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 */
 
 /**
- * @inheritdoc
+ * Handle query key down events.
+ *
+ * @param {jQuery.Event} e Key down event
  */
-OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
-       OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
+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 ( this.constructor.static.selectable ) {
-               this.setActive( state );
+       if ( dir ) {
+               highlightedItem = this.results.getHighlightedItem();
+               if ( !highlightedItem ) {
+                       highlightedItem = this.results.getSelectedItem();
+               }
+               nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
+               this.results.highlightItem( nextItem );
+               nextItem.scrollElementIntoView();
        }
+};
 
-       return this;
+/**
+ * Handle select widget select events.
+ *
+ * Clears existing results. Subclasses should repopulate items according to new query.
+ *
+ * @param {string} value New value
+ */
+OO.ui.SearchWidget.prototype.onQueryChange = function () {
+       // Reset
+       this.results.clearItems();
 };
 
 /**
- * Select widget containing button options.
+ * Handle select widget enter key events.
  *
- * Use together with OO.ui.ButtonOptionWidget.
+ * Selects highlighted item.
  *
- * @class
- * @extends OO.ui.SelectWidget
+ * @param {string} value New value
+ */
+OO.ui.SearchWidget.prototype.onQueryEnter = function () {
+       // Reset
+       this.results.selectItem( this.results.getHighlightedItem() );
+};
+
+/**
+ * Handle select widget highlight events.
  *
- * @constructor
- * @param {Object} [config] Configuration options
+ * @param {OO.ui.OptionWidget} item Highlighted item
+ * @fires highlight
  */
-OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
-       // Parent constructor
-       OO.ui.ButtonSelectWidget.super.call( this, config );
+OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
+       this.emit( 'highlight', item ? item.getData() : null );
+};
 
-       // Initialization
-       this.$element.addClass( 'oo-ui-buttonSelectWidget' );
+/**
+ * Handle select widget select events.
+ *
+ * @param {OO.ui.OptionWidget} item Selected item
+ * @fires select
+ */
+OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
+       this.emit( 'select', item ? item.getData() : null );
 };
 
-/* Setup */
+/**
+ * Get the query input.
+ *
+ * @return {OO.ui.TextInputWidget} Query input
+ */
+OO.ui.SearchWidget.prototype.getQuery = function () {
+       return this.query;
+};
 
-OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
+/**
+ * Get the results list.
+ *
+ * @return {OO.ui.SelectWidget} Select list
+ */
+OO.ui.SearchWidget.prototype.getResults = function () {
+       return this.results;
+};
 
 /**
- * Container for content that is overlaid and positioned absolutely.
+ * Selection of options.
+ *
+ * Use together with OO.ui.OptionWidget.
  *
  * @class
  * @extends OO.ui.Widget
- * @mixins OO.ui.LabeledElement
+ * @mixins OO.ui.GroupElement
  *
  * @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
+ * @cfg {OO.ui.OptionWidget[]} [items] Options to add
  */
-OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
+OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
        // Config intialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.PopupWidget.super.call( this, config );
+       OO.ui.SelectWidget.super.call( this, config );
 
        // Mixin constructors
-       OO.ui.LabeledElement.call( this, this.$( '<div>' ), config );
-       OO.ui.ClippableElement.call( this, this.$( '<div>' ), config );
+       OO.ui.GroupWidget.call( this, this.$element, config );
 
        // Properties
-       this.visible = false;
-       this.$popup = this.$( '<div>' );
-       this.$head = this.$( '<div>' );
-       this.$body = this.$clippable;
-       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 );
+       this.pressed = false;
+       this.selecting = null;
+       this.hashes = {};
+       this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
+       this.onMouseMoveHandler = OO.ui.bind( this.onMouseMove, this );
 
        // Events
-       this.closeButton.connect( this, { 'click': 'onCloseButtonClick' } );
+       this.$element.on( {
+               'mousedown': OO.ui.bind( this.onMouseDown, this ),
+               'mouseover': OO.ui.bind( this.onMouseOver, this ),
+               'mouseleave': OO.ui.bind( this.onMouseLeave, this )
+       } );
 
        // 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.$element.addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' );
+       if ( $.isArray( config.items ) ) {
+               this.addItems( config.items );
        }
-       this.$popup
-               .addClass( 'oo-ui-popupWidget-popup' )
-               .append( this.$head, this.$body );
-       this.$element.hide()
-               .addClass( 'oo-ui-popupWidget' )
-               .append( this.$popup, this.$tail );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabeledElement );
-OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
+OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
+
+// Need to mixin base class as well
+OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
+OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
 
 /* Events */
 
 /**
- * @event hide
+ * @event highlight
+ * @param {OO.ui.OptionWidget|null} item Highlighted item
  */
 
 /**
- * @event show
+ * @event press
+ * @param {OO.ui.OptionWidget|null} item Pressed item
+ */
+
+/**
+ * @event select
+ * @param {OO.ui.OptionWidget|null} item Selected item
+ */
+
+/**
+ * @event choose
+ * @param {OO.ui.OptionWidget|null} item Chosen 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 */
 
 /**
- * Handles mouse down events.
+ * Handle mouse down events.
  *
+ * @private
  * @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();
+OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
+       var item;
+
+       if ( !this.isDisabled() && e.which === 1 ) {
+               this.togglePressed( true );
+               item = this.getTargetItem( e );
+               if ( item && item.isSelectable() ) {
+                       this.pressItem( item );
+                       this.selecting = item;
+                       this.getElementDocument().addEventListener(
+                               'mouseup',
+                               this.onMouseUpHandler,
+                               true
+                       );
+                       this.getElementDocument().addEventListener(
+                               'mousemove',
+                               this.onMouseMoveHandler,
+                               true
+                       );
+               }
        }
+       return false;
 };
 
 /**
- * Bind mouse down listener.
+ * Handle mouse up events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse up event
  */
-OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
-       // Capture clicks outside popup
-       this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
+OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
+       var item;
+
+       this.togglePressed( false );
+       if ( !this.selecting ) {
+               item = this.getTargetItem( e );
+               if ( item && item.isSelectable() ) {
+                       this.selecting = item;
+               }
+       }
+       if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
+               this.pressItem( null );
+               this.chooseItem( this.selecting );
+               this.selecting = null;
+       }
+
+       this.getElementDocument().removeEventListener(
+               'mouseup',
+               this.onMouseUpHandler,
+               true
+       );
+       this.getElementDocument().removeEventListener(
+               'mousemove',
+               this.onMouseMoveHandler,
+               true
+       );
+
+       return false;
 };
 
 /**
- * Handles close button click events.
+ * Handle mouse move events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse move event
  */
-OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
-       if ( this.visible ) {
-               this.hide();
+OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
+       var item;
+
+       if ( !this.isDisabled() && this.pressed ) {
+               item = this.getTargetItem( e );
+               if ( item && item !== this.selecting && item.isSelectable() ) {
+                       this.pressItem( item );
+                       this.selecting = item;
+               }
        }
+       return false;
 };
 
 /**
- * Unbind mouse down listener.
+ * Handle mouse over events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse over event
  */
-OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
-       this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
+OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
+       var item;
+
+       if ( !this.isDisabled() ) {
+               item = this.getTargetItem( e );
+               this.highlightItem( item && item.isHighlightable() ? item : null );
+       }
+       return false;
 };
 
 /**
- * Check if the popup is visible.
+ * Handle mouse leave events.
  *
- * @return {boolean} Popup is visible
+ * @private
+ * @param {jQuery.Event} e Mouse over event
  */
-OO.ui.PopupWidget.prototype.isVisible = function () {
-       return this.visible;
+OO.ui.SelectWidget.prototype.onMouseLeave = function () {
+       if ( !this.isDisabled() ) {
+               this.highlightItem( null );
+       }
+       return false;
 };
 
 /**
- * Set whether to show a tail.
+ * Get the closest item to a jQuery.Event.
  *
- * @return {boolean} Make tail visible
+ * @private
+ * @param {jQuery.Event} e
+ * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
  */
-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' );
+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.
+ *
+ * @return {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.
+ *
+ * @return {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.
+ *
+ * @param {Object} data Item data to search for
+ * @return {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;
 };
 
 /**
- * Check if showing a tail.
+ * Toggle pressed state.
  *
- * @return {boolean} tail is visible
+ * @param {boolean} pressed An option is being pressed
  */
-OO.ui.PopupWidget.prototype.hasTail = function () {
-       return this.tail;
+OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
+       if ( pressed === undefined ) {
+               pressed = !this.pressed;
+       }
+       if ( pressed !== this.pressed ) {
+               this.$element
+                       .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
+                       .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
+               this.pressed = pressed;
+       }
 };
 
 /**
- * Show the context.
+ * Highlight an item.
+ *
+ * Highlighting is mutually exclusive.
  *
- * @fires show
+ * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all
+ * @fires highlight
  * @chainable
  */
-OO.ui.PopupWidget.prototype.show = function () {
-       if ( !this.visible ) {
-               this.setClipping( true );
-               this.$element.show();
-               this.visible = true;
-               this.emit( 'show' );
-               if ( this.autoClose ) {
-                       this.bindMouseDownListener();
+OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
+       var i, len, highlighted,
+               changed = false;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               highlighted = this.items[i] === item;
+               if ( this.items[i].isHighlighted() !== highlighted ) {
+                       this.items[i].setHighlighted( highlighted );
+                       changed = true;
                }
        }
+       if ( changed ) {
+               this.emit( 'highlight', item );
+       }
+
        return this;
 };
 
 /**
- * Hide the context.
+ * Select an item.
  *
- * @fires hide
+ * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
+ * @fires select
  * @chainable
  */
-OO.ui.PopupWidget.prototype.hide = function () {
-       if ( this.visible ) {
-               this.setClipping( false );
-               this.$element.hide();
-               this.visible = false;
-               this.emit( 'hide' );
-               if ( this.autoClose ) {
-                       this.unbindMouseDownListener();
+OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
+       var i, len, selected,
+               changed = false;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               selected = this.items[i] === item;
+               if ( this.items[i].isSelected() !== selected ) {
+                       this.items[i].setSelected( selected );
+                       changed = true;
                }
        }
+       if ( changed ) {
+               this.emit( 'select', item );
+       }
+
        return this;
 };
 
 /**
- * Updates the position and size.
+ * Press an item.
  *
- * @param {number} width Width
- * @param {number} height Height
- * @param {boolean} [transition=false] Use a smooth transition
+ * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
+ * @fires press
  * @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' );
-       }
+OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
+       var i, len, pressed,
+               changed = false;
 
-       if ( overlapRight < 0 ) {
-               popupOffset += overlapRight;
-       } else if ( overlapLeft < 0 ) {
-               popupOffset -= overlapLeft;
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               pressed = this.items[i] === item;
+               if ( this.items[i].isPressed() !== pressed ) {
+                       this.items[i].setPressed( pressed );
+                       changed = true;
+               }
        }
-
-       // 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' );
+       if ( changed ) {
+               this.emit( 'press', item );
        }
 
        return this;
 };
 
 /**
- * Button that shows and hides a popup.
+ * Choose an item.
  *
- * @class
- * @extends OO.ui.ButtonWidget
- * @mixins OO.ui.PopuppableElement
+ * Identical to #selectItem, but may vary in subclasses that want to take additional action when
+ * an item is selected using the keyboard or mouse.
  *
- * @constructor
- * @param {Object} [config] Configuration options
+ * @param {OO.ui.OptionWidget} item Item to choose
+ * @fires choose
+ * @chainable
  */
-OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
-       // Parent constructor
-       OO.ui.PopupButtonWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.PopuppableElement.call( this, config );
+OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
+       this.selectItem( item );
+       this.emit( 'choose', item );
 
-       // Initialization
-       this.$element
-               .addClass( 'oo-ui-popupButtonWidget' )
-               .append( this.popup.$element );
+       return this;
 };
 
-/* Setup */
-
-OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
-OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopuppableElement );
-
-/* Methods */
-
 /**
- * Handles mouse click events.
+ * Get an item relative to another one.
  *
- * @param {jQuery.Event} e Mouse click event
+ * @param {OO.ui.OptionWidget} item Item to start at
+ * @param {number} direction Direction to move in
+ * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
  */
-OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) {
-       // Skip clicks within the popup
-       if ( $.contains( this.popup.$element[0], e.target ) ) {
-               return;
-       }
+OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
+       var inc = direction > 0 ? 1 : -1,
+               len = this.items.length,
+               index = item instanceof OO.ui.OptionWidget ?
+                       $.inArray( item, this.items ) : ( 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 );
 
-       if ( !this.isDisabled() ) {
-               if ( this.popup.isVisible() ) {
-                       this.hidePopup();
-               } else {
-                       this.showPopup();
+       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;
                }
-               OO.ui.PopupButtonWidget.super.prototype.onClick.call( this );
        }
-       return false;
+       return null;
 };
 
 /**
- * Search widget.
- *
- * Combines query and results selection widgets.
- *
- * @class
- * @extends OO.ui.Widget
+ * Get the next selectable item.
  *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string|jQuery} [placeholder] Placeholder text for query input
- * @cfg {string} [value] Initial query value
+ * @return {OO.ui.OptionWidget|null} Item, `null` if ther aren't any selectable items
  */
-OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
-       // Configuration intialization
-       config = config || {};
-
-       // Parent constructor
-       OO.ui.SearchWidget.super.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>' );
+OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
+       var i, len, item;
 
-       // 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 ) );
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               item = this.items[i];
+               if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
+                       return item;
+               }
+       }
 
-       // 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 );
+       return null;
 };
 
-/* Setup */
-
-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.
+ * Add items.
  *
- * @param {jQuery.Event} e Key down event
+ * When items are added with the same values as existing items, the existing items will be
+ * automatically removed before the new items are added.
+ *
+ * @param {OO.ui.OptionWidget[]} items Items to add
+ * @param {number} [index] Index to insert items after
+ * @fires add
+ * @chainable
  */
-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 );
+OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
+       var i, len, item, hash,
+               remove = [];
 
-       if ( dir ) {
-               highlightedItem = this.results.getHighlightedItem();
-               if ( !highlightedItem ) {
-                       highlightedItem = this.results.getSelectedItem();
+       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] );
                }
-               nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
-               this.results.highlightItem( nextItem );
-               nextItem.scrollElementIntoView();
+               this.hashes[hash] = item;
        }
+       if ( remove.length ) {
+               this.removeItems( remove );
+       }
+
+       // Mixin method
+       OO.ui.GroupWidget.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;
 };
 
 /**
- * Handle select widget select events.
+ * Remove items.
  *
- * Clears existing results. Subclasses should repopulate items according to new query.
+ * Items will be detached, not removed, so they can be used later.
  *
- * @param {string} value New value
+ * @param {OO.ui.OptionWidget[]} items Items to remove
+ * @fires remove
+ * @chainable
  */
-OO.ui.SearchWidget.prototype.onQueryChange = function () {
-       // Reset
-       this.results.clearItems();
+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 );
+               }
+       }
+
+       // Mixin method
+       OO.ui.GroupWidget.prototype.removeItems.call( this, items );
+
+       this.emit( 'remove', items );
+
+       return this;
 };
 
 /**
- * Handle select widget enter key events.
+ * Clear all items.
  *
- * Selects highlighted item.
+ * Items will be detached, not removed, so they can be used later.
  *
- * @param {string} value New value
+ * @fires remove
+ * @chainable
  */
-OO.ui.SearchWidget.prototype.onQueryEnter = function () {
-       // Reset
-       this.results.selectItem( this.results.getHighlightedItem() );
-};
+OO.ui.SelectWidget.prototype.clearItems = function () {
+       var items = this.items.slice();
 
-/**
- * Handle select widget highlight events.
- *
- * @param {OO.ui.OptionWidget} item Highlighted item
- * @fires highlight
- */
-OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
-       this.emit( 'highlight', item ? item.getData() : null );
-};
+       // Clear all items
+       this.hashes = {};
+       // Mixin method
+       OO.ui.GroupWidget.prototype.clearItems.call( this );
+       this.selectItem( null );
 
-/**
- * Handle select widget select events.
- *
- * @param {OO.ui.OptionWidget} item Selected item
- * @fires select
- */
-OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
-       this.emit( 'select', item ? item.getData() : null );
-};
+       this.emit( 'remove', items );
 
-/**
- * Get the query input.
- *
- * @return {OO.ui.TextInputWidget} Query input
- */
-OO.ui.SearchWidget.prototype.getQuery = function () {
-       return this.query;
+       return this;
 };
 
 /**
- * Get the results list.
+ * Select widget containing button options.
  *
- * @return {OO.ui.SelectWidget} Select list
- */
-OO.ui.SearchWidget.prototype.getResults = function () {
-       return this.results;
-};
-
-/**
- * Text input widget.
+ * Use together with OO.ui.ButtonOptionWidget.
  *
  * @class
- * @extends OO.ui.InputWidget
+ * @extends OO.ui.SelectWidget
  *
  * @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
- * @cfg {boolean} [autosize=false] Automatically resize to fit content
- * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
  */
-OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
-       config = $.extend( { 'maxRows': 10 }, config );
-
+OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
        // Parent constructor
-       OO.ui.TextInputWidget.super.call( this, config );
-
-       // Properties
-       this.pending = 0;
-       this.multiline = !!config.multiline;
-       this.autosize = !!config.autosize;
-       this.maxRows = config.maxRows;
-
-       // Events
-       this.$input.on( 'keypress', OO.ui.bind( this.onKeyPress, this ) );
-       this.$element.on( 'DOMNodeInsertedIntoDocument', OO.ui.bind( this.onElementAttach, this ) );
+       OO.ui.ButtonSelectWidget.super.call( this, config );
 
        // 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 );
-       }
+       this.$element.addClass( 'oo-ui-buttonSelectWidget' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
-
-/* Events */
+OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
 
 /**
- * User presses enter inside the text box.
+ * Menu widget.
  *
- * Not called if input is multiline.
+ * Use together with OO.ui.MenuItemWidget.
  *
- * @event enter
+ * @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
+ * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
  */
+OO.ui.MenuWidget = function OoUiMenuWidget( config ) {
+       // Config intialization
+       config = config || {};
 
-/* Methods */
+       // Parent constructor
+       OO.ui.MenuWidget.super.call( this, config );
 
-/**
- * Handle 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' );
-       }
-};
+       // Mixin constructors
+       OO.ui.ClippableElement.call( this, this.$group, config );
 
-/**
- * Handle element attach events.
- *
- * @param {jQuery.Event} e Element attach event
- */
-OO.ui.TextInputWidget.prototype.onElementAttach = function () {
-       this.adjustSize();
+       // Properties
+       this.flashing = false;
+       this.visible = false;
+       this.newItems = null;
+       this.autoHide = config.autoHide === undefined || !!config.autoHide;
+       this.$input = config.input ? config.input.$input : null;
+       this.$previousFocus = null;
+       this.isolated = !config.input;
+       this.onKeyDownHandler = OO.ui.bind( this.onKeyDown, this );
+       this.onDocumentMouseDownHandler = OO.ui.bind( this.onDocumentMouseDown, this );
+
+       // Initialization
+       this.$element
+               .hide()
+               .addClass( 'oo-ui-menuWidget' );
 };
 
-/**
- * @inheritdoc
- */
-OO.ui.TextInputWidget.prototype.onEdit = function () {
-       this.adjustSize();
+/* Setup */
 
-       // Parent method
-       return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
-};
+OO.inheritClass( OO.ui.MenuWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.MenuWidget, OO.ui.ClippableElement );
+
+/* Methods */
 
 /**
- * Automatically adjust the size of the text input.
- *
- * This only affects multi-line inputs that are auto-sized.
+ * Handles document mouse down events.
  *
- * @chainable
+ * @param {jQuery.Event} e Key down event
  */
-OO.ui.TextInputWidget.prototype.adjustSize = function () {
-       var $clone, scrollHeight, innerHeight, outerHeight, maxInnerHeight, idealHeight;
-
-       if ( this.multiline && this.autosize ) {
-               $clone = this.$input.clone()
-                       .val( this.$input.val() )
-                       .css( { 'height': 0 } )
-                       .insertAfter( this.$input );
-               // Set inline height property to 0 to measure scroll height
-               scrollHeight = $clone[0].scrollHeight;
-               // Remove inline height property to measure natural heights
-               $clone.css( 'height', '' );
-               innerHeight = $clone.innerHeight();
-               outerHeight = $clone.outerHeight();
-               // Measure max rows height
-               $clone.attr( 'rows', this.maxRows ).css( 'height', 'auto' );
-               maxInnerHeight = $clone.innerHeight();
-               $clone.removeAttr( 'rows' ).css( 'height', '' );
-               $clone.remove();
-               idealHeight = Math.min( maxInnerHeight, scrollHeight );
-               // Only apply inline height when expansion beyond natural height is needed
-               this.$input.css(
-                       'height',
-                       // Use the difference between the inner and outer height as a buffer
-                       idealHeight > outerHeight ? idealHeight + ( outerHeight - innerHeight ) : ''
-               );
+OO.ui.MenuWidget.prototype.onDocumentMouseDown = function ( e ) {
+       if ( !$.contains( this.$element[0], e.target ) ) {
+               this.toggle( false );
        }
-       return this;
 };
 
 /**
- * Get input element.
+ * Handles key down events.
  *
- * @param {Object} [config] Configuration options
- * @return {jQuery} Input element
+ * @param {jQuery.Event} e Key down event
  */
-OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
-       return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="text" />' );
-};
+OO.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
+       var nextItem,
+               handled = false,
+               highlightItem = this.getHighlightedItem();
+
+       if ( !this.isDisabled() && this.isVisible() ) {
+               if ( !highlightItem ) {
+                       highlightItem = this.getSelectedItem();
+               }
+               switch ( e.keyCode ) {
+                       case OO.ui.Keys.ENTER:
+                               this.chooseItem( 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.toggle( false );
+                               handled = true;
+                               break;
+               }
 
-/* Methods */
+               if ( nextItem ) {
+                       this.highlightItem( nextItem );
+                       nextItem.scrollElementIntoView();
+               }
 
-/**
- * Check if input supports multiple lines.
- *
- * @return {boolean}
- */
-OO.ui.TextInputWidget.prototype.isMultiline = function () {
-       return !!this.multiline;
+               if ( handled ) {
+                       e.preventDefault();
+                       e.stopPropagation();
+                       return false;
+               }
+       }
 };
 
 /**
- * Check if input automatically adjusts its size.
- *
- * @return {boolean}
+ * Bind key down listener.
  */
-OO.ui.TextInputWidget.prototype.isAutosizing = function () {
-       return !!this.autosize;
+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 );
+       }
 };
 
 /**
- * Check if input is pending.
- *
- * @return {boolean}
+ * Unbind key down listener.
  */
-OO.ui.TextInputWidget.prototype.isPending = function () {
-       return !!this.pending;
+OO.ui.MenuWidget.prototype.unbindKeyDownListener = function () {
+       if ( this.$input ) {
+               this.$input.off( 'keydown' );
+       } else {
+               this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
+       }
 };
 
 /**
- * Increase the pending stack.
+ * Choose an item.
  *
+ * This will close the menu when done, unlike selectItem which only changes selection.
+ *
+ * @param {OO.ui.OptionWidget} item Item to choose
  * @chainable
  */
-OO.ui.TextInputWidget.prototype.pushPending = function () {
-       if ( this.pending === 0 ) {
-               this.$element.addClass( 'oo-ui-textInputWidget-pending' );
-               this.$input.addClass( 'oo-ui-texture-pending' );
+OO.ui.MenuWidget.prototype.chooseItem = function ( item ) {
+       var widget = this;
+
+       // Parent method
+       OO.ui.MenuWidget.super.prototype.chooseItem.call( this, item );
+
+       if ( item && !this.flashing ) {
+               this.flashing = true;
+               item.flash().done( function () {
+                       widget.toggle( false );
+                       widget.flashing = false;
+               } );
+       } else {
+               this.toggle( false );
        }
-       this.pending++;
 
        return this;
 };
 
 /**
- * Reduce the pending stack.
+ * Add items.
  *
- * Clamped at zero.
+ * Adding an existing item (by value) will move it.
  *
+ * @param {OO.ui.MenuItemWidget[]} items Items to add
+ * @param {number} [index] Index to insert items after
  * @chainable
  */
-OO.ui.TextInputWidget.prototype.popPending = function () {
-       if ( this.pending === 1 ) {
-               this.$element.removeClass( 'oo-ui-textInputWidget-pending' );
-               this.$input.removeClass( 'oo-ui-texture-pending' );
+OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
+       var i, len, item;
+
+       // Parent method
+       OO.ui.MenuWidget.super.prototype.addItems.call( this, items, index );
+
+       // Auto-initialize
+       if ( !this.newItems ) {
+               this.newItems = [];
+       }
+
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+               if ( this.isVisible() ) {
+                       // Defer fitting label until
+                       item.fitLabel();
+               } else {
+                       this.newItems.push( item );
+               }
        }
-       this.pending = Math.max( 0, this.pending - 1 );
 
        return this;
 };
 
 /**
- * Select the contents of the input.
- *
- * @chainable
+ * @inheritdoc
  */
-OO.ui.TextInputWidget.prototype.select = function () {
-       this.$input.select();
+OO.ui.MenuWidget.prototype.toggle = function ( visible ) {
+       visible = !!visible && !!this.items.length;
+
+       var i, len,
+               change = visible !== this.isVisible();
+
+       // Parent method
+       OO.ui.MenuWidget.super.prototype.toggle.call( this, visible );
+
+       if ( change ) {
+               if ( visible ) {
+                       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 && this.newItems.length ) {
+                               for ( i = 0, len = this.newItems.length; i < len; i++ ) {
+                                       this.newItems[i].fitLabel();
+                               }
+                               this.newItems = null;
+                       }
+                       this.setClipping( true );
+
+                       // Auto-hide
+                       if ( this.autoHide ) {
+                               this.getElementDocument().addEventListener(
+                                       'mousedown', this.onDocumentMouseDownHandler, true
+                               );
+                       }
+               } else {
+                       this.unbindKeyDownListener();
+                       if ( this.isolated && this.$previousFocus ) {
+                               this.$previousFocus.focus();
+                               this.$previousFocus = null;
+                       }
+                       this.getElementDocument().removeEventListener(
+                               'mousedown', this.onDocumentMouseDownHandler, true
+                       );
+                       this.setClipping( false );
+               }
+       }
+
        return this;
 };
 
@@ -8437,29 +10358,24 @@ OO.ui.TextInputMenuWidget.prototype.onWindowResize = function () {
 };
 
 /**
- * Show the menu.
- *
- * @chainable
+ * @inheritdoc
  */
-OO.ui.TextInputMenuWidget.prototype.show = function () {
-       // Parent method
-       OO.ui.TextInputMenuWidget.super.prototype.show.call( this );
+OO.ui.TextInputMenuWidget.prototype.toggle = function ( visible ) {
+       visible = !!visible;
 
-       this.position();
-       this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
-       return this;
-};
+       var change = visible !== this.isVisible();
 
-/**
- * Hide the menu.
- *
- * @chainable
- */
-OO.ui.TextInputMenuWidget.prototype.hide = function () {
        // Parent method
-       OO.ui.TextInputMenuWidget.super.prototype.hide.call( this );
+       OO.ui.TextInputMenuWidget.super.prototype.toggle.call( this, visible );
 
-       this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
+       if ( change ) {
+               if ( this.isVisible() ) {
+                       this.position();
+                       this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
+               } else {
+                       this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
+               }
+       }
        return this;
 };
 
@@ -8492,130 +10408,37 @@ OO.ui.TextInputMenuWidget.prototype.position = function () {
                        delete dimensions.left;
                }
        }
-
        this.$element.css( dimensions );
        this.setIdealSize( $container.width() );
-       return this;
-};
-
-/**
- * Width with on and off states.
- *
- * Mixin for widgets with a boolean state.
- *
- * @abstract
- * @class
- *
- * @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.
- *
- * @return {boolean}
- */
-OO.ui.ToggleWidget.prototype.getValue = function () {
-       return this.value;
-};
 
-/**
- * Set the value of the toggle.
- *
- * @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;
 };
 
 /**
- * Button that toggles on and off.
+ * Create an OO.ui.OutlineWidget object.
+ *
+ * Use with OO.ui.OutlineItemWidget.
  *
  * @class
- * @extends OO.ui.ButtonWidget
- * @mixins OO.ui.ToggleWidget
+ * @extends OO.ui.SelectWidget
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {boolean} [value=false] Initial value
  */
-OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
-       // Configuration initialization
+OO.ui.OutlineWidget = function OoUiOutlineWidget( config ) {
+       // Config intialization
        config = config || {};
 
        // Parent constructor
-       OO.ui.ToggleButtonWidget.super.call( this, config );
-
-       // Mixin constructors
-       OO.ui.ToggleWidget.call( this, config );
+       OO.ui.OutlineWidget.super.call( this, config );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-toggleButtonWidget' );
+       this.$element.addClass( 'oo-ui-outlineWidget' );
 };
 
 /* Setup */
 
-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.isDisabled() ) {
-               this.setValue( !this.value );
-       }
-
-       // Parent method
-       return OO.ui.ToggleButtonWidget.super.prototype.onClick.call( this );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
-       value = !!value;
-       if ( value !== this.value ) {
-               this.setActive( value );
-       }
-
-       // Parent method (from mixin)
-       OO.ui.ToggleWidget.prototype.setValue.call( this, value );
-
-       return this;
-};
+OO.inheritClass( OO.ui.OutlineWidget, OO.ui.SelectWidget );
 
 /**
  * Switch that slides on and off.
index a249314..8c2b3f1 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.1.0-pre (85cfc2e735)
+ * OOjs UI v0.1.0-pre (d2451ac748)
  * 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: 2014-07-03T02:33:09Z
+ * Date: 2014-07-14T16:49:51Z
  */
 /* Textures */
 
   direction: ltr;
 }
 
-.oo-ui-dialog {
-  position: fixed;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  left: 0;
-  padding: 1em;
-  line-height: 1em;
-  /* Fix for strange opacity-related rendering issues.
-          CAUTION: -webkit-backface-visibility: hidden; is EXTREMELY DANGEROUS.
-          If applied to a VE surface directly, it will break selection of
-          FocusableNodes, and in the past it's caused transparent PNGs to
-          render as opaque black images. For some reason applying it to the dialog
-          wrapper in the main document fixes opacity-related behavior in the iframe
-          document, but doesn't break the surface inside the iframe. */
-
-  -webkit-backface-visibility: hidden;
-          backface-visibility: hidden;
-}
-
-.oo-ui-dialog > .oo-ui-window-frame {
-  position: fixed;
-  right: 0;
-  left: 0;
-  min-height: 12em;
-  margin: auto;
-  overflow: hidden;
-}
-
-.oo-ui-dialog > .oo-ui-window-frame .oo-ui-frame {
-  width: 100%;
-  height: 100%;
-}
-
-.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-buttonedElement-framed {
-  float: left;
-}
-
-.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-flaggableElement-primary,
-.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-flaggableElement-constructive,
-.oo-ui-dialog-content .oo-ui-window-foot .oo-ui-flaggableElement-destructive {
-  float: right;
-}
-
-.oo-ui-dialog-content-footless .oo-ui-window-foot {
-  display: none;
-}
-
 .oo-ui-frame {
   padding: 0;
   margin: 0;
   background-repeat: no-repeat;
 }
 
-.oo-ui-window-head {
+.oo-ui-window {
+  line-height: 1em;
+}
+
+.oo-ui-window-frame > .oo-ui-frame {
+  width: 100%;
+  height: 100%;
+}
+
+.oo-ui-window-head,
+.oo-ui-window-foot {
   -webkit-user-select: none;
      -moz-user-select: none;
       -ms-user-select: none;
   -webkit-touch-callout: none;
 }
 
-.oo-ui-window-icon {
-  float: left;
-  background-position: center center;
-  background-repeat: no-repeat;
+.oo-ui-window-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
 }
 
-.oo-ui-window-title {
-  float: left;
-  white-space: nowrap;
-  cursor: default;
+.oo-ui-windowManager-modal > .oo-ui-dialog {
+  position: fixed;
+  width: 0;
+  height: 0;
+  overflow: hidden;
 }
 
-.oo-ui-window-overlay {
-  position: absolute;
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup {
   top: 0;
+  right: 0;
+  bottom: 0;
   left: 0;
+  width: auto;
+  height: auto;
+  padding: 1em;
 }
 
-.oo-ui-buttonedElement .oo-ui-buttonedElement-button {
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup > .oo-ui-window-frame {
+  position: fixed;
+  right: 0;
+  left: 0;
+  max-width: 100%;
+  max-height: 100%;
+  margin: auto;
+  overflow: hidden;
+}
+
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup > .oo-ui-window-frame .oo-ui-frame {
+  width: 100%;
+  height: 100%;
+}
+
+.oo-ui-buttonedElement > .oo-ui-buttonedElement-button {
   display: inline-block;
   vertical-align: middle;
   cursor: pointer;
   -webkit-touch-callout: none;
 }
 
-.oo-ui-buttonedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
+.oo-ui-buttonedElement .oo-ui-buttonedElement-button > .oo-ui-iconedElement-icon {
   display: none;
   margin-left: 0;
 }
 
-.oo-ui-buttonedElement .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator {
+.oo-ui-buttonedElement .oo-ui-buttonedElement-button > .oo-ui-indicatedElement-indicator {
   display: none;
   margin-right: -0.75em;
 }
 
-.oo-ui-buttonedElement.oo-ui-widget-disabled .oo-ui-buttonedElement-button {
+.oo-ui-buttonedElement.oo-ui-widget-disabled .oo-ui-buttonedElement-button {
   cursor: default;
 }
 
-.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 {
+.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 {
   display: inline-block;
   vertical-align: middle;
   background-position: center center;
   display: inline-block;
 }
 
-.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+.oo-ui-buttonedElement-frameless .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
   display: inline-block;
   margin-left: 0.25em;
   vertical-align: middle;
 }
 
-.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button {
   display: inline-block;
   text-align: center;
   vertical-align: top;
 }
 
-.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
+.oo-ui-buttonedElement-framed .oo-ui-buttonedElement-button > .oo-ui-labeledElement-label {
   display: inline-block;
   line-height: 1.9em;
   vertical-align: middle;
 }
 
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
-.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-active,
+.oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
   cursor: default;
 }
 
 }
 
 .oo-ui-fieldLayout.oo-ui-fieldLayout-align-top > .oo-ui-labeledElement-label {
+  display: inline-block;
   padding: 0.5em 0;
 }
 
 }
 
 .oo-ui-labelWidget {
+  display: inline-block;
   padding: 0.5em 0;
 }
 
 }
 
 .oo-ui-popupTool .oo-ui-popupWidget-popup,
-.oo-ui-popupTool .oo-ui-popupWidget-tail {
+.oo-ui-popupTool .oo-ui-popupWidget-anchor {
   z-index: 4;
 }
 
   white-space: nowrap;
 }
 
-.oo-ui-optionWidget .oo-ui-iconedElement-icon,
-.oo-ui-optionWidget .oo-ui-indicatedElement-indicator {
+.oo-ui-decoratedOptionWidget .oo-ui-iconedElement-icon,
+.oo-ui-decoratedOptionWidget .oo-ui-indicatedElement-indicator {
   position: absolute;
   top: 50%;
   width: 2em;
   background-repeat: no-repeat;
 }
 
-.oo-ui-optionWidget .oo-ui-iconedElement-icon {
+.oo-ui-decoratedOptionWidget .oo-ui-iconedElement-icon {
   left: 0.5em;
 }
 
-.oo-ui-optionWidget .oo-ui-indicatedElement-indicator {
+.oo-ui-decoratedOptionWidget .oo-ui-indicatedElement-indicator {
   right: 0.5em;
 }
 
 
 .oo-ui-popupWidget-popup {
   position: absolute;
+  z-index: 1;
   overflow: hidden;
 }
 
-.oo-ui-popupWidget-tail {
+.oo-ui-popupWidget-anchor {
+  z-index: 1;
   display: none;
 }
 
-.oo-ui-popupWidget-tailed .oo-ui-popupWidget-popup {
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-popup {
   margin-top: 7px;
 }
 
-.oo-ui-popupWidget-tailed .oo-ui-popupWidget-tail {
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor {
   position: absolute;
   display: block;
   background-repeat: no-repeat;
 }
 
 .oo-ui-popupWidget-body {
+  overflow: hidden;
   clear: both;
 }
 
+.oo-ui-popupWidget-body-padded {
+  padding: 0 1em;
+}
+
 .oo-ui-buttonGroupWidget {
   border-radius: 0.3em;
 }
diff --git a/resources/lib/sinonjs/sinon-1.10.3.js b/resources/lib/sinonjs/sinon-1.10.3.js
new file mode 100644 (file)
index 0000000..703414d
--- /dev/null
@@ -0,0 +1,5073 @@
+/**
+ * Sinon.JS 1.10.3, 2014/07/11
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
+ *
+ * (The BSD License)
+ * 
+ * Copyright (c) 2010-2014, 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 if (typeof fn === "function") { formatio = fn(samsam); } }
+define.amd = {};
+((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 isReallyNaN(val) {
+        return typeof val === 'number' && isNaN(val);
+    }
+
+    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");
+            } else if (wrappedMethod.restore && wrappedMethod.restore.sinon) {
+                error = new TypeError("Attempted to wrap " + property + " which is already wrapped");
+            } else 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 && 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') {
+                if (isReallyNaN(a) && isReallyNaN(b)) {
+                    return true;
+                } else {
+                    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;
+            }
+
+            if (a instanceof RegExp && b instanceof RegExp) {
+              return (a.source === b.source) && (a.global === b.global) &&
+                (a.ignoreCase === b.ignoreCase) && (a.multiline === b.multiline);
+            }
+
+            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 (!(prop in b)) {
+                    return false;
+                }
+
+                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 && typeof require == "function";
+    var isAMD = typeof define === 'function' && typeof define.amd === 'object' && define.amd;
+
+    function makePublicAPI(require, exports, module) {
+        module.exports = sinon;
+        sinon.spy = require("./sinon/spy");
+        sinon.spyCall = require("./sinon/call");
+        sinon.behavior = require("./sinon/behavior");
+        sinon.stub = require("./sinon/stub");
+        sinon.mock = require("./sinon/mock");
+        sinon.collection = require("./sinon/collection");
+        sinon.assert = require("./sinon/assert");
+        sinon.sandbox = require("./sinon/sandbox");
+        sinon.test = require("./sinon/test");
+        sinon.testCase = require("./sinon/test_case");
+        sinon.match = require("./sinon/match");
+    }
+
+    if (isAMD) {
+        define(makePublicAPI);
+    } else if (isNode) {
+        try {
+            formatio = require("formatio");
+        } catch (e) {}
+        makePublicAPI(require, exports, module);
+    }
+
+    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 && typeof require == "function";
+
+    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 (!arguments.length) {
+            throw new TypeError("Matcher expected");
+        } else if (!isMatcher(m2)) {
+            m2 = match(m2);
+        }
+        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 (!arguments.length) {
+            throw new TypeError("Matcher expected");
+        } else if (!isMatcher(m2)) {
+            m2 = match(m2);
+        }
+        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");
+
+    sinon.match = match;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = match; });
+    } else if (commonJSModule) {
+        module.exports = 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 && typeof require == "function";
+    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.proxy.prototype && 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
+
+    sinon.spyCall = createSpyCall;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = createSpyCall; });
+    } else if (commonJSModule) {
+        module.exports = 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 && typeof require == "function";
+    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++);
+
+            // Make call properties available from within the spied function:
+            createCallProperties.call(this);
+
+            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);
+
+            // Make return value and exception available in the calls:
+            createCallProperties.call(this);
+
+            if (exception !== undefined) {
+                throw exception;
+            }
+
+            return returnValue;
+        },
+
+        named: function named(name) {
+            this.displayName = name;
+            return this;
+        },
+
+        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;
+    sinon.spy = spy;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = spy; });
+    } else if (commonJSModule) {
+        module.exports = 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 && typeof require == "function";
+
+    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);
+        }
+    }
+
+    sinon.behavior = proto;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = proto; });
+    } else if (commonJSModule) {
+        module.exports = 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 && typeof require == "function";
+
+    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;
+    }()));
+
+    sinon.stub = stub;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = stub; });
+    } else if (commonJSModule) {
+        module.exports = 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 && typeof require == "function";
+    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;
+            }
+        };
+    }());
+
+    sinon.mock = mock;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = mock; });
+    } else if (commonJSModule) {
+        module.exports = 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 && typeof require == "function";
+    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;
+        }
+    };
+
+    sinon.collection = collection;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = collection; });
+    } else if (commonJSModule) {
+        module.exports = 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) {
+    // node expects setTimeout/setInterval to return a fn object w/ .ref()/.unref()
+    // browsers, a number.
+    // see https://github.com/cjohansen/Sinon.JS/pull/436
+    var timeoutResult = setTimeout(function() {}, 0);
+    var addTimerReturnsObject = typeof timeoutResult === 'object';
+    clearTimeout(timeoutResult);
+
+    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;
+        }
+
+        if (addTimerReturnsObject) {
+            return {
+                id: toId,
+                ref: function() {},
+                unref: function() {}
+            };
+        }
+        else {
+            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 (!timerId) {
+                // null appears to be allowed in most browsers, and appears to be relied upon by some libraries, like Bootstrap carousel
+                return;
+            }
+            if (!this.timeouts) {
+                this.timeouts = [];
+            }
+            // in Node, timerId is an object with .ref()/.unref(), and
+            // its .id field is the actual timer id.
+            if (typeof timerId === 'object') {
+              timerId = timerId.id
+            }
+            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.ProgressEvent = function ProgressEvent(type, progressEventRaw, target) {
+        this.initEvent(type, false, false, target);
+        this.loaded = progressEventRaw.loaded || null;
+        this.total = progressEventRaw.total || null;
+    };
+
+    sinon.ProgressEvent.prototype = new sinon.Event();
+
+    sinon.ProgressEvent.prototype.constructor =  sinon.ProgressEvent;
+
+    sinon.CustomEvent = function CustomEvent(type, customData, target) {
+        this.initEvent(type, false, false, target);
+        this.detail = customData.detail || null;
+    };
+
+    sinon.CustomEvent.prototype = new sinon.Event();
+
+    sinon.CustomEvent.prototype.constructor =  sinon.CustomEvent;
+
+    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 = {};
+    }
+
+    var supportsProgress = typeof ProgressEvent !== "undefined";
+    var supportsCustomEvent = typeof CustomEvent !== "undefined";
+    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 = xhr.supportsXHR && '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.call(this, 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 verifyRequestOpened(xhr) {
+        if (xhr.readyState != FakeXMLHttpRequest.OPENED) {
+            throw new Error("INVALID_STATE_ERR - " + xhr.readyState);
+        }
+    }
+
+    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));
+                    if (supportsProgress) {
+                        this.upload.dispatchEvent(new sinon.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) {
+            verifyRequestOpened(this);
+            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.status = typeof status == "number" ? status : 200;
+            this.statusText = FakeXMLHttpRequest.statusCodes[this.status];
+            this.setResponseHeaders(headers || {});
+            this.setResponseBody(body || "");
+        },
+
+        uploadProgress: function uploadProgress(progressEventRaw) {
+            if (supportsProgress) {
+                this.upload.dispatchEvent(new sinon.ProgressEvent("progress", progressEventRaw));
+            }
+        },
+
+        uploadError: function uploadError(error) {
+            if (supportsCustomEvent) {
+                this.upload.dispatchEvent(new sinon.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;
+
+})((function(){ return 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;
+    }
+
+    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);
+            }
+        },
+
+        log: function(response, request) {
+            var str;
+
+            str =  "Request:\n"  + sinon.format(request)  + "\n\n";
+            str += "Response:\n" + sinon.format(response) + "\n\n";
+
+            sinon.log(str);
+        },
+
+        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, queue.length);
+            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) {
+                    sinon.fakeServer.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 && typeof require == "function") {
+    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;
+            sandbox.injectedKeys.push(key);
+        } 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;
+        },
+
+        restore: function () {
+            sinon.collection.restore.apply(this, arguments);
+            this.restoreContext();
+        },
+
+        restoreContext: function () {
+            if (this.injectedKeys) {
+                for (var i = 0, j = this.injectedKeys.length; i < j; i++) {
+                    delete this.injectInto[this.injectedKeys[i]];
+                }
+                this.injectedKeys = [];
+            }
+        },
+
+        create: function (config) {
+            if (!config) {
+                return sinon.create(sinon.sandbox);
+            }
+
+            var sandbox = prepareSandboxFromConfig(config);
+            sandbox.args = sandbox.args || [];
+            sandbox.injectedKeys = [];
+            sandbox.injectInto = config.injectInto;
+            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 define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = sinon.sandbox; });
+    } else 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 && typeof require == "function";
+
+    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);
+        }
+
+        function sinonSandboxedTest() {
+            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;
+        };
+
+        if (callback.length) {
+            return function sinonAsyncSandboxedTest(callback) {
+                return sinonSandboxedTest.apply(this, arguments);
+            };
+        }
+
+        return sinonSandboxedTest;
+    }
+
+    test.config = {
+        injectIntoThis: true,
+        injectInto: null,
+        properties: ["spy", "stub", "mock", "clock", "server", "requests"],
+        useFakeTimers: true,
+        useFakeServer: true
+    };
+
+    sinon.test = test;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = test; });
+    } else if (commonJSModule) {
+        module.exports = 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 && typeof require == "function";
+
+    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;
+    }
+
+    sinon.testCase = testCase;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = testCase; });
+    } else if (commonJSModule) {
+        module.exports = 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 && typeof require == "function";
+    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;
+        },
+
+        match: function match(actual, expectation) {
+            var matcher = sinon.match(expectation);
+            if (matcher.test(actual)) {
+                assert.pass("match");
+            } else {
+                var formatted = [
+                    "expected value to match",
+                    "    expected = " + sinon.format(expectation),
+                    "    actual = " + sinon.format(actual)
+                ]
+                failAssertion(this, formatted.join("\n"));
+            }
+        }
+    };
+
+    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");
+
+    sinon.assert = assert;
+
+    if (typeof define === "function" && define.amd) {
+        define(["module"], function(module) { module.exports = assert; });
+    } else if (commonJSModule) {
+        module.exports = assert;
+    }
+}(typeof sinon == "object" && sinon || null, typeof window != "undefined" ? window : (typeof self != "undefined") ? self : global));
+
+/**
+ * @depend ../../sinon.js
+ * @depend event.js
+ */
+/*jslint eqeqeq: false, onevar: false*/
+/*global sinon, module, require, XDomainRequest*/
+/**
+ * Fake XDomainRequest object
+ */
+
+if (typeof sinon == "undefined") {
+    this.sinon = {};
+}
+sinon.xdr = { XDomainRequest: this.XDomainRequest };
+
+// wrapper for global
+(function (global) {
+    var xdr = sinon.xdr;
+    xdr.GlobalXDomainRequest = global.XDomainRequest;
+    xdr.supportsXDR = typeof xdr.GlobalXDomainRequest != "undefined";
+    xdr.workingXDR = xdr.supportsXDR ? xdr.GlobalXDomainRequest :  false;
+
+    function FakeXDomainRequest() {
+        this.readyState = FakeXDomainRequest.UNSENT;
+        this.requestBody = null;
+        this.requestHeaders = {};
+        this.status = 0;
+        this.timeout = null;
+
+        if (typeof FakeXDomainRequest.onCreate == "function") {
+            FakeXDomainRequest.onCreate(this);
+        }
+    }
+
+    function verifyState(xdr) {
+        if (xdr.readyState !== FakeXDomainRequest.OPENED) {
+            throw new Error("INVALID_STATE_ERR");
+        }
+
+        if (xdr.sendFlag) {
+            throw new Error("INVALID_STATE_ERR");
+        }
+    }
+
+    function verifyRequestSent(xdr) {
+        if (xdr.readyState == FakeXDomainRequest.UNSENT) {
+            throw new Error("Request not sent");
+        }
+        if (xdr.readyState == FakeXDomainRequest.DONE) {
+            throw new Error("Request done");
+        }
+    }
+
+    function verifyResponseBodyType(body) {
+        if (typeof body != "string") {
+            var error = new Error("Attempted to respond to fake XDomainRequest with " +
+                                  body + ", which is not a string.");
+            error.name = "InvalidBodyException";
+            throw error;
+        }
+    }
+
+    sinon.extend(FakeXDomainRequest.prototype, sinon.EventTarget, {
+        open: function open(method, url) {
+            this.method = method;
+            this.url = url;
+
+            this.responseText = null;
+            this.sendFlag = false;
+
+            this.readyStateChange(FakeXDomainRequest.OPENED);
+        },
+
+        readyStateChange: function readyStateChange(state) {
+            this.readyState = state;
+            var eventName = '';
+            switch (this.readyState) {
+            case FakeXDomainRequest.UNSENT:
+                break;
+            case FakeXDomainRequest.OPENED:
+                break;
+            case FakeXDomainRequest.LOADING:
+                if (this.sendFlag){
+                    //raise the progress event
+                    eventName = 'onprogress';
+                }
+                break;
+            case FakeXDomainRequest.DONE:
+                if (this.isTimeout){
+                    eventName = 'ontimeout'
+                }
+                else if (this.errorFlag || (this.status < 200 || this.status > 299)) {
+                    eventName = 'onerror';
+                }
+                else {
+                    eventName = 'onload'
+                }
+                break;
+            }
+
+            // raising event (if defined)
+            if (eventName) {
+                if (typeof this[eventName] == "function") {
+                    try {
+                        this[eventName]();
+                    } catch (e) {
+                        sinon.logError("Fake XHR " + eventName + " handler", e);
+                    }
+                }
+            }
+        },
+
+        send: function send(data) {
+            verifyState(this);
+
+            if (!/^(get|head)$/i.test(this.method)) {
+                this.requestBody = data;
+            }
+            this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8";
+
+            this.errorFlag = false;
+            this.sendFlag = true;
+            this.readyStateChange(FakeXDomainRequest.OPENED);
+
+            if (typeof this.onSend == "function") {
+                this.onSend(this);
+            }
+        },
+
+        abort: function abort() {
+            this.aborted = true;
+            this.responseText = null;
+            this.errorFlag = true;
+
+            if (this.readyState > sinon.FakeXDomainRequest.UNSENT && this.sendFlag) {
+                this.readyStateChange(sinon.FakeXDomainRequest.DONE);
+                this.sendFlag = false;
+            }
+        },
+
+        setResponseBody: function setResponseBody(body) {
+            verifyRequestSent(this);
+            verifyResponseBodyType(body);
+
+            var chunkSize = this.chunkSize || 10;
+            var index = 0;
+            this.responseText = "";
+
+            do {
+                this.readyStateChange(FakeXDomainRequest.LOADING);
+                this.responseText += body.substring(index, index + chunkSize);
+                index += chunkSize;
+            } while (index < body.length);
+
+            this.readyStateChange(FakeXDomainRequest.DONE);
+        },
+
+        respond: function respond(status, contentType, body) {
+            // content-type ignored, since XDomainRequest does not carry this
+            // we keep the same syntax for respond(...) as for FakeXMLHttpRequest to ease
+            // test integration across browsers
+            this.status = typeof status == "number" ? status : 200;
+            this.setResponseBody(body || "");
+        },
+
+        simulatetimeout: function(){
+            this.status = 0;
+            this.isTimeout = true;
+            // Access to this should actually throw an error
+            this.responseText = undefined;
+            this.readyStateChange(FakeXDomainRequest.DONE);
+        }
+    });
+
+    sinon.extend(FakeXDomainRequest, {
+        UNSENT: 0,
+        OPENED: 1,
+        LOADING: 3,
+        DONE: 4
+    });
+
+    sinon.useFakeXDomainRequest = function () {
+        sinon.FakeXDomainRequest.restore = function restore(keepOnCreate) {
+            if (xdr.supportsXDR) {
+                global.XDomainRequest = xdr.GlobalXDomainRequest;
+            }
+
+            delete sinon.FakeXDomainRequest.restore;
+
+            if (keepOnCreate !== true) {
+                delete sinon.FakeXDomainRequest.onCreate;
+            }
+        };
+        if (xdr.supportsXDR) {
+            global.XDomainRequest = sinon.FakeXDomainRequest;
+        }
+        return sinon.FakeXDomainRequest;
+    };
+
+    sinon.FakeXDomainRequest = FakeXDomainRequest;
+})(this);
+
+if (typeof module == "object" && typeof require == "function") {
+    module.exports = sinon;
+}
+
+return sinon;}.call(typeof window != 'undefined' && window || {}));
diff --git a/resources/lib/sinonjs/sinon-1.9.0.js b/resources/lib/sinonjs/sinon-1.9.0.js
deleted file mode 100644 (file)
index 574e617..0000000
+++ /dev/null
@@ -1,4794 +0,0 @@
-/**
- * Sinon.JS 1.9.0, 2014/03/05
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
- *
- * (The BSD License)
- * 
- * Copyright (c) 2010-2014, 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;
-            }
-
-            if (a instanceof RegExp && b instanceof RegExp) {
-              return (a.source === b.source) && (a.global === b.global) && 
-                (a.ignoreCase === b.ignoreCase) && (a.multiline === b.multiline);
-            }
-
-            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 (!arguments.length) {
-            throw new TypeError("Matcher expected");
-        } else if (!isMatcher(m2)) {
-            m2 = match(m2);
-        }
-        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 (!arguments.length) {
-            throw new TypeError("Matcher expected");
-        } else if (!isMatcher(m2)) {
-            m2 = match(m2);
-        }
-        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.proxy.prototype && 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.ProgressEvent = function ProgressEvent(type, progressEventRaw, target) {
-        this.initEvent(type, false, false, target);
-        this.loaded = progressEventRaw.loaded || null;
-        this.total = progressEventRaw.total || null;
-    };
-
-    sinon.ProgressEvent.prototype = new sinon.Event();
-
-    sinon.ProgressEvent.prototype.constructor =  sinon.ProgressEvent;
-
-    sinon.CustomEvent = function CustomEvent(type, customData, target) {
-        this.initEvent(type, false, false, target);
-        this.detail = customData.detail || null;
-    };
-
-    sinon.CustomEvent.prototype = new sinon.Event();
-
-    sinon.CustomEvent.prototype.constructor =  sinon.CustomEvent;
-
-    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 = {};
-    }
-
-    var supportsProgress = typeof ProgressEvent !== "undefined";
-    var supportsCustomEvent = typeof CustomEvent !== "undefined";
-    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.call(this, 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 verifyRequestOpened(xhr) {
-        if (xhr.readyState != FakeXMLHttpRequest.OPENED) {
-            throw new Error("INVALID_STATE_ERR - " + xhr.readyState);
-        }
-    }
-
-    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));
-                    if (supportsProgress) {
-                        this.upload.dispatchEvent(new sinon.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) {
-            verifyRequestOpened(this);
-            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.status = typeof status == "number" ? status : 200;
-            this.statusText = FakeXMLHttpRequest.statusCodes[this.status];
-            this.setResponseHeaders(headers || {});
-            this.setResponseBody(body || "");
-        },
-
-        uploadProgress: function uploadProgress(progressEventRaw) {
-            if (supportsProgress) {
-                this.upload.dispatchEvent(new sinon.ProgressEvent("progress", progressEventRaw));
-            }
-        },
-
-        uploadError: function uploadError(error) {
-            if (supportsCustomEvent) {
-                this.upload.dispatchEvent(new sinon.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, queue.length);
-            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;
-            sandbox.injectedKeys.push(key);
-        } 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;
-        },
-
-        restore: function () {
-            sinon.collection.restore.apply(this, arguments);
-            this.restoreContext();
-        },
-
-        restoreContext: function () {
-            if (this.injectedKeys) {
-                for (var i = 0, j = this.injectedKeys.length; i < j; i++) {
-                    delete this.injectInto[this.injectedKeys[i]];
-                }
-                this.injectedKeys = [];
-            }
-        },
-
-        create: function (config) {
-            if (!config) {
-                return sinon.create(sinon.sandbox);
-            }
-
-            var sandbox = prepareSandboxFromConfig(config);
-            sandbox.args = sandbox.args || [];
-            sandbox.injectedKeys = [];
-            sandbox.injectInto = config.injectInto;
-            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;
-        },
-
-        match: function match(actual, expectation) {
-            var matcher = sinon.match(expectation);
-            if (matcher.test(actual)) {
-                assert.pass("match");
-            } else {
-                var formatted = [
-                    "expected value to match",
-                    "    expected = " + sinon.format(expectation),
-                    "    actual = " + sinon.format(actual)
-                ]
-                failAssertion(this, formatted.join("\n"));
-            }
-        }
-    };
-
-    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/lib/sinonjs/sinon-ie-1.10.3.js b/resources/lib/sinonjs/sinon-ie-1.10.3.js
new file mode 100644 (file)
index 0000000..de8c23d
--- /dev/null
@@ -0,0 +1,100 @@
+/**
+ * Sinon.JS 1.10.3, 2014/07/11
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
+ *
+ * (The BSD License)
+ * 
+ * Copyright (c) 2010-2014, 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;
+/*global sinon*/
+/**
+ * Helps IE run the fake XDomainRequest. 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 XDR to work in IE, don't include this file.
+ */
+function XDomainRequest() {}
+
+// Reassign the original function. Now its writable attribute
+// should be true. Hackish, I know, but it works.
+XDomainRequest = sinon.xdr.XDomainRequest || undefined;
diff --git a/resources/lib/sinonjs/sinon-ie-1.9.0.js b/resources/lib/sinonjs/sinon-ie-1.9.0.js
deleted file mode 100644 (file)
index c9fbd9d..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-/**
- * Sinon.JS 1.9.0, 2014/03/05
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
- *
- * (The BSD License)
- * 
- * Copyright (c) 2010-2014, 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 915cd85..f38decd 100644 (file)
@@ -1,4 +1,6 @@
 ( function ( mw, $ ) {
+       // @deprecated since 1.24.  The 'jquery.json' module will be removed in MW 1.25.  Use the 'json' module.
+
        mw.log.deprecate( $, 'toJSON', $.toJSON, 'Use JSON.stringify instead (module "json" for polyfill).' );
        mw.log.deprecate( $, 'evalJSON', $.evalJSON, 'Use JSON.parse instead (module "json" for polyfill).' );
        mw.log.deprecate( $, 'secureEvalJSON', $.secureEvalJSON, 'Use JSON.parse instead (module "json" for polyfill).' );
index 8f46645..6bde5d3 100644 (file)
@@ -14,9 +14,7 @@
 .ui-widget .ui-widget { font-size: 1em; }
 .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: sans-serif; font-size: 1em; }
 .ui-widget-content { border: 1px solid #cccccc; /* @embed */ background: #f2f5f7 url(images/ui-bg_highlight-hard_100_f2f5f7_1x100.png) 50% top repeat-x; color: #362b36; }
-.ui-widget-content a { color: #362b36; }
 .ui-widget-header { border-bottom: 1px solid #bbbbbb; line-height: 1em; /* @embed */ background: #ffffff url(images/ui-bg_highlight-soft_100_ffffff_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; }
-.ui-widget-header a { color: #222222; }
 
 /* Interaction states
 ----------------------------------*/
index 66a3c56..f8641e1 100644 (file)
         * @chainable
         */
        $.fn.arrowSteps = function () {
-               var $steps, width, arrowWidth,
+               var $steps, width, arrowWidth, $stepDiv,
+                       $el = this,
                        paddingSide = $( 'body' ).hasClass( 'rtl' ) ? 'padding-left' : 'padding-right';
 
-               this.addClass( 'arrowSteps' );
-               $steps = this.find( 'li' );
+               $el.addClass( 'arrowSteps' );
+               $steps = $el.find( 'li' );
 
                width = parseInt( 100 / $steps.length, 10 );
                $steps.css( 'width', width + '%' );
                // Every step except the last one has an arrow pointing forward:
                // at the right hand side in LTR languages, and at the left hand side in RTL.
                // Also add in the padding for the calculated arrow width.
-               arrowWidth = parseInt( this.outerHeight(), 10 );
-               $steps.filter( ':not(:last-child)' ).addClass( 'arrow' )
-                       .find( 'div' ).css( paddingSide, arrowWidth.toString() + 'px' );
+               $stepDiv = $steps.filter( ':not(:last-child)' ).addClass( 'arrow' ).find( 'div' );
+
+               // Execute when complete page is fully loaded, including all frames, objects and images
+               $( window ).load( function () {
+                       arrowWidth = parseInt( $el.outerHeight(), 10 );
+                       $stepDiv.css( paddingSide, arrowWidth.toString() + 'px' );
+               } );
+
+               $el.data( 'arrowSteps', $steps );
 
-               this.data( 'arrowSteps', $steps );
                return this;
        };
 
index edfb34a..e88ae5e 100644 (file)
                                mw.track( 'mw.deprecate', 'api.cbParam' );
                                mw.log.warn( msg );
                        }
+
                        return this.postWithToken( 'edit', params ).done( ok ).fail( err );
                },
 
                /**
-                * Api helper to grab an edit token.
+                * API helper to grab an edit token.
                 *
                 * @param {Function} [ok] Success callback (deprecated)
                 * @param {Function} [err] Error callback (deprecated)
                                mw.track( 'mw.deprecate', 'api.cbParam' );
                                mw.log.warn( msg );
                        }
+
                        return this.getToken( 'edit' ).done( ok ).fail( err );
                },
 
                /**
-                * Create a new section of the page.
+                * Post a new section to the page.
                 * @see #postWithEditToken
                 * @param {mw.Title|String} title Target page
                 * @param {string} header
                 * @param {string} message wikitext message
+                * @param {Object} [additionalParams] Additional API parameters, e.g. `{ redirect: true }`
                 * @param {Function} [ok] Success handler (deprecated)
                 * @param {Function} [err] Error handler (deprecated)
                 * @return {jQuery.Promise}
                 */
-               newSection: function ( title, header, message, ok, err ) {
+               newSection: function ( title, header, message, additionalParams, ok, err ) {
+                       // Until we remove 'ok' and 'err' parameters, we have to support code that passes them,
+                       // but not additionalParams...
+                       if ( $.isFunction( additionalParams ) ) {
+                               err = ok;
+                               ok = additionalParams;
+                               additionalParams = undefined;
+                       }
+
                        if ( ok || err ) {
                                mw.track( 'mw.deprecate', 'api.cbParam' );
                                mw.log.warn( msg );
                        }
-                       return this.postWithEditToken( {
+
+                       return this.postWithEditToken( $.extend( {
                                action: 'edit',
                                section: 'new',
                                format: 'json',
                                title: String( title ),
                                summary: header,
                                text: message
-                       } ).done( ok ).fail( err );
+                       }, additionalParams ) ).done( ok ).fail( err );
                }
        } );
 
index b65a62b..8c28884 100644 (file)
 }
 
 .transition(@value) {
-       -webkit-transition: @value;
-       -moz-transition: @value;
-       -o-transition: @value;
-       transition: @value;
+       -webkit-transition: @value; // Safari 3.1-6.0, iOS 3.2-6.1, Android 2.1-4.3
+       -moz-transition: @value; // Firefox 4-15
+       -o-transition: @value; // Opera 10.5-12.0
+       transition: @value; // Chrome 26+, Firefox 16+, IE 10+, Safari 6.1+, Opera 12.1+, iOS 7+, Android 4.4+
 }
 
 .box-sizing(@value) {
-       -webkit-box-sizing: @value;
-       -moz-box-sizing: @value;
-       box-sizing: @value;
+       -webkit-box-sizing: @value; // Safari 3.1-5.0, iOS 3.2-4.3, Android 2.1-3.0
+       -moz-box-sizing: @value; // Firefox 4-28,
+       box-sizing: @value; // Chrome 10+, Firefox 29+, IE 8+, Safari 5.1+, Opera 10+, iOS 5+, Android 4+
 }
 
 .box-shadow(@value) {
-       -webkit-box-shadow: @value; // Android 2.3+, iOS 4.0.2-4.2, Safari 3-4
-       box-shadow: @value; // Chrome 6+, Firefox 4+, IE 9+, iOS 5+, Opera 10.50+
+       -webkit-box-shadow: @value; // Safari 3.1-5.0, iOS 3.2-4.3, Android 2.1-3.0
+       box-shadow: @value; // Chrome 10+, Firefox 4+, IE 9+, Safari 5.1+, Opera 11+, iOS 5+, Android 4+
 }
index 6533db1..88de7d8 100644 (file)
         * dialog box. Submitting that dialog box appends its contents to a
         * wiki page that you specify, as a new section.
         *
-        * Not compatible with LiquidThreads.
+        * This feature works with classic MediaWiki pages
+        * and is not compatible with LiquidThreads or Flow.
         *
-        * Minimal example in how to use it:
+        * Minimal usage example:
         *
         *     var feedback = new mw.Feedback();
         *     $( '#myButton' ).click( function () { feedback.launch(); } );
                                        )
                                );
 
-                               // undo some damage from dialog css
-                               this.$dialog.find( 'a' ).css( {
-                                       color: '#0645ad'
-                               } );
-
-                               this.$dialog.dialog( {
-                                       width: 500,
-                                       autoOpen: false,
-                                       title: mw.msg( this.dialogTitleMessageKey ),
-                                       modal: true,
-                                       buttons: fb.buttons
-                               } );
+                       this.$dialog.dialog( {
+                               width: 500,
+                               autoOpen: false,
+                               title: mw.msg( this.dialogTitleMessageKey ),
+                               modal: true,
+                               buttons: fb.buttons
+                       } );
 
                        this.subjectInput = this.$dialog.find( 'input.feedback-subject' ).get( 0 );
                        this.messageInput = this.$dialog.find( 'textarea.feedback-message' ).get( 0 );
-
                },
 
                /**
                displayBugs: function () {
                        var fb = this,
                                bugsButtons = {};
+
                        this.display( 'bugs' );
                        bugsButtons[ mw.msg( 'feedback-bugnew' ) ] = function () {
                                window.open( fb.bugsLink, '_blank' );
                displayThanks: function () {
                        var fb = this,
                                closeButton = {};
+
                        this.display( 'thanks' );
                        closeButton[ mw.msg( 'feedback-close' ) ] = function () {
                                fb.$dialog.dialog( 'close' );
                displayForm: function ( contents ) {
                        var fb = this,
                                formButtons = {};
+
                        this.subjectInput.value = ( contents && contents.subject ) ? contents.subject : '';
                        this.messageInput.value = ( contents && contents.message ) ? contents.message : '';
 
                displayError: function ( message ) {
                        var fb = this,
                                closeButton = {};
+
                        this.display( 'error' );
                        this.$dialog.find( '.feedback-error-msg' ).msg( message );
                        closeButton[ mw.msg( 'feedback-close' ) ] = function () {
 
                        this.displaySubmitting();
 
-                       this.api.newSection( this.title, subject, message ).done( ok ).fail( err );
+                       // Post the message, resolving redirects
+                       this.api.newSection(
+                               this.title,
+                               subject,
+                               message,
+                               { redirect: true }
+                       ).done( ok ).fail( err );
                },
 
                /**
                        this.$dialog.dialog( 'open' );
                        this.subjectInput.focus();
                }
-
        };
-
 }( mediaWiki, jQuery ) );
index 12888bd..4d1d563 100644 (file)
                                /**
                                 * Execute a function as soon as one or more required modules are ready.
                                 *
+                                * If the required modules are already loaded, the function will be
+                                * executed immediately and the modules will not be reloaded.
+                                *
                                 * Example of inline dependency on OOjs:
                                 *
                                 *     mw.loader.using( 'oojs', function () {
index 8674693..ba907dd 100644 (file)
@@ -1,8 +1,10 @@
 {
        "@metadata": {
                "authors": [
-                       "Martorell"
+                       "Martorell",
+                       "Toniher"
                ]
        },
+       "monobook-desc": "El tema clàssic de MediaWiki des del 2004, que rep el nom d'una foto en blanc i negre d'un llibre en el fons de la pàgina.",
        "monobook.css": "/* Editeu aquest fitxer per personalitzar l'aparença del monobook per a tot el lloc sencer */"
 }
index 2e61ad3..0e53e4a 100644 (file)
@@ -6,6 +6,7 @@
                ]
        },
        "skinname-monobook": "MonoBook",
+       "monobook-desc": "A aparencia clásica de MediaWiki desde 2004; recibe o seu nome pola foto en branco e negro dun libro que aparece no fondo das páxinas",
        "monobook.css": "/* O CSS que se coloque aquí afectará a quen use a aparencia Monobook */",
        "monobook.js": "/* Calquera JavaScript que haxa aquí será cargado para os usuarios que usen a aparencia MonoBook */"
 }
index 83ce352..d87d663 100644 (file)
@@ -2,10 +2,12 @@
        "@metadata": {
                "authors": [
                        "Iwan Novirion",
-                       "Rex"
+                       "Rex",
+                       "Arifin.wijaya"
                ]
        },
        "skinname-monobook": "MonoBook",
+       "monobook-desc": "Kulit MediaWiki klasik sejak tahun 2004, dinamai foto hitam-putih dari buku di latar belakang halaman",
        "monobook.css": "/* CSS yang ada di sini akan diterapkan pada kulit Monobook. */",
        "monobook.js": "/* Semua JavaScript di sini akan dimuatkan untuk para pengguna yang menggunakan kulit MonoBook */"
 }
index 0425b83..df34886 100644 (file)
@@ -2,11 +2,12 @@
        "@metadata": {
                "authors": [
                        "Hamilton Abreu",
-                       "Fúlvio"
+                       "Fúlvio",
+                       "Vitorvicentevalente"
                ]
        },
        "skinname-monobook": "MonoBook",
-       "monobook-desc": "A clássica skin do MediaWiki desde 2004, tendo este nome devido a uma imagem em preto-e-branco de um livro no plano de fundo da página",
+       "monobook-desc": "O tema clássico do MediaWiki desde 2004, tendo este nome sido atribuído devido a uma imagem a preto-e-branco de um livro no plano de fundo da página",
        "monobook.css": "/* Código CSS colocado aqui afectará os utilizadores do tema Monobook */",
        "monobook.js": "/* Código Javascript colocado aqui será carregado para utilizadores do tema Monobook */"
 }
index 7813cc1..e6a6a78 100644 (file)
@@ -4,6 +4,7 @@
                        "Cwlin0416"
                ]
        },
+       "monobook-desc": "MediaWiki 自 2004 年以來的經典外觀,根據頁面背景的書本黑白照命名",
        "monobook.css": "/* 此 CSS 會影響使用 Monobook 介面外觀的使用者 */",
        "monobook.js": "/* 此 JavaScript 會用於使用 Monobook 介面外觀使用者 */"
 }
index 05a1e61..5bb6f1a 100644 (file)
@@ -1,5 +1,8 @@
 /* mediawiki.notification */
-.skin-vector {
+
+// This wrapper class is needed to ensure these rules have larger CSS
+// selector specificity than default styles
+.mediawiki {
        .mw-notification-area {
                font-size: 0.8em;
        }
index a371374..31d5173 100644 (file)
@@ -6,9 +6,11 @@
                        "Calak",
                        "Paucabot",
                        "Ssola",
-                       "Vriullop"
+                       "Vriullop",
+                       "Toniher"
                ]
        },
+       "vector-skin-desc": "Versió moderna del MonoBook amb un nou aspesctes i moltes millores en la usabilitat",
        "vector-action-addsection": "Nova secció",
        "vector-action-delete": "Esborra",
        "vector-action-move": "Reanomena",
index de19c39..455b9bd 100644 (file)
@@ -6,6 +6,7 @@
                        "Vivaelcelta"
                ]
        },
+       "vector-skin-desc": "Versión moderna da aparencia MonoBook, cun aspecto fresco e moitas melloras na usabilidade",
        "vector.css": "/* O CSS que se coloque aquí afectará a quen use a aparencia Vector */",
        "vector.js": "/* Calquera JavaScript que haxa aquí será cargado para os usuarios que usen a aparencia Vector */",
        "vector-action-addsection": "Nova sección",
index 3116632..ef439a3 100644 (file)
@@ -9,6 +9,7 @@
                ]
        },
        "skinname-vector": "Vektor",
+       "vector-skin-desc": "Versi modern dari MonoBook dengan tampilan segar dan banyak perbaikan kegunaan",
        "vector.css": "/* CSS nan ado di siko diterapkan pado kulik Vektor. */",
        "vector.js": "/* Semua JavaScript di sini akan dimuatkan untuk para pengguna yang menggunakan kulit Vector */",
        "vector-action-addsection": "Bagian baru",
index 8f6f09a..2f756b5 100644 (file)
@@ -5,7 +5,7 @@
                ]
        },
        "vector-action-addsection": "موضوع اضاف بكيد",
-       "vector-action-delete": "حذف بكيد",
+       "vector-action-delete": "پاکسا کردن",
        "vector-action-move": "جاوه جا بوئيت",
        "vector-action-protect": "حمايت بكيد",
        "vector-action-undelete": "حذف نبيئني",
index 2bf1407..7bd2fd1 100644 (file)
@@ -27,5 +27,5 @@
        "vector-view-history": "Переглянути історію",
        "vector-view-view": "Читати",
        "vector-view-viewsource": "Переглянути код",
-       "vector-more-actions": "Ð\91Ñ\96лÑ\8cÑ\88е"
+       "vector-more-actions": "Ще"
 }
index 3e67c21..0036c34 100644 (file)
@@ -7,6 +7,7 @@
                        "Mark85296341"
                ]
        },
+       "vector-skin-desc": "現代版的 MonoBook,有著較新穎的外觀與許多使用性的改進",
        "vector.css": "/* 此 CSS 會影響使用 Vector 介面外觀的使用者 */",
        "vector.js": "/* 此 JavaScript 會用於使用 Vector 介面外觀使用者 */",
        "vector-action-addsection": "加入主題",
index 6097370..4a3889d 100644 (file)
@@ -1155,11 +1155,10 @@ Ruby markup (W3C-style)
 : <ruby>東<rb>京</rb><rp>(</rp><rt>とう</rt><rt>きょう</rt><rp>)</rp></ruby>
 ; Double-sided ruby
 : <ruby><rb>旧</rb><rb>金</rb><rb>山</rb><rt>jiù</rt><rt>jīn</rt><rt>shān</rt><rtc>San Francisco</rtc></ruby>
-
 <ruby>
-<rb>♥</rb><rtc><rt>Heart</rt></rtc><rtc lang=fr><rt>Cœur</rt></rtc>
-<rb>☘</rb><rtc><rt>Shamrock</rt></rtc><rtc lang=fr><rt>Trèfle</rt></rtc>
-<rb>✶</rb><rtc><rt>Star</rt></rtc><rtc lang=fr><rt>Étoile</rt></rtc>
+<rb>♥</rb><rtc><rt>Heart</rt></rtc><rtc lang="fr"><rt>Cœur</rt></rtc>
+<rb>☘</rb><rtc><rt>Shamrock</rt></rtc><rtc lang="fr"><rt>Trèfle</rt></rtc>
+<rb>✶</rb><rtc><rt>Star</rt></rtc><rtc lang="fr"><rt>Étoile</rt></rtc>
 </ruby>
 !! html
 <dl><dt> Mono-ruby for individual base characters</dt>
@@ -2623,41 +2622,28 @@ File:foobar.jpg
 !!end
 
 !! test
-Leading pipes outside of tables
-!! options
-parsoid
-!! wikitext
-| foo
-!! html
-<p>| foo</p>
-!! end
-
-!! test
-Leading pipes outside of tables 2
-!! options
-parsoid
-!! wikitext
-a
-| foo
-b
-!! html
-<p>a
-| foo
-b</p>
-!! end
-
-!! test
-Leading pipes outside of tables 3
-!! options
-parsoid
+Table wikitext syntax outside wiki-tables
 !! wikitext
 a
+! not a table heading
+|- not a table row
+| not a table cell
 | class="foo bar" | baz
 b
+|}
+|-
+c
 !! html
 <p>a
+! not a table heading
+|- not a table row
+| not a table cell
 | class="foo bar" | baz
-b</p>
+b
+|}
+|-
+c
+</p>
 !! end
 
 !!test
@@ -6169,6 +6155,16 @@ Link with multiple ":" in a subpage-supporting namespace (bug 63636)
 <p><a rel="mw:WikiLink" href="./User:Foo/Test/63636:Bar">Test</a></p>
 !! end
 
+!! test
+Purely hash wikilink
+!! options
+title=[[User:test/123]]
+!! wikitext
+[[#a|b]]
+!! html/parsoid
+<p data-parsoid='{}'><a rel="mw:WikiLink" href="../User:Test/123#a" data-parsoid='{"stx":"piped","a":{"href":"../User:Test/123#a"},"sa":{"href":"#a"}}'>b</a></p>
+!! end
+
 !! test
 1. Interaction of linktrail and template encapsulation
 !! options
@@ -6406,9 +6402,12 @@ Interlanguage link
 !! wikitext
 Blah blah blah
 [[zh:Chinese]]
-!! html
+!! html/php
 <p>Blah blah blah
 </p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese"/></p>
 !! end
 
 !! test
@@ -6417,9 +6416,13 @@ Double interlanguage link
 Blah blah blah
 [[es:Spanish]]
 [[zh:Chinese]]
-!! html
+!! html/php
 <p>Blah blah blah
 </p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/Spanish"/>
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese"/></p>
 !! end
 
 !! test
@@ -6429,9 +6432,12 @@ language=ln
 !! wikitext
 Blah blah blah
 [[zh:Chinese]]
-!! html
+!! html/php
 <p>Blah blah blah
 </p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese"/></p>
 !! end
 
 !! test
@@ -6442,19 +6448,26 @@ language=ln
 Blah blah blah
 [[es:Spanish]]
 [[zh:Chinese]]
-!! html
+!! html/php
 <p>Blah blah blah
 </p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/Spanish"/>
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/Chinese"/></p>
 !! end
 
 !! test
 "Extra" interlanguage links (bug 32189 / gerrit 111390)
 !! wikitext
 Blah blah blah
-[[mul:Multilingual]]
-!! html
+[[mul:Article]]
+!! html/php
 <p>Blah blah blah
 </p>
+!! html/parsoid
+<p>Blah blah blah
+<link rel="mw:PageProp/Language" title="Multilingual" href="//wikisource.org/wiki/Article"/></p>
 !! end
 
 !! test
@@ -8714,6 +8727,42 @@ Un-closed <includeonly>
 !! html
 !! end
 
+!! test
+Includes and comments at SOL
+!! wikitext
+<!-- comment --><noinclude><!-- comment --></noinclude><!-- comment -->== hu ==
+
+<noinclude>
+some
+</noinclude>* stuff
+* here
+
+<includeonly>can have stuff</includeonly>=== here ===
+
+!! html/php
+<h2><span class="mw-headline" id="hu">hu</span></h2>
+<p>some
+</p>
+<ul><li> stuff</li>
+<li> here</li></ul>
+<h3><span class="mw-headline" id="here">here</span></h3>
+
+!! html/parsoid
+<!-- comment --><meta typeof="mw:Includes/NoInclude" data-parsoid='{"src":"&lt;noinclude>"}'/><!-- comment --><meta typeof="mw:Includes/NoInclude/End" data-parsoid='{"src":"&lt;/noinclude>"}'/><!-- comment -->
+<h2 data-parsoid='{}'> hu </h2>
+
+<meta typeof="mw:Includes/NoInclude" data-parsoid='{"src":"&lt;noinclude>"}'/>
+
+<p data-parsoid='{}'>some</p>
+<meta typeof="mw:Includes/NoInclude/End" data-parsoid='{"src":"&lt;/noinclude>"}'/>
+<ul data-parsoid='{}'>
+<li data-parsoid='{}'> stuff</li>
+
+<li data-parsoid='{}'> here</li></ul>
+
+<h3 data-parsoid='{}'> here </h3>
+!! end
+
 # TODO: test with DOM fragment reuse!
 !! test
 Parsoid: DOM fragment reuse
@@ -8883,9 +8932,10 @@ parsoid=wt2html,wt2wt
 |c
 |}
 !!html/parsoid
-<meta typeof="mw:Includes/IncludeOnly" data-parsoid='{"src":"&lt;includeonly>a&lt;/includeonly>"}'/><meta typeof="mw:Includes/IncludeOnly/End" data-parsoid='{"src":"&lt;/includeonly>"}'/><span data-parsoid='{"fostered":true,"autoInsertedEnd":true,"autoInsertedStart":true}'>{{{b}}}</span><table about="#mwt1" typeof="mw:Param" data-parsoid='{"a":{" ":null},"sa":{" ":""},"src":"{| {{{b}}}\n|c\n|}"}'>
+<meta typeof="mw:Includes/IncludeOnly" data-parsoid='{"src":"&lt;includeonly>a&lt;/includeonly>"'/><meta typeof="mw:Includes/IncludeOnly/End" data-parsoid='{"src":""}'/><table about="#mwt2" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"{{{b}}}","html":"&lt;span about=\"#mwt1\" typeof=\"mw:Param\" data-parsoid=\"{&amp;quot;dsr&amp;quot;:[31,38,null,null],&amp;quot;src&amp;quot;:&amp;quot;{{{b}}}&amp;quot;}\">{{{b}}}&lt;/span>"},{"html":""}]]}' data-parsoid='{"a":{"{{{b}}}":null},"sa":{"{{{b}}}":""}}'>
 <tbody><tr><td>c</td></tr>
 </tbody></table>
+
 !!end
 
 ###
@@ -11926,15 +11976,19 @@ parsoid
 ### Inter-language links
 ###
 !! test
-Inter-language links
+Interlanguage links
 !! options
 ill
 !! wikitext
 [[es:Alimento]]
 [[fr:Nourriture]]
-[[zh:&#39135;&#21697;]]
-!! html
+[[zh:食品]]
+!! html/php
 es:Alimento fr:Nourriture zh:食品
+!! html/parsoid
+<p><link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/Alimento"/>
+<link rel="mw:PageProp/Language" href="//fr.wikipedia.org/wiki/Nourriture"/>
+<link rel="mw:PageProp/Language" href="//zh.wikipedia.org/wiki/食品"/></p>
 !! end
 
 !! test
@@ -11946,8 +12000,13 @@ ill
 [[es:2]]
 [[fr:1]]
 [[fr:2]]
-!! html
+!! html/php
 es:1 fr:1
+!! html/parsoid
+<p><link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/1"/>
+<link rel="mw:PageProp/Language" href="//es.wikipedia.org/wiki/2"/>
+<link rel="mw:PageProp/Language" href="//fr.wikipedia.org/wiki/1"/>
+<link rel="mw:PageProp/Language" href="//fr.wikipedia.org/wiki/2"/></p>
 !! end
 
 ###
@@ -18476,12 +18535,12 @@ B <ref group="X" name="b" />
 <ref name="b">foo</ref>
 </references>
 !! html
-<p>A <span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"foo bar for a"},"attrs":{}}' id="cite_ref-2-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref>foo bar for a&lt;/ref>"}'><a href="#cite_note-2" data-parsoid="{}">[2]</a></span>
-B <span about="#mwt4" class="reference" data-mw='{"name":"ref","attrs":{"group":"X","name":"b"}}' id="cite_ref-b-3-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref name=\"b\" group=\"X\" />"}'><a href="#cite_note-b-3" data-parsoid="{}">[X 1]</a></span></p>
+<p>A <span about="#mwt2" class="reference" data-mw='{"name":"ref","body":{"html":"foo bar for a"},"attrs":{}}' id="cite_ref-1-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref>foo bar for a&lt;/ref>"}'><a href="#cite_note-1" data-parsoid="{}">[1]</a></span>
+B <span about="#mwt4" class="reference" data-mw='{"name":"ref","attrs":{"group":"X","name":"b"}}' id="cite_ref-b-2-0" rel="dc:references" typeof="mw:Extension/ref" data-parsoid='{"src":"&lt;ref group=\"X\" name=\"b\" />"}'><a href="#cite_note-b-2" data-parsoid="{}">[X 1]</a></span></p>
 
-<ol class="references" typeof="mw:Extension/references" about="#mwt6" data-parsoid='{"src":"&lt;references />"}' data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-b-1" id="cite_note-b-1" data-parsoid="{}"><span rel="mw:referencedBy" data-parsoid="{}">↑</span> foo</li><li about="#cite_note-2" id="cite_note-2" data-parsoid="{}"><span rel="mw:referencedBy" data-parsoid="{}"><a href="#cite_ref-2-0" data-parsoid="{}">↑</a></span> foo bar for a</li></ol>
+<ol class="references" typeof="mw:Extension/references" about="#mwt6" data-parsoid='{"src":"&lt;references />"}' data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1" data-parsoid="{}"><span rel="mw:referencedBy" data-parsoid="{}"><a href="#cite_ref-1-0" data-parsoid="{}">↑</a></span> foo bar for a</li></ol>
 
-<ol class="references" typeof="mw:Extension/references" about="#mwt8" data-parsoid='{"src":"&lt;references group=\"X\">\n&lt;ref name=\"b\">foo&lt;/ref>\n&lt;/references>","group":"X"}' data-mw='{"name":"references","body":{"extsrc":"&lt;ref name=\"b\">foo&lt;/ref>","html":"\n&lt;span about=\"#mwt10\" class=\"reference\" data-mw=&#39;{\"name\":\"ref\",\"body\":{\"html\":\"foo\"},\"attrs\":{\"name\":\"b\"}}&#39; rel=\"dc:references\" typeof=\"mw:Extension/ref\">&lt;a href=\"#cite_note-b-1\">[1]&lt;/a>&lt;/span>\n"},"attrs":{"group":"X"}}'><li about="#cite_note-b-3" id="cite_note-b-3" data-parsoid="{}"><span rel="mw:referencedBy" data-parsoid="{}"><a href="#cite_ref-b-3-0" data-parsoid="{}">↑</a></span> </li></ol>
+<ol class="references" typeof="mw:Extension/references" about="#mwt8" data-parsoid='{"src":"&lt;references group=\"X\">\n&lt;ref name=\"b\">foo&lt;/ref>\n&lt;/references>","group":"X"}' data-mw='{"name":"references","body":{"extsrc":"&lt;ref name=\"b\">foo&lt;/ref>","html":"\n&lt;span about=\"#mwt10\" class=\"reference\" data-mw=&#39;{\"name\":\"ref\",\"body\":{\"html\":\"foo\"},\"attrs\":{\"name\":\"b\"}}&#39; rel=\"dc:references\" typeof=\"mw:Extension/ref\">&lt;a href=\"#cite_note-b-2\">[X 1]&lt;/a>&lt;/span>\n"},"attrs":{"group":"X"}}'><li about="#cite_note-b-2" id="cite_note-b-2" data-parsoid="{}"><span rel="mw:referencedBy" data-parsoid="{}"><a href="#cite_ref-b-2-0" data-parsoid="{}">↑</a></span> foo</li></ol>
 !! end
 
 !! test
@@ -19622,6 +19681,16 @@ parsoid
  <figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" height="25" width="220"/></a><figcaption>caption</figcaption></figure>
 !! end
 
+!! test
+5. Nowiki escaping should account for indent-pres
+!! options
+parsoid=html2wt
+!! html
+<pre>==foo==</pre>
+!! wikitext
+ ==foo==
+!! end
+
 #### --------------- Behavior Switches --------------------
 !! test
 1. Valid behavior switches should be escaped
@@ -20414,6 +20483,17 @@ parsoid=html2wt
 # All these tests are marked Parsoid html2wt and html2html only
 # ----------------------------------------------------------------
 
+!! test
+Serialize interwiki links pointing to the current wiki as plain wiki links (bug 65869)
+!! options
+parsoid=html2wt
+language=es
+!! wikitext
+[[Foo]]
+!! html
+<p><a rel="mw:ExtLink" href="http://es.wikipedia.org/wiki/Foo">Foo</a></p>
+!! end
+
 !! test
 Image: Modifying size of an image (1)
 !! options
index 542b3d6..5fee505 100644 (file)
@@ -175,13 +175,13 @@ mw.loader.implement("test.quux",function($,jQuery){mw.test.baz({token:123});},{"
                );
        }
 
-
        /**
         * @dataProvider provideMakeResourceLoaderLink
         * @covers OutputPage::makeResourceLoaderLink
         */
        public function testMakeResourceLoaderLink( $args, $expectedHtml) {
                $this->setMwGlobals( array(
+                       'wgResourceLoaderDebug' => false,
                        'wgResourceLoaderUseESI' => true,
                        'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php',
                        // Affects whether CDATA is inserted
@@ -195,6 +195,7 @@ mw.loader.implement("test.quux",function($,jQuery){mw.test.baz({token:123});},{"
                $method = $class->getMethod( 'makeResourceLoaderLink' );
                $method->setAccessible( true );
                $ctx = new RequestContext();
+               $ctx->setLanguage( 'en' );
                $out = new OutputPage( $ctx );
                $rl = $out->getResourceLoader();
                $rl->register( array(
index dbebeb7..1f1d750 100644 (file)
@@ -114,6 +114,9 @@ class SpecialSearchTest extends MediaWikiTestCase {
         * https://gerrit.wikimedia.org/r/4841
         */
        public function testSearchTermIsNotExpanded() {
+               $this->setMwGlobals( array(
+                       'wgSearchType' => null,
+               ) );
 
                # Initialize [[Special::Search]]
                $search = new SpecialSearch();
diff --git a/tests/phpunit/maintenance/getSlaveServerTest.php b/tests/phpunit/maintenance/getSlaveServerTest.php
deleted file mode 100644 (file)
index 165dc55..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-require_once __DIR__ . "/../../../maintenance/getSlaveServer.php";
-
-/**
- * Tests for getSlaveServer
- *
- * @group Database
- * @covers GetSlaveServer
- */
-class GetSlaveServerTest extends MediaWikiTestCase {
-
-       /**
-        * Yields a regular expression that matches a good DB server name
-        *
-        * It matches IPs or hostnames, both optionally followed by a
-        * port specification
-        *
-        * @return string The regular expression
-        */
-       private function getServerRE() {
-               if ( $this->db->getType() === 'sqlite' ) {
-                       // for SQLite, only the empty string is a good server name
-                       return '';
-               }
-
-               $octet = '([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])';
-               $ip = "(($octet\.){3}$octet)";
-
-               $label = '([a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)';
-               $hostname = "($label(\.$label)*)";
-
-               return "($ip|$hostname)(:[0-9]{1,5})?";
-       }
-
-       function testPlain() {
-               $gss = new GetSlaveServer();
-               $gss->execute();
-
-               $this->expectOutputRegex( "/^" . self::getServerRE() . "\n$/D" );
-       }
-
-       function testXmlDumpsBackupUseCase() {
-               global $wgDBprefix;
-
-               global $argv;
-               $argv = array( null, "--globals" );
-
-               $gss = new GetSlaveServer();
-               $gss->loadParamsAndArgs();
-               $gss->execute();
-               $gss->globals();
-
-               // The main answer
-               $output = $this->getActualOutput();
-               $firstLineEndPos = strpos( $output, "\n" );
-               if ( $firstLineEndPos === false ) {
-                       $this->fail( "Could not find end of first line of output" );
-               }
-               $firstLine = substr( $output, 0, $firstLineEndPos );
-               $this->assertRegExp( "/^" . self::getServerRE() . "$/D",
-                       $firstLine, "DB Server" );
-
-               // xmldumps-backup relies on the wgDBprefix in the output.
-               $this->expectOutputRegex( "/^[[:space:]]*\[wgDBprefix\][[:space:]]*=> "
-                       . $wgDBprefix . "$/m" );
-       }
-}
index b2b5322..34007ed 100644 (file)
@@ -8,14 +8,14 @@ return array(
 
        'test.sinonjs' => array(
                'scripts' => array(
-                       'resources/lib/sinonjs/sinon-1.9.0.js',
+                       'resources/lib/sinonjs/sinon-1.10.3.js',
                        // We want tests to work in IE, but can't include this as it
                        // will break the placeholders in Sinon because the hack it uses
                        // to hijack IE globals relies on running in the global scope
                        // and in ResourceLoader this won't be running in the global scope.
                        // Including it results (among other things) in sandboxed timers
                        // being broken due to Date inheritance being undefined.
-                       // 'resources/lib/sinonjs/sinon-ie-1.9.0.js',
+                       // 'resources/lib/sinonjs/sinon-ie-1.10.3.js',
                ),
                'targets' => array( 'desktop', 'mobile' ),
        ),
index 50e89da..2eda8f1 100644 (file)
                                        // As a convenience feature, automatically restore warnings if they're
                                        // still suppressed by the end of the test.
                                        restoreWarnings();
+
+                                       // Check for incomplete animations/requests/etc and throw
+                                       // error if there are any.
+                                       if ( $.timers && $.timers.length !== 0 ) {
+                                               // Test may need to use fake timers, wait for animations or
+                                               // call $.fx.stop().
+                                               throw new Error( 'Unfinished animations: ' + $.timers.length );
+                                       }
+                                       if ( $.active !== undefined && $.active !== 0 ) {
+                                               // Test may need to use fake XHR, wait for requests or
+                                               // call abort().
+                                               throw new Error( 'Unfinished AJAX requests: ' + $.active );
+                                       }
                                }
                        };
                };
index c0042c2..89cc834 100644 (file)
--- a/thumb.php
+++ b/thumb.php
@@ -118,7 +118,7 @@ function wfStreamThumb( array $params ) {
        }
        if ( isset( $params['width'] ) && substr( $params['width'], -2 ) == 'px' ) {
                // strip the px (pixel) suffix, if found
-               $params['width'] = substr( $width, 0, strlen( $width ) - 2 );
+               $params['width'] = substr( $params['width'], 0, -2 );
        }
        if ( isset( $params['p'] ) ) {
                $params['page'] = $params['p'];