See the task for more details. This is a backwards-compatible change.
If your script only needs a subset of OOjs UI functionality, you can
use one of the new smaller modules instead of the old big one.
New modules:
oojs-ui-core
The core JavaScript library.
oojs-ui-widgets
Additional widgets and layouts module.
oojs-ui-toolbars
Toolbar and tools module.
oojs-ui-windows
Windows and dialogs module.
Changed modules:
oojs-ui.styles
Now correctly only loads the styles needed by OOjs UI PHP.
oojs-ui
Now just loads core+widgets+toolbars+windows as dependencies.
Using the new modules in I58799e22f9c0a2f78c1b4a02c4b7af576157883a.
Bug: T113677
Change-Id: I0a3bf8fb25fb82325705a473cebd883e20b3ab8d
$this->getLanguage()->getDir()
);
$this->addModuleStyles( array(
- 'oojs-ui.styles',
+ 'oojs-ui-core.styles',
'oojs-ui.styles.icons',
'oojs-ui.styles.indicators',
'oojs-ui.styles.textures',
mkdir -p "$REPO_DIR/$TARGET_DIR/images"
mkdir -p "$REPO_DIR/$TARGET_DIR/themes/mediawiki/images"
mkdir -p "$REPO_DIR/$TARGET_DIR/themes/apex/images"
-cp ./node_modules/oojs-ui/dist/oojs-ui.js "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/{oojs-ui-mediawiki-noimages.css,oojs-ui-mediawiki.js} "$REPO_DIR/$TARGET_DIR"
-cp ./node_modules/oojs-ui/dist/{oojs-ui-apex-noimages.css,oojs-ui-apex.js} "$REPO_DIR/$TARGET_DIR"
+cp ./node_modules/oojs-ui/dist/oojs-ui-core.js "$REPO_DIR/$TARGET_DIR"
+cp ./node_modules/oojs-ui/dist/oojs-ui-core-{mediawiki,apex}.css "$REPO_DIR/$TARGET_DIR"
+cp ./node_modules/oojs-ui/dist/oojs-ui-widgets.js "$REPO_DIR/$TARGET_DIR"
+cp ./node_modules/oojs-ui/dist/oojs-ui-widgets-{mediawiki,apex}.css "$REPO_DIR/$TARGET_DIR"
+cp ./node_modules/oojs-ui/dist/oojs-ui-toolbars.js "$REPO_DIR/$TARGET_DIR"
+cp ./node_modules/oojs-ui/dist/oojs-ui-toolbars-{mediawiki,apex}.css "$REPO_DIR/$TARGET_DIR"
+cp ./node_modules/oojs-ui/dist/oojs-ui-windows.js "$REPO_DIR/$TARGET_DIR"
+cp ./node_modules/oojs-ui/dist/oojs-ui-windows-{mediawiki,apex}.css "$REPO_DIR/$TARGET_DIR"
+cp ./node_modules/oojs-ui/dist/oojs-ui-{mediawiki,apex}.js "$REPO_DIR/$TARGET_DIR"
cp -R ./node_modules/oojs-ui/dist/i18n "$REPO_DIR/$TARGET_DIR"
cp -R ./node_modules/oojs-ui/dist/images "$REPO_DIR/$TARGET_DIR"
cp -R ./node_modules/oojs-ui/dist/themes/mediawiki/images "$REPO_DIR/$TARGET_DIR/themes/mediawiki"
$themes = array_map( 'strtolower', $themes );
$themes['default'] = 'mediawiki';
+ // Helper function to generate paths to files used in 'skinStyles' and 'skinScripts'.
+ $getSkinSpecific = function ( $module, $ext = 'css' ) use ( $themes ) {
+ return array_combine(
+ array_keys( $themes ),
+ array_map( function ( $theme ) use ( $module, $ext ) {
+ $module = $module ? "$module-" : '';
+ // TODO Allow extensions to specify this path somehow
+ return "resources/lib/oojs-ui/oojs-ui-$module$theme.$ext";
+ }, array_values( $themes ) )
+ );
+ };
+
$modules = array();
+
+ // Omnibus module.
$modules['oojs-ui'] = array(
+ 'dependencies' => array(
+ 'oojs-ui-core',
+ 'oojs-ui-widgets',
+ 'oojs-ui-toolbars',
+ 'oojs-ui-windows',
+ ),
+ 'targets' => array( 'desktop', 'mobile' ),
+ );
+
+ // The core JavaScript library.
+ $modules['oojs-ui-core'] = array(
'scripts' => array(
- 'resources/lib/oojs-ui/oojs-ui.js',
+ 'resources/lib/oojs-ui/oojs-ui-core.js',
'resources/src/oojs-ui-local.js',
),
- 'skinScripts' => array_combine(
- array_keys( $themes ),
- array_map( function ( $theme ) {
- // TODO Allow extensions to specify this path somehow
- return "resources/lib/oojs-ui/oojs-ui-$theme.js";
- }, array_values( $themes ) )
- ),
+ 'skinScripts' => $getSkinSpecific( null, 'js' ),
'dependencies' => array(
'es5-shim',
'oojs',
'oojs-ui.styles.textures',
'mediawiki.language',
),
+ 'targets' => array( 'desktop', 'mobile' ),
+ );
+ // This contains only the styles required by core widgets.
+ $modules['oojs-ui-core.styles'] = array(
+ 'position' => 'top',
+ 'styles' => 'resources/src/oojs-ui-local.css', // HACK, see inside the file
+ 'skinStyles' => $getSkinSpecific( 'core' ),
+ 'targets' => array( 'desktop', 'mobile' ),
+ );
+
+ // Deprecated old name for the module 'oojs-ui-core.styles'.
+ $modules['oojs-ui.styles'] = $modules['oojs-ui-core.styles'];
+
+ // Additional widgets and layouts module.
+ $modules['oojs-ui-widgets'] = array(
+ 'scripts' => 'resources/lib/oojs-ui/oojs-ui-widgets.js',
+ 'skinStyles' => $getSkinSpecific( 'widgets' ),
+ 'dependencies' => 'oojs-ui-core',
'messages' => array(
- 'ooui-dialog-message-accept',
- 'ooui-dialog-message-reject',
- 'ooui-dialog-process-continue',
- 'ooui-dialog-process-dismiss',
- 'ooui-dialog-process-error',
- 'ooui-dialog-process-retry',
'ooui-outline-control-move-down',
'ooui-outline-control-move-up',
'ooui-outline-control-remove',
'ooui-selectfile-dragdrop-placeholder',
'ooui-selectfile-not-supported',
'ooui-selectfile-placeholder',
+ ),
+ 'targets' => array( 'desktop', 'mobile' ),
+ );
+ // Toolbar and tools module.
+ $modules['oojs-ui-toolbars'] = array(
+ 'scripts' => 'resources/lib/oojs-ui/oojs-ui-toolbars.js',
+ 'skinStyles' => $getSkinSpecific( 'toolbars' ),
+ 'dependencies' => 'oojs-ui-core',
+ 'messages' => array(
'ooui-toolbar-more',
'ooui-toolgroup-collapse',
'ooui-toolgroup-expand',
),
'targets' => array( 'desktop', 'mobile' ),
);
- $modules['oojs-ui.styles'] = array(
- 'position' => 'top',
- 'styles' => 'resources/src/oojs-ui-local.css', // HACK, see inside the file
- 'skinStyles' => array_combine(
- array_keys( $themes ),
- array_map( function ( $theme ) {
- // TODO Allow extensions to specify this path somehow
- return "resources/lib/oojs-ui/oojs-ui-$theme-noimages.css";
- }, array_values( $themes ) )
+ // Windows and dialogs module.
+ $modules['oojs-ui-windows'] = array(
+ 'scripts' => 'resources/lib/oojs-ui/oojs-ui-windows.js',
+ 'skinStyles' => $getSkinSpecific( 'windows' ),
+ 'dependencies' => 'oojs-ui-core',
+ 'messages' => array(
+ 'ooui-dialog-message-accept',
+ 'ooui-dialog-message-reject',
+ 'ooui-dialog-process-continue',
+ 'ooui-dialog-process-dismiss',
+ 'ooui-dialog-process-error',
+ 'ooui-dialog-process-retry',
),
'targets' => array( 'desktop', 'mobile' ),
);
+++ /dev/null
-/*!
- * OOjs UI v0.15.2
- * https://www.mediawiki.org/wiki/OOjs_UI
- *
- * Copyright 2011–2016 OOjs UI Team and other contributors.
- * Released under the MIT license
- * http://oojs.mit-license.org
- *
- * Date: 2016-02-02T22:07:06Z
- */
-@-webkit-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-moz-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-ms-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-o-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-.oo-ui-element-hidden {
- display: none !important;
-}
-.oo-ui-buttonElement > .oo-ui-buttonElement-button {
- cursor: pointer;
- display: inline-block;
- vertical-align: middle;
- font: inherit;
- white-space: nowrap;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-.oo-ui-buttonElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon,
-.oo-ui-buttonElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
- display: none;
-}
-.oo-ui-buttonElement.oo-ui-widget-disabled > .oo-ui-buttonElement-button {
- cursor: default;
-}
-.oo-ui-buttonElement.oo-ui-indicatorElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator,
-.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-buttonElement-frameless {
- display: inline-block;
- position: relative;
-}
-.oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button {
- display: inline-block;
- vertical-align: top;
- text-align: center;
-}
-.oo-ui-buttonElement-framed.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled > .oo-ui-buttonElement-button,
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button,
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- cursor: default;
-}
-.oo-ui-buttonElement > .oo-ui-buttonElement-button {
- color: #333333;
-}
-.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
- margin-left: 0;
-}
-.oo-ui-buttonElement.oo-ui-indicatorElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
- width: 0.9375em;
- height: 0.9375em;
- margin: 0.46875em;
-}
-.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
- margin-left: 0.46875em;
-}
-.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
- width: 1.875em;
- height: 1.875em;
-}
-.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:hover,
-.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:focus {
- outline: none;
-}
-.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:hover > .oo-ui-iconElement-icon,
-.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:focus > .oo-ui-iconElement-icon {
- opacity: 1;
-}
-.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:hover > .oo-ui-labelElement-label,
-.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:focus > .oo-ui-labelElement-label {
- color: #000000;
-}
-.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #333333;
-}
-.oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- margin-left: 0.25em;
-}
-.oo-ui-buttonElement-frameless > input.oo-ui-buttonElement-button {
- padding-left: 0.25em;
- color: #333333;
-}
-.oo-ui-buttonElement-frameless > input.oo-ui-buttonElement-button:hover,
-.oo-ui-buttonElement-frameless > input.oo-ui-buttonElement-button:focus {
- color: #000000;
-}
-.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #087ecc;
-}
-.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #76ab36;
-}
-.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #d45353;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
- opacity: 0.2;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #cccccc;
-}
-.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button {
- padding: 0.2em 0.8em;
- border-radius: 0.3em;
- text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
- border: 1px #c9c9c9 solid;
- -webkit-transition: border-color 100ms ease;
- -moz-transition: border-color 100ms ease;
- transition: border-color 100ms ease;
- background-color: #eeeeee;
- background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #ffffff), color-stop(100%, #dddddd));
- background-image: -webkit-linear-gradient(top, #ffffff 0, #dddddd 100%);
- background-image: -moz-linear-gradient(top, #ffffff 0, #dddddd 100%);
- background-image: linear-gradient(to bottom, #ffffff 0, #dddddd 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffffff', endColorstr='#ffdddddd' )";
-}
-.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button:hover,
-.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button:focus {
- border-color: #aaaaaa;
- outline: none;
-}
-.oo-ui-buttonElement-framed > input.oo-ui-buttonElement-button,
-.oo-ui-buttonElement-framed.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- line-height: 1.875em;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
-.oo-ui-buttonElement-framed.oo-ui-buttonElement-active > .oo-ui-buttonElement-button,
-.oo-ui-buttonElement-framed.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.07);
- color: black;
- border-color: #c9c9c9;
- background-color: #eeeeee;
- background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #dddddd), color-stop(100%, #ffffff));
- background-image: -webkit-linear-gradient(top, #dddddd 0, #ffffff 100%);
- background-image: -moz-linear-gradient(top, #dddddd 0, #ffffff 100%);
- background-image: linear-gradient(to bottom, #dddddd 0, #ffffff 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffdddddd', endColorstr='#ffffffff' )";
-}
-.oo-ui-buttonElement-framed.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
- margin-left: -0.5em;
- margin-right: -0.5em;
-}
-.oo-ui-buttonElement-framed.oo-ui-iconElement.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
- margin-right: 0.3em;
-}
-.oo-ui-buttonElement-framed.oo-ui-indicatorElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
- margin-left: -0.005em;
- margin-right: -0.005em;
-}
-.oo-ui-buttonElement-framed.oo-ui-indicatorElement.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator,
-.oo-ui-buttonElement-framed.oo-ui-indicatorElement.oo-ui-iconElement:not( .oo-ui-labelElement ) > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
- margin-left: 0.46875em;
- margin-right: -0.275em;
-}
-.oo-ui-buttonElement-framed.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button {
- border: 1px solid #a6cee1;
- background-color: #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%);
- background-image: -moz-linear-gradient(top, #eaf4fa 0, #b0d9ee 100%);
- background-image: linear-gradient(to bottom, #eaf4fa 0, #b0d9ee 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffeaf4fa', endColorstr='#ffb0d9ee' )";
-}
-.oo-ui-buttonElement-framed.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:hover,
-.oo-ui-buttonElement-framed.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:focus {
- border-color: #9dc2d4;
-}
-.oo-ui-buttonElement-framed.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
-.oo-ui-buttonElement-framed.oo-ui-flaggedElement-progressive.oo-ui-buttonElement-active > .oo-ui-buttonElement-button,
-.oo-ui-buttonElement-framed.oo-ui-flaggedElement-progressive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- border: 1px solid #a6cee1;
- background-color: #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%);
- background-image: -moz-linear-gradient(top, #b0d9ee 0, #eaf4fa 100%);
- background-image: linear-gradient(to bottom, #b0d9ee 0, #eaf4fa 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffb0d9ee', endColorstr='#ffeaf4fa' )";
-}
-.oo-ui-buttonElement-framed.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button {
- border: 1px solid #b8d892;
- background-color: #daf0bd;
- background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #f0fbe1), color-stop(100%, #c3e59a));
- background-image: -webkit-linear-gradient(top, #f0fbe1 0, #c3e59a 100%);
- background-image: -moz-linear-gradient(top, #f0fbe1 0, #c3e59a 100%);
- background-image: linear-gradient(to bottom, #f0fbe1 0, #c3e59a 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff0fbe1', endColorstr='#ffc3e59a' )";
-}
-.oo-ui-buttonElement-framed.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:hover,
-.oo-ui-buttonElement-framed.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:focus {
- border-color: #adcb89;
-}
-.oo-ui-buttonElement-framed.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
-.oo-ui-buttonElement-framed.oo-ui-flaggedElement-constructive.oo-ui-buttonElement-active > .oo-ui-buttonElement-button,
-.oo-ui-buttonElement-framed.oo-ui-flaggedElement-constructive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- border: 1px solid #b8d892;
- background-color: #daf0bd;
- background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #c3e59a), color-stop(100%, #f0fbe1));
- background-image: -webkit-linear-gradient(top, #c3e59a 0, #f0fbe1 100%);
- background-image: -moz-linear-gradient(top, #c3e59a 0, #f0fbe1 100%);
- background-image: linear-gradient(to bottom, #c3e59a 0, #f0fbe1 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffc3e59a', endColorstr='#fff0fbe1' )";
-}
-.oo-ui-buttonElement-framed.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button {
- color: #d45353;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled > .oo-ui-buttonElement-button,
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button,
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- opacity: 0.5;
- -webkit-transform: translate3d(0, 0, 0);
- box-shadow: none;
- color: #333333;
- background: #eeeeee;
- border-color: #cccccc;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled > .oo-ui-buttonElement-button:hover,
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button:hover,
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button:hover,
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled > .oo-ui-buttonElement-button:focus,
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button:focus,
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button:focus {
- border-color: #cccccc;
- box-shadow: none;
-}
-.oo-ui-clippableElement-clippable {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-iconElement .oo-ui-iconElement-icon,
-.oo-ui-iconElement.oo-ui-iconElement-icon {
- background-size: contain;
- background-position: center center;
- background-repeat: no-repeat;
-}
-.oo-ui-iconElement .oo-ui-iconElement-icon,
-.oo-ui-iconElement.oo-ui-iconElement-icon {
- opacity: 0.8;
-}
-.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator,
-.oo-ui-indicatorElement.oo-ui-indicatorElement-indicator {
- background-size: contain;
- background-position: center center;
- background-repeat: no-repeat;
-}
-.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator,
-.oo-ui-indicatorElement.oo-ui-indicatorElement-indicator {
- opacity: 0.8;
-}
-.oo-ui-pendingElement-pending {
- background-image: /* @embed */ url(themes/apex/images/textures/pending.gif);
-}
-.oo-ui-fieldLayout {
- display: block;
- margin-bottom: 1em;
-}
-.oo-ui-fieldLayout:before,
-.oo-ui-fieldLayout:after {
- content: " ";
- display: table;
-}
-.oo-ui-fieldLayout:after {
- clear: both;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
- display: block;
- float: left;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- text-align: right;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body {
- display: table;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
- display: table-cell;
- vertical-align: middle;
-}
-.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- display: inline-block;
-}
-.oo-ui-fieldLayout > .oo-ui-fieldLayout-help {
- float: right;
-}
-.oo-ui-fieldLayout > .oo-ui-fieldLayout-help > .oo-ui-popupWidget > .oo-ui-popupWidget-popup {
- z-index: 1;
-}
-.oo-ui-fieldLayout > .oo-ui-fieldLayout-help .oo-ui-fieldLayout-help-content {
- padding: 0.5em 0.75em;
- line-height: 1.5em;
-}
-.oo-ui-fieldLayout:last-child {
- margin-bottom: 0;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- padding-top: 0.5em;
- margin-right: 5%;
- width: 35%;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
- width: 60%;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline {
- margin-bottom: 1.25em;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- padding: 0.25em 0.25em 0.25em 0.5em;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-top.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- padding: 0.5em 0;
-}
-.oo-ui-fieldLayout > .oo-ui-popupButtonWidget {
- margin-right: 0;
- margin-top: 0.25em;
-}
-.oo-ui-fieldLayout > .oo-ui-popupButtonWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-fieldLayout-disabled > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- color: #cccccc;
-}
-.oo-ui-fieldLayout-messages {
- list-style: none none;
- margin: 0;
- padding: 0;
- margin-top: 0.25em;
- margin-left: 0.25em;
-}
-.oo-ui-fieldLayout-messages > li {
- margin: 0;
- padding: 0;
-}
-.oo-ui-fieldLayout-messages .oo-ui-iconWidget {
- display: none;
-}
-.oo-ui-fieldLayout-messages .oo-ui-fieldLayout-messages-error {
- color: #d45353;
-}
-.oo-ui-fieldLayout-messages .oo-ui-labelWidget {
- padding: 0;
- line-height: 1.875em;
- vertical-align: middle;
-}
-.oo-ui-actionFieldLayout-input,
-.oo-ui-actionFieldLayout-button {
- display: table-cell;
- vertical-align: middle;
-}
-.oo-ui-actionFieldLayout-input {
- padding-right: 1em;
-}
-.oo-ui-actionFieldLayout-button {
- width: 1%;
- white-space: nowrap;
-}
-.oo-ui-fieldsetLayout {
- position: relative;
- margin: 0;
- padding: 0;
- border: none;
-}
-.oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-iconElement-icon {
- display: block;
- position: absolute;
-}
-.oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label {
- display: inline-block;
-}
-.oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help {
- float: right;
-}
-.oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help > .oo-ui-popupWidget > .oo-ui-popupWidget-popup {
- z-index: 1;
-}
-.oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help .oo-ui-fieldsetLayout-help-content {
- padding: 0.5em 0.75em;
- line-height: 1.5em;
-}
-.oo-ui-fieldsetLayout + .oo-ui-fieldsetLayout,
-.oo-ui-fieldsetLayout + .oo-ui-formLayout {
- margin-top: 2em;
-}
-.oo-ui-fieldsetLayout > .oo-ui-labelElement-label {
- font-size: 1.1em;
- margin-bottom: 0.5em;
- padding: 0.25em 0;
- font-weight: bold;
-}
-.oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-labelElement-label {
- padding-left: 2em;
- line-height: 1.8em;
-}
-.oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-iconElement-icon {
- left: 0;
- top: 0.25em;
- width: 1.875em;
- height: 1.875em;
-}
-.oo-ui-fieldsetLayout > .oo-ui-popupButtonWidget {
- margin-right: 0;
-}
-.oo-ui-fieldsetLayout > .oo-ui-popupButtonWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-formLayout + .oo-ui-fieldsetLayout,
-.oo-ui-formLayout + .oo-ui-formLayout {
- margin-top: 2em;
-}
-.oo-ui-panelLayout {
- position: relative;
-}
-.oo-ui-panelLayout-scrollable {
- overflow-y: auto;
-}
-.oo-ui-panelLayout-expanded {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
-}
-.oo-ui-panelLayout-padded {
- padding: 1.25em;
-}
-.oo-ui-panelLayout-framed {
- border-radius: 0.5em;
- box-shadow: 0 0.25em 1em rgba(0, 0, 0, 0.25);
-}
-.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed {
- margin: 1em 0;
-}
-.oo-ui-horizontalLayout > .oo-ui-widget {
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-horizontalLayout > .oo-ui-layout {
- display: inline-block;
-}
-.oo-ui-horizontalLayout > .oo-ui-layout,
-.oo-ui-horizontalLayout > .oo-ui-widget {
- margin-right: 0.5em;
-}
-.oo-ui-horizontalLayout > .oo-ui-layout:last-child,
-.oo-ui-horizontalLayout > .oo-ui-widget:last-child {
- margin-right: 0;
-}
-.oo-ui-horizontalLayout > .oo-ui-layout {
- margin-bottom: 0;
-}
-.oo-ui-optionWidget {
- position: relative;
- display: block;
- padding: 0.25em 0.5em;
- border: none;
-}
-.oo-ui-optionWidget.oo-ui-widget-enabled {
- cursor: pointer;
-}
-.oo-ui-optionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
- display: block;
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
-}
-.oo-ui-optionWidget-highlighted {
- background-color: #e1f3ff;
-}
-.oo-ui-optionWidget .oo-ui-labelElement-label {
- line-height: 1.5em;
-}
-.oo-ui-selectWidget-depressed .oo-ui-optionWidget-selected {
- background-color: #a7dcff;
-}
-.oo-ui-selectWidget-pressed .oo-ui-optionWidget-pressed,
-.oo-ui-selectWidget-pressed .oo-ui-optionWidget-pressed.oo-ui-optionWidget-highlighted,
-.oo-ui-selectWidget-pressed .oo-ui-optionWidget-pressed.oo-ui-optionWidget-highlighted.oo-ui-optionWidget-selected {
- background-color: #a7dcff;
-}
-.oo-ui-optionWidget.oo-ui-widget-disabled {
- color: #cccccc;
-}
-.oo-ui-decoratedOptionWidget {
- padding: 0.5em 2em 0.5em 3em;
-}
-.oo-ui-decoratedOptionWidget .oo-ui-iconElement-icon,
-.oo-ui-decoratedOptionWidget .oo-ui-indicatorElement-indicator {
- position: absolute;
-}
-.oo-ui-decoratedOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
-.oo-ui-decoratedOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- top: 0;
- height: 100%;
-}
-.oo-ui-decoratedOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
- width: 1.875em;
- left: 0.5em;
-}
-.oo-ui-decoratedOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- width: 0.9375em;
- right: 0.5em;
-}
-.oo-ui-decoratedOptionWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
-.oo-ui-decoratedOptionWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
- opacity: 0.2;
-}
-.oo-ui-radioSelectWidget {
- padding: 0.75em 0 0.5em 0;
-}
-.oo-ui-radioOptionWidget {
- cursor: default;
- padding: 0;
- background-color: transparent;
-}
-.oo-ui-radioOptionWidget .oo-ui-radioInputWidget,
-.oo-ui-radioOptionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-radioOptionWidget.oo-ui-optionWidget-selected,
-.oo-ui-radioOptionWidget.oo-ui-optionWidget-pressed,
-.oo-ui-radioOptionWidget.oo-ui-optionWidget-highlighted {
- background-color: transparent;
-}
-.oo-ui-radioOptionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
- padding-left: 0.5em;
-}
-.oo-ui-radioOptionWidget .oo-ui-radioInputWidget {
- margin-right: 0;
-}
-.oo-ui-labelWidget {
- display: inline-block;
- padding: 0.5em 0;
-}
-.oo-ui-iconWidget {
- display: inline-block;
- vertical-align: middle;
- line-height: 2.5em;
- height: 1.875em;
- width: 1.875em;
-}
-.oo-ui-iconWidget.oo-ui-widget-disabled {
- opacity: 0.2;
-}
-.oo-ui-indicatorWidget {
- display: inline-block;
- vertical-align: middle;
- line-height: 2.5em;
- height: 0.9375em;
- width: 0.9375em;
- margin: 0.46875em;
-}
-.oo-ui-indicatorWidget.oo-ui-widget-disabled {
- opacity: 0.2;
-}
-.oo-ui-buttonWidget {
- display: inline-block;
- vertical-align: middle;
- margin-right: 0.5em;
-}
-.oo-ui-buttonWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-buttonGroupWidget {
- display: inline-block;
- white-space: nowrap;
- border-radius: 0.3em;
- margin-right: 0.5em;
-}
-.oo-ui-buttonGroupWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-buttonGroupWidget .oo-ui-buttonElement {
- margin-right: 0;
-}
-.oo-ui-buttonGroupWidget .oo-ui-buttonElement:last-child {
- margin-right: 0;
-}
-.oo-ui-buttonGroupWidget .oo-ui-buttonElement-framed .oo-ui-buttonElement-button {
- border-radius: 0;
- margin-left: -1px;
-}
-.oo-ui-buttonGroupWidget .oo-ui-buttonElement-framed:first-child .oo-ui-buttonElement-button {
- border-bottom-left-radius: 0.3em;
- border-top-left-radius: 0.3em;
- margin-left: 0;
-}
-.oo-ui-buttonGroupWidget .oo-ui-buttonElement-framed:last-child .oo-ui-buttonElement-button {
- border-bottom-right-radius: 0.3em;
- border-top-right-radius: 0.3em;
-}
-.oo-ui-popupWidget {
- position: absolute;
- /* @noflip */
- left: 0;
-}
-.oo-ui-popupWidget-popup {
- position: relative;
- overflow: hidden;
- z-index: 1;
-}
-.oo-ui-popupWidget-anchor {
- display: none;
- z-index: 1;
-}
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor {
- display: block;
- position: absolute;
- top: 0;
- /* @noflip */
- left: 0;
- background-repeat: no-repeat;
-}
-.oo-ui-popupWidget-head {
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-.oo-ui-popupWidget-head > .oo-ui-buttonWidget {
- float: right;
-}
-.oo-ui-popupWidget-head > .oo-ui-labelElement-label {
- float: left;
- cursor: default;
-}
-.oo-ui-popupWidget-body {
- clear: both;
- overflow: hidden;
-}
-.oo-ui-popupWidget-popup {
- background-color: #ffffff;
- border: 1px solid #cccccc;
- border-radius: 0.25em;
- box-shadow: 0 0.15em 0.5em 0 rgba(0, 0, 0, 0.2);
-}
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-popup {
- margin-top: 6px;
-}
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before,
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
- content: "";
- position: absolute;
- width: 0;
- height: 0;
- border-style: solid;
- border-color: transparent;
- border-top: 0;
-}
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before {
- bottom: -7px;
- left: -6px;
- border-bottom-color: #aaaaaa;
- border-width: 7px;
-}
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
- bottom: -7px;
- left: -5px;
- border-bottom-color: #ffffff;
- border-width: 6px;
-}
-.oo-ui-popupWidget-transitioning .oo-ui-popupWidget-popup {
- -webkit-transition: width 100ms ease, height 100ms ease, left 100ms ease;
- -moz-transition: width 100ms ease, height 100ms ease, left 100ms ease;
- transition: width 100ms ease, height 100ms ease, left 100ms ease;
-}
-.oo-ui-popupWidget-head {
- height: 2.5em;
-}
-.oo-ui-popupWidget-head > .oo-ui-buttonWidget {
- margin: 0.25em;
-}
-.oo-ui-popupWidget-head > .oo-ui-labelElement-label {
- margin: 0.75em 1em;
-}
-.oo-ui-popupWidget-body-padded {
- padding: 0 1em;
-}
-.oo-ui-popupButtonWidget {
- position: relative;
-}
-.oo-ui-popupButtonWidget .oo-ui-popupWidget {
- position: absolute;
- cursor: auto;
-}
-.oo-ui-popupButtonWidget.oo-ui-buttonElement-frameless > .oo-ui-popupWidget {
- /* @noflip */
- left: 1em;
-}
-.oo-ui-popupButtonWidget.oo-ui-buttonElement-framed > .oo-ui-popupWidget {
- /* @noflip */
- left: 1.25em;
-}
-.oo-ui-inputWidget {
- margin-right: 0.5em;
-}
-.oo-ui-inputWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-buttonInputWidget {
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-buttonInputWidget > button,
-.oo-ui-buttonInputWidget > input {
- border: 0;
- padding: 0;
- background-color: transparent;
-}
-.oo-ui-dropdownInputWidget {
- position: relative;
- vertical-align: middle;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- width: 100%;
- max-width: 50em;
-}
-.oo-ui-dropdownInputWidget select {
- display: inline-block;
- width: 100%;
- resize: none;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-dropdownInputWidget select {
- background-color: #ffffff;
- height: 2.5em;
- padding: 0.5em;
- font-size: inherit;
- font-family: inherit;
- border: 1px solid rgba(0, 0, 0, 0.1);
- border-radius: 0.25em;
-}
-.oo-ui-dropdownInputWidget.oo-ui-widget-enabled select:hover,
-.oo-ui-dropdownInputWidget.oo-ui-widget-enabled select:focus {
- border-color: rgba(0, 0, 0, 0.2);
- outline: none;
-}
-.oo-ui-dropdownInputWidget.oo-ui-widget-disabled select {
- color: #cccccc;
- border-color: #dddddd;
- background-color: #f3f3f3;
-}
-.oo-ui-radioSelectInputWidget .oo-ui-fieldLayout {
- margin-bottom: 0;
-}
-.oo-ui-textInputWidget {
- position: relative;
- vertical-align: middle;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- width: 100%;
- max-width: 50em;
-}
-.oo-ui-textInputWidget input,
-.oo-ui-textInputWidget textarea {
- display: inline-block;
- width: 100%;
- resize: none;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-textInputWidget textarea {
- overflow: auto;
-}
-.oo-ui-textInputWidget input[type="search"] {
- -webkit-appearance: none;
-}
-.oo-ui-textInputWidget input[type="search"]::-ms-clear {
- display: none;
-}
-.oo-ui-textInputWidget input[type="search"]::-ms-reveal {
- display: none;
-}
-.oo-ui-textInputWidget input[type="search"]::-webkit-search-decoration,
-.oo-ui-textInputWidget input[type="search"]::-webkit-search-cancel-button,
-.oo-ui-textInputWidget input[type="search"]::-webkit-search-results-button,
-.oo-ui-textInputWidget input[type="search"]::-webkit-search-results-decoration {
- display: none;
-}
-.oo-ui-textInputWidget > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator,
-.oo-ui-textInputWidget > .oo-ui-labelElement-label {
- display: none;
-}
-.oo-ui-textInputWidget.oo-ui-iconElement > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
- display: block;
- position: absolute;
- top: 0;
- height: 100%;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
- cursor: text;
-}
-.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-textInputWidget-type-search > .oo-ui-indicatorElement-indicator {
- cursor: pointer;
-}
-.oo-ui-textInputWidget.oo-ui-labelElement > .oo-ui-labelElement-label {
- display: block;
-}
-.oo-ui-textInputWidget > .oo-ui-iconElement-icon {
- left: 0;
-}
-.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator {
- right: 0;
-}
-.oo-ui-textInputWidget > .oo-ui-labelElement-label {
- position: absolute;
- top: 0;
-}
-.oo-ui-textInputWidget-labelPosition-after > .oo-ui-labelElement-label {
- right: 0;
-}
-.oo-ui-textInputWidget-labelPosition-before > .oo-ui-labelElement-label {
- left: 0;
-}
-.oo-ui-textInputWidget input,
-.oo-ui-textInputWidget textarea {
- padding: 0.5em;
- line-height: 1.275em;
- font-size: inherit;
- font-family: inherit;
- background-color: #ffffff;
- color: black;
- border: 1px solid #cccccc;
- box-shadow: 0 0 0 white, inset 0 0.1em 0.2em #dddddd;
- border-radius: 0.25em;
- -webkit-transition: border-color 250ms ease, box-shadow 250ms ease;
- -moz-transition: border-color 250ms ease, box-shadow 250ms ease;
- transition: border-color 250ms ease, box-shadow 250ms ease;
-}
-.oo-ui-textInputWidget input.oo-ui-pendingElement-pending,
-.oo-ui-textInputWidget textarea.oo-ui-pendingElement-pending {
- background-color: transparent;
-}
-.oo-ui-textInputWidget.oo-ui-widget-enabled input:focus,
-.oo-ui-textInputWidget.oo-ui-widget-enabled textarea:focus {
- outline: none;
- border-color: #a7dcff;
- box-shadow: 0 0 0.3em #a7dcff, 0 0 0 white;
-}
-.oo-ui-textInputWidget.oo-ui-widget-enabled input[readonly],
-.oo-ui-textInputWidget.oo-ui-widget-enabled textarea[readonly] {
- color: #777777;
-}
-.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid input,
-.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid textarea {
- background-color: #ffdddd;
-}
-.oo-ui-textInputWidget.oo-ui-widget-disabled input,
-.oo-ui-textInputWidget.oo-ui-widget-disabled textarea {
- color: #cccccc;
- text-shadow: 0 1px 1px #ffffff;
- border-color: #dddddd;
- background-color: #f3f3f3;
-}
-.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
- opacity: 0.2;
-}
-.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-labelElement-label {
- color: #dddddd;
- text-shadow: 0 1px 1px #ffffff;
-}
-.oo-ui-textInputWidget.oo-ui-iconElement input,
-.oo-ui-textInputWidget.oo-ui-iconElement textarea {
- padding-left: 2.475em;
-}
-.oo-ui-textInputWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
- width: 1.875em;
- max-height: 2.375em;
- margin-left: 0.3em;
-}
-.oo-ui-textInputWidget.oo-ui-indicatorElement input,
-.oo-ui-textInputWidget.oo-ui-indicatorElement textarea {
- padding-right: 2.4875em;
-}
-.oo-ui-textInputWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- width: 0.9375em;
- max-height: 2.375em;
- margin-right: 0.775em;
-}
-.oo-ui-textInputWidget > .oo-ui-labelElement-label {
- padding: 0.4em;
- line-height: 1.5em;
- color: #888888;
-}
-.oo-ui-textInputWidget-labelPosition-after.oo-ui-indicatorElement > .oo-ui-labelElement-label {
- margin-right: 2.0875em;
-}
-.oo-ui-textInputWidget-labelPosition-before.oo-ui-iconElement > .oo-ui-labelElement-label {
- margin-left: 2.075em;
-}
-.oo-ui-menuSelectWidget {
- position: absolute;
- background-color: #ffffff;
- margin-top: -1px;
- border: 1px solid #cccccc;
- border-radius: 0 0 0.25em 0.25em;
- box-shadow: 0 0.15em 1em 0 rgba(0, 0, 0, 0.2);
-}
-.oo-ui-menuSelectWidget input {
- position: absolute;
- width: 0;
- height: 0;
- overflow: hidden;
- opacity: 0;
-}
-.oo-ui-menuOptionWidget {
- position: relative;
-}
-.oo-ui-menuOptionWidget .oo-ui-iconElement-icon {
- display: none;
-}
-.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected {
- background-color: transparent;
-}
-.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected .oo-ui-iconElement-icon {
- display: block;
-}
-.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected {
- background-color: transparent;
-}
-.oo-ui-menuOptionWidget.oo-ui-optionWidget-highlighted,
-.oo-ui-menuOptionWidget.oo-ui-optionWidget-highlighted.oo-ui-optionWidget-selected {
- background-color: #e1f3ff;
-}
-.oo-ui-menuSectionOptionWidget {
- cursor: default;
- padding: 0.33em 0.75em;
- color: #888888;
-}
-.oo-ui-dropdownWidget {
- display: inline-block;
- position: relative;
- width: 100%;
- max-width: 50em;
- background-color: #ffffff;
- margin-right: 0.5em;
-}
-.oo-ui-dropdownWidget-handle {
- width: 100%;
- display: inline-block;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-dropdownWidget-handle .oo-ui-indicatorElement-indicator,
-.oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
- position: absolute;
-}
-.oo-ui-dropdownWidget > .oo-ui-menuSelectWidget {
- z-index: 1;
- width: 100%;
-}
-.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle {
- cursor: pointer;
-}
-.oo-ui-dropdownWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-dropdownWidget-handle {
- height: 2.5em;
- border: 1px solid rgba(0, 0, 0, 0.1);
- border-radius: 0.25em;
-}
-.oo-ui-dropdownWidget-handle:hover {
- border-color: rgba(0, 0, 0, 0.2);
-}
-.oo-ui-dropdownWidget-handle .oo-ui-indicatorElement-indicator {
- right: 0;
-}
-.oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
- left: 0.25em;
-}
-.oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {
- line-height: 2.5em;
- margin: 0 0.5em;
-}
-.oo-ui-dropdownWidget-handle .oo-ui-indicatorElement-indicator {
- top: 0;
- width: 0.9375em;
- height: 0.9375em;
- margin: 0.775em;
-}
-.oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
- top: 0;
- width: 1.875em;
- height: 1.875em;
- margin: 0.3em;
-}
-.oo-ui-dropdownWidget.oo-ui-widget-disabled .oo-ui-dropdownWidget-handle {
- color: #cccccc;
- text-shadow: 0 1px 1px #ffffff;
- border-color: #dddddd;
- background-color: #f3f3f3;
-}
-.oo-ui-dropdownWidget.oo-ui-widget-disabled .oo-ui-dropdownWidget-handle:focus {
- outline: 0;
-}
-.oo-ui-dropdownWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
- opacity: 0.2;
-}
-.oo-ui-dropdownWidget.oo-ui-iconElement .oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {
- margin-left: 3em;
-}
-.oo-ui-dropdownWidget.oo-ui-indicatorElement .oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {
- margin-right: 2em;
-}
-.oo-ui-comboBoxInputWidget {
- display: inline-block;
- position: relative;
- width: 100%;
- max-width: 50em;
- margin-right: 0.5em;
-}
-.oo-ui-comboBoxInputWidget > .oo-ui-menuSelectWidget {
- z-index: 1;
- width: 100%;
-}
-.oo-ui-comboBoxInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
- cursor: pointer;
-}
-.oo-ui-comboBoxInputWidget-php input::-webkit-calendar-picker-indicator {
- opacity: 0 !important;
- position: absolute;
- right: 0;
- top: 0;
- height: 2.5em;
- width: 2.5em;
- padding: 0;
-}
-.oo-ui-comboBoxInputWidget-php > .oo-ui-indicatorElement-indicator {
- pointer-events: none;
-}
-.oo-ui-comboBoxInputWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-comboBoxInputWidget.oo-ui-widget-disabled .oo-ui-textInputWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator,
-.oo-ui-comboBoxInputWidget-empty .oo-ui-textInputWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- cursor: default;
- opacity: 0.2;
-}
-.oo-ui-comboBoxInputWidget > .oo-ui-selectWidget {
- margin-top: -3px;
-}
-
-/*!
- * OOjs UI v0.15.2
- * https://www.mediawiki.org/wiki/OOjs_UI
- *
- * Copyright 2011–2016 OOjs UI Team and other contributors.
- * Released under the MIT license
- * http://oojs.mit-license.org
- *
- * Date: 2016-02-02T22:07:06Z
- */
-@-webkit-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-moz-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-ms-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-o-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-.oo-ui-draggableElement {
- cursor: -webkit-grab -moz-grab, url(images/grab.cur), move;
-}
-.oo-ui-draggableElement-dragging {
- cursor: -webkit-grabbing -moz-grabbing, url(images/grabbing.cur), move;
- background: rgba(0, 0, 0, 0.2);
- opacity: 0.4;
-}
-.oo-ui-draggableGroupElement-horizontal .oo-ui-draggableElement.oo-ui-optionWidget {
- display: inline-block;
-}
-.oo-ui-draggableGroupElement-placeholder {
- position: absolute;
- display: block;
- background: rgba(0, 0, 0, 0.4);
-}
-.oo-ui-lookupElement > .oo-ui-menuSelectWidget {
- z-index: 1;
- width: 100%;
-}
-.oo-ui-bookletLayout-stackLayout.oo-ui-stackLayout-continuous > .oo-ui-panelLayout-scrollable {
- overflow-y: hidden;
-}
-.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout {
- width: 100%;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout-scrollable {
- overflow-y: auto;
-}
-.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout-padded {
- padding: 2em;
-}
-.oo-ui-bookletLayout-outlinePanel-editable > .oo-ui-outlineSelectWidget {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 3em;
- overflow-y: auto;
-}
-.oo-ui-bookletLayout-outlinePanel > .oo-ui-outlineControlsWidget {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
-}
-.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout {
- padding: 1.5em;
-}
-.oo-ui-bookletLayout-outlinePanel {
- border-right: 1px solid #dddddd;
-}
-.oo-ui-bookletLayout-outlinePanel > .oo-ui-outlineControlsWidget {
- box-shadow: 0 0 0.25em rgba(0, 0, 0, 0.25);
-}
-.oo-ui-indexLayout > .oo-ui-menuLayout-menu {
- height: 3em;
-}
-.oo-ui-indexLayout > .oo-ui-menuLayout-content {
- top: 3em;
-}
-.oo-ui-indexLayout-stackLayout > .oo-ui-panelLayout {
- padding: 1.5em;
-}
-.oo-ui-menuLayout {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
-}
-.oo-ui-menuLayout-menu,
-.oo-ui-menuLayout-content {
- position: absolute;
- -webkit-transition: all 200ms ease;
- -moz-transition: all 200ms ease;
- transition: all 200ms ease;
-}
-.oo-ui-menuLayout-menu {
- height: 18em;
- width: 18em;
-}
-.oo-ui-menuLayout-content {
- top: 18em;
- left: 18em;
- right: 18em;
- bottom: 18em;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-hideMenu > .oo-ui-menuLayout-menu {
- width: 0 !important;
- height: 0 !important;
- overflow: hidden;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-hideMenu > .oo-ui-menuLayout-content {
- top: 0 !important;
- left: 0 !important;
- right: 0 !important;
- bottom: 0 !important;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-top > .oo-ui-menuLayout-menu {
- width: auto !important;
- left: 0;
- top: 0;
- right: 0;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-top > .oo-ui-menuLayout-content {
- right: 0 !important;
- bottom: 0 !important;
- left: 0 !important;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-after > .oo-ui-menuLayout-menu {
- height: auto !important;
- top: 0;
- right: 0;
- bottom: 0;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-after > .oo-ui-menuLayout-content {
- bottom: 0 !important;
- left: 0 !important;
- top: 0 !important;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-bottom > .oo-ui-menuLayout-menu {
- width: auto !important;
- right: 0;
- bottom: 0;
- left: 0;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-bottom > .oo-ui-menuLayout-content {
- left: 0 !important;
- top: 0 !important;
- right: 0 !important;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-before > .oo-ui-menuLayout-menu {
- height: auto !important;
- bottom: 0;
- left: 0;
- top: 0;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-before > .oo-ui-menuLayout-content {
- top: 0 !important;
- right: 0 !important;
- bottom: 0 !important;
-}
-.oo-ui-stackLayout-continuous > .oo-ui-panelLayout {
- display: block;
- position: relative;
-}
-.oo-ui-buttonSelectWidget {
- display: inline-block;
- white-space: nowrap;
- border-radius: 0.3em;
- margin-right: 0.5em;
-}
-.oo-ui-buttonSelectWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget .oo-ui-buttonElement-button {
- border-radius: 0;
- margin-left: -1px;
-}
-.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget:first-child .oo-ui-buttonElement-button {
- border-bottom-left-radius: 0.3em;
- border-top-left-radius: 0.3em;
- margin-left: 0;
-}
-.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget:last-child .oo-ui-buttonElement-button {
- border-bottom-right-radius: 0.3em;
- border-top-right-radius: 0.3em;
-}
-.oo-ui-buttonOptionWidget {
- display: inline-block;
- padding: 0;
- background-color: transparent;
-}
-.oo-ui-buttonOptionWidget .oo-ui-buttonElement-button {
- position: relative;
-}
-.oo-ui-buttonOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
-.oo-ui-buttonOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- position: static;
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-buttonOptionWidget .oo-ui-buttonElement-button {
- height: 1.875em;
-}
-.oo-ui-buttonOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
- margin-top: 0;
-}
-.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected,
-.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
-.oo-ui-buttonOptionWidget.oo-ui-optionWidget-highlighted {
- background-color: transparent;
-}
-.oo-ui-toggleButtonWidget {
- display: inline-block;
- vertical-align: middle;
- margin-right: 0.5em;
-}
-.oo-ui-toggleButtonWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-toggleSwitchWidget {
- position: relative;
- display: inline-block;
- vertical-align: middle;
- overflow: hidden;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- -webkit-transform: translateZ(0);
- -moz-transform: translateZ(0);
- -ms-transform: translateZ(0);
- transform: translateZ(0);
- height: 2em;
- width: 4em;
- border-radius: 1em;
- box-shadow: 0 0 0 white, inset 0 0.1em 0.2em #dddddd;
- border: 1px solid #cccccc;
- margin-right: 0.5em;
- background-color: #eeeeee;
- background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #dddddd), color-stop(100%, #ffffff));
- background-image: -webkit-linear-gradient(top, #dddddd 0, #ffffff 100%);
- background-image: -moz-linear-gradient(top, #dddddd 0, #ffffff 100%);
- background-image: linear-gradient(to bottom, #dddddd 0, #ffffff 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffdddddd', endColorstr='#ffffffff' )";
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled {
- cursor: pointer;
-}
-.oo-ui-toggleSwitchWidget-grip {
- position: absolute;
- display: block;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-toggleSwitchWidget .oo-ui-toggleSwitchWidget-glow {
- position: absolute;
- top: 0;
- bottom: 0;
- right: 0;
- left: 0;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-glow {
- display: none;
-}
-.oo-ui-toggleSwitchWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-disabled {
- opacity: 0.5;
-}
-.oo-ui-toggleSwitchWidget-grip {
- top: 0.25em;
- left: 0.25em;
- width: 1.5em;
- height: 1.5em;
- margin-top: -1px;
- border-radius: 1em;
- box-shadow: 0 0.1em 0.25em rgba(0, 0, 0, 0.1);
- border: 1px #c9c9c9 solid;
- -webkit-transition: left 250ms ease, margin-left 250ms ease;
- -moz-transition: left 250ms ease, margin-left 250ms ease;
- transition: left 250ms ease, margin-left 250ms ease;
- background-color: #eeeeee;
- background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #ffffff), color-stop(100%, #dddddd));
- background-image: -webkit-linear-gradient(top, #ffffff 0, #dddddd 100%);
- background-image: -moz-linear-gradient(top, #ffffff 0, #dddddd 100%);
- background-image: linear-gradient(to bottom, #ffffff 0, #dddddd 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffffff', endColorstr='#ffdddddd' )";
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover,
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover .oo-ui-toggleSwitchWidget-grip {
- border-color: #aaaaaa;
-}
-.oo-ui-toggleSwitchWidget .oo-ui-toggleSwitchWidget-glow {
- border-radius: 1em;
- box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.07);
- -webkit-transition: opacity 250ms ease;
- -moz-transition: opacity 250ms ease;
- transition: opacity 250ms ease;
- background-color: #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%);
- background-image: -moz-linear-gradient(top, #b0d9ee 0, #eaf4fa 100%);
- background-image: linear-gradient(to bottom, #b0d9ee 0, #eaf4fa 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffb0d9ee', endColorstr='#ffeaf4fa' )";
-}
-.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-glow {
- opacity: 1;
-}
-.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-grip {
- left: 2.25em;
- margin-left: -2px;
-}
-.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-glow {
- display: block;
- opacity: 0;
-}
-.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-grip {
- left: 0.25em;
- margin-left: 0;
-}
-.oo-ui-progressBarWidget {
- max-width: 50em;
- background-color: #ffffff;
- border: 1px solid #cccccc;
- border-radius: 0.25em;
- overflow: hidden;
-}
-.oo-ui-progressBarWidget-bar {
- height: 1em;
- border-right: 1px solid #cccccc;
- -webkit-transition: width 250ms ease, margin-left 250ms ease;
- -moz-transition: width 250ms ease, margin-left 250ms ease;
- transition: width 250ms ease, margin-left 250ms ease;
- background-color: #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%);
- background-image: -moz-linear-gradient(top, #eaf4fa 0, #b0d9ee 100%);
- background-image: linear-gradient(to bottom, #eaf4fa 0, #b0d9ee 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffeaf4fa', endColorstr='#ffb0d9ee' )";
-}
-.oo-ui-progressBarWidget-indeterminate .oo-ui-progressBarWidget-bar {
- -webkit-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
- -moz-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
- animation: oo-ui-progressBarWidget-slide 2s infinite linear;
- width: 40%;
- margin-left: -10%;
- border-left: 1px solid #a6cee1;
-}
-.oo-ui-progressBarWidget.oo-ui-widget-disabled {
- opacity: 0.6;
-}
-.oo-ui-selectFileWidget {
- display: inline-block;
- vertical-align: middle;
- width: 100%;
- max-width: 50em;
- margin-right: 0.5em;
-}
-.oo-ui-selectFileWidget-selectButton {
- display: table-cell;
- vertical-align: middle;
-}
-.oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button {
- position: relative;
- overflow: hidden;
-}
-.oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button > input[type="file"] {
- position: absolute;
- margin: 0;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- width: 100%;
- height: 100%;
- opacity: 0;
- z-index: 1;
- cursor: pointer;
- padding-top: 100px;
-}
-.oo-ui-selectFileWidget-selectButton.oo-ui-widget-disabled > .oo-ui-buttonElement-button > input[type="file"] {
- display: none;
-}
-.oo-ui-selectFileWidget-info {
- width: 100%;
- display: table-cell;
- vertical-align: middle;
- position: relative;
- overflow: hidden;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- text-overflow: ellipsis;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileName {
- float: left;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileType {
- float: right;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator,
-.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
- position: absolute;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
- z-index: 2;
-}
-.oo-ui-selectFileWidget-dropTarget {
- cursor: default;
-}
-.oo-ui-selectFileWidget-supported.oo-ui-widget-enabled .oo-ui-selectFileWidget-dropTarget {
- cursor: pointer;
-}
-.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-clearButton,
-.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-clearButton {
- display: none;
-}
-.oo-ui-selectFileWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button {
- margin-left: 0.5em;
-}
-.oo-ui-selectFileWidget-info {
- height: 2.4em;
- background-color: #ffffff;
- border: 1px solid rgba(0, 0, 0, 0.1);
- border-radius: 0.25em;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
- right: 0;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon {
- left: 0;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
- line-height: 2.3em;
- margin: 0;
- overflow: hidden;
- white-space: nowrap;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- text-overflow: ellipsis;
- left: 0.5em;
- right: 0.5em;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileType {
- color: #888888;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
- top: 0;
- width: 1.875em;
- margin-right: 0;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
- height: 2.3em;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
- top: 0;
- width: 0.9375em;
- height: 2.3em;
- margin-right: 0.775em;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon {
- top: 0;
- width: 1.875em;
- height: 2.3em;
- margin-left: 0.3em;
-}
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info {
- color: #cccccc;
- text-shadow: 0 1px 1px #ffffff;
- border-color: #dddddd;
- background-color: #f3f3f3;
-}
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
- opacity: 0.2;
-}
-.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-label {
- color: #cccccc;
-}
-.oo-ui-selectFileWidget.oo-ui-iconElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
- left: 2.475em;
-}
-.oo-ui-selectFileWidget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
- right: 2.175em;
-}
-.oo-ui-selectFileWidget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-clearButton {
- right: 0;
-}
-.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
- right: 4.2625em;
-}
-.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-clearButton {
- right: 2.0875em;
-}
-.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label,
-.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
- right: 0.5em;
-}
-.oo-ui-selectFileWidget-empty.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label,
-.oo-ui-selectFileWidget-notsupported.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
- right: 2em;
-}
-.oo-ui-selectFileWidget-dropTarget {
- line-height: 3.5em;
- background-color: #ffffff;
- border: 1px dashed #aaaaaa;
- padding: 0.5em 1em;
- margin-bottom: 0.5em;
- text-align: center;
- vertical-align: middle;
-}
-.oo-ui-selectFileWidget-supported.oo-ui-widget-enabled .oo-ui-selectFileWidget-dropTarget:hover,
-.oo-ui-selectFileWidget-supported.oo-ui-widget-enabled.oo-ui-selectFileWidget-canDrop oo-ui-selectfilewidget-droptarget {
- background-color: #e1f3ff;
-}
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-dropTarget,
-.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-dropTarget {
- color: #cccccc;
- text-shadow: 0 1px 1px #ffffff;
- border-color: #dddddd;
- background-color: #f3f3f3;
-}
-.oo-ui-outlineOptionWidget {
- position: relative;
- cursor: pointer;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- font-size: 1.1em;
- padding: 0.75em;
-}
-.oo-ui-outlineOptionWidget.oo-ui-indicatorElement .oo-ui-labelElement-label {
- padding-right: 1.5em;
-}
-.oo-ui-outlineOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- opacity: 0.5;
-}
-.oo-ui-outlineOptionWidget-level-0 {
- padding-left: 3.5em;
-}
-.oo-ui-outlineOptionWidget-level-0 .oo-ui-iconElement-icon {
- left: 1em;
-}
-.oo-ui-outlineOptionWidget-level-1 {
- padding-left: 5em;
-}
-.oo-ui-outlineOptionWidget-level-1 .oo-ui-iconElement-icon {
- left: 2.5em;
-}
-.oo-ui-outlineOptionWidget-level-2 {
- padding-left: 6.5em;
-}
-.oo-ui-outlineOptionWidget-level-2 .oo-ui-iconElement-icon {
- left: 4em;
-}
-.oo-ui-selectWidget-depressed .oo-ui-outlineOptionWidget.oo-ui-optionWidget-selected {
- background-color: #a7dcff;
- text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
-}
-.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-important {
- font-weight: bold;
-}
-.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-placeholder {
- font-style: italic;
-}
-.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-empty .oo-ui-iconElement-icon {
- opacity: 0.5;
-}
-.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-empty .oo-ui-labelElement-label {
- color: #777777;
-}
-.oo-ui-outlineControlsWidget {
- height: 3em;
- background-color: #ffffff;
-}
-.oo-ui-outlineControlsWidget-items,
-.oo-ui-outlineControlsWidget-movers {
- float: left;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-outlineControlsWidget > .oo-ui-iconElement-icon {
- float: left;
- background-position: right center;
-}
-.oo-ui-outlineControlsWidget-items {
- float: left;
-}
-.oo-ui-outlineControlsWidget-items .oo-ui-buttonWidget {
- float: left;
-}
-.oo-ui-outlineControlsWidget-movers {
- float: right;
-}
-.oo-ui-outlineControlsWidget-movers .oo-ui-buttonWidget {
- float: right;
-}
-.oo-ui-outlineControlsWidget-items,
-.oo-ui-outlineControlsWidget-movers {
- height: 2em;
- margin: 0.5em 0.5em 0.5em 0;
- padding: 0;
-}
-.oo-ui-outlineControlsWidget > .oo-ui-iconElement-icon {
- width: 1.5em;
- height: 2em;
- margin: 0.5em 0 0.5em 0.5em;
- opacity: 0.2;
-}
-.oo-ui-tabSelectWidget {
- text-align: left;
- white-space: nowrap;
- overflow: hidden;
- background-color: #eeeeee;
- box-shadow: inset 0 -0.015em 0.1em rgba(0, 0, 0, 0.1);
-}
-.oo-ui-tabOptionWidget {
- display: inline-block;
- vertical-align: bottom;
- padding: 0.5em 1em;
- margin: 0.5em 0 0 0.75em;
- border: 1px solid transparent;
- border-bottom: none;
- border-top-left-radius: 0.5em;
- border-top-right-radius: 0.5em;
-}
-.oo-ui-tabOptionWidget.oo-ui-indicatorElement .oo-ui-labelElement-label {
- padding-right: 1.5em;
-}
-.oo-ui-tabOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- opacity: 0.5;
-}
-.oo-ui-selectWidget-pressed .oo-ui-tabOptionWidget.oo-ui-optionWidget-pressed {
- background-color: transparent;
-}
-.oo-ui-tabOptionWidget.oo-ui-widget-enabled:hover {
- background-color: rgba(255, 255, 255, 0.2);
- border-color: #dddddd;
-}
-.oo-ui-tabOptionWidget.oo-ui-widget-enabled:active {
- background-color: #ffffff;
- border-color: #dddddd;
-}
-.oo-ui-selectWidget-pressed .oo-ui-tabOptionWidget.oo-ui-optionWidget-selected,
-.oo-ui-selectWidget-depressed .oo-ui-tabOptionWidget.oo-ui-optionWidget-selected,
-.oo-ui-tabOptionWidget.oo-ui-optionWidget-selected:hover {
- background-color: #ffffff;
- border-color: #dddddd;
-}
-.oo-ui-capsuleMultiSelectWidget {
- display: inline-block;
- position: relative;
- width: 100%;
- max-width: 50em;
-}
-.oo-ui-capsuleMultiSelectWidget-handle {
- width: 100%;
- display: inline-block;
- position: relative;
-}
-.oo-ui-capsuleMultiSelectWidget-content {
- position: relative;
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-content > input {
- display: none;
-}
-.oo-ui-capsuleMultiSelectWidget-group {
- display: inline;
-}
-.oo-ui-capsuleMultiSelectWidget > .oo-ui-menuSelectWidget {
- z-index: 1;
- width: 100%;
-}
-.oo-ui-capsuleMultiSelectWidget-handle {
- background-color: #ffffff;
- cursor: text;
- min-height: 2.4em;
- margin-right: 0.5em;
- padding: 0.15em 0.25em;
- border: 1px solid rgba(0, 0, 0, 0.1);
- border-radius: 0.25em;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-capsuleMultiSelectWidget-handle:last-child {
- margin-right: 0;
-}
-.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator,
-.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon {
- position: absolute;
- background-position: center center;
- background-repeat: no-repeat;
-}
-.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-capsuleMultiSelectWidget-content > input {
- border: none;
- line-height: 1.675em;
- margin: 0;
- margin-left: 0.2em;
- padding: 0;
- font-size: inherit;
- font-family: inherit;
- background-color: transparent;
- color: black;
- vertical-align: middle;
-}
-.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-capsuleMultiSelectWidget-content > input:focus {
- outline: none;
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-indicatorElement .oo-ui-capsuleMultiSelectWidget-handle {
- padding-right: 2.4875em;
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-indicatorElement .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator {
- right: 0;
- top: 0;
- width: 0.9375em;
- height: 0.9375em;
- margin: 0.775em;
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-iconElement .oo-ui-capsuleMultiSelectWidget-handle {
- padding-left: 2.475em;
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-iconElement .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon {
- left: 0;
- top: 0;
- width: 1.875em;
- height: 1.875em;
- margin: 0.3em;
-}
-.oo-ui-capsuleMultiSelectWidget:hover .oo-ui-capsuleMultiSelectWidget-handle {
- border-color: rgba(0, 0, 0, 0.2);
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-handle {
- color: #cccccc;
- text-shadow: 0 1px 1px #ffffff;
- border-color: #dddddd;
- background-color: #f3f3f3;
- cursor: default;
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon,
-.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator {
- opacity: 0.2;
-}
-.oo-ui-capsuleMultiSelectWidget .oo-ui-selectWidget {
- border-top-color: #ffffff;
-}
-.oo-ui-capsuleItemWidget {
- position: relative;
- display: inline-block;
- cursor: default;
- white-space: nowrap;
- width: auto;
- max-width: 100%;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- vertical-align: middle;
- padding: 0 0.4em;
- margin: 0.1em;
- height: 1.7em;
- line-height: 1.7em;
- background-color: #eeeeee;
- background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #ffffff), color-stop(100%, #dddddd));
- background-image: -webkit-linear-gradient(top, #ffffff 0, #dddddd 100%);
- background-image: -moz-linear-gradient(top, #ffffff 0, #dddddd 100%);
- background-image: linear-gradient(to bottom, #ffffff 0, #dddddd 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffffff', endColorstr='#ffdddddd' )";
- border: 1px solid #cccccc;
- color: #555555;
- border-radius: 0.25em;
-}
-.oo-ui-capsuleItemWidget > .oo-ui-iconElement-icon {
- cursor: pointer;
-}
-.oo-ui-capsuleItemWidget.oo-ui-widget-disabled > .oo-ui-iconElement-icon {
- cursor: default;
-}
-.oo-ui-capsuleItemWidget.oo-ui-labelElement .oo-ui-labelElement-label {
- display: block;
- text-overflow: ellipsis;
- overflow: hidden;
-}
-.oo-ui-capsuleItemWidget.oo-ui-indicatorElement > .oo-ui-labelElement-label {
- padding-right: 1.3375em;
-}
-.oo-ui-capsuleItemWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
- position: absolute;
- right: 0.4em;
- top: 0;
- width: 0.9375em;
- height: 100%;
- background-repeat: no-repeat;
-}
-.oo-ui-capsuleItemWidget.oo-ui-indicatorElement > .oo-ui-indicator-clear {
- cursor: pointer;
-}
-.oo-ui-capsuleItemWidget.oo-ui-widget-disabled {
- opacity: 0.5;
- -webkit-transform: translate3d(0, 0, 0);
- box-shadow: none;
- color: #333333;
- background: #eeeeee;
- border-color: #cccccc;
-}
-.oo-ui-capsuleItemWidget.oo-ui-widget-disabled > .oo-ui-indicatorElement-indicator {
- opacity: 0.2;
-}
-.oo-ui-searchWidget-query {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
-}
-.oo-ui-searchWidget-query .oo-ui-textInputWidget {
- width: 100%;
-}
-.oo-ui-searchWidget-results {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- overflow-x: hidden;
- overflow-y: auto;
-}
-.oo-ui-searchWidget-query {
- height: 4em;
- padding: 0 1em;
- box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.2);
-}
-.oo-ui-searchWidget-query .oo-ui-textInputWidget {
- margin: 0.75em 0;
-}
-.oo-ui-searchWidget-results {
- top: 4em;
- padding: 1em;
- line-height: 0;
-}
-.oo-ui-numberInputWidget {
- display: inline-block;
- position: relative;
- max-width: 50em;
-}
-.oo-ui-numberInputWidget-field {
- display: table;
- table-layout: fixed;
- width: 100%;
-}
-.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget,
-.oo-ui-numberInputWidget-field > .oo-ui-textInputWidget {
- display: table-cell;
- vertical-align: middle;
-}
-.oo-ui-numberInputWidget-field > .oo-ui-textInputWidget {
- width: 100%;
-}
-.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget {
- white-space: nowrap;
-}
-.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget > .oo-ui-buttonElement-button {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget {
- width: 2.25em;
-}
-.oo-ui-numberInputWidget-minusButton.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- border-right-width: 0;
-}
-.oo-ui-numberInputWidget-plusButton.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
- border-left-width: 0;
-}
-.oo-ui-numberInputWidget .oo-ui-textInputWidget input {
- border-radius: 0;
-}
-
-/*!
- * OOjs UI v0.15.2
- * https://www.mediawiki.org/wiki/OOjs_UI
- *
- * Copyright 2011–2016 OOjs UI Team and other contributors.
- * Released under the MIT license
- * http://oojs.mit-license.org
- *
- * Date: 2016-02-02T22:07:06Z
- */
-@-webkit-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-moz-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-ms-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-o-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-.oo-ui-popupTool .oo-ui-popupWidget-popup,
-.oo-ui-popupTool .oo-ui-popupWidget-anchor {
- z-index: 4;
-}
-.oo-ui-popupTool .oo-ui-popupWidget {
- /* @noflip */
- margin-left: 1.25em;
-}
-.oo-ui-toolGroupTool > .oo-ui-popupToolGroup {
- border: 0;
- border-radius: 0;
- margin: 0;
-}
-.oo-ui-toolGroupTool:first-child > .oo-ui-popupToolGroup {
- border-top-left-radius: 0.3125em;
- border-bottom-left-radius: 0.3125em;
-}
-.oo-ui-toolGroupTool:last-child > .oo-ui-popupToolGroup {
- border-top-right-radius: 0.3125em;
- border-bottom-right-radius: 0.3125em;
-}
-.oo-ui-toolGroupTool > .oo-ui-popupToolGroup > .oo-ui-popupToolGroup-handle {
- height: 1.875em;
- padding: 0.3125em;
-}
-.oo-ui-toolGroupTool > .oo-ui-popupToolGroup > .oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
- height: 1.875em;
- width: 1.875em;
-}
-.oo-ui-toolGroupTool > .oo-ui-popupToolGroup.oo-ui-labelElement > .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- line-height: 2.1em;
-}
-.oo-ui-toolGroup {
- display: inline-block;
- vertical-align: middle;
- margin: 0.375em;
- border-radius: 0.3125em;
- border: 1px solid transparent;
- -webkit-transition: border-color 250ms ease;
- -moz-transition: border-color 250ms ease;
- transition: border-color 250ms ease;
-}
-.oo-ui-toolGroup-empty {
- display: none;
-}
-.oo-ui-toolGroup .oo-ui-tool-link {
- text-decoration: none;
-}
-.oo-ui-toolbar-narrow .oo-ui-toolGroup + .oo-ui-toolGroup {
- margin-left: 0;
-}
-.oo-ui-toolGroup.oo-ui-widget-enabled:hover {
- border-color: rgba(0, 0, 0, 0.1);
-}
-.oo-ui-toolGroup.oo-ui-widget-enabled .oo-ui-tool-link .oo-ui-tool-title {
- color: #000000;
-}
-.oo-ui-barToolGroup > .oo-ui-iconElement-icon,
-.oo-ui-barToolGroup > .oo-ui-labelElement-label {
- display: none;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link {
- cursor: pointer;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool {
- display: inline-block;
- position: relative;
- vertical-align: top;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link {
- display: block;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-tool-accel {
- display: none;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-iconElement > .oo-ui-tool-link .oo-ui-iconElement-icon {
- display: inline-block;
- vertical-align: top;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-iconElement > .oo-ui-tool-link .oo-ui-tool-title {
- display: none;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-iconElement.oo-ui-tool-with-label > .oo-ui-tool-link .oo-ui-tool-title {
- display: inline;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link {
- outline: 0;
- cursor: default;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool {
- margin: -1px 0 -1px -1px;
- border: 1px solid transparent;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool:first-child {
- border-top-left-radius: 0.3125em;
- border-bottom-left-radius: 0.3125em;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool:last-child {
- margin-right: -1px;
- border-top-right-radius: 0.3125em;
- border-bottom-right-radius: 0.3125em;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link {
- height: 1.875em;
- padding: 0.3125em;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-iconElement-icon {
- height: 1.875em;
- width: 1.875em;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-tool-title {
- line-height: 2.1em;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled:hover {
- border-color: rgba(0, 0, 0, 0.2);
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-tool-active.oo-ui-widget-enabled {
- border-color: rgba(0, 0, 0, 0.2);
- box-shadow: inset 0 0.0875em 0.0875em 0 rgba(0, 0, 0, 0.07);
- background-color: #f8fbfd;
- background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #f1f7fb), color-stop(100%, #ffffff));
- background-image: -webkit-linear-gradient(top, #f1f7fb 0, #ffffff 100%);
- background-image: -moz-linear-gradient(top, #f1f7fb 0, #ffffff 100%);
- background-image: linear-gradient(to bottom, #f1f7fb 0, #ffffff 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff1f7fb', endColorstr='#ffffffff' )";
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-tool-active.oo-ui-widget-enabled + .oo-ui-tool-active.oo-ui-widget-enabled {
- border-left-color: rgba(0, 0, 0, 0.1);
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link:focus {
- outline: 0;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link .oo-ui-tool-title {
- color: #cccccc;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link .oo-ui-iconElement-icon {
- opacity: 0.2;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled:hover > .oo-ui-tool-link .oo-ui-iconElement-icon {
- opacity: 1;
-}
-.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool:focus {
- outline: 0;
-}
-.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link:focus {
- outline: 0;
-}
-.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-tool-title {
- color: #cccccc;
-}
-.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-iconElement-icon {
- opacity: 0.2;
-}
-.oo-ui-popupToolGroup {
- position: relative;
- height: 2.5em;
- min-width: 2.5em;
-}
-.oo-ui-popupToolGroup-handle {
- display: block;
- cursor: pointer;
-}
-.oo-ui-popupToolGroup-handle .oo-ui-indicatorElement-indicator,
-.oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
- position: absolute;
-}
-.oo-ui-popupToolGroup.oo-ui-widget-disabled .oo-ui-popupToolGroup-handle {
- outline: 0;
- cursor: default;
-}
-.oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
- display: none;
- position: absolute;
- z-index: 4;
-}
-.oo-ui-popupToolGroup-active.oo-ui-widget-enabled > .oo-ui-toolGroup-tools {
- display: block;
-}
-.oo-ui-popupToolGroup-left > .oo-ui-toolGroup-tools {
- left: 0;
-}
-.oo-ui-popupToolGroup-right > .oo-ui-toolGroup-tools {
- right: 0;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link {
- display: table;
- width: 100%;
- vertical-align: middle;
- white-space: nowrap;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-iconElement-icon,
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel,
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-title {
- display: table-cell;
- vertical-align: middle;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel {
- text-align: right;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel:not(:empty) {
- padding-left: 3em;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup {
- min-width: 1.875em;
-}
-.oo-ui-popupToolGroup.oo-ui-iconElement {
- min-width: 3.125em;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-iconElement {
- min-width: 2.5em;
-}
-.oo-ui-popupToolGroup.oo-ui-indicatorElement.oo-ui-iconElement {
- min-width: 4.375em;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-indicatorElement.oo-ui-iconElement {
- min-width: 3.75em;
-}
-.oo-ui-popupToolGroup.oo-ui-labelElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- line-height: 2.6em;
- margin: 0 1em;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-labelElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- margin: 0 0.5em;
-}
-.oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-iconElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- margin-left: 3em;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-iconElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- margin-left: 2.5em;
-}
-.oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-indicatorElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- margin-right: 2.25em;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-indicatorElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- margin-right: 1.75em;
-}
-.oo-ui-popupToolGroup-handle .oo-ui-indicatorElement-indicator {
- width: 0.9375em;
- height: 0.9375em;
- margin: 0.78125em;
- top: 0;
- right: 0;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup-handle .oo-ui-indicatorElement-indicator {
- right: -0.3125em;
-}
-.oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
- width: 1.875em;
- height: 1.875em;
- margin: 0.3125em;
- top: 0;
- left: 0.3125em;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
- left: 0;
-}
-.oo-ui-popupToolGroup-header {
- line-height: 2.6em;
- margin: 0 0.6em;
- font-weight: bold;
-}
-.oo-ui-popupToolGroup-active.oo-ui-widget-enabled {
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
- box-shadow: inset 0 0.0875em 0.0875em 0 rgba(0, 0, 0, 0.07);
- background-color: #f8fbfd;
- background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #f1f7fb), color-stop(100%, #ffffff));
- background-image: -webkit-linear-gradient(top, #f1f7fb 0, #ffffff 100%);
- background-image: -moz-linear-gradient(top, #f1f7fb 0, #ffffff 100%);
- background-image: linear-gradient(to bottom, #f1f7fb 0, #ffffff 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff1f7fb', endColorstr='#ffffffff' )";
-}
-.oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
- top: 2.5em;
- margin: 0 -1px;
- border: 1px solid #cccccc;
- background-color: white;
- box-shadow: 0 0.3125em 1.25em rgba(0, 0, 0, 0.25);
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link {
- padding: 0.3125em 0 0.3125em 0.3125em;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-iconElement-icon {
- height: 1.875em;
- width: 1.875em;
- min-width: 1.875em;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-title {
- padding-left: 0.5em;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel,
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-title {
- line-height: 2em;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel {
- color: #888888;
-}
-.oo-ui-listToolGroup .oo-ui-tool {
- display: block;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-listToolGroup .oo-ui-tool-link {
- cursor: pointer;
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link {
- cursor: default;
-}
-.oo-ui-listToolGroup .oo-ui-toolGroup-tools {
- padding: 0.3125em;
-}
-.oo-ui-listToolGroup.oo-ui-popupToolGroup-active {
- border-color: rgba(0, 0, 0, 0.2);
-}
-.oo-ui-listToolGroup .oo-ui-tool {
- border: 1px solid transparent;
- margin: -1px 0;
- padding: 0 0.625em 0 0;
-}
-.oo-ui-listToolGroup .oo-ui-tool-active.oo-ui-widget-enabled {
- border-color: rgba(0, 0, 0, 0.1);
- box-shadow: inset 0 0.0875em 0.0875em 0 rgba(0, 0, 0, 0.07);
- background-color: #f8fbfd;
- background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #f1f7fb), color-stop(100%, #ffffff));
- background-image: -webkit-linear-gradient(top, #f1f7fb 0, #ffffff 100%);
- background-image: -moz-linear-gradient(top, #f1f7fb 0, #ffffff 100%);
- background-image: linear-gradient(to bottom, #f1f7fb 0, #ffffff 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff1f7fb', endColorstr='#ffffffff' )";
-}
-.oo-ui-listToolGroup .oo-ui-tool-active.oo-ui-widget-enabled + .oo-ui-tool-active.oo-ui-widget-enabled {
- border-top-color: rgba(0, 0, 0, 0.1);
-}
-.oo-ui-listToolGroup .oo-ui-tool-active.oo-ui-widget-enabled:hover {
- border-color: rgba(0, 0, 0, 0.2);
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover {
- border-color: rgba(0, 0, 0, 0.2);
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover .oo-ui-tool-link .oo-ui-iconElement-icon {
- opacity: 1;
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-title {
- color: #cccccc;
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-accel {
- color: #dddddd;
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-iconElement-icon {
- opacity: 0.2;
-}
-.oo-ui-listToolGroup.oo-ui-widget-disabled {
- color: #cccccc;
-}
-.oo-ui-listToolGroup.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator,
-.oo-ui-listToolGroup.oo-ui-widget-disabled .oo-ui-iconElement-icon {
- opacity: 0.2;
-}
-.oo-ui-menuToolGroup {
- border-color: rgba(0, 0, 0, 0.1);
-}
-.oo-ui-menuToolGroup .oo-ui-tool {
- display: block;
-}
-.oo-ui-menuToolGroup .oo-ui-tool-link {
- cursor: pointer;
-}
-.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link {
- cursor: default;
-}
-.oo-ui-menuToolGroup .oo-ui-popupToolGroup-handle {
- min-width: 10em;
-}
-.oo-ui-toolbar-narrow .oo-ui-menuToolGroup .oo-ui-popupToolGroup-handle {
- min-width: 8.125em;
-}
-.oo-ui-menuToolGroup .oo-ui-toolGroup-tools {
- padding: 0.3125em 0 0.3125em 0;
-}
-.oo-ui-menuToolGroup.oo-ui-widget-enabled:hover {
- border-color: rgba(0, 0, 0, 0.2);
-}
-.oo-ui-menuToolGroup.oo-ui-popupToolGroup-active {
- border-color: rgba(0, 0, 0, 0.25);
-}
-.oo-ui-menuToolGroup .oo-ui-tool {
- padding: 0 1.25em 0 0.3125em;
-}
-.oo-ui-menuToolGroup .oo-ui-tool-link .oo-ui-iconElement-icon {
- background-image: none;
-}
-.oo-ui-menuToolGroup .oo-ui-tool-active .oo-ui-tool-link .oo-ui-iconElement-icon {
- background-image: url("themes/apex/images/icons/check.png");
- background-image: -webkit-linear-gradient(transparent, transparent), /* @embed */ url("themes/apex/images/icons/check.svg");
- background-image: linear-gradient(transparent, transparent), /* @embed */ url("themes/apex/images/icons/check.svg");
- background-image: -o-linear-gradient(transparent, transparent), url("themes/apex/images/icons/check.png");
- background-size: contain;
- background-position: center center;
- background-repeat: no-repeat;
-}
-.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover {
- background-color: #e1f3ff;
-}
-.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-title {
- color: #cccccc;
-}
-.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-iconElement-icon {
- opacity: 0.2;
-}
-.oo-ui-menuToolGroup.oo-ui-widget-disabled {
- color: #cccccc;
- border-color: rgba(0, 0, 0, 0.05);
-}
-.oo-ui-menuToolGroup.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator,
-.oo-ui-menuToolGroup.oo-ui-widget-disabled .oo-ui-iconElement-icon {
- opacity: 0.2;
-}
-.oo-ui-toolbar {
- clear: both;
-}
-.oo-ui-toolbar-bar {
- line-height: 1em;
- position: relative;
-}
-.oo-ui-toolbar-actions {
- float: right;
-}
-.oo-ui-toolbar-actions .oo-ui-toolbar {
- display: inline-block;
-}
-.oo-ui-toolbar-tools {
- display: inline;
- white-space: nowrap;
-}
-.oo-ui-toolbar-narrow .oo-ui-toolbar-tools {
- white-space: normal;
-}
-.oo-ui-toolbar-tools .oo-ui-tool {
- white-space: normal;
-}
-.oo-ui-toolbar-tools,
-.oo-ui-toolbar-actions,
-.oo-ui-toolbar-shadow {
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-.oo-ui-toolbar-actions .oo-ui-popupWidget {
- -webkit-touch-callout: default;
- -webkit-user-select: all;
- -moz-user-select: all;
- -ms-user-select: all;
- user-select: all;
-}
-.oo-ui-toolbar-shadow {
- background-position: left top;
- background-repeat: repeat-x;
- position: absolute;
- width: 100%;
- pointer-events: none;
-}
-.oo-ui-toolbar-bar {
- border-bottom: 1px solid #cccccc;
- background-color: #f8fbfd;
- background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #ffffff), color-stop(100%, #f1f7fb));
- background-image: -webkit-linear-gradient(top, #ffffff 0, #f1f7fb 100%);
- background-image: -moz-linear-gradient(top, #ffffff 0, #f1f7fb 100%);
- background-image: linear-gradient(to bottom, #ffffff 0, #f1f7fb 100%);
- -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffffff', endColorstr='#fff1f7fb' )";
-}
-.oo-ui-toolbar-bar .oo-ui-toolbar-bar {
- border: none;
- background: none;
-}
-.oo-ui-toolbar-actions > .oo-ui-buttonElement-framed,
-.oo-ui-toolbar-actions > .oo-ui-buttonElement-framed:last-child {
- margin-top: 0.4em;
- margin-bottom: 0.4em;
- margin-right: 0.5em;
-}
-.oo-ui-toolbar-actions > .oo-ui-buttonElement-frameless.oo-ui-labelElement,
-.oo-ui-toolbar-actions > .oo-ui-buttonElement-frameless:last-child.oo-ui-labelElement {
- margin: 0;
-}
-.oo-ui-toolbar-actions > .oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button,
-.oo-ui-toolbar-actions > .oo-ui-buttonElement-frameless:last-child.oo-ui-labelElement > .oo-ui-buttonElement-button {
- margin: 0;
- padding: 0 0.3125em;
-}
-.oo-ui-toolbar-actions > .oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label,
-.oo-ui-toolbar-actions > .oo-ui-buttonElement-frameless:last-child.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- margin: 0 1em;
- line-height: 3.40625em;
-}
-.oo-ui-toolbar-shadow {
- background-image: /* @embed */ url(themes/apex/images/toolbar-shadow.png);
- bottom: -9px;
- height: 9px;
- opacity: 0.5;
- -webkit-transition: opacity 500ms ease;
- -moz-transition: opacity 500ms ease;
- transition: opacity 500ms ease;
-}
-
-/*!
- * OOjs UI v0.15.2
- * https://www.mediawiki.org/wiki/OOjs_UI
- *
- * Copyright 2011–2016 OOjs UI Team and other contributors.
- * Released under the MIT license
- * http://oojs.mit-license.org
- *
- * Date: 2016-02-02T22:07:06Z
- */
-@-webkit-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-moz-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-ms-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-o-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-.oo-ui-actionWidget.oo-ui-pendingElement-pending {
- background-image: /* @embed */ url(themes/apex/images/textures/pending.gif);
-}
-.oo-ui-window {
- background-color: transparent;
- background-image: none;
-}
-.oo-ui-window-frame {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-window-content:focus {
- outline: none;
-}
-.oo-ui-window-head,
-.oo-ui-window-foot {
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-.oo-ui-window-body {
- margin: 0;
- padding: 0;
- background: none;
-}
-.oo-ui-window-overlay {
- position: absolute;
- top: 0;
- /* @noflip */
- left: 0;
-}
-.oo-ui-dialog-content > .oo-ui-window-head,
-.oo-ui-dialog-content > .oo-ui-window-body,
-.oo-ui-dialog-content > .oo-ui-window-foot {
- position: absolute;
- left: 0;
- right: 0;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-dialog-content > .oo-ui-window-head {
- overflow: hidden;
- z-index: 1;
- top: 0;
-}
-.oo-ui-dialog-content > .oo-ui-window-body {
- overflow: auto;
- z-index: 2;
- top: 0;
- bottom: 0;
-}
-.oo-ui-dialog-content > .oo-ui-window-foot {
- overflow: hidden;
- z-index: 1;
- bottom: 0;
-}
-.oo-ui-dialog-content > .oo-ui-window-body {
- box-shadow: 0 0 0.66em rgba(0, 0, 0, 0.25);
-}
-.oo-ui-messageDialog-actions-horizontal {
- display: table;
- table-layout: fixed;
- width: 100%;
-}
-.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget {
- display: table-cell;
- width: 1%;
-}
-.oo-ui-messageDialog-actions-vertical {
- display: block;
-}
-.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget {
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget {
- position: relative;
- text-align: center;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-buttonElement-button {
- display: block;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-labelElement-label {
- position: relative;
- top: auto;
- bottom: auto;
- display: inline;
- white-space: nowrap;
-}
-.oo-ui-messageDialog-content .oo-ui-window-body {
- box-shadow: 0 0 0.33em rgba(0, 0, 0, 0.33);
-}
-.oo-ui-messageDialog-title,
-.oo-ui-messageDialog-message {
- display: block;
- text-align: center;
-}
-.oo-ui-messageDialog-title.oo-ui-labelElement,
-.oo-ui-messageDialog-message.oo-ui-labelElement {
- padding-top: 0.5em;
-}
-.oo-ui-messageDialog-title {
- font-size: 1.5em;
- line-height: 1em;
- color: #000000;
-}
-.oo-ui-messageDialog-message {
- font-size: 0.9em;
- line-height: 1.25em;
- color: #666666;
-}
-.oo-ui-messageDialog-message-verbose {
- font-size: 1.1em;
- line-height: 1.5em;
- text-align: left;
-}
-.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget {
- border-right: 1px solid #e5e5e5;
- margin: 0;
-}
-.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget:last-child {
- border-right-width: 0;
-}
-.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget {
- border-bottom: 1px solid #e5e5e5;
- margin: 0;
-}
-.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget:last-child {
- border-bottom-width: 0;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget {
- height: 3.4em;
- margin-right: 0;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
- text-align: center;
- line-height: 3.4em;
- padding: 0 2em;
-}
-.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-flaggedElement-progressive:hover {
- background-color: rgba(8, 126, 204, 0.05);
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-progressive:active {
- background-color: rgba(8, 126, 204, 0.1);
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-progressive .oo-ui-labelElement-label {
- font-weight: bold;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:hover {
- background-color: rgba(118, 171, 54, 0.05);
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:active {
- background-color: rgba(118, 171, 54, 0.1);
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:hover {
- background-color: rgba(212, 83, 83, 0.05);
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:active {
- background-color: rgba(212, 83, 83, 0.1);
-}
-.oo-ui-processDialog-location {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.oo-ui-processDialog-title {
- display: inline;
- padding: 0;
-}
-.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-processDialog-actions-primary {
- position: absolute;
- top: 0;
- bottom: 0;
-}
-.oo-ui-processDialog-actions-safe {
- left: 0;
-}
-.oo-ui-processDialog-actions-primary {
- right: 0;
-}
-.oo-ui-processDialog-errors {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 2;
- overflow-x: hidden;
- overflow-y: auto;
-}
-.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.33em rgba(0, 0, 0, 0.33);
-}
-.oo-ui-processDialog-navigation {
- position: relative;
- height: 3.4em;
- padding: 0 1em;
-}
-.oo-ui-processDialog-location {
- padding: 0.75em 0;
- height: 1.875em;
- cursor: default;
- text-align: center;
-}
-.oo-ui-processDialog-title {
- font-weight: bold;
- line-height: 1.875em;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-buttonElement-button,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-buttonElement-button,
-.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-buttonElement-button {
- min-width: 1.875em;
- min-height: 1.875em;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-labelElement-label,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-labelElement-label,
-.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-labelElement-label {
- line-height: 1.875em;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
-.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
- margin-top: -0.125em;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-framed,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-framed,
-.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-framed {
- margin: 0.75em;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-framed .oo-ui-buttonElement-button,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-framed .oo-ui-buttonElement-button,
-.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-framed .oo-ui-buttonElement-button {
- padding: 0 1em;
- vertical-align: middle;
- margin: -1px;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless,
-.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-frameless {
- margin: 0;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button,
-.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button {
- padding: 0.75em 1em;
- vertical-align: middle;
-}
-.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-flaggedElement-progressive:hover,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-progressive:hover {
- background-color: rgba(8, 126, 204, 0.05);
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggedElement-progressive:active,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-progressive:active {
- background-color: rgba(8, 126, 204, 0.1);
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggedElement-progressive .oo-ui-labelElement-label,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-progressive .oo-ui-labelElement-label {
- font-weight: bold;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:hover,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:hover {
- background-color: rgba(118, 171, 54, 0.05);
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:active,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:active {
- background-color: rgba(118, 171, 54, 0.1);
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:hover,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:hover {
- background-color: rgba(212, 83, 83, 0.05);
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:active,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:active {
- background-color: rgba(212, 83, 83, 0.1);
-}
-.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement {
- margin-right: 0;
-}
-.oo-ui-processDialog > .oo-ui-window-frame {
- min-height: 5em;
-}
-.oo-ui-processDialog-errors {
- background-color: rgba(255, 255, 255, 0.9);
- padding: 3em 3em 1.5em 3em;
- text-align: center;
-}
-.oo-ui-processDialog-errors .oo-ui-buttonWidget {
- margin: 2em 1em 2em 1em;
-}
-.oo-ui-processDialog-errors-title {
- font-size: 1.5em;
- color: #000000;
- margin-bottom: 2em;
-}
-.oo-ui-processDialog-error {
- text-align: left;
- margin: 1em;
- padding: 1em;
- border: 1px solid #ff9e9e;
- background-color: #fff7f7;
- border-radius: 0.25em;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog {
- position: fixed;
- width: 0;
- height: 0;
- overflow: hidden;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-active {
- width: auto;
- height: auto;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- padding: 1em;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup > .oo-ui-window-frame {
- position: absolute;
- right: 0;
- left: 0;
- margin: auto;
- overflow: hidden;
- max-width: 100%;
- max-height: 100%;
-}
-.oo-ui-windowManager-fullscreen > .oo-ui-dialog > .oo-ui-window-frame {
- width: 100%;
- height: 100%;
- top: 0;
- bottom: 0;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog {
- background-color: rgba(255, 255, 255, 0.5);
- opacity: 0;
- -webkit-transition: opacity 250ms ease;
- -moz-transition: opacity 250ms ease;
- transition: opacity 250ms ease;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog > .oo-ui-window-frame {
- background-color: #ffffff;
- opacity: 0;
- -webkit-transform: scale(0.5);
- -moz-transform: scale(0.5);
- -ms-transform: scale(0.5);
- transform: scale(0.5);
- -webkit-transition: all 250ms ease;
- -moz-transition: all 250ms ease;
- transition: all 250ms ease;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup {
- opacity: 1;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-ready > .oo-ui-window-frame {
- opacity: 1;
- -webkit-transform: scale(1);
- -moz-transform: scale(1);
- -ms-transform: scale(1);
- transform: scale(1);
-}
-.oo-ui-windowManager-modal.oo-ui-windowManager-floating > .oo-ui-dialog > .oo-ui-window-frame {
- top: 1em;
- bottom: 1em;
- border: 1px solid #cccccc;
- border-radius: 0.5em;
- box-shadow: 0 0.2em 1em rgba(0, 0, 0, 0.3);
-}
--- /dev/null
+/*!
+ * OOjs UI v0.15.2
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: 2016-02-02T22:07:06Z
+ */
+@-webkit-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-moz-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-ms-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-o-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+.oo-ui-element-hidden {
+ display: none !important;
+}
+.oo-ui-buttonElement > .oo-ui-buttonElement-button {
+ cursor: pointer;
+ display: inline-block;
+ vertical-align: middle;
+ font: inherit;
+ white-space: nowrap;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.oo-ui-buttonElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon,
+.oo-ui-buttonElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
+ display: none;
+}
+.oo-ui-buttonElement.oo-ui-widget-disabled > .oo-ui-buttonElement-button {
+ cursor: default;
+}
+.oo-ui-buttonElement.oo-ui-indicatorElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator,
+.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-buttonElement-frameless {
+ display: inline-block;
+ position: relative;
+}
+.oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button {
+ display: inline-block;
+ vertical-align: top;
+ text-align: center;
+}
+.oo-ui-buttonElement-framed.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled > .oo-ui-buttonElement-button,
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button,
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
+ cursor: default;
+}
+.oo-ui-buttonElement > .oo-ui-buttonElement-button {
+ color: #333333;
+}
+.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+ margin-left: 0;
+}
+.oo-ui-buttonElement.oo-ui-indicatorElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
+ width: 0.9375em;
+ height: 0.9375em;
+ margin: 0.46875em;
+}
+.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
+ margin-left: 0.46875em;
+}
+.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+ width: 1.875em;
+ height: 1.875em;
+}
+.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:hover,
+.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:focus {
+ outline: none;
+}
+.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:hover > .oo-ui-iconElement-icon,
+.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:focus > .oo-ui-iconElement-icon {
+ opacity: 1;
+}
+.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:hover > .oo-ui-labelElement-label,
+.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:focus > .oo-ui-labelElement-label {
+ color: #000000;
+}
+.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ color: #333333;
+}
+.oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ margin-left: 0.25em;
+}
+.oo-ui-buttonElement-frameless > input.oo-ui-buttonElement-button {
+ padding-left: 0.25em;
+ color: #333333;
+}
+.oo-ui-buttonElement-frameless > input.oo-ui-buttonElement-button:hover,
+.oo-ui-buttonElement-frameless > input.oo-ui-buttonElement-button:focus {
+ color: #000000;
+}
+.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ color: #087ecc;
+}
+.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ color: #76ab36;
+}
+.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ color: #d45353;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+ opacity: 0.2;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ color: #cccccc;
+}
+.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button {
+ padding: 0.2em 0.8em;
+ border-radius: 0.3em;
+ text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
+ border: 1px #c9c9c9 solid;
+ -webkit-transition: border-color 100ms ease;
+ -moz-transition: border-color 100ms ease;
+ transition: border-color 100ms ease;
+ background-color: #eeeeee;
+ background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #ffffff), color-stop(100%, #dddddd));
+ background-image: -webkit-linear-gradient(top, #ffffff 0, #dddddd 100%);
+ background-image: -moz-linear-gradient(top, #ffffff 0, #dddddd 100%);
+ background-image: linear-gradient(to bottom, #ffffff 0, #dddddd 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffffff', endColorstr='#ffdddddd' )";
+}
+.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button:hover,
+.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button:focus {
+ border-color: #aaaaaa;
+ outline: none;
+}
+.oo-ui-buttonElement-framed > input.oo-ui-buttonElement-button,
+.oo-ui-buttonElement-framed.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ line-height: 1.875em;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
+.oo-ui-buttonElement-framed.oo-ui-buttonElement-active > .oo-ui-buttonElement-button,
+.oo-ui-buttonElement-framed.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
+ box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.07);
+ color: black;
+ border-color: #c9c9c9;
+ background-color: #eeeeee;
+ background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #dddddd), color-stop(100%, #ffffff));
+ background-image: -webkit-linear-gradient(top, #dddddd 0, #ffffff 100%);
+ background-image: -moz-linear-gradient(top, #dddddd 0, #ffffff 100%);
+ background-image: linear-gradient(to bottom, #dddddd 0, #ffffff 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffdddddd', endColorstr='#ffffffff' )";
+}
+.oo-ui-buttonElement-framed.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+ margin-left: -0.5em;
+ margin-right: -0.5em;
+}
+.oo-ui-buttonElement-framed.oo-ui-iconElement.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+ margin-right: 0.3em;
+}
+.oo-ui-buttonElement-framed.oo-ui-indicatorElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
+ margin-left: -0.005em;
+ margin-right: -0.005em;
+}
+.oo-ui-buttonElement-framed.oo-ui-indicatorElement.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator,
+.oo-ui-buttonElement-framed.oo-ui-indicatorElement.oo-ui-iconElement:not( .oo-ui-labelElement ) > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
+ margin-left: 0.46875em;
+ margin-right: -0.275em;
+}
+.oo-ui-buttonElement-framed.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button {
+ border: 1px solid #a6cee1;
+ background-color: #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%);
+ background-image: -moz-linear-gradient(top, #eaf4fa 0, #b0d9ee 100%);
+ background-image: linear-gradient(to bottom, #eaf4fa 0, #b0d9ee 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffeaf4fa', endColorstr='#ffb0d9ee' )";
+}
+.oo-ui-buttonElement-framed.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:hover,
+.oo-ui-buttonElement-framed.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:focus {
+ border-color: #9dc2d4;
+}
+.oo-ui-buttonElement-framed.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
+.oo-ui-buttonElement-framed.oo-ui-flaggedElement-progressive.oo-ui-buttonElement-active > .oo-ui-buttonElement-button,
+.oo-ui-buttonElement-framed.oo-ui-flaggedElement-progressive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
+ border: 1px solid #a6cee1;
+ background-color: #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%);
+ background-image: -moz-linear-gradient(top, #b0d9ee 0, #eaf4fa 100%);
+ background-image: linear-gradient(to bottom, #b0d9ee 0, #eaf4fa 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffb0d9ee', endColorstr='#ffeaf4fa' )";
+}
+.oo-ui-buttonElement-framed.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button {
+ border: 1px solid #b8d892;
+ background-color: #daf0bd;
+ background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #f0fbe1), color-stop(100%, #c3e59a));
+ background-image: -webkit-linear-gradient(top, #f0fbe1 0, #c3e59a 100%);
+ background-image: -moz-linear-gradient(top, #f0fbe1 0, #c3e59a 100%);
+ background-image: linear-gradient(to bottom, #f0fbe1 0, #c3e59a 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff0fbe1', endColorstr='#ffc3e59a' )";
+}
+.oo-ui-buttonElement-framed.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:hover,
+.oo-ui-buttonElement-framed.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:focus {
+ border-color: #adcb89;
+}
+.oo-ui-buttonElement-framed.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
+.oo-ui-buttonElement-framed.oo-ui-flaggedElement-constructive.oo-ui-buttonElement-active > .oo-ui-buttonElement-button,
+.oo-ui-buttonElement-framed.oo-ui-flaggedElement-constructive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
+ border: 1px solid #b8d892;
+ background-color: #daf0bd;
+ background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #c3e59a), color-stop(100%, #f0fbe1));
+ background-image: -webkit-linear-gradient(top, #c3e59a 0, #f0fbe1 100%);
+ background-image: -moz-linear-gradient(top, #c3e59a 0, #f0fbe1 100%);
+ background-image: linear-gradient(to bottom, #c3e59a 0, #f0fbe1 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffc3e59a', endColorstr='#fff0fbe1' )";
+}
+.oo-ui-buttonElement-framed.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button {
+ color: #d45353;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled > .oo-ui-buttonElement-button,
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button,
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
+ opacity: 0.5;
+ -webkit-transform: translate3d(0, 0, 0);
+ box-shadow: none;
+ color: #333333;
+ background: #eeeeee;
+ border-color: #cccccc;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled > .oo-ui-buttonElement-button:hover,
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button:hover,
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button:hover,
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled > .oo-ui-buttonElement-button:focus,
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button:focus,
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button:focus {
+ border-color: #cccccc;
+ box-shadow: none;
+}
+.oo-ui-clippableElement-clippable {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-iconElement .oo-ui-iconElement-icon,
+.oo-ui-iconElement.oo-ui-iconElement-icon {
+ background-size: contain;
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+.oo-ui-iconElement .oo-ui-iconElement-icon,
+.oo-ui-iconElement.oo-ui-iconElement-icon {
+ opacity: 0.8;
+}
+.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator,
+.oo-ui-indicatorElement.oo-ui-indicatorElement-indicator {
+ background-size: contain;
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator,
+.oo-ui-indicatorElement.oo-ui-indicatorElement-indicator {
+ opacity: 0.8;
+}
+.oo-ui-pendingElement-pending {
+ background-image: /* @embed */ url(themes/apex/images/textures/pending.gif);
+}
+.oo-ui-fieldLayout {
+ display: block;
+ margin-bottom: 1em;
+}
+.oo-ui-fieldLayout:before,
+.oo-ui-fieldLayout:after {
+ content: " ";
+ display: table;
+}
+.oo-ui-fieldLayout:after {
+ clear: both;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
+ display: block;
+ float: left;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ text-align: right;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body {
+ display: table;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
+ display: table-cell;
+ vertical-align: middle;
+}
+.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ display: inline-block;
+}
+.oo-ui-fieldLayout > .oo-ui-fieldLayout-help {
+ float: right;
+}
+.oo-ui-fieldLayout > .oo-ui-fieldLayout-help > .oo-ui-popupWidget > .oo-ui-popupWidget-popup {
+ z-index: 1;
+}
+.oo-ui-fieldLayout > .oo-ui-fieldLayout-help .oo-ui-fieldLayout-help-content {
+ padding: 0.5em 0.75em;
+ line-height: 1.5em;
+}
+.oo-ui-fieldLayout:last-child {
+ margin-bottom: 0;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ padding-top: 0.5em;
+ margin-right: 5%;
+ width: 35%;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
+ width: 60%;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline {
+ margin-bottom: 1.25em;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ padding: 0.25em 0.25em 0.25em 0.5em;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-top.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ padding: 0.5em 0;
+}
+.oo-ui-fieldLayout > .oo-ui-popupButtonWidget {
+ margin-right: 0;
+ margin-top: 0.25em;
+}
+.oo-ui-fieldLayout > .oo-ui-popupButtonWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-fieldLayout-disabled > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ color: #cccccc;
+}
+.oo-ui-fieldLayout-messages {
+ list-style: none none;
+ margin: 0;
+ padding: 0;
+ margin-top: 0.25em;
+ margin-left: 0.25em;
+}
+.oo-ui-fieldLayout-messages > li {
+ margin: 0;
+ padding: 0;
+}
+.oo-ui-fieldLayout-messages .oo-ui-iconWidget {
+ display: none;
+}
+.oo-ui-fieldLayout-messages .oo-ui-fieldLayout-messages-error {
+ color: #d45353;
+}
+.oo-ui-fieldLayout-messages .oo-ui-labelWidget {
+ padding: 0;
+ line-height: 1.875em;
+ vertical-align: middle;
+}
+.oo-ui-actionFieldLayout-input,
+.oo-ui-actionFieldLayout-button {
+ display: table-cell;
+ vertical-align: middle;
+}
+.oo-ui-actionFieldLayout-input {
+ padding-right: 1em;
+}
+.oo-ui-actionFieldLayout-button {
+ width: 1%;
+ white-space: nowrap;
+}
+.oo-ui-fieldsetLayout {
+ position: relative;
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+.oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-iconElement-icon {
+ display: block;
+ position: absolute;
+}
+.oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label {
+ display: inline-block;
+}
+.oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help {
+ float: right;
+}
+.oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help > .oo-ui-popupWidget > .oo-ui-popupWidget-popup {
+ z-index: 1;
+}
+.oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help .oo-ui-fieldsetLayout-help-content {
+ padding: 0.5em 0.75em;
+ line-height: 1.5em;
+}
+.oo-ui-fieldsetLayout + .oo-ui-fieldsetLayout,
+.oo-ui-fieldsetLayout + .oo-ui-formLayout {
+ margin-top: 2em;
+}
+.oo-ui-fieldsetLayout > .oo-ui-labelElement-label {
+ font-size: 1.1em;
+ margin-bottom: 0.5em;
+ padding: 0.25em 0;
+ font-weight: bold;
+}
+.oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-labelElement-label {
+ padding-left: 2em;
+ line-height: 1.8em;
+}
+.oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-iconElement-icon {
+ left: 0;
+ top: 0.25em;
+ width: 1.875em;
+ height: 1.875em;
+}
+.oo-ui-fieldsetLayout > .oo-ui-popupButtonWidget {
+ margin-right: 0;
+}
+.oo-ui-fieldsetLayout > .oo-ui-popupButtonWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-formLayout + .oo-ui-fieldsetLayout,
+.oo-ui-formLayout + .oo-ui-formLayout {
+ margin-top: 2em;
+}
+.oo-ui-panelLayout {
+ position: relative;
+}
+.oo-ui-panelLayout-scrollable {
+ overflow-y: auto;
+}
+.oo-ui-panelLayout-expanded {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+.oo-ui-panelLayout-padded {
+ padding: 1.25em;
+}
+.oo-ui-panelLayout-framed {
+ border-radius: 0.5em;
+ box-shadow: 0 0.25em 1em rgba(0, 0, 0, 0.25);
+}
+.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed {
+ margin: 1em 0;
+}
+.oo-ui-horizontalLayout > .oo-ui-widget {
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-horizontalLayout > .oo-ui-layout {
+ display: inline-block;
+}
+.oo-ui-horizontalLayout > .oo-ui-layout,
+.oo-ui-horizontalLayout > .oo-ui-widget {
+ margin-right: 0.5em;
+}
+.oo-ui-horizontalLayout > .oo-ui-layout:last-child,
+.oo-ui-horizontalLayout > .oo-ui-widget:last-child {
+ margin-right: 0;
+}
+.oo-ui-horizontalLayout > .oo-ui-layout {
+ margin-bottom: 0;
+}
+.oo-ui-optionWidget {
+ position: relative;
+ display: block;
+ padding: 0.25em 0.5em;
+ border: none;
+}
+.oo-ui-optionWidget.oo-ui-widget-enabled {
+ cursor: pointer;
+}
+.oo-ui-optionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
+ display: block;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+.oo-ui-optionWidget-highlighted {
+ background-color: #e1f3ff;
+}
+.oo-ui-optionWidget .oo-ui-labelElement-label {
+ line-height: 1.5em;
+}
+.oo-ui-selectWidget-depressed .oo-ui-optionWidget-selected {
+ background-color: #a7dcff;
+}
+.oo-ui-selectWidget-pressed .oo-ui-optionWidget-pressed,
+.oo-ui-selectWidget-pressed .oo-ui-optionWidget-pressed.oo-ui-optionWidget-highlighted,
+.oo-ui-selectWidget-pressed .oo-ui-optionWidget-pressed.oo-ui-optionWidget-highlighted.oo-ui-optionWidget-selected {
+ background-color: #a7dcff;
+}
+.oo-ui-optionWidget.oo-ui-widget-disabled {
+ color: #cccccc;
+}
+.oo-ui-decoratedOptionWidget {
+ padding: 0.5em 2em 0.5em 3em;
+}
+.oo-ui-decoratedOptionWidget .oo-ui-iconElement-icon,
+.oo-ui-decoratedOptionWidget .oo-ui-indicatorElement-indicator {
+ position: absolute;
+}
+.oo-ui-decoratedOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
+.oo-ui-decoratedOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+ top: 0;
+ height: 100%;
+}
+.oo-ui-decoratedOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
+ width: 1.875em;
+ left: 0.5em;
+}
+.oo-ui-decoratedOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+ width: 0.9375em;
+ right: 0.5em;
+}
+.oo-ui-decoratedOptionWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
+.oo-ui-decoratedOptionWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+}
+.oo-ui-radioSelectWidget {
+ padding: 0.75em 0 0.5em 0;
+}
+.oo-ui-radioOptionWidget {
+ cursor: default;
+ padding: 0;
+ background-color: transparent;
+}
+.oo-ui-radioOptionWidget .oo-ui-radioInputWidget,
+.oo-ui-radioOptionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-radioOptionWidget.oo-ui-optionWidget-selected,
+.oo-ui-radioOptionWidget.oo-ui-optionWidget-pressed,
+.oo-ui-radioOptionWidget.oo-ui-optionWidget-highlighted {
+ background-color: transparent;
+}
+.oo-ui-radioOptionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
+ padding-left: 0.5em;
+}
+.oo-ui-radioOptionWidget .oo-ui-radioInputWidget {
+ margin-right: 0;
+}
+.oo-ui-labelWidget {
+ display: inline-block;
+ padding: 0.5em 0;
+}
+.oo-ui-iconWidget {
+ display: inline-block;
+ vertical-align: middle;
+ line-height: 2.5em;
+ height: 1.875em;
+ width: 1.875em;
+}
+.oo-ui-iconWidget.oo-ui-widget-disabled {
+ opacity: 0.2;
+}
+.oo-ui-indicatorWidget {
+ display: inline-block;
+ vertical-align: middle;
+ line-height: 2.5em;
+ height: 0.9375em;
+ width: 0.9375em;
+ margin: 0.46875em;
+}
+.oo-ui-indicatorWidget.oo-ui-widget-disabled {
+ opacity: 0.2;
+}
+.oo-ui-buttonWidget {
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: 0.5em;
+}
+.oo-ui-buttonWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-buttonGroupWidget {
+ display: inline-block;
+ white-space: nowrap;
+ border-radius: 0.3em;
+ margin-right: 0.5em;
+}
+.oo-ui-buttonGroupWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-buttonGroupWidget .oo-ui-buttonElement {
+ margin-right: 0;
+}
+.oo-ui-buttonGroupWidget .oo-ui-buttonElement:last-child {
+ margin-right: 0;
+}
+.oo-ui-buttonGroupWidget .oo-ui-buttonElement-framed .oo-ui-buttonElement-button {
+ border-radius: 0;
+ margin-left: -1px;
+}
+.oo-ui-buttonGroupWidget .oo-ui-buttonElement-framed:first-child .oo-ui-buttonElement-button {
+ border-bottom-left-radius: 0.3em;
+ border-top-left-radius: 0.3em;
+ margin-left: 0;
+}
+.oo-ui-buttonGroupWidget .oo-ui-buttonElement-framed:last-child .oo-ui-buttonElement-button {
+ border-bottom-right-radius: 0.3em;
+ border-top-right-radius: 0.3em;
+}
+.oo-ui-popupWidget {
+ position: absolute;
+ /* @noflip */
+ left: 0;
+}
+.oo-ui-popupWidget-popup {
+ position: relative;
+ overflow: hidden;
+ z-index: 1;
+}
+.oo-ui-popupWidget-anchor {
+ display: none;
+ z-index: 1;
+}
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor {
+ display: block;
+ position: absolute;
+ top: 0;
+ /* @noflip */
+ left: 0;
+ background-repeat: no-repeat;
+}
+.oo-ui-popupWidget-head {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.oo-ui-popupWidget-head > .oo-ui-buttonWidget {
+ float: right;
+}
+.oo-ui-popupWidget-head > .oo-ui-labelElement-label {
+ float: left;
+ cursor: default;
+}
+.oo-ui-popupWidget-body {
+ clear: both;
+ overflow: hidden;
+}
+.oo-ui-popupWidget-popup {
+ background-color: #ffffff;
+ border: 1px solid #cccccc;
+ border-radius: 0.25em;
+ box-shadow: 0 0.15em 0.5em 0 rgba(0, 0, 0, 0.2);
+}
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-popup {
+ margin-top: 6px;
+}
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before,
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
+ content: "";
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-style: solid;
+ border-color: transparent;
+ border-top: 0;
+}
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before {
+ bottom: -7px;
+ left: -6px;
+ border-bottom-color: #aaaaaa;
+ border-width: 7px;
+}
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
+ bottom: -7px;
+ left: -5px;
+ border-bottom-color: #ffffff;
+ border-width: 6px;
+}
+.oo-ui-popupWidget-transitioning .oo-ui-popupWidget-popup {
+ -webkit-transition: width 100ms ease, height 100ms ease, left 100ms ease;
+ -moz-transition: width 100ms ease, height 100ms ease, left 100ms ease;
+ transition: width 100ms ease, height 100ms ease, left 100ms ease;
+}
+.oo-ui-popupWidget-head {
+ height: 2.5em;
+}
+.oo-ui-popupWidget-head > .oo-ui-buttonWidget {
+ margin: 0.25em;
+}
+.oo-ui-popupWidget-head > .oo-ui-labelElement-label {
+ margin: 0.75em 1em;
+}
+.oo-ui-popupWidget-body-padded {
+ padding: 0 1em;
+}
+.oo-ui-popupButtonWidget {
+ position: relative;
+}
+.oo-ui-popupButtonWidget .oo-ui-popupWidget {
+ position: absolute;
+ cursor: auto;
+}
+.oo-ui-popupButtonWidget.oo-ui-buttonElement-frameless > .oo-ui-popupWidget {
+ /* @noflip */
+ left: 1em;
+}
+.oo-ui-popupButtonWidget.oo-ui-buttonElement-framed > .oo-ui-popupWidget {
+ /* @noflip */
+ left: 1.25em;
+}
+.oo-ui-inputWidget {
+ margin-right: 0.5em;
+}
+.oo-ui-inputWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-buttonInputWidget {
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-buttonInputWidget > button,
+.oo-ui-buttonInputWidget > input {
+ border: 0;
+ padding: 0;
+ background-color: transparent;
+}
+.oo-ui-dropdownInputWidget {
+ position: relative;
+ vertical-align: middle;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 50em;
+}
+.oo-ui-dropdownInputWidget select {
+ display: inline-block;
+ width: 100%;
+ resize: none;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-dropdownInputWidget select {
+ background-color: #ffffff;
+ height: 2.5em;
+ padding: 0.5em;
+ font-size: inherit;
+ font-family: inherit;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ border-radius: 0.25em;
+}
+.oo-ui-dropdownInputWidget.oo-ui-widget-enabled select:hover,
+.oo-ui-dropdownInputWidget.oo-ui-widget-enabled select:focus {
+ border-color: rgba(0, 0, 0, 0.2);
+ outline: none;
+}
+.oo-ui-dropdownInputWidget.oo-ui-widget-disabled select {
+ color: #cccccc;
+ border-color: #dddddd;
+ background-color: #f3f3f3;
+}
+.oo-ui-radioSelectInputWidget .oo-ui-fieldLayout {
+ margin-bottom: 0;
+}
+.oo-ui-textInputWidget {
+ position: relative;
+ vertical-align: middle;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 50em;
+}
+.oo-ui-textInputWidget input,
+.oo-ui-textInputWidget textarea {
+ display: inline-block;
+ width: 100%;
+ resize: none;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-textInputWidget textarea {
+ overflow: auto;
+}
+.oo-ui-textInputWidget input[type="search"] {
+ -webkit-appearance: none;
+}
+.oo-ui-textInputWidget input[type="search"]::-ms-clear {
+ display: none;
+}
+.oo-ui-textInputWidget input[type="search"]::-ms-reveal {
+ display: none;
+}
+.oo-ui-textInputWidget input[type="search"]::-webkit-search-decoration,
+.oo-ui-textInputWidget input[type="search"]::-webkit-search-cancel-button,
+.oo-ui-textInputWidget input[type="search"]::-webkit-search-results-button,
+.oo-ui-textInputWidget input[type="search"]::-webkit-search-results-decoration {
+ display: none;
+}
+.oo-ui-textInputWidget > .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator,
+.oo-ui-textInputWidget > .oo-ui-labelElement-label {
+ display: none;
+}
+.oo-ui-textInputWidget.oo-ui-iconElement > .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
+ display: block;
+ position: absolute;
+ top: 0;
+ height: 100%;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
+ cursor: text;
+}
+.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-textInputWidget-type-search > .oo-ui-indicatorElement-indicator {
+ cursor: pointer;
+}
+.oo-ui-textInputWidget.oo-ui-labelElement > .oo-ui-labelElement-label {
+ display: block;
+}
+.oo-ui-textInputWidget > .oo-ui-iconElement-icon {
+ left: 0;
+}
+.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator {
+ right: 0;
+}
+.oo-ui-textInputWidget > .oo-ui-labelElement-label {
+ position: absolute;
+ top: 0;
+}
+.oo-ui-textInputWidget-labelPosition-after > .oo-ui-labelElement-label {
+ right: 0;
+}
+.oo-ui-textInputWidget-labelPosition-before > .oo-ui-labelElement-label {
+ left: 0;
+}
+.oo-ui-textInputWidget input,
+.oo-ui-textInputWidget textarea {
+ padding: 0.5em;
+ line-height: 1.275em;
+ font-size: inherit;
+ font-family: inherit;
+ background-color: #ffffff;
+ color: black;
+ border: 1px solid #cccccc;
+ box-shadow: 0 0 0 white, inset 0 0.1em 0.2em #dddddd;
+ border-radius: 0.25em;
+ -webkit-transition: border-color 250ms ease, box-shadow 250ms ease;
+ -moz-transition: border-color 250ms ease, box-shadow 250ms ease;
+ transition: border-color 250ms ease, box-shadow 250ms ease;
+}
+.oo-ui-textInputWidget input.oo-ui-pendingElement-pending,
+.oo-ui-textInputWidget textarea.oo-ui-pendingElement-pending {
+ background-color: transparent;
+}
+.oo-ui-textInputWidget.oo-ui-widget-enabled input:focus,
+.oo-ui-textInputWidget.oo-ui-widget-enabled textarea:focus {
+ outline: none;
+ border-color: #a7dcff;
+ box-shadow: 0 0 0.3em #a7dcff, 0 0 0 white;
+}
+.oo-ui-textInputWidget.oo-ui-widget-enabled input[readonly],
+.oo-ui-textInputWidget.oo-ui-widget-enabled textarea[readonly] {
+ color: #777777;
+}
+.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid input,
+.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid textarea {
+ background-color: #ffdddd;
+}
+.oo-ui-textInputWidget.oo-ui-widget-disabled input,
+.oo-ui-textInputWidget.oo-ui-widget-disabled textarea {
+ color: #cccccc;
+ text-shadow: 0 1px 1px #ffffff;
+ border-color: #dddddd;
+ background-color: #f3f3f3;
+}
+.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+}
+.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-labelElement-label {
+ color: #dddddd;
+ text-shadow: 0 1px 1px #ffffff;
+}
+.oo-ui-textInputWidget.oo-ui-iconElement input,
+.oo-ui-textInputWidget.oo-ui-iconElement textarea {
+ padding-left: 2.475em;
+}
+.oo-ui-textInputWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
+ width: 1.875em;
+ max-height: 2.375em;
+ margin-left: 0.3em;
+}
+.oo-ui-textInputWidget.oo-ui-indicatorElement input,
+.oo-ui-textInputWidget.oo-ui-indicatorElement textarea {
+ padding-right: 2.4875em;
+}
+.oo-ui-textInputWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+ width: 0.9375em;
+ max-height: 2.375em;
+ margin-right: 0.775em;
+}
+.oo-ui-textInputWidget > .oo-ui-labelElement-label {
+ padding: 0.4em;
+ line-height: 1.5em;
+ color: #888888;
+}
+.oo-ui-textInputWidget-labelPosition-after.oo-ui-indicatorElement > .oo-ui-labelElement-label {
+ margin-right: 2.0875em;
+}
+.oo-ui-textInputWidget-labelPosition-before.oo-ui-iconElement > .oo-ui-labelElement-label {
+ margin-left: 2.075em;
+}
+.oo-ui-menuSelectWidget {
+ position: absolute;
+ background-color: #ffffff;
+ margin-top: -1px;
+ border: 1px solid #cccccc;
+ border-radius: 0 0 0.25em 0.25em;
+ box-shadow: 0 0.15em 1em 0 rgba(0, 0, 0, 0.2);
+}
+.oo-ui-menuSelectWidget input {
+ position: absolute;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+ opacity: 0;
+}
+.oo-ui-menuOptionWidget {
+ position: relative;
+}
+.oo-ui-menuOptionWidget .oo-ui-iconElement-icon {
+ display: none;
+}
+.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected {
+ background-color: transparent;
+}
+.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected .oo-ui-iconElement-icon {
+ display: block;
+}
+.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected {
+ background-color: transparent;
+}
+.oo-ui-menuOptionWidget.oo-ui-optionWidget-highlighted,
+.oo-ui-menuOptionWidget.oo-ui-optionWidget-highlighted.oo-ui-optionWidget-selected {
+ background-color: #e1f3ff;
+}
+.oo-ui-menuSectionOptionWidget {
+ cursor: default;
+ padding: 0.33em 0.75em;
+ color: #888888;
+}
+.oo-ui-dropdownWidget {
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ max-width: 50em;
+ background-color: #ffffff;
+ margin-right: 0.5em;
+}
+.oo-ui-dropdownWidget-handle {
+ width: 100%;
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-dropdownWidget-handle .oo-ui-indicatorElement-indicator,
+.oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
+ position: absolute;
+}
+.oo-ui-dropdownWidget > .oo-ui-menuSelectWidget {
+ z-index: 1;
+ width: 100%;
+}
+.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle {
+ cursor: pointer;
+}
+.oo-ui-dropdownWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-dropdownWidget-handle {
+ height: 2.5em;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ border-radius: 0.25em;
+}
+.oo-ui-dropdownWidget-handle:hover {
+ border-color: rgba(0, 0, 0, 0.2);
+}
+.oo-ui-dropdownWidget-handle .oo-ui-indicatorElement-indicator {
+ right: 0;
+}
+.oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
+ left: 0.25em;
+}
+.oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {
+ line-height: 2.5em;
+ margin: 0 0.5em;
+}
+.oo-ui-dropdownWidget-handle .oo-ui-indicatorElement-indicator {
+ top: 0;
+ width: 0.9375em;
+ height: 0.9375em;
+ margin: 0.775em;
+}
+.oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
+ top: 0;
+ width: 1.875em;
+ height: 1.875em;
+ margin: 0.3em;
+}
+.oo-ui-dropdownWidget.oo-ui-widget-disabled .oo-ui-dropdownWidget-handle {
+ color: #cccccc;
+ text-shadow: 0 1px 1px #ffffff;
+ border-color: #dddddd;
+ background-color: #f3f3f3;
+}
+.oo-ui-dropdownWidget.oo-ui-widget-disabled .oo-ui-dropdownWidget-handle:focus {
+ outline: 0;
+}
+.oo-ui-dropdownWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+}
+.oo-ui-dropdownWidget.oo-ui-iconElement .oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {
+ margin-left: 3em;
+}
+.oo-ui-dropdownWidget.oo-ui-indicatorElement .oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {
+ margin-right: 2em;
+}
+.oo-ui-comboBoxInputWidget {
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ max-width: 50em;
+ margin-right: 0.5em;
+}
+.oo-ui-comboBoxInputWidget > .oo-ui-menuSelectWidget {
+ z-index: 1;
+ width: 100%;
+}
+.oo-ui-comboBoxInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
+ cursor: pointer;
+}
+.oo-ui-comboBoxInputWidget-php input::-webkit-calendar-picker-indicator {
+ opacity: 0 !important;
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 2.5em;
+ width: 2.5em;
+ padding: 0;
+}
+.oo-ui-comboBoxInputWidget-php > .oo-ui-indicatorElement-indicator {
+ pointer-events: none;
+}
+.oo-ui-comboBoxInputWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-comboBoxInputWidget.oo-ui-widget-disabled .oo-ui-textInputWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator,
+.oo-ui-comboBoxInputWidget-empty .oo-ui-textInputWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+ cursor: default;
+ opacity: 0.2;
+}
+.oo-ui-comboBoxInputWidget > .oo-ui-selectWidget {
+ margin-top: -3px;
+}
--- /dev/null
+/*!
+ * OOjs UI v0.15.2
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: 2016-02-02T22:07:06Z
+ */
+@-webkit-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-moz-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+.oo-ui-element-hidden {
+ display: none !important;
+}
+.oo-ui-buttonElement > .oo-ui-buttonElement-button {
+ cursor: pointer;
+ display: inline-block;
+ vertical-align: middle;
+ font: inherit;
+ white-space: nowrap;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.oo-ui-buttonElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon,
+.oo-ui-buttonElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
+ display: none;
+}
+.oo-ui-buttonElement.oo-ui-widget-disabled > .oo-ui-buttonElement-button {
+ cursor: default;
+}
+.oo-ui-buttonElement.oo-ui-indicatorElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator,
+.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-buttonElement-frameless {
+ display: inline-block;
+ position: relative;
+}
+.oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button {
+ display: inline-block;
+ vertical-align: top;
+ text-align: center;
+}
+.oo-ui-buttonElement-framed.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled > .oo-ui-buttonElement-button,
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button,
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
+ cursor: default;
+}
+.oo-ui-buttonElement > .oo-ui-buttonElement-button {
+ font-weight: bold;
+ text-decoration: none;
+}
+.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+ margin-left: 0;
+}
+.oo-ui-buttonElement.oo-ui-indicatorElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
+ width: 0.9375em;
+ height: 0.9375em;
+}
+.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
+ margin-left: 0.46875em;
+}
+.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+ width: 1.875em;
+ height: 1.875em;
+}
+.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:focus {
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.2);
+ outline: none;
+}
+.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button .oo-ui-indicatorElement-indicator {
+ margin-right: 0;
+}
+.oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ margin-left: 0.25em;
+ margin-right: 0.25em;
+}
+.oo-ui-buttonElement-frameless > input.oo-ui-buttonElement-button {
+ padding-left: 0.25em;
+ padding-right: 0.25em;
+ color: #333333;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled > input.oo-ui-buttonElement-button,
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ color: #555555;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > input.oo-ui-buttonElement-button,
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ color: #444444;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:hover > .oo-ui-labelElement-label,
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:focus > .oo-ui-labelElement-label {
+ color: #2962cc;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ color: #347bff;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active > .oo-ui-labelElement-label,
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ color: #1f4999;
+ box-shadow: none;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:hover > .oo-ui-labelElement-label,
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:focus > .oo-ui-labelElement-label {
+ color: #008064;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ color: #00af89;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active > .oo-ui-labelElement-label,
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ color: #005946;
+ box-shadow: none;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:hover > .oo-ui-labelElement-label,
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:focus > .oo-ui-labelElement-label {
+ color: #8c130d;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ color: #d11d13;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active > .oo-ui-labelElement-label,
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ color: #73100a;
+ box-shadow: none;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonElement-button {
+ color: #cccccc;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonElement-button:focus {
+ box-shadow: none;
+}
+.oo-ui-buttonElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon,
+.oo-ui-buttonElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+}
+.oo-ui-buttonElement-framed.oo-ui-iconElement.oo-ui-labelElement > .oo-ui-buttonElement-button,
+.oo-ui-buttonElement-framed.oo-ui-iconElement.oo-ui-indicatorElement > .oo-ui-buttonElement-button {
+ padding-left: 2.4em;
+}
+.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button {
+ padding: 0.5em 1em;
+ min-height: 1.2em;
+ min-width: 1em;
+ border-radius: 2px;
+ position: relative;
+ -webkit-transition: background 100ms ease, color 100ms ease, border-color 100ms ease, box-shadow 100ms ease;
+ -moz-transition: background 100ms ease, color 100ms ease, border-color 100ms ease, box-shadow 100ms ease;
+ transition: background 100ms ease, color 100ms ease, border-color 100ms ease, box-shadow 100ms ease;
+}
+.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button:hover,
+.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button:focus {
+ outline: none;
+}
+.oo-ui-buttonElement-framed > input.oo-ui-buttonElement-button,
+.oo-ui-buttonElement-framed.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ line-height: 1.2em;
+ display: inline-block;
+}
+.oo-ui-buttonElement-framed.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+ position: absolute;
+ top: 0.2em;
+ left: 0.5625em;
+}
+.oo-ui-buttonElement-framed.oo-ui-iconElement.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ margin-left: 0.3em;
+}
+.oo-ui-buttonElement-framed.oo-ui-indicatorElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
+ display: inline-block;
+}
+.oo-ui-buttonElement-framed.oo-ui-indicatorElement.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator,
+.oo-ui-buttonElement-framed.oo-ui-indicatorElement.oo-ui-iconElement:not( .oo-ui-labelElement ) > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
+ margin-left: 0.46875em;
+ margin-right: -0.275em;
+}
+.oo-ui-buttonElement-framed.oo-ui-indicatorElement.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
+ position: relative;
+ left: 0.2em;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-disabled > .oo-ui-buttonElement-button {
+ background: #dddddd;
+ color: #ffffff;
+ border: 1px solid #dddddd;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button {
+ color: #555555;
+ background-color: #ffffff;
+ border: 1px solid #cccccc;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button:hover {
+ background-color: #ebebeb;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button:focus {
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
+ background-color: #d9d9d9;
+ border-color: #d9d9d9;
+ box-shadow: none;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
+ background-color: #999999;
+ color: #ffffff;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button {
+ color: #347bff;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:hover {
+ background-color: rgba(52, 123, 255, 0.1);
+ border-color: rgba(31, 73, 153, 0.5);
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:focus {
+ box-shadow: inset 0 0 0 1px #1f4999;
+ border-color: #1f4999;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
+ color: #1f4999;
+ border-color: #1f4999;
+ box-shadow: none;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
+ background-color: #999999;
+ color: #ffffff;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button {
+ color: #00af89;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:hover {
+ background-color: rgba(0, 171, 137, 0.1);
+ border-color: rgba(0, 89, 70, 0.5);
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:focus {
+ box-shadow: inset 0 0 0 1px #005946;
+ border-color: #005946;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
+ color: #005946;
+ border-color: #005946;
+ box-shadow: none;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
+ background-color: #999999;
+ color: #ffffff;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button {
+ color: #d11d13;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:hover {
+ background-color: rgba(209, 29, 19, 0.1);
+ border-color: rgba(115, 16, 10, 0.5);
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:focus {
+ box-shadow: inset 0 0 0 1px #73100a;
+ border-color: #73100a;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
+ color: #73100a;
+ border-color: #73100a;
+ box-shadow: none;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
+ background-color: #999999;
+ color: #ffffff;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button {
+ color: #ffffff;
+ background-color: #347bff;
+ border-color: #347bff;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:hover {
+ background: #2962cc;
+ border-color: #2962cc;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:focus {
+ box-shadow: inset 0 0 0 1px #ffffff;
+ border-color: #347bff;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
+ color: #ffffff;
+ background-color: #1f4999;
+ border-color: #1f4999;
+ box-shadow: none;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
+ background-color: #999999;
+ color: #ffffff;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button {
+ color: #ffffff;
+ background-color: #00af89;
+ border-color: #00af89;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:hover {
+ background: #008064;
+ border-color: #008064;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:focus {
+ box-shadow: inset 0 0 0 1px #ffffff;
+ border-color: #00af89;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
+ color: #ffffff;
+ background-color: #005946;
+ border-color: #005946;
+ box-shadow: none;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
+ background-color: #999999;
+ color: #ffffff;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button {
+ color: #ffffff;
+ background-color: #d11d13;
+ border-color: #d11d13;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:hover {
+ background: #8c130d;
+ border-color: #8c130d;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:focus {
+ box-shadow: inset 0 0 0 1px #ffffff;
+ border-color: #d11d13;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
+ color: #ffffff;
+ background-color: #73100a;
+ border-color: #73100a;
+ box-shadow: none;
+}
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
+ background-color: #999999;
+ color: #ffffff;
+}
+.oo-ui-clippableElement-clippable {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-iconElement .oo-ui-iconElement-icon,
+.oo-ui-iconElement.oo-ui-iconElement-icon {
+ background-size: contain;
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator,
+.oo-ui-indicatorElement.oo-ui-indicatorElement-indicator {
+ background-size: contain;
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+.oo-ui-pendingElement-pending {
+ background-image: /* @embed */ url(themes/mediawiki/images/textures/pending.gif);
+}
+.oo-ui-fieldLayout {
+ display: block;
+ margin-bottom: 1em;
+}
+.oo-ui-fieldLayout:before,
+.oo-ui-fieldLayout:after {
+ content: " ";
+ display: table;
+}
+.oo-ui-fieldLayout:after {
+ clear: both;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
+ display: block;
+ float: left;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ text-align: right;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body {
+ display: table;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
+ display: table-cell;
+ vertical-align: middle;
+}
+.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ display: inline-block;
+}
+.oo-ui-fieldLayout > .oo-ui-fieldLayout-help {
+ float: right;
+}
+.oo-ui-fieldLayout > .oo-ui-fieldLayout-help > .oo-ui-popupWidget > .oo-ui-popupWidget-popup {
+ z-index: 1;
+}
+.oo-ui-fieldLayout > .oo-ui-fieldLayout-help .oo-ui-fieldLayout-help-content {
+ padding: 0.5em 0.75em;
+ line-height: 1.5em;
+}
+.oo-ui-fieldLayout:last-child {
+ margin-bottom: 0;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ padding-top: 0.5em;
+ margin-right: 5%;
+ width: 35%;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field,
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
+ width: 60%;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline {
+ margin-bottom: 1.25em;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ padding: 0.25em 0.25em 0.25em 1em;
+}
+.oo-ui-fieldLayout.oo-ui-fieldLayout-align-top.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ padding-top: 0.25em;
+ padding-bottom: 0.5em;
+}
+.oo-ui-fieldLayout > .oo-ui-popupButtonWidget {
+ margin-right: 0;
+}
+.oo-ui-fieldLayout > .oo-ui-popupButtonWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-fieldLayout-disabled > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
+ color: #cccccc;
+}
+.oo-ui-fieldLayout-messages {
+ list-style: none none;
+ margin: 0.25em 0 0 0.25em;
+ padding: 0;
+}
+.oo-ui-fieldLayout-messages > li {
+ margin: 0;
+ padding: 0;
+ display: table;
+}
+.oo-ui-fieldLayout-messages .oo-ui-iconWidget {
+ display: table-cell;
+ border-right: 0.5em solid transparent;
+}
+.oo-ui-fieldLayout-messages .oo-ui-labelWidget {
+ display: table-cell;
+ padding: 0;
+ line-height: 1.875em;
+ vertical-align: middle;
+}
+.oo-ui-actionFieldLayout-input,
+.oo-ui-actionFieldLayout-button {
+ display: table-cell;
+ vertical-align: middle;
+}
+.oo-ui-actionFieldLayout-input {
+ padding-right: 1em;
+}
+.oo-ui-actionFieldLayout-button {
+ width: 1%;
+ white-space: nowrap;
+}
+.oo-ui-fieldsetLayout {
+ position: relative;
+ margin: 0;
+ padding: 0;
+ border: 0;
+}
+.oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-iconElement-icon {
+ display: block;
+ position: absolute;
+}
+.oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label {
+ display: inline-block;
+}
+.oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help {
+ float: right;
+}
+.oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help > .oo-ui-popupWidget > .oo-ui-popupWidget-popup {
+ z-index: 1;
+}
+.oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help .oo-ui-fieldsetLayout-help-content {
+ padding: 0.5em 0.75em;
+ line-height: 1.5em;
+}
+.oo-ui-fieldsetLayout + .oo-ui-fieldsetLayout,
+.oo-ui-fieldsetLayout + .oo-ui-formLayout {
+ margin-top: 2em;
+}
+.oo-ui-fieldsetLayout > .oo-ui-labelElement-label {
+ font-size: 1.1em;
+ margin-bottom: 0.5em;
+ padding: 0.25em 0;
+ font-weight: bold;
+}
+.oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-labelElement-label {
+ padding-left: 2em;
+ line-height: 1.8em;
+}
+.oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-iconElement-icon {
+ left: 0;
+ top: 0.25em;
+ width: 1.875em;
+ height: 1.875em;
+}
+.oo-ui-fieldsetLayout > .oo-ui-popupButtonWidget {
+ margin-right: 0;
+}
+.oo-ui-fieldsetLayout > .oo-ui-popupButtonWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-formLayout + .oo-ui-fieldsetLayout,
+.oo-ui-formLayout + .oo-ui-formLayout {
+ margin-top: 2em;
+}
+.oo-ui-panelLayout {
+ position: relative;
+}
+.oo-ui-panelLayout-scrollable {
+ overflow-y: auto;
+}
+.oo-ui-panelLayout-expanded {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+.oo-ui-panelLayout-padded {
+ padding: 1.25em;
+}
+.oo-ui-panelLayout-framed {
+ border: 1px solid #aaaaaa;
+ border-radius: 2px;
+ box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
+}
+.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed {
+ margin: 1em 0;
+}
+.oo-ui-horizontalLayout > .oo-ui-widget {
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-horizontalLayout > .oo-ui-layout {
+ display: inline-block;
+}
+.oo-ui-horizontalLayout > .oo-ui-layout,
+.oo-ui-horizontalLayout > .oo-ui-widget {
+ margin-right: 0.5em;
+}
+.oo-ui-horizontalLayout > .oo-ui-layout:last-child,
+.oo-ui-horizontalLayout > .oo-ui-widget:last-child {
+ margin-right: 0;
+}
+.oo-ui-horizontalLayout > .oo-ui-layout {
+ margin-bottom: 0;
+}
+.oo-ui-optionWidget {
+ position: relative;
+ display: block;
+ padding: 0.25em 0.5em;
+ border: 0;
+}
+.oo-ui-optionWidget.oo-ui-widget-enabled {
+ cursor: pointer;
+}
+.oo-ui-optionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
+ display: block;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+.oo-ui-optionWidget-highlighted {
+ background-color: #eeeeee;
+}
+.oo-ui-optionWidget .oo-ui-labelElement-label {
+ line-height: 1.5em;
+}
+.oo-ui-selectWidget-depressed .oo-ui-optionWidget-selected,
+.oo-ui-selectWidget-pressed .oo-ui-optionWidget-pressed,
+.oo-ui-selectWidget-pressed .oo-ui-optionWidget-pressed.oo-ui-optionWidget-highlighted,
+.oo-ui-selectWidget-pressed .oo-ui-optionWidget-pressed.oo-ui-optionWidget-highlighted.oo-ui-optionWidget-selected {
+ background-color: #d0d0d0;
+}
+.oo-ui-optionWidget.oo-ui-widget-disabled {
+ color: #cccccc;
+}
+.oo-ui-decoratedOptionWidget {
+ padding: 0.5em 2em 0.5em 3em;
+}
+.oo-ui-decoratedOptionWidget .oo-ui-iconElement-icon,
+.oo-ui-decoratedOptionWidget .oo-ui-indicatorElement-indicator {
+ position: absolute;
+}
+.oo-ui-decoratedOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
+.oo-ui-decoratedOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+ top: 0;
+ height: 100%;
+}
+.oo-ui-decoratedOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
+ width: 1.875em;
+ left: 0.5em;
+}
+.oo-ui-decoratedOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+ width: 0.9375em;
+ right: 0.5em;
+}
+.oo-ui-decoratedOptionWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
+.oo-ui-decoratedOptionWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+}
+.oo-ui-radioOptionWidget {
+ cursor: default;
+ padding: 0.25em 0;
+ background-color: transparent;
+}
+.oo-ui-radioOptionWidget .oo-ui-radioInputWidget,
+.oo-ui-radioOptionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-radioOptionWidget.oo-ui-optionWidget-selected,
+.oo-ui-radioOptionWidget.oo-ui-optionWidget-pressed,
+.oo-ui-radioOptionWidget.oo-ui-optionWidget-highlighted {
+ background-color: transparent;
+}
+.oo-ui-radioOptionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
+ padding: 0.25em 0.25em 0.25em 1em;
+}
+.oo-ui-radioOptionWidget .oo-ui-radioInputWidget {
+ margin-right: 0;
+}
+.oo-ui-labelWidget {
+ display: inline-block;
+}
+.oo-ui-iconWidget {
+ display: inline-block;
+ vertical-align: middle;
+ line-height: 2.5em;
+ width: 1.875em;
+ height: 1.875em;
+}
+.oo-ui-iconWidget.oo-ui-widget-disabled {
+ opacity: 0.2;
+}
+.oo-ui-indicatorWidget {
+ display: inline-block;
+ vertical-align: middle;
+ line-height: 2.5em;
+ width: 0.9375em;
+ height: 0.9375em;
+ margin: 0.46875em;
+}
+.oo-ui-indicatorWidget.oo-ui-widget-disabled {
+ opacity: 0.2;
+}
+.oo-ui-buttonWidget {
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: 0.5em;
+}
+.oo-ui-buttonWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-buttonGroupWidget {
+ display: inline-block;
+ white-space: nowrap;
+ border-radius: 2px;
+ margin-right: 0.5em;
+}
+.oo-ui-buttonGroupWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-buttonGroupWidget .oo-ui-buttonElement {
+ margin-right: 0;
+}
+.oo-ui-buttonGroupWidget .oo-ui-buttonElement:last-child {
+ margin-right: 0;
+}
+.oo-ui-buttonGroupWidget .oo-ui-buttonElement-framed .oo-ui-buttonElement-button {
+ border-radius: 0;
+ margin-left: -1px;
+}
+.oo-ui-buttonGroupWidget .oo-ui-buttonElement-framed:first-child .oo-ui-buttonElement-button {
+ border-bottom-left-radius: 2px;
+ border-top-left-radius: 2px;
+ margin-left: 0;
+}
+.oo-ui-buttonGroupWidget .oo-ui-buttonElement-framed:last-child .oo-ui-buttonElement-button {
+ border-bottom-right-radius: 2px;
+ border-top-right-radius: 2px;
+}
+.oo-ui-popupWidget {
+ position: absolute;
+ /* @noflip */
+ left: 0;
+}
+.oo-ui-popupWidget-popup {
+ position: relative;
+ overflow: hidden;
+ z-index: 1;
+}
+.oo-ui-popupWidget-anchor {
+ display: none;
+ z-index: 1;
+}
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor {
+ display: block;
+ position: absolute;
+ top: 0;
+ /* @noflip */
+ left: 0;
+ background-repeat: no-repeat;
+}
+.oo-ui-popupWidget-head {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.oo-ui-popupWidget-head > .oo-ui-buttonWidget {
+ float: right;
+}
+.oo-ui-popupWidget-head > .oo-ui-labelElement-label {
+ float: left;
+ cursor: default;
+}
+.oo-ui-popupWidget-body {
+ clear: both;
+ overflow: hidden;
+}
+.oo-ui-popupWidget-popup {
+ background-color: #ffffff;
+ border: 1px solid #aaaaaa;
+ border-radius: 2px;
+ box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
+}
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-popup {
+ margin-top: 9px;
+}
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before,
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
+ content: "";
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-style: solid;
+ border-color: transparent;
+ border-top: 0;
+}
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before {
+ bottom: -10px;
+ left: -9px;
+ border-bottom-color: #888888;
+ border-width: 10px;
+}
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
+ bottom: -10px;
+ left: -8px;
+ border-bottom-color: #ffffff;
+ border-width: 9px;
+}
+.oo-ui-popupWidget-transitioning .oo-ui-popupWidget-popup {
+ -webkit-transition: width 100ms ease, height 100ms ease, left 100ms ease;
+ -moz-transition: width 100ms ease, height 100ms ease, left 100ms ease;
+ transition: width 100ms ease, height 100ms ease, left 100ms ease;
+}
+.oo-ui-popupWidget-head {
+ height: 2.5em;
+}
+.oo-ui-popupWidget-head > .oo-ui-buttonWidget {
+ margin: 0.25em;
+}
+.oo-ui-popupWidget-head > .oo-ui-labelElement-label {
+ margin: 0.75em 1em;
+}
+.oo-ui-popupWidget-body-padded {
+ padding: 0 1em;
+}
+.oo-ui-popupButtonWidget {
+ position: relative;
+}
+.oo-ui-popupButtonWidget .oo-ui-popupWidget {
+ position: absolute;
+ cursor: auto;
+}
+.oo-ui-popupButtonWidget.oo-ui-buttonElement-frameless > .oo-ui-popupWidget {
+ /* @noflip */
+ left: 1em;
+}
+.oo-ui-popupButtonWidget.oo-ui-buttonElement-framed > .oo-ui-popupWidget {
+ /* @noflip */
+ left: 1.75em;
+}
+.oo-ui-inputWidget {
+ margin-right: 0.5em;
+}
+.oo-ui-inputWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-buttonInputWidget {
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-buttonInputWidget > button,
+.oo-ui-buttonInputWidget > input {
+ border: 0;
+ padding: 0;
+ background-color: transparent;
+}
+.oo-ui-checkboxInputWidget {
+ position: relative;
+ line-height: 1.6em;
+ white-space: nowrap;
+}
+.oo-ui-checkboxInputWidget * {
+ font: inherit;
+ vertical-align: middle;
+}
+.oo-ui-checkboxInputWidget input[type="checkbox"] {
+ opacity: 0;
+ z-index: 1;
+ position: relative;
+ cursor: pointer;
+ margin: 0;
+ width: 1.6em;
+ height: 1.6em;
+ max-width: none;
+}
+.oo-ui-checkboxInputWidget input[type="checkbox"] + span {
+ -webkit-transition: background-size 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ -moz-transition: background-size 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ transition: background-size 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ position: absolute;
+ left: 0;
+ border-radius: 2px;
+ width: 1.6em;
+ height: 1.6em;
+ background-color: white;
+ border: 1px solid #777777;
+ background-image: url("themes/mediawiki/images/icons/check-constructive.png");
+ background-image: -webkit-linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/check-constructive.svg");
+ background-image: linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/check-constructive.svg");
+ background-image: -o-linear-gradient(transparent, transparent), url("themes/mediawiki/images/icons/check-constructive.png");
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-origin: border-box;
+ background-size: 0 0;
+}
+.oo-ui-checkboxInputWidget input[type="checkbox"]:checked + span {
+ background-size: 100% 100%;
+}
+.oo-ui-checkboxInputWidget input[type="checkbox"]:active + span {
+ background-color: #cccccc;
+ border-color: #cccccc;
+}
+.oo-ui-checkboxInputWidget input[type="checkbox"]:focus + span {
+ border-width: 2px;
+}
+.oo-ui-checkboxInputWidget input[type="checkbox"]:focus:hover + span,
+.oo-ui-checkboxInputWidget input[type="checkbox"]:hover + span {
+ border-bottom-width: 3px;
+}
+.oo-ui-checkboxInputWidget input[type="checkbox"]:disabled {
+ cursor: default;
+}
+.oo-ui-checkboxInputWidget input[type="checkbox"]:disabled + span {
+ background-color: #dddddd;
+ border-color: #dddddd;
+}
+.oo-ui-checkboxInputWidget input[type="checkbox"]:disabled:checked + span {
+ background-image: url("themes/mediawiki/images/icons/check-invert.png");
+ background-image: -webkit-linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/check-invert.svg");
+ background-image: linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/check-invert.svg");
+ background-image: -o-linear-gradient(transparent, transparent), url("themes/mediawiki/images/icons/check-invert.png");
+}
+.oo-ui-dropdownInputWidget {
+ position: relative;
+ vertical-align: middle;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 50em;
+}
+.oo-ui-dropdownInputWidget select {
+ display: inline-block;
+ width: 100%;
+ resize: none;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-dropdownInputWidget select {
+ background-color: #ffffff;
+ height: 2.275em;
+ font-size: inherit;
+ font-family: inherit;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ border: 1px solid #cccccc;
+ border-radius: 2px;
+ padding-left: 1em;
+ vertical-align: middle;
+}
+.oo-ui-dropdownInputWidget.oo-ui-widget-enabled select:hover,
+.oo-ui-dropdownInputWidget.oo-ui-widget-enabled select:focus {
+ border-color: #aaaaaa;
+ outline: none;
+}
+.oo-ui-dropdownInputWidget.oo-ui-widget-disabled select {
+ color: #cccccc;
+ border-color: #dddddd;
+ background-color: #f3f3f3;
+}
+.oo-ui-radioInputWidget {
+ position: relative;
+ line-height: 1.6em;
+ white-space: nowrap;
+}
+.oo-ui-radioInputWidget * {
+ font: inherit;
+ vertical-align: middle;
+}
+.oo-ui-radioInputWidget input[type="radio"] {
+ opacity: 0;
+ z-index: 1;
+ position: relative;
+ cursor: pointer;
+ margin: 0;
+ width: 1.6em;
+ height: 1.6em;
+ max-width: none;
+}
+.oo-ui-radioInputWidget input[type="radio"] + span {
+ -webkit-transition: background-size 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ -moz-transition: background-size 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ transition: background-size 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ position: absolute;
+ left: 0;
+ border-radius: 100%;
+ width: 1.6em;
+ height: 1.6em;
+ background: white;
+ border: 1px solid #777777;
+ background-image: url("themes/mediawiki/images/icons/circle-constructive.png");
+ background-image: -webkit-linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/circle-constructive.svg");
+ background-image: linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/circle-constructive.svg");
+ background-image: -o-linear-gradient(transparent, transparent), url("themes/mediawiki/images/icons/circle-constructive.png");
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-origin: border-box;
+ background-size: 0 0;
+}
+.oo-ui-radioInputWidget input[type="radio"]:checked + span {
+ background-size: 100% 100%;
+}
+.oo-ui-radioInputWidget input[type="radio"]:active + span {
+ background-color: #cccccc;
+ border-color: #cccccc;
+}
+.oo-ui-radioInputWidget input[type="radio"]:focus + span {
+ border-width: 2px;
+}
+.oo-ui-radioInputWidget input[type="radio"]:focus:hover + span,
+.oo-ui-radioInputWidget input[type="radio"]:hover + span {
+ border-bottom-width: 3px;
+}
+.oo-ui-radioInputWidget input[type="radio"]:disabled {
+ cursor: default;
+}
+.oo-ui-radioInputWidget input[type="radio"]:disabled + span {
+ background-color: #dddddd;
+ border-color: #dddddd;
+}
+.oo-ui-radioInputWidget input[type="radio"]:disabled:checked + span {
+ background-image: url("themes/mediawiki/images/icons/circle-invert.png");
+ background-image: -webkit-linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/circle-invert.svg");
+ background-image: linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/circle-invert.svg");
+ background-image: -o-linear-gradient(transparent, transparent), url("themes/mediawiki/images/icons/circle-invert.png");
+}
+.oo-ui-radioSelectInputWidget .oo-ui-fieldLayout {
+ margin-bottom: 0;
+}
+.oo-ui-textInputWidget {
+ position: relative;
+ vertical-align: middle;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 50em;
+}
+.oo-ui-textInputWidget input,
+.oo-ui-textInputWidget textarea {
+ display: inline-block;
+ width: 100%;
+ resize: none;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-textInputWidget textarea {
+ overflow: auto;
+}
+.oo-ui-textInputWidget input[type="search"] {
+ -webkit-appearance: none;
+}
+.oo-ui-textInputWidget input[type="search"]::-ms-clear {
+ display: none;
+}
+.oo-ui-textInputWidget input[type="search"]::-ms-reveal {
+ display: none;
+}
+.oo-ui-textInputWidget input[type="search"]::-webkit-search-decoration,
+.oo-ui-textInputWidget input[type="search"]::-webkit-search-cancel-button,
+.oo-ui-textInputWidget input[type="search"]::-webkit-search-results-button,
+.oo-ui-textInputWidget input[type="search"]::-webkit-search-results-decoration {
+ display: none;
+}
+.oo-ui-textInputWidget > .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator,
+.oo-ui-textInputWidget > .oo-ui-labelElement-label {
+ display: none;
+}
+.oo-ui-textInputWidget.oo-ui-iconElement > .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
+ display: block;
+ position: absolute;
+ top: 0;
+ height: 100%;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
+ cursor: text;
+}
+.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-textInputWidget-type-search > .oo-ui-indicatorElement-indicator {
+ cursor: pointer;
+}
+.oo-ui-textInputWidget.oo-ui-labelElement > .oo-ui-labelElement-label {
+ display: block;
+}
+.oo-ui-textInputWidget > .oo-ui-iconElement-icon {
+ left: 0;
+}
+.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator {
+ right: 0;
+}
+.oo-ui-textInputWidget > .oo-ui-labelElement-label {
+ position: absolute;
+ top: 0;
+}
+.oo-ui-textInputWidget-labelPosition-after > .oo-ui-labelElement-label {
+ right: 0;
+}
+.oo-ui-textInputWidget-labelPosition-before > .oo-ui-labelElement-label {
+ left: 0;
+}
+.oo-ui-textInputWidget input,
+.oo-ui-textInputWidget textarea {
+ padding: 0.5em;
+ line-height: 1.275em;
+ margin: 0;
+ font-size: inherit;
+ font-family: inherit;
+ background-color: #ffffff;
+ color: #000000;
+ border: 1px solid #cccccc;
+ border-radius: 2px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-textInputWidget input.oo-ui-pendingElement-pending,
+.oo-ui-textInputWidget textarea.oo-ui-pendingElement-pending {
+ background-color: transparent;
+}
+.oo-ui-textInputWidget.oo-ui-widget-enabled input,
+.oo-ui-textInputWidget.oo-ui-widget-enabled textarea {
+ box-shadow: inset 0 0 0 0.1em #ffffff;
+ -webkit-transition: border 200ms cubic-bezier(0.39, 0.575, 0.565, 1), box-shadow 200ms cubic-bezier(0.39, 0.575, 0.565, 1);
+ -moz-transition: border 200ms cubic-bezier(0.39, 0.575, 0.565, 1), box-shadow 200ms cubic-bezier(0.39, 0.575, 0.565, 1);
+ transition: border 200ms cubic-bezier(0.39, 0.575, 0.565, 1), box-shadow 200ms cubic-bezier(0.39, 0.575, 0.565, 1);
+}
+.oo-ui-textInputWidget.oo-ui-widget-enabled input:focus,
+.oo-ui-textInputWidget.oo-ui-widget-enabled textarea:focus {
+ outline: none;
+ border-color: #347bff;
+ box-shadow: inset 0 0 0 0.1em #347bff;
+}
+.oo-ui-textInputWidget.oo-ui-widget-enabled input[readonly],
+.oo-ui-textInputWidget.oo-ui-widget-enabled textarea[readonly] {
+ color: #777777;
+ text-shadow: 0 1px 1px #ffffff;
+}
+.oo-ui-textInputWidget.oo-ui-widget-enabled input[readonly]:focus,
+.oo-ui-textInputWidget.oo-ui-widget-enabled textarea[readonly]:focus {
+ border-color: #cccccc;
+ box-shadow: inset 0 0 0 0.1em #cccccc;
+}
+.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid input,
+.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid textarea {
+ border-color: #ff0000;
+}
+.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid input:focus,
+.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid textarea:focus {
+ border-color: #ff0000;
+ box-shadow: inset 0 0 0 0.1em #ff0000;
+}
+.oo-ui-textInputWidget.oo-ui-widget-disabled input,
+.oo-ui-textInputWidget.oo-ui-widget-disabled textarea {
+ color: #cccccc;
+ text-shadow: 0 1px 1px #ffffff;
+ border-color: #dddddd;
+ background-color: #f3f3f3;
+}
+.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+}
+.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-labelElement-label {
+ color: #dddddd;
+ text-shadow: 0 1px 1px #ffffff;
+}
+.oo-ui-textInputWidget.oo-ui-iconElement input,
+.oo-ui-textInputWidget.oo-ui-iconElement textarea {
+ padding-left: 2.875em;
+}
+.oo-ui-textInputWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
+ left: 0;
+ width: 1.875em;
+ max-height: 2.375em;
+ margin-left: 0.5em;
+ height: 100%;
+ background-position: right center;
+}
+.oo-ui-textInputWidget.oo-ui-indicatorElement input,
+.oo-ui-textInputWidget.oo-ui-indicatorElement textarea {
+ padding-right: 2.4875em;
+}
+.oo-ui-textInputWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+ width: 0.9375em;
+ max-height: 2.375em;
+ margin: 0 0.775em;
+ height: 100%;
+}
+.oo-ui-textInputWidget > .oo-ui-labelElement-label {
+ padding: 0.4em;
+ line-height: 1.5em;
+ color: #888888;
+}
+.oo-ui-textInputWidget-labelPosition-after.oo-ui-indicatorElement > .oo-ui-labelElement-label {
+ margin-right: 2.0875em;
+}
+.oo-ui-textInputWidget-labelPosition-before.oo-ui-iconElement > .oo-ui-labelElement-label {
+ margin-left: 2.475em;
+}
+.oo-ui-menuSelectWidget {
+ position: absolute;
+ background-color: #ffffff;
+ margin-top: -1px;
+ border: 1px solid #aaaaaa;
+ border-radius: 0 0 2px 2px;
+ box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
+}
+.oo-ui-menuSelectWidget input {
+ position: absolute;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+ opacity: 0;
+}
+.oo-ui-menuOptionWidget {
+ position: relative;
+ padding: 0.5em 1em;
+}
+.oo-ui-menuOptionWidget .oo-ui-iconElement-icon {
+ display: none;
+}
+.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected {
+ background-color: transparent;
+}
+.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected .oo-ui-iconElement-icon {
+ display: block;
+}
+.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected {
+ background-color: #d8e6fe;
+ color: rgba(0, 0, 0, 0.8);
+}
+.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected .oo-ui-iconElement-icon {
+ display: none;
+}
+.oo-ui-menuOptionWidget.oo-ui-optionWidget-highlighted {
+ background-color: #eeeeee;
+ color: #000000;
+}
+.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected.oo-ui-menuOptionWidget.oo-ui-optionWidget-highlighted {
+ background-color: #d8e6fe;
+}
+.oo-ui-menuSectionOptionWidget {
+ cursor: default;
+ padding: 0.33em 0.75em;
+ color: #888888;
+}
+.oo-ui-dropdownWidget {
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ max-width: 50em;
+ background-color: #ffffff;
+ margin-right: 0.5em;
+}
+.oo-ui-dropdownWidget-handle {
+ width: 100%;
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-dropdownWidget-handle .oo-ui-indicatorElement-indicator,
+.oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
+ position: absolute;
+}
+.oo-ui-dropdownWidget > .oo-ui-menuSelectWidget {
+ z-index: 1;
+ width: 100%;
+}
+.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle {
+ cursor: pointer;
+}
+.oo-ui-dropdownWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-dropdownWidget-handle {
+ padding: 0.5em 0;
+ height: 2.275em;
+ line-height: 1.275;
+ border: 1px solid #cccccc;
+ border-radius: 2px;
+}
+.oo-ui-dropdownWidget-handle .oo-ui-indicatorElement-indicator {
+ right: 0;
+}
+.oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
+ left: 0.25em;
+}
+.oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {
+ line-height: 1.275em;
+ margin: 0 1em;
+}
+.oo-ui-dropdownWidget-handle .oo-ui-indicatorElement-indicator {
+ top: 0;
+ width: 0.9375em;
+ height: 0.9375em;
+ margin: 0.775em;
+}
+.oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
+ top: 0;
+ width: 1.875em;
+ height: 1.875em;
+ margin: 0.3em;
+}
+.oo-ui-dropdownWidget:hover .oo-ui-dropdownWidget-handle {
+ border-color: #aaaaaa;
+}
+.oo-ui-dropdownWidget.oo-ui-widget-disabled .oo-ui-dropdownWidget-handle {
+ color: #cccccc;
+ text-shadow: 0 1px 1px #ffffff;
+ border-color: #dddddd;
+ background-color: #f3f3f3;
+}
+.oo-ui-dropdownWidget.oo-ui-widget-disabled .oo-ui-dropdownWidget-handle:focus {
+ outline: 0;
+}
+.oo-ui-dropdownWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+}
+.oo-ui-dropdownWidget.oo-ui-iconElement .oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {
+ margin-left: 3em;
+}
+.oo-ui-dropdownWidget.oo-ui-indicatorElement .oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {
+ margin-right: 2em;
+}
+.oo-ui-dropdownWidget .oo-ui-selectWidget {
+ border-top-color: #ffffff;
+}
+.oo-ui-comboBoxInputWidget {
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ max-width: 50em;
+ margin-right: 0.5em;
+}
+.oo-ui-comboBoxInputWidget > .oo-ui-menuSelectWidget {
+ z-index: 1;
+ width: 100%;
+}
+.oo-ui-comboBoxInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
+ cursor: pointer;
+}
+.oo-ui-comboBoxInputWidget-php input::-webkit-calendar-picker-indicator {
+ opacity: 0 !important;
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 2.5em;
+ width: 2.5em;
+ padding: 0;
+}
+.oo-ui-comboBoxInputWidget-php > .oo-ui-indicatorElement-indicator {
+ pointer-events: none;
+}
+.oo-ui-comboBoxInputWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-comboBoxInputWidget input,
+.oo-ui-comboBoxInputWidget textarea {
+ height: 2.35em;
+}
--- /dev/null
+/*!
+ * OOjs UI v0.15.2
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: 2016-02-02T22:07:00Z
+ */
+( function ( OO ) {
+
+'use strict';
+
+/**
+ * Namespace for all classes, static methods and static properties.
+ *
+ * @class
+ * @singleton
+ */
+OO.ui = {};
+
+OO.ui.bind = $.proxy;
+
+/**
+ * @property {Object}
+ */
+OO.ui.Keys = {
+ UNDEFINED: 0,
+ BACKSPACE: 8,
+ DELETE: 46,
+ LEFT: 37,
+ RIGHT: 39,
+ UP: 38,
+ DOWN: 40,
+ ENTER: 13,
+ END: 35,
+ HOME: 36,
+ TAB: 9,
+ PAGEUP: 33,
+ PAGEDOWN: 34,
+ ESCAPE: 27,
+ SHIFT: 16,
+ SPACE: 32
+};
+
+/**
+ * Constants for MouseEvent.which
+ *
+ * @property {Object}
+ */
+OO.ui.MouseButtons = {
+ LEFT: 1,
+ MIDDLE: 2,
+ RIGHT: 3
+};
+
+/**
+ * @property {Number}
+ */
+OO.ui.elementId = 0;
+
+/**
+ * Generate a unique ID for element
+ *
+ * @return {String} [id]
+ */
+OO.ui.generateElementId = function () {
+ OO.ui.elementId += 1;
+ return 'oojsui-' + OO.ui.elementId;
+};
+
+/**
+ * Check if an element is focusable.
+ * Inspired from :focusable in jQueryUI v1.11.4 - 2015-04-14
+ *
+ * @param {jQuery} element Element to test
+ * @return {boolean}
+ */
+OO.ui.isFocusableElement = function ( $element ) {
+ var nodeName,
+ element = $element[ 0 ];
+
+ // Anything disabled is not focusable
+ if ( element.disabled ) {
+ return false;
+ }
+
+ // Check if the element is visible
+ if ( !(
+ // This is quicker than calling $element.is( ':visible' )
+ $.expr.filters.visible( element ) &&
+ // Check that all parents are visible
+ !$element.parents().addBack().filter( function () {
+ return $.css( this, 'visibility' ) === 'hidden';
+ } ).length
+ ) ) {
+ return false;
+ }
+
+ // Check if the element is ContentEditable, which is the string 'true'
+ if ( element.contentEditable === 'true' ) {
+ return true;
+ }
+
+ // Anything with a non-negative numeric tabIndex is focusable.
+ // Use .prop to avoid browser bugs
+ if ( $element.prop( 'tabIndex' ) >= 0 ) {
+ return true;
+ }
+
+ // Some element types are naturally focusable
+ // (indexOf is much faster than regex in Chrome and about the
+ // same in FF: https://jsperf.com/regex-vs-indexof-array2)
+ nodeName = element.nodeName.toLowerCase();
+ if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName ) !== -1 ) {
+ return true;
+ }
+
+ // Links and areas are focusable if they have an href
+ if ( ( nodeName === 'a' || nodeName === 'area' ) && $element.attr( 'href' ) !== undefined ) {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Find a focusable child
+ *
+ * @param {jQuery} $container Container to search in
+ * @param {boolean} [backwards] Search backwards
+ * @return {jQuery} Focusable child, an empty jQuery object if none found
+ */
+OO.ui.findFocusable = function ( $container, backwards ) {
+ var $focusable = $( [] ),
+ // $focusableCandidates is a superset of things that
+ // could get matched by isFocusableElement
+ $focusableCandidates = $container
+ .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
+
+ if ( backwards ) {
+ $focusableCandidates = Array.prototype.reverse.call( $focusableCandidates );
+ }
+
+ $focusableCandidates.each( function () {
+ var $this = $( this );
+ if ( OO.ui.isFocusableElement( $this ) ) {
+ $focusable = $this;
+ return false;
+ }
+ } );
+ return $focusable;
+};
+
+/**
+ * Get the user's language and any fallback languages.
+ *
+ * These language codes are used to localize user interface elements in the user's language.
+ *
+ * In environments that provide a localization system, this function should be overridden to
+ * return the user's language(s). The default implementation returns English (en) only.
+ *
+ * @return {string[]} Language codes, in descending order of priority
+ */
+OO.ui.getUserLanguages = function () {
+ return [ 'en' ];
+};
+
+/**
+ * Get a value in an object keyed by language code.
+ *
+ * @param {Object.<string,Mixed>} obj Object keyed by language code
+ * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
+ * @param {string} [fallback] Fallback code, used if no matching language can be found
+ * @return {Mixed} Local value
+ */
+OO.ui.getLocalValue = function ( obj, lang, fallback ) {
+ var i, len, langs;
+
+ // Requested language
+ if ( obj[ lang ] ) {
+ return obj[ lang ];
+ }
+ // Known user language
+ langs = OO.ui.getUserLanguages();
+ for ( i = 0, len = langs.length; i < len; i++ ) {
+ lang = langs[ i ];
+ if ( obj[ lang ] ) {
+ return obj[ lang ];
+ }
+ }
+ // Fallback language
+ if ( obj[ fallback ] ) {
+ return obj[ fallback ];
+ }
+ // First existing language
+ for ( lang in obj ) {
+ return obj[ lang ];
+ }
+
+ return undefined;
+};
+
+/**
+ * Check if a node is contained within another node
+ *
+ * Similar to jQuery#contains except a list of containers can be supplied
+ * and a boolean argument allows you to include the container in the match list
+ *
+ * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
+ * @param {HTMLElement} contained Node to find
+ * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
+ * @return {boolean} The node is in the list of target nodes
+ */
+OO.ui.contains = function ( containers, contained, matchContainers ) {
+ var i;
+ if ( !Array.isArray( containers ) ) {
+ containers = [ containers ];
+ }
+ for ( i = containers.length - 1; i >= 0; i-- ) {
+ if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) {
+ return true;
+ }
+ }
+ return false;
+};
+
+/**
+ * Return a function, that, as long as it continues to be invoked, will not
+ * be triggered. The function will be called after it stops being called for
+ * N milliseconds. If `immediate` is passed, trigger the function on the
+ * leading edge, instead of the trailing.
+ *
+ * Ported from: http://underscorejs.org/underscore.js
+ *
+ * @param {Function} func
+ * @param {number} wait
+ * @param {boolean} immediate
+ * @return {Function}
+ */
+OO.ui.debounce = function ( func, wait, immediate ) {
+ var timeout;
+ return function () {
+ var context = this,
+ args = arguments,
+ later = function () {
+ timeout = null;
+ if ( !immediate ) {
+ func.apply( context, args );
+ }
+ };
+ if ( immediate && !timeout ) {
+ func.apply( context, args );
+ }
+ clearTimeout( timeout );
+ timeout = setTimeout( later, wait );
+ };
+};
+
+/**
+ * Proxy for `node.addEventListener( eventName, handler, true )`.
+ *
+ * @param {HTMLElement} node
+ * @param {string} eventName
+ * @param {Function} handler
+ * @deprecated
+ */
+OO.ui.addCaptureEventListener = function ( node, eventName, handler ) {
+ node.addEventListener( eventName, handler, true );
+};
+
+/**
+ * Proxy for `node.removeEventListener( eventName, handler, true )`.
+ *
+ * @param {HTMLElement} node
+ * @param {string} eventName
+ * @param {Function} handler
+ * @deprecated
+ */
+OO.ui.removeCaptureEventListener = function ( node, eventName, handler ) {
+ node.removeEventListener( eventName, handler, true );
+};
+
+/**
+ * Reconstitute a JavaScript object corresponding to a widget created by
+ * the PHP implementation.
+ *
+ * This is an alias for `OO.ui.Element.static.infuse()`.
+ *
+ * @param {string|HTMLElement|jQuery} idOrNode
+ * A DOM id (if a string) or node for the widget to infuse.
+ * @return {OO.ui.Element}
+ * The `OO.ui.Element` corresponding to this (infusable) document node.
+ */
+OO.ui.infuse = function ( idOrNode ) {
+ return OO.ui.Element.static.infuse( idOrNode );
+};
+
+( function () {
+ /**
+ * Message store for the default implementation of OO.ui.msg
+ *
+ * Environments that provide a localization system should not use this, but should override
+ * OO.ui.msg altogether.
+ *
+ * @private
+ */
+ var messages = {
+ // Tool tip for a button that moves items in a list down one place
+ 'ooui-outline-control-move-down': 'Move item down',
+ // Tool tip for a button that moves items in a list up one place
+ 'ooui-outline-control-move-up': 'Move item up',
+ // Tool tip for a button that removes items from a list
+ '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 fake tool that expands the full list of tools in a toolbar group
+ 'ooui-toolgroup-expand': 'More',
+ // Label for the fake tool that collapses the full list of tools in a toolbar group
+ 'ooui-toolgroup-collapse': 'Fewer',
+ // 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 only recoverable errors
+ 'ooui-dialog-process-retry': 'Try again',
+ // Label for process dialog retry action button, visible when describing only warnings
+ 'ooui-dialog-process-continue': 'Continue',
+ // Label for the file selection widget's select file button
+ 'ooui-selectfile-button-select': 'Select a file',
+ // Label for the file selection widget if file selection is not supported
+ 'ooui-selectfile-not-supported': 'File selection is not supported',
+ // Label for the file selection widget when no file is currently selected
+ 'ooui-selectfile-placeholder': 'No file is selected',
+ // Label for the file selection widget's drop target
+ 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
+ };
+
+ /**
+ * Get a localized message.
+ *
+ * In environments that provide a localization system, this function should be overridden to
+ * return the message translated in the user's language. The default implementation always returns
+ * English messages.
+ *
+ * After the message key, message parameters may optionally be passed. In the default implementation,
+ * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
+ * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
+ * they support unnamed, ordered message parameters.
+ *
+ * @param {string} key Message key
+ * @param {Mixed...} [params] Message parameters
+ * @return {string} Translated message with parameters substituted
+ */
+ OO.ui.msg = function ( key ) {
+ var message = messages[ key ],
+ params = Array.prototype.slice.call( arguments, 1 );
+ if ( typeof message === 'string' ) {
+ // Perform $1 substitution
+ message = message.replace( /\$(\d+)/g, function ( unused, n ) {
+ var i = parseInt( n, 10 );
+ return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
+ } );
+ } else {
+ // Return placeholder if message not found
+ message = '[' + key + ']';
+ }
+ return message;
+ };
+} )();
+
+/**
+ * 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.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();
+ }
+ return msg;
+};
+
+/**
+ * @param {string} url
+ * @return {boolean}
+ */
+OO.ui.isSafeUrl = function ( url ) {
+ // Keep this function in sync with php/Tag.php
+ var i, protocolWhitelist;
+
+ function stringStartsWith( haystack, needle ) {
+ return haystack.substr( 0, needle.length ) === needle;
+ }
+
+ protocolWhitelist = [
+ 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
+ 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
+ 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
+ ];
+
+ if ( url === '' ) {
+ return true;
+ }
+
+ for ( i = 0; i < protocolWhitelist.length; i++ ) {
+ if ( stringStartsWith( url, protocolWhitelist[ i ] + ':' ) ) {
+ return true;
+ }
+ }
+
+ // This matches '//' too
+ if ( stringStartsWith( url, '/' ) || stringStartsWith( url, './' ) ) {
+ return true;
+ }
+ if ( stringStartsWith( url, '?' ) || stringStartsWith( url, '#' ) ) {
+ return true;
+ }
+
+ return false;
+};
+
+/*!
+ * Mixin namespace.
+ */
+
+/**
+ * Namespace for OOjs UI mixins.
+ *
+ * Mixins are named according to the type of object they are intended to
+ * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
+ * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
+ * is intended to be mixed in to an instance of OO.ui.Widget.
+ *
+ * @class
+ * @singleton
+ */
+OO.ui.mixin = {};
+
+/**
+ * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
+ * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
+ * connected to them and can't be interacted with.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
+ * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
+ * for an example.
+ * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
+ * @cfg {string} [id] The HTML id attribute used in the rendered tag.
+ * @cfg {string} [text] Text to insert
+ * @cfg {Array} [content] An array of content elements to append (after #text).
+ * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
+ * Instances of OO.ui.Element will have their $element appended.
+ * @cfg {jQuery} [$content] Content elements to append (after #text).
+ * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
+ * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
+ * Data can also be specified with the #setData method.
+ */
+OO.ui.Element = function OoUiElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$ = $;
+ this.visible = true;
+ this.data = config.data;
+ this.$element = config.$element ||
+ $( document.createElement( this.getTagName() ) );
+ this.elementGroup = null;
+ this.debouncedUpdateThemeClassesHandler = OO.ui.debounce( this.debouncedUpdateThemeClasses );
+
+ // Initialization
+ if ( Array.isArray( config.classes ) ) {
+ this.$element.addClass( config.classes.join( ' ' ) );
+ }
+ if ( config.id ) {
+ this.$element.attr( 'id', config.id );
+ }
+ if ( config.text ) {
+ this.$element.text( config.text );
+ }
+ if ( config.content ) {
+ // The `content` property treats plain strings as text; use an
+ // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
+ // appropriate $element appended.
+ this.$element.append( config.content.map( function ( v ) {
+ if ( typeof v === 'string' ) {
+ // Escape string so it is properly represented in HTML.
+ return document.createTextNode( v );
+ } else if ( v instanceof OO.ui.HtmlSnippet ) {
+ // Bypass escaping.
+ return v.toString();
+ } else if ( v instanceof OO.ui.Element ) {
+ return v.$element;
+ }
+ return v;
+ } ) );
+ }
+ if ( config.$content ) {
+ // The `$content` property treats plain strings as HTML.
+ this.$element.append( config.$content );
+ }
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.Element );
+
+/* Static Properties */
+
+/**
+ * The name of the HTML tag used by the element.
+ *
+ * The static value may be ignored if the #getTagName method is overridden.
+ *
+ * @static
+ * @inheritable
+ * @property {string}
+ */
+OO.ui.Element.static.tagName = 'div';
+
+/* Static Methods */
+
+/**
+ * Reconstitute a JavaScript object corresponding to a widget created
+ * by the PHP implementation.
+ *
+ * @param {string|HTMLElement|jQuery} idOrNode
+ * A DOM id (if a string) or node for the widget to infuse.
+ * @return {OO.ui.Element}
+ * The `OO.ui.Element` corresponding to this (infusable) document node.
+ * For `Tag` objects emitted on the HTML side (used occasionally for content)
+ * the value returned is a newly-created Element wrapping around the existing
+ * DOM node.
+ */
+OO.ui.Element.static.infuse = function ( idOrNode ) {
+ var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, false );
+ // Verify that the type matches up.
+ // FIXME: uncomment after T89721 is fixed (see T90929)
+ /*
+ if ( !( obj instanceof this['class'] ) ) {
+ throw new Error( 'Infusion type mismatch!' );
+ }
+ */
+ return obj;
+};
+
+/**
+ * Implementation helper for `infuse`; skips the type check and has an
+ * extra property so that only the top-level invocation touches the DOM.
+ * @private
+ * @param {string|HTMLElement|jQuery} idOrNode
+ * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
+ * when the top-level widget of this infusion is inserted into DOM,
+ * replacing the original node; or false for top-level invocation.
+ * @return {OO.ui.Element}
+ */
+OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
+ // look for a cached result of a previous infusion.
+ var id, $elem, data, cls, parts, parent, obj, top, state;
+ if ( typeof idOrNode === 'string' ) {
+ id = idOrNode;
+ $elem = $( document.getElementById( id ) );
+ } else {
+ $elem = $( idOrNode );
+ id = $elem.attr( 'id' );
+ }
+ if ( !$elem.length ) {
+ throw new Error( 'Widget not found: ' + id );
+ }
+ data = $elem.data( 'ooui-infused' ) || $elem[ 0 ].oouiInfused;
+ if ( data ) {
+ // cached!
+ if ( data === true ) {
+ throw new Error( 'Circular dependency! ' + id );
+ }
+ return data;
+ }
+ data = $elem.attr( 'data-ooui' );
+ if ( !data ) {
+ throw new Error( 'No infusion data found: ' + id );
+ }
+ try {
+ data = $.parseJSON( data );
+ } catch ( _ ) {
+ data = null;
+ }
+ if ( !( data && data._ ) ) {
+ throw new Error( 'No valid infusion data found: ' + id );
+ }
+ if ( data._ === 'Tag' ) {
+ // Special case: this is a raw Tag; wrap existing node, don't rebuild.
+ return new OO.ui.Element( { $element: $elem } );
+ }
+ parts = data._.split( '.' );
+ cls = OO.getProp.apply( OO, [ window ].concat( parts ) );
+ if ( cls === undefined ) {
+ // The PHP output might be old and not including the "OO.ui" prefix
+ // TODO: Remove this back-compat after next major release
+ cls = OO.getProp.apply( OO, [ OO.ui ].concat( parts ) );
+ if ( cls === undefined ) {
+ throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
+ }
+ }
+
+ // Verify that we're creating an OO.ui.Element instance
+ parent = cls.parent;
+
+ while ( parent !== undefined ) {
+ if ( parent === OO.ui.Element ) {
+ // Safe
+ break;
+ }
+
+ parent = parent.parent;
+ }
+
+ if ( parent !== OO.ui.Element ) {
+ throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
+ }
+
+ if ( domPromise === false ) {
+ top = $.Deferred();
+ domPromise = top.promise();
+ }
+ $elem.data( 'ooui-infused', true ); // prevent loops
+ data.id = id; // implicit
+ data = OO.copy( data, null, function deserialize( value ) {
+ if ( OO.isPlainObject( value ) ) {
+ if ( value.tag ) {
+ return OO.ui.Element.static.unsafeInfuse( value.tag, domPromise );
+ }
+ if ( value.html ) {
+ return new OO.ui.HtmlSnippet( value.html );
+ }
+ }
+ } );
+ // allow widgets to reuse parts of the DOM
+ data = cls.static.reusePreInfuseDOM( $elem[ 0 ], data );
+ // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
+ state = cls.static.gatherPreInfuseState( $elem[ 0 ], data );
+ // rebuild widget
+ // jscs:disable requireCapitalizedConstructors
+ obj = new cls( data );
+ // jscs:enable requireCapitalizedConstructors
+ // now replace old DOM with this new DOM.
+ if ( top ) {
+ // An efficient constructor might be able to reuse the entire DOM tree of the original element,
+ // so only mutate the DOM if we need to.
+ if ( $elem[ 0 ] !== obj.$element[ 0 ] ) {
+ $elem.replaceWith( obj.$element );
+ // This element is now gone from the DOM, but if anyone is holding a reference to it,
+ // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
+ // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
+ $elem[ 0 ].oouiInfused = obj;
+ }
+ top.resolve();
+ }
+ obj.$element.data( 'ooui-infused', obj );
+ // set the 'data-ooui' attribute so we can identify infused widgets
+ obj.$element.attr( 'data-ooui', '' );
+ // restore dynamic state after the new element is inserted into DOM
+ domPromise.done( obj.restorePreInfuseState.bind( obj, state ) );
+ return obj;
+};
+
+/**
+ * Pick out parts of `node`'s DOM to be reused when infusing a widget.
+ *
+ * This method **must not** make any changes to the DOM, only find interesting pieces and add them
+ * to `config` (which should then be returned). Actual DOM juggling should then be done by the
+ * constructor, which will be given the enhanced config.
+ *
+ * @protected
+ * @param {HTMLElement} node
+ * @param {Object} config
+ * @return {Object}
+ */
+OO.ui.Element.static.reusePreInfuseDOM = function ( node, config ) {
+ return config;
+};
+
+/**
+ * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
+ * (and its children) that represent an Element of the same class and the given configuration,
+ * generated by the PHP implementation.
+ *
+ * This method is called just before `node` is detached from the DOM. The return value of this
+ * function will be passed to #restorePreInfuseState after the newly created widget's #$element
+ * is inserted into DOM to replace `node`.
+ *
+ * @protected
+ * @param {HTMLElement} node
+ * @param {Object} config
+ * @return {Object}
+ */
+OO.ui.Element.static.gatherPreInfuseState = function () {
+ return {};
+};
+
+/**
+ * Get a jQuery function within a specific document.
+ *
+ * @static
+ * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
+ * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
+ * not in an iframe
+ * @return {Function} Bound jQuery function
+ */
+OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
+ function wrapper( selector ) {
+ return $( selector, wrapper.context );
+ }
+
+ wrapper.context = this.getDocument( context );
+
+ if ( $iframe ) {
+ wrapper.$iframe = $iframe;
+ }
+
+ return wrapper;
+};
+
+/**
+ * Get the document of an element.
+ *
+ * @static
+ * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
+ * @return {HTMLDocument|null} Document object
+ */
+OO.ui.Element.static.getDocument = function ( obj ) {
+ // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
+ return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
+ // Empty jQuery selections might have a context
+ obj.context ||
+ // HTMLElement
+ obj.ownerDocument ||
+ // Window
+ obj.document ||
+ // HTMLDocument
+ ( obj.nodeType === 9 && obj ) ||
+ null;
+};
+
+/**
+ * Get the window of an element or document.
+ *
+ * @static
+ * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
+ * @return {Window} Window object
+ */
+OO.ui.Element.static.getWindow = function ( obj ) {
+ var doc = this.getDocument( obj );
+ return doc.defaultView;
+};
+
+/**
+ * Get the direction of an element or document.
+ *
+ * @static
+ * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
+ * @return {string} Text direction, either 'ltr' or 'rtl'
+ */
+OO.ui.Element.static.getDir = function ( obj ) {
+ var isDoc, isWin;
+
+ if ( obj instanceof jQuery ) {
+ obj = obj[ 0 ];
+ }
+ isDoc = obj.nodeType === 9;
+ isWin = obj.document !== undefined;
+ if ( isDoc || isWin ) {
+ if ( isWin ) {
+ obj = obj.document;
+ }
+ obj = obj.body;
+ }
+ return $( obj ).css( 'direction' );
+};
+
+/**
+ * Get the offset between two frames.
+ *
+ * TODO: Make this function not use recursion.
+ *
+ * @static
+ * @param {Window} from Window of the child frame
+ * @param {Window} [to=window] Window of the parent frame
+ * @param {Object} [offset] Offset to start with, used internally
+ * @return {Object} Offset object, containing left and top properties
+ */
+OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
+ var i, len, frames, frame, rect;
+
+ if ( !to ) {
+ to = window;
+ }
+ if ( !offset ) {
+ offset = { top: 0, left: 0 };
+ }
+ if ( from.parent === from ) {
+ return offset;
+ }
+
+ // Get iframe element
+ frames = from.parent.document.getElementsByTagName( 'iframe' );
+ for ( i = 0, len = frames.length; i < len; i++ ) {
+ if ( frames[ i ].contentWindow === from ) {
+ frame = frames[ i ];
+ break;
+ }
+ }
+
+ // Recursively accumulate offset values
+ if ( frame ) {
+ rect = frame.getBoundingClientRect();
+ offset.left += rect.left;
+ offset.top += rect.top;
+ if ( from !== to ) {
+ this.getFrameOffset( from.parent, offset );
+ }
+ }
+ return offset;
+};
+
+/**
+ * Get the offset between two elements.
+ *
+ * The two elements may be in a different frame, but in that case the frame $element is in must
+ * be contained in the frame $anchor is in.
+ *
+ * @static
+ * @param {jQuery} $element Element whose position to get
+ * @param {jQuery} $anchor Element to get $element's position relative to
+ * @return {Object} Translated position coordinates, containing top and left properties
+ */
+OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
+ var iframe, iframePos,
+ pos = $element.offset(),
+ anchorPos = $anchor.offset(),
+ elementDocument = this.getDocument( $element ),
+ anchorDocument = this.getDocument( $anchor );
+
+ // If $element isn't in the same document as $anchor, traverse up
+ while ( elementDocument !== anchorDocument ) {
+ iframe = elementDocument.defaultView.frameElement;
+ if ( !iframe ) {
+ throw new Error( '$element frame is not contained in $anchor frame' );
+ }
+ iframePos = $( iframe ).offset();
+ pos.left += iframePos.left;
+ pos.top += iframePos.top;
+ elementDocument = iframe.ownerDocument;
+ }
+ pos.left -= anchorPos.left;
+ pos.top -= anchorPos.top;
+ return pos;
+};
+
+/**
+ * Get element border sizes.
+ *
+ * @static
+ * @param {HTMLElement} el Element to measure
+ * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
+ */
+OO.ui.Element.static.getBorders = function ( el ) {
+ var doc = el.ownerDocument,
+ win = doc.defaultView,
+ style = win.getComputedStyle( el, null ),
+ $el = $( el ),
+ top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
+ left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
+ bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
+ right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
+
+ return {
+ top: top,
+ left: left,
+ bottom: bottom,
+ right: right
+ };
+};
+
+/**
+ * Get dimensions of an element or window.
+ *
+ * @static
+ * @param {HTMLElement|Window} el Element to measure
+ * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
+ */
+OO.ui.Element.static.getDimensions = function ( el ) {
+ var $el, $win,
+ doc = el.ownerDocument || el.document,
+ win = doc.defaultView;
+
+ if ( win === el || el === doc.documentElement ) {
+ $win = $( win );
+ return {
+ borders: { top: 0, left: 0, bottom: 0, right: 0 },
+ scroll: {
+ top: $win.scrollTop(),
+ left: $win.scrollLeft()
+ },
+ scrollbar: { right: 0, bottom: 0 },
+ rect: {
+ top: 0,
+ left: 0,
+ bottom: $win.innerHeight(),
+ right: $win.innerWidth()
+ }
+ };
+ } else {
+ $el = $( el );
+ return {
+ borders: this.getBorders( el ),
+ scroll: {
+ top: $el.scrollTop(),
+ left: $el.scrollLeft()
+ },
+ scrollbar: {
+ right: $el.innerWidth() - el.clientWidth,
+ bottom: $el.innerHeight() - el.clientHeight
+ },
+ rect: el.getBoundingClientRect()
+ };
+ }
+};
+
+/**
+ * Get scrollable object parent
+ *
+ * documentElement can't be used to get or set the scrollTop
+ * property on Blink. Changing and testing its value lets us
+ * use 'body' or 'documentElement' based on what is working.
+ *
+ * https://code.google.com/p/chromium/issues/detail?id=303131
+ *
+ * @static
+ * @param {HTMLElement} el Element to find scrollable parent for
+ * @return {HTMLElement} Scrollable parent
+ */
+OO.ui.Element.static.getRootScrollableElement = function ( el ) {
+ var scrollTop, body;
+
+ if ( OO.ui.scrollableElement === undefined ) {
+ body = el.ownerDocument.body;
+ scrollTop = body.scrollTop;
+ body.scrollTop = 1;
+
+ if ( body.scrollTop === 1 ) {
+ body.scrollTop = scrollTop;
+ OO.ui.scrollableElement = 'body';
+ } else {
+ OO.ui.scrollableElement = 'documentElement';
+ }
+ }
+
+ return el.ownerDocument[ OO.ui.scrollableElement ];
+};
+
+/**
+ * Get closest scrollable container.
+ *
+ * Traverses up until either a scrollable element or the root is reached, in which case the window
+ * will be returned.
+ *
+ * @static
+ * @param {HTMLElement} el Element to find scrollable container for
+ * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
+ * @return {HTMLElement} Closest scrollable container
+ */
+OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
+ var i, val,
+ // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
+ props = [ 'overflow-x', 'overflow-y' ],
+ $parent = $( el ).parent();
+
+ if ( dimension === 'x' || dimension === 'y' ) {
+ props = [ 'overflow-' + dimension ];
+ }
+
+ while ( $parent.length ) {
+ if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
+ return $parent[ 0 ];
+ }
+ i = props.length;
+ while ( i-- ) {
+ val = $parent.css( props[ i ] );
+ if ( val === 'auto' || val === 'scroll' ) {
+ return $parent[ 0 ];
+ }
+ }
+ $parent = $parent.parent();
+ }
+ return this.getDocument( el ).body;
+};
+
+/**
+ * Scroll element into view.
+ *
+ * @static
+ * @param {HTMLElement} el Element to scroll into view
+ * @param {Object} [config] Configuration options
+ * @param {string} [config.duration] jQuery animation duration value
+ * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
+ * to scroll in both directions
+ * @param {Function} [config.complete] Function to call when scrolling completes
+ */
+OO.ui.Element.static.scrollIntoView = function ( el, config ) {
+ var rel, anim, callback, sc, $sc, eld, scd, $win;
+
+ // Configuration initialization
+ config = config || {};
+
+ anim = {};
+ callback = typeof config.complete === 'function' && config.complete;
+ sc = this.getClosestScrollableContainer( el, config.direction );
+ $sc = $( sc );
+ eld = this.getDimensions( el );
+ scd = this.getDimensions( sc );
+ $win = $( this.getWindow( el ) );
+
+ // Compute the distances between the edges of el and the edges of the scroll viewport
+ if ( $sc.is( 'html, body' ) ) {
+ // If the scrollable container is the root, this is easy
+ rel = {
+ top: eld.rect.top,
+ bottom: $win.innerHeight() - eld.rect.bottom,
+ left: eld.rect.left,
+ right: $win.innerWidth() - eld.rect.right
+ };
+ } else {
+ // Otherwise, we have to subtract el's coordinates from sc's coordinates
+ rel = {
+ top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
+ bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
+ left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
+ right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
+ };
+ }
+
+ if ( !config.direction || config.direction === 'y' ) {
+ if ( rel.top < 0 ) {
+ anim.scrollTop = scd.scroll.top + rel.top;
+ } else if ( rel.top > 0 && rel.bottom < 0 ) {
+ anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
+ }
+ }
+ if ( !config.direction || config.direction === 'x' ) {
+ if ( rel.left < 0 ) {
+ anim.scrollLeft = scd.scroll.left + rel.left;
+ } else if ( rel.left > 0 && rel.right < 0 ) {
+ anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
+ }
+ }
+ if ( !$.isEmptyObject( anim ) ) {
+ $sc.stop( true ).animate( anim, config.duration || 'fast' );
+ if ( callback ) {
+ $sc.queue( function ( next ) {
+ callback();
+ next();
+ } );
+ }
+ } else {
+ if ( callback ) {
+ callback();
+ }
+ }
+};
+
+/**
+ * Force the browser to reconsider whether it really needs to render scrollbars inside the element
+ * and reserve space for them, because it probably doesn't.
+ *
+ * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
+ * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
+ * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
+ * and then reattach (or show) them back.
+ *
+ * @static
+ * @param {HTMLElement} el Element to reconsider the scrollbars on
+ */
+OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
+ var i, len, scrollLeft, scrollTop, nodes = [];
+ // Save scroll position
+ scrollLeft = el.scrollLeft;
+ scrollTop = el.scrollTop;
+ // Detach all children
+ while ( el.firstChild ) {
+ nodes.push( el.firstChild );
+ el.removeChild( el.firstChild );
+ }
+ // Force reflow
+ void el.offsetHeight;
+ // Reattach all children
+ for ( i = 0, len = nodes.length; i < len; i++ ) {
+ el.appendChild( nodes[ i ] );
+ }
+ // Restore scroll position (no-op if scrollbars disappeared)
+ el.scrollLeft = scrollLeft;
+ el.scrollTop = scrollTop;
+};
+
+/* Methods */
+
+/**
+ * Toggle visibility of an element.
+ *
+ * @param {boolean} [show] Make element visible, omit to toggle visibility
+ * @fires visible
+ * @chainable
+ */
+OO.ui.Element.prototype.toggle = function ( show ) {
+ show = show === undefined ? !this.visible : !!show;
+
+ if ( show !== this.isVisible() ) {
+ this.visible = show;
+ this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
+ this.emit( 'toggle', show );
+ }
+
+ return this;
+};
+
+/**
+ * Check if element is visible.
+ *
+ * @return {boolean} element is visible
+ */
+OO.ui.Element.prototype.isVisible = function () {
+ return this.visible;
+};
+
+/**
+ * Get element data.
+ *
+ * @return {Mixed} Element data
+ */
+OO.ui.Element.prototype.getData = function () {
+ return this.data;
+};
+
+/**
+ * Set element data.
+ *
+ * @param {Mixed} Element data
+ * @chainable
+ */
+OO.ui.Element.prototype.setData = function ( data ) {
+ this.data = data;
+ return this;
+};
+
+/**
+ * Check if element supports one or more methods.
+ *
+ * @param {string|string[]} methods Method or list of methods to check
+ * @return {boolean} All methods are supported
+ */
+OO.ui.Element.prototype.supports = function ( methods ) {
+ var i, len,
+ support = 0;
+
+ methods = Array.isArray( methods ) ? methods : [ methods ];
+ for ( i = 0, len = methods.length; i < len; i++ ) {
+ if ( $.isFunction( this[ methods[ i ] ] ) ) {
+ support++;
+ }
+ }
+
+ return methods.length === support;
+};
+
+/**
+ * Update the theme-provided classes.
+ *
+ * @localdoc This is called in element mixins and widget classes any time state changes.
+ * Updating is debounced, minimizing overhead of changing multiple attributes and
+ * guaranteeing that theme updates do not occur within an element's constructor
+ */
+OO.ui.Element.prototype.updateThemeClasses = function () {
+ this.debouncedUpdateThemeClassesHandler();
+};
+
+/**
+ * @private
+ * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to
+ * make them synchronous.
+ */
+OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
+ OO.ui.theme.updateElementClasses( this );
+};
+
+/**
+ * Get the HTML tag name.
+ *
+ * Override this method to base the result on instance information.
+ *
+ * @return {string} HTML tag name
+ */
+OO.ui.Element.prototype.getTagName = function () {
+ return this.constructor.static.tagName;
+};
+
+/**
+ * Check if the element is attached to the DOM
+ * @return {boolean} The element is attached to the DOM
+ */
+OO.ui.Element.prototype.isElementAttached = function () {
+ return $.contains( this.getElementDocument(), this.$element[ 0 ] );
+};
+
+/**
+ * Get the DOM document.
+ *
+ * @return {HTMLDocument} Document object
+ */
+OO.ui.Element.prototype.getElementDocument = function () {
+ // Don't cache this in other ways either because subclasses could can change this.$element
+ return OO.ui.Element.static.getDocument( this.$element );
+};
+
+/**
+ * Get the DOM window.
+ *
+ * @return {Window} Window object
+ */
+OO.ui.Element.prototype.getElementWindow = function () {
+ return OO.ui.Element.static.getWindow( this.$element );
+};
+
+/**
+ * Get closest scrollable container.
+ */
+OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
+ return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
+};
+
+/**
+ * Get group element is in.
+ *
+ * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
+ */
+OO.ui.Element.prototype.getElementGroup = function () {
+ return this.elementGroup;
+};
+
+/**
+ * Set group element is in.
+ *
+ * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
+ * @chainable
+ */
+OO.ui.Element.prototype.setElementGroup = function ( group ) {
+ this.elementGroup = group;
+ return this;
+};
+
+/**
+ * Scroll element into view.
+ *
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
+ return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
+};
+
+/**
+ * Restore the pre-infusion dynamic state for this widget.
+ *
+ * This method is called after #$element has been inserted into DOM. The parameter is the return
+ * value of #gatherPreInfuseState.
+ *
+ * @protected
+ * @param {Object} state
+ */
+OO.ui.Element.prototype.restorePreInfuseState = function () {
+};
+
+/**
+ * Wraps an HTML snippet for use with configuration values which default
+ * to strings. This bypasses the default html-escaping done to string
+ * values.
+ *
+ * @class
+ *
+ * @constructor
+ * @param {string} [content] HTML content
+ */
+OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
+ // Properties
+ this.content = content;
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.HtmlSnippet );
+
+/* Methods */
+
+/**
+ * Render into HTML.
+ *
+ * @return {string} Unchanged HTML snippet.
+ */
+OO.ui.HtmlSnippet.prototype.toString = function () {
+ return this.content;
+};
+
+/**
+ * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
+ * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
+ * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
+ * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
+ * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.Layout = function OoUiLayout( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.Layout.parent.call( this, config );
+
+ // Mixin constructors
+ OO.EventEmitter.call( this );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-layout' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.Layout, OO.ui.Element );
+OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
+
+/**
+ * Widgets are compositions of one or more OOjs UI elements that users can both view
+ * and interact with. All widgets can be configured and modified via a standard API,
+ * and their state can change dynamically according to a model.
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
+ * appearance reflects this state.
+ */
+OO.ui.Widget = function OoUiWidget( config ) {
+ // Initialize config
+ config = $.extend( { disabled: false }, config );
+
+ // Parent constructor
+ OO.ui.Widget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.EventEmitter.call( this );
+
+ // Properties
+ 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 );
+
+/* Static Properties */
+
+/**
+ * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
+ * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
+ * handling.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.Widget.static.supportsSimpleLabel = false;
+
+/* Events */
+
+/**
+ * @event disable
+ *
+ * A 'disable' event is emitted when the disabled state of the widget changes
+ * (i.e. on disable **and** enable).
+ *
+ * @param {boolean} disabled Widget is disabled
+ */
+
+/**
+ * @event toggle
+ *
+ * A 'toggle' event is emitted when the visibility of the widget changes.
+ *
+ * @param {boolean} visible Widget is visible
+ */
+
+/* Methods */
+
+/**
+ * Check if the widget is disabled.
+ *
+ * @return {boolean} Widget is disabled
+ */
+OO.ui.Widget.prototype.isDisabled = function () {
+ return this.disabled;
+};
+
+/**
+ * Set the 'disabled' state of the widget.
+ *
+ * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
+ *
+ * @param {boolean} disabled Disable widget
+ * @chainable
+ */
+OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
+ var isDisabled;
+
+ 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.$element.attr( 'aria-disabled', isDisabled.toString() );
+ this.emit( 'disable', isDisabled );
+ this.updateThemeClasses();
+ }
+ this.wasDisabled = isDisabled;
+
+ return this;
+};
+
+/**
+ * Update the disabled state, in case of changes in parent widget.
+ *
+ * @chainable
+ */
+OO.ui.Widget.prototype.updateDisabled = function () {
+ this.setDisabled( this.disabled );
+ return this;
+};
+
+/**
+ * Theme logic.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.Theme = function OoUiTheme( config ) {
+ // Configuration initialization
+ config = config || {};
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.Theme );
+
+/* Methods */
+
+/**
+ * Get a list of classes to be applied to a widget.
+ *
+ * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
+ * otherwise state transitions will not work properly.
+ *
+ * @param {OO.ui.Element} element Element for which to get classes
+ * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
+ */
+OO.ui.Theme.prototype.getElementClasses = function () {
+ return { on: [], off: [] };
+};
+
+/**
+ * Update CSS classes provided by the theme.
+ *
+ * For elements with theme logic hooks, this should be called any time there's a state change.
+ *
+ * @param {OO.ui.Element} element Element for which to update classes
+ * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
+ */
+OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
+ var $elements = $( [] ),
+ classes = this.getElementClasses( element );
+
+ if ( element.$icon ) {
+ $elements = $elements.add( element.$icon );
+ }
+ if ( element.$indicator ) {
+ $elements = $elements.add( element.$indicator );
+ }
+
+ $elements
+ .removeClass( classes.off.join( ' ' ) )
+ .addClass( classes.on.join( ' ' ) );
+};
+
+/**
+ * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
+ * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
+ * order in which users will navigate through the focusable elements via the "tab" key.
+ *
+ * @example
+ * // TabIndexedElement is mixed into the ButtonWidget class
+ * // to provide a tabIndex property.
+ * var button1 = new OO.ui.ButtonWidget( {
+ * label: 'fourth',
+ * tabIndex: 4
+ * } );
+ * var button2 = new OO.ui.ButtonWidget( {
+ * label: 'second',
+ * tabIndex: 2
+ * } );
+ * var button3 = new OO.ui.ButtonWidget( {
+ * label: 'third',
+ * tabIndex: 3
+ * } );
+ * var button4 = new OO.ui.ButtonWidget( {
+ * label: 'first',
+ * tabIndex: 1
+ * } );
+ * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
+ * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
+ * functionality will be applied to it instead.
+ * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
+ * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
+ * to remove the element from the tab-navigation flow.
+ */
+OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
+ // Configuration initialization
+ config = $.extend( { tabIndex: 0 }, config );
+
+ // Properties
+ this.$tabIndexed = null;
+ this.tabIndex = null;
+
+ // Events
+ this.connect( this, { disable: 'onTabIndexedElementDisable' } );
+
+ // Initialization
+ this.setTabIndex( config.tabIndex );
+ this.setTabIndexedElement( config.$tabIndexed || this.$element );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.mixin.TabIndexedElement );
+
+/* Methods */
+
+/**
+ * Set the element that should use the tabindex functionality.
+ *
+ * This method is used to retarget a tabindex mixin so that its functionality applies
+ * to the specified element. If an element is currently using the functionality, the mixin’s
+ * effect on that element is removed before the new element is set up.
+ *
+ * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
+ * @chainable
+ */
+OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
+ var tabIndex = this.tabIndex;
+ // Remove attributes from old $tabIndexed
+ this.setTabIndex( null );
+ // Force update of new $tabIndexed
+ this.$tabIndexed = $tabIndexed;
+ this.tabIndex = tabIndex;
+ return this.updateTabIndex();
+};
+
+/**
+ * Set the value of the tabindex.
+ *
+ * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
+ * @chainable
+ */
+OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
+ tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
+
+ if ( this.tabIndex !== tabIndex ) {
+ this.tabIndex = tabIndex;
+ this.updateTabIndex();
+ }
+
+ return this;
+};
+
+/**
+ * Update the `tabindex` attribute, in case of changes to tab index or
+ * disabled state.
+ *
+ * @private
+ * @chainable
+ */
+OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
+ if ( this.$tabIndexed ) {
+ if ( this.tabIndex !== null ) {
+ // Do not index over disabled elements
+ this.$tabIndexed.attr( {
+ tabindex: this.isDisabled() ? -1 : this.tabIndex,
+ // Support: ChromeVox and NVDA
+ // These do not seem to inherit aria-disabled from parent elements
+ 'aria-disabled': this.isDisabled().toString()
+ } );
+ } else {
+ this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
+ }
+ }
+ return this;
+};
+
+/**
+ * Handle disable events.
+ *
+ * @private
+ * @param {boolean} disabled Element is disabled
+ */
+OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
+ this.updateTabIndex();
+};
+
+/**
+ * Get the value of the tabindex.
+ *
+ * @return {number|null} Tabindex value
+ */
+OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
+ return this.tabIndex;
+};
+
+/**
+ * ButtonElement is often mixed into other classes to generate a button, which is a clickable
+ * interface element that can be configured with access keys for accessibility.
+ * See the [OOjs UI documentation on MediaWiki] [1] for examples.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$button] The button element created by the class.
+ * If this configuration is omitted, the button element will use a generated `<a>`.
+ * @cfg {boolean} [framed=true] Render the button with a frame
+ */
+OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$button = null;
+ this.framed = null;
+ this.active = false;
+ this.onMouseUpHandler = this.onMouseUp.bind( this );
+ this.onMouseDownHandler = this.onMouseDown.bind( this );
+ this.onKeyDownHandler = this.onKeyDown.bind( this );
+ this.onKeyUpHandler = this.onKeyUp.bind( this );
+ this.onClickHandler = this.onClick.bind( this );
+ this.onKeyPressHandler = this.onKeyPress.bind( this );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-buttonElement' );
+ this.toggleFramed( config.framed === undefined || config.framed );
+ this.setButtonElement( config.$button || $( '<a>' ) );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.mixin.ButtonElement );
+
+/* Static Properties */
+
+/**
+ * Cancel mouse down events.
+ *
+ * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
+ * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
+ * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
+ * parent widget.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
+
+/* Events */
+
+/**
+ * A 'click' event is emitted when the button element is clicked.
+ *
+ * @event click
+ */
+
+/* Methods */
+
+/**
+ * Set the button element.
+ *
+ * This method is used to retarget a button mixin so that its functionality applies to
+ * the specified button element instead of the one created by the class. If a button element
+ * is already set, the method will remove the mixin’s effect on that element.
+ *
+ * @param {jQuery} $button Element to use as button
+ */
+OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
+ if ( this.$button ) {
+ this.$button
+ .removeClass( 'oo-ui-buttonElement-button' )
+ .removeAttr( 'role accesskey' )
+ .off( {
+ mousedown: this.onMouseDownHandler,
+ keydown: this.onKeyDownHandler,
+ click: this.onClickHandler,
+ keypress: this.onKeyPressHandler
+ } );
+ }
+
+ this.$button = $button
+ .addClass( 'oo-ui-buttonElement-button' )
+ .attr( { role: 'button' } )
+ .on( {
+ mousedown: this.onMouseDownHandler,
+ keydown: this.onKeyDownHandler,
+ click: this.onClickHandler,
+ keypress: this.onKeyPressHandler
+ } );
+};
+
+/**
+ * Handles mouse down events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
+ if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
+ return;
+ }
+ this.$element.addClass( 'oo-ui-buttonElement-pressed' );
+ // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
+ // reliably 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;
+ }
+};
+
+/**
+ * Handles mouse up events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse up event
+ */
+OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
+ if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
+ return;
+ }
+ this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
+ // Stop listening for mouseup, since we only needed this once
+ this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
+};
+
+/**
+ * Handles mouse click events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse click event
+ * @fires click
+ */
+OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
+ if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
+ if ( this.emit( 'click' ) ) {
+ return false;
+ }
+ }
+};
+
+/**
+ * Handles key down events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
+ if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
+ return;
+ }
+ this.$element.addClass( 'oo-ui-buttonElement-pressed' );
+ // Run the keyup handler no matter where the key is when the button is let go, so we can
+ // reliably remove the pressed class
+ this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
+};
+
+/**
+ * Handles key up events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key up event
+ */
+OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
+ if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
+ return;
+ }
+ this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
+ // Stop listening for keyup, since we only needed this once
+ this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
+};
+
+/**
+ * Handles key press events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key press event
+ * @fires click
+ */
+OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
+ if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
+ if ( this.emit( 'click' ) ) {
+ return false;
+ }
+ }
+};
+
+/**
+ * Check if button has a frame.
+ *
+ * @return {boolean} Button is framed
+ */
+OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
+ return this.framed;
+};
+
+/**
+ * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
+ *
+ * @param {boolean} [framed] Make button framed, omit to toggle
+ * @chainable
+ */
+OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
+ framed = framed === undefined ? !this.framed : !!framed;
+ if ( framed !== this.framed ) {
+ this.framed = framed;
+ this.$element
+ .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
+ .toggleClass( 'oo-ui-buttonElement-framed', framed );
+ this.updateThemeClasses();
+ }
+
+ return this;
+};
+
+/**
+ * Set the button's active state.
+ *
+ * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
+ * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
+ * for other button types.
+ *
+ * @param {boolean} value Make button active
+ * @chainable
+ */
+OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
+ this.active = !!value;
+ this.$element.toggleClass( 'oo-ui-buttonElement-active', this.active );
+ return this;
+};
+
+/**
+ * Check if the button is active
+ *
+ * @return {boolean} The button is active
+ */
+OO.ui.mixin.ButtonElement.prototype.isActive = function () {
+ return this.active;
+};
+
+/**
+ * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
+ * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
+ * items from the group is done through the interface the class provides.
+ * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$group] The container element created by the class. If this configuration
+ * is omitted, the group element will use a generated `<div>`.
+ */
+OO.ui.mixin.GroupElement = function OoUiMixinGroupElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$group = null;
+ this.items = [];
+ this.aggregateItemEvents = {};
+
+ // Initialization
+ this.setGroupElement( config.$group || $( '<div>' ) );
+};
+
+/* Methods */
+
+/**
+ * Set the group element.
+ *
+ * If an element is already set, items will be moved to the new element.
+ *
+ * @param {jQuery} $group Element to use as group
+ */
+OO.ui.mixin.GroupElement.prototype.setGroupElement = function ( $group ) {
+ var i, len;
+
+ this.$group = $group;
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ this.$group.append( this.items[ i ].$element );
+ }
+};
+
+/**
+ * Check if a group contains no items.
+ *
+ * @return {boolean} Group is empty
+ */
+OO.ui.mixin.GroupElement.prototype.isEmpty = function () {
+ return !this.items.length;
+};
+
+/**
+ * Get all items in the group.
+ *
+ * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
+ * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
+ * from a group).
+ *
+ * @return {OO.ui.Element[]} An array of items.
+ */
+OO.ui.mixin.GroupElement.prototype.getItems = function () {
+ return this.items.slice( 0 );
+};
+
+/**
+ * Get an item by its data.
+ *
+ * Only the first item with matching data will be returned. To return all matching items,
+ * use the #getItemsFromData method.
+ *
+ * @param {Object} data Item data to search for
+ * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
+ */
+OO.ui.mixin.GroupElement.prototype.getItemFromData = function ( data ) {
+ var i, len, item,
+ hash = OO.getHash( data );
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ item = this.items[ i ];
+ if ( hash === OO.getHash( item.getData() ) ) {
+ return item;
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Get items by their data.
+ *
+ * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
+ *
+ * @param {Object} data Item data to search for
+ * @return {OO.ui.Element[]} Items with equivalent data
+ */
+OO.ui.mixin.GroupElement.prototype.getItemsFromData = function ( data ) {
+ var i, len, item,
+ hash = OO.getHash( data ),
+ items = [];
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ item = this.items[ i ];
+ if ( hash === OO.getHash( item.getData() ) ) {
+ items.push( item );
+ }
+ }
+
+ return items;
+};
+
+/**
+ * Aggregate the events emitted by the group.
+ *
+ * When events are aggregated, the group will listen to all contained items for the event,
+ * and then emit the event under a new name. The new event will contain an additional leading
+ * parameter containing the item that emitted the original event. Other arguments emitted from
+ * the original event are passed through.
+ *
+ * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
+ * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
+ * A `null` value will remove aggregated events.
+
+ * @throws {Error} An error is thrown if aggregation already exists.
+ */
+OO.ui.mixin.GroupElement.prototype.aggregate = function ( events ) {
+ var i, len, item, add, remove, itemEvent, groupEvent;
+
+ for ( itemEvent in events ) {
+ groupEvent = events[ itemEvent ];
+
+ // Remove existing aggregated event
+ if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
+ // 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', this.aggregateItemEvents[ itemEvent ], item ];
+ item.disconnect( this, remove );
+ }
+ }
+ // Prevent future items from aggregating event
+ delete this.aggregateItemEvents[ itemEvent ];
+ }
+
+ // 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 );
+ }
+ }
+ }
+ }
+};
+
+/**
+ * Add items to the group.
+ *
+ * Items will be added to the end of the group array unless the optional `index` parameter specifies
+ * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
+ *
+ * @param {OO.ui.Element[]} items An array of items to add to the group
+ * @param {number} [index] Index of the insertion point
+ * @chainable
+ */
+OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
+ var i, len, item, event, events, currentIndex,
+ itemElements = [];
+
+ for ( i = 0, len = items.length; i < len; i++ ) {
+ item = items[ i ];
+
+ // Check if item exists then remove it first, effectively "moving" it
+ currentIndex = this.items.indexOf( item );
+ if ( currentIndex >= 0 ) {
+ this.removeItems( [ item ] );
+ // Adjust index to compensate for removal
+ if ( currentIndex < index ) {
+ index--;
+ }
+ }
+ // Add the item
+ if ( 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;
+};
+
+/**
+ * Remove the specified items from a group.
+ *
+ * Removed items are detached (not removed) from the DOM so that they may be reused.
+ * To remove all items from a group, you may wish to use the #clearItems method instead.
+ *
+ * @param {OO.ui.Element[]} items An array of items to remove
+ * @chainable
+ */
+OO.ui.mixin.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 = this.items.indexOf( item );
+ if ( index !== -1 ) {
+ if (
+ item.connect && item.disconnect &&
+ !$.isEmptyObject( this.aggregateItemEvents )
+ ) {
+ remove = {};
+ if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
+ remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
+ }
+ item.disconnect( this, remove );
+ }
+ item.setElementGroup( null );
+ this.items.splice( index, 1 );
+ item.$element.detach();
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Clear all items from the group.
+ *
+ * Cleared items are detached from the DOM, not removed, so that they may be reused.
+ * To remove only a subset of items from a group, use the #removeItems method.
+ *
+ * @chainable
+ */
+OO.ui.mixin.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 ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
+ remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
+ }
+ item.disconnect( this, remove );
+ }
+ item.setElementGroup( null );
+ item.$element.detach();
+ }
+
+ this.items = [];
+ return this;
+};
+
+/**
+ * IconElement is often mixed into other classes to generate an icon.
+ * Icons are graphics, about the size of normal text. They are used to aid the user
+ * in locating a control or to convey information in a space-efficient way. See the
+ * [OOjs UI documentation on MediaWiki] [1] for a list of icons
+ * included in the library.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
+ * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
+ * the icon element be set to an existing icon instead of the one generated by this class, set a
+ * value using a jQuery selection. For example:
+ *
+ * // Use a <div> tag instead of a <span>
+ * $icon: $("<div>")
+ * // Use an existing icon element instead of the one generated by the class
+ * $icon: this.$element
+ * // Use an icon element from a child widget
+ * $icon: this.childwidget.$element
+ * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
+ * symbolic names. A map is used for i18n purposes and contains a `default` icon
+ * name and additional names keyed by language code. The `default` name is used when no icon is keyed
+ * by the user's language.
+ *
+ * Example of an i18n map:
+ *
+ * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
+ * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
+ * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
+ * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
+ * text. The icon title is displayed when users move the mouse over the icon.
+ */
+OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$icon = null;
+ this.icon = null;
+ this.iconTitle = null;
+
+ // Initialization
+ this.setIcon( config.icon || this.constructor.static.icon );
+ this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
+ this.setIconElement( config.$icon || $( '<span>' ) );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.mixin.IconElement );
+
+/* Static Properties */
+
+/**
+ * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
+ * for i18n purposes and contains a `default` icon name and additional names keyed by
+ * language code. The `default` name is used when no icon is keyed by the user's language.
+ *
+ * Example of an i18n map:
+ *
+ * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
+ *
+ * Note: the static property will be overridden if the #icon configuration is used.
+ *
+ * @static
+ * @inheritable
+ * @property {Object|string}
+ */
+OO.ui.mixin.IconElement.static.icon = null;
+
+/**
+ * The icon title, displayed when users move the mouse over the icon. The value can be text, a
+ * function that returns title text, or `null` for no title.
+ *
+ * The static property will be overridden if the #iconTitle configuration is used.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function|null}
+ */
+OO.ui.mixin.IconElement.static.iconTitle = null;
+
+/* Methods */
+
+/**
+ * Set the icon element. This method is used to retarget an icon mixin so that its functionality
+ * applies to the specified icon element instead of the one created by the class. If an icon
+ * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
+ * and mixin methods will no longer affect the element.
+ *
+ * @param {jQuery} $icon Element to use as icon
+ */
+OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
+ if ( this.$icon ) {
+ this.$icon
+ .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
+ .removeAttr( 'title' );
+ }
+
+ this.$icon = $icon
+ .addClass( 'oo-ui-iconElement-icon' )
+ .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
+ if ( this.iconTitle !== null ) {
+ this.$icon.attr( 'title', this.iconTitle );
+ }
+
+ this.updateThemeClasses();
+};
+
+/**
+ * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
+ * The icon parameter can also be set to a map of icon names. See the #icon config setting
+ * for an example.
+ *
+ * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
+ * by language code, or `null` to remove the icon.
+ * @chainable
+ */
+OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
+ icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
+ icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
+
+ if ( this.icon !== icon ) {
+ if ( this.$icon ) {
+ if ( this.icon !== null ) {
+ this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
+ }
+ if ( icon !== null ) {
+ this.$icon.addClass( 'oo-ui-icon-' + icon );
+ }
+ }
+ this.icon = icon;
+ }
+
+ this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
+ this.updateThemeClasses();
+
+ return this;
+};
+
+/**
+ * Set the icon title. Use `null` to remove the title.
+ *
+ * @param {string|Function|null} iconTitle A text string used as the icon title,
+ * a function that returns title text, or `null` for no title.
+ * @chainable
+ */
+OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
+ iconTitle = typeof iconTitle === 'function' ||
+ ( typeof iconTitle === 'string' && iconTitle.length ) ?
+ OO.ui.resolveMsg( iconTitle ) : null;
+
+ if ( this.iconTitle !== iconTitle ) {
+ this.iconTitle = iconTitle;
+ if ( this.$icon ) {
+ if ( this.iconTitle !== null ) {
+ this.$icon.attr( 'title', iconTitle );
+ } else {
+ this.$icon.removeAttr( 'title' );
+ }
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Get the symbolic name of the icon.
+ *
+ * @return {string} Icon name
+ */
+OO.ui.mixin.IconElement.prototype.getIcon = function () {
+ return this.icon;
+};
+
+/**
+ * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
+ *
+ * @return {string} Icon title text
+ */
+OO.ui.mixin.IconElement.prototype.getIconTitle = function () {
+ return this.iconTitle;
+};
+
+/**
+ * IndicatorElement is often mixed into other classes to generate an indicator.
+ * Indicators are small graphics that are generally used in two ways:
+ *
+ * - To draw attention to the status of an item. For example, an indicator might be
+ * used to show that an item in a list has errors that need to be resolved.
+ * - To clarify the function of a control that acts in an exceptional way (a button
+ * that opens a menu instead of performing an action directly, for example).
+ *
+ * For a list of indicators included in the library, please see the
+ * [OOjs UI documentation on MediaWiki] [1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
+ * configuration is omitted, the indicator element will use a generated `<span>`.
+ * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
+ * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
+ * in the library.
+ * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
+ * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
+ * or a function that returns title text. The indicator title is displayed when users move
+ * the mouse over the indicator.
+ */
+OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$indicator = null;
+ this.indicator = null;
+ this.indicatorTitle = null;
+
+ // Initialization
+ this.setIndicator( config.indicator || this.constructor.static.indicator );
+ this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
+ this.setIndicatorElement( config.$indicator || $( '<span>' ) );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.mixin.IndicatorElement );
+
+/* Static Properties */
+
+/**
+ * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
+ * The static property will be overridden if the #indicator configuration is used.
+ *
+ * @static
+ * @inheritable
+ * @property {string|null}
+ */
+OO.ui.mixin.IndicatorElement.static.indicator = null;
+
+/**
+ * A text string used as the indicator title, a function that returns title text, or `null`
+ * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function|null}
+ */
+OO.ui.mixin.IndicatorElement.static.indicatorTitle = null;
+
+/* Methods */
+
+/**
+ * Set the indicator element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $indicator Element to use as indicator
+ */
+OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
+ if ( this.$indicator ) {
+ this.$indicator
+ .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
+ .removeAttr( 'title' );
+ }
+
+ this.$indicator = $indicator
+ .addClass( 'oo-ui-indicatorElement-indicator' )
+ .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
+ if ( this.indicatorTitle !== null ) {
+ this.$indicator.attr( 'title', this.indicatorTitle );
+ }
+
+ this.updateThemeClasses();
+};
+
+/**
+ * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
+ *
+ * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
+ * @chainable
+ */
+OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
+ indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
+
+ if ( this.indicator !== indicator ) {
+ if ( this.$indicator ) {
+ if ( this.indicator !== null ) {
+ this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
+ }
+ if ( indicator !== null ) {
+ this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
+ }
+ }
+ this.indicator = indicator;
+ }
+
+ this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
+ this.updateThemeClasses();
+
+ return this;
+};
+
+/**
+ * Set the indicator title.
+ *
+ * The title is displayed when a user moves the mouse over the indicator.
+ *
+ * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
+ * `null` for no indicator title
+ * @chainable
+ */
+OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
+ indicatorTitle = typeof indicatorTitle === 'function' ||
+ ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
+ OO.ui.resolveMsg( indicatorTitle ) : null;
+
+ if ( this.indicatorTitle !== indicatorTitle ) {
+ this.indicatorTitle = indicatorTitle;
+ if ( this.$indicator ) {
+ if ( this.indicatorTitle !== null ) {
+ this.$indicator.attr( 'title', indicatorTitle );
+ } else {
+ this.$indicator.removeAttr( 'title' );
+ }
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
+ *
+ * @return {string} Symbolic name of indicator
+ */
+OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () {
+ return this.indicator;
+};
+
+/**
+ * Get the indicator title.
+ *
+ * The title is displayed when a user moves the mouse over the indicator.
+ *
+ * @return {string} Indicator title text
+ */
+OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
+ return this.indicatorTitle;
+};
+
+/**
+ * LabelElement is often mixed into other classes to generate a label, which
+ * helps identify the function of an interface element.
+ * See the [OOjs UI documentation on MediaWiki] [1] for more information.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$label] The label element created by the class. If this
+ * configuration is omitted, the label element will use a generated `<span>`.
+ * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
+ * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
+ * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
+ * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
+ * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
+ * The label will be truncated to fit if necessary.
+ */
+OO.ui.mixin.LabelElement = function OoUiMixinLabelElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$label = null;
+ this.label = null;
+ this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
+
+ // Initialization
+ this.setLabel( config.label || this.constructor.static.label );
+ this.setLabelElement( config.$label || $( '<span>' ) );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.mixin.LabelElement );
+
+/* Events */
+
+/**
+ * @event labelChange
+ * @param {string} value
+ */
+
+/* Static Properties */
+
+/**
+ * The label text. The label can be specified as a plaintext string, a function that will
+ * produce a string in the future, or `null` for no label. The static value will
+ * be overridden if a label is specified with the #label config option.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function|null}
+ */
+OO.ui.mixin.LabelElement.static.label = null;
+
+/* Methods */
+
+/**
+ * Set the label element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $label Element to use as label
+ */
+OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
+ if ( this.$label ) {
+ this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
+ }
+
+ this.$label = $label.addClass( 'oo-ui-labelElement-label' );
+ this.setLabelContent( this.label );
+};
+
+/**
+ * Set the label.
+ *
+ * An empty string will result in the label being hidden. A string containing only whitespace will
+ * be converted to a single ` `.
+ *
+ * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
+ * text; or null for no label
+ * @chainable
+ */
+OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
+ label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
+ label = ( ( typeof label === 'string' && label.length ) || label instanceof jQuery || label instanceof OO.ui.HtmlSnippet ) ? label : null;
+
+ this.$element.toggleClass( 'oo-ui-labelElement', !!label );
+
+ if ( this.label !== label ) {
+ if ( this.$label ) {
+ this.setLabelContent( label );
+ }
+ this.label = label;
+ this.emit( 'labelChange' );
+ }
+
+ return this;
+};
+
+/**
+ * Get the label.
+ *
+ * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
+ * text; or null for no label
+ */
+OO.ui.mixin.LabelElement.prototype.getLabel = function () {
+ return this.label;
+};
+
+/**
+ * Fit the label.
+ *
+ * @chainable
+ */
+OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
+ if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
+ this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
+ }
+
+ return this;
+};
+
+/**
+ * Set the content of the label.
+ *
+ * Do not call this method until after the label element has been set by #setLabelElement.
+ *
+ * @private
+ * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
+ * text; or null for no label
+ */
+OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
+ if ( typeof label === 'string' ) {
+ if ( label.match( /^\s*$/ ) ) {
+ // Convert whitespace only string to a single non-breaking space
+ this.$label.html( ' ' );
+ } else {
+ this.$label.text( label );
+ }
+ } else if ( label instanceof OO.ui.HtmlSnippet ) {
+ this.$label.html( label.toString() );
+ } else if ( label instanceof jQuery ) {
+ this.$label.empty().append( label );
+ } else {
+ this.$label.empty();
+ }
+};
+
+/**
+ * The FlaggedElement class is an attribute mixin, meaning that it is used to add
+ * additional functionality to an element created by another class. The class provides
+ * a ‘flags’ property assigned the name (or an array of names) of styling flags,
+ * which are used to customize the look and feel of a widget to better describe its
+ * importance and functionality.
+ *
+ * The library currently contains the following styling flags for general use:
+ *
+ * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
+ * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
+ * - **constructive**: Constructive styling is applied to convey that the widget will create something.
+ *
+ * The flags affect the appearance of the buttons:
+ *
+ * @example
+ * // FlaggedElement is mixed into ButtonWidget to provide styling flags
+ * var button1 = new OO.ui.ButtonWidget( {
+ * label: 'Constructive',
+ * flags: 'constructive'
+ * } );
+ * var button2 = new OO.ui.ButtonWidget( {
+ * label: 'Destructive',
+ * flags: 'destructive'
+ * } );
+ * var button3 = new OO.ui.ButtonWidget( {
+ * label: 'Progressive',
+ * flags: 'progressive'
+ * } );
+ * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
+ *
+ * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
+ * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
+ * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
+ * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
+ * @cfg {jQuery} [$flagged] The flagged element. By default,
+ * the flagged functionality is applied to the element created by the class ($element).
+ * If a different element is specified, the flagged functionality will be applied to it instead.
+ */
+OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.flags = {};
+ this.$flagged = null;
+
+ // Initialization
+ this.setFlags( config.flags );
+ this.setFlaggedElement( config.$flagged || this.$element );
+};
+
+/* Events */
+
+/**
+ * @event flag
+ * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
+ * parameter contains the name of each modified flag and indicates whether it was
+ * added or removed.
+ *
+ * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
+ * that the flag was added, `false` that the flag was removed.
+ */
+
+/* Methods */
+
+/**
+ * Set the flagged element.
+ *
+ * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
+ * If an element is already set, the method will remove the mixin’s effect on that element.
+ *
+ * @param {jQuery} $flagged Element that should be flagged
+ */
+OO.ui.mixin.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
+ var classNames = Object.keys( this.flags ).map( function ( flag ) {
+ return 'oo-ui-flaggedElement-' + flag;
+ } ).join( ' ' );
+
+ if ( this.$flagged ) {
+ this.$flagged.removeClass( classNames );
+ }
+
+ this.$flagged = $flagged.addClass( classNames );
+};
+
+/**
+ * Check if the specified flag is set.
+ *
+ * @param {string} flag Name of flag
+ * @return {boolean} The flag is set
+ */
+OO.ui.mixin.FlaggedElement.prototype.hasFlag = function ( flag ) {
+ // This may be called before the constructor, thus before this.flags is set
+ return this.flags && ( flag in this.flags );
+};
+
+/**
+ * Get the names of all flags set.
+ *
+ * @return {string[]} Flag names
+ */
+OO.ui.mixin.FlaggedElement.prototype.getFlags = function () {
+ // This may be called before the constructor, thus before this.flags is set
+ return Object.keys( this.flags || {} );
+};
+
+/**
+ * Clear all flags.
+ *
+ * @chainable
+ * @fires flag
+ */
+OO.ui.mixin.FlaggedElement.prototype.clearFlags = function () {
+ var flag, className,
+ changes = {},
+ remove = [],
+ classPrefix = 'oo-ui-flaggedElement-';
+
+ for ( flag in this.flags ) {
+ className = classPrefix + flag;
+ changes[ flag ] = false;
+ delete this.flags[ flag ];
+ remove.push( className );
+ }
+
+ if ( this.$flagged ) {
+ this.$flagged.removeClass( remove.join( ' ' ) );
+ }
+
+ this.updateThemeClasses();
+ this.emit( 'flag', changes );
+
+ return this;
+};
+
+/**
+ * Add one or more flags.
+ *
+ * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
+ * or an object keyed by flag name with a boolean value that indicates whether the flag should
+ * be added (`true`) or removed (`false`).
+ * @chainable
+ * @fires flag
+ */
+OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
+ var i, len, flag, className,
+ changes = {},
+ add = [],
+ remove = [],
+ classPrefix = 'oo-ui-flaggedElement-';
+
+ if ( typeof flags === 'string' ) {
+ className = classPrefix + flags;
+ // Set
+ if ( !this.flags[ flags ] ) {
+ this.flags[ flags ] = true;
+ add.push( className );
+ }
+ } else if ( Array.isArray( flags ) ) {
+ for ( i = 0, len = flags.length; i < len; i++ ) {
+ flag = flags[ i ];
+ className = classPrefix + flag;
+ // Set
+ if ( !this.flags[ flag ] ) {
+ changes[ flag ] = true;
+ this.flags[ flag ] = true;
+ add.push( className );
+ }
+ }
+ } else if ( OO.isPlainObject( flags ) ) {
+ for ( flag in flags ) {
+ className = classPrefix + flag;
+ if ( flags[ flag ] ) {
+ // Set
+ if ( !this.flags[ flag ] ) {
+ changes[ flag ] = true;
+ this.flags[ flag ] = true;
+ add.push( className );
+ }
+ } else {
+ // Remove
+ if ( this.flags[ flag ] ) {
+ changes[ flag ] = false;
+ delete this.flags[ flag ];
+ remove.push( className );
+ }
+ }
+ }
+ }
+
+ if ( this.$flagged ) {
+ this.$flagged
+ .addClass( add.join( ' ' ) )
+ .removeClass( remove.join( ' ' ) );
+ }
+
+ this.updateThemeClasses();
+ this.emit( 'flag', changes );
+
+ return this;
+};
+
+/**
+ * TitledElement is mixed into other classes to provide a `title` attribute.
+ * Titles are rendered by the browser and are made visible when the user moves
+ * the mouse over the element. Titles are not visible on touch devices.
+ *
+ * @example
+ * // TitledElement provides a 'title' attribute to the
+ * // ButtonWidget class
+ * var button = new OO.ui.ButtonWidget( {
+ * label: 'Button with Title',
+ * title: 'I am a button'
+ * } );
+ * $( 'body' ).append( button.$element );
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
+ * If this config is omitted, the title functionality is applied to $element, the
+ * element created by the class.
+ * @cfg {string|Function} [title] The title text or a function that returns text. If
+ * this config is omitted, the value of the {@link #static-title static title} property is used.
+ */
+OO.ui.mixin.TitledElement = function OoUiMixinTitledElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$titled = null;
+ this.title = null;
+
+ // Initialization
+ this.setTitle( config.title || this.constructor.static.title );
+ this.setTitledElement( config.$titled || this.$element );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.mixin.TitledElement );
+
+/* Static Properties */
+
+/**
+ * The title text, a function that returns text, or `null` for no title. The value of the static property
+ * is overridden if the #title config option is used.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function|null}
+ */
+OO.ui.mixin.TitledElement.static.title = null;
+
+/* Methods */
+
+/**
+ * Set the titled element.
+ *
+ * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
+ * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
+ *
+ * @param {jQuery} $titled Element that should use the 'titled' functionality
+ */
+OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
+ if ( this.$titled ) {
+ this.$titled.removeAttr( 'title' );
+ }
+
+ this.$titled = $titled;
+ if ( this.title ) {
+ this.$titled.attr( 'title', this.title );
+ }
+};
+
+/**
+ * Set title.
+ *
+ * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
+ * @chainable
+ */
+OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
+ title = typeof title === 'function' ? OO.ui.resolveMsg( title ) : title;
+ title = ( typeof title === 'string' && title.length ) ? title : null;
+
+ if ( this.title !== title ) {
+ if ( this.$titled ) {
+ if ( title !== null ) {
+ this.$titled.attr( 'title', title );
+ } else {
+ this.$titled.removeAttr( 'title' );
+ }
+ }
+ this.title = title;
+ }
+
+ return this;
+};
+
+/**
+ * Get title.
+ *
+ * @return {string} Title string
+ */
+OO.ui.mixin.TitledElement.prototype.getTitle = function () {
+ return this.title;
+};
+
+/**
+ * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
+ * Accesskeys allow an user to go to a specific element by using
+ * a shortcut combination of a browser specific keys + the key
+ * set to the field.
+ *
+ * @example
+ * // AccessKeyedElement provides an 'accesskey' attribute to the
+ * // ButtonWidget class
+ * var button = new OO.ui.ButtonWidget( {
+ * label: 'Button with Accesskey',
+ * accessKey: 'k'
+ * } );
+ * $( 'body' ).append( button.$element );
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
+ * If this config is omitted, the accesskey functionality is applied to $element, the
+ * element created by the class.
+ * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
+ * this config is omitted, no accesskey will be added.
+ */
+OO.ui.mixin.AccessKeyedElement = function OoUiMixinAccessKeyedElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$accessKeyed = null;
+ this.accessKey = null;
+
+ // Initialization
+ this.setAccessKey( config.accessKey || null );
+ this.setAccessKeyedElement( config.$accessKeyed || this.$element );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.mixin.AccessKeyedElement );
+
+/* Static Properties */
+
+/**
+ * The access key, a function that returns a key, or `null` for no accesskey.
+ *
+ * @static
+ * @inheritable
+ * @property {string|Function|null}
+ */
+OO.ui.mixin.AccessKeyedElement.static.accessKey = null;
+
+/* Methods */
+
+/**
+ * Set the accesskeyed element.
+ *
+ * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
+ * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
+ *
+ * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
+ */
+OO.ui.mixin.AccessKeyedElement.prototype.setAccessKeyedElement = function ( $accessKeyed ) {
+ if ( this.$accessKeyed ) {
+ this.$accessKeyed.removeAttr( 'accesskey' );
+ }
+
+ this.$accessKeyed = $accessKeyed;
+ if ( this.accessKey ) {
+ this.$accessKeyed.attr( 'accesskey', this.accessKey );
+ }
+};
+
+/**
+ * Set accesskey.
+ *
+ * @param {string|Function|null} accesskey Key, a function that returns a key, or `null` for no accesskey
+ * @chainable
+ */
+OO.ui.mixin.AccessKeyedElement.prototype.setAccessKey = function ( accessKey ) {
+ accessKey = typeof accessKey === 'string' ? OO.ui.resolveMsg( accessKey ) : null;
+
+ if ( this.accessKey !== accessKey ) {
+ if ( this.$accessKeyed ) {
+ if ( accessKey !== null ) {
+ this.$accessKeyed.attr( 'accesskey', accessKey );
+ } else {
+ this.$accessKeyed.removeAttr( 'accesskey' );
+ }
+ }
+ this.accessKey = accessKey;
+ }
+
+ return this;
+};
+
+/**
+ * Get accesskey.
+ *
+ * @return {string} accessKey string
+ */
+OO.ui.mixin.AccessKeyedElement.prototype.getAccessKey = function () {
+ return this.accessKey;
+};
+
+/**
+ * ButtonWidget is a generic widget for buttons. A wide variety of looks,
+ * feels, and functionality can be customized via the class’s configuration options
+ * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
+ * and examples.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
+ *
+ * @example
+ * // A button widget
+ * var button = new OO.ui.ButtonWidget( {
+ * label: 'Button with Icon',
+ * icon: 'remove',
+ * iconTitle: 'Remove'
+ * } );
+ * $( 'body' ).append( button.$element );
+ *
+ * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.ButtonElement
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.IndicatorElement
+ * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.TitledElement
+ * @mixins OO.ui.mixin.FlaggedElement
+ * @mixins OO.ui.mixin.TabIndexedElement
+ * @mixins OO.ui.mixin.AccessKeyedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [href] Hyperlink to visit when the button is clicked.
+ * @cfg {string} [target] The frame or window in which to open the hyperlink.
+ * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
+ */
+OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.ButtonWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.ButtonElement.call( this, config );
+ OO.ui.mixin.IconElement.call( this, config );
+ OO.ui.mixin.IndicatorElement.call( this, config );
+ OO.ui.mixin.LabelElement.call( this, config );
+ OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
+ OO.ui.mixin.FlaggedElement.call( this, config );
+ OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
+ OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$button } ) );
+
+ // Properties
+ this.href = null;
+ this.target = null;
+ this.noFollow = false;
+
+ // Events
+ this.connect( this, { disable: 'onDisable' } );
+
+ // Initialization
+ 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 );
+ this.setNoFollow( config.noFollow );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.ButtonElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IconElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IndicatorElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TitledElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
+OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.AccessKeyedElement );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
+ if ( !this.isDisabled() ) {
+ // Remove the tab-index while the button is down to prevent the button from stealing focus
+ this.$button.removeAttr( 'tabindex' );
+ }
+
+ return OO.ui.mixin.ButtonElement.prototype.onMouseDown.call( this, e );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
+ if ( !this.isDisabled() ) {
+ // Restore the tab-index after the button is up to restore the button's accessibility
+ this.$button.attr( 'tabindex', this.tabIndex );
+ }
+
+ return OO.ui.mixin.ButtonElement.prototype.onMouseUp.call( this, e );
+};
+
+/**
+ * Get hyperlink location.
+ *
+ * @return {string} Hyperlink location
+ */
+OO.ui.ButtonWidget.prototype.getHref = function () {
+ return this.href;
+};
+
+/**
+ * Get hyperlink target.
+ *
+ * @return {string} Hyperlink target
+ */
+OO.ui.ButtonWidget.prototype.getTarget = function () {
+ return this.target;
+};
+
+/**
+ * Get search engine traversal hint.
+ *
+ * @return {boolean} Whether search engines should avoid traversing this hyperlink
+ */
+OO.ui.ButtonWidget.prototype.getNoFollow = function () {
+ return this.noFollow;
+};
+
+/**
+ * Set hyperlink location.
+ *
+ * @param {string|null} href Hyperlink location, null to remove
+ */
+OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
+ href = typeof href === 'string' ? href : null;
+ if ( href !== null && !OO.ui.isSafeUrl( href ) ) {
+ href = './' + href;
+ }
+
+ if ( href !== this.href ) {
+ this.href = href;
+ this.updateHref();
+ }
+
+ return this;
+};
+
+/**
+ * Update the `href` attribute, in case of changes to href or
+ * disabled state.
+ *
+ * @private
+ * @chainable
+ */
+OO.ui.ButtonWidget.prototype.updateHref = function () {
+ if ( this.href !== null && !this.isDisabled() ) {
+ this.$button.attr( 'href', this.href );
+ } else {
+ this.$button.removeAttr( 'href' );
+ }
+
+ return this;
+};
+
+/**
+ * Handle disable events.
+ *
+ * @private
+ * @param {boolean} disabled Element is disabled
+ */
+OO.ui.ButtonWidget.prototype.onDisable = function () {
+ this.updateHref();
+};
+
+/**
+ * Set hyperlink target.
+ *
+ * @param {string|null} target Hyperlink target, null to remove
+ */
+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;
+};
+
+/**
+ * Set search engine traversal hint.
+ *
+ * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
+ */
+OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
+ noFollow = typeof noFollow === 'boolean' ? noFollow : true;
+
+ if ( noFollow !== this.noFollow ) {
+ this.noFollow = noFollow;
+ if ( noFollow ) {
+ this.$button.attr( 'rel', 'nofollow' );
+ } else {
+ this.$button.removeAttr( 'rel' );
+ }
+ }
+
+ return this;
+};
+
+/**
+ * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
+ * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
+ * removed, and cleared from the group.
+ *
+ * @example
+ * // Example: A ButtonGroupWidget with two buttons
+ * var button1 = new OO.ui.PopupButtonWidget( {
+ * label: 'Select a category',
+ * icon: 'menu',
+ * popup: {
+ * $content: $( '<p>List of categories...</p>' ),
+ * padded: true,
+ * align: 'left'
+ * }
+ * } );
+ * var button2 = new OO.ui.ButtonWidget( {
+ * label: 'Add item'
+ * });
+ * var buttonGroup = new OO.ui.ButtonGroupWidget( {
+ * items: [button1, button2]
+ * } );
+ * $( 'body' ).append( buttonGroup.$element );
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
+ */
+OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.ButtonGroupWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-buttonGroupWidget' );
+ if ( Array.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
+
+/**
+ * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
+ * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
+ * for a list of icons included in the library.
+ *
+ * @example
+ * // An icon widget with a label
+ * var myIcon = new OO.ui.IconWidget( {
+ * icon: 'help',
+ * iconTitle: 'Help'
+ * } );
+ * // Create a label.
+ * var iconLabel = new OO.ui.LabelWidget( {
+ * label: 'Help'
+ * } );
+ * $( 'body' ).append( myIcon.$element, iconLabel.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.TitledElement
+ * @mixins OO.ui.mixin.FlaggedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.IconWidget = function OoUiIconWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.IconWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
+ OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
+ OO.ui.mixin.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-iconWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.IconElement );
+OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.TitledElement );
+OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.FlaggedElement );
+
+/* Static Properties */
+
+OO.ui.IconWidget.static.tagName = 'span';
+
+/**
+ * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
+ * attention to the status of an item or to clarify the function of a control. For a list of
+ * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * @example
+ * // Example of an indicator widget
+ * var indicator1 = new OO.ui.IndicatorWidget( {
+ * indicator: 'alert'
+ * } );
+ *
+ * // Create a fieldset layout to add a label
+ * var fieldset = new OO.ui.FieldsetLayout();
+ * fieldset.addItems( [
+ * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
+ * ] );
+ * $( 'body' ).append( fieldset.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.IndicatorElement
+ * @mixins OO.ui.mixin.TitledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.IndicatorWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
+ OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-indicatorWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.IndicatorElement );
+OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.TitledElement );
+
+/* Static Properties */
+
+OO.ui.IndicatorWidget.static.tagName = 'span';
+
+/**
+ * LabelWidgets help identify the function of interface elements. Each LabelWidget can
+ * be configured with a `label` option that is set to a string, a label node, or a function:
+ *
+ * - String: a plaintext string
+ * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
+ * label that includes a link or special styling, such as a gray color or additional graphical elements.
+ * - Function: a function that will produce a string in the future. Functions are used
+ * in cases where the value of the label is not currently defined.
+ *
+ * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
+ * will come into focus when the label is clicked.
+ *
+ * @example
+ * // Examples of LabelWidgets
+ * var label1 = new OO.ui.LabelWidget( {
+ * label: 'plaintext label'
+ * } );
+ * var label2 = new OO.ui.LabelWidget( {
+ * label: $( '<a href="default.html">jQuery label</a>' )
+ * } );
+ * // Create a fieldset layout with fields for each example
+ * var fieldset = new OO.ui.FieldsetLayout();
+ * fieldset.addItems( [
+ * new OO.ui.FieldLayout( label1 ),
+ * new OO.ui.FieldLayout( label2 )
+ * ] );
+ * $( 'body' ).append( fieldset.$element );
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.LabelElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
+ * Clicking the label will focus the specified input field.
+ */
+OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.LabelWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
+ OO.ui.mixin.TitledElement.call( this, config );
+
+ // Properties
+ this.input = config.input;
+
+ // Events
+ if ( this.input instanceof OO.ui.InputWidget ) {
+ this.$element.on( 'click', this.onClick.bind( this ) );
+ }
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-labelWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement );
+
+/* Static Properties */
+
+OO.ui.LabelWidget.static.tagName = 'span';
+
+/* Methods */
+
+/**
+ * Handles label mouse click events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse click event
+ */
+OO.ui.LabelWidget.prototype.onClick = function () {
+ this.input.simulateLabelClick();
+ return false;
+};
+
+/**
+ * PendingElement is a mixin that is used to create elements that notify users that something is happening
+ * and that they should wait before proceeding. The pending state is visually represented with a pending
+ * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
+ * field of a {@link OO.ui.TextInputWidget text input widget}.
+ *
+ * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
+ * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
+ * in process dialogs.
+ *
+ * @example
+ * function MessageDialog( config ) {
+ * MessageDialog.parent.call( this, config );
+ * }
+ * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
+ *
+ * MessageDialog.static.actions = [
+ * { action: 'save', label: 'Done', flags: 'primary' },
+ * { label: 'Cancel', flags: 'safe' }
+ * ];
+ *
+ * MessageDialog.prototype.initialize = function () {
+ * MessageDialog.parent.prototype.initialize.apply( this, arguments );
+ * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
+ * this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending state. Note that action widgets can be marked pending in message dialogs but not process dialogs.</p>' );
+ * this.$body.append( this.content.$element );
+ * };
+ * MessageDialog.prototype.getBodyHeight = function () {
+ * return 100;
+ * }
+ * MessageDialog.prototype.getActionProcess = function ( action ) {
+ * var dialog = this;
+ * if ( action === 'save' ) {
+ * dialog.getActions().get({actions: 'save'})[0].pushPending();
+ * return new OO.ui.Process()
+ * .next( 1000 )
+ * .next( function () {
+ * dialog.getActions().get({actions: 'save'})[0].popPending();
+ * } );
+ * }
+ * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
+ * };
+ *
+ * var windowManager = new OO.ui.WindowManager();
+ * $( 'body' ).append( windowManager.$element );
+ *
+ * var dialog = new MessageDialog();
+ * windowManager.addWindows( [ dialog ] );
+ * windowManager.openWindow( dialog );
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
+ */
+OO.ui.mixin.PendingElement = function OoUiMixinPendingElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.pending = 0;
+ this.$pending = null;
+
+ // Initialisation
+ this.setPendingElement( config.$pending || this.$element );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.mixin.PendingElement );
+
+/* Methods */
+
+/**
+ * Set the pending element (and clean up any existing one).
+ *
+ * @param {jQuery} $pending The element to set to pending.
+ */
+OO.ui.mixin.PendingElement.prototype.setPendingElement = function ( $pending ) {
+ if ( this.$pending ) {
+ this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
+ }
+
+ this.$pending = $pending;
+ if ( this.pending > 0 ) {
+ this.$pending.addClass( 'oo-ui-pendingElement-pending' );
+ }
+};
+
+/**
+ * Check if an element is pending.
+ *
+ * @return {boolean} Element is pending
+ */
+OO.ui.mixin.PendingElement.prototype.isPending = function () {
+ return !!this.pending;
+};
+
+/**
+ * Increase the pending counter. The pending state will remain active until the counter is zero
+ * (i.e., the number of calls to #pushPending and #popPending is the same).
+ *
+ * @chainable
+ */
+OO.ui.mixin.PendingElement.prototype.pushPending = function () {
+ if ( this.pending === 0 ) {
+ this.$pending.addClass( 'oo-ui-pendingElement-pending' );
+ this.updateThemeClasses();
+ }
+ this.pending++;
+
+ return this;
+};
+
+/**
+ * Decrease the pending counter. The pending state will remain active until the counter is zero
+ * (i.e., the number of calls to #pushPending and #popPending is the same).
+ *
+ * @chainable
+ */
+OO.ui.mixin.PendingElement.prototype.popPending = function () {
+ if ( this.pending === 1 ) {
+ this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
+ this.updateThemeClasses();
+ }
+ this.pending = Math.max( 0, this.pending - 1 );
+
+ return this;
+};
+
+/**
+ * Element that can be automatically clipped to visible boundaries.
+ *
+ * Whenever the element's natural height changes, you have to call
+ * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
+ * clipping correctly.
+ *
+ * The dimensions of #$clippableContainer will be compared to the boundaries of the
+ * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
+ * then #$clippable will be given a fixed reduced height and/or width and will be made
+ * scrollable. By default, #$clippable and #$clippableContainer are the same element,
+ * but you can build a static footer by setting #$clippableContainer to an element that contains
+ * #$clippable and the footer.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
+ * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
+ * omit to use #$clippable
+ */
+OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$clippable = null;
+ this.$clippableContainer = null;
+ this.clipping = false;
+ this.clippedHorizontally = false;
+ this.clippedVertically = false;
+ this.$clippableScrollableContainer = null;
+ this.$clippableScroller = null;
+ this.$clippableWindow = null;
+ this.idealWidth = null;
+ this.idealHeight = null;
+ this.onClippableScrollHandler = this.clip.bind( this );
+ this.onClippableWindowResizeHandler = this.clip.bind( this );
+
+ // Initialization
+ if ( config.$clippableContainer ) {
+ this.setClippableContainer( config.$clippableContainer );
+ }
+ this.setClippableElement( config.$clippable || this.$element );
+};
+
+/* Methods */
+
+/**
+ * Set clippable element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $clippable Element to make clippable
+ */
+OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
+ if ( this.$clippable ) {
+ this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
+ this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
+ OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
+ }
+
+ this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
+ this.clip();
+};
+
+/**
+ * Set clippable container.
+ *
+ * This is the container that will be measured when deciding whether to clip. When clipping,
+ * #$clippable will be resized in order to keep the clippable container fully visible.
+ *
+ * If the clippable container is unset, #$clippable will be used.
+ *
+ * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
+ */
+OO.ui.mixin.ClippableElement.prototype.setClippableContainer = function ( $clippableContainer ) {
+ this.$clippableContainer = $clippableContainer;
+ if ( this.$clippable ) {
+ this.clip();
+ }
+};
+
+/**
+ * Toggle clipping.
+ *
+ * Do not turn clipping on until after the element is attached to the DOM and visible.
+ *
+ * @param {boolean} [clipping] Enable clipping, omit to toggle
+ * @chainable
+ */
+OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
+ clipping = clipping === undefined ? !this.clipping : !!clipping;
+
+ if ( this.clipping !== clipping ) {
+ this.clipping = clipping;
+ if ( clipping ) {
+ this.$clippableScrollableContainer = $( this.getClosestScrollableElementContainer() );
+ // If the clippable container is the root, we have to listen to scroll events and check
+ // jQuery.scrollTop on the window because of browser inconsistencies
+ this.$clippableScroller = this.$clippableScrollableContainer.is( 'html, body' ) ?
+ $( OO.ui.Element.static.getWindow( this.$clippableScrollableContainer ) ) :
+ this.$clippableScrollableContainer;
+ this.$clippableScroller.on( 'scroll', this.onClippableScrollHandler );
+ this.$clippableWindow = $( this.getElementWindow() )
+ .on( 'resize', this.onClippableWindowResizeHandler );
+ // Initial clip after visible
+ this.clip();
+ } else {
+ this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
+ OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
+
+ this.$clippableScrollableContainer = null;
+ this.$clippableScroller.off( 'scroll', this.onClippableScrollHandler );
+ this.$clippableScroller = null;
+ this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
+ this.$clippableWindow = null;
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
+ *
+ * @return {boolean} Element will be clipped to the visible area
+ */
+OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
+ return this.clipping;
+};
+
+/**
+ * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
+ *
+ * @return {boolean} Part of the element is being clipped
+ */
+OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
+ return this.clippedHorizontally || this.clippedVertically;
+};
+
+/**
+ * Check if the right of the element is being clipped by the nearest scrollable container.
+ *
+ * @return {boolean} Part of the element is being clipped
+ */
+OO.ui.mixin.ClippableElement.prototype.isClippedHorizontally = function () {
+ return this.clippedHorizontally;
+};
+
+/**
+ * Check if the bottom of the element is being clipped by the nearest scrollable container.
+ *
+ * @return {boolean} Part of the element is being clipped
+ */
+OO.ui.mixin.ClippableElement.prototype.isClippedVertically = function () {
+ return this.clippedVertically;
+};
+
+/**
+ * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
+ *
+ * @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.mixin.ClippableElement.prototype.setIdealSize = function ( width, height ) {
+ this.idealWidth = width;
+ this.idealHeight = height;
+
+ if ( !this.clipping ) {
+ // Update dimensions
+ this.$clippable.css( { width: width, height: height } );
+ }
+ // While clipping, idealWidth and idealHeight are not considered
+};
+
+/**
+ * Clip element to visible boundaries and allow scrolling when needed. Call this method when
+ * the element's natural height changes.
+ *
+ * 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.
+ *
+ * @chainable
+ */
+OO.ui.mixin.ClippableElement.prototype.clip = function () {
+ var $container, extraHeight, extraWidth, ccOffset,
+ $scrollableContainer, scOffset, scHeight, scWidth,
+ ccWidth, scrollerIsWindow, scrollTop, scrollLeft,
+ desiredWidth, desiredHeight, allotedWidth, allotedHeight,
+ naturalWidth, naturalHeight, clipWidth, clipHeight,
+ buffer = 7; // Chosen by fair dice roll
+
+ if ( !this.clipping ) {
+ // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
+ return this;
+ }
+
+ $container = this.$clippableContainer || this.$clippable;
+ extraHeight = $container.outerHeight() - this.$clippable.outerHeight();
+ extraWidth = $container.outerWidth() - this.$clippable.outerWidth();
+ ccOffset = $container.offset();
+ $scrollableContainer = this.$clippableScrollableContainer.is( 'html, body' ) ?
+ this.$clippableWindow : this.$clippableScrollableContainer;
+ scOffset = $scrollableContainer.offset() || { top: 0, left: 0 };
+ scHeight = $scrollableContainer.innerHeight() - buffer;
+ scWidth = $scrollableContainer.innerWidth() - buffer;
+ ccWidth = $container.outerWidth() + buffer;
+ scrollerIsWindow = this.$clippableScroller[ 0 ] === this.$clippableWindow[ 0 ];
+ scrollTop = scrollerIsWindow ? this.$clippableScroller.scrollTop() : 0;
+ scrollLeft = scrollerIsWindow ? this.$clippableScroller.scrollLeft() : 0;
+ desiredWidth = ccOffset.left < 0 ?
+ ccWidth + ccOffset.left :
+ ( scOffset.left + scrollLeft + scWidth ) - ccOffset.left;
+ desiredHeight = ( scOffset.top + scrollTop + scHeight ) - ccOffset.top;
+ allotedWidth = Math.ceil( desiredWidth - extraWidth );
+ allotedHeight = Math.ceil( desiredHeight - extraHeight );
+ naturalWidth = this.$clippable.prop( 'scrollWidth' );
+ naturalHeight = this.$clippable.prop( 'scrollHeight' );
+ clipWidth = allotedWidth < naturalWidth;
+ clipHeight = allotedHeight < naturalHeight;
+
+ if ( clipWidth ) {
+ this.$clippable.css( { overflowX: 'scroll', width: Math.max( 0, allotedWidth ) } );
+ } else {
+ this.$clippable.css( { width: this.idealWidth ? this.idealWidth - extraWidth : '', overflowX: '' } );
+ }
+ if ( clipHeight ) {
+ this.$clippable.css( { overflowY: 'scroll', height: Math.max( 0, allotedHeight ) } );
+ } else {
+ this.$clippable.css( { height: this.idealHeight ? this.idealHeight - extraHeight : '', overflowY: '' } );
+ }
+
+ // If we stopped clipping in at least one of the dimensions
+ if ( ( this.clippedHorizontally && !clipWidth ) || ( this.clippedVertically && !clipHeight ) ) {
+ OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
+ }
+
+ this.clippedHorizontally = clipWidth;
+ this.clippedVertically = clipHeight;
+
+ return this;
+};
+
+/**
+ * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
+ * By default, each popup has an anchor that points toward its origin.
+ * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
+ *
+ * @example
+ * // A popup widget.
+ * var popup = new OO.ui.PopupWidget( {
+ * $content: $( '<p>Hi there!</p>' ),
+ * padded: true,
+ * width: 300
+ * } );
+ *
+ * $( 'body' ).append( popup.$element );
+ * // To display the popup, toggle the visibility to 'true'.
+ * popup.toggle( true );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.ClippableElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {number} [width=320] Width of popup in pixels
+ * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
+ * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
+ * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
+ * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
+ * popup is leaning towards the right of the screen.
+ * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
+ * in the given language, which means it will flip to the correct positioning in right-to-left languages.
+ * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
+ * sentence in the given language.
+ * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
+ * See the [OOjs UI docs on MediaWiki][3] for an example.
+ * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
+ * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
+ * @cfg {jQuery} [$content] Content to append to the popup's body
+ * @cfg {jQuery} [$footer] Content to append to the popup's footer
+ * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
+ * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
+ * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
+ * for an example.
+ * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
+ * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
+ * button.
+ * @cfg {boolean} [padded] Add padding to the popup's body
+ */
+OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.PopupWidget.parent.call( this, config );
+
+ // Properties (must be set before ClippableElement constructor call)
+ this.$body = $( '<div>' );
+ this.$popup = $( '<div>' );
+
+ // Mixin constructors
+ OO.ui.mixin.LabelElement.call( this, config );
+ OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, {
+ $clippable: this.$body,
+ $clippableContainer: this.$popup
+ } ) );
+
+ // Properties
+ this.$head = $( '<div>' );
+ this.$footer = $( '<div>' );
+ this.$anchor = $( '<div>' );
+ // If undefined, will be computed lazily in updateDimensions()
+ this.$container = config.$container;
+ this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
+ this.autoClose = !!config.autoClose;
+ this.$autoCloseIgnore = config.$autoCloseIgnore;
+ this.transitionTimeout = null;
+ this.anchor = null;
+ this.width = config.width !== undefined ? config.width : 320;
+ this.height = config.height !== undefined ? config.height : null;
+ this.setAlignment( config.align );
+ this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
+ this.onMouseDownHandler = this.onMouseDown.bind( this );
+ this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
+
+ // Events
+ this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
+
+ // Initialization
+ this.toggleAnchor( config.anchor === undefined || config.anchor );
+ 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 );
+ this.$footer.addClass( 'oo-ui-popupWidget-footer' );
+ if ( !config.head ) {
+ this.$head.addClass( 'oo-ui-element-hidden' );
+ }
+ if ( !config.$footer ) {
+ this.$footer.addClass( 'oo-ui-element-hidden' );
+ }
+ this.$popup
+ .addClass( 'oo-ui-popupWidget-popup' )
+ .append( this.$head, this.$body, this.$footer );
+ this.$element
+ .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.$footer instanceof jQuery ) {
+ this.$footer.append( config.$footer );
+ }
+ if ( config.padded ) {
+ this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
+ }
+
+ // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
+ // that reference properties not initialized at that time of parent class construction
+ // TODO: Find a better way to handle post-constructor setup
+ this.visible = false;
+ this.$element.addClass( 'oo-ui-element-hidden' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
+
+/* Methods */
+
+/**
+ * Handles mouse down events.
+ *
+ * @private
+ * @param {MouseEvent} 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.
+ *
+ * @private
+ */
+OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
+ // Capture clicks outside popup
+ this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
+};
+
+/**
+ * Handles close button click events.
+ *
+ * @private
+ */
+OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
+ if ( this.isVisible() ) {
+ this.toggle( false );
+ }
+};
+
+/**
+ * Unbind mouse down listener.
+ *
+ * @private
+ */
+OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
+ this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
+};
+
+/**
+ * Handles key down events.
+ *
+ * @private
+ * @param {KeyboardEvent} e Key down event
+ */
+OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
+ if (
+ e.which === OO.ui.Keys.ESCAPE &&
+ this.isVisible()
+ ) {
+ this.toggle( false );
+ e.preventDefault();
+ e.stopPropagation();
+ }
+};
+
+/**
+ * Bind key down listener.
+ *
+ * @private
+ */
+OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
+ this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
+};
+
+/**
+ * Unbind key down listener.
+ *
+ * @private
+ */
+OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
+ this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
+};
+
+/**
+ * Show, hide, or toggle the visibility of the anchor.
+ *
+ * @param {boolean} [show] Show anchor, omit to toggle
+ */
+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;
+ }
+};
+
+/**
+ * Check if the anchor is visible.
+ *
+ * @return {boolean} Anchor is visible
+ */
+OO.ui.PopupWidget.prototype.hasAnchor = function () {
+ return this.anchor;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.PopupWidget.prototype.toggle = function ( show ) {
+ var change;
+ show = show === undefined ? !this.isVisible() : !!show;
+
+ change = show !== this.isVisible();
+
+ // Parent method
+ OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
+
+ if ( change ) {
+ if ( show ) {
+ if ( this.autoClose ) {
+ this.bindMouseDownListener();
+ this.bindKeyDownListener();
+ }
+ this.updateDimensions();
+ this.toggleClipping( true );
+ } else {
+ this.toggleClipping( false );
+ if ( this.autoClose ) {
+ this.unbindMouseDownListener();
+ this.unbindKeyDownListener();
+ }
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Set the size of the popup.
+ *
+ * Changing the size may also change the popup's position depending on the alignment.
+ *
+ * @param {number} width Width in pixels
+ * @param {number} height Height in pixels
+ * @param {boolean} [transition=false] Use a smooth transition
+ * @chainable
+ */
+OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
+ this.width = width;
+ this.height = height !== undefined ? height : null;
+ if ( this.isVisible() ) {
+ this.updateDimensions( transition );
+ }
+};
+
+/**
+ * Update the size and position.
+ *
+ * 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.PopupWidget.prototype.updateDimensions = function ( transition ) {
+ var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
+ popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
+ align = this.align,
+ widget = this;
+
+ if ( !this.$container ) {
+ // Lazy-initialize $container if not specified in constructor
+ this.$container = $( this.getClosestScrollableElementContainer() );
+ }
+
+ // Set height and width before measuring things, since it might cause our measurements
+ // to change (e.g. due to scrollbars appearing or disappearing)
+ this.$popup.css( {
+ width: this.width,
+ height: this.height !== null ? this.height : 'auto'
+ } );
+
+ // If we are in RTL, we need to flip the alignment, unless it is center
+ if ( align === 'forwards' || align === 'backwards' ) {
+ if ( this.$container.css( 'direction' ) === 'rtl' ) {
+ align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
+ } else {
+ align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
+ }
+
+ }
+
+ // Compute initial popupOffset based on alignment
+ popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
+
+ // Figure out if this will cause the popup to go beyond the edge of the container
+ originOffset = this.$element.offset().left;
+ containerLeft = this.$container.offset().left;
+ containerWidth = this.$container.innerWidth();
+ containerRight = containerLeft + containerWidth;
+ popupLeft = popupOffset - this.containerPadding;
+ popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
+ overlapLeft = ( originOffset + popupLeft ) - containerLeft;
+ overlapRight = containerRight - ( originOffset + popupRight );
+
+ // Adjust offset to make the popup not go beyond the edge, if needed
+ if ( overlapRight < 0 ) {
+ popupOffset += overlapRight;
+ } else if ( overlapLeft < 0 ) {
+ popupOffset -= overlapLeft;
+ }
+
+ // Adjust offset to avoid anchor being rendered too close to the edge
+ // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
+ // TODO: Find a measurement that works for CSS anchors and image anchors
+ anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
+ if ( popupOffset + this.width < anchorWidth ) {
+ popupOffset = anchorWidth - this.width;
+ } else if ( -popupOffset < anchorWidth ) {
+ popupOffset = -anchorWidth;
+ }
+
+ // Prevent transition from being interrupted
+ clearTimeout( this.transitionTimeout );
+ if ( transition ) {
+ // Enable transition
+ this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
+ }
+
+ // Position body relative to anchor
+ this.$popup.css( 'margin-left', popupOffset );
+
+ 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' );
+ }
+
+ // Reevaluate clipping state since we've relocated and resized the popup
+ this.clip();
+
+ return this;
+};
+
+/**
+ * Set popup alignment
+ * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
+ * `backwards` or `forwards`.
+ */
+OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
+ // Validate alignment and transform deprecated values
+ if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
+ this.align = { left: 'force-right', right: 'force-left' }[ align ] || align;
+ } else {
+ this.align = 'center';
+ }
+};
+
+/**
+ * Get popup alignment
+ * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
+ * `backwards` or `forwards`.
+ */
+OO.ui.PopupWidget.prototype.getAlignment = function () {
+ return this.align;
+};
+
+/**
+ * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
+ * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
+ * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
+ * See {@link OO.ui.PopupWidget PopupWidget} for an example.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object} [popup] Configuration to pass to popup
+ * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
+ */
+OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.popup = new OO.ui.PopupWidget( $.extend(
+ { autoClose: true },
+ config.popup,
+ { $autoCloseIgnore: this.$element }
+ ) );
+};
+
+/* Methods */
+
+/**
+ * Get popup.
+ *
+ * @return {OO.ui.PopupWidget} Popup widget
+ */
+OO.ui.mixin.PopupElement.prototype.getPopup = function () {
+ return this.popup;
+};
+
+/**
+ * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
+ * which is used to display additional information or options.
+ *
+ * @example
+ * // Example of a popup button.
+ * var popupButton = new OO.ui.PopupButtonWidget( {
+ * label: 'Popup button with options',
+ * icon: 'menu',
+ * popup: {
+ * $content: $( '<p>Additional options here.</p>' ),
+ * padded: true,
+ * align: 'force-left'
+ * }
+ * } );
+ * // Append the button to the DOM.
+ * $( 'body' ).append( popupButton.$element );
+ *
+ * @class
+ * @extends OO.ui.ButtonWidget
+ * @mixins OO.ui.mixin.PopupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
+ // Parent constructor
+ OO.ui.PopupButtonWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.PopupElement.call( this, config );
+
+ // Events
+ this.connect( this, { click: 'onAction' } );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-popupButtonWidget' )
+ .attr( 'aria-haspopup', 'true' )
+ .append( this.popup.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
+OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.mixin.PopupElement );
+
+/* Methods */
+
+/**
+ * Handle the button action being triggered.
+ *
+ * @private
+ */
+OO.ui.PopupButtonWidget.prototype.onAction = function () {
+ this.popup.toggle();
+};
+
+/**
+ * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
+ *
+ * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
+ *
+ * @private
+ * @abstract
+ * @class
+ * @extends OO.ui.mixin.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.mixin.GroupWidget = function OoUiMixinGroupWidget( config ) {
+ // Parent constructor
+ OO.ui.mixin.GroupWidget.parent.call( this, config );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.mixin.GroupWidget, OO.ui.mixin.GroupElement );
+
+/* Methods */
+
+/**
+ * Set the disabled state of the widget.
+ *
+ * This will also update the disabled state of child widgets.
+ *
+ * @param {boolean} disabled Disable widget
+ * @chainable
+ */
+OO.ui.mixin.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.mixin.GroupElement constructor
+ if ( this.items ) {
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ this.items[ i ].updateDisabled();
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
+ *
+ * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
+ * allows bidirectional communication.
+ *
+ * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
+ *
+ * @private
+ * @abstract
+ * @class
+ *
+ * @constructor
+ */
+OO.ui.mixin.ItemWidget = function OoUiMixinItemWidget() {
+ //
+};
+
+/* Methods */
+
+/**
+ * Check if widget is disabled.
+ *
+ * Checks parent if present, making disabled state inheritable.
+ *
+ * @return {boolean} Widget is disabled
+ */
+OO.ui.mixin.ItemWidget.prototype.isDisabled = function () {
+ return this.disabled ||
+ ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
+};
+
+/**
+ * Set group element is in.
+ *
+ * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
+ * @chainable
+ */
+OO.ui.mixin.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();
+
+ return this;
+};
+
+/**
+ * OptionWidgets are special elements that can be selected and configured with data. The
+ * data is often unique for each option, but it does not have to be. OptionWidgets are used
+ * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
+ * and examples, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.FlaggedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.OptionWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.ItemWidget.call( this );
+ OO.ui.mixin.LabelElement.call( this, config );
+ OO.ui.mixin.FlaggedElement.call( this, config );
+
+ // Properties
+ this.selected = false;
+ this.highlighted = false;
+ this.pressed = false;
+
+ // Initialization
+ this.$element
+ .data( 'oo-ui-optionWidget', this )
+ .attr( 'role', 'option' )
+ .attr( 'aria-selected', 'false' )
+ .addClass( 'oo-ui-optionWidget' )
+ .append( this.$label );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
+
+/* Static Properties */
+
+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 */
+
+/**
+ * Check if the option can be selected.
+ *
+ * @return {boolean} Item is selectable
+ */
+OO.ui.OptionWidget.prototype.isSelectable = function () {
+ return this.constructor.static.selectable && !this.isDisabled() && this.isVisible();
+};
+
+/**
+ * Check if the option can be highlighted. A highlight indicates that the option
+ * may be selected when a user presses enter or clicks. Disabled items cannot
+ * be highlighted.
+ *
+ * @return {boolean} Item is highlightable
+ */
+OO.ui.OptionWidget.prototype.isHighlightable = function () {
+ return this.constructor.static.highlightable && !this.isDisabled() && this.isVisible();
+};
+
+/**
+ * Check if the option can be pressed. The pressed state occurs when a user mouses
+ * down on an item, but has not yet let go of the mouse.
+ *
+ * @return {boolean} Item is pressable
+ */
+OO.ui.OptionWidget.prototype.isPressable = function () {
+ return this.constructor.static.pressable && !this.isDisabled() && this.isVisible();
+};
+
+/**
+ * Check if the option is selected.
+ *
+ * @return {boolean} Item is selected
+ */
+OO.ui.OptionWidget.prototype.isSelected = function () {
+ return this.selected;
+};
+
+/**
+ * Check if the option is highlighted. A highlight indicates that the
+ * item may be selected when a user presses enter or clicks.
+ *
+ * @return {boolean} Item is highlighted
+ */
+OO.ui.OptionWidget.prototype.isHighlighted = function () {
+ return this.highlighted;
+};
+
+/**
+ * Check if the option is pressed. The pressed state occurs when a user mouses
+ * down on an item, but has not yet let go of the mouse. The item may appear
+ * selected, but it will not be selected until the user releases the mouse.
+ *
+ * @return {boolean} Item is pressed
+ */
+OO.ui.OptionWidget.prototype.isPressed = function () {
+ return this.pressed;
+};
+
+/**
+ * Set the option’s selected state. In general, all modifications to the selection
+ * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
+ * method instead of this method.
+ *
+ * @param {boolean} [state=false] Select option
+ * @chainable
+ */
+OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
+ if ( this.constructor.static.selectable ) {
+ this.selected = !!state;
+ this.$element
+ .toggleClass( 'oo-ui-optionWidget-selected', state )
+ .attr( 'aria-selected', state.toString() );
+ if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
+ this.scrollElementIntoView();
+ }
+ this.updateThemeClasses();
+ }
+ return this;
+};
+
+/**
+ * Set the option’s highlighted state. In general, all programmatic
+ * modifications to the highlight should be handled by the
+ * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
+ * method instead of this method.
+ *
+ * @param {boolean} [state=false] Highlight option
+ * @chainable
+ */
+OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
+ if ( this.constructor.static.highlightable ) {
+ this.highlighted = !!state;
+ this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
+ this.updateThemeClasses();
+ }
+ return this;
+};
+
+/**
+ * Set the option’s pressed state. In general, all
+ * programmatic modifications to the pressed state should be handled by the
+ * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
+ * method instead of this method.
+ *
+ * @param {boolean} [state=false] Press option
+ * @chainable
+ */
+OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
+ if ( this.constructor.static.pressable ) {
+ this.pressed = !!state;
+ this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
+ this.updateThemeClasses();
+ }
+ return this;
+};
+
+/**
+ * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
+ * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
+ * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
+ * menu selects}.
+ *
+ * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
+ * information, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * @example
+ * // Example of a select widget with three options
+ * var select = new OO.ui.SelectWidget( {
+ * items: [
+ * new OO.ui.OptionWidget( {
+ * data: 'a',
+ * label: 'Option One',
+ * } ),
+ * new OO.ui.OptionWidget( {
+ * data: 'b',
+ * label: 'Option Two',
+ * } ),
+ * new OO.ui.OptionWidget( {
+ * data: 'c',
+ * label: 'Option Three',
+ * } )
+ * ]
+ * } );
+ * $( 'body' ).append( select.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.GroupWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
+ * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
+ * the [OOjs UI documentation on MediaWiki] [2] for examples.
+ * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
+ */
+OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.SelectWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
+
+ // Properties
+ this.pressed = false;
+ this.selecting = null;
+ this.onMouseUpHandler = this.onMouseUp.bind( this );
+ this.onMouseMoveHandler = this.onMouseMove.bind( this );
+ this.onKeyDownHandler = this.onKeyDown.bind( this );
+ this.onKeyPressHandler = this.onKeyPress.bind( this );
+ this.keyPressBuffer = '';
+ this.keyPressBufferTimer = null;
+
+ // Events
+ this.connect( this, {
+ toggle: 'onToggle'
+ } );
+ this.$element.on( {
+ mousedown: this.onMouseDown.bind( this ),
+ mouseover: this.onMouseOver.bind( this ),
+ mouseleave: this.onMouseLeave.bind( this )
+ } );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
+ .attr( 'role', 'listbox' );
+ if ( Array.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
+
+// Need to mixin base class as well
+OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupElement );
+OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
+
+/* Static */
+OO.ui.SelectWidget.static.passAllFilter = function () {
+ return true;
+};
+
+/* Events */
+
+/**
+ * @event highlight
+ *
+ * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
+ *
+ * @param {OO.ui.OptionWidget|null} item Highlighted item
+ */
+
+/**
+ * @event press
+ *
+ * A `press` event is emitted when the #pressItem method is used to programmatically modify the
+ * pressed state of an option.
+ *
+ * @param {OO.ui.OptionWidget|null} item Pressed item
+ */
+
+/**
+ * @event select
+ *
+ * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
+ *
+ * @param {OO.ui.OptionWidget|null} item Selected item
+ */
+
+/**
+ * @event choose
+ * A `choose` event is emitted when an item is chosen with the #chooseItem method.
+ * @param {OO.ui.OptionWidget} item Chosen item
+ */
+
+/**
+ * @event add
+ *
+ * An `add` event is emitted when options are added to the select with the #addItems method.
+ *
+ * @param {OO.ui.OptionWidget[]} items Added items
+ * @param {number} index Index of insertion point
+ */
+
+/**
+ * @event remove
+ *
+ * A `remove` event is emitted when options are removed from the select with the #clearItems
+ * or #removeItems methods.
+ *
+ * @param {OO.ui.OptionWidget[]} items Removed items
+ */
+
+/* Methods */
+
+/**
+ * Handle mouse down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
+ var item;
+
+ if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
+ 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;
+};
+
+/**
+ * Handle mouse up events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse up event
+ */
+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 === OO.ui.MouseButtons.LEFT && 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;
+};
+
+/**
+ * Handle mouse move events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse move event
+ */
+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;
+};
+
+/**
+ * Handle mouse over events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse over event
+ */
+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;
+};
+
+/**
+ * Handle mouse leave events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse over event
+ */
+OO.ui.SelectWidget.prototype.onMouseLeave = function () {
+ if ( !this.isDisabled() ) {
+ this.highlightItem( null );
+ }
+ return false;
+};
+
+/**
+ * Handle key down events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
+ var nextItem,
+ handled = false,
+ currentItem = this.getHighlightedItem() || this.getSelectedItem();
+
+ if ( !this.isDisabled() && this.isVisible() ) {
+ switch ( e.keyCode ) {
+ case OO.ui.Keys.ENTER:
+ if ( currentItem && currentItem.constructor.static.highlightable ) {
+ // Was only highlighted, now let's select it. No-op if already selected.
+ this.chooseItem( currentItem );
+ handled = true;
+ }
+ break;
+ case OO.ui.Keys.UP:
+ case OO.ui.Keys.LEFT:
+ this.clearKeyPressBuffer();
+ nextItem = this.getRelativeSelectableItem( currentItem, -1 );
+ handled = true;
+ break;
+ case OO.ui.Keys.DOWN:
+ case OO.ui.Keys.RIGHT:
+ this.clearKeyPressBuffer();
+ nextItem = this.getRelativeSelectableItem( currentItem, 1 );
+ handled = true;
+ break;
+ case OO.ui.Keys.ESCAPE:
+ case OO.ui.Keys.TAB:
+ if ( currentItem && currentItem.constructor.static.highlightable ) {
+ currentItem.setHighlighted( false );
+ }
+ this.unbindKeyDownListener();
+ this.unbindKeyPressListener();
+ // Don't prevent tabbing away / defocusing
+ handled = false;
+ break;
+ }
+
+ if ( nextItem ) {
+ if ( nextItem.constructor.static.highlightable ) {
+ this.highlightItem( nextItem );
+ } else {
+ this.chooseItem( nextItem );
+ }
+ nextItem.scrollElementIntoView();
+ }
+
+ if ( handled ) {
+ // Can't just return false, because e is not always a jQuery event
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+};
+
+/**
+ * Bind key down listener.
+ *
+ * @protected
+ */
+OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
+ this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
+};
+
+/**
+ * Unbind key down listener.
+ *
+ * @protected
+ */
+OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
+ this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
+};
+
+/**
+ * Clear the key-press buffer
+ *
+ * @protected
+ */
+OO.ui.SelectWidget.prototype.clearKeyPressBuffer = function () {
+ if ( this.keyPressBufferTimer ) {
+ clearTimeout( this.keyPressBufferTimer );
+ this.keyPressBufferTimer = null;
+ }
+ this.keyPressBuffer = '';
+};
+
+/**
+ * Handle key press events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key press event
+ */
+OO.ui.SelectWidget.prototype.onKeyPress = function ( e ) {
+ var c, filter, item;
+
+ if ( !e.charCode ) {
+ if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.keyPressBuffer !== '' ) {
+ this.keyPressBuffer = this.keyPressBuffer.substr( 0, this.keyPressBuffer.length - 1 );
+ return false;
+ }
+ return;
+ }
+ if ( String.fromCodePoint ) {
+ c = String.fromCodePoint( e.charCode );
+ } else {
+ c = String.fromCharCode( e.charCode );
+ }
+
+ if ( this.keyPressBufferTimer ) {
+ clearTimeout( this.keyPressBufferTimer );
+ }
+ this.keyPressBufferTimer = setTimeout( this.clearKeyPressBuffer.bind( this ), 1500 );
+
+ item = this.getHighlightedItem() || this.getSelectedItem();
+
+ if ( this.keyPressBuffer === c ) {
+ // Common (if weird) special case: typing "xxxx" will cycle through all
+ // the items beginning with "x".
+ if ( item ) {
+ item = this.getRelativeSelectableItem( item, 1 );
+ }
+ } else {
+ this.keyPressBuffer += c;
+ }
+
+ filter = this.getItemMatcher( this.keyPressBuffer, false );
+ if ( !item || !filter( item ) ) {
+ item = this.getRelativeSelectableItem( item, 1, filter );
+ }
+ if ( item ) {
+ if ( item.constructor.static.highlightable ) {
+ this.highlightItem( item );
+ } else {
+ this.chooseItem( item );
+ }
+ item.scrollElementIntoView();
+ }
+
+ return false;
+};
+
+/**
+ * Get a matcher for the specific string
+ *
+ * @protected
+ * @param {string} s String to match against items
+ * @param {boolean} [exact=false] Only accept exact matches
+ * @return {Function} function ( OO.ui.OptionItem ) => boolean
+ */
+OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
+ var re;
+
+ if ( s.normalize ) {
+ s = s.normalize();
+ }
+ s = exact ? s.trim() : s.replace( /^\s+/, '' );
+ re = '^\\s*' + s.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
+ if ( exact ) {
+ re += '\\s*$';
+ }
+ re = new RegExp( re, 'i' );
+ return function ( item ) {
+ var l = item.getLabel();
+ if ( typeof l !== 'string' ) {
+ l = item.$label.text();
+ }
+ if ( l.normalize ) {
+ l = l.normalize();
+ }
+ return re.test( l );
+ };
+};
+
+/**
+ * Bind key press listener.
+ *
+ * @protected
+ */
+OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
+ this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler, true );
+};
+
+/**
+ * Unbind key down listener.
+ *
+ * If you override this, be sure to call this.clearKeyPressBuffer() from your
+ * implementation.
+ *
+ * @protected
+ */
+OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
+ this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler, true );
+ this.clearKeyPressBuffer();
+};
+
+/**
+ * Visibility change handler
+ *
+ * @protected
+ * @param {boolean} visible
+ */
+OO.ui.SelectWidget.prototype.onToggle = function ( visible ) {
+ if ( !visible ) {
+ this.clearKeyPressBuffer();
+ }
+};
+
+/**
+ * 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
+ */
+OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
+ return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || 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;
+};
+
+/**
+ * Toggle pressed state.
+ *
+ * Press is a state that occurs when a user mouses down on an item, but
+ * has not yet let go of the mouse. The item may appear selected, but it will not be selected
+ * until the user releases the mouse.
+ *
+ * @param {boolean} pressed An option is being pressed
+ */
+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;
+ }
+};
+
+/**
+ * Highlight an option. If the `item` param is omitted, no options will be highlighted
+ * and any existing highlight will be removed. The highlight is mutually exclusive.
+ *
+ * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
+ * @fires highlight
+ * @chainable
+ */
+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;
+};
+
+/**
+ * Fetch an item by its label.
+ *
+ * @param {string} label Label of the item to select.
+ * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
+ * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
+ */
+OO.ui.SelectWidget.prototype.getItemFromLabel = function ( label, prefix ) {
+ var i, item, found,
+ len = this.items.length,
+ filter = this.getItemMatcher( label, true );
+
+ for ( i = 0; i < len; i++ ) {
+ item = this.items[ i ];
+ if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
+ return item;
+ }
+ }
+
+ if ( prefix ) {
+ found = null;
+ filter = this.getItemMatcher( label, false );
+ for ( i = 0; i < len; i++ ) {
+ item = this.items[ i ];
+ if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
+ if ( found ) {
+ return null;
+ }
+ found = item;
+ }
+ }
+ if ( found ) {
+ return found;
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Programmatically select an option by its label. If the item does not exist,
+ * all options will be deselected.
+ *
+ * @param {string} [label] Label of the item to select.
+ * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
+ * @fires select
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.selectItemByLabel = function ( label, prefix ) {
+ var itemFromLabel = this.getItemFromLabel( label, !!prefix );
+ if ( label === undefined || !itemFromLabel ) {
+ return this.selectItem();
+ }
+ return this.selectItem( itemFromLabel );
+};
+
+/**
+ * Programmatically select an option by its data. If the `data` parameter is omitted,
+ * or if the item does not exist, all options will be deselected.
+ *
+ * @param {Object|string} [data] Value of the item to select, omit to deselect all
+ * @fires select
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
+ var itemFromData = this.getItemFromData( data );
+ if ( data === undefined || !itemFromData ) {
+ return this.selectItem();
+ }
+ return this.selectItem( itemFromData );
+};
+
+/**
+ * Programmatically select an option by its reference. If the `item` parameter is omitted,
+ * all options will be deselected.
+ *
+ * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
+ * @fires select
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
+ var i, len, 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;
+};
+
+/**
+ * Press an item.
+ *
+ * Press is a state that occurs when a user mouses down on an item, but has not
+ * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
+ * releases the mouse.
+ *
+ * @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;
+
+ 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 );
+ }
+
+ return this;
+};
+
+/**
+ * Choose an item.
+ *
+ * Note that ‘choose’ should never be modified programmatically. A user can choose
+ * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
+ * use the #selectItem method.
+ *
+ * This method is identical to #selectItem, but may vary in subclasses that take additional action
+ * when users choose an item with the keyboard or mouse.
+ *
+ * @param {OO.ui.OptionWidget} item Item to choose
+ * @fires choose
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
+ if ( item ) {
+ this.selectItem( item );
+ this.emit( 'choose', item );
+ }
+
+ return this;
+};
+
+/**
+ * Get an option by its position relative to the specified item (or to the start of the option array,
+ * if item is `null`). The direction in which to search through the option array is specified with a
+ * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
+ * `null` if there are no options in the array.
+ *
+ * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
+ * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
+ * @param {Function} filter Only consider items for which this function returns
+ * true. Function takes an OO.ui.OptionWidget and returns a boolean.
+ * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
+ */
+OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction, filter ) {
+ var currentIndex, nextIndex, i,
+ increase = direction > 0 ? 1 : -1,
+ len = this.items.length;
+
+ if ( !$.isFunction( filter ) ) {
+ filter = OO.ui.SelectWidget.static.passAllFilter;
+ }
+
+ if ( item instanceof OO.ui.OptionWidget ) {
+ currentIndex = this.items.indexOf( item );
+ nextIndex = ( currentIndex + increase + len ) % len;
+ } else {
+ // If no item is selected and moving forward, start at the beginning.
+ // If moving backward, start at the end.
+ nextIndex = direction > 0 ? 0 : len - 1;
+ }
+
+ for ( i = 0; i < len; i++ ) {
+ item = this.items[ nextIndex ];
+ if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
+ return item;
+ }
+ nextIndex = ( nextIndex + increase + len ) % len;
+ }
+ return null;
+};
+
+/**
+ * Get the next selectable item or `null` if there are no selectable items.
+ * Disabled options and menu-section markers and breaks are not selectable.
+ *
+ * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
+ */
+OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
+ var i, len, item;
+
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ item = this.items[ i ];
+ if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
+ return item;
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Add an array of options to the select. Optionally, an index number can be used to
+ * specify an insertion point.
+ *
+ * @param {OO.ui.OptionWidget[]} items Items to add
+ * @param {number} [index] Index to insert items after
+ * @fires add
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
+ // Mixin method
+ OO.ui.mixin.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;
+};
+
+/**
+ * Remove the specified array of options from the select. Options will be detached
+ * from the DOM, not removed, so they can be reused later. To remove all options from
+ * the select, you may wish to use the #clearItems method instead.
+ *
+ * @param {OO.ui.OptionWidget[]} items Items to remove
+ * @fires remove
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
+ var i, len, item;
+
+ // Deselect items being removed
+ for ( i = 0, len = items.length; i < len; i++ ) {
+ item = items[ i ];
+ if ( item.isSelected() ) {
+ this.selectItem( null );
+ }
+ }
+
+ // Mixin method
+ OO.ui.mixin.GroupWidget.prototype.removeItems.call( this, items );
+
+ this.emit( 'remove', items );
+
+ return this;
+};
+
+/**
+ * Clear all options from the select. Options will be detached from the DOM, not removed,
+ * so that they can be reused later. To remove a subset of options from the select, use
+ * the #removeItems method.
+ *
+ * @fires remove
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.clearItems = function () {
+ var items = this.items.slice();
+
+ // Mixin method
+ OO.ui.mixin.GroupWidget.prototype.clearItems.call( this );
+
+ // Clear selection
+ this.selectItem( null );
+
+ this.emit( 'remove', items );
+
+ return this;
+};
+
+/**
+ * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
+ * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
+ * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
+ * options. For more information about options and selects, please see the
+ * [OOjs UI documentation on MediaWiki][1].
+ *
+ * @example
+ * // Decorated options in a select widget
+ * var select = new OO.ui.SelectWidget( {
+ * items: [
+ * new OO.ui.DecoratedOptionWidget( {
+ * data: 'a',
+ * label: 'Option with icon',
+ * icon: 'help'
+ * } ),
+ * new OO.ui.DecoratedOptionWidget( {
+ * data: 'b',
+ * label: 'Option with indicator',
+ * indicator: 'next'
+ * } )
+ * ]
+ * } );
+ * $( 'body' ).append( select.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
+ *
+ * @class
+ * @extends OO.ui.OptionWidget
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.IndicatorElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
+ // Parent constructor
+ OO.ui.DecoratedOptionWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.IconElement.call( this, config );
+ OO.ui.mixin.IndicatorElement.call( this, config );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-decoratedOptionWidget' )
+ .prepend( this.$icon )
+ .append( this.$indicator );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
+OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IconElement );
+OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IndicatorElement );
+
+/**
+ * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
+ * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
+ * the [OOjs UI documentation on MediaWiki] [1] for more information.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
+ *
+ * @class
+ * @extends OO.ui.DecoratedOptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
+ // Configuration initialization
+ config = $.extend( { icon: 'check' }, config );
+
+ // Parent constructor
+ OO.ui.MenuOptionWidget.parent.call( this, config );
+
+ // Initialization
+ this.$element
+ .attr( 'role', 'menuitem' )
+ .addClass( 'oo-ui-menuOptionWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
+
+/* Static Properties */
+
+OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
+
+/**
+ * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
+ * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
+ *
+ * @example
+ * var myDropdown = new OO.ui.DropdownWidget( {
+ * menu: {
+ * items: [
+ * new OO.ui.MenuSectionOptionWidget( {
+ * label: 'Dogs'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'corgi',
+ * label: 'Welsh Corgi'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'poodle',
+ * label: 'Standard Poodle'
+ * } ),
+ * new OO.ui.MenuSectionOptionWidget( {
+ * label: 'Cats'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'lion',
+ * label: 'Lion'
+ * } )
+ * ]
+ * }
+ * } );
+ * $( 'body' ).append( myDropdown.$element );
+ *
+ * @class
+ * @extends OO.ui.DecoratedOptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
+ // Parent constructor
+ OO.ui.MenuSectionOptionWidget.parent.call( this, config );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
+
+/* Static Properties */
+
+OO.ui.MenuSectionOptionWidget.static.selectable = false;
+
+OO.ui.MenuSectionOptionWidget.static.highlightable = false;
+
+/**
+ * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
+ * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
+ * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
+ * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
+ * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
+ * and customized to be opened, closed, and displayed as needed.
+ *
+ * By default, menus are clipped to the visible viewport and are not visible when a user presses the
+ * mouse outside the menu.
+ *
+ * Menus also have support for keyboard interaction:
+ *
+ * - Enter/Return key: choose and select a menu option
+ * - Up-arrow key: highlight the previous menu option
+ * - Down-arrow key: highlight the next menu option
+ * - Esc key: hide the menu
+ *
+ * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ * @mixins OO.ui.mixin.ClippableElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
+ * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
+ * and {@link OO.ui.mixin.LookupElement LookupElement}
+ * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
+ * the text the user types. This config is used by {@link OO.ui.CapsuleMultiSelectWidget CapsuleMultiSelectWidget}
+ * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
+ * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
+ * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
+ * that button, unless the button (or its parent widget) is passed in here.
+ * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
+ * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
+ */
+OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.MenuSelectWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
+
+ // Properties
+ this.newItems = null;
+ this.autoHide = config.autoHide === undefined || !!config.autoHide;
+ this.filterFromInput = !!config.filterFromInput;
+ this.$input = config.$input ? config.$input : config.input ? config.input.$input : null;
+ this.$widget = config.widget ? config.widget.$element : null;
+ this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
+ this.onInputEditHandler = OO.ui.debounce( this.updateItemVisibility.bind( this ), 100 );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-menuSelectWidget' )
+ .attr( 'role', 'menu' );
+
+ // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
+ // that reference properties not initialized at that time of parent class construction
+ // TODO: Find a better way to handle post-constructor setup
+ this.visible = false;
+ this.$element.addClass( 'oo-ui-element-hidden' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.ClippableElement );
+
+/* Methods */
+
+/**
+ * Handles document mouse down events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
+ if (
+ !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
+ ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
+ ) {
+ this.toggle( false );
+ }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
+ var currentItem = this.getHighlightedItem() || this.getSelectedItem();
+
+ if ( !this.isDisabled() && this.isVisible() ) {
+ switch ( e.keyCode ) {
+ case OO.ui.Keys.LEFT:
+ case OO.ui.Keys.RIGHT:
+ // Do nothing if a text field is associated, arrow keys will be handled natively
+ if ( !this.$input ) {
+ OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
+ }
+ break;
+ case OO.ui.Keys.ESCAPE:
+ case OO.ui.Keys.TAB:
+ if ( currentItem ) {
+ currentItem.setHighlighted( false );
+ }
+ this.toggle( false );
+ // Don't prevent tabbing away, prevent defocusing
+ if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ break;
+ default:
+ OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
+ return;
+ }
+ }
+};
+
+/**
+ * Update menu item visibility after input changes.
+ * @protected
+ */
+OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
+ var i, item,
+ len = this.items.length,
+ showAll = !this.isVisible(),
+ filter = showAll ? null : this.getItemMatcher( this.$input.val() );
+
+ for ( i = 0; i < len; i++ ) {
+ item = this.items[ i ];
+ if ( item instanceof OO.ui.OptionWidget ) {
+ item.toggle( showAll || filter( item ) );
+ }
+ }
+
+ // Reevaluate clipping
+ this.clip();
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
+ if ( this.$input ) {
+ this.$input.on( 'keydown', this.onKeyDownHandler );
+ } else {
+ OO.ui.MenuSelectWidget.parent.prototype.bindKeyDownListener.call( this );
+ }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
+ if ( this.$input ) {
+ this.$input.off( 'keydown', this.onKeyDownHandler );
+ } else {
+ OO.ui.MenuSelectWidget.parent.prototype.unbindKeyDownListener.call( this );
+ }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuSelectWidget.prototype.bindKeyPressListener = function () {
+ if ( this.$input ) {
+ if ( this.filterFromInput ) {
+ this.$input.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
+ }
+ } else {
+ OO.ui.MenuSelectWidget.parent.prototype.bindKeyPressListener.call( this );
+ }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuSelectWidget.prototype.unbindKeyPressListener = function () {
+ if ( this.$input ) {
+ if ( this.filterFromInput ) {
+ this.$input.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
+ this.updateItemVisibility();
+ }
+ } else {
+ OO.ui.MenuSelectWidget.parent.prototype.unbindKeyPressListener.call( this );
+ }
+};
+
+/**
+ * Choose an item.
+ *
+ * When a user chooses an item, the menu is closed.
+ *
+ * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
+ * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
+ * @param {OO.ui.OptionWidget} item Item to choose
+ * @chainable
+ */
+OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
+ OO.ui.MenuSelectWidget.parent.prototype.chooseItem.call( this, item );
+ this.toggle( false );
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
+ var i, len, item;
+
+ // Parent method
+ OO.ui.MenuSelectWidget.parent.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 has been attached
+ item.fitLabel();
+ } else {
+ this.newItems.push( item );
+ }
+ }
+
+ // Reevaluate clipping
+ this.clip();
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
+ // Parent method
+ OO.ui.MenuSelectWidget.parent.prototype.removeItems.call( this, items );
+
+ // Reevaluate clipping
+ this.clip();
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuSelectWidget.prototype.clearItems = function () {
+ // Parent method
+ OO.ui.MenuSelectWidget.parent.prototype.clearItems.call( this );
+
+ // Reevaluate clipping
+ this.clip();
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
+ var i, len, change;
+
+ visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
+ change = visible !== this.isVisible();
+
+ // Parent method
+ OO.ui.MenuSelectWidget.parent.prototype.toggle.call( this, visible );
+
+ if ( change ) {
+ if ( visible ) {
+ this.bindKeyDownListener();
+ this.bindKeyPressListener();
+
+ if ( this.newItems && this.newItems.length ) {
+ for ( i = 0, len = this.newItems.length; i < len; i++ ) {
+ this.newItems[ i ].fitLabel();
+ }
+ this.newItems = null;
+ }
+ this.toggleClipping( true );
+
+ // Auto-hide
+ if ( this.autoHide ) {
+ this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
+ }
+ } else {
+ this.unbindKeyDownListener();
+ this.unbindKeyPressListener();
+ this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
+ this.toggleClipping( false );
+ }
+ }
+
+ return this;
+};
+
+/**
+ * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
+ * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
+ * users can interact with it.
+ *
+ * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
+ * OO.ui.DropdownInputWidget instead.
+ *
+ * @example
+ * // Example: A DropdownWidget with a menu that contains three options
+ * var dropDown = new OO.ui.DropdownWidget( {
+ * label: 'Dropdown menu: Select a menu option',
+ * menu: {
+ * items: [
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'a',
+ * label: 'First'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'b',
+ * label: 'Second'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'c',
+ * label: 'Third'
+ * } )
+ * ]
+ * }
+ * } );
+ *
+ * $( 'body' ).append( dropDown.$element );
+ *
+ * dropDown.getMenu().selectItemByData( 'b' );
+ *
+ * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
+ *
+ * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.IndicatorElement
+ * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.TitledElement
+ * @mixins OO.ui.mixin.TabIndexedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
+ * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
+ * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
+ * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
+ */
+OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
+ // Configuration initialization
+ config = $.extend( { indicator: 'down' }, config );
+
+ // Parent constructor
+ OO.ui.DropdownWidget.parent.call( this, config );
+
+ // Properties (must be set before TabIndexedElement constructor call)
+ this.$handle = this.$( '<span>' );
+ this.$overlay = config.$overlay || this.$element;
+
+ // Mixin constructors
+ OO.ui.mixin.IconElement.call( this, config );
+ OO.ui.mixin.IndicatorElement.call( this, config );
+ OO.ui.mixin.LabelElement.call( this, config );
+ OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
+ OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
+
+ // Properties
+ this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend( {
+ widget: this,
+ $container: this.$element
+ }, config.menu ) );
+
+ // Events
+ this.$handle.on( {
+ click: this.onClick.bind( this ),
+ keypress: this.onKeyPress.bind( this )
+ } );
+ this.menu.connect( this, { select: 'onMenuSelect' } );
+
+ // Initialization
+ this.$handle
+ .addClass( 'oo-ui-dropdownWidget-handle' )
+ .append( this.$icon, this.$label, this.$indicator );
+ this.$element
+ .addClass( 'oo-ui-dropdownWidget' )
+ .append( this.$handle );
+ this.$overlay.append( this.menu.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IconElement );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IndicatorElement );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TitledElement );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TabIndexedElement );
+
+/* Methods */
+
+/**
+ * Get the menu.
+ *
+ * @return {OO.ui.MenuSelectWidget} Menu of widget
+ */
+OO.ui.DropdownWidget.prototype.getMenu = function () {
+ return this.menu;
+};
+
+/**
+ * Handles menu select events.
+ *
+ * @private
+ * @param {OO.ui.MenuOptionWidget} item Selected menu item
+ */
+OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
+ var selectedLabel;
+
+ if ( !item ) {
+ this.setLabel( null );
+ 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 );
+};
+
+/**
+ * Handle mouse click events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse click event
+ */
+OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
+ if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
+ this.menu.toggle();
+ }
+ return false;
+};
+
+/**
+ * Handle key press events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ */
+OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
+ if ( !this.isDisabled() &&
+ ( ( e.which === OO.ui.Keys.SPACE && !this.menu.isVisible() ) || e.which === OO.ui.Keys.ENTER )
+ ) {
+ this.menu.toggle();
+ return false;
+ }
+};
+
+/**
+ * RadioOptionWidget is an option widget that looks like a radio button.
+ * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
+ * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
+ *
+ * @class
+ * @extends OO.ui.OptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties (must be done before parent constructor which calls #setDisabled)
+ this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
+
+ // Parent constructor
+ OO.ui.RadioOptionWidget.parent.call( this, config );
+
+ // Events
+ this.radio.$input.on( 'focus', this.onInputFocus.bind( this ) );
+
+ // Initialization
+ // Remove implicit role, we're handling it ourselves
+ this.radio.$input.attr( 'role', 'presentation' );
+ this.$element
+ .addClass( 'oo-ui-radioOptionWidget' )
+ .attr( 'role', 'radio' )
+ .attr( 'aria-checked', 'false' )
+ .removeAttr( 'aria-selected' )
+ .prepend( this.radio.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
+
+/* Static Properties */
+
+OO.ui.RadioOptionWidget.static.highlightable = false;
+
+OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
+
+OO.ui.RadioOptionWidget.static.pressable = false;
+
+OO.ui.RadioOptionWidget.static.tagName = 'label';
+
+/* Methods */
+
+/**
+ * @param {jQuery.Event} e Focus event
+ * @private
+ */
+OO.ui.RadioOptionWidget.prototype.onInputFocus = function () {
+ this.radio.$input.blur();
+ this.$element.parent().focus();
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
+ OO.ui.RadioOptionWidget.parent.prototype.setSelected.call( this, state );
+
+ this.radio.setSelected( state );
+ this.$element
+ .attr( 'aria-checked', state.toString() )
+ .removeAttr( 'aria-selected' );
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
+ OO.ui.RadioOptionWidget.parent.prototype.setDisabled.call( this, disabled );
+
+ this.radio.setDisabled( this.isDisabled() );
+
+ return this;
+};
+
+/**
+ * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
+ * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
+ * an interface for adding, removing and selecting options.
+ * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
+ *
+ * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
+ * OO.ui.RadioSelectInputWidget instead.
+ *
+ * @example
+ * // A RadioSelectWidget with RadioOptions.
+ * var option1 = new OO.ui.RadioOptionWidget( {
+ * data: 'a',
+ * label: 'Selected radio option'
+ * } );
+ *
+ * var option2 = new OO.ui.RadioOptionWidget( {
+ * data: 'b',
+ * label: 'Unselected radio option'
+ * } );
+ *
+ * var radioSelect=new OO.ui.RadioSelectWidget( {
+ * items: [ option1, option2 ]
+ * } );
+ *
+ * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
+ * radioSelect.selectItem( option1 );
+ *
+ * $( 'body' ).append( radioSelect.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
+
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ * @mixins OO.ui.mixin.TabIndexedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
+ // Parent constructor
+ OO.ui.RadioSelectWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.TabIndexedElement.call( this, config );
+
+ // Events
+ this.$element.on( {
+ focus: this.bindKeyDownListener.bind( this ),
+ blur: this.unbindKeyDownListener.bind( this )
+ } );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-radioSelectWidget' )
+ .attr( 'role', 'radiogroup' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
+
+/**
+ * Element that will stick under a specified container, even when it is inserted elsewhere in the
+ * document (for example, in a OO.ui.Window's $overlay).
+ *
+ * The elements's position is automatically calculated and maintained when window is resized or the
+ * page is scrolled. If you reposition the container manually, you have to call #position to make
+ * sure the element is still placed correctly.
+ *
+ * As positioning is only possible when both the element and the container are attached to the DOM
+ * and visible, it's only done after you call #togglePositioning. You might want to do this inside
+ * the #toggle method to display a floating popup, for example.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
+ * @cfg {jQuery} [$floatableContainer] Node to position below
+ */
+OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$floatable = null;
+ this.$floatableContainer = null;
+ this.$floatableWindow = null;
+ this.$floatableClosestScrollable = null;
+ this.onFloatableScrollHandler = this.position.bind( this );
+ this.onFloatableWindowResizeHandler = this.position.bind( this );
+
+ // Initialization
+ this.setFloatableContainer( config.$floatableContainer );
+ this.setFloatableElement( config.$floatable || this.$element );
+};
+
+/* Methods */
+
+/**
+ * Set floatable element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $floatable Element to make floatable
+ */
+OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
+ if ( this.$floatable ) {
+ this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
+ this.$floatable.css( { left: '', top: '' } );
+ }
+
+ this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
+ this.position();
+};
+
+/**
+ * Set floatable container.
+ *
+ * The element will be always positioned under the specified container.
+ *
+ * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
+ */
+OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
+ this.$floatableContainer = $floatableContainer;
+ if ( this.$floatable ) {
+ this.position();
+ }
+};
+
+/**
+ * Toggle positioning.
+ *
+ * Do not turn positioning on until after the element is attached to the DOM and visible.
+ *
+ * @param {boolean} [positioning] Enable positioning, omit to toggle
+ * @chainable
+ */
+OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
+ var closestScrollableOfContainer, closestScrollableOfFloatable;
+
+ positioning = positioning === undefined ? !this.positioning : !!positioning;
+
+ if ( this.positioning !== positioning ) {
+ this.positioning = positioning;
+
+ closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
+ closestScrollableOfFloatable = OO.ui.Element.static.getClosestScrollableContainer( this.$floatable[ 0 ] );
+ if ( closestScrollableOfContainer !== closestScrollableOfFloatable ) {
+ // If the scrollable is the root, we have to listen to scroll events
+ // on the window because of browser inconsistencies (or do we? someone should verify this)
+ if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
+ closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
+ }
+ }
+
+ if ( positioning ) {
+ this.$floatableWindow = $( this.getElementWindow() );
+ this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
+
+ if ( closestScrollableOfContainer !== closestScrollableOfFloatable ) {
+ this.$floatableClosestScrollable = $( closestScrollableOfContainer );
+ this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
+ }
+
+ // Initial position after visible
+ this.position();
+ } else {
+ if ( this.$floatableWindow ) {
+ this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
+ this.$floatableWindow = null;
+ }
+
+ if ( this.$floatableClosestScrollable ) {
+ this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
+ this.$floatableClosestScrollable = null;
+ }
+
+ this.$floatable.css( { left: '', top: '' } );
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Position the floatable below its container.
+ *
+ * This should only be done when both of them are attached to the DOM and visible.
+ *
+ * @chainable
+ */
+OO.ui.mixin.FloatableElement.prototype.position = function () {
+ var pos;
+
+ if ( !this.positioning ) {
+ return this;
+ }
+
+ pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
+
+ // Position under container
+ pos.top += this.$floatableContainer.height();
+ this.$floatable.css( pos );
+
+ // We updated the position, so re-evaluate the clipping state.
+ // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
+ // will not notice the need to update itself.)
+ // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
+ // it not listen to the right events in the right places?
+ if ( this.clip ) {
+ this.clip();
+ }
+
+ return this;
+};
+
+/**
+ * FloatingMenuSelectWidget is a menu that will stick under a specified
+ * container, even when it is inserted elsewhere in the document (for example,
+ * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
+ * menu from being clipped too aggresively.
+ *
+ * The menu's position is automatically calculated and maintained when the menu
+ * is toggled or the window is resized.
+ *
+ * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
+ *
+ * @class
+ * @extends OO.ui.MenuSelectWidget
+ * @mixins OO.ui.mixin.FloatableElement
+ *
+ * @constructor
+ * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
+ * Deprecated, omit this parameter and specify `$container` instead.
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
+ */
+OO.ui.FloatingMenuSelectWidget = function OoUiFloatingMenuSelectWidget( inputWidget, config ) {
+ // Allow 'inputWidget' parameter and config for backwards compatibility
+ if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
+ config = inputWidget;
+ inputWidget = config.inputWidget;
+ }
+
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.FloatingMenuSelectWidget.parent.call( this, config );
+
+ // Properties (must be set before mixin constructors)
+ this.inputWidget = inputWidget; // For backwards compatibility
+ this.$container = config.$container || this.inputWidget.$element;
+
+ // Mixins constructors
+ OO.ui.mixin.FloatableElement.call( this, $.extend( {}, config, { $floatableContainer: this.$container } ) );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-floatingMenuSelectWidget' );
+ // For backwards compatibility
+ this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.FloatingMenuSelectWidget, OO.ui.MenuSelectWidget );
+OO.mixinClass( OO.ui.FloatingMenuSelectWidget, OO.ui.mixin.FloatableElement );
+
+// For backwards compatibility
+OO.ui.TextInputMenuSelectWidget = OO.ui.FloatingMenuSelectWidget;
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.FloatingMenuSelectWidget.prototype.toggle = function ( visible ) {
+ var change;
+ visible = visible === undefined ? !this.isVisible() : !!visible;
+ change = visible !== this.isVisible();
+
+ if ( change && visible ) {
+ // Make sure the width is set before the parent method runs.
+ this.setIdealSize( this.$container.width() );
+ }
+
+ // Parent method
+ // This will call this.clip(), which is nonsensical since we're not positioned yet...
+ OO.ui.FloatingMenuSelectWidget.parent.prototype.toggle.call( this, visible );
+
+ if ( change ) {
+ this.togglePositioning( this.isVisible() );
+ }
+
+ return this;
+};
+
+/**
+ * InputWidget is the base class for all input widgets, which
+ * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
+ * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
+ * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.FlaggedElement
+ * @mixins OO.ui.mixin.TabIndexedElement
+ * @mixins OO.ui.mixin.TitledElement
+ * @mixins OO.ui.mixin.AccessKeyedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
+ * @cfg {string} [value=''] The value of the input.
+ * @cfg {string} [dir] The directionality of the input (ltr/rtl).
+ * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
+ * before it is accepted.
+ */
+OO.ui.InputWidget = function OoUiInputWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.InputWidget.parent.call( this, config );
+
+ // Properties
+ this.$input = this.getInputElement( config );
+ this.value = '';
+ this.inputFilter = config.inputFilter;
+
+ // Mixin constructors
+ OO.ui.mixin.FlaggedElement.call( this, config );
+ OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
+ OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
+ OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$input } ) );
+
+ // Events
+ this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
+
+ // Initialization
+ this.$input
+ .addClass( 'oo-ui-inputWidget-input' )
+ .attr( 'name', config.name )
+ .prop( 'disabled', this.isDisabled() );
+ this.$element
+ .addClass( 'oo-ui-inputWidget' )
+ .append( this.$input );
+ this.setValue( config.value );
+ this.setAccessKey( config.accessKey );
+ if ( config.dir ) {
+ this.setDir( config.dir );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.FlaggedElement );
+OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
+OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TitledElement );
+OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.AccessKeyedElement );
+
+/* Static Properties */
+
+OO.ui.InputWidget.static.supportsSimpleLabel = true;
+
+/* Static Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.InputWidget.static.reusePreInfuseDOM = function ( node, config ) {
+ config = OO.ui.InputWidget.parent.static.reusePreInfuseDOM( node, config );
+ // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
+ config.$input = $( node ).find( '.oo-ui-inputWidget-input' );
+ return config;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.InputWidget.static.gatherPreInfuseState = function ( node, config ) {
+ var state = OO.ui.InputWidget.parent.static.gatherPreInfuseState( node, config );
+ state.value = config.$input.val();
+ // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
+ state.focus = config.$input.is( ':focus' );
+ return state;
+};
+
+/* Events */
+
+/**
+ * @event change
+ *
+ * A change event is emitted when the value of the input changes.
+ *
+ * @param {string} value
+ */
+
+/* Methods */
+
+/**
+ * Get input element.
+ *
+ * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
+ * different circumstances. The element must have a `value` property (like form elements).
+ *
+ * @protected
+ * @param {Object} config Configuration options
+ * @return {jQuery} Input element
+ */
+OO.ui.InputWidget.prototype.getInputElement = function ( config ) {
+ // See #reusePreInfuseDOM about config.$input
+ return config.$input || $( '<input>' );
+};
+
+/**
+ * Handle potentially value-changing events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
+ */
+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() );
+ } );
+ }
+};
+
+/**
+ * Get the value of the input.
+ *
+ * @return {string} Input value
+ */
+OO.ui.InputWidget.prototype.getValue = function () {
+ // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
+ // it, and we won't know unless they're kind enough to trigger a 'change' event.
+ var value = this.$input.val();
+ if ( this.value !== value ) {
+ this.setValue( value );
+ }
+ return this.value;
+};
+
+/**
+ * Set the directionality of the input, either RTL (right-to-left) or LTR (left-to-right).
+ *
+ * @deprecated since v0.13.1, use #setDir directly
+ * @param {boolean} isRTL Directionality is right-to-left
+ * @chainable
+ */
+OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
+ this.setDir( isRTL ? 'rtl' : 'ltr' );
+ return this;
+};
+
+/**
+ * Set the directionality of the input.
+ *
+ * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
+ * @chainable
+ */
+OO.ui.InputWidget.prototype.setDir = function ( dir ) {
+ this.$input.prop( 'dir', dir );
+ return this;
+};
+
+/**
+ * Set the value of the input.
+ *
+ * @param {string} value New value
+ * @fires change
+ * @chainable
+ */
+OO.ui.InputWidget.prototype.setValue = function ( value ) {
+ value = this.cleanUpValue( value );
+ // Update the DOM if it has changed. Note that with cleanUpValue, it
+ // is possible for the DOM value to change without this.value changing.
+ if ( this.$input.val() !== value ) {
+ this.$input.val( value );
+ }
+ if ( this.value !== value ) {
+ this.value = value;
+ this.emit( 'change', this.value );
+ }
+ return this;
+};
+
+/**
+ * Set the input's access key.
+ * FIXME: This is the same code as in OO.ui.mixin.ButtonElement, maybe find a better place for it?
+ *
+ * @param {string} accessKey Input's access key, use empty string to remove
+ * @chainable
+ */
+OO.ui.InputWidget.prototype.setAccessKey = function ( accessKey ) {
+ accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
+
+ if ( this.accessKey !== accessKey ) {
+ if ( this.$input ) {
+ if ( accessKey !== null ) {
+ this.$input.attr( 'accesskey', accessKey );
+ } else {
+ this.$input.removeAttr( 'accesskey' );
+ }
+ }
+ this.accessKey = accessKey;
+ }
+
+ return this;
+};
+
+/**
+ * Clean up incoming value.
+ *
+ * Ensures value is a string, and converts undefined and null to empty string.
+ *
+ * @private
+ * @param {string} value Original value
+ * @return {string} Cleaned up value
+ */
+OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
+ if ( value === undefined || value === null ) {
+ return '';
+ } else if ( this.inputFilter ) {
+ return this.inputFilter( String( value ) );
+ } else {
+ return String( value );
+ }
+};
+
+/**
+ * Simulate the behavior of clicking on a label bound to this input. This method is only called by
+ * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
+ * called directly.
+ */
+OO.ui.InputWidget.prototype.simulateLabelClick = function () {
+ if ( !this.isDisabled() ) {
+ if ( this.$input.is( ':checkbox, :radio' ) ) {
+ this.$input.click();
+ }
+ if ( this.$input.is( ':input' ) ) {
+ this.$input[ 0 ].focus();
+ }
+ }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
+ OO.ui.InputWidget.parent.prototype.setDisabled.call( this, state );
+ if ( this.$input ) {
+ this.$input.prop( 'disabled', this.isDisabled() );
+ }
+ return this;
+};
+
+/**
+ * Focus the input.
+ *
+ * @chainable
+ */
+OO.ui.InputWidget.prototype.focus = function () {
+ this.$input[ 0 ].focus();
+ return this;
+};
+
+/**
+ * Blur the input.
+ *
+ * @chainable
+ */
+OO.ui.InputWidget.prototype.blur = function () {
+ this.$input[ 0 ].blur();
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.InputWidget.prototype.restorePreInfuseState = function ( state ) {
+ OO.ui.InputWidget.parent.prototype.restorePreInfuseState.call( this, state );
+ if ( state.value !== undefined && state.value !== this.getValue() ) {
+ this.setValue( state.value );
+ }
+ if ( state.focus ) {
+ this.focus();
+ }
+};
+
+/**
+ * ButtonInputWidget is used to submit HTML forms and is intended to be used within
+ * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
+ * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
+ * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
+ * [OOjs UI documentation on MediaWiki] [1] for more information.
+ *
+ * @example
+ * // A ButtonInputWidget rendered as an HTML button, the default.
+ * var button = new OO.ui.ButtonInputWidget( {
+ * label: 'Input button',
+ * icon: 'check',
+ * value: 'check'
+ * } );
+ * $( 'body' ).append( button.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ * @mixins OO.ui.mixin.ButtonElement
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.IndicatorElement
+ * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.TitledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
+ * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
+ * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
+ * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
+ * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
+ */
+OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
+ // Configuration initialization
+ config = $.extend( { type: 'button', useInputTag: false }, config );
+
+ // Properties (must be set before parent constructor, which calls #setValue)
+ this.useInputTag = config.useInputTag;
+
+ // Parent constructor
+ OO.ui.ButtonInputWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
+ OO.ui.mixin.IconElement.call( this, config );
+ OO.ui.mixin.IndicatorElement.call( this, config );
+ OO.ui.mixin.LabelElement.call( this, config );
+ OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
+
+ // Initialization
+ if ( !config.useInputTag ) {
+ this.$input.append( this.$icon, this.$label, this.$indicator );
+ }
+ this.$element.addClass( 'oo-ui-buttonInputWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
+OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
+OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
+OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
+OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
+
+/* Static Properties */
+
+/**
+ * Disable generating `<label>` elements for buttons. One would very rarely need additional label
+ * for a button, and it's already a big clickable target, and it causes unexpected rendering.
+ */
+OO.ui.ButtonInputWidget.static.supportsSimpleLabel = false;
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ * @protected
+ */
+OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
+ var type;
+ // See InputWidget#reusePreInfuseDOM about config.$input
+ if ( config.$input ) {
+ return config.$input.empty();
+ }
+ type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ? config.type : 'button';
+ return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
+};
+
+/**
+ * Set label value.
+ *
+ * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
+ *
+ * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
+ * text, or `null` for no label
+ * @chainable
+ */
+OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
+ OO.ui.mixin.LabelElement.prototype.setLabel.call( this, label );
+
+ if ( this.useInputTag ) {
+ if ( typeof label === 'function' ) {
+ label = OO.ui.resolveMsg( label );
+ }
+ if ( label instanceof jQuery ) {
+ label = label.text();
+ }
+ if ( !label ) {
+ label = '';
+ }
+ this.$input.val( label );
+ }
+
+ return this;
+};
+
+/**
+ * Set the value of the input.
+ *
+ * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
+ * they do not support {@link #value values}.
+ *
+ * @param {string} value New value
+ * @chainable
+ */
+OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
+ if ( !this.useInputTag ) {
+ OO.ui.ButtonInputWidget.parent.prototype.setValue.call( this, value );
+ }
+ return this;
+};
+
+/**
+ * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
+ * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
+ * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
+ * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
+ *
+ * @example
+ * // An example of selected, unselected, and disabled checkbox inputs
+ * var checkbox1=new OO.ui.CheckboxInputWidget( {
+ * value: 'a',
+ * selected: true
+ * } );
+ * var checkbox2=new OO.ui.CheckboxInputWidget( {
+ * value: 'b'
+ * } );
+ * var checkbox3=new OO.ui.CheckboxInputWidget( {
+ * value:'c',
+ * disabled: true
+ * } );
+ * // Create a fieldset layout with fields for each checkbox.
+ * var fieldset = new OO.ui.FieldsetLayout( {
+ * label: 'Checkboxes'
+ * } );
+ * fieldset.addItems( [
+ * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
+ * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
+ * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
+ * ] );
+ * $( 'body' ).append( fieldset.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
+ */
+OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.CheckboxInputWidget.parent.call( this, config );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-checkboxInputWidget' )
+ // Required for pretty styling in MediaWiki theme
+ .append( $( '<span>' ) );
+ this.setSelected( config.selected !== undefined ? config.selected : false );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
+
+/* Static Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CheckboxInputWidget.static.gatherPreInfuseState = function ( node, config ) {
+ var state = OO.ui.CheckboxInputWidget.parent.static.gatherPreInfuseState( node, config );
+ state.checked = config.$input.prop( 'checked' );
+ return state;
+};
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ * @protected
+ */
+OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
+ return $( '<input type="checkbox" />' );
+};
+
+/**
+ * @inheritdoc
+ */
+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.setSelected( widget.$input.prop( 'checked' ) );
+ } );
+ }
+};
+
+/**
+ * Set selection state of this checkbox.
+ *
+ * @param {boolean} state `true` for selected
+ * @chainable
+ */
+OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
+ state = !!state;
+ if ( this.selected !== state ) {
+ this.selected = state;
+ this.$input.prop( 'checked', this.selected );
+ this.emit( 'change', this.selected );
+ }
+ return this;
+};
+
+/**
+ * Check if this checkbox is selected.
+ *
+ * @return {boolean} Checkbox is selected
+ */
+OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
+ // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
+ // it, and we won't know unless they're kind enough to trigger a 'change' event.
+ var selected = this.$input.prop( 'checked' );
+ if ( this.selected !== selected ) {
+ this.setSelected( selected );
+ }
+ return this.selected;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CheckboxInputWidget.prototype.restorePreInfuseState = function ( state ) {
+ OO.ui.CheckboxInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
+ if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
+ this.setSelected( state.checked );
+ }
+};
+
+/**
+ * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
+ * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
+ * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
+ * more information about input widgets.
+ *
+ * A DropdownInputWidget always has a value (one of the options is always selected), unless there
+ * are no options. If no `value` configuration option is provided, the first option is selected.
+ * If you need a state representing no value (no option being selected), use a DropdownWidget.
+ *
+ * This and OO.ui.RadioSelectInputWidget support the same configuration options.
+ *
+ * @example
+ * // Example: A DropdownInputWidget with three options
+ * var dropdownInput = new OO.ui.DropdownInputWidget( {
+ * options: [
+ * { data: 'a', label: 'First' },
+ * { data: 'b', label: 'Second'},
+ * { data: 'c', label: 'Third' }
+ * ]
+ * } );
+ * $( 'body' ).append( dropdownInput.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ * @mixins OO.ui.mixin.TitledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
+ * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
+ */
+OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties (must be done before parent constructor which calls #setDisabled)
+ this.dropdownWidget = new OO.ui.DropdownWidget( config.dropdown );
+
+ // Parent constructor
+ OO.ui.DropdownInputWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.TitledElement.call( this, config );
+
+ // Events
+ this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
+
+ // Initialization
+ this.setOptions( config.options || [] );
+ this.$element
+ .addClass( 'oo-ui-dropdownInputWidget' )
+ .append( this.dropdownWidget.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
+OO.mixinClass( OO.ui.DropdownInputWidget, OO.ui.mixin.TitledElement );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ * @protected
+ */
+OO.ui.DropdownInputWidget.prototype.getInputElement = function ( config ) {
+ // See InputWidget#reusePreInfuseDOM about config.$input
+ if ( config.$input ) {
+ return config.$input.addClass( 'oo-ui-element-hidden' );
+ }
+ return $( '<input type="hidden">' );
+};
+
+/**
+ * Handles menu select events.
+ *
+ * @private
+ * @param {OO.ui.MenuOptionWidget} item Selected menu item
+ */
+OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
+ this.setValue( item.getData() );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
+ value = this.cleanUpValue( value );
+ this.dropdownWidget.getMenu().selectItemByData( value );
+ OO.ui.DropdownInputWidget.parent.prototype.setValue.call( this, value );
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
+ this.dropdownWidget.setDisabled( state );
+ OO.ui.DropdownInputWidget.parent.prototype.setDisabled.call( this, state );
+ return this;
+};
+
+/**
+ * Set the options available for this input.
+ *
+ * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
+ * @chainable
+ */
+OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
+ var
+ value = this.getValue(),
+ widget = this;
+
+ // Rebuild the dropdown menu
+ this.dropdownWidget.getMenu()
+ .clearItems()
+ .addItems( options.map( function ( opt ) {
+ var optValue = widget.cleanUpValue( opt.data );
+ return new OO.ui.MenuOptionWidget( {
+ data: optValue,
+ label: opt.label !== undefined ? opt.label : optValue
+ } );
+ } ) );
+
+ // Restore the previous value, or reset to something sensible
+ if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
+ // Previous value is still available, ensure consistency with the dropdown
+ this.setValue( value );
+ } else {
+ // No longer valid, reset
+ if ( options.length ) {
+ this.setValue( options[ 0 ].data );
+ }
+ }
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.DropdownInputWidget.prototype.focus = function () {
+ this.dropdownWidget.getMenu().toggle( true );
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.DropdownInputWidget.prototype.blur = function () {
+ this.dropdownWidget.getMenu().toggle( false );
+ return this;
+};
+
+/**
+ * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
+ * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
+ * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
+ * please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
+ *
+ * @example
+ * // An example of selected, unselected, and disabled radio inputs
+ * var radio1 = new OO.ui.RadioInputWidget( {
+ * value: 'a',
+ * selected: true
+ * } );
+ * var radio2 = new OO.ui.RadioInputWidget( {
+ * value: 'b'
+ * } );
+ * var radio3 = new OO.ui.RadioInputWidget( {
+ * value: 'c',
+ * disabled: true
+ * } );
+ * // Create a fieldset layout with fields for each radio button.
+ * var fieldset = new OO.ui.FieldsetLayout( {
+ * label: 'Radio inputs'
+ * } );
+ * fieldset.addItems( [
+ * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
+ * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
+ * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
+ * ] );
+ * $( 'body' ).append( fieldset.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
+ */
+OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.RadioInputWidget.parent.call( this, config );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-radioInputWidget' )
+ // Required for pretty styling in MediaWiki theme
+ .append( $( '<span>' ) );
+ this.setSelected( config.selected !== undefined ? config.selected : false );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
+
+/* Static Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.RadioInputWidget.static.gatherPreInfuseState = function ( node, config ) {
+ var state = OO.ui.RadioInputWidget.parent.static.gatherPreInfuseState( node, config );
+ state.checked = config.$input.prop( 'checked' );
+ return state;
+};
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ * @protected
+ */
+OO.ui.RadioInputWidget.prototype.getInputElement = function () {
+ return $( '<input type="radio" />' );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.RadioInputWidget.prototype.onEdit = function () {
+ // RadioInputWidget doesn't track its state.
+};
+
+/**
+ * Set selection state of this radio button.
+ *
+ * @param {boolean} state `true` for selected
+ * @chainable
+ */
+OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
+ // RadioInputWidget doesn't track its state.
+ this.$input.prop( 'checked', state );
+ return this;
+};
+
+/**
+ * Check if this radio button is selected.
+ *
+ * @return {boolean} Radio is selected
+ */
+OO.ui.RadioInputWidget.prototype.isSelected = function () {
+ return this.$input.prop( 'checked' );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.RadioInputWidget.prototype.restorePreInfuseState = function ( state ) {
+ OO.ui.RadioInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
+ if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
+ this.setSelected( state.checked );
+ }
+};
+
+/**
+ * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
+ * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
+ * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
+ * more information about input widgets.
+ *
+ * This and OO.ui.DropdownInputWidget support the same configuration options.
+ *
+ * @example
+ * // Example: A RadioSelectInputWidget with three options
+ * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
+ * options: [
+ * { data: 'a', label: 'First' },
+ * { data: 'b', label: 'Second'},
+ * { data: 'c', label: 'Third' }
+ * ]
+ * } );
+ * $( 'body' ).append( radioSelectInput.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
+ */
+OO.ui.RadioSelectInputWidget = function OoUiRadioSelectInputWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties (must be done before parent constructor which calls #setDisabled)
+ this.radioSelectWidget = new OO.ui.RadioSelectWidget();
+
+ // Parent constructor
+ OO.ui.RadioSelectInputWidget.parent.call( this, config );
+
+ // Events
+ this.radioSelectWidget.connect( this, { select: 'onMenuSelect' } );
+
+ // Initialization
+ this.setOptions( config.options || [] );
+ this.$element
+ .addClass( 'oo-ui-radioSelectInputWidget' )
+ .append( this.radioSelectWidget.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.RadioSelectInputWidget, OO.ui.InputWidget );
+
+/* Static Properties */
+
+OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
+
+/* Static Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.RadioSelectInputWidget.static.gatherPreInfuseState = function ( node, config ) {
+ var state = OO.ui.RadioSelectInputWidget.parent.static.gatherPreInfuseState( node, config );
+ state.value = $( node ).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
+ return state;
+};
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ * @protected
+ */
+OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
+ return $( '<input type="hidden">' );
+};
+
+/**
+ * Handles menu select events.
+ *
+ * @private
+ * @param {OO.ui.RadioOptionWidget} item Selected menu item
+ */
+OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
+ this.setValue( item.getData() );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
+ value = this.cleanUpValue( value );
+ this.radioSelectWidget.selectItemByData( value );
+ OO.ui.RadioSelectInputWidget.parent.prototype.setValue.call( this, value );
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
+ this.radioSelectWidget.setDisabled( state );
+ OO.ui.RadioSelectInputWidget.parent.prototype.setDisabled.call( this, state );
+ return this;
+};
+
+/**
+ * Set the options available for this input.
+ *
+ * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
+ * @chainable
+ */
+OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
+ var
+ value = this.getValue(),
+ widget = this;
+
+ // Rebuild the radioSelect menu
+ this.radioSelectWidget
+ .clearItems()
+ .addItems( options.map( function ( opt ) {
+ var optValue = widget.cleanUpValue( opt.data );
+ return new OO.ui.RadioOptionWidget( {
+ data: optValue,
+ label: opt.label !== undefined ? opt.label : optValue
+ } );
+ } ) );
+
+ // Restore the previous value, or reset to something sensible
+ if ( this.radioSelectWidget.getItemFromData( value ) ) {
+ // Previous value is still available, ensure consistency with the radioSelect
+ this.setValue( value );
+ } else {
+ // No longer valid, reset
+ if ( options.length ) {
+ this.setValue( options[ 0 ].data );
+ }
+ }
+
+ return this;
+};
+
+/**
+ * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
+ * size of the field as well as its presentation. In addition, these widgets can be configured
+ * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
+ * validation-pattern (used to determine if an input value is valid or not) and an input filter,
+ * which modifies incoming values rather than validating them.
+ * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
+ *
+ * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
+ *
+ * @example
+ * // Example of a text input widget
+ * var textInput = new OO.ui.TextInputWidget( {
+ * value: 'Text input'
+ * } )
+ * $( 'body' ).append( textInput.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.IndicatorElement
+ * @mixins OO.ui.mixin.PendingElement
+ * @mixins OO.ui.mixin.LabelElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
+ * 'email' or 'url'. Ignored if `multiline` is true.
+ *
+ * Some values of `type` result in additional behaviors:
+ *
+ * - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
+ * empties the text field
+ * @cfg {string} [placeholder] Placeholder text
+ * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
+ * instruct the browser to focus this widget.
+ * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
+ * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
+ * @cfg {boolean} [multiline=false] Allow multiple lines of text
+ * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
+ * specifies minimum number of rows to display.
+ * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
+ * Use the #maxRows config to specify a maximum number of displayed rows.
+ * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
+ * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
+ * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
+ * the value or placeholder text: `'before'` or `'after'`
+ * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
+ * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
+ * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
+ * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
+ * (the value must contain only numbers); when RegExp, a regular expression that must match the
+ * value for it to be considered valid; when Function, a function receiving the value as parameter
+ * that must return true, or promise resolving to true, for it to be considered valid.
+ */
+OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
+ // Configuration initialization
+ config = $.extend( {
+ type: 'text',
+ labelPosition: 'after'
+ }, config );
+ if ( config.type === 'search' ) {
+ if ( config.icon === undefined ) {
+ config.icon = 'search';
+ }
+ // indicator: 'clear' is set dynamically later, depending on value
+ }
+ if ( config.required ) {
+ if ( config.indicator === undefined ) {
+ config.indicator = 'required';
+ }
+ }
+
+ // Parent constructor
+ OO.ui.TextInputWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.IconElement.call( this, config );
+ OO.ui.mixin.IndicatorElement.call( this, config );
+ OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$input } ) );
+ OO.ui.mixin.LabelElement.call( this, config );
+
+ // Properties
+ this.type = this.getSaneType( config );
+ this.readOnly = false;
+ this.multiline = !!config.multiline;
+ this.autosize = !!config.autosize;
+ this.minRows = config.rows !== undefined ? config.rows : '';
+ this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
+ this.validate = null;
+ this.styleHeight = null;
+ this.scrollWidth = null;
+
+ // Clone for resizing
+ if ( this.autosize ) {
+ this.$clone = this.$input
+ .clone()
+ .insertAfter( this.$input )
+ .attr( 'aria-hidden', 'true' )
+ .addClass( 'oo-ui-element-hidden' );
+ }
+
+ this.setValidation( config.validate );
+ this.setLabelPosition( config.labelPosition );
+
+ // Events
+ this.$input.on( {
+ keypress: this.onKeyPress.bind( this ),
+ blur: this.onBlur.bind( this )
+ } );
+ this.$input.one( {
+ focus: this.onElementAttach.bind( this )
+ } );
+ this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
+ this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
+ this.on( 'labelChange', this.updatePosition.bind( this ) );
+ this.connect( this, {
+ change: 'onChange',
+ disable: 'onDisable'
+ } );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type )
+ .append( this.$icon, this.$indicator );
+ this.setReadOnly( !!config.readOnly );
+ this.updateSearchIndicator();
+ if ( config.placeholder ) {
+ this.$input.attr( 'placeholder', config.placeholder );
+ }
+ if ( config.maxLength !== undefined ) {
+ this.$input.attr( 'maxlength', config.maxLength );
+ }
+ if ( config.autofocus ) {
+ this.$input.attr( 'autofocus', 'autofocus' );
+ }
+ if ( config.required ) {
+ this.$input.attr( 'required', 'required' );
+ this.$input.attr( 'aria-required', 'true' );
+ }
+ if ( config.autocomplete === false ) {
+ this.$input.attr( 'autocomplete', 'off' );
+ // Turning off autocompletion also disables "form caching" when the user navigates to a
+ // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
+ $( window ).on( {
+ beforeunload: function () {
+ this.$input.removeAttr( 'autocomplete' );
+ }.bind( this ),
+ pageshow: function () {
+ // Browsers don't seem to actually fire this event on "Back", they instead just reload the
+ // whole page... it shouldn't hurt, though.
+ this.$input.attr( 'autocomplete', 'off' );
+ }.bind( this )
+ } );
+ }
+ if ( this.multiline && config.rows ) {
+ this.$input.attr( 'rows', config.rows );
+ }
+ if ( this.label || config.autosize ) {
+ this.installParentChangeDetector();
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
+OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
+OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
+OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
+OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
+
+/* Static Properties */
+
+OO.ui.TextInputWidget.static.validationPatterns = {
+ 'non-empty': /.+/,
+ integer: /^\d+$/
+};
+
+/* Static Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.TextInputWidget.static.gatherPreInfuseState = function ( node, config ) {
+ var state = OO.ui.TextInputWidget.parent.static.gatherPreInfuseState( node, config );
+ if ( config.multiline ) {
+ state.scrollTop = config.$input.scrollTop();
+ }
+ return state;
+};
+
+/* Events */
+
+/**
+ * An `enter` event is emitted when the user presses 'enter' inside the text box.
+ *
+ * Not emitted if the input is multiline.
+ *
+ * @event enter
+ */
+
+/**
+ * A `resize` event is emitted when autosize is set and the widget resizes
+ *
+ * @event resize
+ */
+
+/* Methods */
+
+/**
+ * Handle icon mouse down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse down event
+ * @fires icon
+ */
+OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
+ if ( e.which === OO.ui.MouseButtons.LEFT ) {
+ this.$input[ 0 ].focus();
+ return false;
+ }
+};
+
+/**
+ * Handle indicator mouse down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse down event
+ * @fires indicator
+ */
+OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
+ if ( e.which === OO.ui.MouseButtons.LEFT ) {
+ if ( this.type === 'search' ) {
+ // Clear the text field
+ this.setValue( '' );
+ }
+ this.$input[ 0 ].focus();
+ return false;
+ }
+};
+
+/**
+ * Handle key press events.
+ *
+ * @private
+ * @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', e );
+ }
+};
+
+/**
+ * Handle blur events.
+ *
+ * @private
+ * @param {jQuery.Event} e Blur event
+ */
+OO.ui.TextInputWidget.prototype.onBlur = function () {
+ this.setValidityFlag();
+};
+
+/**
+ * Handle element attach events.
+ *
+ * @private
+ * @param {jQuery.Event} e Element attach event
+ */
+OO.ui.TextInputWidget.prototype.onElementAttach = function () {
+ // Any previously calculated size is now probably invalid if we reattached elsewhere
+ this.valCache = null;
+ this.adjustSize();
+ this.positionLabel();
+};
+
+/**
+ * Handle change events.
+ *
+ * @param {string} value
+ * @private
+ */
+OO.ui.TextInputWidget.prototype.onChange = function () {
+ this.updateSearchIndicator();
+ this.setValidityFlag();
+ this.adjustSize();
+};
+
+/**
+ * Handle disable events.
+ *
+ * @param {boolean} disabled Element is disabled
+ * @private
+ */
+OO.ui.TextInputWidget.prototype.onDisable = function () {
+ this.updateSearchIndicator();
+};
+
+/**
+ * Check if the input is {@link #readOnly read-only}.
+ *
+ * @return {boolean}
+ */
+OO.ui.TextInputWidget.prototype.isReadOnly = function () {
+ return this.readOnly;
+};
+
+/**
+ * Set the {@link #readOnly read-only} state of the input.
+ *
+ * @param {boolean} state Make input read-only
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
+ this.readOnly = !!state;
+ this.$input.prop( 'readOnly', this.readOnly );
+ this.updateSearchIndicator();
+ return this;
+};
+
+/**
+ * Support function for making #onElementAttach work across browsers.
+ *
+ * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
+ * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
+ *
+ * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
+ * first time that the element gets attached to the documented.
+ */
+OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
+ var mutationObserver, onRemove, topmostNode, fakeParentNode,
+ MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
+ widget = this;
+
+ if ( MutationObserver ) {
+ // The new way. If only it wasn't so ugly.
+
+ if ( this.$element.closest( 'html' ).length ) {
+ // Widget is attached already, do nothing. This breaks the functionality of this function when
+ // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
+ // would require observation of the whole document, which would hurt performance of other,
+ // more important code.
+ return;
+ }
+
+ // Find topmost node in the tree
+ topmostNode = this.$element[ 0 ];
+ while ( topmostNode.parentNode ) {
+ topmostNode = topmostNode.parentNode;
+ }
+
+ // We have no way to detect the $element being attached somewhere without observing the entire
+ // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
+ // parent node of $element, and instead detect when $element is removed from it (and thus
+ // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
+ // doesn't get attached, we end up back here and create the parent.
+
+ mutationObserver = new MutationObserver( function ( mutations ) {
+ var i, j, removedNodes;
+ for ( i = 0; i < mutations.length; i++ ) {
+ removedNodes = mutations[ i ].removedNodes;
+ for ( j = 0; j < removedNodes.length; j++ ) {
+ if ( removedNodes[ j ] === topmostNode ) {
+ setTimeout( onRemove, 0 );
+ return;
+ }
+ }
+ }
+ } );
+
+ onRemove = function () {
+ // If the node was attached somewhere else, report it
+ if ( widget.$element.closest( 'html' ).length ) {
+ widget.onElementAttach();
+ }
+ mutationObserver.disconnect();
+ widget.installParentChangeDetector();
+ };
+
+ // Create a fake parent and observe it
+ fakeParentNode = $( '<div>' ).append( topmostNode )[ 0 ];
+ mutationObserver.observe( fakeParentNode, { childList: true } );
+ } else {
+ // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
+ // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
+ this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
+ }
+};
+
+/**
+ * Automatically adjust the size of the text input.
+ *
+ * This only affects #multiline inputs that are {@link #autosize autosized}.
+ *
+ * @chainable
+ * @fires resize
+ */
+OO.ui.TextInputWidget.prototype.adjustSize = function () {
+ var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError,
+ idealHeight, newHeight, scrollWidth, property;
+
+ if ( this.multiline && this.$input.val() !== this.valCache ) {
+ if ( this.autosize ) {
+ this.$clone
+ .val( this.$input.val() )
+ .attr( 'rows', this.minRows )
+ // Set inline height property to 0 to measure scroll height
+ .css( 'height', 0 );
+
+ this.$clone.removeClass( 'oo-ui-element-hidden' );
+
+ this.valCache = this.$input.val();
+
+ scrollHeight = this.$clone[ 0 ].scrollHeight;
+
+ // Remove inline height property to measure natural heights
+ this.$clone.css( 'height', '' );
+ innerHeight = this.$clone.innerHeight();
+ outerHeight = this.$clone.outerHeight();
+
+ // Measure max rows height
+ this.$clone
+ .attr( 'rows', this.maxRows )
+ .css( 'height', 'auto' )
+ .val( '' );
+ maxInnerHeight = this.$clone.innerHeight();
+
+ // Difference between reported innerHeight and scrollHeight with no scrollbars present
+ // Equals 1 on Blink-based browsers and 0 everywhere else
+ measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
+ idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
+
+ this.$clone.addClass( 'oo-ui-element-hidden' );
+
+ // Only apply inline height when expansion beyond natural height is needed
+ // Use the difference between the inner and outer height as a buffer
+ newHeight = idealHeight > innerHeight ? idealHeight + ( outerHeight - innerHeight ) : '';
+ if ( newHeight !== this.styleHeight ) {
+ this.$input.css( 'height', newHeight );
+ this.styleHeight = newHeight;
+ this.emit( 'resize' );
+ }
+ }
+ scrollWidth = this.$input[ 0 ].offsetWidth - this.$input[ 0 ].clientWidth;
+ if ( scrollWidth !== this.scrollWidth ) {
+ property = this.$element.css( 'direction' ) === 'rtl' ? 'left' : 'right';
+ // Reset
+ this.$label.css( { right: '', left: '' } );
+ this.$indicator.css( { right: '', left: '' } );
+
+ if ( scrollWidth ) {
+ this.$indicator.css( property, scrollWidth );
+ if ( this.labelPosition === 'after' ) {
+ this.$label.css( property, scrollWidth );
+ }
+ }
+
+ this.scrollWidth = scrollWidth;
+ this.positionLabel();
+ }
+ }
+ return this;
+};
+
+/**
+ * @inheritdoc
+ * @protected
+ */
+OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
+ return config.multiline ?
+ $( '<textarea>' ) :
+ $( '<input type="' + this.getSaneType( config ) + '" />' );
+};
+
+/**
+ * Get sanitized value for 'type' for given config.
+ *
+ * @param {Object} config Configuration options
+ * @return {string|null}
+ * @private
+ */
+OO.ui.TextInputWidget.prototype.getSaneType = function ( config ) {
+ var type = [ 'text', 'password', 'search', 'email', 'url' ].indexOf( config.type ) !== -1 ?
+ config.type :
+ 'text';
+ return config.multiline ? 'multiline' : type;
+};
+
+/**
+ * Check if the input supports multiple lines.
+ *
+ * @return {boolean}
+ */
+OO.ui.TextInputWidget.prototype.isMultiline = function () {
+ return !!this.multiline;
+};
+
+/**
+ * Check if the input automatically adjusts its size.
+ *
+ * @return {boolean}
+ */
+OO.ui.TextInputWidget.prototype.isAutosizing = function () {
+ return !!this.autosize;
+};
+
+/**
+ * Focus the input and select a specified range within the text.
+ *
+ * @param {number} from Select from offset
+ * @param {number} [to] Select to offset, defaults to from
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
+ var isBackwards, start, end,
+ input = this.$input[ 0 ];
+
+ to = to || from;
+
+ isBackwards = to < from;
+ start = isBackwards ? to : from;
+ end = isBackwards ? from : to;
+
+ this.focus();
+
+ input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
+ return this;
+};
+
+/**
+ * Get an object describing the current selection range in a directional manner
+ *
+ * @return {Object} Object containing 'from' and 'to' offsets
+ */
+OO.ui.TextInputWidget.prototype.getRange = function () {
+ var input = this.$input[ 0 ],
+ start = input.selectionStart,
+ end = input.selectionEnd,
+ isBackwards = input.selectionDirection === 'backward';
+
+ return {
+ from: isBackwards ? end : start,
+ to: isBackwards ? start : end
+ };
+};
+
+/**
+ * Get the length of the text input value.
+ *
+ * This could differ from the length of #getValue if the
+ * value gets filtered
+ *
+ * @return {number} Input length
+ */
+OO.ui.TextInputWidget.prototype.getInputLength = function () {
+ return this.$input[ 0 ].value.length;
+};
+
+/**
+ * Focus the input and select the entire text.
+ *
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.select = function () {
+ return this.selectRange( 0, this.getInputLength() );
+};
+
+/**
+ * Focus the input and move the cursor to the start.
+ *
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.moveCursorToStart = function () {
+ return this.selectRange( 0 );
+};
+
+/**
+ * Focus the input and move the cursor to the end.
+ *
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.moveCursorToEnd = function () {
+ return this.selectRange( this.getInputLength() );
+};
+
+/**
+ * Insert new content into the input.
+ *
+ * @param {string} content Content to be inserted
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.insertContent = function ( content ) {
+ var start, end,
+ range = this.getRange(),
+ value = this.getValue();
+
+ start = Math.min( range.from, range.to );
+ end = Math.max( range.from, range.to );
+
+ this.setValue( value.slice( 0, start ) + content + value.slice( end ) );
+ this.selectRange( start + content.length );
+ return this;
+};
+
+/**
+ * Insert new content either side of a selection.
+ *
+ * @param {string} pre Content to be inserted before the selection
+ * @param {string} post Content to be inserted after the selection
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.encapsulateContent = function ( pre, post ) {
+ var start, end,
+ range = this.getRange(),
+ offset = pre.length;
+
+ start = Math.min( range.from, range.to );
+ end = Math.max( range.from, range.to );
+
+ this.selectRange( start ).insertContent( pre );
+ this.selectRange( offset + end ).insertContent( post );
+
+ this.selectRange( offset + start, offset + end );
+ return this;
+};
+
+/**
+ * Set the validation pattern.
+ *
+ * The validation pattern is either a regular expression, a function, or the symbolic name of a
+ * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
+ * value must contain only numbers).
+ *
+ * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
+ * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
+ */
+OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
+ if ( validate instanceof RegExp || validate instanceof Function ) {
+ this.validate = validate;
+ } else {
+ this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
+ }
+};
+
+/**
+ * Sets the 'invalid' flag appropriately.
+ *
+ * @param {boolean} [isValid] Optionally override validation result
+ */
+OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
+ var widget = this,
+ setFlag = function ( valid ) {
+ if ( !valid ) {
+ widget.$input.attr( 'aria-invalid', 'true' );
+ } else {
+ widget.$input.removeAttr( 'aria-invalid' );
+ }
+ widget.setFlags( { invalid: !valid } );
+ };
+
+ if ( isValid !== undefined ) {
+ setFlag( isValid );
+ } else {
+ this.getValidity().then( function () {
+ setFlag( true );
+ }, function () {
+ setFlag( false );
+ } );
+ }
+};
+
+/**
+ * Check if a value is valid.
+ *
+ * This method returns a promise that resolves with a boolean `true` if the current value is
+ * considered valid according to the supplied {@link #validate validation pattern}.
+ *
+ * @deprecated
+ * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
+ */
+OO.ui.TextInputWidget.prototype.isValid = function () {
+ var result;
+
+ if ( this.validate instanceof Function ) {
+ result = this.validate( this.getValue() );
+ if ( result && $.isFunction( result.promise ) ) {
+ return result.promise();
+ } else {
+ return $.Deferred().resolve( !!result ).promise();
+ }
+ } else {
+ return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
+ }
+};
+
+/**
+ * Get the validity of current value.
+ *
+ * This method returns a promise that resolves if the value is valid and rejects if
+ * it isn't. Uses the {@link #validate validation pattern} to check for validity.
+ *
+ * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
+ */
+OO.ui.TextInputWidget.prototype.getValidity = function () {
+ var result;
+
+ function rejectOrResolve( valid ) {
+ if ( valid ) {
+ return $.Deferred().resolve().promise();
+ } else {
+ return $.Deferred().reject().promise();
+ }
+ }
+
+ if ( this.validate instanceof Function ) {
+ result = this.validate( this.getValue() );
+ if ( result && $.isFunction( result.promise ) ) {
+ return result.promise().then( function ( valid ) {
+ return rejectOrResolve( valid );
+ } );
+ } else {
+ return rejectOrResolve( result );
+ }
+ } else {
+ return rejectOrResolve( this.getValue().match( this.validate ) );
+ }
+};
+
+/**
+ * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
+ *
+ * @param {string} labelPosition Label position, 'before' or 'after'
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
+ this.labelPosition = labelPosition;
+ this.updatePosition();
+ return this;
+};
+
+/**
+ * Update the position of the inline label.
+ *
+ * This method is called by #setLabelPosition, and can also be called on its own if
+ * something causes the label to be mispositioned.
+ *
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.updatePosition = function () {
+ var after = this.labelPosition === 'after';
+
+ this.$element
+ .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
+ .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
+
+ this.valCache = null;
+ this.scrollWidth = null;
+ this.adjustSize();
+ this.positionLabel();
+
+ return this;
+};
+
+/**
+ * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
+ * already empty or when it's not editable.
+ */
+OO.ui.TextInputWidget.prototype.updateSearchIndicator = function () {
+ if ( this.type === 'search' ) {
+ if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
+ this.setIndicator( null );
+ } else {
+ this.setIndicator( 'clear' );
+ }
+ }
+};
+
+/**
+ * Position the label by setting the correct padding on the input.
+ *
+ * @private
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.positionLabel = function () {
+ var after, rtl, property;
+ // Clear old values
+ this.$input
+ // Clear old values if present
+ .css( {
+ 'padding-right': '',
+ 'padding-left': ''
+ } );
+
+ if ( this.label ) {
+ this.$element.append( this.$label );
+ } else {
+ this.$label.detach();
+ return;
+ }
+
+ after = this.labelPosition === 'after';
+ rtl = this.$element.css( 'direction' ) === 'rtl';
+ property = after === rtl ? 'padding-left' : 'padding-right';
+
+ this.$input.css( property, this.$label.outerWidth( true ) + ( after ? this.scrollWidth : 0 ) );
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.TextInputWidget.prototype.restorePreInfuseState = function ( state ) {
+ OO.ui.TextInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
+ if ( state.scrollTop !== undefined ) {
+ this.$input.scrollTop( state.scrollTop );
+ }
+};
+
+/**
+ * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
+ * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
+ * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
+ *
+ * - by typing a value in the text input field. If the value exactly matches the value of a menu
+ * option, that option will appear to be selected.
+ * - by choosing a value from the menu. The value of the chosen option will then appear in the text
+ * input field.
+ *
+ * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
+ *
+ * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * @example
+ * // Example: A ComboBoxInputWidget.
+ * var comboBox = new OO.ui.ComboBoxInputWidget( {
+ * label: 'ComboBoxInputWidget',
+ * value: 'Option 1',
+ * menu: {
+ * items: [
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'Option 1',
+ * label: 'Option One'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'Option 2',
+ * label: 'Option Two'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'Option 3',
+ * label: 'Option Three'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'Option 4',
+ * label: 'Option Four'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'Option 5',
+ * label: 'Option Five'
+ * } )
+ * ]
+ * }
+ * } );
+ * $( 'body' ).append( comboBox.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
+ *
+ * @class
+ * @extends OO.ui.TextInputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
+ * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
+ * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
+ * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
+ * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
+ */
+OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
+ // Configuration initialization
+ config = $.extend( {
+ indicator: 'down'
+ }, config );
+ // For backwards-compatibility with ComboBoxWidget config
+ $.extend( config, config.input );
+
+ // Parent constructor
+ OO.ui.ComboBoxInputWidget.parent.call( this, config );
+
+ // Properties
+ this.$overlay = config.$overlay || this.$element;
+ this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
+ {
+ widget: this,
+ input: this,
+ $container: this.$element,
+ disabled: this.isDisabled()
+ },
+ config.menu
+ ) );
+ // For backwards-compatibility with ComboBoxWidget
+ this.input = this;
+
+ // Events
+ this.$indicator.on( {
+ click: this.onIndicatorClick.bind( this ),
+ keypress: this.onIndicatorKeyPress.bind( this )
+ } );
+ this.connect( this, {
+ change: 'onInputChange',
+ enter: 'onInputEnter'
+ } );
+ this.menu.connect( this, {
+ choose: 'onMenuChoose',
+ add: 'onMenuItemsChange',
+ remove: 'onMenuItemsChange'
+ } );
+
+ // Initialization
+ this.$input.attr( {
+ role: 'combobox',
+ 'aria-autocomplete': 'list'
+ } );
+ // Do not override options set via config.menu.items
+ if ( config.options !== undefined ) {
+ this.setOptions( config.options );
+ }
+ // Extra class for backwards-compatibility with ComboBoxWidget
+ this.$element.addClass( 'oo-ui-comboBoxInputWidget oo-ui-comboBoxWidget' );
+ this.$overlay.append( this.menu.$element );
+ this.onMenuItemsChange();
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ComboBoxInputWidget, OO.ui.TextInputWidget );
+
+/* Methods */
+
+/**
+ * Get the combobox's menu.
+ * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
+ */
+OO.ui.ComboBoxInputWidget.prototype.getMenu = function () {
+ return this.menu;
+};
+
+/**
+ * Get the combobox's text input widget.
+ * @return {OO.ui.TextInputWidget} Text input widget
+ */
+OO.ui.ComboBoxInputWidget.prototype.getInput = function () {
+ return this;
+};
+
+/**
+ * Handle input change events.
+ *
+ * @private
+ * @param {string} value New value
+ */
+OO.ui.ComboBoxInputWidget.prototype.onInputChange = function ( value ) {
+ var match = this.menu.getItemFromData( value );
+
+ this.menu.selectItem( match );
+ if ( this.menu.getHighlightedItem() ) {
+ this.menu.highlightItem( match );
+ }
+
+ if ( !this.isDisabled() ) {
+ this.menu.toggle( true );
+ }
+};
+
+/**
+ * Handle mouse click events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse click event
+ */
+OO.ui.ComboBoxInputWidget.prototype.onIndicatorClick = function ( e ) {
+ if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
+ this.menu.toggle();
+ this.$input[ 0 ].focus();
+ }
+ return false;
+};
+
+/**
+ * Handle key press events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ */
+OO.ui.ComboBoxInputWidget.prototype.onIndicatorKeyPress = function ( e ) {
+ if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
+ this.menu.toggle();
+ this.$input[ 0 ].focus();
+ return false;
+ }
+};
+
+/**
+ * Handle input enter events.
+ *
+ * @private
+ */
+OO.ui.ComboBoxInputWidget.prototype.onInputEnter = function () {
+ if ( !this.isDisabled() ) {
+ this.menu.toggle( false );
+ }
+};
+
+/**
+ * Handle menu choose events.
+ *
+ * @private
+ * @param {OO.ui.OptionWidget} item Chosen item
+ */
+OO.ui.ComboBoxInputWidget.prototype.onMenuChoose = function ( item ) {
+ this.setValue( item.getData() );
+};
+
+/**
+ * Handle menu item change events.
+ *
+ * @private
+ */
+OO.ui.ComboBoxInputWidget.prototype.onMenuItemsChange = function () {
+ var match = this.menu.getItemFromData( this.getValue() );
+ this.menu.selectItem( match );
+ if ( this.menu.getHighlightedItem() ) {
+ this.menu.highlightItem( match );
+ }
+ this.$element.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu.isEmpty() );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ComboBoxInputWidget.prototype.setDisabled = function ( disabled ) {
+ // Parent method
+ OO.ui.ComboBoxInputWidget.parent.prototype.setDisabled.call( this, disabled );
+
+ if ( this.menu ) {
+ this.menu.setDisabled( this.isDisabled() );
+ }
+
+ return this;
+};
+
+/**
+ * Set the options available for this input.
+ *
+ * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
+ * @chainable
+ */
+OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) {
+ this.getMenu()
+ .clearItems()
+ .addItems( options.map( function ( opt ) {
+ return new OO.ui.MenuOptionWidget( {
+ data: opt.data,
+ label: opt.label !== undefined ? opt.label : opt.data
+ } );
+ } ) );
+
+ return this;
+};
+
+/**
+ * @class
+ * @deprecated Use OO.ui.ComboBoxInputWidget instead.
+ */
+OO.ui.ComboBoxWidget = OO.ui.ComboBoxInputWidget;
+
+/**
+ * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
+ * which is a widget that is specified by reference before any optional configuration settings.
+ *
+ * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
+ *
+ * - **left**: The label is placed before the field-widget and aligned with the left margin.
+ * A left-alignment is used for forms with many fields.
+ * - **right**: The label is placed before the field-widget and aligned to the right margin.
+ * A right-alignment is used for long but familiar forms which users tab through,
+ * verifying the current field with a quick glance at the label.
+ * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
+ * that users fill out from top to bottom.
+ * - **inline**: The label is placed after the field-widget and aligned to the left.
+ * An inline-alignment is best used with checkboxes or radio buttons.
+ *
+ * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
+ * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
+ * @class
+ * @extends OO.ui.Layout
+ * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.TitledElement
+ *
+ * @constructor
+ * @param {OO.ui.Widget} fieldWidget Field widget
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
+ * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
+ * The array may contain strings or OO.ui.HtmlSnippet instances.
+ * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
+ * The array may contain strings or OO.ui.HtmlSnippet instances.
+ * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
+ * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
+ * For important messages, you are advised to use `notices`, as they are always shown.
+ *
+ * @throws {Error} An error is thrown if no widget is specified
+ */
+OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
+ var hasInputWidget, div;
+
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
+ config = fieldWidget;
+ fieldWidget = config.fieldWidget;
+ }
+
+ // Make sure we have required constructor arguments
+ if ( fieldWidget === undefined ) {
+ throw new Error( 'Widget not found' );
+ }
+
+ hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel;
+
+ // Configuration initialization
+ config = $.extend( { align: 'left' }, config );
+
+ // Parent constructor
+ OO.ui.FieldLayout.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.LabelElement.call( this, config );
+ OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
+
+ // Properties
+ this.fieldWidget = fieldWidget;
+ this.errors = [];
+ this.notices = [];
+ this.$field = $( '<div>' );
+ this.$messages = $( '<ul>' );
+ this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
+ this.align = null;
+ if ( config.help ) {
+ this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ classes: [ 'oo-ui-fieldLayout-help' ],
+ framed: false,
+ icon: 'info'
+ } );
+
+ div = $( '<div>' );
+ if ( config.help instanceof OO.ui.HtmlSnippet ) {
+ div.html( config.help.toString() );
+ } else {
+ div.text( config.help );
+ }
+ this.popupButtonWidget.getPopup().$body.append(
+ div.addClass( 'oo-ui-fieldLayout-help-content' )
+ );
+ this.$help = this.popupButtonWidget.$element;
+ } else {
+ this.$help = $( [] );
+ }
+
+ // Events
+ if ( hasInputWidget ) {
+ this.$label.on( 'click', this.onLabelClick.bind( this ) );
+ }
+ this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-fieldLayout' )
+ .append( this.$help, this.$body );
+ this.$body.addClass( 'oo-ui-fieldLayout-body' );
+ this.$messages.addClass( 'oo-ui-fieldLayout-messages' );
+ this.$field
+ .addClass( 'oo-ui-fieldLayout-field' )
+ .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
+ .append( this.fieldWidget.$element );
+
+ this.setErrors( config.errors || [] );
+ this.setNotices( config.notices || [] );
+ this.setAlignment( config.align );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
+OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.TitledElement );
+
+/* Methods */
+
+/**
+ * Handle field disable events.
+ *
+ * @private
+ * @param {boolean} value Field is disabled
+ */
+OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
+ this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
+};
+
+/**
+ * Handle label mouse click events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse click event
+ */
+OO.ui.FieldLayout.prototype.onLabelClick = function () {
+ this.fieldWidget.simulateLabelClick();
+ return false;
+};
+
+/**
+ * Get the widget contained by the field.
+ *
+ * @return {OO.ui.Widget} Field widget
+ */
+OO.ui.FieldLayout.prototype.getField = function () {
+ return this.fieldWidget;
+};
+
+/**
+ * @protected
+ * @param {string} kind 'error' or 'notice'
+ * @param {string|OO.ui.HtmlSnippet} text
+ * @return {jQuery}
+ */
+OO.ui.FieldLayout.prototype.makeMessage = function ( kind, text ) {
+ var $listItem, $icon, message;
+ $listItem = $( '<li>' );
+ if ( kind === 'error' ) {
+ $icon = new OO.ui.IconWidget( { icon: 'alert', flags: [ 'warning' ] } ).$element;
+ } else if ( kind === 'notice' ) {
+ $icon = new OO.ui.IconWidget( { icon: 'info' } ).$element;
+ } else {
+ $icon = '';
+ }
+ message = new OO.ui.LabelWidget( { label: text } );
+ $listItem
+ .append( $icon, message.$element )
+ .addClass( 'oo-ui-fieldLayout-messages-' + kind );
+ return $listItem;
+};
+
+/**
+ * Set the field alignment mode.
+ *
+ * @private
+ * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
+ * @chainable
+ */
+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.$body.append( this.$field, this.$label );
+ } else {
+ this.$body.append( this.$label, this.$field );
+ }
+ // Set classes. The following classes can be used here:
+ // * oo-ui-fieldLayout-align-left
+ // * oo-ui-fieldLayout-align-right
+ // * oo-ui-fieldLayout-align-top
+ // * oo-ui-fieldLayout-align-inline
+ if ( this.align ) {
+ this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
+ }
+ this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
+ this.align = value;
+ }
+
+ return this;
+};
+
+/**
+ * Set the list of error messages.
+ *
+ * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
+ * The array may contain strings or OO.ui.HtmlSnippet instances.
+ * @chainable
+ */
+OO.ui.FieldLayout.prototype.setErrors = function ( errors ) {
+ this.errors = errors.slice();
+ this.updateMessages();
+ return this;
+};
+
+/**
+ * Set the list of notice messages.
+ *
+ * @param {Array} notices Notices about the widget, which will be displayed below the widget.
+ * The array may contain strings or OO.ui.HtmlSnippet instances.
+ * @chainable
+ */
+OO.ui.FieldLayout.prototype.setNotices = function ( notices ) {
+ this.notices = notices.slice();
+ this.updateMessages();
+ return this;
+};
+
+/**
+ * Update the rendering of error and notice messages.
+ *
+ * @private
+ */
+OO.ui.FieldLayout.prototype.updateMessages = function () {
+ var i;
+ this.$messages.empty();
+
+ if ( this.errors.length || this.notices.length ) {
+ this.$body.after( this.$messages );
+ } else {
+ this.$messages.remove();
+ return;
+ }
+
+ for ( i = 0; i < this.notices.length; i++ ) {
+ this.$messages.append( this.makeMessage( 'notice', this.notices[ i ] ) );
+ }
+ for ( i = 0; i < this.errors.length; i++ ) {
+ this.$messages.append( this.makeMessage( 'error', this.errors[ i ] ) );
+ }
+};
+
+/**
+ * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
+ * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
+ * is required and is specified before any optional configuration settings.
+ *
+ * Labels can be aligned in one of four ways:
+ *
+ * - **left**: The label is placed before the field-widget and aligned with the left margin.
+ * A left-alignment is used for forms with many fields.
+ * - **right**: The label is placed before the field-widget and aligned to the right margin.
+ * A right-alignment is used for long but familiar forms which users tab through,
+ * verifying the current field with a quick glance at the label.
+ * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
+ * that users fill out from top to bottom.
+ * - **inline**: The label is placed after the field-widget and aligned to the left.
+ * An inline-alignment is best used with checkboxes or radio buttons.
+ *
+ * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
+ * text is specified.
+ *
+ * @example
+ * // Example of an ActionFieldLayout
+ * var actionFieldLayout = new OO.ui.ActionFieldLayout(
+ * new OO.ui.TextInputWidget( {
+ * placeholder: 'Field widget'
+ * } ),
+ * new OO.ui.ButtonWidget( {
+ * label: 'Button'
+ * } ),
+ * {
+ * label: 'An ActionFieldLayout. This label is aligned top',
+ * align: 'top',
+ * help: 'This is help text'
+ * }
+ * );
+ *
+ * $( 'body' ).append( actionFieldLayout.$element );
+ *
+ * @class
+ * @extends OO.ui.FieldLayout
+ *
+ * @constructor
+ * @param {OO.ui.Widget} fieldWidget Field widget
+ * @param {OO.ui.ButtonWidget} buttonWidget Button widget
+ */
+OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
+ config = fieldWidget;
+ fieldWidget = config.fieldWidget;
+ buttonWidget = config.buttonWidget;
+ }
+
+ // Parent constructor
+ OO.ui.ActionFieldLayout.parent.call( this, fieldWidget, config );
+
+ // Properties
+ this.buttonWidget = buttonWidget;
+ this.$button = $( '<div>' );
+ this.$input = $( '<div>' );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-actionFieldLayout' );
+ this.$button
+ .addClass( 'oo-ui-actionFieldLayout-button' )
+ .append( this.buttonWidget.$element );
+ this.$input
+ .addClass( 'oo-ui-actionFieldLayout-input' )
+ .append( this.fieldWidget.$element );
+ this.$field
+ .append( this.$input, this.$button );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
+
+/**
+ * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
+ * which each contain an individual widget and, optionally, a label. Each Fieldset can be
+ * configured with a label as well. For more information and examples,
+ * please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * @example
+ * // Example of a fieldset layout
+ * var input1 = new OO.ui.TextInputWidget( {
+ * placeholder: 'A text input field'
+ * } );
+ *
+ * var input2 = new OO.ui.TextInputWidget( {
+ * placeholder: 'A text input field'
+ * } );
+ *
+ * var fieldset = new OO.ui.FieldsetLayout( {
+ * label: 'Example of a fieldset layout'
+ * } );
+ *
+ * fieldset.addItems( [
+ * new OO.ui.FieldLayout( input1, {
+ * label: 'Field One'
+ * } ),
+ * new OO.ui.FieldLayout( input2, {
+ * label: 'Field Two'
+ * } )
+ * ] );
+ * $( 'body' ).append( fieldset.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
+ *
+ * @class
+ * @extends OO.ui.Layout
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
+ */
+OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.FieldsetLayout.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.IconElement.call( this, config );
+ OO.ui.mixin.LabelElement.call( this, config );
+ OO.ui.mixin.GroupElement.call( this, config );
+
+ if ( config.help ) {
+ this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ classes: [ 'oo-ui-fieldsetLayout-help' ],
+ framed: false,
+ icon: 'info'
+ } );
+
+ this.popupButtonWidget.getPopup().$body.append(
+ $( '<div>' )
+ .text( config.help )
+ .addClass( 'oo-ui-fieldsetLayout-help-content' )
+ );
+ this.$help = this.popupButtonWidget.$element;
+ } else {
+ this.$help = $( [] );
+ }
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-fieldsetLayout' )
+ .prepend( this.$help, this.$icon, this.$label, this.$group );
+ if ( Array.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
+
+/**
+ * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
+ * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
+ * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
+ * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
+ *
+ * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
+ * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
+ * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
+ * some fancier controls. Some controls have both regular and InputWidget variants, for example
+ * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
+ * often have simplified APIs to match the capabilities of HTML forms.
+ * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
+ * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
+ *
+ * @example
+ * // Example of a form layout that wraps a fieldset layout
+ * var input1 = new OO.ui.TextInputWidget( {
+ * placeholder: 'Username'
+ * } );
+ * var input2 = new OO.ui.TextInputWidget( {
+ * placeholder: 'Password',
+ * type: 'password'
+ * } );
+ * var submit = new OO.ui.ButtonInputWidget( {
+ * label: 'Submit'
+ * } );
+ *
+ * var fieldset = new OO.ui.FieldsetLayout( {
+ * label: 'A form layout'
+ * } );
+ * fieldset.addItems( [
+ * new OO.ui.FieldLayout( input1, {
+ * label: 'Username',
+ * align: 'top'
+ * } ),
+ * new OO.ui.FieldLayout( input2, {
+ * label: 'Password',
+ * align: 'top'
+ * } ),
+ * new OO.ui.FieldLayout( submit )
+ * ] );
+ * var form = new OO.ui.FormLayout( {
+ * items: [ fieldset ],
+ * action: '/api/formhandler',
+ * method: 'get'
+ * } )
+ * $( 'body' ).append( form.$element );
+ *
+ * @class
+ * @extends OO.ui.Layout
+ * @mixins OO.ui.mixin.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [method] HTML form `method` attribute
+ * @cfg {string} [action] HTML form `action` attribute
+ * @cfg {string} [enctype] HTML form `enctype` attribute
+ * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
+ */
+OO.ui.FormLayout = function OoUiFormLayout( config ) {
+ var action;
+
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.FormLayout.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
+
+ // Events
+ this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
+
+ // Make sure the action is safe
+ action = config.action;
+ if ( action !== undefined && !OO.ui.isSafeUrl( action ) ) {
+ action = './' + action;
+ }
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-formLayout' )
+ .attr( {
+ method: config.method,
+ action: action,
+ enctype: config.enctype
+ } );
+ if ( Array.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
+OO.mixinClass( OO.ui.FormLayout, OO.ui.mixin.GroupElement );
+
+/* Events */
+
+/**
+ * A 'submit' event is emitted when the form is submitted.
+ *
+ * @event submit
+ */
+
+/* Static Properties */
+
+OO.ui.FormLayout.static.tagName = 'form';
+
+/* Methods */
+
+/**
+ * Handle form submit events.
+ *
+ * @private
+ * @param {jQuery.Event} e Submit event
+ * @fires submit
+ */
+OO.ui.FormLayout.prototype.onFormSubmit = function () {
+ if ( this.emit( 'submit' ) ) {
+ return false;
+ }
+};
+
+/**
+ * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
+ * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
+ *
+ * @example
+ * // Example of a panel layout
+ * var panel = new OO.ui.PanelLayout( {
+ * expanded: false,
+ * framed: true,
+ * padded: true,
+ * $content: $( '<p>A panel layout with padding and a frame.</p>' )
+ * } );
+ * $( 'body' ).append( panel.$element );
+ *
+ * @class
+ * @extends OO.ui.Layout
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [scrollable=false] Allow vertical scrolling
+ * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
+ * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
+ * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
+ */
+OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
+ // Configuration initialization
+ config = $.extend( {
+ scrollable: false,
+ padded: false,
+ expanded: true,
+ framed: false
+ }, config );
+
+ // Parent constructor
+ OO.ui.PanelLayout.parent.call( this, config );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-panelLayout' );
+ if ( config.scrollable ) {
+ this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
+ }
+ if ( config.padded ) {
+ this.$element.addClass( 'oo-ui-panelLayout-padded' );
+ }
+ if ( config.expanded ) {
+ this.$element.addClass( 'oo-ui-panelLayout-expanded' );
+ }
+ if ( config.framed ) {
+ this.$element.addClass( 'oo-ui-panelLayout-framed' );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
+
+/* Methods */
+
+/**
+ * Focus the panel layout
+ *
+ * The default implementation just focuses the first focusable element in the panel
+ */
+OO.ui.PanelLayout.prototype.focus = function () {
+ OO.ui.findFocusable( this.$element ).focus();
+};
+
+/**
+ * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
+ * items), with small margins between them. Convenient when you need to put a number of block-level
+ * widgets on a single line next to each other.
+ *
+ * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
+ *
+ * @example
+ * // HorizontalLayout with a text input and a label
+ * var layout = new OO.ui.HorizontalLayout( {
+ * items: [
+ * new OO.ui.LabelWidget( { label: 'Label' } ),
+ * new OO.ui.TextInputWidget( { value: 'Text' } )
+ * ]
+ * } );
+ * $( 'body' ).append( layout.$element );
+ *
+ * @class
+ * @extends OO.ui.Layout
+ * @mixins OO.ui.mixin.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
+ */
+OO.ui.HorizontalLayout = function OoUiHorizontalLayout( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.HorizontalLayout.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-horizontalLayout' );
+ if ( Array.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.HorizontalLayout, OO.ui.Layout );
+OO.mixinClass( OO.ui.HorizontalLayout, OO.ui.mixin.GroupElement );
+
+}( OO ) );
+++ /dev/null
-/*!
- * OOjs UI v0.15.2
- * https://www.mediawiki.org/wiki/OOjs_UI
- *
- * Copyright 2011–2016 OOjs UI Team and other contributors.
- * Released under the MIT license
- * http://oojs.mit-license.org
- *
- * Date: 2016-02-02T22:07:06Z
- */
-@-webkit-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-moz-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-.oo-ui-element-hidden {
- display: none !important;
-}
-.oo-ui-buttonElement > .oo-ui-buttonElement-button {
- cursor: pointer;
- display: inline-block;
- vertical-align: middle;
- font: inherit;
- white-space: nowrap;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-.oo-ui-buttonElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon,
-.oo-ui-buttonElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
- display: none;
-}
-.oo-ui-buttonElement.oo-ui-widget-disabled > .oo-ui-buttonElement-button {
- cursor: default;
-}
-.oo-ui-buttonElement.oo-ui-indicatorElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator,
-.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-buttonElement-frameless {
- display: inline-block;
- position: relative;
-}
-.oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button {
- display: inline-block;
- vertical-align: top;
- text-align: center;
-}
-.oo-ui-buttonElement-framed.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled > .oo-ui-buttonElement-button,
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button,
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- cursor: default;
-}
-.oo-ui-buttonElement > .oo-ui-buttonElement-button {
- font-weight: bold;
- text-decoration: none;
-}
-.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
- margin-left: 0;
-}
-.oo-ui-buttonElement.oo-ui-indicatorElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
- width: 0.9375em;
- height: 0.9375em;
-}
-.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
- margin-left: 0.46875em;
-}
-.oo-ui-buttonElement.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
- width: 1.875em;
- height: 1.875em;
-}
-.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button:focus {
- box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.2);
- outline: none;
-}
-.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button .oo-ui-indicatorElement-indicator {
- margin-right: 0;
-}
-.oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- margin-left: 0.25em;
- margin-right: 0.25em;
-}
-.oo-ui-buttonElement-frameless > input.oo-ui-buttonElement-button {
- padding-left: 0.25em;
- padding-right: 0.25em;
- color: #333333;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled > input.oo-ui-buttonElement-button,
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #555555;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > input.oo-ui-buttonElement-button,
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #444444;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:hover > .oo-ui-labelElement-label,
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:focus > .oo-ui-labelElement-label {
- color: #2962cc;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #347bff;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active > .oo-ui-labelElement-label,
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #1f4999;
- box-shadow: none;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:hover > .oo-ui-labelElement-label,
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:focus > .oo-ui-labelElement-label {
- color: #008064;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #00af89;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active > .oo-ui-labelElement-label,
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #005946;
- box-shadow: none;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:hover > .oo-ui-labelElement-label,
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:focus > .oo-ui-labelElement-label {
- color: #8c130d;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #d11d13;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active > .oo-ui-labelElement-label,
-.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- color: #73100a;
- box-shadow: none;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonElement-button {
- color: #cccccc;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonElement-button:focus {
- box-shadow: none;
-}
-.oo-ui-buttonElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon,
-.oo-ui-buttonElement-frameless.oo-ui-widget-disabled > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
- opacity: 0.2;
-}
-.oo-ui-buttonElement-framed.oo-ui-iconElement.oo-ui-labelElement > .oo-ui-buttonElement-button,
-.oo-ui-buttonElement-framed.oo-ui-iconElement.oo-ui-indicatorElement > .oo-ui-buttonElement-button {
- padding-left: 2.4em;
-}
-.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button {
- padding: 0.5em 1em;
- min-height: 1.2em;
- min-width: 1em;
- border-radius: 2px;
- position: relative;
- -webkit-transition: background 100ms ease, color 100ms ease, border-color 100ms ease, box-shadow 100ms ease;
- -moz-transition: background 100ms ease, color 100ms ease, border-color 100ms ease, box-shadow 100ms ease;
- transition: background 100ms ease, color 100ms ease, border-color 100ms ease, box-shadow 100ms ease;
-}
-.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button:hover,
-.oo-ui-buttonElement-framed > .oo-ui-buttonElement-button:focus {
- outline: none;
-}
-.oo-ui-buttonElement-framed > input.oo-ui-buttonElement-button,
-.oo-ui-buttonElement-framed.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- line-height: 1.2em;
- display: inline-block;
-}
-.oo-ui-buttonElement-framed.oo-ui-iconElement > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
- position: absolute;
- top: 0.2em;
- left: 0.5625em;
-}
-.oo-ui-buttonElement-framed.oo-ui-iconElement.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- margin-left: 0.3em;
-}
-.oo-ui-buttonElement-framed.oo-ui-indicatorElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
- display: inline-block;
-}
-.oo-ui-buttonElement-framed.oo-ui-indicatorElement.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator,
-.oo-ui-buttonElement-framed.oo-ui-indicatorElement.oo-ui-iconElement:not( .oo-ui-labelElement ) > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
- margin-left: 0.46875em;
- margin-right: -0.275em;
-}
-.oo-ui-buttonElement-framed.oo-ui-indicatorElement.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator {
- position: relative;
- left: 0.2em;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-disabled > .oo-ui-buttonElement-button {
- background: #dddddd;
- color: #ffffff;
- border: 1px solid #dddddd;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button {
- color: #555555;
- background-color: #ffffff;
- border: 1px solid #cccccc;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button:hover {
- background-color: #ebebeb;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button:focus {
- box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- background-color: #d9d9d9;
- border-color: #d9d9d9;
- box-shadow: none;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
- background-color: #999999;
- color: #ffffff;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button {
- color: #347bff;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:hover {
- background-color: rgba(52, 123, 255, 0.1);
- border-color: rgba(31, 73, 153, 0.5);
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:focus {
- box-shadow: inset 0 0 0 1px #1f4999;
- border-color: #1f4999;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- color: #1f4999;
- border-color: #1f4999;
- box-shadow: none;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
- background-color: #999999;
- color: #ffffff;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button {
- color: #00af89;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:hover {
- background-color: rgba(0, 171, 137, 0.1);
- border-color: rgba(0, 89, 70, 0.5);
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:focus {
- box-shadow: inset 0 0 0 1px #005946;
- border-color: #005946;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- color: #005946;
- border-color: #005946;
- box-shadow: none;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
- background-color: #999999;
- color: #ffffff;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button {
- color: #d11d13;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:hover {
- background-color: rgba(209, 29, 19, 0.1);
- border-color: rgba(115, 16, 10, 0.5);
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:focus {
- box-shadow: inset 0 0 0 1px #73100a;
- border-color: #73100a;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- color: #73100a;
- border-color: #73100a;
- box-shadow: none;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
- background-color: #999999;
- color: #ffffff;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button {
- color: #ffffff;
- background-color: #347bff;
- border-color: #347bff;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:hover {
- background: #2962cc;
- border-color: #2962cc;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:focus {
- box-shadow: inset 0 0 0 1px #ffffff;
- border-color: #347bff;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- color: #ffffff;
- background-color: #1f4999;
- border-color: #1f4999;
- box-shadow: none;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
- background-color: #999999;
- color: #ffffff;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button {
- color: #ffffff;
- background-color: #00af89;
- border-color: #00af89;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:hover {
- background: #008064;
- border-color: #008064;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:focus {
- box-shadow: inset 0 0 0 1px #ffffff;
- border-color: #00af89;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- color: #ffffff;
- background-color: #005946;
- border-color: #005946;
- box-shadow: none;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-constructive.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
- background-color: #999999;
- color: #ffffff;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button {
- color: #ffffff;
- background-color: #d11d13;
- border-color: #d11d13;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:hover {
- background: #8c130d;
- border-color: #8c130d;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:focus {
- box-shadow: inset 0 0 0 1px #ffffff;
- border-color: #d11d13;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
- color: #ffffff;
- background-color: #73100a;
- border-color: #73100a;
- box-shadow: none;
-}
-.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
- background-color: #999999;
- color: #ffffff;
-}
-.oo-ui-clippableElement-clippable {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-iconElement .oo-ui-iconElement-icon,
-.oo-ui-iconElement.oo-ui-iconElement-icon {
- background-size: contain;
- background-position: center center;
- background-repeat: no-repeat;
-}
-.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator,
-.oo-ui-indicatorElement.oo-ui-indicatorElement-indicator {
- background-size: contain;
- background-position: center center;
- background-repeat: no-repeat;
-}
-.oo-ui-pendingElement-pending {
- background-image: /* @embed */ url(themes/mediawiki/images/textures/pending.gif);
-}
-.oo-ui-fieldLayout {
- display: block;
- margin-bottom: 1em;
-}
-.oo-ui-fieldLayout:before,
-.oo-ui-fieldLayout:after {
- content: " ";
- display: table;
-}
-.oo-ui-fieldLayout:after {
- clear: both;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
- display: block;
- float: left;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- text-align: right;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body {
- display: table;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
- display: table-cell;
- vertical-align: middle;
-}
-.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- display: inline-block;
-}
-.oo-ui-fieldLayout > .oo-ui-fieldLayout-help {
- float: right;
-}
-.oo-ui-fieldLayout > .oo-ui-fieldLayout-help > .oo-ui-popupWidget > .oo-ui-popupWidget-popup {
- z-index: 1;
-}
-.oo-ui-fieldLayout > .oo-ui-fieldLayout-help .oo-ui-fieldLayout-help-content {
- padding: 0.5em 0.75em;
- line-height: 1.5em;
-}
-.oo-ui-fieldLayout:last-child {
- margin-bottom: 0;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- padding-top: 0.5em;
- margin-right: 5%;
- width: 35%;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field,
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
- width: 60%;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline {
- margin-bottom: 1.25em;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- padding: 0.25em 0.25em 0.25em 1em;
-}
-.oo-ui-fieldLayout.oo-ui-fieldLayout-align-top.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- padding-top: 0.25em;
- padding-bottom: 0.5em;
-}
-.oo-ui-fieldLayout > .oo-ui-popupButtonWidget {
- margin-right: 0;
-}
-.oo-ui-fieldLayout > .oo-ui-popupButtonWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-fieldLayout-disabled > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label {
- color: #cccccc;
-}
-.oo-ui-fieldLayout-messages {
- list-style: none none;
- margin: 0.25em 0 0 0.25em;
- padding: 0;
-}
-.oo-ui-fieldLayout-messages > li {
- margin: 0;
- padding: 0;
- display: table;
-}
-.oo-ui-fieldLayout-messages .oo-ui-iconWidget {
- display: table-cell;
- border-right: 0.5em solid transparent;
-}
-.oo-ui-fieldLayout-messages .oo-ui-labelWidget {
- display: table-cell;
- padding: 0;
- line-height: 1.875em;
- vertical-align: middle;
-}
-.oo-ui-actionFieldLayout-input,
-.oo-ui-actionFieldLayout-button {
- display: table-cell;
- vertical-align: middle;
-}
-.oo-ui-actionFieldLayout-input {
- padding-right: 1em;
-}
-.oo-ui-actionFieldLayout-button {
- width: 1%;
- white-space: nowrap;
-}
-.oo-ui-fieldsetLayout {
- position: relative;
- margin: 0;
- padding: 0;
- border: 0;
-}
-.oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-iconElement-icon {
- display: block;
- position: absolute;
-}
-.oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label {
- display: inline-block;
-}
-.oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help {
- float: right;
-}
-.oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help > .oo-ui-popupWidget > .oo-ui-popupWidget-popup {
- z-index: 1;
-}
-.oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help .oo-ui-fieldsetLayout-help-content {
- padding: 0.5em 0.75em;
- line-height: 1.5em;
-}
-.oo-ui-fieldsetLayout + .oo-ui-fieldsetLayout,
-.oo-ui-fieldsetLayout + .oo-ui-formLayout {
- margin-top: 2em;
-}
-.oo-ui-fieldsetLayout > .oo-ui-labelElement-label {
- font-size: 1.1em;
- margin-bottom: 0.5em;
- padding: 0.25em 0;
- font-weight: bold;
-}
-.oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-labelElement-label {
- padding-left: 2em;
- line-height: 1.8em;
-}
-.oo-ui-fieldsetLayout.oo-ui-iconElement > .oo-ui-iconElement-icon {
- left: 0;
- top: 0.25em;
- width: 1.875em;
- height: 1.875em;
-}
-.oo-ui-fieldsetLayout > .oo-ui-popupButtonWidget {
- margin-right: 0;
-}
-.oo-ui-fieldsetLayout > .oo-ui-popupButtonWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-formLayout + .oo-ui-fieldsetLayout,
-.oo-ui-formLayout + .oo-ui-formLayout {
- margin-top: 2em;
-}
-.oo-ui-panelLayout {
- position: relative;
-}
-.oo-ui-panelLayout-scrollable {
- overflow-y: auto;
-}
-.oo-ui-panelLayout-expanded {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
-}
-.oo-ui-panelLayout-padded {
- padding: 1.25em;
-}
-.oo-ui-panelLayout-framed {
- border: 1px solid #aaaaaa;
- border-radius: 2px;
- box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
-}
-.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed {
- margin: 1em 0;
-}
-.oo-ui-horizontalLayout > .oo-ui-widget {
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-horizontalLayout > .oo-ui-layout {
- display: inline-block;
-}
-.oo-ui-horizontalLayout > .oo-ui-layout,
-.oo-ui-horizontalLayout > .oo-ui-widget {
- margin-right: 0.5em;
-}
-.oo-ui-horizontalLayout > .oo-ui-layout:last-child,
-.oo-ui-horizontalLayout > .oo-ui-widget:last-child {
- margin-right: 0;
-}
-.oo-ui-horizontalLayout > .oo-ui-layout {
- margin-bottom: 0;
-}
-.oo-ui-optionWidget {
- position: relative;
- display: block;
- padding: 0.25em 0.5em;
- border: 0;
-}
-.oo-ui-optionWidget.oo-ui-widget-enabled {
- cursor: pointer;
-}
-.oo-ui-optionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
- display: block;
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
-}
-.oo-ui-optionWidget-highlighted {
- background-color: #eeeeee;
-}
-.oo-ui-optionWidget .oo-ui-labelElement-label {
- line-height: 1.5em;
-}
-.oo-ui-selectWidget-depressed .oo-ui-optionWidget-selected,
-.oo-ui-selectWidget-pressed .oo-ui-optionWidget-pressed,
-.oo-ui-selectWidget-pressed .oo-ui-optionWidget-pressed.oo-ui-optionWidget-highlighted,
-.oo-ui-selectWidget-pressed .oo-ui-optionWidget-pressed.oo-ui-optionWidget-highlighted.oo-ui-optionWidget-selected {
- background-color: #d0d0d0;
-}
-.oo-ui-optionWidget.oo-ui-widget-disabled {
- color: #cccccc;
-}
-.oo-ui-decoratedOptionWidget {
- padding: 0.5em 2em 0.5em 3em;
-}
-.oo-ui-decoratedOptionWidget .oo-ui-iconElement-icon,
-.oo-ui-decoratedOptionWidget .oo-ui-indicatorElement-indicator {
- position: absolute;
-}
-.oo-ui-decoratedOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
-.oo-ui-decoratedOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- top: 0;
- height: 100%;
-}
-.oo-ui-decoratedOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
- width: 1.875em;
- left: 0.5em;
-}
-.oo-ui-decoratedOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- width: 0.9375em;
- right: 0.5em;
-}
-.oo-ui-decoratedOptionWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
-.oo-ui-decoratedOptionWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
- opacity: 0.2;
-}
-.oo-ui-radioOptionWidget {
- cursor: default;
- padding: 0.25em 0;
- background-color: transparent;
-}
-.oo-ui-radioOptionWidget .oo-ui-radioInputWidget,
-.oo-ui-radioOptionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-radioOptionWidget.oo-ui-optionWidget-selected,
-.oo-ui-radioOptionWidget.oo-ui-optionWidget-pressed,
-.oo-ui-radioOptionWidget.oo-ui-optionWidget-highlighted {
- background-color: transparent;
-}
-.oo-ui-radioOptionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
- padding: 0.25em 0.25em 0.25em 1em;
-}
-.oo-ui-radioOptionWidget .oo-ui-radioInputWidget {
- margin-right: 0;
-}
-.oo-ui-labelWidget {
- display: inline-block;
-}
-.oo-ui-iconWidget {
- display: inline-block;
- vertical-align: middle;
- line-height: 2.5em;
- width: 1.875em;
- height: 1.875em;
-}
-.oo-ui-iconWidget.oo-ui-widget-disabled {
- opacity: 0.2;
-}
-.oo-ui-indicatorWidget {
- display: inline-block;
- vertical-align: middle;
- line-height: 2.5em;
- width: 0.9375em;
- height: 0.9375em;
- margin: 0.46875em;
-}
-.oo-ui-indicatorWidget.oo-ui-widget-disabled {
- opacity: 0.2;
-}
-.oo-ui-buttonWidget {
- display: inline-block;
- vertical-align: middle;
- margin-right: 0.5em;
-}
-.oo-ui-buttonWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-buttonGroupWidget {
- display: inline-block;
- white-space: nowrap;
- border-radius: 2px;
- margin-right: 0.5em;
-}
-.oo-ui-buttonGroupWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-buttonGroupWidget .oo-ui-buttonElement {
- margin-right: 0;
-}
-.oo-ui-buttonGroupWidget .oo-ui-buttonElement:last-child {
- margin-right: 0;
-}
-.oo-ui-buttonGroupWidget .oo-ui-buttonElement-framed .oo-ui-buttonElement-button {
- border-radius: 0;
- margin-left: -1px;
-}
-.oo-ui-buttonGroupWidget .oo-ui-buttonElement-framed:first-child .oo-ui-buttonElement-button {
- border-bottom-left-radius: 2px;
- border-top-left-radius: 2px;
- margin-left: 0;
-}
-.oo-ui-buttonGroupWidget .oo-ui-buttonElement-framed:last-child .oo-ui-buttonElement-button {
- border-bottom-right-radius: 2px;
- border-top-right-radius: 2px;
-}
-.oo-ui-popupWidget {
- position: absolute;
- /* @noflip */
- left: 0;
-}
-.oo-ui-popupWidget-popup {
- position: relative;
- overflow: hidden;
- z-index: 1;
-}
-.oo-ui-popupWidget-anchor {
- display: none;
- z-index: 1;
-}
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor {
- display: block;
- position: absolute;
- top: 0;
- /* @noflip */
- left: 0;
- background-repeat: no-repeat;
-}
-.oo-ui-popupWidget-head {
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-.oo-ui-popupWidget-head > .oo-ui-buttonWidget {
- float: right;
-}
-.oo-ui-popupWidget-head > .oo-ui-labelElement-label {
- float: left;
- cursor: default;
-}
-.oo-ui-popupWidget-body {
- clear: both;
- overflow: hidden;
-}
-.oo-ui-popupWidget-popup {
- background-color: #ffffff;
- border: 1px solid #aaaaaa;
- border-radius: 2px;
- box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
-}
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-popup {
- margin-top: 9px;
-}
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before,
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
- content: "";
- position: absolute;
- width: 0;
- height: 0;
- border-style: solid;
- border-color: transparent;
- border-top: 0;
-}
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before {
- bottom: -10px;
- left: -9px;
- border-bottom-color: #888888;
- border-width: 10px;
-}
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
- bottom: -10px;
- left: -8px;
- border-bottom-color: #ffffff;
- border-width: 9px;
-}
-.oo-ui-popupWidget-transitioning .oo-ui-popupWidget-popup {
- -webkit-transition: width 100ms ease, height 100ms ease, left 100ms ease;
- -moz-transition: width 100ms ease, height 100ms ease, left 100ms ease;
- transition: width 100ms ease, height 100ms ease, left 100ms ease;
-}
-.oo-ui-popupWidget-head {
- height: 2.5em;
-}
-.oo-ui-popupWidget-head > .oo-ui-buttonWidget {
- margin: 0.25em;
-}
-.oo-ui-popupWidget-head > .oo-ui-labelElement-label {
- margin: 0.75em 1em;
-}
-.oo-ui-popupWidget-body-padded {
- padding: 0 1em;
-}
-.oo-ui-popupButtonWidget {
- position: relative;
-}
-.oo-ui-popupButtonWidget .oo-ui-popupWidget {
- position: absolute;
- cursor: auto;
-}
-.oo-ui-popupButtonWidget.oo-ui-buttonElement-frameless > .oo-ui-popupWidget {
- /* @noflip */
- left: 1em;
-}
-.oo-ui-popupButtonWidget.oo-ui-buttonElement-framed > .oo-ui-popupWidget {
- /* @noflip */
- left: 1.75em;
-}
-.oo-ui-inputWidget {
- margin-right: 0.5em;
-}
-.oo-ui-inputWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-buttonInputWidget {
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-buttonInputWidget > button,
-.oo-ui-buttonInputWidget > input {
- border: 0;
- padding: 0;
- background-color: transparent;
-}
-.oo-ui-checkboxInputWidget {
- position: relative;
- line-height: 1.6em;
- white-space: nowrap;
-}
-.oo-ui-checkboxInputWidget * {
- font: inherit;
- vertical-align: middle;
-}
-.oo-ui-checkboxInputWidget input[type="checkbox"] {
- opacity: 0;
- z-index: 1;
- position: relative;
- cursor: pointer;
- margin: 0;
- width: 1.6em;
- height: 1.6em;
- max-width: none;
-}
-.oo-ui-checkboxInputWidget input[type="checkbox"] + span {
- -webkit-transition: background-size 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
- -moz-transition: background-size 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
- transition: background-size 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- position: absolute;
- left: 0;
- border-radius: 2px;
- width: 1.6em;
- height: 1.6em;
- background-color: white;
- border: 1px solid #777777;
- background-image: url("themes/mediawiki/images/icons/check-constructive.png");
- background-image: -webkit-linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/check-constructive.svg");
- background-image: linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/check-constructive.svg");
- background-image: -o-linear-gradient(transparent, transparent), url("themes/mediawiki/images/icons/check-constructive.png");
- background-repeat: no-repeat;
- background-position: center center;
- background-origin: border-box;
- background-size: 0 0;
-}
-.oo-ui-checkboxInputWidget input[type="checkbox"]:checked + span {
- background-size: 100% 100%;
-}
-.oo-ui-checkboxInputWidget input[type="checkbox"]:active + span {
- background-color: #cccccc;
- border-color: #cccccc;
-}
-.oo-ui-checkboxInputWidget input[type="checkbox"]:focus + span {
- border-width: 2px;
-}
-.oo-ui-checkboxInputWidget input[type="checkbox"]:focus:hover + span,
-.oo-ui-checkboxInputWidget input[type="checkbox"]:hover + span {
- border-bottom-width: 3px;
-}
-.oo-ui-checkboxInputWidget input[type="checkbox"]:disabled {
- cursor: default;
-}
-.oo-ui-checkboxInputWidget input[type="checkbox"]:disabled + span {
- background-color: #dddddd;
- border-color: #dddddd;
-}
-.oo-ui-checkboxInputWidget input[type="checkbox"]:disabled:checked + span {
- background-image: url("themes/mediawiki/images/icons/check-invert.png");
- background-image: -webkit-linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/check-invert.svg");
- background-image: linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/check-invert.svg");
- background-image: -o-linear-gradient(transparent, transparent), url("themes/mediawiki/images/icons/check-invert.png");
-}
-.oo-ui-dropdownInputWidget {
- position: relative;
- vertical-align: middle;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- width: 100%;
- max-width: 50em;
-}
-.oo-ui-dropdownInputWidget select {
- display: inline-block;
- width: 100%;
- resize: none;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-dropdownInputWidget select {
- background-color: #ffffff;
- height: 2.275em;
- font-size: inherit;
- font-family: inherit;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- border: 1px solid #cccccc;
- border-radius: 2px;
- padding-left: 1em;
- vertical-align: middle;
-}
-.oo-ui-dropdownInputWidget.oo-ui-widget-enabled select:hover,
-.oo-ui-dropdownInputWidget.oo-ui-widget-enabled select:focus {
- border-color: #aaaaaa;
- outline: none;
-}
-.oo-ui-dropdownInputWidget.oo-ui-widget-disabled select {
- color: #cccccc;
- border-color: #dddddd;
- background-color: #f3f3f3;
-}
-.oo-ui-radioInputWidget {
- position: relative;
- line-height: 1.6em;
- white-space: nowrap;
-}
-.oo-ui-radioInputWidget * {
- font: inherit;
- vertical-align: middle;
-}
-.oo-ui-radioInputWidget input[type="radio"] {
- opacity: 0;
- z-index: 1;
- position: relative;
- cursor: pointer;
- margin: 0;
- width: 1.6em;
- height: 1.6em;
- max-width: none;
-}
-.oo-ui-radioInputWidget input[type="radio"] + span {
- -webkit-transition: background-size 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
- -moz-transition: background-size 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
- transition: background-size 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- position: absolute;
- left: 0;
- border-radius: 100%;
- width: 1.6em;
- height: 1.6em;
- background: white;
- border: 1px solid #777777;
- background-image: url("themes/mediawiki/images/icons/circle-constructive.png");
- background-image: -webkit-linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/circle-constructive.svg");
- background-image: linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/circle-constructive.svg");
- background-image: -o-linear-gradient(transparent, transparent), url("themes/mediawiki/images/icons/circle-constructive.png");
- background-repeat: no-repeat;
- background-position: center center;
- background-origin: border-box;
- background-size: 0 0;
-}
-.oo-ui-radioInputWidget input[type="radio"]:checked + span {
- background-size: 100% 100%;
-}
-.oo-ui-radioInputWidget input[type="radio"]:active + span {
- background-color: #cccccc;
- border-color: #cccccc;
-}
-.oo-ui-radioInputWidget input[type="radio"]:focus + span {
- border-width: 2px;
-}
-.oo-ui-radioInputWidget input[type="radio"]:focus:hover + span,
-.oo-ui-radioInputWidget input[type="radio"]:hover + span {
- border-bottom-width: 3px;
-}
-.oo-ui-radioInputWidget input[type="radio"]:disabled {
- cursor: default;
-}
-.oo-ui-radioInputWidget input[type="radio"]:disabled + span {
- background-color: #dddddd;
- border-color: #dddddd;
-}
-.oo-ui-radioInputWidget input[type="radio"]:disabled:checked + span {
- background-image: url("themes/mediawiki/images/icons/circle-invert.png");
- background-image: -webkit-linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/circle-invert.svg");
- background-image: linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/circle-invert.svg");
- background-image: -o-linear-gradient(transparent, transparent), url("themes/mediawiki/images/icons/circle-invert.png");
-}
-.oo-ui-radioSelectInputWidget .oo-ui-fieldLayout {
- margin-bottom: 0;
-}
-.oo-ui-textInputWidget {
- position: relative;
- vertical-align: middle;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- width: 100%;
- max-width: 50em;
-}
-.oo-ui-textInputWidget input,
-.oo-ui-textInputWidget textarea {
- display: inline-block;
- width: 100%;
- resize: none;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-textInputWidget textarea {
- overflow: auto;
-}
-.oo-ui-textInputWidget input[type="search"] {
- -webkit-appearance: none;
-}
-.oo-ui-textInputWidget input[type="search"]::-ms-clear {
- display: none;
-}
-.oo-ui-textInputWidget input[type="search"]::-ms-reveal {
- display: none;
-}
-.oo-ui-textInputWidget input[type="search"]::-webkit-search-decoration,
-.oo-ui-textInputWidget input[type="search"]::-webkit-search-cancel-button,
-.oo-ui-textInputWidget input[type="search"]::-webkit-search-results-button,
-.oo-ui-textInputWidget input[type="search"]::-webkit-search-results-decoration {
- display: none;
-}
-.oo-ui-textInputWidget > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator,
-.oo-ui-textInputWidget > .oo-ui-labelElement-label {
- display: none;
-}
-.oo-ui-textInputWidget.oo-ui-iconElement > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
- display: block;
- position: absolute;
- top: 0;
- height: 100%;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
- cursor: text;
-}
-.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-textInputWidget-type-search > .oo-ui-indicatorElement-indicator {
- cursor: pointer;
-}
-.oo-ui-textInputWidget.oo-ui-labelElement > .oo-ui-labelElement-label {
- display: block;
-}
-.oo-ui-textInputWidget > .oo-ui-iconElement-icon {
- left: 0;
-}
-.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator {
- right: 0;
-}
-.oo-ui-textInputWidget > .oo-ui-labelElement-label {
- position: absolute;
- top: 0;
-}
-.oo-ui-textInputWidget-labelPosition-after > .oo-ui-labelElement-label {
- right: 0;
-}
-.oo-ui-textInputWidget-labelPosition-before > .oo-ui-labelElement-label {
- left: 0;
-}
-.oo-ui-textInputWidget input,
-.oo-ui-textInputWidget textarea {
- padding: 0.5em;
- line-height: 1.275em;
- margin: 0;
- font-size: inherit;
- font-family: inherit;
- background-color: #ffffff;
- color: #000000;
- border: 1px solid #cccccc;
- border-radius: 2px;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-textInputWidget input.oo-ui-pendingElement-pending,
-.oo-ui-textInputWidget textarea.oo-ui-pendingElement-pending {
- background-color: transparent;
-}
-.oo-ui-textInputWidget.oo-ui-widget-enabled input,
-.oo-ui-textInputWidget.oo-ui-widget-enabled textarea {
- box-shadow: inset 0 0 0 0.1em #ffffff;
- -webkit-transition: border 200ms cubic-bezier(0.39, 0.575, 0.565, 1), box-shadow 200ms cubic-bezier(0.39, 0.575, 0.565, 1);
- -moz-transition: border 200ms cubic-bezier(0.39, 0.575, 0.565, 1), box-shadow 200ms cubic-bezier(0.39, 0.575, 0.565, 1);
- transition: border 200ms cubic-bezier(0.39, 0.575, 0.565, 1), box-shadow 200ms cubic-bezier(0.39, 0.575, 0.565, 1);
-}
-.oo-ui-textInputWidget.oo-ui-widget-enabled input:focus,
-.oo-ui-textInputWidget.oo-ui-widget-enabled textarea:focus {
- outline: none;
- border-color: #347bff;
- box-shadow: inset 0 0 0 0.1em #347bff;
-}
-.oo-ui-textInputWidget.oo-ui-widget-enabled input[readonly],
-.oo-ui-textInputWidget.oo-ui-widget-enabled textarea[readonly] {
- color: #777777;
- text-shadow: 0 1px 1px #ffffff;
-}
-.oo-ui-textInputWidget.oo-ui-widget-enabled input[readonly]:focus,
-.oo-ui-textInputWidget.oo-ui-widget-enabled textarea[readonly]:focus {
- border-color: #cccccc;
- box-shadow: inset 0 0 0 0.1em #cccccc;
-}
-.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid input,
-.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid textarea {
- border-color: #ff0000;
-}
-.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid input:focus,
-.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-flaggedElement-invalid textarea:focus {
- border-color: #ff0000;
- box-shadow: inset 0 0 0 0.1em #ff0000;
-}
-.oo-ui-textInputWidget.oo-ui-widget-disabled input,
-.oo-ui-textInputWidget.oo-ui-widget-disabled textarea {
- color: #cccccc;
- text-shadow: 0 1px 1px #ffffff;
- border-color: #dddddd;
- background-color: #f3f3f3;
-}
-.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
- opacity: 0.2;
-}
-.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-labelElement-label {
- color: #dddddd;
- text-shadow: 0 1px 1px #ffffff;
-}
-.oo-ui-textInputWidget.oo-ui-iconElement input,
-.oo-ui-textInputWidget.oo-ui-iconElement textarea {
- padding-left: 2.875em;
-}
-.oo-ui-textInputWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
- left: 0;
- width: 1.875em;
- max-height: 2.375em;
- margin-left: 0.5em;
- height: 100%;
- background-position: right center;
-}
-.oo-ui-textInputWidget.oo-ui-indicatorElement input,
-.oo-ui-textInputWidget.oo-ui-indicatorElement textarea {
- padding-right: 2.4875em;
-}
-.oo-ui-textInputWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- width: 0.9375em;
- max-height: 2.375em;
- margin: 0 0.775em;
- height: 100%;
-}
-.oo-ui-textInputWidget > .oo-ui-labelElement-label {
- padding: 0.4em;
- line-height: 1.5em;
- color: #888888;
-}
-.oo-ui-textInputWidget-labelPosition-after.oo-ui-indicatorElement > .oo-ui-labelElement-label {
- margin-right: 2.0875em;
-}
-.oo-ui-textInputWidget-labelPosition-before.oo-ui-iconElement > .oo-ui-labelElement-label {
- margin-left: 2.475em;
-}
-.oo-ui-menuSelectWidget {
- position: absolute;
- background-color: #ffffff;
- margin-top: -1px;
- border: 1px solid #aaaaaa;
- border-radius: 0 0 2px 2px;
- box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
-}
-.oo-ui-menuSelectWidget input {
- position: absolute;
- width: 0;
- height: 0;
- overflow: hidden;
- opacity: 0;
-}
-.oo-ui-menuOptionWidget {
- position: relative;
- padding: 0.5em 1em;
-}
-.oo-ui-menuOptionWidget .oo-ui-iconElement-icon {
- display: none;
-}
-.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected {
- background-color: transparent;
-}
-.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected .oo-ui-iconElement-icon {
- display: block;
-}
-.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected {
- background-color: #d8e6fe;
- color: rgba(0, 0, 0, 0.8);
-}
-.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected .oo-ui-iconElement-icon {
- display: none;
-}
-.oo-ui-menuOptionWidget.oo-ui-optionWidget-highlighted {
- background-color: #eeeeee;
- color: #000000;
-}
-.oo-ui-menuOptionWidget.oo-ui-optionWidget-selected.oo-ui-menuOptionWidget.oo-ui-optionWidget-highlighted {
- background-color: #d8e6fe;
-}
-.oo-ui-menuSectionOptionWidget {
- cursor: default;
- padding: 0.33em 0.75em;
- color: #888888;
-}
-.oo-ui-dropdownWidget {
- display: inline-block;
- position: relative;
- width: 100%;
- max-width: 50em;
- background-color: #ffffff;
- margin-right: 0.5em;
-}
-.oo-ui-dropdownWidget-handle {
- width: 100%;
- display: inline-block;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-dropdownWidget-handle .oo-ui-indicatorElement-indicator,
-.oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
- position: absolute;
-}
-.oo-ui-dropdownWidget > .oo-ui-menuSelectWidget {
- z-index: 1;
- width: 100%;
-}
-.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle {
- cursor: pointer;
-}
-.oo-ui-dropdownWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-dropdownWidget-handle {
- padding: 0.5em 0;
- height: 2.275em;
- line-height: 1.275;
- border: 1px solid #cccccc;
- border-radius: 2px;
-}
-.oo-ui-dropdownWidget-handle .oo-ui-indicatorElement-indicator {
- right: 0;
-}
-.oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
- left: 0.25em;
-}
-.oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {
- line-height: 1.275em;
- margin: 0 1em;
-}
-.oo-ui-dropdownWidget-handle .oo-ui-indicatorElement-indicator {
- top: 0;
- width: 0.9375em;
- height: 0.9375em;
- margin: 0.775em;
-}
-.oo-ui-dropdownWidget-handle .oo-ui-iconElement-icon {
- top: 0;
- width: 1.875em;
- height: 1.875em;
- margin: 0.3em;
-}
-.oo-ui-dropdownWidget:hover .oo-ui-dropdownWidget-handle {
- border-color: #aaaaaa;
-}
-.oo-ui-dropdownWidget.oo-ui-widget-disabled .oo-ui-dropdownWidget-handle {
- color: #cccccc;
- text-shadow: 0 1px 1px #ffffff;
- border-color: #dddddd;
- background-color: #f3f3f3;
-}
-.oo-ui-dropdownWidget.oo-ui-widget-disabled .oo-ui-dropdownWidget-handle:focus {
- outline: 0;
-}
-.oo-ui-dropdownWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
- opacity: 0.2;
-}
-.oo-ui-dropdownWidget.oo-ui-iconElement .oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {
- margin-left: 3em;
-}
-.oo-ui-dropdownWidget.oo-ui-indicatorElement .oo-ui-dropdownWidget-handle .oo-ui-labelElement-label {
- margin-right: 2em;
-}
-.oo-ui-dropdownWidget .oo-ui-selectWidget {
- border-top-color: #ffffff;
-}
-.oo-ui-comboBoxInputWidget {
- display: inline-block;
- position: relative;
- width: 100%;
- max-width: 50em;
- margin-right: 0.5em;
-}
-.oo-ui-comboBoxInputWidget > .oo-ui-menuSelectWidget {
- z-index: 1;
- width: 100%;
-}
-.oo-ui-comboBoxInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
- cursor: pointer;
-}
-.oo-ui-comboBoxInputWidget-php input::-webkit-calendar-picker-indicator {
- opacity: 0 !important;
- position: absolute;
- right: 0;
- top: 0;
- height: 2.5em;
- width: 2.5em;
- padding: 0;
-}
-.oo-ui-comboBoxInputWidget-php > .oo-ui-indicatorElement-indicator {
- pointer-events: none;
-}
-.oo-ui-comboBoxInputWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-comboBoxInputWidget input,
-.oo-ui-comboBoxInputWidget textarea {
- height: 2.35em;
-}
-
-/*!
- * OOjs UI v0.15.2
- * https://www.mediawiki.org/wiki/OOjs_UI
- *
- * Copyright 2011–2016 OOjs UI Team and other contributors.
- * Released under the MIT license
- * http://oojs.mit-license.org
- *
- * Date: 2016-02-02T22:07:06Z
- */
-@-webkit-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-moz-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-.oo-ui-draggableElement {
- cursor: -webkit-grab -moz-grab, url(images/grab.cur), move;
-}
-.oo-ui-draggableElement-dragging {
- cursor: -webkit-grabbing -moz-grabbing, url(images/grabbing.cur), move;
- background: rgba(0, 0, 0, 0.2);
- opacity: 0.4;
-}
-.oo-ui-draggableGroupElement-horizontal .oo-ui-draggableElement.oo-ui-optionWidget {
- display: inline-block;
-}
-.oo-ui-draggableGroupElement-placeholder {
- position: absolute;
- display: block;
- background: rgba(0, 0, 0, 0.4);
-}
-.oo-ui-lookupElement > .oo-ui-menuSelectWidget {
- z-index: 1;
- width: 100%;
-}
-.oo-ui-bookletLayout-stackLayout.oo-ui-stackLayout-continuous > .oo-ui-panelLayout-scrollable {
- overflow-y: hidden;
-}
-.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout {
- width: 100%;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout-scrollable {
- overflow-y: auto;
-}
-.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout-padded {
- padding: 2em;
-}
-.oo-ui-bookletLayout-outlinePanel-editable > .oo-ui-outlineSelectWidget {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 3em;
- overflow-y: auto;
-}
-.oo-ui-bookletLayout-outlinePanel > .oo-ui-outlineControlsWidget {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
-}
-.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout {
- padding: 1.5em;
-}
-.oo-ui-bookletLayout-outlinePanel {
- border-right: 1px solid #dddddd;
-}
-.oo-ui-bookletLayout-outlinePanel > .oo-ui-outlineControlsWidget {
- box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
-}
-.oo-ui-indexLayout > .oo-ui-menuLayout-menu {
- height: 3em;
-}
-.oo-ui-indexLayout > .oo-ui-menuLayout-content {
- top: 3em;
-}
-.oo-ui-indexLayout-stackLayout > .oo-ui-panelLayout {
- padding: 1.5em;
-}
-.oo-ui-indexLayout > .oo-ui-menuLayout-menu {
- height: 2.75em;
-}
-.oo-ui-indexLayout > .oo-ui-menuLayout-content {
- top: 2.75em;
-}
-.oo-ui-menuLayout {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
-}
-.oo-ui-menuLayout-menu,
-.oo-ui-menuLayout-content {
- position: absolute;
- -webkit-transition: all 200ms ease;
- -moz-transition: all 200ms ease;
- transition: all 200ms ease;
-}
-.oo-ui-menuLayout-menu {
- height: 18em;
- width: 18em;
-}
-.oo-ui-menuLayout-content {
- top: 18em;
- left: 18em;
- right: 18em;
- bottom: 18em;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-hideMenu > .oo-ui-menuLayout-menu {
- width: 0 !important;
- height: 0 !important;
- overflow: hidden;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-hideMenu > .oo-ui-menuLayout-content {
- top: 0 !important;
- left: 0 !important;
- right: 0 !important;
- bottom: 0 !important;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-top > .oo-ui-menuLayout-menu {
- width: auto !important;
- left: 0;
- top: 0;
- right: 0;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-top > .oo-ui-menuLayout-content {
- right: 0 !important;
- bottom: 0 !important;
- left: 0 !important;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-after > .oo-ui-menuLayout-menu {
- height: auto !important;
- top: 0;
- right: 0;
- bottom: 0;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-after > .oo-ui-menuLayout-content {
- bottom: 0 !important;
- left: 0 !important;
- top: 0 !important;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-bottom > .oo-ui-menuLayout-menu {
- width: auto !important;
- right: 0;
- bottom: 0;
- left: 0;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-bottom > .oo-ui-menuLayout-content {
- left: 0 !important;
- top: 0 !important;
- right: 0 !important;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-before > .oo-ui-menuLayout-menu {
- height: auto !important;
- bottom: 0;
- left: 0;
- top: 0;
-}
-.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-before > .oo-ui-menuLayout-content {
- top: 0 !important;
- right: 0 !important;
- bottom: 0 !important;
-}
-.oo-ui-stackLayout-continuous > .oo-ui-panelLayout {
- display: block;
- position: relative;
-}
-.oo-ui-buttonSelectWidget {
- display: inline-block;
- white-space: nowrap;
- border-radius: 2px;
- margin-right: 0.5em;
-}
-.oo-ui-buttonSelectWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget .oo-ui-buttonElement-button {
- border-radius: 0;
- margin-left: -1px;
-}
-.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget:first-child .oo-ui-buttonElement-button {
- border-bottom-left-radius: 2px;
- border-top-left-radius: 2px;
- margin-left: 0;
-}
-.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget:last-child .oo-ui-buttonElement-button {
- border-bottom-right-radius: 2px;
- border-top-right-radius: 2px;
-}
-.oo-ui-buttonOptionWidget {
- display: inline-block;
- padding: 0;
- background-color: transparent;
-}
-.oo-ui-buttonOptionWidget .oo-ui-buttonElement-button {
- position: relative;
-}
-.oo-ui-buttonOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
-.oo-ui-buttonOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- position: static;
- display: inline-block;
- vertical-align: middle;
-}
-.oo-ui-buttonOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
- margin-top: 0;
-}
-.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected,
-.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
-.oo-ui-buttonOptionWidget.oo-ui-optionWidget-highlighted {
- background-color: transparent;
-}
-.oo-ui-buttonOptionWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
-.oo-ui-buttonOptionWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
- opacity: 1;
-}
-.oo-ui-toggleButtonWidget {
- display: inline-block;
- vertical-align: middle;
- margin-right: 0.5em;
-}
-.oo-ui-toggleButtonWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-toggleSwitchWidget {
- position: relative;
- display: inline-block;
- vertical-align: middle;
- overflow: hidden;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- -webkit-transform: translateZ(0);
- -moz-transform: translateZ(0);
- -ms-transform: translateZ(0);
- transform: translateZ(0);
- height: 2em;
- width: 3.5em;
- border: 1px solid #777777;
- border-radius: 1em;
- background-color: #ffffff;
- margin-right: 0.5em;
- -webkit-transition: background-color 100ms ease, border-color 100ms ease;
- -moz-transition: background-color 100ms ease, border-color 100ms ease;
- transition: background-color 100ms ease, border-color 100ms ease;
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled {
- cursor: pointer;
-}
-.oo-ui-toggleSwitchWidget-grip {
- position: absolute;
- display: block;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-toggleSwitchWidget .oo-ui-toggleSwitchWidget-glow {
- position: absolute;
- top: 0;
- bottom: 0;
- right: 0;
- left: 0;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-glow {
- display: none;
-}
-.oo-ui-toggleSwitchWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-toggleSwitchWidget:before {
- content: "";
- display: block;
- position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
- border: 1px solid transparent;
- border-radius: 1em;
- z-index: 1;
-}
-.oo-ui-toggleSwitchWidget-grip {
- top: 0.35em;
- width: 1.2em;
- height: 1.2em;
- border-radius: 1.2em;
- background-color: #555555;
- -webkit-transition: left 100ms ease, margin-left 100ms ease;
- -moz-transition: left 100ms ease, margin-left 100ms ease;
- transition: left 100ms ease, margin-left 100ms ease;
-}
-.oo-ui-toggleSwitchWidget-glow {
- display: none;
-}
-.oo-ui-toggleSwitchWidget.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-grip {
- left: 1.9em;
- margin-left: -2px;
-}
-.oo-ui-toggleSwitchWidget.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-grip {
- left: 0.4em;
- margin-left: 0;
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled.oo-ui-toggleWidget-on {
- background-color: #347bff;
- border-color: #347bff;
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-grip {
- background-color: #ffffff;
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover {
- border-color: #2962cc;
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover.oo-ui-toggleWidget-on {
- background-color: #2962cc;
- border-color: #2962cc;
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:focus {
- border-color: #347bff;
- outline: none;
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:focus.oo-ui-toggleWidget-on {
- border-color: #347bff;
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:focus.oo-ui-toggleWidget-on:before {
- border-color: #ffffff;
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:active,
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:active:hover {
- background-color: #347bff;
- border-color: #347bff;
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:active .oo-ui-toggleSwitchWidget-grip,
-.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:active:hover .oo-ui-toggleSwitchWidget-grip {
- background-color: #ffffff;
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-disabled {
- background: #dddddd;
- border-color: #dddddd;
- outline: 0;
-}
-.oo-ui-toggleSwitchWidget.oo-ui-widget-disabled .oo-ui-toggleSwitchWidget-grip {
- background: #ffffff;
-}
-.oo-ui-progressBarWidget {
- max-width: 50em;
- background-color: #ffffff;
- border: 1px solid #cccccc;
- border-radius: 2px;
- overflow: hidden;
-}
-.oo-ui-progressBarWidget-bar {
- height: 1em;
- background: #dddddd;
- -webkit-transition: width 200ms, margin-left 200ms;
- -moz-transition: width 200ms, margin-left 200ms;
- transition: width 200ms, margin-left 200ms;
-}
-.oo-ui-progressBarWidget-indeterminate .oo-ui-progressBarWidget-bar {
- -webkit-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
- -moz-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
- animation: oo-ui-progressBarWidget-slide 2s infinite linear;
- width: 40%;
- margin-left: -10%;
- border-left-width: 1px;
-}
-.oo-ui-progressBarWidget.oo-ui-widget-disabled {
- opacity: 0.6;
-}
-.oo-ui-selectFileWidget {
- display: inline-block;
- vertical-align: middle;
- width: 100%;
- max-width: 50em;
- margin-right: 0.5em;
-}
-.oo-ui-selectFileWidget-selectButton {
- display: table-cell;
- vertical-align: middle;
-}
-.oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button {
- position: relative;
- overflow: hidden;
-}
-.oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button > input[type="file"] {
- position: absolute;
- margin: 0;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- width: 100%;
- height: 100%;
- opacity: 0;
- z-index: 1;
- cursor: pointer;
- padding-top: 100px;
-}
-.oo-ui-selectFileWidget-selectButton.oo-ui-widget-disabled > .oo-ui-buttonElement-button > input[type="file"] {
- display: none;
-}
-.oo-ui-selectFileWidget-info {
- width: 100%;
- display: table-cell;
- vertical-align: middle;
- position: relative;
- overflow: hidden;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- text-overflow: ellipsis;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileName {
- float: left;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileType {
- float: right;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator,
-.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
- position: absolute;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
- z-index: 2;
-}
-.oo-ui-selectFileWidget-dropTarget {
- cursor: default;
-}
-.oo-ui-selectFileWidget-supported.oo-ui-widget-enabled .oo-ui-selectFileWidget-dropTarget {
- cursor: pointer;
-}
-.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-clearButton,
-.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-clearButton {
- display: none;
-}
-.oo-ui-selectFileWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button {
- margin-left: 0.5em;
-}
-.oo-ui-selectFileWidget-info {
- height: 2.4em;
- background-color: #ffffff;
- border: 1px solid #cccccc;
- border-radius: 2px;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
- right: 0;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon {
- left: 0;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
- line-height: 2.3em;
- margin: 0;
- overflow: hidden;
- white-space: nowrap;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- text-overflow: ellipsis;
- left: 0.5em;
- right: 0.5em;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileType {
- color: #888888;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
- top: 0;
- width: 1.875em;
- margin-right: 0;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
- height: 2.3em;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
- top: 0;
- width: 0.9375em;
- height: 2.3em;
- margin-right: 0.775em;
-}
-.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon {
- top: 0;
- width: 1.875em;
- height: 2.3em;
- margin-left: 0.5em;
-}
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info {
- color: #cccccc;
- text-shadow: 0 1px 1px #ffffff;
- border-color: #dddddd;
- background-color: #f3f3f3;
-}
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
- opacity: 0.2;
-}
-.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-label {
- color: #cccccc;
-}
-.oo-ui-selectFileWidget.oo-ui-iconElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
- left: 2.875em;
-}
-.oo-ui-selectFileWidget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
- right: 2.375em;
-}
-.oo-ui-selectFileWidget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-clearButton {
- right: 0;
-}
-.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
- right: 4.4625em;
-}
-.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-clearButton {
- right: 2.0875em;
-}
-.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label,
-.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
- right: 0.5em;
-}
-.oo-ui-selectFileWidget-empty.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label,
-.oo-ui-selectFileWidget-notsupported.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
- right: 2em;
-}
-.oo-ui-selectFileWidget-dropTarget {
- line-height: 3.5em;
- background-color: #ffffff;
- border: 1px dashed #cccccc;
- padding: 0.5em 1em;
- margin-bottom: 0.5em;
- text-align: center;
- vertical-align: middle;
-}
-.oo-ui-selectFileWidget-supported.oo-ui-widget-enabled .oo-ui-selectFileWidget-dropTarget:hover {
- background-color: #eeeeee;
-}
-.oo-ui-selectFileWidget-supported.oo-ui-widget-enabled.oo-ui-selectFileWidget-canDrop .oo-ui-selectFileWidget-dropTarget {
- background: rgba(52, 123, 255, 0.1);
-}
-.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-dropTarget,
-.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-dropTarget {
- color: #cccccc;
- text-shadow: 0 1px 1px #ffffff;
- border-color: #dddddd;
- background-color: #f3f3f3;
-}
-.oo-ui-outlineOptionWidget {
- position: relative;
- cursor: pointer;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- font-size: 1.1em;
- padding: 0.75em;
-}
-.oo-ui-outlineOptionWidget.oo-ui-indicatorElement .oo-ui-labelElement-label {
- padding-right: 1.5em;
-}
-.oo-ui-outlineOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- opacity: 0.5;
-}
-.oo-ui-outlineOptionWidget-level-0 {
- padding-left: 3.5em;
-}
-.oo-ui-outlineOptionWidget-level-0 .oo-ui-iconElement-icon {
- left: 1em;
-}
-.oo-ui-outlineOptionWidget-level-1 {
- padding-left: 5em;
-}
-.oo-ui-outlineOptionWidget-level-1 .oo-ui-iconElement-icon {
- left: 2.5em;
-}
-.oo-ui-outlineOptionWidget-level-2 {
- padding-left: 6.5em;
-}
-.oo-ui-outlineOptionWidget-level-2 .oo-ui-iconElement-icon {
- left: 4em;
-}
-.oo-ui-selectWidget-depressed .oo-ui-outlineOptionWidget.oo-ui-optionWidget-selected {
- background-color: #d0d0d0;
- text-shadow: 0 1px 1px #ffffff;
-}
-.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-important {
- font-weight: bold;
-}
-.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-placeholder {
- font-style: italic;
-}
-.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-empty .oo-ui-iconElement-icon {
- opacity: 0.5;
-}
-.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-empty .oo-ui-labelElement-label {
- color: #777777;
-}
-.oo-ui-outlineControlsWidget {
- height: 3em;
- background-color: #ffffff;
-}
-.oo-ui-outlineControlsWidget-items,
-.oo-ui-outlineControlsWidget-movers {
- float: left;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-outlineControlsWidget > .oo-ui-iconElement-icon {
- float: left;
- background-position: right center;
-}
-.oo-ui-outlineControlsWidget-items {
- float: left;
-}
-.oo-ui-outlineControlsWidget-items .oo-ui-buttonWidget {
- float: left;
-}
-.oo-ui-outlineControlsWidget-movers {
- float: right;
-}
-.oo-ui-outlineControlsWidget-movers .oo-ui-buttonWidget {
- float: right;
-}
-.oo-ui-outlineControlsWidget-items,
-.oo-ui-outlineControlsWidget-movers {
- height: 2em;
- margin: 0.5em 0.5em 0.5em 0;
- padding: 0;
-}
-.oo-ui-outlineControlsWidget > .oo-ui-iconElement-icon {
- width: 1.5em;
- height: 2em;
- margin: 0.5em 0 0.5em 0.5em;
- opacity: 0.2;
-}
-.oo-ui-tabSelectWidget {
- text-align: left;
- white-space: nowrap;
- overflow: hidden;
- background-color: #dddddd;
-}
-.oo-ui-tabOptionWidget {
- display: inline-block;
- vertical-align: bottom;
- padding: 0.35em 1em;
- margin: 0.5em 0 0 0.75em;
- border: 1px solid transparent;
- border-bottom: none;
- border-top-left-radius: 2px;
- border-top-right-radius: 2px;
- color: #555555;
- font-weight: bold;
-}
-.oo-ui-tabOptionWidget.oo-ui-widget-enabled:hover {
- background-color: rgba(255, 255, 255, 0.3);
-}
-.oo-ui-tabOptionWidget.oo-ui-widget-enabled:active {
- background-color: rgba(255, 255, 255, 0.8);
-}
-.oo-ui-tabOptionWidget.oo-ui-indicatorElement .oo-ui-labelElement-label {
- padding-right: 1.5em;
-}
-.oo-ui-tabOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- opacity: 0.5;
-}
-.oo-ui-selectWidget-pressed .oo-ui-tabOptionWidget.oo-ui-optionWidget-selected,
-.oo-ui-selectWidget-depressed .oo-ui-tabOptionWidget.oo-ui-optionWidget-selected,
-.oo-ui-tabOptionWidget.oo-ui-optionWidget-selected:hover {
- background-color: #ffffff;
- color: #333333;
-}
-.oo-ui-capsuleMultiSelectWidget {
- display: inline-block;
- position: relative;
- width: 100%;
- max-width: 50em;
-}
-.oo-ui-capsuleMultiSelectWidget-handle {
- width: 100%;
- display: inline-block;
- position: relative;
-}
-.oo-ui-capsuleMultiSelectWidget-content {
- position: relative;
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-content > input {
- display: none;
-}
-.oo-ui-capsuleMultiSelectWidget-group {
- display: inline;
-}
-.oo-ui-capsuleMultiSelectWidget > .oo-ui-menuSelectWidget {
- z-index: 1;
- width: 100%;
-}
-.oo-ui-capsuleMultiSelectWidget-handle {
- background-color: #ffffff;
- cursor: text;
- min-height: 2.4em;
- margin-right: 0.5em;
- padding: 0.15em 0.25em;
- border: 1px solid #cccccc;
- border-radius: 2px;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-capsuleMultiSelectWidget-handle:last-child {
- margin-right: 0;
-}
-.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator,
-.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon {
- position: absolute;
- background-position: center center;
- background-repeat: no-repeat;
-}
-.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-capsuleMultiSelectWidget-content > input {
- border: 0;
- line-height: 1.675em;
- margin: 0 0 0 0.2em;
- padding: 0;
- font-size: inherit;
- font-family: inherit;
- background-color: transparent;
- color: #000000;
- vertical-align: middle;
-}
-.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-capsuleMultiSelectWidget-content > input:focus {
- outline: none;
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-indicatorElement .oo-ui-capsuleMultiSelectWidget-handle {
- padding-right: 2.4875em;
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-indicatorElement .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator {
- right: 0;
- top: 0;
- width: 0.9375em;
- height: 0.9375em;
- margin: 0.775em;
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-iconElement .oo-ui-capsuleMultiSelectWidget-handle {
- padding-left: 2.475em;
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-iconElement .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon {
- left: 0;
- top: 0;
- width: 1.875em;
- height: 1.875em;
- margin: 0.3em;
-}
-.oo-ui-capsuleMultiSelectWidget:hover .oo-ui-capsuleMultiSelectWidget-handle {
- border-color: #aaaaaa;
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-handle {
- color: #cccccc;
- text-shadow: 0 1px 1px #ffffff;
- border-color: #dddddd;
- background-color: #f3f3f3;
- cursor: default;
-}
-.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon,
-.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator {
- opacity: 0.2;
-}
-.oo-ui-capsuleMultiSelectWidget .oo-ui-selectWidget {
- border-top-color: #ffffff;
-}
-.oo-ui-capsuleItemWidget {
- position: relative;
- display: inline-block;
- cursor: default;
- white-space: nowrap;
- width: auto;
- max-width: 100%;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- vertical-align: middle;
- padding: 0 0.4em;
- margin: 0.1em;
- height: 1.7em;
- line-height: 1.7em;
- background-color: #eeeeee;
- border: 1px solid #cccccc;
- color: #555555;
- border-radius: 2px;
-}
-.oo-ui-capsuleItemWidget > .oo-ui-iconElement-icon {
- cursor: pointer;
-}
-.oo-ui-capsuleItemWidget.oo-ui-widget-disabled > .oo-ui-iconElement-icon {
- cursor: default;
-}
-.oo-ui-capsuleItemWidget.oo-ui-labelElement .oo-ui-labelElement-label {
- display: block;
- text-overflow: ellipsis;
- overflow: hidden;
-}
-.oo-ui-capsuleItemWidget.oo-ui-indicatorElement > .oo-ui-labelElement-label {
- padding-right: 1.3375em;
-}
-.oo-ui-capsuleItemWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
- position: absolute;
- right: 0.4em;
- top: 0;
- width: 0.9375em;
- height: 100%;
- background-repeat: no-repeat;
-}
-.oo-ui-capsuleItemWidget.oo-ui-indicatorElement > .oo-ui-indicator-clear {
- cursor: pointer;
-}
-.oo-ui-capsuleItemWidget.oo-ui-widget-disabled {
- color: #cccccc;
- text-shadow: 0 1px 1px #ffffff;
- border-color: #dddddd;
- background-color: #f3f3f3;
-}
-.oo-ui-capsuleItemWidget.oo-ui-widget-disabled > .oo-ui-indicatorElement-indicator {
- opacity: 0.2;
-}
-.oo-ui-searchWidget-query {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
-}
-.oo-ui-searchWidget-query .oo-ui-textInputWidget {
- width: 100%;
-}
-.oo-ui-searchWidget-results {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- overflow-x: hidden;
- overflow-y: auto;
-}
-.oo-ui-searchWidget-query {
- height: 4em;
- padding: 0 1em;
- border-bottom: 1px solid #cccccc;
-}
-.oo-ui-searchWidget-query .oo-ui-textInputWidget {
- margin: 0.75em 0;
-}
-.oo-ui-searchWidget-results {
- top: 4em;
- padding: 1em;
- line-height: 0;
-}
-.oo-ui-numberInputWidget {
- display: inline-block;
- position: relative;
- max-width: 50em;
-}
-.oo-ui-numberInputWidget-field {
- display: table;
- table-layout: fixed;
- width: 100%;
-}
-.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget,
-.oo-ui-numberInputWidget-field > .oo-ui-textInputWidget {
- display: table-cell;
- vertical-align: middle;
-}
-.oo-ui-numberInputWidget-field > .oo-ui-textInputWidget {
- width: 100%;
-}
-.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget {
- white-space: nowrap;
-}
-.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget > .oo-ui-buttonElement-button {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget {
- width: 2.5em;
-}
-.oo-ui-numberInputWidget-minusButton.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- border-right-width: 0;
-}
-.oo-ui-numberInputWidget-plusButton.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
- border-left-width: 0;
-}
-.oo-ui-numberInputWidget .oo-ui-textInputWidget input {
- border-radius: 0;
-}
-
-/*!
- * OOjs UI v0.15.2
- * https://www.mediawiki.org/wiki/OOjs_UI
- *
- * Copyright 2011–2016 OOjs UI Team and other contributors.
- * Released under the MIT license
- * http://oojs.mit-license.org
- *
- * Date: 2016-02-02T22:07:06Z
- */
-@-webkit-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-moz-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-.oo-ui-popupTool .oo-ui-popupWidget-popup,
-.oo-ui-popupTool .oo-ui-popupWidget-anchor {
- z-index: 4;
-}
-.oo-ui-popupTool .oo-ui-popupWidget {
- /* @noflip */
- margin-left: 1.25em;
-}
-.oo-ui-toolGroupTool > .oo-ui-popupToolGroup {
- border: 0;
- border-radius: 0;
- margin: 0;
-}
-.oo-ui-toolGroupTool > .oo-ui-toolGroup {
- border-right: none;
-}
-.oo-ui-toolGroupTool > .oo-ui-popupToolGroup > .oo-ui-popupToolGroup-handle {
- height: 2.5em;
- padding: 0.3125em;
-}
-.oo-ui-toolGroupTool > .oo-ui-popupToolGroup > .oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
- height: 2.5em;
- width: 1.875em;
-}
-.oo-ui-toolGroupTool > .oo-ui-popupToolGroup.oo-ui-labelElement > .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- line-height: 2.1em;
-}
-.oo-ui-toolGroup {
- display: inline-block;
- vertical-align: middle;
- border-radius: 0;
- border-right: 1px solid #dddddd;
-}
-.oo-ui-toolGroup-empty {
- display: none;
-}
-.oo-ui-toolGroup .oo-ui-tool-link {
- text-decoration: none;
-}
-.oo-ui-toolbar-narrow .oo-ui-toolGroup + .oo-ui-toolGroup {
- margin-left: 0;
-}
-.oo-ui-toolGroup .oo-ui-toolGroup .oo-ui-widget-enabled {
- border-right: none !important;
-}
-.oo-ui-barToolGroup > .oo-ui-iconElement-icon,
-.oo-ui-barToolGroup > .oo-ui-labelElement-label {
- display: none;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link {
- cursor: pointer;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool {
- display: inline-block;
- position: relative;
- vertical-align: top;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link {
- display: block;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-tool-accel {
- display: none;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-iconElement > .oo-ui-tool-link .oo-ui-iconElement-icon {
- display: inline-block;
- vertical-align: top;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-iconElement > .oo-ui-tool-link .oo-ui-tool-title {
- display: none;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-iconElement.oo-ui-tool-with-label > .oo-ui-tool-link .oo-ui-tool-title {
- display: inline;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link {
- outline: 0;
- cursor: default;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link {
- height: 1.875em;
- padding: 0.625em;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-iconElement-icon {
- height: 1.875em;
- width: 1.875em;
-}
-.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-tool-title {
- line-height: 2.1em;
- padding: 0 0.4em;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled:hover {
- border-color: rgba(0, 0, 0, 0.2);
- background-color: #eeeeee;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool > a.oo-ui-tool-link .oo-ui-tool-title {
- color: #555555;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-tool-active.oo-ui-widget-enabled {
- border-color: rgba(0, 0, 0, 0.2);
- box-shadow: inset 0 0.07em 0.07em 0 rgba(0, 0, 0, 0.07);
- background-color: #e5e5e5;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-tool-active.oo-ui-widget-enabled:hover {
- background-color: #eeeeee;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-tool-active.oo-ui-widget-enabled + .oo-ui-tool-active.oo-ui-widget-enabled {
- border-left-color: rgba(0, 0, 0, 0.1);
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link .oo-ui-tool-title {
- color: #cccccc;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link .oo-ui-iconElement-icon {
- opacity: 0.2;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled > .oo-ui-tool-link .oo-ui-iconElement-icon {
- opacity: 0.7;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled:hover > .oo-ui-tool-link .oo-ui-iconElement-icon {
- opacity: 0.9;
-}
-.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled:active {
- background-color: #e7e7e7;
-}
-.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool > a.oo-ui-tool-link .oo-ui-tool-title {
- color: #cccccc;
-}
-.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool > a.oo-ui-tool-link .oo-ui-iconElement-icon {
- opacity: 0.2;
-}
-.oo-ui-popupToolGroup {
- position: relative;
- height: 3.125em;
- min-width: 2em;
-}
-.oo-ui-popupToolGroup-handle {
- display: block;
- cursor: pointer;
-}
-.oo-ui-popupToolGroup-handle .oo-ui-indicatorElement-indicator,
-.oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
- position: absolute;
-}
-.oo-ui-popupToolGroup.oo-ui-widget-disabled .oo-ui-popupToolGroup-handle {
- outline: 0;
- cursor: default;
-}
-.oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
- display: none;
- position: absolute;
- z-index: 4;
-}
-.oo-ui-popupToolGroup-active.oo-ui-widget-enabled > .oo-ui-toolGroup-tools {
- display: block;
-}
-.oo-ui-popupToolGroup-left > .oo-ui-toolGroup-tools {
- left: 0;
-}
-.oo-ui-popupToolGroup-right > .oo-ui-toolGroup-tools {
- right: 0;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link {
- display: table;
- width: 100%;
- vertical-align: middle;
- white-space: nowrap;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-iconElement-icon,
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel,
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-title {
- display: table-cell;
- vertical-align: middle;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel {
- text-align: right;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel:not(:empty) {
- padding-left: 3em;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup {
- min-width: 1.875em;
-}
-.oo-ui-popupToolGroup.oo-ui-iconElement {
- min-width: 3.125em;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-iconElement {
- min-width: 2.5em;
-}
-.oo-ui-popupToolGroup.oo-ui-indicatorElement.oo-ui-iconElement {
- min-width: 4.375em;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-indicatorElement.oo-ui-iconElement {
- min-width: 3.75em;
-}
-.oo-ui-popupToolGroup.oo-ui-labelElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- line-height: 2.6em;
- margin: 0 1em;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-labelElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- margin: 0 0.5em;
-}
-.oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-iconElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- margin-left: 3em;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-iconElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- margin-left: 2.5em;
-}
-.oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-indicatorElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- margin-right: 2em;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-indicatorElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
- margin-right: 1.75em;
-}
-.oo-ui-popupToolGroup.oo-ui-widget-enabled .oo-ui-popupToolGroup-handle:hover {
- background-color: #eeeeee;
-}
-.oo-ui-popupToolGroup.oo-ui-widget-enabled .oo-ui-popupToolGroup-handle:active {
- background-color: #e5e5e5;
-}
-.oo-ui-popupToolGroup-handle {
- padding: 0.3125em;
- height: 2.5em;
-}
-.oo-ui-popupToolGroup-handle .oo-ui-indicatorElement-indicator {
- width: 0.9375em;
- height: 1.625em;
- margin: 0.78125em 0.5em;
- top: 0;
- right: 0;
- opacity: 0.3;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup-handle .oo-ui-indicatorElement-indicator {
- right: -0.3125em;
-}
-.oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
- width: 1.875em;
- height: 2.6em;
- margin: 0.25em;
- top: 0;
- left: 0.3125em;
- opacity: 0.7;
-}
-.oo-ui-toolbar-narrow .oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
- left: 0;
-}
-.oo-ui-popupToolGroup-header {
- line-height: 2.6em;
- margin: 0 0.6em;
- font-weight: bold;
-}
-.oo-ui-popupToolGroup-active.oo-ui-widget-enabled {
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
- box-shadow: inset 0 0.07em 0.07em 0 rgba(0, 0, 0, 0.07);
- background-color: #eeeeee;
-}
-.oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
- top: 3.125em;
- margin: 0 -1px;
- border: 1px solid #cccccc;
- background-color: #ffffff;
- box-shadow: 0 2px 3px rgba(0, 0, 0, 0.2);
- min-width: 16em;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link {
- padding: 0.4em 0.625em;
- box-sizing: border-box;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-iconElement-icon {
- height: 2.5em;
- width: 1.875em;
- min-width: 1.875em;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-title {
- padding-left: 0.5em;
- color: #555555;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel,
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-title {
- line-height: 2em;
-}
-.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel {
- color: #888888;
-}
-.oo-ui-listToolGroup .oo-ui-tool {
- display: block;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-listToolGroup .oo-ui-tool-link {
- cursor: pointer;
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link {
- cursor: default;
-}
-.oo-ui-listToolGroup.oo-ui-popupToolGroup-active {
- border-color: rgba(0, 0, 0, 0.2);
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover {
- border-color: rgba(0, 0, 0, 0.2);
- background-color: #eeeeee;
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-enabled:active {
- background-color: #e7e7e7;
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover .oo-ui-tool-link .oo-ui-iconElement-icon {
- opacity: 0.9;
-}
-.oo-ui-listToolGroup .oo-ui-tool-active.oo-ui-widget-enabled {
- border-color: rgba(0, 0, 0, 0.1);
- box-shadow: inset 0 0.07em 0.07em 0 rgba(0, 0, 0, 0.07);
- background-color: #e5e5e5;
-}
-.oo-ui-listToolGroup .oo-ui-tool-active.oo-ui-widget-enabled + .oo-ui-tool-active.oo-ui-widget-enabled {
- border-top-color: rgba(0, 0, 0, 0.1);
-}
-.oo-ui-listToolGroup .oo-ui-tool-active.oo-ui-widget-enabled:hover {
- border-color: rgba(0, 0, 0, 0.2);
- background-color: #eeeeee;
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-title {
- color: #cccccc;
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-accel {
- color: #dddddd;
-}
-.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-iconElement-icon {
- opacity: 0.2;
-}
-.oo-ui-listToolGroup.oo-ui-widget-disabled {
- color: #cccccc;
-}
-.oo-ui-listToolGroup.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator,
-.oo-ui-listToolGroup.oo-ui-widget-disabled .oo-ui-iconElement-icon {
- opacity: 0.2;
-}
-.oo-ui-menuToolGroup .oo-ui-tool {
- display: block;
-}
-.oo-ui-menuToolGroup .oo-ui-tool-link {
- cursor: pointer;
-}
-.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link {
- cursor: default;
-}
-.oo-ui-menuToolGroup .oo-ui-popupToolGroup-handle {
- min-width: 10em;
-}
-.oo-ui-toolbar-narrow .oo-ui-menuToolGroup .oo-ui-popupToolGroup-handle {
- min-width: 8.125em;
-}
-.oo-ui-menuToolGroup .oo-ui-tool-link .oo-ui-iconElement-icon {
- background-image: none;
-}
-.oo-ui-menuToolGroup .oo-ui-tool-active .oo-ui-tool-link .oo-ui-iconElement-icon {
- background-image: url("themes/mediawiki/images/icons/check.png");
- background-image: -webkit-linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/check.svg");
- background-image: linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/check.svg");
- background-image: -o-linear-gradient(transparent, transparent), url("themes/mediawiki/images/icons/check.png");
- background-size: contain;
- background-position: center center;
- background-repeat: no-repeat;
-}
-.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover {
- background-color: #eeeeee;
-}
-.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-title {
- color: #cccccc;
-}
-.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-iconElement-icon {
- opacity: 0.2;
-}
-.oo-ui-menuToolGroup.oo-ui-widget-disabled {
- color: #cccccc;
-}
-.oo-ui-menuToolGroup.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator,
-.oo-ui-menuToolGroup.oo-ui-widget-disabled .oo-ui-iconElement-icon {
- opacity: 0.2;
-}
-.oo-ui-toolbar {
- clear: both;
-}
-.oo-ui-toolbar-bar {
- line-height: 1em;
- position: relative;
-}
-.oo-ui-toolbar-actions {
- float: right;
-}
-.oo-ui-toolbar-actions .oo-ui-toolbar {
- display: inline-block;
-}
-.oo-ui-toolbar-tools {
- display: inline;
- white-space: nowrap;
-}
-.oo-ui-toolbar-narrow .oo-ui-toolbar-tools {
- white-space: normal;
-}
-.oo-ui-toolbar-tools .oo-ui-tool {
- white-space: normal;
-}
-.oo-ui-toolbar-tools,
-.oo-ui-toolbar-actions,
-.oo-ui-toolbar-shadow {
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-.oo-ui-toolbar-actions .oo-ui-popupWidget {
- -webkit-touch-callout: default;
- -webkit-user-select: all;
- -moz-user-select: all;
- -ms-user-select: all;
- user-select: all;
-}
-.oo-ui-toolbar-shadow {
- background-position: left top;
- background-repeat: repeat-x;
- position: absolute;
- width: 100%;
- pointer-events: none;
-}
-.oo-ui-toolbar-bar {
- border-bottom: 1px solid #cccccc;
- background-color: #ffffff;
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
- font-weight: 500;
- color: #555555;
-}
-.oo-ui-toolbar-bar .oo-ui-toolbar-bar {
- border: 0;
- background: none;
- box-shadow: none;
-}
-.oo-ui-toolbar-actions > .oo-ui-buttonElement.oo-ui-labelElement {
- margin: 0;
-}
-.oo-ui-toolbar-actions > .oo-ui-buttonElement.oo-ui-labelElement > .oo-ui-buttonElement-button {
- border: 0;
- border-radius: 0;
- margin: 0;
- padding: 0 0.3125em;
-}
-.oo-ui-toolbar-actions > .oo-ui-buttonElement.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
- margin: 0 1em;
- line-height: 3.125em;
-}
-
-/*!
- * OOjs UI v0.15.2
- * https://www.mediawiki.org/wiki/OOjs_UI
- *
- * Copyright 2011–2016 OOjs UI Team and other contributors.
- * Released under the MIT license
- * http://oojs.mit-license.org
- *
- * Date: 2016-02-02T22:07:06Z
- */
-@-webkit-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@-moz-keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-@keyframes oo-ui-progressBarWidget-slide {
- from {
- margin-left: -40%;
- }
- to {
- margin-left: 100%;
- }
-}
-.oo-ui-window {
- background: transparent;
-}
-.oo-ui-window-frame {
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-window-content:focus {
- outline: none;
-}
-.oo-ui-window-head,
-.oo-ui-window-foot {
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-.oo-ui-window-body {
- margin: 0;
- padding: 0;
- background: none;
-}
-.oo-ui-window-overlay {
- position: absolute;
- top: 0;
- /* @noflip */
- left: 0;
-}
-.oo-ui-dialog-content > .oo-ui-window-head,
-.oo-ui-dialog-content > .oo-ui-window-body,
-.oo-ui-dialog-content > .oo-ui-window-foot {
- position: absolute;
- left: 0;
- right: 0;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-.oo-ui-dialog-content > .oo-ui-window-head {
- overflow: hidden;
- z-index: 1;
- top: 0;
-}
-.oo-ui-dialog-content > .oo-ui-window-body {
- overflow: auto;
- z-index: 2;
- top: 0;
- bottom: 0;
-}
-.oo-ui-dialog-content > .oo-ui-window-foot {
- overflow: hidden;
- z-index: 1;
- bottom: 0;
-}
-.oo-ui-dialog-content > .oo-ui-window-body {
- outline: 1px solid #aaaaaa;
-}
-.oo-ui-messageDialog-actions-horizontal {
- display: table;
- table-layout: fixed;
- width: 100%;
-}
-.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget {
- display: table-cell;
- width: 1%;
-}
-.oo-ui-messageDialog-actions-vertical {
- display: block;
-}
-.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget {
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget {
- position: relative;
- text-align: center;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-buttonElement-button {
- display: block;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-labelElement-label {
- position: relative;
- top: auto;
- bottom: auto;
- display: inline;
- white-space: nowrap;
-}
-.oo-ui-messageDialog-title,
-.oo-ui-messageDialog-message {
- display: block;
- text-align: center;
-}
-.oo-ui-messageDialog-title.oo-ui-labelElement,
-.oo-ui-messageDialog-message.oo-ui-labelElement {
- padding-top: 0.5em;
-}
-.oo-ui-messageDialog-title {
- font-size: 1.5em;
- line-height: 1em;
- color: #000000;
-}
-.oo-ui-messageDialog-message {
- font-size: 0.9em;
- line-height: 1.25em;
- color: #555555;
-}
-.oo-ui-messageDialog-message-verbose {
- font-size: 1.1em;
- line-height: 1.5em;
- text-align: left;
-}
-.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget {
- border-right: 1px solid #e5e5e5;
- margin: 0;
-}
-.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget:last-child {
- border-right-width: 0;
-}
-.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget {
- border-bottom: 1px solid #e5e5e5;
- margin: 0;
-}
-.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget:last-child {
- border-bottom-width: 0;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget {
- height: 3.4em;
- margin-right: 0;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget:last-child {
- margin-right: 0;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
- text-align: center;
- line-height: 3.4em;
- padding: 0 2em;
-}
-.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-flaggedElement-progressive:hover {
- background-color: rgba(8, 126, 204, 0.05);
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-progressive:active {
- background-color: rgba(8, 126, 204, 0.1);
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-progressive .oo-ui-labelElement-label {
- font-weight: bold;
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:hover {
- background-color: rgba(118, 171, 54, 0.05);
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:active {
- background-color: rgba(118, 171, 54, 0.1);
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:hover {
- background-color: rgba(212, 83, 83, 0.05);
-}
-.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:active {
- background-color: rgba(212, 83, 83, 0.1);
-}
-.oo-ui-processDialog-location {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.oo-ui-processDialog-title {
- display: inline;
- padding: 0;
-}
-.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-processDialog-actions-primary {
- position: absolute;
- top: 0;
- bottom: 0;
-}
-.oo-ui-processDialog-actions-safe {
- left: 0;
-}
-.oo-ui-processDialog-actions-primary {
- right: 0;
-}
-.oo-ui-processDialog-errors {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 2;
- overflow-x: hidden;
- overflow-y: auto;
-}
-.oo-ui-processDialog-content .oo-ui-window-head {
- height: 3.4em;
-}
-.oo-ui-processDialog-content .oo-ui-window-body {
- top: 3.4em;
- outline: 1px solid rgba(0, 0, 0, 0.2);
-}
-.oo-ui-processDialog-navigation {
- position: relative;
- height: 3.4em;
- padding: 0 1em;
-}
-.oo-ui-processDialog-location {
- padding: 0.75em 0;
- height: 1.875em;
- cursor: default;
- text-align: center;
-}
-.oo-ui-processDialog-title {
- font-weight: bold;
- line-height: 1.875em;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-framed,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-framed,
-.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-framed {
- margin: 0.5em;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless,
-.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-frameless {
- margin: 0;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button,
-.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button {
- padding: 0.75em 1em;
- vertical-align: middle;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-labelElement-label,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-labelElement-label,
-.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-labelElement-label {
- line-height: 1.875em;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless:hover,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless:hover {
- background-color: rgba(0, 0, 0, 0.05);
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless:active,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless:active {
- background-color: rgba(0, 0, 0, 0.1);
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive:hover,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive:hover {
- background-color: rgba(8, 126, 204, 0.05);
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive:active,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive:active {
- background-color: rgba(8, 126, 204, 0.1);
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive .oo-ui-labelElement-label,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive .oo-ui-labelElement-label {
- font-weight: bold;
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-constructive:hover,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-constructive:hover {
- background-color: rgba(118, 171, 54, 0.05);
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-constructive:active,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-constructive:active {
- background-color: rgba(118, 171, 54, 0.1);
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-destructive:hover,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-destructive:hover {
- background-color: rgba(212, 83, 83, 0.05);
-}
-.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-destructive:active,
-.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-destructive:active {
- background-color: rgba(212, 83, 83, 0.1);
-}
-.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement {
- margin-right: 0;
-}
-.oo-ui-processDialog > .oo-ui-window-frame {
- min-height: 5em;
-}
-.oo-ui-processDialog-errors {
- background-color: rgba(255, 255, 255, 0.9);
- padding: 3em 3em 1.5em 3em;
- text-align: center;
-}
-.oo-ui-processDialog-errors .oo-ui-buttonWidget {
- margin: 2em 1em 2em 1em;
-}
-.oo-ui-processDialog-errors-title {
- font-size: 1.5em;
- color: #000000;
- margin-bottom: 2em;
-}
-.oo-ui-processDialog-error {
- text-align: left;
- margin: 1em;
- padding: 1em;
- border: 1px solid #ff9e9e;
- background-color: #fff7f7;
- border-radius: 2px;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog {
- position: fixed;
- width: 0;
- height: 0;
- overflow: hidden;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-active {
- width: auto;
- height: auto;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- padding: 1em;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup > .oo-ui-window-frame {
- position: absolute;
- right: 0;
- left: 0;
- margin: auto;
- overflow: hidden;
- max-width: 100%;
- max-height: 100%;
-}
-.oo-ui-windowManager-fullscreen > .oo-ui-dialog > .oo-ui-window-frame {
- width: 100%;
- height: 100%;
- top: 0;
- bottom: 0;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog {
- background-color: rgba(255, 255, 255, 0.5);
- opacity: 0;
- -webkit-transition: opacity 250ms ease;
- -moz-transition: opacity 250ms ease;
- transition: opacity 250ms ease;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog > .oo-ui-window-frame {
- background-color: #ffffff;
- opacity: 0;
- -webkit-transform: scale(0.5);
- -moz-transform: scale(0.5);
- -ms-transform: scale(0.5);
- transform: scale(0.5);
- -webkit-transition: all 250ms ease;
- -moz-transition: all 250ms ease;
- transition: all 250ms ease;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup {
- opacity: 1;
-}
-.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-ready > .oo-ui-window-frame {
- opacity: 1;
- -webkit-transform: scale(1);
- -moz-transform: scale(1);
- -ms-transform: scale(1);
- transform: scale(1);
-}
-.oo-ui-windowManager-modal.oo-ui-windowManager-floating > .oo-ui-dialog > .oo-ui-window-frame {
- top: 1em;
- bottom: 1em;
- border: 1px solid #aaaaaa;
- border-radius: 2px;
- box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
-}
--- /dev/null
+/*!
+ * OOjs UI v0.15.2
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: 2016-02-02T22:07:06Z
+ */
+@-webkit-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-moz-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-ms-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-o-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+.oo-ui-popupTool .oo-ui-popupWidget-popup,
+.oo-ui-popupTool .oo-ui-popupWidget-anchor {
+ z-index: 4;
+}
+.oo-ui-popupTool .oo-ui-popupWidget {
+ /* @noflip */
+ margin-left: 1.25em;
+}
+.oo-ui-toolGroupTool > .oo-ui-popupToolGroup {
+ border: 0;
+ border-radius: 0;
+ margin: 0;
+}
+.oo-ui-toolGroupTool:first-child > .oo-ui-popupToolGroup {
+ border-top-left-radius: 0.3125em;
+ border-bottom-left-radius: 0.3125em;
+}
+.oo-ui-toolGroupTool:last-child > .oo-ui-popupToolGroup {
+ border-top-right-radius: 0.3125em;
+ border-bottom-right-radius: 0.3125em;
+}
+.oo-ui-toolGroupTool > .oo-ui-popupToolGroup > .oo-ui-popupToolGroup-handle {
+ height: 1.875em;
+ padding: 0.3125em;
+}
+.oo-ui-toolGroupTool > .oo-ui-popupToolGroup > .oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
+ height: 1.875em;
+ width: 1.875em;
+}
+.oo-ui-toolGroupTool > .oo-ui-popupToolGroup.oo-ui-labelElement > .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ line-height: 2.1em;
+}
+.oo-ui-toolGroup {
+ display: inline-block;
+ vertical-align: middle;
+ margin: 0.375em;
+ border-radius: 0.3125em;
+ border: 1px solid transparent;
+ -webkit-transition: border-color 250ms ease;
+ -moz-transition: border-color 250ms ease;
+ transition: border-color 250ms ease;
+}
+.oo-ui-toolGroup-empty {
+ display: none;
+}
+.oo-ui-toolGroup .oo-ui-tool-link {
+ text-decoration: none;
+}
+.oo-ui-toolbar-narrow .oo-ui-toolGroup + .oo-ui-toolGroup {
+ margin-left: 0;
+}
+.oo-ui-toolGroup.oo-ui-widget-enabled:hover {
+ border-color: rgba(0, 0, 0, 0.1);
+}
+.oo-ui-toolGroup.oo-ui-widget-enabled .oo-ui-tool-link .oo-ui-tool-title {
+ color: #000000;
+}
+.oo-ui-barToolGroup > .oo-ui-iconElement-icon,
+.oo-ui-barToolGroup > .oo-ui-labelElement-label {
+ display: none;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link {
+ cursor: pointer;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool {
+ display: inline-block;
+ position: relative;
+ vertical-align: top;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link {
+ display: block;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-tool-accel {
+ display: none;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-iconElement > .oo-ui-tool-link .oo-ui-iconElement-icon {
+ display: inline-block;
+ vertical-align: top;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-iconElement > .oo-ui-tool-link .oo-ui-tool-title {
+ display: none;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-iconElement.oo-ui-tool-with-label > .oo-ui-tool-link .oo-ui-tool-title {
+ display: inline;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link {
+ outline: 0;
+ cursor: default;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool {
+ margin: -1px 0 -1px -1px;
+ border: 1px solid transparent;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool:first-child {
+ border-top-left-radius: 0.3125em;
+ border-bottom-left-radius: 0.3125em;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool:last-child {
+ margin-right: -1px;
+ border-top-right-radius: 0.3125em;
+ border-bottom-right-radius: 0.3125em;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link {
+ height: 1.875em;
+ padding: 0.3125em;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-iconElement-icon {
+ height: 1.875em;
+ width: 1.875em;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-tool-title {
+ line-height: 2.1em;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled:hover {
+ border-color: rgba(0, 0, 0, 0.2);
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-tool-active.oo-ui-widget-enabled {
+ border-color: rgba(0, 0, 0, 0.2);
+ box-shadow: inset 0 0.0875em 0.0875em 0 rgba(0, 0, 0, 0.07);
+ background-color: #f8fbfd;
+ background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #f1f7fb), color-stop(100%, #ffffff));
+ background-image: -webkit-linear-gradient(top, #f1f7fb 0, #ffffff 100%);
+ background-image: -moz-linear-gradient(top, #f1f7fb 0, #ffffff 100%);
+ background-image: linear-gradient(to bottom, #f1f7fb 0, #ffffff 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff1f7fb', endColorstr='#ffffffff' )";
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-tool-active.oo-ui-widget-enabled + .oo-ui-tool-active.oo-ui-widget-enabled {
+ border-left-color: rgba(0, 0, 0, 0.1);
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link:focus {
+ outline: 0;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link .oo-ui-tool-title {
+ color: #cccccc;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link .oo-ui-iconElement-icon {
+ opacity: 0.2;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled:hover > .oo-ui-tool-link .oo-ui-iconElement-icon {
+ opacity: 1;
+}
+.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool:focus {
+ outline: 0;
+}
+.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link:focus {
+ outline: 0;
+}
+.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-tool-title {
+ color: #cccccc;
+}
+.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-iconElement-icon {
+ opacity: 0.2;
+}
+.oo-ui-popupToolGroup {
+ position: relative;
+ height: 2.5em;
+ min-width: 2.5em;
+}
+.oo-ui-popupToolGroup-handle {
+ display: block;
+ cursor: pointer;
+}
+.oo-ui-popupToolGroup-handle .oo-ui-indicatorElement-indicator,
+.oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
+ position: absolute;
+}
+.oo-ui-popupToolGroup.oo-ui-widget-disabled .oo-ui-popupToolGroup-handle {
+ outline: 0;
+ cursor: default;
+}
+.oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
+ display: none;
+ position: absolute;
+ z-index: 4;
+}
+.oo-ui-popupToolGroup-active.oo-ui-widget-enabled > .oo-ui-toolGroup-tools {
+ display: block;
+}
+.oo-ui-popupToolGroup-left > .oo-ui-toolGroup-tools {
+ left: 0;
+}
+.oo-ui-popupToolGroup-right > .oo-ui-toolGroup-tools {
+ right: 0;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link {
+ display: table;
+ width: 100%;
+ vertical-align: middle;
+ white-space: nowrap;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-iconElement-icon,
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel,
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-title {
+ display: table-cell;
+ vertical-align: middle;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel {
+ text-align: right;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel:not(:empty) {
+ padding-left: 3em;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup {
+ min-width: 1.875em;
+}
+.oo-ui-popupToolGroup.oo-ui-iconElement {
+ min-width: 3.125em;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-iconElement {
+ min-width: 2.5em;
+}
+.oo-ui-popupToolGroup.oo-ui-indicatorElement.oo-ui-iconElement {
+ min-width: 4.375em;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-indicatorElement.oo-ui-iconElement {
+ min-width: 3.75em;
+}
+.oo-ui-popupToolGroup.oo-ui-labelElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ line-height: 2.6em;
+ margin: 0 1em;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-labelElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ margin: 0 0.5em;
+}
+.oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-iconElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ margin-left: 3em;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-iconElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ margin-left: 2.5em;
+}
+.oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-indicatorElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ margin-right: 2.25em;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-indicatorElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ margin-right: 1.75em;
+}
+.oo-ui-popupToolGroup-handle .oo-ui-indicatorElement-indicator {
+ width: 0.9375em;
+ height: 0.9375em;
+ margin: 0.78125em;
+ top: 0;
+ right: 0;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup-handle .oo-ui-indicatorElement-indicator {
+ right: -0.3125em;
+}
+.oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
+ width: 1.875em;
+ height: 1.875em;
+ margin: 0.3125em;
+ top: 0;
+ left: 0.3125em;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
+ left: 0;
+}
+.oo-ui-popupToolGroup-header {
+ line-height: 2.6em;
+ margin: 0 0.6em;
+ font-weight: bold;
+}
+.oo-ui-popupToolGroup-active.oo-ui-widget-enabled {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ box-shadow: inset 0 0.0875em 0.0875em 0 rgba(0, 0, 0, 0.07);
+ background-color: #f8fbfd;
+ background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #f1f7fb), color-stop(100%, #ffffff));
+ background-image: -webkit-linear-gradient(top, #f1f7fb 0, #ffffff 100%);
+ background-image: -moz-linear-gradient(top, #f1f7fb 0, #ffffff 100%);
+ background-image: linear-gradient(to bottom, #f1f7fb 0, #ffffff 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff1f7fb', endColorstr='#ffffffff' )";
+}
+.oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
+ top: 2.5em;
+ margin: 0 -1px;
+ border: 1px solid #cccccc;
+ background-color: white;
+ box-shadow: 0 0.3125em 1.25em rgba(0, 0, 0, 0.25);
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link {
+ padding: 0.3125em 0 0.3125em 0.3125em;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-iconElement-icon {
+ height: 1.875em;
+ width: 1.875em;
+ min-width: 1.875em;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-title {
+ padding-left: 0.5em;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel,
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-title {
+ line-height: 2em;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel {
+ color: #888888;
+}
+.oo-ui-listToolGroup .oo-ui-tool {
+ display: block;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-listToolGroup .oo-ui-tool-link {
+ cursor: pointer;
+}
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link {
+ cursor: default;
+}
+.oo-ui-listToolGroup .oo-ui-toolGroup-tools {
+ padding: 0.3125em;
+}
+.oo-ui-listToolGroup.oo-ui-popupToolGroup-active {
+ border-color: rgba(0, 0, 0, 0.2);
+}
+.oo-ui-listToolGroup .oo-ui-tool {
+ border: 1px solid transparent;
+ margin: -1px 0;
+ padding: 0 0.625em 0 0;
+}
+.oo-ui-listToolGroup .oo-ui-tool-active.oo-ui-widget-enabled {
+ border-color: rgba(0, 0, 0, 0.1);
+ box-shadow: inset 0 0.0875em 0.0875em 0 rgba(0, 0, 0, 0.07);
+ background-color: #f8fbfd;
+ background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #f1f7fb), color-stop(100%, #ffffff));
+ background-image: -webkit-linear-gradient(top, #f1f7fb 0, #ffffff 100%);
+ background-image: -moz-linear-gradient(top, #f1f7fb 0, #ffffff 100%);
+ background-image: linear-gradient(to bottom, #f1f7fb 0, #ffffff 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff1f7fb', endColorstr='#ffffffff' )";
+}
+.oo-ui-listToolGroup .oo-ui-tool-active.oo-ui-widget-enabled + .oo-ui-tool-active.oo-ui-widget-enabled {
+ border-top-color: rgba(0, 0, 0, 0.1);
+}
+.oo-ui-listToolGroup .oo-ui-tool-active.oo-ui-widget-enabled:hover {
+ border-color: rgba(0, 0, 0, 0.2);
+}
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover {
+ border-color: rgba(0, 0, 0, 0.2);
+}
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover .oo-ui-tool-link .oo-ui-iconElement-icon {
+ opacity: 1;
+}
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-title {
+ color: #cccccc;
+}
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-accel {
+ color: #dddddd;
+}
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-iconElement-icon {
+ opacity: 0.2;
+}
+.oo-ui-listToolGroup.oo-ui-widget-disabled {
+ color: #cccccc;
+}
+.oo-ui-listToolGroup.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator,
+.oo-ui-listToolGroup.oo-ui-widget-disabled .oo-ui-iconElement-icon {
+ opacity: 0.2;
+}
+.oo-ui-menuToolGroup {
+ border-color: rgba(0, 0, 0, 0.1);
+}
+.oo-ui-menuToolGroup .oo-ui-tool {
+ display: block;
+}
+.oo-ui-menuToolGroup .oo-ui-tool-link {
+ cursor: pointer;
+}
+.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link {
+ cursor: default;
+}
+.oo-ui-menuToolGroup .oo-ui-popupToolGroup-handle {
+ min-width: 10em;
+}
+.oo-ui-toolbar-narrow .oo-ui-menuToolGroup .oo-ui-popupToolGroup-handle {
+ min-width: 8.125em;
+}
+.oo-ui-menuToolGroup .oo-ui-toolGroup-tools {
+ padding: 0.3125em 0 0.3125em 0;
+}
+.oo-ui-menuToolGroup.oo-ui-widget-enabled:hover {
+ border-color: rgba(0, 0, 0, 0.2);
+}
+.oo-ui-menuToolGroup.oo-ui-popupToolGroup-active {
+ border-color: rgba(0, 0, 0, 0.25);
+}
+.oo-ui-menuToolGroup .oo-ui-tool {
+ padding: 0 1.25em 0 0.3125em;
+}
+.oo-ui-menuToolGroup .oo-ui-tool-link .oo-ui-iconElement-icon {
+ background-image: none;
+}
+.oo-ui-menuToolGroup .oo-ui-tool-active .oo-ui-tool-link .oo-ui-iconElement-icon {
+ background-image: url("themes/apex/images/icons/check.png");
+ background-image: -webkit-linear-gradient(transparent, transparent), /* @embed */ url("themes/apex/images/icons/check.svg");
+ background-image: linear-gradient(transparent, transparent), /* @embed */ url("themes/apex/images/icons/check.svg");
+ background-image: -o-linear-gradient(transparent, transparent), url("themes/apex/images/icons/check.png");
+ background-size: contain;
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover {
+ background-color: #e1f3ff;
+}
+.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-title {
+ color: #cccccc;
+}
+.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-iconElement-icon {
+ opacity: 0.2;
+}
+.oo-ui-menuToolGroup.oo-ui-widget-disabled {
+ color: #cccccc;
+ border-color: rgba(0, 0, 0, 0.05);
+}
+.oo-ui-menuToolGroup.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator,
+.oo-ui-menuToolGroup.oo-ui-widget-disabled .oo-ui-iconElement-icon {
+ opacity: 0.2;
+}
+.oo-ui-toolbar {
+ clear: both;
+}
+.oo-ui-toolbar-bar {
+ line-height: 1em;
+ position: relative;
+}
+.oo-ui-toolbar-actions {
+ float: right;
+}
+.oo-ui-toolbar-actions .oo-ui-toolbar {
+ display: inline-block;
+}
+.oo-ui-toolbar-tools {
+ display: inline;
+ white-space: nowrap;
+}
+.oo-ui-toolbar-narrow .oo-ui-toolbar-tools {
+ white-space: normal;
+}
+.oo-ui-toolbar-tools .oo-ui-tool {
+ white-space: normal;
+}
+.oo-ui-toolbar-tools,
+.oo-ui-toolbar-actions,
+.oo-ui-toolbar-shadow {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.oo-ui-toolbar-actions .oo-ui-popupWidget {
+ -webkit-touch-callout: default;
+ -webkit-user-select: all;
+ -moz-user-select: all;
+ -ms-user-select: all;
+ user-select: all;
+}
+.oo-ui-toolbar-shadow {
+ background-position: left top;
+ background-repeat: repeat-x;
+ position: absolute;
+ width: 100%;
+ pointer-events: none;
+}
+.oo-ui-toolbar-bar {
+ border-bottom: 1px solid #cccccc;
+ background-color: #f8fbfd;
+ background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #ffffff), color-stop(100%, #f1f7fb));
+ background-image: -webkit-linear-gradient(top, #ffffff 0, #f1f7fb 100%);
+ background-image: -moz-linear-gradient(top, #ffffff 0, #f1f7fb 100%);
+ background-image: linear-gradient(to bottom, #ffffff 0, #f1f7fb 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffffff', endColorstr='#fff1f7fb' )";
+}
+.oo-ui-toolbar-bar .oo-ui-toolbar-bar {
+ border: none;
+ background: none;
+}
+.oo-ui-toolbar-actions > .oo-ui-buttonElement-framed,
+.oo-ui-toolbar-actions > .oo-ui-buttonElement-framed:last-child {
+ margin-top: 0.4em;
+ margin-bottom: 0.4em;
+ margin-right: 0.5em;
+}
+.oo-ui-toolbar-actions > .oo-ui-buttonElement-frameless.oo-ui-labelElement,
+.oo-ui-toolbar-actions > .oo-ui-buttonElement-frameless:last-child.oo-ui-labelElement {
+ margin: 0;
+}
+.oo-ui-toolbar-actions > .oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button,
+.oo-ui-toolbar-actions > .oo-ui-buttonElement-frameless:last-child.oo-ui-labelElement > .oo-ui-buttonElement-button {
+ margin: 0;
+ padding: 0 0.3125em;
+}
+.oo-ui-toolbar-actions > .oo-ui-buttonElement-frameless.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label,
+.oo-ui-toolbar-actions > .oo-ui-buttonElement-frameless:last-child.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ margin: 0 1em;
+ line-height: 3.40625em;
+}
+.oo-ui-toolbar-shadow {
+ background-image: /* @embed */ url(themes/apex/images/toolbar-shadow.png);
+ bottom: -9px;
+ height: 9px;
+ opacity: 0.5;
+ -webkit-transition: opacity 500ms ease;
+ -moz-transition: opacity 500ms ease;
+ transition: opacity 500ms ease;
+}
--- /dev/null
+/*!
+ * OOjs UI v0.15.2
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: 2016-02-02T22:07:06Z
+ */
+@-webkit-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-moz-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+.oo-ui-popupTool .oo-ui-popupWidget-popup,
+.oo-ui-popupTool .oo-ui-popupWidget-anchor {
+ z-index: 4;
+}
+.oo-ui-popupTool .oo-ui-popupWidget {
+ /* @noflip */
+ margin-left: 1.25em;
+}
+.oo-ui-toolGroupTool > .oo-ui-popupToolGroup {
+ border: 0;
+ border-radius: 0;
+ margin: 0;
+}
+.oo-ui-toolGroupTool > .oo-ui-toolGroup {
+ border-right: none;
+}
+.oo-ui-toolGroupTool > .oo-ui-popupToolGroup > .oo-ui-popupToolGroup-handle {
+ height: 2.5em;
+ padding: 0.3125em;
+}
+.oo-ui-toolGroupTool > .oo-ui-popupToolGroup > .oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
+ height: 2.5em;
+ width: 1.875em;
+}
+.oo-ui-toolGroupTool > .oo-ui-popupToolGroup.oo-ui-labelElement > .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ line-height: 2.1em;
+}
+.oo-ui-toolGroup {
+ display: inline-block;
+ vertical-align: middle;
+ border-radius: 0;
+ border-right: 1px solid #dddddd;
+}
+.oo-ui-toolGroup-empty {
+ display: none;
+}
+.oo-ui-toolGroup .oo-ui-tool-link {
+ text-decoration: none;
+}
+.oo-ui-toolbar-narrow .oo-ui-toolGroup + .oo-ui-toolGroup {
+ margin-left: 0;
+}
+.oo-ui-toolGroup .oo-ui-toolGroup .oo-ui-widget-enabled {
+ border-right: none !important;
+}
+.oo-ui-barToolGroup > .oo-ui-iconElement-icon,
+.oo-ui-barToolGroup > .oo-ui-labelElement-label {
+ display: none;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link {
+ cursor: pointer;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool {
+ display: inline-block;
+ position: relative;
+ vertical-align: top;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link {
+ display: block;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-tool-accel {
+ display: none;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-iconElement > .oo-ui-tool-link .oo-ui-iconElement-icon {
+ display: inline-block;
+ vertical-align: top;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-iconElement > .oo-ui-tool-link .oo-ui-tool-title {
+ display: none;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-iconElement.oo-ui-tool-with-label > .oo-ui-tool-link .oo-ui-tool-title {
+ display: inline;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link {
+ outline: 0;
+ cursor: default;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link {
+ height: 1.875em;
+ padding: 0.625em;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-iconElement-icon {
+ height: 1.875em;
+ width: 1.875em;
+}
+.oo-ui-barToolGroup > .oo-ui-toolGroup-tools > .oo-ui-tool > .oo-ui-tool-link .oo-ui-tool-title {
+ line-height: 2.1em;
+ padding: 0 0.4em;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled:hover {
+ border-color: rgba(0, 0, 0, 0.2);
+ background-color: #eeeeee;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool > a.oo-ui-tool-link .oo-ui-tool-title {
+ color: #555555;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-tool-active.oo-ui-widget-enabled {
+ border-color: rgba(0, 0, 0, 0.2);
+ box-shadow: inset 0 0.07em 0.07em 0 rgba(0, 0, 0, 0.07);
+ background-color: #e5e5e5;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-tool-active.oo-ui-widget-enabled:hover {
+ background-color: #eeeeee;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-tool-active.oo-ui-widget-enabled + .oo-ui-tool-active.oo-ui-widget-enabled {
+ border-left-color: rgba(0, 0, 0, 0.1);
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link .oo-ui-tool-title {
+ color: #cccccc;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-disabled > .oo-ui-tool-link .oo-ui-iconElement-icon {
+ opacity: 0.2;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled > .oo-ui-tool-link .oo-ui-iconElement-icon {
+ opacity: 0.7;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled:hover > .oo-ui-tool-link .oo-ui-iconElement-icon {
+ opacity: 0.9;
+}
+.oo-ui-barToolGroup.oo-ui-widget-enabled > .oo-ui-toolGroup-tools > .oo-ui-tool.oo-ui-widget-enabled:active {
+ background-color: #e7e7e7;
+}
+.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool > a.oo-ui-tool-link .oo-ui-tool-title {
+ color: #cccccc;
+}
+.oo-ui-barToolGroup.oo-ui-widget-disabled > .oo-ui-toolGroup-tools > .oo-ui-tool > a.oo-ui-tool-link .oo-ui-iconElement-icon {
+ opacity: 0.2;
+}
+.oo-ui-popupToolGroup {
+ position: relative;
+ height: 3.125em;
+ min-width: 2em;
+}
+.oo-ui-popupToolGroup-handle {
+ display: block;
+ cursor: pointer;
+}
+.oo-ui-popupToolGroup-handle .oo-ui-indicatorElement-indicator,
+.oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
+ position: absolute;
+}
+.oo-ui-popupToolGroup.oo-ui-widget-disabled .oo-ui-popupToolGroup-handle {
+ outline: 0;
+ cursor: default;
+}
+.oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
+ display: none;
+ position: absolute;
+ z-index: 4;
+}
+.oo-ui-popupToolGroup-active.oo-ui-widget-enabled > .oo-ui-toolGroup-tools {
+ display: block;
+}
+.oo-ui-popupToolGroup-left > .oo-ui-toolGroup-tools {
+ left: 0;
+}
+.oo-ui-popupToolGroup-right > .oo-ui-toolGroup-tools {
+ right: 0;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link {
+ display: table;
+ width: 100%;
+ vertical-align: middle;
+ white-space: nowrap;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-iconElement-icon,
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel,
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-title {
+ display: table-cell;
+ vertical-align: middle;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel {
+ text-align: right;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel:not(:empty) {
+ padding-left: 3em;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup {
+ min-width: 1.875em;
+}
+.oo-ui-popupToolGroup.oo-ui-iconElement {
+ min-width: 3.125em;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-iconElement {
+ min-width: 2.5em;
+}
+.oo-ui-popupToolGroup.oo-ui-indicatorElement.oo-ui-iconElement {
+ min-width: 4.375em;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-indicatorElement.oo-ui-iconElement {
+ min-width: 3.75em;
+}
+.oo-ui-popupToolGroup.oo-ui-labelElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ line-height: 2.6em;
+ margin: 0 1em;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-labelElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ margin: 0 0.5em;
+}
+.oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-iconElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ margin-left: 3em;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-iconElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ margin-left: 2.5em;
+}
+.oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-indicatorElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ margin-right: 2em;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup.oo-ui-labelElement.oo-ui-indicatorElement .oo-ui-popupToolGroup-handle .oo-ui-labelElement-label {
+ margin-right: 1.75em;
+}
+.oo-ui-popupToolGroup.oo-ui-widget-enabled .oo-ui-popupToolGroup-handle:hover {
+ background-color: #eeeeee;
+}
+.oo-ui-popupToolGroup.oo-ui-widget-enabled .oo-ui-popupToolGroup-handle:active {
+ background-color: #e5e5e5;
+}
+.oo-ui-popupToolGroup-handle {
+ padding: 0.3125em;
+ height: 2.5em;
+}
+.oo-ui-popupToolGroup-handle .oo-ui-indicatorElement-indicator {
+ width: 0.9375em;
+ height: 1.625em;
+ margin: 0.78125em 0.5em;
+ top: 0;
+ right: 0;
+ opacity: 0.3;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup-handle .oo-ui-indicatorElement-indicator {
+ right: -0.3125em;
+}
+.oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
+ width: 1.875em;
+ height: 2.6em;
+ margin: 0.25em;
+ top: 0;
+ left: 0.3125em;
+ opacity: 0.7;
+}
+.oo-ui-toolbar-narrow .oo-ui-popupToolGroup-handle .oo-ui-iconElement-icon {
+ left: 0;
+}
+.oo-ui-popupToolGroup-header {
+ line-height: 2.6em;
+ margin: 0 0.6em;
+ font-weight: bold;
+}
+.oo-ui-popupToolGroup-active.oo-ui-widget-enabled {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ box-shadow: inset 0 0.07em 0.07em 0 rgba(0, 0, 0, 0.07);
+ background-color: #eeeeee;
+}
+.oo-ui-popupToolGroup .oo-ui-toolGroup-tools {
+ top: 3.125em;
+ margin: 0 -1px;
+ border: 1px solid #cccccc;
+ background-color: #ffffff;
+ box-shadow: 0 2px 3px rgba(0, 0, 0, 0.2);
+ min-width: 16em;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link {
+ padding: 0.4em 0.625em;
+ box-sizing: border-box;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-iconElement-icon {
+ height: 2.5em;
+ width: 1.875em;
+ min-width: 1.875em;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-title {
+ padding-left: 0.5em;
+ color: #555555;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel,
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-title {
+ line-height: 2em;
+}
+.oo-ui-popupToolGroup .oo-ui-tool-link .oo-ui-tool-accel {
+ color: #888888;
+}
+.oo-ui-listToolGroup .oo-ui-tool {
+ display: block;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-listToolGroup .oo-ui-tool-link {
+ cursor: pointer;
+}
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link {
+ cursor: default;
+}
+.oo-ui-listToolGroup.oo-ui-popupToolGroup-active {
+ border-color: rgba(0, 0, 0, 0.2);
+}
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover {
+ border-color: rgba(0, 0, 0, 0.2);
+ background-color: #eeeeee;
+}
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-enabled:active {
+ background-color: #e7e7e7;
+}
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover .oo-ui-tool-link .oo-ui-iconElement-icon {
+ opacity: 0.9;
+}
+.oo-ui-listToolGroup .oo-ui-tool-active.oo-ui-widget-enabled {
+ border-color: rgba(0, 0, 0, 0.1);
+ box-shadow: inset 0 0.07em 0.07em 0 rgba(0, 0, 0, 0.07);
+ background-color: #e5e5e5;
+}
+.oo-ui-listToolGroup .oo-ui-tool-active.oo-ui-widget-enabled + .oo-ui-tool-active.oo-ui-widget-enabled {
+ border-top-color: rgba(0, 0, 0, 0.1);
+}
+.oo-ui-listToolGroup .oo-ui-tool-active.oo-ui-widget-enabled:hover {
+ border-color: rgba(0, 0, 0, 0.2);
+ background-color: #eeeeee;
+}
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-title {
+ color: #cccccc;
+}
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-accel {
+ color: #dddddd;
+}
+.oo-ui-listToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-iconElement-icon {
+ opacity: 0.2;
+}
+.oo-ui-listToolGroup.oo-ui-widget-disabled {
+ color: #cccccc;
+}
+.oo-ui-listToolGroup.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator,
+.oo-ui-listToolGroup.oo-ui-widget-disabled .oo-ui-iconElement-icon {
+ opacity: 0.2;
+}
+.oo-ui-menuToolGroup .oo-ui-tool {
+ display: block;
+}
+.oo-ui-menuToolGroup .oo-ui-tool-link {
+ cursor: pointer;
+}
+.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link {
+ cursor: default;
+}
+.oo-ui-menuToolGroup .oo-ui-popupToolGroup-handle {
+ min-width: 10em;
+}
+.oo-ui-toolbar-narrow .oo-ui-menuToolGroup .oo-ui-popupToolGroup-handle {
+ min-width: 8.125em;
+}
+.oo-ui-menuToolGroup .oo-ui-tool-link .oo-ui-iconElement-icon {
+ background-image: none;
+}
+.oo-ui-menuToolGroup .oo-ui-tool-active .oo-ui-tool-link .oo-ui-iconElement-icon {
+ background-image: url("themes/mediawiki/images/icons/check.png");
+ background-image: -webkit-linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/check.svg");
+ background-image: linear-gradient(transparent, transparent), /* @embed */ url("themes/mediawiki/images/icons/check.svg");
+ background-image: -o-linear-gradient(transparent, transparent), url("themes/mediawiki/images/icons/check.png");
+ background-size: contain;
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-enabled:hover {
+ background-color: #eeeeee;
+}
+.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-tool-title {
+ color: #cccccc;
+}
+.oo-ui-menuToolGroup .oo-ui-tool.oo-ui-widget-disabled .oo-ui-tool-link .oo-ui-iconElement-icon {
+ opacity: 0.2;
+}
+.oo-ui-menuToolGroup.oo-ui-widget-disabled {
+ color: #cccccc;
+}
+.oo-ui-menuToolGroup.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator,
+.oo-ui-menuToolGroup.oo-ui-widget-disabled .oo-ui-iconElement-icon {
+ opacity: 0.2;
+}
+.oo-ui-toolbar {
+ clear: both;
+}
+.oo-ui-toolbar-bar {
+ line-height: 1em;
+ position: relative;
+}
+.oo-ui-toolbar-actions {
+ float: right;
+}
+.oo-ui-toolbar-actions .oo-ui-toolbar {
+ display: inline-block;
+}
+.oo-ui-toolbar-tools {
+ display: inline;
+ white-space: nowrap;
+}
+.oo-ui-toolbar-narrow .oo-ui-toolbar-tools {
+ white-space: normal;
+}
+.oo-ui-toolbar-tools .oo-ui-tool {
+ white-space: normal;
+}
+.oo-ui-toolbar-tools,
+.oo-ui-toolbar-actions,
+.oo-ui-toolbar-shadow {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.oo-ui-toolbar-actions .oo-ui-popupWidget {
+ -webkit-touch-callout: default;
+ -webkit-user-select: all;
+ -moz-user-select: all;
+ -ms-user-select: all;
+ user-select: all;
+}
+.oo-ui-toolbar-shadow {
+ background-position: left top;
+ background-repeat: repeat-x;
+ position: absolute;
+ width: 100%;
+ pointer-events: none;
+}
+.oo-ui-toolbar-bar {
+ border-bottom: 1px solid #cccccc;
+ background-color: #ffffff;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ font-weight: 500;
+ color: #555555;
+}
+.oo-ui-toolbar-bar .oo-ui-toolbar-bar {
+ border: 0;
+ background: none;
+ box-shadow: none;
+}
+.oo-ui-toolbar-actions > .oo-ui-buttonElement.oo-ui-labelElement {
+ margin: 0;
+}
+.oo-ui-toolbar-actions > .oo-ui-buttonElement.oo-ui-labelElement > .oo-ui-buttonElement-button {
+ border: 0;
+ border-radius: 0;
+ margin: 0;
+ padding: 0 0.3125em;
+}
+.oo-ui-toolbar-actions > .oo-ui-buttonElement.oo-ui-labelElement > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
+ margin: 0 1em;
+ line-height: 3.125em;
+}
--- /dev/null
+/*!
+ * OOjs UI v0.15.2
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: 2016-02-02T22:07:00Z
+ */
+( function ( OO ) {
+
+'use strict';
+
+/**
+ * Toolbars are complex interface components that permit users to easily access a variety
+ * of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional commands that are
+ * part of the toolbar, but not configured as tools.
+ *
+ * Individual tools are customized and then registered with a {@link OO.ui.ToolFactory tool factory}, which creates
+ * the tools on demand. Each tool has a symbolic name (used when registering the tool), a title (e.g., ‘Insert
+ * image’), and an icon.
+ *
+ * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be {@link OO.ui.MenuToolGroup menus}
+ * of tools, {@link OO.ui.ListToolGroup lists} of tools, or a single {@link OO.ui.BarToolGroup bar} of tools.
+ * The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
+ * any order, but each can only appear once in the toolbar.
+ *
+ * The toolbar can be synchronized with the state of the external "application", like a text
+ * editor's editing area, marking tools as active/inactive (e.g. a 'bold' tool would be shown as
+ * active when the text cursor was inside bolded text) or enabled/disabled (e.g. a table caption
+ * tool would be disabled while the user is not editing a table). A state change is signalled by
+ * emitting the {@link #event-updateState 'updateState' event}, which calls Tools'
+ * {@link OO.ui.Tool#onUpdateState onUpdateState method}.
+ *
+ * The following is an example of a basic toolbar.
+ *
+ * @example
+ * // Example of a toolbar
+ * // Create the toolbar
+ * var toolFactory = new OO.ui.ToolFactory();
+ * var toolGroupFactory = new OO.ui.ToolGroupFactory();
+ * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
+ *
+ * // We will be placing status text in this element when tools are used
+ * var $area = $( '<p>' ).text( 'Toolbar example' );
+ *
+ * // Define the tools that we're going to place in our toolbar
+ *
+ * // Create a class inheriting from OO.ui.Tool
+ * function SearchTool() {
+ * SearchTool.parent.apply( this, arguments );
+ * }
+ * OO.inheritClass( SearchTool, OO.ui.Tool );
+ * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
+ * // of 'icon' and 'title' (displayed icon and text).
+ * SearchTool.static.name = 'search';
+ * SearchTool.static.icon = 'search';
+ * SearchTool.static.title = 'Search...';
+ * // Defines the action that will happen when this tool is selected (clicked).
+ * SearchTool.prototype.onSelect = function () {
+ * $area.text( 'Search tool clicked!' );
+ * // Never display this tool as "active" (selected).
+ * this.setActive( false );
+ * };
+ * SearchTool.prototype.onUpdateState = function () {};
+ * // Make this tool available in our toolFactory and thus our toolbar
+ * toolFactory.register( SearchTool );
+ *
+ * // Register two more tools, nothing interesting here
+ * function SettingsTool() {
+ * SettingsTool.parent.apply( this, arguments );
+ * }
+ * OO.inheritClass( SettingsTool, OO.ui.Tool );
+ * SettingsTool.static.name = 'settings';
+ * SettingsTool.static.icon = 'settings';
+ * SettingsTool.static.title = 'Change settings';
+ * SettingsTool.prototype.onSelect = function () {
+ * $area.text( 'Settings tool clicked!' );
+ * this.setActive( false );
+ * };
+ * SettingsTool.prototype.onUpdateState = function () {};
+ * toolFactory.register( SettingsTool );
+ *
+ * // Register two more tools, nothing interesting here
+ * function StuffTool() {
+ * StuffTool.parent.apply( this, arguments );
+ * }
+ * OO.inheritClass( StuffTool, OO.ui.Tool );
+ * StuffTool.static.name = 'stuff';
+ * StuffTool.static.icon = 'ellipsis';
+ * StuffTool.static.title = 'More stuff';
+ * StuffTool.prototype.onSelect = function () {
+ * $area.text( 'More stuff tool clicked!' );
+ * this.setActive( false );
+ * };
+ * StuffTool.prototype.onUpdateState = function () {};
+ * toolFactory.register( StuffTool );
+ *
+ * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
+ * // little popup window (a PopupWidget).
+ * function HelpTool( toolGroup, config ) {
+ * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
+ * padded: true,
+ * label: 'Help',
+ * head: true
+ * } }, config ) );
+ * this.popup.$body.append( '<p>I am helpful!</p>' );
+ * }
+ * OO.inheritClass( HelpTool, OO.ui.PopupTool );
+ * HelpTool.static.name = 'help';
+ * HelpTool.static.icon = 'help';
+ * HelpTool.static.title = 'Help';
+ * toolFactory.register( HelpTool );
+ *
+ * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
+ * // used once (but not all defined tools must be used).
+ * toolbar.setup( [
+ * {
+ * // 'bar' tool groups display tools' icons only, side-by-side.
+ * type: 'bar',
+ * include: [ 'search', 'help' ]
+ * },
+ * {
+ * // 'list' tool groups display both the titles and icons, in a dropdown list.
+ * type: 'list',
+ * indicator: 'down',
+ * label: 'More',
+ * include: [ 'settings', 'stuff' ]
+ * }
+ * // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
+ * // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
+ * // since it's more complicated to use. (See the next example snippet on this page.)
+ * ] );
+ *
+ * // Create some UI around the toolbar and place it in the document
+ * var frame = new OO.ui.PanelLayout( {
+ * expanded: false,
+ * framed: true
+ * } );
+ * var contentFrame = new OO.ui.PanelLayout( {
+ * expanded: false,
+ * padded: true
+ * } );
+ * frame.$element.append(
+ * toolbar.$element,
+ * contentFrame.$element.append( $area )
+ * );
+ * $( 'body' ).append( frame.$element );
+ *
+ * // Here is where the toolbar is actually built. This must be done after inserting it into the
+ * // document.
+ * toolbar.initialize();
+ * toolbar.emit( 'updateState' );
+ *
+ * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
+ * {@link #event-updateState 'updateState' event}.
+ *
+ * @example
+ * // Create the toolbar
+ * var toolFactory = new OO.ui.ToolFactory();
+ * var toolGroupFactory = new OO.ui.ToolGroupFactory();
+ * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
+ *
+ * // We will be placing status text in this element when tools are used
+ * var $area = $( '<p>' ).text( 'Toolbar example' );
+ *
+ * // Define the tools that we're going to place in our toolbar
+ *
+ * // Create a class inheriting from OO.ui.Tool
+ * function SearchTool() {
+ * SearchTool.parent.apply( this, arguments );
+ * }
+ * OO.inheritClass( SearchTool, OO.ui.Tool );
+ * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
+ * // of 'icon' and 'title' (displayed icon and text).
+ * SearchTool.static.name = 'search';
+ * SearchTool.static.icon = 'search';
+ * SearchTool.static.title = 'Search...';
+ * // Defines the action that will happen when this tool is selected (clicked).
+ * SearchTool.prototype.onSelect = function () {
+ * $area.text( 'Search tool clicked!' );
+ * // Never display this tool as "active" (selected).
+ * this.setActive( false );
+ * };
+ * SearchTool.prototype.onUpdateState = function () {};
+ * // Make this tool available in our toolFactory and thus our toolbar
+ * toolFactory.register( SearchTool );
+ *
+ * // Register two more tools, nothing interesting here
+ * function SettingsTool() {
+ * SettingsTool.parent.apply( this, arguments );
+ * this.reallyActive = false;
+ * }
+ * OO.inheritClass( SettingsTool, OO.ui.Tool );
+ * SettingsTool.static.name = 'settings';
+ * SettingsTool.static.icon = 'settings';
+ * SettingsTool.static.title = 'Change settings';
+ * SettingsTool.prototype.onSelect = function () {
+ * $area.text( 'Settings tool clicked!' );
+ * // Toggle the active state on each click
+ * this.reallyActive = !this.reallyActive;
+ * this.setActive( this.reallyActive );
+ * // To update the menu label
+ * this.toolbar.emit( 'updateState' );
+ * };
+ * SettingsTool.prototype.onUpdateState = function () {};
+ * toolFactory.register( SettingsTool );
+ *
+ * // Register two more tools, nothing interesting here
+ * function StuffTool() {
+ * StuffTool.parent.apply( this, arguments );
+ * this.reallyActive = false;
+ * }
+ * OO.inheritClass( StuffTool, OO.ui.Tool );
+ * StuffTool.static.name = 'stuff';
+ * StuffTool.static.icon = 'ellipsis';
+ * StuffTool.static.title = 'More stuff';
+ * StuffTool.prototype.onSelect = function () {
+ * $area.text( 'More stuff tool clicked!' );
+ * // Toggle the active state on each click
+ * this.reallyActive = !this.reallyActive;
+ * this.setActive( this.reallyActive );
+ * // To update the menu label
+ * this.toolbar.emit( 'updateState' );
+ * };
+ * StuffTool.prototype.onUpdateState = function () {};
+ * toolFactory.register( StuffTool );
+ *
+ * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
+ * // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
+ * function HelpTool( toolGroup, config ) {
+ * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
+ * padded: true,
+ * label: 'Help',
+ * head: true
+ * } }, config ) );
+ * this.popup.$body.append( '<p>I am helpful!</p>' );
+ * }
+ * OO.inheritClass( HelpTool, OO.ui.PopupTool );
+ * HelpTool.static.name = 'help';
+ * HelpTool.static.icon = 'help';
+ * HelpTool.static.title = 'Help';
+ * toolFactory.register( HelpTool );
+ *
+ * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
+ * // used once (but not all defined tools must be used).
+ * toolbar.setup( [
+ * {
+ * // 'bar' tool groups display tools' icons only, side-by-side.
+ * type: 'bar',
+ * include: [ 'search', 'help' ]
+ * },
+ * {
+ * // 'menu' tool groups display both the titles and icons, in a dropdown menu.
+ * // Menu label indicates which items are selected.
+ * type: 'menu',
+ * indicator: 'down',
+ * include: [ 'settings', 'stuff' ]
+ * }
+ * ] );
+ *
+ * // Create some UI around the toolbar and place it in the document
+ * var frame = new OO.ui.PanelLayout( {
+ * expanded: false,
+ * framed: true
+ * } );
+ * var contentFrame = new OO.ui.PanelLayout( {
+ * expanded: false,
+ * padded: true
+ * } );
+ * frame.$element.append(
+ * toolbar.$element,
+ * contentFrame.$element.append( $area )
+ * );
+ * $( 'body' ).append( frame.$element );
+ *
+ * // Here is where the toolbar is actually built. This must be done after inserting it into the
+ * // document.
+ * toolbar.initialize();
+ * toolbar.emit( 'updateState' );
+ *
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
+ * @mixins OO.ui.mixin.GroupElement
+ *
+ * @constructor
+ * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
+ * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [actions] Add an actions section to the toolbar. Actions are commands that are included
+ * in the toolbar, but are not configured as tools. By default, actions are displayed on the right side of
+ * the toolbar.
+ * @cfg {boolean} [shadow] Add a shadow below the toolbar.
+ */
+OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
+ config = toolFactory;
+ toolFactory = config.toolFactory;
+ toolGroupFactory = config.toolGroupFactory;
+ }
+
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.Toolbar.parent.call( this, config );
+
+ // Mixin constructors
+ OO.EventEmitter.call( this );
+ OO.ui.mixin.GroupElement.call( this, config );
+
+ // Properties
+ this.toolFactory = toolFactory;
+ this.toolGroupFactory = toolGroupFactory;
+ this.groups = [];
+ this.tools = {};
+ this.$bar = $( '<div>' );
+ this.$actions = $( '<div>' );
+ this.initialized = false;
+ this.onWindowResizeHandler = this.onWindowResize.bind( this );
+
+ // Events
+ this.$element
+ .add( this.$bar ).add( this.$group ).add( this.$actions )
+ .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
+
+ // Initialization
+ this.$group.addClass( 'oo-ui-toolbar-tools' );
+ if ( config.actions ) {
+ this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
+ }
+ this.$bar
+ .addClass( 'oo-ui-toolbar-bar' )
+ .append( this.$group, '<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.Toolbar, OO.ui.Element );
+OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
+OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
+
+/* Events */
+
+/**
+ * @event updateState
+ *
+ * An 'updateState' event must be emitted on the Toolbar (by calling `toolbar.emit( 'updateState' )`)
+ * every time the state of the application using the toolbar changes, and an update to the state of
+ * tools is required.
+ *
+ * @param {Mixed...} data Application-defined parameters
+ */
+
+/* Methods */
+
+/**
+ * Get the tool factory.
+ *
+ * @return {OO.ui.ToolFactory} Tool factory
+ */
+OO.ui.Toolbar.prototype.getToolFactory = function () {
+ return this.toolFactory;
+};
+
+/**
+ * Get the toolgroup factory.
+ *
+ * @return {OO.Factory} Toolgroup factory
+ */
+OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
+ return this.toolGroupFactory;
+};
+
+/**
+ * Handles mouse down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
+ var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
+ $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
+ if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
+ return false;
+ }
+};
+
+/**
+ * Handle window resize event.
+ *
+ * @private
+ * @param {jQuery.Event} e Window resize event
+ */
+OO.ui.Toolbar.prototype.onWindowResize = function () {
+ this.$element.toggleClass(
+ 'oo-ui-toolbar-narrow',
+ this.$bar.width() <= this.narrowThreshold
+ );
+};
+
+/**
+ * Sets up handles and preloads required information for the toolbar to work.
+ * This must be called after it is attached to a visible document and before doing anything else.
+ */
+OO.ui.Toolbar.prototype.initialize = function () {
+ if ( !this.initialized ) {
+ this.initialized = true;
+ this.narrowThreshold = this.$group.width() + this.$actions.width();
+ $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
+ this.onWindowResize();
+ }
+};
+
+/**
+ * Set up the toolbar.
+ *
+ * The toolbar is set up with a list of toolgroup configurations that specify the type of
+ * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or {@link OO.ui.ListToolGroup list})
+ * to add and which tools to include, exclude, promote, or demote within that toolgroup. Please
+ * see {@link OO.ui.ToolGroup toolgroups} for more information about including tools in toolgroups.
+ *
+ * @param {Object.<string,Array>} groups List of toolgroup configurations
+ * @param {Array|string} [groups.include] Tools to include in the toolgroup
+ * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
+ * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
+ * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
+ */
+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 = OO.ui.msg( 'ooui-toolbar-more' );
+ }
+ }
+ // Check type has been registered
+ type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
+ items.push(
+ this.getToolGroupFactory().create( type, this, group )
+ );
+ }
+ this.addItems( items );
+};
+
+/**
+ * Remove all tools and toolgroups 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();
+};
+
+/**
+ * Destroy the toolbar.
+ *
+ * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar. Call
+ * this method whenever you are done using a toolbar.
+ */
+OO.ui.Toolbar.prototype.destroy = function () {
+ $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
+ this.reset();
+ this.$element.remove();
+};
+
+/**
+ * Check if the tool is available.
+ *
+ * Available tools are ones that have not yet been added to the toolbar.
+ *
+ * @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.
+ *
+ * The OOjs UI library does not contain an accelerator system, but this is the hook for one. To
+ * use an accelerator system, subclass the toolbar and override this method, which is meant to return a label
+ * that describes the accelerator keys for the tool passed (by symbolic name) to the method.
+ *
+ * @param {string} name Symbolic name of tool
+ * @return {string|undefined} Tool accelerator label if available
+ */
+OO.ui.Toolbar.prototype.getToolAccelerator = function () {
+ return undefined;
+};
+
+/**
+ * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute {@link OO.ui.Toolbar toolbars}.
+ * Each tool is configured with a static name, title, and icon and is customized with the command to carry
+ * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
+ * which creates the tools on demand.
+ *
+ * Every Tool subclass must implement two methods:
+ *
+ * - {@link #onUpdateState}
+ * - {@link #onSelect}
+ *
+ * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
+ * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
+ * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
+ *
+ * For more information, please see the [OOjs UI documentation on MediaWiki][1].
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.FlaggedElement
+ * @mixins OO.ui.mixin.TabIndexedElement
+ *
+ * @constructor
+ * @param {OO.ui.ToolGroup} toolGroup
+ * @param {Object} [config] Configuration options
+ * @cfg {string|Function} [title] Title text or a function that returns text. If this config is omitted, the value of
+ * the {@link #static-title static title} property is used.
+ *
+ * The title is used in different ways depending on the type of toolgroup that contains the tool. The
+ * title is used as a tooltip if the tool is part of a {@link OO.ui.BarToolGroup bar} toolgroup, or as the label text if the tool is
+ * part of a {@link OO.ui.ListToolGroup list} or {@link OO.ui.MenuToolGroup menu} toolgroup.
+ *
+ * For bar toolgroups, a description of the accelerator key is appended to the title if an accelerator key
+ * is associated with an action by the same name as the tool and accelerator functionality has been added to the application.
+ * To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
+ */
+OO.ui.Tool = function OoUiTool( toolGroup, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
+ config = toolGroup;
+ toolGroup = config.toolGroup;
+ }
+
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.Tool.parent.call( this, config );
+
+ // Properties
+ this.toolGroup = toolGroup;
+ this.toolbar = this.toolGroup.getToolbar();
+ this.active = false;
+ this.$title = $( '<span>' );
+ this.$accel = $( '<span>' );
+ this.$link = $( '<a>' );
+ this.title = null;
+
+ // Mixin constructors
+ OO.ui.mixin.IconElement.call( this, config );
+ OO.ui.mixin.FlaggedElement.call( this, config );
+ OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$link } ) );
+
+ // Events
+ this.toolbar.connect( this, { updateState: 'onUpdateState' } );
+
+ // Initialization
+ this.$title.addClass( 'oo-ui-tool-title' );
+ this.$accel
+ .addClass( 'oo-ui-tool-accel' )
+ .prop( {
+ // This may need to be changed if the key names are ever localized,
+ // but for now they are essentially written in English
+ dir: 'ltr',
+ lang: 'en'
+ } );
+ this.$link
+ .addClass( 'oo-ui-tool-link' )
+ .append( this.$icon, this.$title, this.$accel )
+ .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' )
+ )
+ .toggleClass( 'oo-ui-tool-with-label', this.constructor.static.displayBothIconAndLabel )
+ .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.mixin.IconElement );
+OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
+OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
+
+/* Static Properties */
+
+/**
+ * @static
+ * @inheritdoc
+ */
+OO.ui.Tool.static.tagName = 'span';
+
+/**
+ * Symbolic name of tool.
+ *
+ * The symbolic name is used internally to register the tool with a {@link OO.ui.ToolFactory ToolFactory}. It can
+ * also be used when adding tools to toolgroups.
+ *
+ * @abstract
+ * @static
+ * @inheritable
+ * @property {string}
+ */
+OO.ui.Tool.static.name = '';
+
+/**
+ * Symbolic name of the group.
+ *
+ * The group name is used to associate tools with each other so that they can be selected later by
+ * a {@link OO.ui.ToolGroup toolgroup}.
+ *
+ * @abstract
+ * @static
+ * @inheritable
+ * @property {string}
+ */
+OO.ui.Tool.static.group = '';
+
+/**
+ * Tool title text or a function that returns title text. The value of the static property is overridden if the #title config option is used.
+ *
+ * @abstract
+ * @static
+ * @inheritable
+ * @property {string|Function}
+ */
+OO.ui.Tool.static.title = '';
+
+/**
+ * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
+ * Normally only the icon is displayed, or only the label if no icon is given.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.Tool.static.displayBothIconAndLabel = false;
+
+/**
+ * Add tool to catch-all groups automatically.
+ *
+ * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
+ * can be included in a toolgroup using the wildcard selector, an asterisk (*).
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.Tool.static.autoAddToCatchall = true;
+
+/**
+ * Add tool to named groups automatically.
+ *
+ * By default, tools that are configured with a static ‘group’ property are added
+ * to that group and will be selected when the symbolic name of the group is specified (e.g., when
+ * toolgroups include tools by group name).
+ *
+ * @static
+ * @property {boolean}
+ * @inheritable
+ */
+OO.ui.Tool.static.autoAddToGroup = true;
+
+/**
+ * Check if this tool is compatible with given data.
+ *
+ * This is a stub that can be overridden to provide support for filtering tools based on an
+ * arbitrary piece of information (e.g., where the cursor is in a document). The implementation
+ * must also call this method so that the compatibility check can be performed.
+ *
+ * @static
+ * @inheritable
+ * @param {Mixed} data Data to check
+ * @return {boolean} Tool can be used with data
+ */
+OO.ui.Tool.static.isCompatibleWith = function () {
+ return false;
+};
+
+/* Methods */
+
+/**
+ * Handle the toolbar state being updated. This method is called when the
+ * {@link OO.ui.Toolbar#event-updateState 'updateState' event} is emitted on the
+ * {@link OO.ui.Toolbar Toolbar} that uses this tool, and should set the state of this tool
+ * depending on application state (usually by calling #setDisabled to enable or disable the tool,
+ * or #setActive to mark is as currently in-use or not).
+ *
+ * This is an abstract method that must be overridden in a concrete subclass.
+ *
+ * @method
+ * @protected
+ * @abstract
+ */
+OO.ui.Tool.prototype.onUpdateState = null;
+
+/**
+ * Handle the tool being selected. This method is called when the user triggers this tool,
+ * usually by clicking on its label/icon.
+ *
+ * This is an abstract method that must be overridden in a concrete subclass.
+ *
+ * @method
+ * @protected
+ * @abstract
+ */
+OO.ui.Tool.prototype.onSelect = null;
+
+/**
+ * Check if the tool is active.
+ *
+ * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
+ * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
+ *
+ * @return {boolean} Tool is active
+ */
+OO.ui.Tool.prototype.isActive = function () {
+ return this.active;
+};
+
+/**
+ * Make the tool appear active or inactive.
+ *
+ * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
+ * appear pressed or not.
+ *
+ * @param {boolean} state Make tool 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' );
+ }
+};
+
+/**
+ * Set the tool #title.
+ *
+ * @param {string|Function} title Title text or a function that returns text
+ * @chainable
+ */
+OO.ui.Tool.prototype.setTitle = function ( title ) {
+ this.title = OO.ui.resolveMsg( title );
+ this.updateTitle();
+ return this;
+};
+
+/**
+ * Get the tool #title.
+ *
+ * @return {string} Title text
+ */
+OO.ui.Tool.prototype.getTitle = function () {
+ return this.title;
+};
+
+/**
+ * Get the tool's symbolic name.
+ *
+ * @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.text( this.title );
+ this.$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.
+ *
+ * Destroying the tool removes all event handlers and the tool’s DOM elements.
+ * Call this method whenever you are done using a tool.
+ */
+OO.ui.Tool.prototype.destroy = function () {
+ this.toolbar.disconnect( this );
+ this.$element.remove();
+};
+
+/**
+ * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a {@link OO.ui.Toolbar toolbar}.
+ * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or {@link OO.ui.MenuToolGroup menu})
+ * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
+ * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
+ *
+ * Toolgroups can contain individual tools, groups of tools, or all available tools, as specified
+ * using the `include` config option. See OO.ui.ToolFactory#extract on documentation of the format.
+ * The options `exclude`, `promote`, and `demote` support the same formats.
+ *
+ * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
+ * please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.GroupElement
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ * @cfg {Array|string} [include] List of tools to include in the toolgroup, see above.
+ * @cfg {Array|string} [exclude] List of tools to exclude from the toolgroup, see above.
+ * @cfg {Array|string} [promote] List of tools to promote to the beginning of the toolgroup, see above.
+ * @cfg {Array|string} [demote] List of tools to demote to the end of the toolgroup, see above.
+ * This setting is particularly useful when tools have been added to the toolgroup
+ * en masse (e.g., via the catch-all selector).
+ */
+OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( toolbar ) && config === undefined ) {
+ config = toolbar;
+ toolbar = config.toolbar;
+ }
+
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.ToolGroup.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.GroupElement.call( this, 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.onCapturedMouseKeyUpHandler = this.onCapturedMouseKeyUp.bind( this );
+
+ // Events
+ this.$element.on( {
+ mousedown: this.onMouseKeyDown.bind( this ),
+ mouseup: this.onMouseKeyUp.bind( this ),
+ keydown: this.onMouseKeyDown.bind( this ),
+ keyup: this.onMouseKeyUp.bind( this ),
+ focus: this.onMouseOverFocus.bind( this ),
+ blur: this.onMouseOutBlur.bind( this ),
+ mouseover: this.onMouseOverFocus.bind( this ),
+ mouseout: this.onMouseOutBlur.bind( 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.mixin.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.
+ *
+ * Note: The OOjs UI library does not include an accelerator system, but does contain
+ * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
+ * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
+ * meant to return a label that describes the accelerator keys for a given tool (e.g., 'Ctrl + M').
+ *
+ * @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.parent.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.parent.prototype.updateDisabled.apply( this, arguments );
+};
+
+/**
+ * Handle mouse down and key down events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse down or key down event
+ */
+OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
+ if (
+ !this.isDisabled() &&
+ ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+ ) {
+ this.pressed = this.getTargetTool( e );
+ if ( this.pressed ) {
+ this.pressed.setActive( true );
+ this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
+ this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
+ }
+ return false;
+ }
+};
+
+/**
+ * Handle captured mouse up and key up events.
+ *
+ * @protected
+ * @param {Event} e Mouse up or key up event
+ */
+OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function ( e ) {
+ this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
+ this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
+ // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
+ // released, but since `this.pressed` will no longer be true, the second call will be ignored.
+ this.onMouseKeyUp( e );
+};
+
+/**
+ * Handle mouse up and key up events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse up or key up event
+ */
+OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
+ var tool = this.getTargetTool( e );
+
+ if (
+ !this.isDisabled() && this.pressed && this.pressed === tool &&
+ ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+ ) {
+ this.pressed.onSelect();
+ this.pressed = null;
+ return false;
+ }
+
+ this.pressed = null;
+};
+
+/**
+ * Handle mouse over and focus events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse over or focus event
+ */
+OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
+ var tool = this.getTargetTool( e );
+
+ if ( this.pressed && this.pressed === tool ) {
+ this.pressed.setActive( true );
+ }
+};
+
+/**
+ * Handle mouse out and blur events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse out or blur event
+ */
+OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
+ var tool = this.getTargetTool( e );
+
+ if ( this.pressed && this.pressed === tool ) {
+ this.pressed.setActive( false );
+ }
+};
+
+/**
+ * Get the closest tool to a jQuery.Event.
+ *
+ * Only tool links are considered, which prevents other elements in the tool such as popups from
+ * triggering tool group interactions.
+ *
+ * @private
+ * @param {jQuery.Event} e
+ * @return {OO.ui.Tool|null} Tool, `null` if none was found
+ */
+OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
+ var tool,
+ $item = $( e.target ).closest( '.oo-ui-tool-link' );
+
+ if ( $item.length ) {
+ tool = $item.parent().data( 'oo-ui-tool' );
+ }
+
+ return tool && !tool.isDisabled() ? tool : null;
+};
+
+/**
+ * Handle tool registry register events.
+ *
+ * If a tool is registered after the group is created, we must repopulate the list to account for:
+ *
+ * - a tool being added that may be included
+ * - a tool already included being overridden
+ *
+ * @protected
+ * @param {string} name Symbolic name of tool
+ */
+OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
+ this.populate();
+};
+
+/**
+ * Get the toolbar that contains the toolgroup.
+ *
+ * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
+ */
+OO.ui.ToolGroup.prototype.getToolbar = function () {
+ return this.toolbar;
+};
+
+/**
+ * Add and remove tools based on configuration.
+ */
+OO.ui.ToolGroup.prototype.populate = function () {
+ var i, len, name, tool,
+ toolFactory = this.toolbar.getToolFactory(),
+ names = {},
+ add = [],
+ remove = [],
+ list = this.toolbar.getToolFactory().getTools(
+ this.include, this.exclude, this.promote, this.demote
+ );
+
+ // Build a list of needed tools
+ for ( i = 0, len = list.length; i < len; i++ ) {
+ name = list[ i ];
+ if (
+ // Tool exists
+ toolFactory.lookup( name ) &&
+ // Tool is available or is already in this group
+ ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
+ ) {
+ // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
+ // creating it, but we can't call reserveTool() yet because we haven't created the tool.
+ this.toolbar.tools[ name ] = true;
+ tool = this.tools[ name ];
+ if ( !tool ) {
+ // Auto-initialize tools on first use
+ this.tools[ name ] = tool = toolFactory.create( name, this );
+ tool.updateTitle();
+ }
+ this.toolbar.reserveTool( tool );
+ add.push( tool );
+ names[ name ] = true;
+ }
+ }
+ // Remove tools that are no longer needed
+ for ( name in this.tools ) {
+ if ( !names[ name ] ) {
+ this.tools[ name ].destroy();
+ this.toolbar.releaseTool( this.tools[ name ] );
+ remove.push( this.tools[ name ] );
+ delete this.tools[ name ];
+ }
+ }
+ if ( remove.length ) {
+ this.removeItems( remove );
+ }
+ // Update emptiness state
+ if ( add.length ) {
+ this.$element.removeClass( 'oo-ui-toolGroup-empty' );
+ } else {
+ this.$element.addClass( 'oo-ui-toolGroup-empty' );
+ }
+ // Re-add tools (moving existing ones to new locations)
+ this.addItems( add );
+ // Disabled state may depend on items
+ this.updateDisabled();
+};
+
+/**
+ * Destroy toolgroup.
+ */
+OO.ui.ToolGroup.prototype.destroy = function () {
+ var name;
+
+ this.clearItems();
+ this.toolbar.getToolFactory().disconnect( this );
+ for ( name in this.tools ) {
+ this.toolbar.releaseTool( this.tools[ name ] );
+ this.tools[ name ].disconnect( this ).destroy();
+ delete this.tools[ name ];
+ }
+ this.$element.remove();
+};
+
+/**
+ * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools}, {@link OO.ui.PopupTool PopupTools},
+ * and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be registered with a tool factory. Tools are
+ * registered by their symbolic name. See {@link OO.ui.Toolbar toolbars} for an example.
+ *
+ * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
+ *
+ * @class
+ * @extends OO.Factory
+ * @constructor
+ */
+OO.ui.ToolFactory = function OoUiToolFactory() {
+ // Parent constructor
+ OO.ui.ToolFactory.parent.call( this );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
+
+/* Methods */
+
+/**
+ * Get tools from the factory
+ *
+ * @param {Array|string} [include] Included tools, see #extract for format
+ * @param {Array|string} [exclude] Excluded tools, see #extract for format
+ * @param {Array|string} [promote] Promoted tools, see #extract for format
+ * @param {Array|string} [demote] Demoted tools, see #extract for format
+ * @return {string[]} List of tools
+ */
+OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
+ var i, len, included, promoted, demoted,
+ auto = [],
+ used = {};
+
+ // Collect included and not excluded tools
+ included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
+
+ // Promotion
+ promoted = this.extract( promote, used );
+ demoted = this.extract( demote, used );
+
+ // Auto
+ for ( i = 0, len = included.length; i < len; i++ ) {
+ if ( !used[ included[ i ] ] ) {
+ auto.push( included[ i ] );
+ }
+ }
+
+ return promoted.concat( auto ).concat( demoted );
+};
+
+/**
+ * Get a flat list of names from a list of names or groups.
+ *
+ * Normally, `collection` is an array of tool specifications. Tools can be specified in the
+ * following ways:
+ *
+ * - To include an individual tool, use the symbolic name: `{ name: 'tool-name' }` or `'tool-name'`.
+ * - To include all tools in a group, use the group name: `{ group: 'group-name' }`. (To assign the
+ * tool to a group, use OO.ui.Tool.static.group.)
+ *
+ * Alternatively, to include all tools that are not yet assigned to any other toolgroup, use the
+ * catch-all selector `'*'`.
+ *
+ * If `used` is passed, tool names that appear as properties in this object will be considered
+ * already assigned, and will not be returned even if specified otherwise. The tool names extracted
+ * by this function call will be added as new properties in the object.
+ *
+ * @private
+ * @param {Array|string} collection List of tools, see above
+ * @param {Object} [used] Object containing information about used tools, see above
+ * @return {string[]} List of extracted tool names
+ */
+OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
+ var i, len, item, name, tool,
+ names = [];
+
+ if ( collection === '*' ) {
+ for ( name in this.registry ) {
+ tool = this.registry[ name ];
+ if (
+ // Only add tools by group name when auto-add is enabled
+ tool.static.autoAddToCatchall &&
+ // Exclude already used tools
+ ( !used || !used[ name ] )
+ ) {
+ names.push( name );
+ if ( used ) {
+ used[ name ] = true;
+ }
+ }
+ }
+ } else if ( Array.isArray( collection ) ) {
+ for ( i = 0, len = collection.length; i < len; i++ ) {
+ item = collection[ i ];
+ // Allow plain strings as shorthand for named tools
+ if ( typeof item === 'string' ) {
+ item = { name: item };
+ }
+ if ( OO.isPlainObject( item ) ) {
+ if ( item.group ) {
+ for ( name in this.registry ) {
+ tool = this.registry[ name ];
+ if (
+ // Include tools with matching group
+ tool.static.group === item.group &&
+ // Only add tools by group name when auto-add is enabled
+ tool.static.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;
+};
+
+/**
+ * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes must
+ * specify a symbolic name and be registered with the factory. The following classes are registered by
+ * default:
+ *
+ * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
+ * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
+ * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
+ *
+ * See {@link OO.ui.Toolbar toolbars} for an example.
+ *
+ * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
+ * @class
+ * @extends OO.Factory
+ * @constructor
+ */
+OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
+ var i, l, defaultClasses;
+ // Parent constructor
+ OO.Factory.call( this );
+
+ defaultClasses = this.constructor.static.getDefaultClasses();
+
+ // Register default toolgroups
+ for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
+ this.register( defaultClasses[ i ] );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
+
+/* Static Methods */
+
+/**
+ * Get a default set of classes to be registered on construction.
+ *
+ * @return {Function[]} Default classes
+ */
+OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
+ return [
+ OO.ui.BarToolGroup,
+ OO.ui.ListToolGroup,
+ OO.ui.MenuToolGroup
+ ];
+};
+
+/**
+ * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}. Each popup tool is configured
+ * with a static name, title, and icon, as well with as any popup configurations. Unlike other tools, popup tools do not require that developers specify
+ * an #onSelect or #onUpdateState method, as these methods have been implemented already.
+ *
+ * // Example of a popup tool. When selected, a popup tool displays
+ * // a popup window.
+ * function HelpTool( toolGroup, config ) {
+ * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
+ * padded: true,
+ * label: 'Help',
+ * head: true
+ * } }, config ) );
+ * this.popup.$body.append( '<p>I am helpful!</p>' );
+ * };
+ * OO.inheritClass( HelpTool, OO.ui.PopupTool );
+ * HelpTool.static.name = 'help';
+ * HelpTool.static.icon = 'help';
+ * HelpTool.static.title = 'Help';
+ * toolFactory.register( HelpTool );
+ *
+ * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}. For more information about
+ * toolbars in genreral, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Tool
+ * @mixins OO.ui.mixin.PopupElement
+ *
+ * @constructor
+ * @param {OO.ui.ToolGroup} toolGroup
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
+ config = toolGroup;
+ toolGroup = config.toolGroup;
+ }
+
+ // Parent constructor
+ OO.ui.PopupTool.parent.call( this, toolGroup, config );
+
+ // Mixin constructors
+ OO.ui.mixin.PopupElement.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.mixin.PopupElement );
+
+/* Methods */
+
+/**
+ * Handle the tool being selected.
+ *
+ * @inheritdoc
+ */
+OO.ui.PopupTool.prototype.onSelect = function () {
+ if ( !this.isDisabled() ) {
+ this.popup.toggle();
+ }
+ this.setActive( false );
+ return false;
+};
+
+/**
+ * Handle the toolbar state being updated.
+ *
+ * @inheritdoc
+ */
+OO.ui.PopupTool.prototype.onUpdateState = function () {
+ this.setActive( false );
+};
+
+/**
+ * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
+ * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
+ * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
+ * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
+ * when the ToolGroupTool is selected.
+ *
+ * // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2', defined elsewhere.
+ *
+ * function SettingsTool() {
+ * SettingsTool.parent.apply( this, arguments );
+ * };
+ * OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
+ * SettingsTool.static.name = 'settings';
+ * SettingsTool.static.title = 'Change settings';
+ * SettingsTool.static.groupConfig = {
+ * icon: 'settings',
+ * label: 'ToolGroupTool',
+ * include: [ 'setting1', 'setting2' ]
+ * };
+ * toolFactory.register( SettingsTool );
+ *
+ * For more information, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * Please note that this implementation is subject to change per [T74159] [2].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars#ToolGroupTool
+ * [2]: https://phabricator.wikimedia.org/T74159
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Tool
+ *
+ * @constructor
+ * @param {OO.ui.ToolGroup} toolGroup
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
+ config = toolGroup;
+ toolGroup = config.toolGroup;
+ }
+
+ // Parent constructor
+ OO.ui.ToolGroupTool.parent.call( this, toolGroup, config );
+
+ // Properties
+ this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
+
+ // Events
+ this.innerToolGroup.connect( this, { disable: 'onToolGroupDisable' } );
+
+ // Initialization
+ this.$link.remove();
+ this.$element
+ .addClass( 'oo-ui-toolGroupTool' )
+ .append( this.innerToolGroup.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
+
+/* Static Properties */
+
+/**
+ * Toolgroup configuration.
+ *
+ * The toolgroup configuration consists of the tools to include, as well as an icon and label
+ * to use for the bar item. Tools can be included by symbolic name, group, or with the
+ * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
+ *
+ * @property {Object.<string,Array>}
+ */
+OO.ui.ToolGroupTool.static.groupConfig = {};
+
+/* Methods */
+
+/**
+ * Handle the tool being selected.
+ *
+ * @inheritdoc
+ */
+OO.ui.ToolGroupTool.prototype.onSelect = function () {
+ this.innerToolGroup.setActive( !this.innerToolGroup.active );
+ return false;
+};
+
+/**
+ * Synchronize disabledness state of the tool with the inner toolgroup.
+ *
+ * @private
+ * @param {boolean} disabled Element is disabled
+ */
+OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
+ this.setDisabled( disabled );
+};
+
+/**
+ * Handle the toolbar state being updated.
+ *
+ * @inheritdoc
+ */
+OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
+ this.setActive( false );
+};
+
+/**
+ * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
+ *
+ * @param {Object.<string,Array>} group Toolgroup configuration. Please see {@link OO.ui.ToolGroup toolgroup} for
+ * more information.
+ * @return {OO.ui.ListToolGroup}
+ */
+OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
+ if ( group.include === '*' ) {
+ // Apply defaults to catch-all groups
+ if ( group.label === undefined ) {
+ group.label = OO.ui.msg( 'ooui-toolbar-more' );
+ }
+ }
+
+ return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
+};
+
+/**
+ * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
+ * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
+ * and {@link OO.ui.ListToolGroup ListToolGroup}). The {@link OO.ui.Tool tools} in a BarToolGroup are
+ * displayed by icon in a single row. The title of the tool is displayed when users move the mouse over
+ * the tool.
+ *
+ * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar is
+ * set up.
+ *
+ * @example
+ * // Example of a BarToolGroup with two tools
+ * var toolFactory = new OO.ui.ToolFactory();
+ * var toolGroupFactory = new OO.ui.ToolGroupFactory();
+ * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
+ *
+ * // We will be placing status text in this element when tools are used
+ * var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
+ *
+ * // Define the tools that we're going to place in our toolbar
+ *
+ * // Create a class inheriting from OO.ui.Tool
+ * function SearchTool() {
+ * SearchTool.parent.apply( this, arguments );
+ * }
+ * OO.inheritClass( SearchTool, OO.ui.Tool );
+ * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
+ * // of 'icon' and 'title' (displayed icon and text).
+ * SearchTool.static.name = 'search';
+ * SearchTool.static.icon = 'search';
+ * SearchTool.static.title = 'Search...';
+ * // Defines the action that will happen when this tool is selected (clicked).
+ * SearchTool.prototype.onSelect = function () {
+ * $area.text( 'Search tool clicked!' );
+ * // Never display this tool as "active" (selected).
+ * this.setActive( false );
+ * };
+ * SearchTool.prototype.onUpdateState = function () {};
+ * // Make this tool available in our toolFactory and thus our toolbar
+ * toolFactory.register( SearchTool );
+ *
+ * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
+ * // little popup window (a PopupWidget).
+ * function HelpTool( toolGroup, config ) {
+ * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
+ * padded: true,
+ * label: 'Help',
+ * head: true
+ * } }, config ) );
+ * this.popup.$body.append( '<p>I am helpful!</p>' );
+ * }
+ * OO.inheritClass( HelpTool, OO.ui.PopupTool );
+ * HelpTool.static.name = 'help';
+ * HelpTool.static.icon = 'help';
+ * HelpTool.static.title = 'Help';
+ * toolFactory.register( HelpTool );
+ *
+ * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
+ * // used once (but not all defined tools must be used).
+ * toolbar.setup( [
+ * {
+ * // 'bar' tool groups display tools by icon only
+ * type: 'bar',
+ * include: [ 'search', 'help' ]
+ * }
+ * ] );
+ *
+ * // Create some UI around the toolbar and place it in the document
+ * var frame = new OO.ui.PanelLayout( {
+ * expanded: false,
+ * framed: true
+ * } );
+ * var contentFrame = new OO.ui.PanelLayout( {
+ * expanded: false,
+ * padded: true
+ * } );
+ * frame.$element.append(
+ * toolbar.$element,
+ * contentFrame.$element.append( $area )
+ * );
+ * $( 'body' ).append( frame.$element );
+ *
+ * // Here is where the toolbar is actually built. This must be done after inserting it into the
+ * // document.
+ * toolbar.initialize();
+ *
+ * For more information about how to add tools to a bar tool group, please see {@link OO.ui.ToolGroup toolgroup}.
+ * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
+ *
+ * @class
+ * @extends OO.ui.ToolGroup
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( toolbar ) && config === undefined ) {
+ config = toolbar;
+ toolbar = config.toolbar;
+ }
+
+ // Parent constructor
+ OO.ui.BarToolGroup.parent.call( this, toolbar, config );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-barToolGroup' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
+
+/* Static Properties */
+
+OO.ui.BarToolGroup.static.titleTooltips = true;
+
+OO.ui.BarToolGroup.static.accelTooltips = true;
+
+OO.ui.BarToolGroup.static.name = 'bar';
+
+/**
+ * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
+ * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup--an overlaid menu or list of tools with an
+ * optional icon and label. This class can be used for other base classes that also use this functionality.
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.ToolGroup
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.IndicatorElement
+ * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.TitledElement
+ * @mixins OO.ui.mixin.ClippableElement
+ * @mixins OO.ui.mixin.TabIndexedElement
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [header] Text to display at the top of the popup
+ */
+OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( toolbar ) && config === undefined ) {
+ config = toolbar;
+ toolbar = config.toolbar;
+ }
+
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.PopupToolGroup.parent.call( this, toolbar, config );
+
+ // Properties
+ this.active = false;
+ this.dragging = false;
+ this.onBlurHandler = this.onBlur.bind( this );
+ this.$handle = $( '<span>' );
+
+ // Mixin constructors
+ OO.ui.mixin.IconElement.call( this, config );
+ OO.ui.mixin.IndicatorElement.call( this, config );
+ OO.ui.mixin.LabelElement.call( this, config );
+ OO.ui.mixin.TitledElement.call( this, config );
+ OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
+ OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
+
+ // Events
+ this.$handle.on( {
+ keydown: this.onHandleMouseKeyDown.bind( this ),
+ keyup: this.onHandleMouseKeyUp.bind( this ),
+ mousedown: this.onHandleMouseKeyDown.bind( this ),
+ mouseup: this.onHandleMouseKeyUp.bind( this )
+ } );
+
+ // Initialization
+ this.$handle
+ .addClass( 'oo-ui-popupToolGroup-handle' )
+ .append( this.$icon, this.$label, this.$indicator );
+ // If the pop-up should have a header, add it to the top of the toolGroup.
+ // Note: If this feature is useful for other widgets, we could abstract it into an
+ // OO.ui.HeaderedElement mixin constructor.
+ if ( config.header !== undefined ) {
+ this.$group
+ .prepend( $( '<span>' )
+ .addClass( 'oo-ui-popupToolGroup-header' )
+ .text( config.header )
+ );
+ }
+ this.$element
+ .addClass( 'oo-ui-popupToolGroup' )
+ .prepend( this.$handle );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IconElement );
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IndicatorElement );
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TitledElement );
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.ClippableElement );
+OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TabIndexedElement );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.PopupToolGroup.prototype.setDisabled = function () {
+ // Parent method
+ OO.ui.PopupToolGroup.parent.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/keyup, so it is not a normal blur event object.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse up or key up event
+ */
+OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
+ // Only deactivate when clicking outside the dropdown element
+ if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
+ this.setActive( false );
+ }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
+ // Only close toolgroup when a tool was actually selected
+ if (
+ !this.isDisabled() && this.pressed && this.pressed === this.getTargetTool( e ) &&
+ ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+ ) {
+ this.setActive( false );
+ }
+ return OO.ui.PopupToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
+};
+
+/**
+ * Handle mouse up and key up events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse up or key up event
+ */
+OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
+ if (
+ !this.isDisabled() &&
+ ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+ ) {
+ return false;
+ }
+};
+
+/**
+ * Handle mouse down and key down events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse down or key down event
+ */
+OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
+ if (
+ !this.isDisabled() &&
+ ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+ ) {
+ this.setActive( !this.active );
+ return false;
+ }
+};
+
+/**
+ * Switch into 'active' mode.
+ *
+ * When active, the popup is visible. A mouseup event anywhere in the document will trigger
+ * deactivation.
+ */
+OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
+ var containerWidth, containerLeft;
+ value = !!value;
+ if ( this.active !== value ) {
+ this.active = value;
+ if ( value ) {
+ this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
+ this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler, true );
+
+ this.$clippable.css( 'left', '' );
+ // Try anchoring the popup to the left first
+ this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
+ this.toggleClipping( true );
+ if ( this.isClippedHorizontally() ) {
+ // Anchoring to the left caused the popup to clip, so anchor it to the right instead
+ this.toggleClipping( false );
+ this.$element
+ .removeClass( 'oo-ui-popupToolGroup-left' )
+ .addClass( 'oo-ui-popupToolGroup-right' );
+ this.toggleClipping( true );
+ }
+ if ( this.isClippedHorizontally() ) {
+ // Anchoring to the right also caused the popup to clip, so just make it fill the container
+ containerWidth = this.$clippableScrollableContainer.width();
+ containerLeft = this.$clippableScrollableContainer.offset().left;
+
+ this.toggleClipping( false );
+ this.$element.removeClass( 'oo-ui-popupToolGroup-right' );
+
+ this.$clippable.css( {
+ left: -( this.$element.offset().left - containerLeft ),
+ width: containerWidth
+ } );
+ }
+ } else {
+ this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
+ this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler, true );
+ this.$element.removeClass(
+ 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
+ );
+ this.toggleClipping( false );
+ }
+ }
+};
+
+/**
+ * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
+ * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
+ * and {@link OO.ui.BarToolGroup BarToolGroup}). The {@link OO.ui.Tool tools} in a ListToolGroup are displayed
+ * by label in a dropdown menu. The title of the tool is used as the label text. The menu itself can be configured
+ * with a label, icon, indicator, header, and title.
+ *
+ * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a ‘More’ option that
+ * users can select to see the full list of tools. If a collapsed toolgroup is expanded, a ‘Fewer’ option permits
+ * users to collapse the list again.
+ *
+ * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the toolbar is set up. The factory
+ * requires the ListToolGroup's symbolic name, 'list', which is specified along with the other configurations. For more
+ * information about how to add tools to a ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
+ *
+ * @example
+ * // Example of a ListToolGroup
+ * var toolFactory = new OO.ui.ToolFactory();
+ * var toolGroupFactory = new OO.ui.ToolGroupFactory();
+ * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
+ *
+ * // Configure and register two tools
+ * function SettingsTool() {
+ * SettingsTool.parent.apply( this, arguments );
+ * }
+ * OO.inheritClass( SettingsTool, OO.ui.Tool );
+ * SettingsTool.static.name = 'settings';
+ * SettingsTool.static.icon = 'settings';
+ * SettingsTool.static.title = 'Change settings';
+ * SettingsTool.prototype.onSelect = function () {
+ * this.setActive( false );
+ * };
+ * SettingsTool.prototype.onUpdateState = function () {};
+ * toolFactory.register( SettingsTool );
+ * // Register two more tools, nothing interesting here
+ * function StuffTool() {
+ * StuffTool.parent.apply( this, arguments );
+ * }
+ * OO.inheritClass( StuffTool, OO.ui.Tool );
+ * StuffTool.static.name = 'stuff';
+ * StuffTool.static.icon = 'search';
+ * StuffTool.static.title = 'Change the world';
+ * StuffTool.prototype.onSelect = function () {
+ * this.setActive( false );
+ * };
+ * StuffTool.prototype.onUpdateState = function () {};
+ * toolFactory.register( StuffTool );
+ * toolbar.setup( [
+ * {
+ * // Configurations for list toolgroup.
+ * type: 'list',
+ * label: 'ListToolGroup',
+ * indicator: 'down',
+ * icon: 'ellipsis',
+ * title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
+ * header: 'This is the header',
+ * include: [ 'settings', 'stuff' ],
+ * allowCollapse: ['stuff']
+ * }
+ * ] );
+ *
+ * // Create some UI around the toolbar and place it in the document
+ * var frame = new OO.ui.PanelLayout( {
+ * expanded: false,
+ * framed: true
+ * } );
+ * frame.$element.append(
+ * toolbar.$element
+ * );
+ * $( 'body' ).append( frame.$element );
+ * // Build the toolbar. This must be done after the toolbar has been appended to the document.
+ * toolbar.initialize();
+ *
+ * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
+ *
+ * @class
+ * @extends OO.ui.PopupToolGroup
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ * @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible tools
+ * will only be displayed if users click the ‘More’ option displayed at the bottom of the list. If
+ * the list is expanded, a ‘Fewer’ option permits users to collapse the list again. Any tools that
+ * are included in the toolgroup, but are not designated as collapsible, will always be displayed.
+ * To open a collapsible list in its expanded state, set #expanded to 'true'.
+ * @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as collapsible.
+ * Unless #expanded is set to true, the collapsible tools will be collapsed when the list is first opened.
+ * @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools have
+ * been designated as collapsible. When expanded is set to true, all tools in the group will be displayed
+ * when the list is first opened. Users can collapse the list with a ‘Fewer’ option at the bottom.
+ */
+OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( toolbar ) && config === undefined ) {
+ config = toolbar;
+ toolbar = config.toolbar;
+ }
+
+ // Configuration initialization
+ config = config || {};
+
+ // Properties (must be set before parent constructor, which calls #populate)
+ this.allowCollapse = config.allowCollapse;
+ this.forceExpand = config.forceExpand;
+ this.expanded = config.expanded !== undefined ? config.expanded : false;
+ this.collapsibleTools = [];
+
+ // Parent constructor
+ OO.ui.ListToolGroup.parent.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.name = 'list';
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ListToolGroup.prototype.populate = function () {
+ var i, len, allowCollapse = [];
+
+ OO.ui.ListToolGroup.parent.prototype.populate.call( this );
+
+ // Update the list of collapsible tools
+ if ( this.allowCollapse !== undefined ) {
+ allowCollapse = this.allowCollapse;
+ } else if ( this.forceExpand !== undefined ) {
+ allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
+ }
+
+ this.collapsibleTools = [];
+ for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
+ if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
+ this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
+ }
+ }
+
+ // Keep at the end, even when tools are added
+ this.$group.append( this.getExpandCollapseTool().$element );
+
+ this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
+ this.updateCollapsibleState();
+};
+
+OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
+ var ExpandCollapseTool;
+ if ( this.expandCollapseTool === undefined ) {
+ ExpandCollapseTool = function () {
+ ExpandCollapseTool.parent.apply( this, arguments );
+ };
+
+ OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
+
+ ExpandCollapseTool.prototype.onSelect = function () {
+ this.toolGroup.expanded = !this.toolGroup.expanded;
+ this.toolGroup.updateCollapsibleState();
+ this.setActive( false );
+ };
+ ExpandCollapseTool.prototype.onUpdateState = function () {
+ // Do nothing. Tool interface requires an implementation of this function.
+ };
+
+ ExpandCollapseTool.static.name = 'more-fewer';
+
+ this.expandCollapseTool = new ExpandCollapseTool( this );
+ }
+ return this.expandCollapseTool;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
+ // Do not close the popup when the user wants to show more/fewer tools
+ if (
+ $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
+ ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+ ) {
+ // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
+ // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
+ return OO.ui.ListToolGroup.parent.parent.prototype.onMouseKeyUp.call( this, e );
+ } else {
+ return OO.ui.ListToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
+ }
+};
+
+OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
+ var i, len;
+
+ this.getExpandCollapseTool()
+ .setIcon( this.expanded ? 'collapse' : 'expand' )
+ .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
+
+ for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
+ this.collapsibleTools[ i ].toggle( this.expanded );
+ }
+};
+
+/**
+ * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
+ * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.BarToolGroup BarToolGroup}
+ * and {@link OO.ui.ListToolGroup ListToolGroup}). MenuToolGroups contain selectable {@link OO.ui.Tool tools},
+ * which are displayed by label in a dropdown menu. The tool's title is used as the label text, and the
+ * menu label is updated to reflect which tool or tools are currently selected. If no tools are selected,
+ * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
+ *
+ * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
+ * is set up.
+ *
+ * @example
+ * // Example of a MenuToolGroup
+ * var toolFactory = new OO.ui.ToolFactory();
+ * var toolGroupFactory = new OO.ui.ToolGroupFactory();
+ * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
+ *
+ * // We will be placing status text in this element when tools are used
+ * var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the dropdown menu.' );
+ *
+ * // Define the tools that we're going to place in our toolbar
+ *
+ * function SettingsTool() {
+ * SettingsTool.parent.apply( this, arguments );
+ * this.reallyActive = false;
+ * }
+ * OO.inheritClass( SettingsTool, OO.ui.Tool );
+ * SettingsTool.static.name = 'settings';
+ * SettingsTool.static.icon = 'settings';
+ * SettingsTool.static.title = 'Change settings';
+ * SettingsTool.prototype.onSelect = function () {
+ * $area.text( 'Settings tool clicked!' );
+ * // Toggle the active state on each click
+ * this.reallyActive = !this.reallyActive;
+ * this.setActive( this.reallyActive );
+ * // To update the menu label
+ * this.toolbar.emit( 'updateState' );
+ * };
+ * SettingsTool.prototype.onUpdateState = function () {};
+ * toolFactory.register( SettingsTool );
+ *
+ * function StuffTool() {
+ * StuffTool.parent.apply( this, arguments );
+ * this.reallyActive = false;
+ * }
+ * OO.inheritClass( StuffTool, OO.ui.Tool );
+ * StuffTool.static.name = 'stuff';
+ * StuffTool.static.icon = 'ellipsis';
+ * StuffTool.static.title = 'More stuff';
+ * StuffTool.prototype.onSelect = function () {
+ * $area.text( 'More stuff tool clicked!' );
+ * // Toggle the active state on each click
+ * this.reallyActive = !this.reallyActive;
+ * this.setActive( this.reallyActive );
+ * // To update the menu label
+ * this.toolbar.emit( 'updateState' );
+ * };
+ * StuffTool.prototype.onUpdateState = function () {};
+ * toolFactory.register( StuffTool );
+ *
+ * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
+ * // used once (but not all defined tools must be used).
+ * toolbar.setup( [
+ * {
+ * type: 'menu',
+ * header: 'This is the (optional) header',
+ * title: 'This is the (optional) title',
+ * indicator: 'down',
+ * include: [ 'settings', 'stuff' ]
+ * }
+ * ] );
+ *
+ * // Create some UI around the toolbar and place it in the document
+ * var frame = new OO.ui.PanelLayout( {
+ * expanded: false,
+ * framed: true
+ * } );
+ * var contentFrame = new OO.ui.PanelLayout( {
+ * expanded: false,
+ * padded: true
+ * } );
+ * frame.$element.append(
+ * toolbar.$element,
+ * contentFrame.$element.append( $area )
+ * );
+ * $( 'body' ).append( frame.$element );
+ *
+ * // Here is where the toolbar is actually built. This must be done after inserting it into the
+ * // document.
+ * toolbar.initialize();
+ * toolbar.emit( 'updateState' );
+ *
+ * For more information about how to add tools to a MenuToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
+ * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki] [1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
+ *
+ * @class
+ * @extends OO.ui.PopupToolGroup
+ *
+ * @constructor
+ * @param {OO.ui.Toolbar} toolbar
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( toolbar ) && config === undefined ) {
+ config = toolbar;
+ toolbar = config.toolbar;
+ }
+
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.MenuToolGroup.parent.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.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.
+ *
+ * @private
+ */
+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( ', ' ) || ' ' );
+};
+
+}( OO ) );
--- /dev/null
+/*!
+ * OOjs UI v0.15.2
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: 2016-02-02T22:07:06Z
+ */
+@-webkit-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-moz-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-ms-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-o-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+.oo-ui-draggableElement {
+ cursor: -webkit-grab -moz-grab, url(images/grab.cur), move;
+}
+.oo-ui-draggableElement-dragging {
+ cursor: -webkit-grabbing -moz-grabbing, url(images/grabbing.cur), move;
+ background: rgba(0, 0, 0, 0.2);
+ opacity: 0.4;
+}
+.oo-ui-draggableGroupElement-horizontal .oo-ui-draggableElement.oo-ui-optionWidget {
+ display: inline-block;
+}
+.oo-ui-draggableGroupElement-placeholder {
+ position: absolute;
+ display: block;
+ background: rgba(0, 0, 0, 0.4);
+}
+.oo-ui-lookupElement > .oo-ui-menuSelectWidget {
+ z-index: 1;
+ width: 100%;
+}
+.oo-ui-bookletLayout-stackLayout.oo-ui-stackLayout-continuous > .oo-ui-panelLayout-scrollable {
+ overflow-y: hidden;
+}
+.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout {
+ width: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout-scrollable {
+ overflow-y: auto;
+}
+.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout-padded {
+ padding: 2em;
+}
+.oo-ui-bookletLayout-outlinePanel-editable > .oo-ui-outlineSelectWidget {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 3em;
+ overflow-y: auto;
+}
+.oo-ui-bookletLayout-outlinePanel > .oo-ui-outlineControlsWidget {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+}
+.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout {
+ padding: 1.5em;
+}
+.oo-ui-bookletLayout-outlinePanel {
+ border-right: 1px solid #dddddd;
+}
+.oo-ui-bookletLayout-outlinePanel > .oo-ui-outlineControlsWidget {
+ box-shadow: 0 0 0.25em rgba(0, 0, 0, 0.25);
+}
+.oo-ui-indexLayout > .oo-ui-menuLayout-menu {
+ height: 3em;
+}
+.oo-ui-indexLayout > .oo-ui-menuLayout-content {
+ top: 3em;
+}
+.oo-ui-indexLayout-stackLayout > .oo-ui-panelLayout {
+ padding: 1.5em;
+}
+.oo-ui-menuLayout {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+.oo-ui-menuLayout-menu,
+.oo-ui-menuLayout-content {
+ position: absolute;
+ -webkit-transition: all 200ms ease;
+ -moz-transition: all 200ms ease;
+ transition: all 200ms ease;
+}
+.oo-ui-menuLayout-menu {
+ height: 18em;
+ width: 18em;
+}
+.oo-ui-menuLayout-content {
+ top: 18em;
+ left: 18em;
+ right: 18em;
+ bottom: 18em;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-hideMenu > .oo-ui-menuLayout-menu {
+ width: 0 !important;
+ height: 0 !important;
+ overflow: hidden;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-hideMenu > .oo-ui-menuLayout-content {
+ top: 0 !important;
+ left: 0 !important;
+ right: 0 !important;
+ bottom: 0 !important;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-top > .oo-ui-menuLayout-menu {
+ width: auto !important;
+ left: 0;
+ top: 0;
+ right: 0;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-top > .oo-ui-menuLayout-content {
+ right: 0 !important;
+ bottom: 0 !important;
+ left: 0 !important;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-after > .oo-ui-menuLayout-menu {
+ height: auto !important;
+ top: 0;
+ right: 0;
+ bottom: 0;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-after > .oo-ui-menuLayout-content {
+ bottom: 0 !important;
+ left: 0 !important;
+ top: 0 !important;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-bottom > .oo-ui-menuLayout-menu {
+ width: auto !important;
+ right: 0;
+ bottom: 0;
+ left: 0;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-bottom > .oo-ui-menuLayout-content {
+ left: 0 !important;
+ top: 0 !important;
+ right: 0 !important;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-before > .oo-ui-menuLayout-menu {
+ height: auto !important;
+ bottom: 0;
+ left: 0;
+ top: 0;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-before > .oo-ui-menuLayout-content {
+ top: 0 !important;
+ right: 0 !important;
+ bottom: 0 !important;
+}
+.oo-ui-stackLayout-continuous > .oo-ui-panelLayout {
+ display: block;
+ position: relative;
+}
+.oo-ui-buttonSelectWidget {
+ display: inline-block;
+ white-space: nowrap;
+ border-radius: 0.3em;
+ margin-right: 0.5em;
+}
+.oo-ui-buttonSelectWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget .oo-ui-buttonElement-button {
+ border-radius: 0;
+ margin-left: -1px;
+}
+.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget:first-child .oo-ui-buttonElement-button {
+ border-bottom-left-radius: 0.3em;
+ border-top-left-radius: 0.3em;
+ margin-left: 0;
+}
+.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget:last-child .oo-ui-buttonElement-button {
+ border-bottom-right-radius: 0.3em;
+ border-top-right-radius: 0.3em;
+}
+.oo-ui-buttonOptionWidget {
+ display: inline-block;
+ padding: 0;
+ background-color: transparent;
+}
+.oo-ui-buttonOptionWidget .oo-ui-buttonElement-button {
+ position: relative;
+}
+.oo-ui-buttonOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
+.oo-ui-buttonOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+ position: static;
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-buttonOptionWidget .oo-ui-buttonElement-button {
+ height: 1.875em;
+}
+.oo-ui-buttonOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
+ margin-top: 0;
+}
+.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected,
+.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+.oo-ui-buttonOptionWidget.oo-ui-optionWidget-highlighted {
+ background-color: transparent;
+}
+.oo-ui-toggleButtonWidget {
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: 0.5em;
+}
+.oo-ui-toggleButtonWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-toggleSwitchWidget {
+ position: relative;
+ display: inline-block;
+ vertical-align: middle;
+ overflow: hidden;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ -webkit-transform: translateZ(0);
+ -moz-transform: translateZ(0);
+ -ms-transform: translateZ(0);
+ transform: translateZ(0);
+ height: 2em;
+ width: 4em;
+ border-radius: 1em;
+ box-shadow: 0 0 0 white, inset 0 0.1em 0.2em #dddddd;
+ border: 1px solid #cccccc;
+ margin-right: 0.5em;
+ background-color: #eeeeee;
+ background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #dddddd), color-stop(100%, #ffffff));
+ background-image: -webkit-linear-gradient(top, #dddddd 0, #ffffff 100%);
+ background-image: -moz-linear-gradient(top, #dddddd 0, #ffffff 100%);
+ background-image: linear-gradient(to bottom, #dddddd 0, #ffffff 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffdddddd', endColorstr='#ffffffff' )";
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled {
+ cursor: pointer;
+}
+.oo-ui-toggleSwitchWidget-grip {
+ position: absolute;
+ display: block;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-toggleSwitchWidget .oo-ui-toggleSwitchWidget-glow {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-glow {
+ display: none;
+}
+.oo-ui-toggleSwitchWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-disabled {
+ opacity: 0.5;
+}
+.oo-ui-toggleSwitchWidget-grip {
+ top: 0.25em;
+ left: 0.25em;
+ width: 1.5em;
+ height: 1.5em;
+ margin-top: -1px;
+ border-radius: 1em;
+ box-shadow: 0 0.1em 0.25em rgba(0, 0, 0, 0.1);
+ border: 1px #c9c9c9 solid;
+ -webkit-transition: left 250ms ease, margin-left 250ms ease;
+ -moz-transition: left 250ms ease, margin-left 250ms ease;
+ transition: left 250ms ease, margin-left 250ms ease;
+ background-color: #eeeeee;
+ background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #ffffff), color-stop(100%, #dddddd));
+ background-image: -webkit-linear-gradient(top, #ffffff 0, #dddddd 100%);
+ background-image: -moz-linear-gradient(top, #ffffff 0, #dddddd 100%);
+ background-image: linear-gradient(to bottom, #ffffff 0, #dddddd 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffffff', endColorstr='#ffdddddd' )";
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover,
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover .oo-ui-toggleSwitchWidget-grip {
+ border-color: #aaaaaa;
+}
+.oo-ui-toggleSwitchWidget .oo-ui-toggleSwitchWidget-glow {
+ border-radius: 1em;
+ box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.07);
+ -webkit-transition: opacity 250ms ease;
+ -moz-transition: opacity 250ms ease;
+ transition: opacity 250ms ease;
+ background-color: #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%);
+ background-image: -moz-linear-gradient(top, #b0d9ee 0, #eaf4fa 100%);
+ background-image: linear-gradient(to bottom, #b0d9ee 0, #eaf4fa 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffb0d9ee', endColorstr='#ffeaf4fa' )";
+}
+.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-glow {
+ opacity: 1;
+}
+.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-grip {
+ left: 2.25em;
+ margin-left: -2px;
+}
+.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-glow {
+ display: block;
+ opacity: 0;
+}
+.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-grip {
+ left: 0.25em;
+ margin-left: 0;
+}
+.oo-ui-progressBarWidget {
+ max-width: 50em;
+ background-color: #ffffff;
+ border: 1px solid #cccccc;
+ border-radius: 0.25em;
+ overflow: hidden;
+}
+.oo-ui-progressBarWidget-bar {
+ height: 1em;
+ border-right: 1px solid #cccccc;
+ -webkit-transition: width 250ms ease, margin-left 250ms ease;
+ -moz-transition: width 250ms ease, margin-left 250ms ease;
+ transition: width 250ms ease, margin-left 250ms ease;
+ background-color: #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%);
+ background-image: -moz-linear-gradient(top, #eaf4fa 0, #b0d9ee 100%);
+ background-image: linear-gradient(to bottom, #eaf4fa 0, #b0d9ee 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffeaf4fa', endColorstr='#ffb0d9ee' )";
+}
+.oo-ui-progressBarWidget-indeterminate .oo-ui-progressBarWidget-bar {
+ -webkit-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
+ -moz-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
+ animation: oo-ui-progressBarWidget-slide 2s infinite linear;
+ width: 40%;
+ margin-left: -10%;
+ border-left: 1px solid #a6cee1;
+}
+.oo-ui-progressBarWidget.oo-ui-widget-disabled {
+ opacity: 0.6;
+}
+.oo-ui-selectFileWidget {
+ display: inline-block;
+ vertical-align: middle;
+ width: 100%;
+ max-width: 50em;
+ margin-right: 0.5em;
+}
+.oo-ui-selectFileWidget-selectButton {
+ display: table-cell;
+ vertical-align: middle;
+}
+.oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button {
+ position: relative;
+ overflow: hidden;
+}
+.oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button > input[type="file"] {
+ position: absolute;
+ margin: 0;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ z-index: 1;
+ cursor: pointer;
+ padding-top: 100px;
+}
+.oo-ui-selectFileWidget-selectButton.oo-ui-widget-disabled > .oo-ui-buttonElement-button > input[type="file"] {
+ display: none;
+}
+.oo-ui-selectFileWidget-info {
+ width: 100%;
+ display: table-cell;
+ vertical-align: middle;
+ position: relative;
+ overflow: hidden;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ text-overflow: ellipsis;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileName {
+ float: left;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileType {
+ float: right;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator,
+.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
+ position: absolute;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
+ z-index: 2;
+}
+.oo-ui-selectFileWidget-dropTarget {
+ cursor: default;
+}
+.oo-ui-selectFileWidget-supported.oo-ui-widget-enabled .oo-ui-selectFileWidget-dropTarget {
+ cursor: pointer;
+}
+.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-clearButton,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-clearButton {
+ display: none;
+}
+.oo-ui-selectFileWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button {
+ margin-left: 0.5em;
+}
+.oo-ui-selectFileWidget-info {
+ height: 2.4em;
+ background-color: #ffffff;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ border-radius: 0.25em;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
+ right: 0;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon {
+ left: 0;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
+ line-height: 2.3em;
+ margin: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ text-overflow: ellipsis;
+ left: 0.5em;
+ right: 0.5em;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileType {
+ color: #888888;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
+ top: 0;
+ width: 1.875em;
+ margin-right: 0;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+ height: 2.3em;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
+ top: 0;
+ width: 0.9375em;
+ height: 2.3em;
+ margin-right: 0.775em;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon {
+ top: 0;
+ width: 1.875em;
+ height: 2.3em;
+ margin-left: 0.3em;
+}
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info {
+ color: #cccccc;
+ text-shadow: 0 1px 1px #ffffff;
+ border-color: #dddddd;
+ background-color: #f3f3f3;
+}
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+}
+.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-label {
+ color: #cccccc;
+}
+.oo-ui-selectFileWidget.oo-ui-iconElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+ left: 2.475em;
+}
+.oo-ui-selectFileWidget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+ right: 2.175em;
+}
+.oo-ui-selectFileWidget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-clearButton {
+ right: 0;
+}
+.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+ right: 4.2625em;
+}
+.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-clearButton {
+ right: 2.0875em;
+}
+.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+ right: 0.5em;
+}
+.oo-ui-selectFileWidget-empty.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label,
+.oo-ui-selectFileWidget-notsupported.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+ right: 2em;
+}
+.oo-ui-selectFileWidget-dropTarget {
+ line-height: 3.5em;
+ background-color: #ffffff;
+ border: 1px dashed #aaaaaa;
+ padding: 0.5em 1em;
+ margin-bottom: 0.5em;
+ text-align: center;
+ vertical-align: middle;
+}
+.oo-ui-selectFileWidget-supported.oo-ui-widget-enabled .oo-ui-selectFileWidget-dropTarget:hover,
+.oo-ui-selectFileWidget-supported.oo-ui-widget-enabled.oo-ui-selectFileWidget-canDrop oo-ui-selectfilewidget-droptarget {
+ background-color: #e1f3ff;
+}
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-dropTarget,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-dropTarget {
+ color: #cccccc;
+ text-shadow: 0 1px 1px #ffffff;
+ border-color: #dddddd;
+ background-color: #f3f3f3;
+}
+.oo-ui-outlineOptionWidget {
+ position: relative;
+ cursor: pointer;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ font-size: 1.1em;
+ padding: 0.75em;
+}
+.oo-ui-outlineOptionWidget.oo-ui-indicatorElement .oo-ui-labelElement-label {
+ padding-right: 1.5em;
+}
+.oo-ui-outlineOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+ opacity: 0.5;
+}
+.oo-ui-outlineOptionWidget-level-0 {
+ padding-left: 3.5em;
+}
+.oo-ui-outlineOptionWidget-level-0 .oo-ui-iconElement-icon {
+ left: 1em;
+}
+.oo-ui-outlineOptionWidget-level-1 {
+ padding-left: 5em;
+}
+.oo-ui-outlineOptionWidget-level-1 .oo-ui-iconElement-icon {
+ left: 2.5em;
+}
+.oo-ui-outlineOptionWidget-level-2 {
+ padding-left: 6.5em;
+}
+.oo-ui-outlineOptionWidget-level-2 .oo-ui-iconElement-icon {
+ left: 4em;
+}
+.oo-ui-selectWidget-depressed .oo-ui-outlineOptionWidget.oo-ui-optionWidget-selected {
+ background-color: #a7dcff;
+ text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
+}
+.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-important {
+ font-weight: bold;
+}
+.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-placeholder {
+ font-style: italic;
+}
+.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-empty .oo-ui-iconElement-icon {
+ opacity: 0.5;
+}
+.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-empty .oo-ui-labelElement-label {
+ color: #777777;
+}
+.oo-ui-outlineControlsWidget {
+ height: 3em;
+ background-color: #ffffff;
+}
+.oo-ui-outlineControlsWidget-items,
+.oo-ui-outlineControlsWidget-movers {
+ float: left;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-outlineControlsWidget > .oo-ui-iconElement-icon {
+ float: left;
+ background-position: right center;
+}
+.oo-ui-outlineControlsWidget-items {
+ float: left;
+}
+.oo-ui-outlineControlsWidget-items .oo-ui-buttonWidget {
+ float: left;
+}
+.oo-ui-outlineControlsWidget-movers {
+ float: right;
+}
+.oo-ui-outlineControlsWidget-movers .oo-ui-buttonWidget {
+ float: right;
+}
+.oo-ui-outlineControlsWidget-items,
+.oo-ui-outlineControlsWidget-movers {
+ height: 2em;
+ margin: 0.5em 0.5em 0.5em 0;
+ padding: 0;
+}
+.oo-ui-outlineControlsWidget > .oo-ui-iconElement-icon {
+ width: 1.5em;
+ height: 2em;
+ margin: 0.5em 0 0.5em 0.5em;
+ opacity: 0.2;
+}
+.oo-ui-tabSelectWidget {
+ text-align: left;
+ white-space: nowrap;
+ overflow: hidden;
+ background-color: #eeeeee;
+ box-shadow: inset 0 -0.015em 0.1em rgba(0, 0, 0, 0.1);
+}
+.oo-ui-tabOptionWidget {
+ display: inline-block;
+ vertical-align: bottom;
+ padding: 0.5em 1em;
+ margin: 0.5em 0 0 0.75em;
+ border: 1px solid transparent;
+ border-bottom: none;
+ border-top-left-radius: 0.5em;
+ border-top-right-radius: 0.5em;
+}
+.oo-ui-tabOptionWidget.oo-ui-indicatorElement .oo-ui-labelElement-label {
+ padding-right: 1.5em;
+}
+.oo-ui-tabOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+ opacity: 0.5;
+}
+.oo-ui-selectWidget-pressed .oo-ui-tabOptionWidget.oo-ui-optionWidget-pressed {
+ background-color: transparent;
+}
+.oo-ui-tabOptionWidget.oo-ui-widget-enabled:hover {
+ background-color: rgba(255, 255, 255, 0.2);
+ border-color: #dddddd;
+}
+.oo-ui-tabOptionWidget.oo-ui-widget-enabled:active {
+ background-color: #ffffff;
+ border-color: #dddddd;
+}
+.oo-ui-selectWidget-pressed .oo-ui-tabOptionWidget.oo-ui-optionWidget-selected,
+.oo-ui-selectWidget-depressed .oo-ui-tabOptionWidget.oo-ui-optionWidget-selected,
+.oo-ui-tabOptionWidget.oo-ui-optionWidget-selected:hover {
+ background-color: #ffffff;
+ border-color: #dddddd;
+}
+.oo-ui-capsuleMultiSelectWidget {
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ max-width: 50em;
+}
+.oo-ui-capsuleMultiSelectWidget-handle {
+ width: 100%;
+ display: inline-block;
+ position: relative;
+}
+.oo-ui-capsuleMultiSelectWidget-content {
+ position: relative;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-content > input {
+ display: none;
+}
+.oo-ui-capsuleMultiSelectWidget-group {
+ display: inline;
+}
+.oo-ui-capsuleMultiSelectWidget > .oo-ui-menuSelectWidget {
+ z-index: 1;
+ width: 100%;
+}
+.oo-ui-capsuleMultiSelectWidget-handle {
+ background-color: #ffffff;
+ cursor: text;
+ min-height: 2.4em;
+ margin-right: 0.5em;
+ padding: 0.15em 0.25em;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ border-radius: 0.25em;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-capsuleMultiSelectWidget-handle:last-child {
+ margin-right: 0;
+}
+.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator,
+.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon {
+ position: absolute;
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-capsuleMultiSelectWidget-content > input {
+ border: none;
+ line-height: 1.675em;
+ margin: 0;
+ margin-left: 0.2em;
+ padding: 0;
+ font-size: inherit;
+ font-family: inherit;
+ background-color: transparent;
+ color: black;
+ vertical-align: middle;
+}
+.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-capsuleMultiSelectWidget-content > input:focus {
+ outline: none;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-indicatorElement .oo-ui-capsuleMultiSelectWidget-handle {
+ padding-right: 2.4875em;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-indicatorElement .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator {
+ right: 0;
+ top: 0;
+ width: 0.9375em;
+ height: 0.9375em;
+ margin: 0.775em;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-iconElement .oo-ui-capsuleMultiSelectWidget-handle {
+ padding-left: 2.475em;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-iconElement .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon {
+ left: 0;
+ top: 0;
+ width: 1.875em;
+ height: 1.875em;
+ margin: 0.3em;
+}
+.oo-ui-capsuleMultiSelectWidget:hover .oo-ui-capsuleMultiSelectWidget-handle {
+ border-color: rgba(0, 0, 0, 0.2);
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-handle {
+ color: #cccccc;
+ text-shadow: 0 1px 1px #ffffff;
+ border-color: #dddddd;
+ background-color: #f3f3f3;
+ cursor: default;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon,
+.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+}
+.oo-ui-capsuleMultiSelectWidget .oo-ui-selectWidget {
+ border-top-color: #ffffff;
+}
+.oo-ui-capsuleItemWidget {
+ position: relative;
+ display: inline-block;
+ cursor: default;
+ white-space: nowrap;
+ width: auto;
+ max-width: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ vertical-align: middle;
+ padding: 0 0.4em;
+ margin: 0.1em;
+ height: 1.7em;
+ line-height: 1.7em;
+ background-color: #eeeeee;
+ background-image: -webkit-gradient(linear, right top, right bottom, color-stop(0, #ffffff), color-stop(100%, #dddddd));
+ background-image: -webkit-linear-gradient(top, #ffffff 0, #dddddd 100%);
+ background-image: -moz-linear-gradient(top, #ffffff 0, #dddddd 100%);
+ background-image: linear-gradient(to bottom, #ffffff 0, #dddddd 100%);
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffffff', endColorstr='#ffdddddd' )";
+ border: 1px solid #cccccc;
+ color: #555555;
+ border-radius: 0.25em;
+}
+.oo-ui-capsuleItemWidget > .oo-ui-iconElement-icon {
+ cursor: pointer;
+}
+.oo-ui-capsuleItemWidget.oo-ui-widget-disabled > .oo-ui-iconElement-icon {
+ cursor: default;
+}
+.oo-ui-capsuleItemWidget.oo-ui-labelElement .oo-ui-labelElement-label {
+ display: block;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+.oo-ui-capsuleItemWidget.oo-ui-indicatorElement > .oo-ui-labelElement-label {
+ padding-right: 1.3375em;
+}
+.oo-ui-capsuleItemWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
+ position: absolute;
+ right: 0.4em;
+ top: 0;
+ width: 0.9375em;
+ height: 100%;
+ background-repeat: no-repeat;
+}
+.oo-ui-capsuleItemWidget.oo-ui-indicatorElement > .oo-ui-indicator-clear {
+ cursor: pointer;
+}
+.oo-ui-capsuleItemWidget.oo-ui-widget-disabled {
+ opacity: 0.5;
+ -webkit-transform: translate3d(0, 0, 0);
+ box-shadow: none;
+ color: #333333;
+ background: #eeeeee;
+ border-color: #cccccc;
+}
+.oo-ui-capsuleItemWidget.oo-ui-widget-disabled > .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+}
+.oo-ui-searchWidget-query {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+}
+.oo-ui-searchWidget-query .oo-ui-textInputWidget {
+ width: 100%;
+}
+.oo-ui-searchWidget-results {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+.oo-ui-searchWidget-query {
+ height: 4em;
+ padding: 0 1em;
+ box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.2);
+}
+.oo-ui-searchWidget-query .oo-ui-textInputWidget {
+ margin: 0.75em 0;
+}
+.oo-ui-searchWidget-results {
+ top: 4em;
+ padding: 1em;
+ line-height: 0;
+}
+.oo-ui-numberInputWidget {
+ display: inline-block;
+ position: relative;
+ max-width: 50em;
+}
+.oo-ui-numberInputWidget-field {
+ display: table;
+ table-layout: fixed;
+ width: 100%;
+}
+.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget,
+.oo-ui-numberInputWidget-field > .oo-ui-textInputWidget {
+ display: table-cell;
+ vertical-align: middle;
+}
+.oo-ui-numberInputWidget-field > .oo-ui-textInputWidget {
+ width: 100%;
+}
+.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget {
+ white-space: nowrap;
+}
+.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget > .oo-ui-buttonElement-button {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget {
+ width: 2.25em;
+}
+.oo-ui-numberInputWidget-minusButton.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-right-width: 0;
+}
+.oo-ui-numberInputWidget-plusButton.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ border-left-width: 0;
+}
+.oo-ui-numberInputWidget .oo-ui-textInputWidget input {
+ border-radius: 0;
+}
--- /dev/null
+/*!
+ * OOjs UI v0.15.2
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: 2016-02-02T22:07:06Z
+ */
+@-webkit-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-moz-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+.oo-ui-draggableElement {
+ cursor: -webkit-grab -moz-grab, url(images/grab.cur), move;
+}
+.oo-ui-draggableElement-dragging {
+ cursor: -webkit-grabbing -moz-grabbing, url(images/grabbing.cur), move;
+ background: rgba(0, 0, 0, 0.2);
+ opacity: 0.4;
+}
+.oo-ui-draggableGroupElement-horizontal .oo-ui-draggableElement.oo-ui-optionWidget {
+ display: inline-block;
+}
+.oo-ui-draggableGroupElement-placeholder {
+ position: absolute;
+ display: block;
+ background: rgba(0, 0, 0, 0.4);
+}
+.oo-ui-lookupElement > .oo-ui-menuSelectWidget {
+ z-index: 1;
+ width: 100%;
+}
+.oo-ui-bookletLayout-stackLayout.oo-ui-stackLayout-continuous > .oo-ui-panelLayout-scrollable {
+ overflow-y: hidden;
+}
+.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout {
+ width: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout-scrollable {
+ overflow-y: auto;
+}
+.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout-padded {
+ padding: 2em;
+}
+.oo-ui-bookletLayout-outlinePanel-editable > .oo-ui-outlineSelectWidget {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 3em;
+ overflow-y: auto;
+}
+.oo-ui-bookletLayout-outlinePanel > .oo-ui-outlineControlsWidget {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+}
+.oo-ui-bookletLayout-stackLayout > .oo-ui-panelLayout {
+ padding: 1.5em;
+}
+.oo-ui-bookletLayout-outlinePanel {
+ border-right: 1px solid #dddddd;
+}
+.oo-ui-bookletLayout-outlinePanel > .oo-ui-outlineControlsWidget {
+ box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
+}
+.oo-ui-indexLayout > .oo-ui-menuLayout-menu {
+ height: 3em;
+}
+.oo-ui-indexLayout > .oo-ui-menuLayout-content {
+ top: 3em;
+}
+.oo-ui-indexLayout-stackLayout > .oo-ui-panelLayout {
+ padding: 1.5em;
+}
+.oo-ui-indexLayout > .oo-ui-menuLayout-menu {
+ height: 2.75em;
+}
+.oo-ui-indexLayout > .oo-ui-menuLayout-content {
+ top: 2.75em;
+}
+.oo-ui-menuLayout {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+.oo-ui-menuLayout-menu,
+.oo-ui-menuLayout-content {
+ position: absolute;
+ -webkit-transition: all 200ms ease;
+ -moz-transition: all 200ms ease;
+ transition: all 200ms ease;
+}
+.oo-ui-menuLayout-menu {
+ height: 18em;
+ width: 18em;
+}
+.oo-ui-menuLayout-content {
+ top: 18em;
+ left: 18em;
+ right: 18em;
+ bottom: 18em;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-hideMenu > .oo-ui-menuLayout-menu {
+ width: 0 !important;
+ height: 0 !important;
+ overflow: hidden;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-hideMenu > .oo-ui-menuLayout-content {
+ top: 0 !important;
+ left: 0 !important;
+ right: 0 !important;
+ bottom: 0 !important;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-top > .oo-ui-menuLayout-menu {
+ width: auto !important;
+ left: 0;
+ top: 0;
+ right: 0;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-top > .oo-ui-menuLayout-content {
+ right: 0 !important;
+ bottom: 0 !important;
+ left: 0 !important;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-after > .oo-ui-menuLayout-menu {
+ height: auto !important;
+ top: 0;
+ right: 0;
+ bottom: 0;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-after > .oo-ui-menuLayout-content {
+ bottom: 0 !important;
+ left: 0 !important;
+ top: 0 !important;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-bottom > .oo-ui-menuLayout-menu {
+ width: auto !important;
+ right: 0;
+ bottom: 0;
+ left: 0;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-bottom > .oo-ui-menuLayout-content {
+ left: 0 !important;
+ top: 0 !important;
+ right: 0 !important;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-before > .oo-ui-menuLayout-menu {
+ height: auto !important;
+ bottom: 0;
+ left: 0;
+ top: 0;
+}
+.oo-ui-menuLayout.oo-ui-menuLayout-showMenu.oo-ui-menuLayout-before > .oo-ui-menuLayout-content {
+ top: 0 !important;
+ right: 0 !important;
+ bottom: 0 !important;
+}
+.oo-ui-stackLayout-continuous > .oo-ui-panelLayout {
+ display: block;
+ position: relative;
+}
+.oo-ui-buttonSelectWidget {
+ display: inline-block;
+ white-space: nowrap;
+ border-radius: 2px;
+ margin-right: 0.5em;
+}
+.oo-ui-buttonSelectWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget .oo-ui-buttonElement-button {
+ border-radius: 0;
+ margin-left: -1px;
+}
+.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget:first-child .oo-ui-buttonElement-button {
+ border-bottom-left-radius: 2px;
+ border-top-left-radius: 2px;
+ margin-left: 0;
+}
+.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget:last-child .oo-ui-buttonElement-button {
+ border-bottom-right-radius: 2px;
+ border-top-right-radius: 2px;
+}
+.oo-ui-buttonOptionWidget {
+ display: inline-block;
+ padding: 0;
+ background-color: transparent;
+}
+.oo-ui-buttonOptionWidget .oo-ui-buttonElement-button {
+ position: relative;
+}
+.oo-ui-buttonOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
+.oo-ui-buttonOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+ position: static;
+ display: inline-block;
+ vertical-align: middle;
+}
+.oo-ui-buttonOptionWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
+ margin-top: 0;
+}
+.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected,
+.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+.oo-ui-buttonOptionWidget.oo-ui-optionWidget-highlighted {
+ background-color: transparent;
+}
+.oo-ui-buttonOptionWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
+.oo-ui-buttonOptionWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
+ opacity: 1;
+}
+.oo-ui-toggleButtonWidget {
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: 0.5em;
+}
+.oo-ui-toggleButtonWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-toggleSwitchWidget {
+ position: relative;
+ display: inline-block;
+ vertical-align: middle;
+ overflow: hidden;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ -webkit-transform: translateZ(0);
+ -moz-transform: translateZ(0);
+ -ms-transform: translateZ(0);
+ transform: translateZ(0);
+ height: 2em;
+ width: 3.5em;
+ border: 1px solid #777777;
+ border-radius: 1em;
+ background-color: #ffffff;
+ margin-right: 0.5em;
+ -webkit-transition: background-color 100ms ease, border-color 100ms ease;
+ -moz-transition: background-color 100ms ease, border-color 100ms ease;
+ transition: background-color 100ms ease, border-color 100ms ease;
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled {
+ cursor: pointer;
+}
+.oo-ui-toggleSwitchWidget-grip {
+ position: absolute;
+ display: block;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-toggleSwitchWidget .oo-ui-toggleSwitchWidget-glow {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-glow {
+ display: none;
+}
+.oo-ui-toggleSwitchWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-toggleSwitchWidget:before {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ border: 1px solid transparent;
+ border-radius: 1em;
+ z-index: 1;
+}
+.oo-ui-toggleSwitchWidget-grip {
+ top: 0.35em;
+ width: 1.2em;
+ height: 1.2em;
+ border-radius: 1.2em;
+ background-color: #555555;
+ -webkit-transition: left 100ms ease, margin-left 100ms ease;
+ -moz-transition: left 100ms ease, margin-left 100ms ease;
+ transition: left 100ms ease, margin-left 100ms ease;
+}
+.oo-ui-toggleSwitchWidget-glow {
+ display: none;
+}
+.oo-ui-toggleSwitchWidget.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-grip {
+ left: 1.9em;
+ margin-left: -2px;
+}
+.oo-ui-toggleSwitchWidget.oo-ui-toggleWidget-off .oo-ui-toggleSwitchWidget-grip {
+ left: 0.4em;
+ margin-left: 0;
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled.oo-ui-toggleWidget-on {
+ background-color: #347bff;
+ border-color: #347bff;
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled.oo-ui-toggleWidget-on .oo-ui-toggleSwitchWidget-grip {
+ background-color: #ffffff;
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover {
+ border-color: #2962cc;
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:hover.oo-ui-toggleWidget-on {
+ background-color: #2962cc;
+ border-color: #2962cc;
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:focus {
+ border-color: #347bff;
+ outline: none;
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:focus.oo-ui-toggleWidget-on {
+ border-color: #347bff;
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:focus.oo-ui-toggleWidget-on:before {
+ border-color: #ffffff;
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:active,
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:active:hover {
+ background-color: #347bff;
+ border-color: #347bff;
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:active .oo-ui-toggleSwitchWidget-grip,
+.oo-ui-toggleSwitchWidget.oo-ui-widget-enabled:active:hover .oo-ui-toggleSwitchWidget-grip {
+ background-color: #ffffff;
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-disabled {
+ background: #dddddd;
+ border-color: #dddddd;
+ outline: 0;
+}
+.oo-ui-toggleSwitchWidget.oo-ui-widget-disabled .oo-ui-toggleSwitchWidget-grip {
+ background: #ffffff;
+}
+.oo-ui-progressBarWidget {
+ max-width: 50em;
+ background-color: #ffffff;
+ border: 1px solid #cccccc;
+ border-radius: 2px;
+ overflow: hidden;
+}
+.oo-ui-progressBarWidget-bar {
+ height: 1em;
+ background: #dddddd;
+ -webkit-transition: width 200ms, margin-left 200ms;
+ -moz-transition: width 200ms, margin-left 200ms;
+ transition: width 200ms, margin-left 200ms;
+}
+.oo-ui-progressBarWidget-indeterminate .oo-ui-progressBarWidget-bar {
+ -webkit-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
+ -moz-animation: oo-ui-progressBarWidget-slide 2s infinite linear;
+ animation: oo-ui-progressBarWidget-slide 2s infinite linear;
+ width: 40%;
+ margin-left: -10%;
+ border-left-width: 1px;
+}
+.oo-ui-progressBarWidget.oo-ui-widget-disabled {
+ opacity: 0.6;
+}
+.oo-ui-selectFileWidget {
+ display: inline-block;
+ vertical-align: middle;
+ width: 100%;
+ max-width: 50em;
+ margin-right: 0.5em;
+}
+.oo-ui-selectFileWidget-selectButton {
+ display: table-cell;
+ vertical-align: middle;
+}
+.oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button {
+ position: relative;
+ overflow: hidden;
+}
+.oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button > input[type="file"] {
+ position: absolute;
+ margin: 0;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ z-index: 1;
+ cursor: pointer;
+ padding-top: 100px;
+}
+.oo-ui-selectFileWidget-selectButton.oo-ui-widget-disabled > .oo-ui-buttonElement-button > input[type="file"] {
+ display: none;
+}
+.oo-ui-selectFileWidget-info {
+ width: 100%;
+ display: table-cell;
+ vertical-align: middle;
+ position: relative;
+ overflow: hidden;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ text-overflow: ellipsis;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileName {
+ float: left;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileType {
+ float: right;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator,
+.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
+ position: absolute;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
+ z-index: 2;
+}
+.oo-ui-selectFileWidget-dropTarget {
+ cursor: default;
+}
+.oo-ui-selectFileWidget-supported.oo-ui-widget-enabled .oo-ui-selectFileWidget-dropTarget {
+ cursor: pointer;
+}
+.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-clearButton,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-clearButton {
+ display: none;
+}
+.oo-ui-selectFileWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-selectFileWidget-selectButton > .oo-ui-buttonElement-button {
+ margin-left: 0.5em;
+}
+.oo-ui-selectFileWidget-info {
+ height: 2.4em;
+ background-color: #ffffff;
+ border: 1px solid #cccccc;
+ border-radius: 2px;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
+ right: 0;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon {
+ left: 0;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label {
+ line-height: 2.3em;
+ margin: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ text-overflow: ellipsis;
+ left: 0.5em;
+ right: 0.5em;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-label > .oo-ui-selectFileWidget-fileType {
+ color: #888888;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton {
+ top: 0;
+ width: 1.875em;
+ margin-right: 0;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-selectFileWidget-clearButton .oo-ui-buttonElement-button > .oo-ui-iconElement-icon {
+ height: 2.3em;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
+ top: 0;
+ width: 0.9375em;
+ height: 2.3em;
+ margin-right: 0.775em;
+}
+.oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon {
+ top: 0;
+ width: 1.875em;
+ height: 2.3em;
+ margin-left: 0.5em;
+}
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info {
+ color: #cccccc;
+ text-shadow: 0 1px 1px #ffffff;
+ border-color: #dddddd;
+ background-color: #f3f3f3;
+}
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-iconElement-icon,
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-info > .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+}
+.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-label {
+ color: #cccccc;
+}
+.oo-ui-selectFileWidget.oo-ui-iconElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+ left: 2.875em;
+}
+.oo-ui-selectFileWidget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+ right: 2.375em;
+}
+.oo-ui-selectFileWidget .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-clearButton {
+ right: 0;
+}
+.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+ right: 4.4625em;
+}
+.oo-ui-selectFileWidget.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-clearButton {
+ right: 2.0875em;
+}
+.oo-ui-selectFileWidget-empty .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+ right: 0.5em;
+}
+.oo-ui-selectFileWidget-empty.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label,
+.oo-ui-selectFileWidget-notsupported.oo-ui-indicatorElement .oo-ui-selectFileWidget-info .oo-ui-selectFileWidget-label {
+ right: 2em;
+}
+.oo-ui-selectFileWidget-dropTarget {
+ line-height: 3.5em;
+ background-color: #ffffff;
+ border: 1px dashed #cccccc;
+ padding: 0.5em 1em;
+ margin-bottom: 0.5em;
+ text-align: center;
+ vertical-align: middle;
+}
+.oo-ui-selectFileWidget-supported.oo-ui-widget-enabled .oo-ui-selectFileWidget-dropTarget:hover {
+ background-color: #eeeeee;
+}
+.oo-ui-selectFileWidget-supported.oo-ui-widget-enabled.oo-ui-selectFileWidget-canDrop .oo-ui-selectFileWidget-dropTarget {
+ background: rgba(52, 123, 255, 0.1);
+}
+.oo-ui-selectFileWidget.oo-ui-widget-disabled .oo-ui-selectFileWidget-dropTarget,
+.oo-ui-selectFileWidget-notsupported .oo-ui-selectFileWidget-dropTarget {
+ color: #cccccc;
+ text-shadow: 0 1px 1px #ffffff;
+ border-color: #dddddd;
+ background-color: #f3f3f3;
+}
+.oo-ui-outlineOptionWidget {
+ position: relative;
+ cursor: pointer;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ font-size: 1.1em;
+ padding: 0.75em;
+}
+.oo-ui-outlineOptionWidget.oo-ui-indicatorElement .oo-ui-labelElement-label {
+ padding-right: 1.5em;
+}
+.oo-ui-outlineOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+ opacity: 0.5;
+}
+.oo-ui-outlineOptionWidget-level-0 {
+ padding-left: 3.5em;
+}
+.oo-ui-outlineOptionWidget-level-0 .oo-ui-iconElement-icon {
+ left: 1em;
+}
+.oo-ui-outlineOptionWidget-level-1 {
+ padding-left: 5em;
+}
+.oo-ui-outlineOptionWidget-level-1 .oo-ui-iconElement-icon {
+ left: 2.5em;
+}
+.oo-ui-outlineOptionWidget-level-2 {
+ padding-left: 6.5em;
+}
+.oo-ui-outlineOptionWidget-level-2 .oo-ui-iconElement-icon {
+ left: 4em;
+}
+.oo-ui-selectWidget-depressed .oo-ui-outlineOptionWidget.oo-ui-optionWidget-selected {
+ background-color: #d0d0d0;
+ text-shadow: 0 1px 1px #ffffff;
+}
+.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-important {
+ font-weight: bold;
+}
+.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-placeholder {
+ font-style: italic;
+}
+.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-empty .oo-ui-iconElement-icon {
+ opacity: 0.5;
+}
+.oo-ui-outlineOptionWidget.oo-ui-flaggedElement-empty .oo-ui-labelElement-label {
+ color: #777777;
+}
+.oo-ui-outlineControlsWidget {
+ height: 3em;
+ background-color: #ffffff;
+}
+.oo-ui-outlineControlsWidget-items,
+.oo-ui-outlineControlsWidget-movers {
+ float: left;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-outlineControlsWidget > .oo-ui-iconElement-icon {
+ float: left;
+ background-position: right center;
+}
+.oo-ui-outlineControlsWidget-items {
+ float: left;
+}
+.oo-ui-outlineControlsWidget-items .oo-ui-buttonWidget {
+ float: left;
+}
+.oo-ui-outlineControlsWidget-movers {
+ float: right;
+}
+.oo-ui-outlineControlsWidget-movers .oo-ui-buttonWidget {
+ float: right;
+}
+.oo-ui-outlineControlsWidget-items,
+.oo-ui-outlineControlsWidget-movers {
+ height: 2em;
+ margin: 0.5em 0.5em 0.5em 0;
+ padding: 0;
+}
+.oo-ui-outlineControlsWidget > .oo-ui-iconElement-icon {
+ width: 1.5em;
+ height: 2em;
+ margin: 0.5em 0 0.5em 0.5em;
+ opacity: 0.2;
+}
+.oo-ui-tabSelectWidget {
+ text-align: left;
+ white-space: nowrap;
+ overflow: hidden;
+ background-color: #dddddd;
+}
+.oo-ui-tabOptionWidget {
+ display: inline-block;
+ vertical-align: bottom;
+ padding: 0.35em 1em;
+ margin: 0.5em 0 0 0.75em;
+ border: 1px solid transparent;
+ border-bottom: none;
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+ color: #555555;
+ font-weight: bold;
+}
+.oo-ui-tabOptionWidget.oo-ui-widget-enabled:hover {
+ background-color: rgba(255, 255, 255, 0.3);
+}
+.oo-ui-tabOptionWidget.oo-ui-widget-enabled:active {
+ background-color: rgba(255, 255, 255, 0.8);
+}
+.oo-ui-tabOptionWidget.oo-ui-indicatorElement .oo-ui-labelElement-label {
+ padding-right: 1.5em;
+}
+.oo-ui-tabOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+ opacity: 0.5;
+}
+.oo-ui-selectWidget-pressed .oo-ui-tabOptionWidget.oo-ui-optionWidget-selected,
+.oo-ui-selectWidget-depressed .oo-ui-tabOptionWidget.oo-ui-optionWidget-selected,
+.oo-ui-tabOptionWidget.oo-ui-optionWidget-selected:hover {
+ background-color: #ffffff;
+ color: #333333;
+}
+.oo-ui-capsuleMultiSelectWidget {
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ max-width: 50em;
+}
+.oo-ui-capsuleMultiSelectWidget-handle {
+ width: 100%;
+ display: inline-block;
+ position: relative;
+}
+.oo-ui-capsuleMultiSelectWidget-content {
+ position: relative;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-content > input {
+ display: none;
+}
+.oo-ui-capsuleMultiSelectWidget-group {
+ display: inline;
+}
+.oo-ui-capsuleMultiSelectWidget > .oo-ui-menuSelectWidget {
+ z-index: 1;
+ width: 100%;
+}
+.oo-ui-capsuleMultiSelectWidget-handle {
+ background-color: #ffffff;
+ cursor: text;
+ min-height: 2.4em;
+ margin-right: 0.5em;
+ padding: 0.15em 0.25em;
+ border: 1px solid #cccccc;
+ border-radius: 2px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-capsuleMultiSelectWidget-handle:last-child {
+ margin-right: 0;
+}
+.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator,
+.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon {
+ position: absolute;
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-capsuleMultiSelectWidget-content > input {
+ border: 0;
+ line-height: 1.675em;
+ margin: 0 0 0 0.2em;
+ padding: 0;
+ font-size: inherit;
+ font-family: inherit;
+ background-color: transparent;
+ color: #000000;
+ vertical-align: middle;
+}
+.oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-capsuleMultiSelectWidget-content > input:focus {
+ outline: none;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-indicatorElement .oo-ui-capsuleMultiSelectWidget-handle {
+ padding-right: 2.4875em;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-indicatorElement .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator {
+ right: 0;
+ top: 0;
+ width: 0.9375em;
+ height: 0.9375em;
+ margin: 0.775em;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-iconElement .oo-ui-capsuleMultiSelectWidget-handle {
+ padding-left: 2.475em;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-iconElement .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon {
+ left: 0;
+ top: 0;
+ width: 1.875em;
+ height: 1.875em;
+ margin: 0.3em;
+}
+.oo-ui-capsuleMultiSelectWidget:hover .oo-ui-capsuleMultiSelectWidget-handle {
+ border-color: #aaaaaa;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-handle {
+ color: #cccccc;
+ text-shadow: 0 1px 1px #ffffff;
+ border-color: #dddddd;
+ background-color: #f3f3f3;
+ cursor: default;
+}
+.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-iconElement-icon,
+.oo-ui-capsuleMultiSelectWidget.oo-ui-widget-disabled .oo-ui-capsuleMultiSelectWidget-handle > .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+}
+.oo-ui-capsuleMultiSelectWidget .oo-ui-selectWidget {
+ border-top-color: #ffffff;
+}
+.oo-ui-capsuleItemWidget {
+ position: relative;
+ display: inline-block;
+ cursor: default;
+ white-space: nowrap;
+ width: auto;
+ max-width: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ vertical-align: middle;
+ padding: 0 0.4em;
+ margin: 0.1em;
+ height: 1.7em;
+ line-height: 1.7em;
+ background-color: #eeeeee;
+ border: 1px solid #cccccc;
+ color: #555555;
+ border-radius: 2px;
+}
+.oo-ui-capsuleItemWidget > .oo-ui-iconElement-icon {
+ cursor: pointer;
+}
+.oo-ui-capsuleItemWidget.oo-ui-widget-disabled > .oo-ui-iconElement-icon {
+ cursor: default;
+}
+.oo-ui-capsuleItemWidget.oo-ui-labelElement .oo-ui-labelElement-label {
+ display: block;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+.oo-ui-capsuleItemWidget.oo-ui-indicatorElement > .oo-ui-labelElement-label {
+ padding-right: 1.3375em;
+}
+.oo-ui-capsuleItemWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
+ position: absolute;
+ right: 0.4em;
+ top: 0;
+ width: 0.9375em;
+ height: 100%;
+ background-repeat: no-repeat;
+}
+.oo-ui-capsuleItemWidget.oo-ui-indicatorElement > .oo-ui-indicator-clear {
+ cursor: pointer;
+}
+.oo-ui-capsuleItemWidget.oo-ui-widget-disabled {
+ color: #cccccc;
+ text-shadow: 0 1px 1px #ffffff;
+ border-color: #dddddd;
+ background-color: #f3f3f3;
+}
+.oo-ui-capsuleItemWidget.oo-ui-widget-disabled > .oo-ui-indicatorElement-indicator {
+ opacity: 0.2;
+}
+.oo-ui-searchWidget-query {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+}
+.oo-ui-searchWidget-query .oo-ui-textInputWidget {
+ width: 100%;
+}
+.oo-ui-searchWidget-results {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+.oo-ui-searchWidget-query {
+ height: 4em;
+ padding: 0 1em;
+ border-bottom: 1px solid #cccccc;
+}
+.oo-ui-searchWidget-query .oo-ui-textInputWidget {
+ margin: 0.75em 0;
+}
+.oo-ui-searchWidget-results {
+ top: 4em;
+ padding: 1em;
+ line-height: 0;
+}
+.oo-ui-numberInputWidget {
+ display: inline-block;
+ position: relative;
+ max-width: 50em;
+}
+.oo-ui-numberInputWidget-field {
+ display: table;
+ table-layout: fixed;
+ width: 100%;
+}
+.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget,
+.oo-ui-numberInputWidget-field > .oo-ui-textInputWidget {
+ display: table-cell;
+ vertical-align: middle;
+}
+.oo-ui-numberInputWidget-field > .oo-ui-textInputWidget {
+ width: 100%;
+}
+.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget {
+ white-space: nowrap;
+}
+.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget > .oo-ui-buttonElement-button {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-numberInputWidget-field > .oo-ui-buttonWidget {
+ width: 2.5em;
+}
+.oo-ui-numberInputWidget-minusButton.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-right-width: 0;
+}
+.oo-ui-numberInputWidget-plusButton.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ border-left-width: 0;
+}
+.oo-ui-numberInputWidget .oo-ui-textInputWidget input {
+ border-radius: 0;
+}
--- /dev/null
+/*!
+ * OOjs UI v0.15.2
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: 2016-02-02T22:07:00Z
+ */
+( function ( OO ) {
+
+'use strict';
+
+/**
+ * DraggableElement is a mixin class used to create elements that can be clicked
+ * and dragged by a mouse to a new position within a group. This class must be used
+ * in conjunction with OO.ui.mixin.DraggableGroupElement, which provides a container for
+ * the draggable elements.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ */
+OO.ui.mixin.DraggableElement = function OoUiMixinDraggableElement() {
+ // Properties
+ this.index = null;
+
+ // Initialize and events
+ this.$element
+ .attr( 'draggable', true )
+ .addClass( 'oo-ui-draggableElement' )
+ .on( {
+ dragstart: this.onDragStart.bind( this ),
+ dragover: this.onDragOver.bind( this ),
+ dragend: this.onDragEnd.bind( this ),
+ drop: this.onDrop.bind( this )
+ } );
+};
+
+OO.initClass( OO.ui.mixin.DraggableElement );
+
+/* Events */
+
+/**
+ * @event dragstart
+ *
+ * A dragstart event is emitted when the user clicks and begins dragging an item.
+ * @param {OO.ui.mixin.DraggableElement} item The item the user has clicked and is dragging with the mouse.
+ */
+
+/**
+ * @event dragend
+ * A dragend event is emitted when the user drags an item and releases the mouse,
+ * thus terminating the drag operation.
+ */
+
+/**
+ * @event drop
+ * A drop event is emitted when the user drags an item and then releases the mouse button
+ * over a valid target.
+ */
+
+/* Static Properties */
+
+/**
+ * @inheritdoc OO.ui.mixin.ButtonElement
+ */
+OO.ui.mixin.DraggableElement.static.cancelButtonMouseDownEvents = false;
+
+/* Methods */
+
+/**
+ * Respond to dragstart event.
+ *
+ * @private
+ * @param {jQuery.Event} event jQuery event
+ * @fires dragstart
+ */
+OO.ui.mixin.DraggableElement.prototype.onDragStart = function ( e ) {
+ var dataTransfer = e.originalEvent.dataTransfer;
+ // Define drop effect
+ dataTransfer.dropEffect = 'none';
+ dataTransfer.effectAllowed = 'move';
+ // Support: Firefox
+ // We must set up a dataTransfer data property or Firefox seems to
+ // ignore the fact the element is draggable.
+ try {
+ dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
+ } catch ( err ) {
+ // The above is only for Firefox. Move on if it fails.
+ }
+ // Add dragging class
+ this.$element.addClass( 'oo-ui-draggableElement-dragging' );
+ // Emit event
+ this.emit( 'dragstart', this );
+ return true;
+};
+
+/**
+ * Respond to dragend event.
+ *
+ * @private
+ * @fires dragend
+ */
+OO.ui.mixin.DraggableElement.prototype.onDragEnd = function () {
+ this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
+ this.emit( 'dragend' );
+};
+
+/**
+ * Handle drop event.
+ *
+ * @private
+ * @param {jQuery.Event} event jQuery event
+ * @fires drop
+ */
+OO.ui.mixin.DraggableElement.prototype.onDrop = function ( e ) {
+ e.preventDefault();
+ this.emit( 'drop', e );
+};
+
+/**
+ * In order for drag/drop to work, the dragover event must
+ * return false and stop propogation.
+ *
+ * @private
+ */
+OO.ui.mixin.DraggableElement.prototype.onDragOver = function ( e ) {
+ e.preventDefault();
+};
+
+/**
+ * Set item index.
+ * Store it in the DOM so we can access from the widget drag event
+ *
+ * @private
+ * @param {number} Item index
+ */
+OO.ui.mixin.DraggableElement.prototype.setIndex = function ( index ) {
+ if ( this.index !== index ) {
+ this.index = index;
+ this.$element.data( 'index', index );
+ }
+};
+
+/**
+ * Get item index
+ *
+ * @private
+ * @return {number} Item index
+ */
+OO.ui.mixin.DraggableElement.prototype.getIndex = function () {
+ return this.index;
+};
+
+/**
+ * DraggableGroupElement is a mixin class used to create a group element to
+ * contain draggable elements, which are items that can be clicked and dragged by a mouse.
+ * The class is used with OO.ui.mixin.DraggableElement.
+ *
+ * @abstract
+ * @class
+ * @mixins OO.ui.mixin.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation
+ * should match the layout of the items. Items displayed in a single row
+ * or in several rows should use horizontal orientation. The vertical orientation should only be
+ * used when the items are displayed in a single column. Defaults to 'vertical'
+ */
+OO.ui.mixin.DraggableGroupElement = function OoUiMixinDraggableGroupElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.mixin.GroupElement.call( this, config );
+
+ // Properties
+ this.orientation = config.orientation || 'vertical';
+ this.dragItem = null;
+ this.itemDragOver = null;
+ this.itemKeys = {};
+ this.sideInsertion = '';
+
+ // Events
+ this.aggregate( {
+ dragstart: 'itemDragStart',
+ dragend: 'itemDragEnd',
+ drop: 'itemDrop'
+ } );
+ this.connect( this, {
+ itemDragStart: 'onItemDragStart',
+ itemDrop: 'onItemDrop',
+ itemDragEnd: 'onItemDragEnd'
+ } );
+ this.$element.on( {
+ dragover: this.onDragOver.bind( this ),
+ dragleave: this.onDragLeave.bind( this )
+ } );
+
+ // Initialize
+ if ( Array.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+ this.$placeholder = $( '<div>' )
+ .addClass( 'oo-ui-draggableGroupElement-placeholder' );
+ this.$element
+ .addClass( 'oo-ui-draggableGroupElement' )
+ .append( this.$status )
+ .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
+ .prepend( this.$placeholder );
+};
+
+/* Setup */
+OO.mixinClass( OO.ui.mixin.DraggableGroupElement, OO.ui.mixin.GroupElement );
+
+/* Events */
+
+/**
+ * A 'reorder' event is emitted when the order of items in the group changes.
+ *
+ * @event reorder
+ * @param {OO.ui.mixin.DraggableElement} item Reordered item
+ * @param {number} [newIndex] New index for the item
+ */
+
+/* Methods */
+
+/**
+ * Respond to item drag start event
+ *
+ * @private
+ * @param {OO.ui.mixin.DraggableElement} item Dragged item
+ */
+OO.ui.mixin.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
+ var i, len;
+
+ // Map the index of each object
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ this.items[ i ].setIndex( i );
+ }
+
+ if ( this.orientation === 'horizontal' ) {
+ // Set the height of the indicator
+ this.$placeholder.css( {
+ height: item.$element.outerHeight(),
+ width: 2
+ } );
+ } else {
+ // Set the width of the indicator
+ this.$placeholder.css( {
+ height: 2,
+ width: item.$element.outerWidth()
+ } );
+ }
+ this.setDragItem( item );
+};
+
+/**
+ * Respond to item drag end event
+ *
+ * @private
+ */
+OO.ui.mixin.DraggableGroupElement.prototype.onItemDragEnd = function () {
+ this.unsetDragItem();
+ return false;
+};
+
+/**
+ * Handle drop event and switch the order of the items accordingly
+ *
+ * @private
+ * @param {OO.ui.mixin.DraggableElement} item Dropped item
+ * @fires reorder
+ */
+OO.ui.mixin.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
+ var toIndex = item.getIndex();
+ // Check if the dropped item is from the current group
+ // TODO: Figure out a way to configure a list of legally droppable
+ // elements even if they are not yet in the list
+ if ( this.getDragItem() ) {
+ // If the insertion point is 'after', the insertion index
+ // is shifted to the right (or to the left in RTL, hence 'after')
+ if ( this.sideInsertion === 'after' ) {
+ toIndex++;
+ }
+ // Emit change event
+ this.emit( 'reorder', this.getDragItem(), toIndex );
+ }
+ this.unsetDragItem();
+ // Return false to prevent propogation
+ return false;
+};
+
+/**
+ * Handle dragleave event.
+ *
+ * @private
+ */
+OO.ui.mixin.DraggableGroupElement.prototype.onDragLeave = function () {
+ // This means the item was dragged outside the widget
+ this.$placeholder
+ .css( 'left', 0 )
+ .addClass( 'oo-ui-element-hidden' );
+};
+
+/**
+ * Respond to dragover event
+ *
+ * @private
+ * @param {jQuery.Event} event Event details
+ */
+OO.ui.mixin.DraggableGroupElement.prototype.onDragOver = function ( e ) {
+ var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
+ itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
+ clientX = e.originalEvent.clientX,
+ clientY = e.originalEvent.clientY;
+
+ // Get the OptionWidget item we are dragging over
+ dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
+ $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
+ if ( $optionWidget[ 0 ] ) {
+ itemOffset = $optionWidget.offset();
+ itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
+ itemPosition = $optionWidget.position();
+ itemIndex = $optionWidget.data( 'index' );
+ }
+
+ if (
+ itemOffset &&
+ this.isDragging() &&
+ itemIndex !== this.getDragItem().getIndex()
+ ) {
+ if ( this.orientation === 'horizontal' ) {
+ // Calculate where the mouse is relative to the item width
+ itemSize = itemBoundingRect.width;
+ itemMidpoint = itemBoundingRect.left + itemSize / 2;
+ dragPosition = clientX;
+ // Which side of the item we hover over will dictate
+ // where the placeholder will appear, on the left or
+ // on the right
+ cssOutput = {
+ left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
+ top: itemPosition.top
+ };
+ } else {
+ // Calculate where the mouse is relative to the item height
+ itemSize = itemBoundingRect.height;
+ itemMidpoint = itemBoundingRect.top + itemSize / 2;
+ dragPosition = clientY;
+ // Which side of the item we hover over will dictate
+ // where the placeholder will appear, on the top or
+ // on the bottom
+ cssOutput = {
+ top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
+ left: itemPosition.left
+ };
+ }
+ // Store whether we are before or after an item to rearrange
+ // For horizontal layout, we need to account for RTL, as this is flipped
+ if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
+ this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
+ } else {
+ this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
+ }
+ // Add drop indicator between objects
+ this.$placeholder
+ .css( cssOutput )
+ .removeClass( 'oo-ui-element-hidden' );
+ } else {
+ // This means the item was dragged outside the widget
+ this.$placeholder
+ .css( 'left', 0 )
+ .addClass( 'oo-ui-element-hidden' );
+ }
+ // Prevent default
+ e.preventDefault();
+};
+
+/**
+ * Set a dragged item
+ *
+ * @param {OO.ui.mixin.DraggableElement} item Dragged item
+ */
+OO.ui.mixin.DraggableGroupElement.prototype.setDragItem = function ( item ) {
+ this.dragItem = item;
+};
+
+/**
+ * Unset the current dragged item
+ */
+OO.ui.mixin.DraggableGroupElement.prototype.unsetDragItem = function () {
+ this.dragItem = null;
+ this.itemDragOver = null;
+ this.$placeholder.addClass( 'oo-ui-element-hidden' );
+ this.sideInsertion = '';
+};
+
+/**
+ * Get the item that is currently being dragged.
+ *
+ * @return {OO.ui.mixin.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged
+ */
+OO.ui.mixin.DraggableGroupElement.prototype.getDragItem = function () {
+ return this.dragItem;
+};
+
+/**
+ * Check if an item in the group is currently being dragged.
+ *
+ * @return {Boolean} Item is being dragged
+ */
+OO.ui.mixin.DraggableGroupElement.prototype.isDragging = function () {
+ return this.getDragItem() !== null;
+};
+
+/**
+ * RequestManager is a mixin that manages the lifecycle of a promise-backed request for a widget, such as
+ * the {@link OO.ui.mixin.LookupElement}.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ */
+OO.ui.mixin.RequestManager = function OoUiMixinRequestManager() {
+ this.requestCache = {};
+ this.requestQuery = null;
+ this.requestRequest = null;
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.mixin.RequestManager );
+
+/**
+ * Get request results for the current query.
+ *
+ * @return {jQuery.Promise} Promise object which will be passed response data as the first argument of
+ * the done event. If the request was aborted to make way for a subsequent request, this promise
+ * may not be rejected, depending on what jQuery feels like doing.
+ */
+OO.ui.mixin.RequestManager.prototype.getRequestData = function () {
+ var widget = this,
+ value = this.getRequestQuery(),
+ deferred = $.Deferred(),
+ ourRequest;
+
+ this.abortRequest();
+ if ( Object.prototype.hasOwnProperty.call( this.requestCache, value ) ) {
+ deferred.resolve( this.requestCache[ value ] );
+ } else {
+ if ( this.pushPending ) {
+ this.pushPending();
+ }
+ this.requestQuery = value;
+ ourRequest = this.requestRequest = this.getRequest();
+ ourRequest
+ .always( function () {
+ // We need to pop pending even if this is an old request, otherwise
+ // the widget will remain pending forever.
+ // TODO: this assumes that an aborted request will fail or succeed soon after
+ // being aborted, or at least eventually. It would be nice if we could popPending()
+ // at abort time, but only if we knew that we hadn't already called popPending()
+ // for that request.
+ if ( widget.popPending ) {
+ widget.popPending();
+ }
+ } )
+ .done( function ( response ) {
+ // If this is an old request (and aborting it somehow caused it to still succeed),
+ // ignore its success completely
+ if ( ourRequest === widget.requestRequest ) {
+ widget.requestQuery = null;
+ widget.requestRequest = null;
+ widget.requestCache[ value ] = widget.getRequestCacheDataFromResponse( response );
+ deferred.resolve( widget.requestCache[ value ] );
+ }
+ } )
+ .fail( function () {
+ // If this is an old request (or a request failing because it's being aborted),
+ // ignore its failure completely
+ if ( ourRequest === widget.requestRequest ) {
+ widget.requestQuery = null;
+ widget.requestRequest = null;
+ deferred.reject();
+ }
+ } );
+ }
+ return deferred.promise();
+};
+
+/**
+ * Abort the currently pending request, if any.
+ *
+ * @private
+ */
+OO.ui.mixin.RequestManager.prototype.abortRequest = function () {
+ var oldRequest = this.requestRequest;
+ if ( oldRequest ) {
+ // First unset this.requestRequest to the fail handler will notice
+ // that the request is no longer current
+ this.requestRequest = null;
+ this.requestQuery = null;
+ oldRequest.abort();
+ }
+};
+
+/**
+ * Get the query to be made.
+ *
+ * @protected
+ * @method
+ * @abstract
+ * @return {string} query to be used
+ */
+OO.ui.mixin.RequestManager.prototype.getRequestQuery = null;
+
+/**
+ * Get a new request object of the current query value.
+ *
+ * @protected
+ * @method
+ * @abstract
+ * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
+ */
+OO.ui.mixin.RequestManager.prototype.getRequest = null;
+
+/**
+ * Pre-process data returned by the request from #getRequest.
+ *
+ * The return value of this function will be cached, and any further queries for the given value
+ * will use the cache rather than doing API requests.
+ *
+ * @protected
+ * @method
+ * @abstract
+ * @param {Mixed} response Response from server
+ * @return {Mixed} Cached result data
+ */
+OO.ui.mixin.RequestManager.prototype.getRequestCacheDataFromResponse = null;
+
+/**
+ * LookupElement is a mixin that creates a {@link OO.ui.FloatingMenuSelectWidget menu} of suggested values for
+ * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
+ * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
+ * from the lookup menu, that value becomes the value of the input field.
+ *
+ * Note that a new menu of suggested items is displayed when a value is chosen from the lookup menu. If this is
+ * not the desired behavior, disable lookup menus with the #setLookupsDisabled method, then set the value, then
+ * re-enable lookups.
+ *
+ * See the [OOjs UI demos][1] for an example.
+ *
+ * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/index.html#widgets-apex-vector-ltr
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
+ * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
+ * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
+ * By default, the lookup menu is not generated and displayed until the user begins to type.
+ * @cfg {boolean} [highlightFirst=true] Whether the first lookup result should be highlighted (so, that the user can
+ * take it over into the input with simply pressing return) automatically or not.
+ */
+OO.ui.mixin.LookupElement = function OoUiMixinLookupElement( config ) {
+ // Configuration initialization
+ config = $.extend( { highlightFirst: true }, config );
+
+ // Mixin constructors
+ OO.ui.mixin.RequestManager.call( this, config );
+
+ // Properties
+ this.$overlay = config.$overlay || this.$element;
+ this.lookupMenu = new OO.ui.FloatingMenuSelectWidget( {
+ widget: this,
+ input: this,
+ $container: config.$container || this.$element
+ } );
+
+ this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
+
+ this.lookupsDisabled = false;
+ this.lookupInputFocused = false;
+ this.lookupHighlightFirstItem = config.highlightFirst;
+
+ // Events
+ this.$input.on( {
+ focus: this.onLookupInputFocus.bind( this ),
+ blur: this.onLookupInputBlur.bind( this ),
+ mousedown: this.onLookupInputMouseDown.bind( this )
+ } );
+ this.connect( this, { change: 'onLookupInputChange' } );
+ this.lookupMenu.connect( this, {
+ toggle: 'onLookupMenuToggle',
+ choose: 'onLookupMenuItemChoose'
+ } );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-lookupElement' );
+ this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
+ this.$overlay.append( this.lookupMenu.$element );
+};
+
+/* Setup */
+
+OO.mixinClass( OO.ui.mixin.LookupElement, OO.ui.mixin.RequestManager );
+
+/* Methods */
+
+/**
+ * Handle input focus event.
+ *
+ * @protected
+ * @param {jQuery.Event} e Input focus event
+ */
+OO.ui.mixin.LookupElement.prototype.onLookupInputFocus = function () {
+ this.lookupInputFocused = true;
+ this.populateLookupMenu();
+};
+
+/**
+ * Handle input blur event.
+ *
+ * @protected
+ * @param {jQuery.Event} e Input blur event
+ */
+OO.ui.mixin.LookupElement.prototype.onLookupInputBlur = function () {
+ this.closeLookupMenu();
+ this.lookupInputFocused = false;
+};
+
+/**
+ * Handle input mouse down event.
+ *
+ * @protected
+ * @param {jQuery.Event} e Input mouse down event
+ */
+OO.ui.mixin.LookupElement.prototype.onLookupInputMouseDown = function () {
+ // Only open the menu if the input was already focused.
+ // This way we allow the user to open the menu again after closing it with Esc
+ // by clicking in the input. Opening (and populating) the menu when initially
+ // clicking into the input is handled by the focus handler.
+ if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
+ this.populateLookupMenu();
+ }
+};
+
+/**
+ * Handle input change event.
+ *
+ * @protected
+ * @param {string} value New input value
+ */
+OO.ui.mixin.LookupElement.prototype.onLookupInputChange = function () {
+ if ( this.lookupInputFocused ) {
+ this.populateLookupMenu();
+ }
+};
+
+/**
+ * Handle the lookup menu being shown/hidden.
+ *
+ * @protected
+ * @param {boolean} visible Whether the lookup menu is now visible.
+ */
+OO.ui.mixin.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
+ if ( !visible ) {
+ // When the menu is hidden, abort any active request and clear the menu.
+ // This has to be done here in addition to closeLookupMenu(), because
+ // MenuSelectWidget will close itself when the user presses Esc.
+ this.abortLookupRequest();
+ this.lookupMenu.clearItems();
+ }
+};
+
+/**
+ * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
+ *
+ * @protected
+ * @param {OO.ui.MenuOptionWidget} item Selected item
+ */
+OO.ui.mixin.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
+ this.setValue( item.getData() );
+};
+
+/**
+ * Get lookup menu.
+ *
+ * @private
+ * @return {OO.ui.FloatingMenuSelectWidget}
+ */
+OO.ui.mixin.LookupElement.prototype.getLookupMenu = function () {
+ return this.lookupMenu;
+};
+
+/**
+ * Disable or re-enable lookups.
+ *
+ * When lookups are disabled, calls to #populateLookupMenu will be ignored.
+ *
+ * @param {boolean} disabled Disable lookups
+ */
+OO.ui.mixin.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
+ this.lookupsDisabled = !!disabled;
+};
+
+/**
+ * Open the menu. If there are no entries in the menu, this does nothing.
+ *
+ * @private
+ * @chainable
+ */
+OO.ui.mixin.LookupElement.prototype.openLookupMenu = function () {
+ if ( !this.lookupMenu.isEmpty() ) {
+ this.lookupMenu.toggle( true );
+ }
+ return this;
+};
+
+/**
+ * Close the menu, empty it, and abort any pending request.
+ *
+ * @private
+ * @chainable
+ */
+OO.ui.mixin.LookupElement.prototype.closeLookupMenu = function () {
+ this.lookupMenu.toggle( false );
+ this.abortLookupRequest();
+ this.lookupMenu.clearItems();
+ return this;
+};
+
+/**
+ * Request menu items based on the input's current value, and when they arrive,
+ * populate the menu with these items and show the menu.
+ *
+ * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
+ *
+ * @private
+ * @chainable
+ */
+OO.ui.mixin.LookupElement.prototype.populateLookupMenu = function () {
+ var widget = this,
+ value = this.getValue();
+
+ if ( this.lookupsDisabled || this.isReadOnly() ) {
+ return;
+ }
+
+ // If the input is empty, clear the menu, unless suggestions when empty are allowed.
+ if ( !this.allowSuggestionsWhenEmpty && value === '' ) {
+ this.closeLookupMenu();
+ // Skip population if there is already a request pending for the current value
+ } else if ( value !== this.lookupQuery ) {
+ this.getLookupMenuItems()
+ .done( function ( items ) {
+ widget.lookupMenu.clearItems();
+ if ( items.length ) {
+ widget.lookupMenu
+ .addItems( items )
+ .toggle( true );
+ widget.initializeLookupMenuSelection();
+ } else {
+ widget.lookupMenu.toggle( false );
+ }
+ } )
+ .fail( function () {
+ widget.lookupMenu.clearItems();
+ } );
+ }
+
+ return this;
+};
+
+/**
+ * Highlight the first selectable item in the menu, if configured.
+ *
+ * @private
+ * @chainable
+ */
+OO.ui.mixin.LookupElement.prototype.initializeLookupMenuSelection = function () {
+ if ( this.lookupHighlightFirstItem && !this.lookupMenu.getSelectedItem() ) {
+ this.lookupMenu.highlightItem( this.lookupMenu.getFirstSelectableItem() );
+ }
+};
+
+/**
+ * Get lookup menu items for the current query.
+ *
+ * @private
+ * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
+ * the done event. If the request was aborted to make way for a subsequent request, this promise
+ * will not be rejected: it will remain pending forever.
+ */
+OO.ui.mixin.LookupElement.prototype.getLookupMenuItems = function () {
+ return this.getRequestData().then( function ( data ) {
+ return this.getLookupMenuOptionsFromData( data );
+ }.bind( this ) );
+};
+
+/**
+ * Abort the currently pending lookup request, if any.
+ *
+ * @private
+ */
+OO.ui.mixin.LookupElement.prototype.abortLookupRequest = function () {
+ this.abortRequest();
+};
+
+/**
+ * Get a new request object of the current lookup query value.
+ *
+ * @protected
+ * @method
+ * @abstract
+ * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
+ */
+OO.ui.mixin.LookupElement.prototype.getLookupRequest = null;
+
+/**
+ * Pre-process data returned by the request from #getLookupRequest.
+ *
+ * The return value of this function will be cached, and any further queries for the given value
+ * will use the cache rather than doing API requests.
+ *
+ * @protected
+ * @method
+ * @abstract
+ * @param {Mixed} response Response from server
+ * @return {Mixed} Cached result data
+ */
+OO.ui.mixin.LookupElement.prototype.getLookupCacheDataFromResponse = null;
+
+/**
+ * Get a list of menu option widgets from the (possibly cached) data returned by
+ * #getLookupCacheDataFromResponse.
+ *
+ * @protected
+ * @method
+ * @abstract
+ * @param {Mixed} data Cached result data, usually an array
+ * @return {OO.ui.MenuOptionWidget[]} Menu items
+ */
+OO.ui.mixin.LookupElement.prototype.getLookupMenuOptionsFromData = null;
+
+/**
+ * Set the read-only state of the widget.
+ *
+ * This will also disable/enable the lookups functionality.
+ *
+ * @param {boolean} readOnly Make input read-only
+ * @chainable
+ */
+OO.ui.mixin.LookupElement.prototype.setReadOnly = function ( readOnly ) {
+ // Parent method
+ // Note: Calling #setReadOnly this way assumes this is mixed into an OO.ui.TextInputWidget
+ OO.ui.TextInputWidget.prototype.setReadOnly.call( this, readOnly );
+
+ // During construction, #setReadOnly is called before the OO.ui.mixin.LookupElement constructor
+ if ( this.isReadOnly() && this.lookupMenu ) {
+ this.closeLookupMenu();
+ }
+
+ return this;
+};
+
+/**
+ * @inheritdoc OO.ui.mixin.RequestManager
+ */
+OO.ui.mixin.LookupElement.prototype.getRequestQuery = function () {
+ return this.getValue();
+};
+
+/**
+ * @inheritdoc OO.ui.mixin.RequestManager
+ */
+OO.ui.mixin.LookupElement.prototype.getRequest = function () {
+ return this.getLookupRequest();
+};
+
+/**
+ * @inheritdoc OO.ui.mixin.RequestManager
+ */
+OO.ui.mixin.LookupElement.prototype.getRequestCacheDataFromResponse = function ( response ) {
+ return this.getLookupCacheDataFromResponse( response );
+};
+
+/**
+ * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
+ * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
+ * rather extended to include the required content and functionality.
+ *
+ * Each card must have a unique symbolic name, which is passed to the constructor. In addition, the card's tab
+ * item is customized (with a label) using the #setupTabItem method. See
+ * {@link OO.ui.IndexLayout IndexLayout} for an example.
+ *
+ * @class
+ * @extends OO.ui.PanelLayout
+ *
+ * @constructor
+ * @param {string} name Unique symbolic name of card
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] Label for card's tab
+ */
+OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( name ) && config === undefined ) {
+ config = name;
+ name = config.name;
+ }
+
+ // Configuration initialization
+ config = $.extend( { scrollable: true }, config );
+
+ // Parent constructor
+ OO.ui.CardLayout.parent.call( this, config );
+
+ // Properties
+ this.name = name;
+ this.label = config.label;
+ this.tabItem = null;
+ this.active = false;
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-cardLayout' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.CardLayout, OO.ui.PanelLayout );
+
+/* Events */
+
+/**
+ * An 'active' event is emitted when the card becomes active. Cards become active when they are
+ * shown in a index layout that is configured to display only one card at a time.
+ *
+ * @event active
+ * @param {boolean} active Card is active
+ */
+
+/* Methods */
+
+/**
+ * Get the symbolic name of the card.
+ *
+ * @return {string} Symbolic name of card
+ */
+OO.ui.CardLayout.prototype.getName = function () {
+ return this.name;
+};
+
+/**
+ * Check if card is active.
+ *
+ * Cards become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to display
+ * only one card at a time. Additional CSS is applied to the card's tab item to reflect the active state.
+ *
+ * @return {boolean} Card is active
+ */
+OO.ui.CardLayout.prototype.isActive = function () {
+ return this.active;
+};
+
+/**
+ * Get tab item.
+ *
+ * The tab item allows users to access the card from the index's tab
+ * navigation. The tab item itself can be customized (with a label, level, etc.) using the #setupTabItem method.
+ *
+ * @return {OO.ui.TabOptionWidget|null} Tab option widget
+ */
+OO.ui.CardLayout.prototype.getTabItem = function () {
+ return this.tabItem;
+};
+
+/**
+ * Set or unset the tab item.
+ *
+ * Specify a {@link OO.ui.TabOptionWidget tab option} to set it,
+ * or `null` to clear the tab item. To customize the tab item itself (e.g., to set a label or tab
+ * level), use #setupTabItem instead of this method.
+ *
+ * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
+ * @chainable
+ */
+OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) {
+ this.tabItem = tabItem || null;
+ if ( tabItem ) {
+ this.setupTabItem();
+ }
+ return this;
+};
+
+/**
+ * Set up the tab item.
+ *
+ * Use this method to customize the tab item (e.g., to add a label or tab level). To set or unset
+ * the tab item itself (with a {@link OO.ui.TabOptionWidget tab option} or `null`), use
+ * the #setTabItem method instead.
+ *
+ * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
+ * @chainable
+ */
+OO.ui.CardLayout.prototype.setupTabItem = function () {
+ if ( this.label ) {
+ this.tabItem.setLabel( this.label );
+ }
+ return this;
+};
+
+/**
+ * Set the card to its 'active' state.
+ *
+ * Cards become active when they are shown in a index layout that is configured to display only one card at a time. Additional
+ * CSS is applied to the tab item to reflect the card's active state. Outside of the index
+ * context, setting the active state on a card does nothing.
+ *
+ * @param {boolean} value Card is active
+ * @fires active
+ */
+OO.ui.CardLayout.prototype.setActive = function ( active ) {
+ active = !!active;
+
+ if ( active !== this.active ) {
+ this.active = active;
+ this.$element.toggleClass( 'oo-ui-cardLayout-active', this.active );
+ this.emit( 'active', this.active );
+ }
+};
+
+/**
+ * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
+ * from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
+ * rather extended to include the required content and functionality.
+ *
+ * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the page's outline
+ * item is customized (with a label, outline level, etc.) using the #setupOutlineItem method. See
+ * {@link OO.ui.BookletLayout BookletLayout} for an example.
+ *
+ * @class
+ * @extends OO.ui.PanelLayout
+ *
+ * @constructor
+ * @param {string} name Unique symbolic name of page
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( name ) && config === undefined ) {
+ config = name;
+ name = config.name;
+ }
+
+ // Configuration initialization
+ config = $.extend( { scrollable: true }, config );
+
+ // Parent constructor
+ OO.ui.PageLayout.parent.call( this, config );
+
+ // Properties
+ this.name = name;
+ this.outlineItem = null;
+ this.active = false;
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-pageLayout' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
+
+/* Events */
+
+/**
+ * An 'active' event is emitted when the page becomes active. Pages become active when they are
+ * shown in a booklet layout that is configured to display only one page at a time.
+ *
+ * @event active
+ * @param {boolean} active Page is active
+ */
+
+/* Methods */
+
+/**
+ * Get the symbolic name of the page.
+ *
+ * @return {string} Symbolic name of page
+ */
+OO.ui.PageLayout.prototype.getName = function () {
+ return this.name;
+};
+
+/**
+ * Check if page is active.
+ *
+ * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is configured to display
+ * only one page at a time. Additional CSS is applied to the page's outline item to reflect the active state.
+ *
+ * @return {boolean} Page is active
+ */
+OO.ui.PageLayout.prototype.isActive = function () {
+ return this.active;
+};
+
+/**
+ * Get outline item.
+ *
+ * The outline item allows users to access the page from the booklet's outline
+ * navigation. The outline item itself can be customized (with a label, level, etc.) using the #setupOutlineItem method.
+ *
+ * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
+ */
+OO.ui.PageLayout.prototype.getOutlineItem = function () {
+ return this.outlineItem;
+};
+
+/**
+ * Set or unset the outline item.
+ *
+ * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
+ * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label or outline
+ * level), use #setupOutlineItem instead of this method.
+ *
+ * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
+ * @chainable
+ */
+OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
+ this.outlineItem = outlineItem || null;
+ if ( outlineItem ) {
+ this.setupOutlineItem();
+ }
+ return this;
+};
+
+/**
+ * Set up the outline item.
+ *
+ * Use this method to customize the outline item (e.g., to add a label or outline level). To set or unset
+ * the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or `null`), use
+ * the #setOutlineItem method instead.
+ *
+ * @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
+ * @chainable
+ */
+OO.ui.PageLayout.prototype.setupOutlineItem = function () {
+ return this;
+};
+
+/**
+ * Set the page to its 'active' state.
+ *
+ * Pages become active when they are shown in a booklet layout that is configured to display only one page at a time. Additional
+ * CSS is applied to the outline item to reflect the page's active state. Outside of the booklet
+ * context, setting the active state on a page does nothing.
+ *
+ * @param {boolean} value Page is active
+ * @fires active
+ */
+OO.ui.PageLayout.prototype.setActive = function ( active ) {
+ active = !!active;
+
+ if ( active !== this.active ) {
+ this.active = active;
+ this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
+ this.emit( 'active', this.active );
+ }
+};
+
+/**
+ * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one panel is displayed
+ * at a time, though the stack layout can also be configured to show all contained panels, one after another,
+ * by setting the #continuous option to 'true'.
+ *
+ * @example
+ * // A stack layout with two panels, configured to be displayed continously
+ * var myStack = new OO.ui.StackLayout( {
+ * items: [
+ * new OO.ui.PanelLayout( {
+ * $content: $( '<p>Panel One</p>' ),
+ * padded: true,
+ * framed: true
+ * } ),
+ * new OO.ui.PanelLayout( {
+ * $content: $( '<p>Panel Two</p>' ),
+ * padded: true,
+ * framed: true
+ * } )
+ * ],
+ * continuous: true
+ * } );
+ * $( 'body' ).append( myStack.$element );
+ *
+ * @class
+ * @extends OO.ui.PanelLayout
+ * @mixins OO.ui.mixin.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel is displayed at a time.
+ * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
+ */
+OO.ui.StackLayout = function OoUiStackLayout( config ) {
+ // Configuration initialization
+ config = $.extend( { scrollable: true }, config );
+
+ // Parent constructor
+ OO.ui.StackLayout.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
+
+ // Properties
+ this.currentItem = null;
+ this.continuous = !!config.continuous;
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-stackLayout' );
+ if ( this.continuous ) {
+ this.$element.addClass( 'oo-ui-stackLayout-continuous' );
+ this.$element.on( 'scroll', OO.ui.debounce( this.onScroll.bind( this ), 250 ) );
+ }
+ if ( Array.isArray( config.items ) ) {
+ this.addItems( config.items );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
+OO.mixinClass( OO.ui.StackLayout, OO.ui.mixin.GroupElement );
+
+/* Events */
+
+/**
+ * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
+ * {@link #clearItems cleared} or {@link #setItem displayed}.
+ *
+ * @event set
+ * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
+ */
+
+/**
+ * When used in continuous mode, this event is emitted when the user scrolls down
+ * far enough such that currentItem is no longer visible.
+ *
+ * @event visibleItemChange
+ * @param {OO.ui.PanelLayout} panel The next visible item in the layout
+ */
+
+/* Methods */
+
+/**
+ * Handle scroll events from the layout element
+ *
+ * @param {jQuery.Event} e
+ * @fires visibleItemChange
+ */
+OO.ui.StackLayout.prototype.onScroll = function () {
+ var currentRect,
+ len = this.items.length,
+ currentIndex = this.items.indexOf( this.currentItem ),
+ newIndex = currentIndex,
+ containerRect = this.$element[ 0 ].getBoundingClientRect();
+
+ if ( !containerRect || ( !containerRect.top && !containerRect.bottom ) ) {
+ // Can't get bounding rect, possibly not attached.
+ return;
+ }
+
+ function getRect( item ) {
+ return item.$element[ 0 ].getBoundingClientRect();
+ }
+
+ function isVisible( item ) {
+ var rect = getRect( item );
+ return rect.bottom > containerRect.top && rect.top < containerRect.bottom;
+ }
+
+ currentRect = getRect( this.currentItem );
+
+ if ( currentRect.bottom < containerRect.top ) {
+ // Scrolled down past current item
+ while ( ++newIndex < len ) {
+ if ( isVisible( this.items[ newIndex ] ) ) {
+ break;
+ }
+ }
+ } else if ( currentRect.top > containerRect.bottom ) {
+ // Scrolled up past current item
+ while ( --newIndex >= 0 ) {
+ if ( isVisible( this.items[ newIndex ] ) ) {
+ break;
+ }
+ }
+ }
+
+ if ( newIndex !== currentIndex ) {
+ this.emit( 'visibleItemChange', this.items[ newIndex ] );
+ }
+};
+
+/**
+ * Get the current panel.
+ *
+ * @return {OO.ui.Layout|null}
+ */
+OO.ui.StackLayout.prototype.getCurrentItem = function () {
+ return this.currentItem;
+};
+
+/**
+ * Unset the current item.
+ *
+ * @private
+ * @param {OO.ui.StackLayout} layout
+ * @fires set
+ */
+OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
+ var prevItem = this.currentItem;
+ if ( prevItem === null ) {
+ return;
+ }
+
+ this.currentItem = null;
+ this.emit( 'set', null );
+};
+
+/**
+ * Add panel layouts to the stack layout.
+ *
+ * Panels will be added to the end of the stack layout array unless the optional index parameter specifies a different
+ * insertion point. Adding a panel that is already in the stack will move it to the end of the array or the point specified
+ * by the index.
+ *
+ * @param {OO.ui.Layout[]} items Panels to add
+ * @param {number} [index] Index of the insertion point
+ * @chainable
+ */
+OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
+ // Update the visibility
+ this.updateHiddenState( items, this.currentItem );
+
+ // Mixin method
+ OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
+
+ if ( !this.currentItem && items.length ) {
+ this.setItem( items[ 0 ] );
+ }
+
+ return this;
+};
+
+/**
+ * Remove the specified panels from the stack layout.
+ *
+ * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all panels,
+ * you may wish to use the #clearItems method instead.
+ *
+ * @param {OO.ui.Layout[]} items Panels to remove
+ * @chainable
+ * @fires set
+ */
+OO.ui.StackLayout.prototype.removeItems = function ( items ) {
+ // Mixin method
+ OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
+
+ if ( items.indexOf( this.currentItem ) !== -1 ) {
+ if ( this.items.length ) {
+ this.setItem( this.items[ 0 ] );
+ } else {
+ this.unsetCurrentItem();
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Clear all panels from the stack layout.
+ *
+ * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
+ * a subset of panels, use the #removeItems method.
+ *
+ * @chainable
+ * @fires set
+ */
+OO.ui.StackLayout.prototype.clearItems = function () {
+ this.unsetCurrentItem();
+ OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
+
+ return this;
+};
+
+/**
+ * Show the specified panel.
+ *
+ * If another panel is currently displayed, it will be hidden.
+ *
+ * @param {OO.ui.Layout} item Panel to show
+ * @chainable
+ * @fires set
+ */
+OO.ui.StackLayout.prototype.setItem = function ( item ) {
+ if ( item !== this.currentItem ) {
+ this.updateHiddenState( this.items, item );
+
+ if ( this.items.indexOf( item ) !== -1 ) {
+ this.currentItem = item;
+ this.emit( 'set', item );
+ } else {
+ this.unsetCurrentItem();
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Update the visibility of all items in case of non-continuous view.
+ *
+ * Ensure all items are hidden except for the selected one.
+ * This method does nothing when the stack is continuous.
+ *
+ * @private
+ * @param {OO.ui.Layout[]} items Item list iterate over
+ * @param {OO.ui.Layout} [selectedItem] Selected item to show
+ */
+OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
+ var i, len;
+
+ if ( !this.continuous ) {
+ for ( i = 0, len = items.length; i < len; i++ ) {
+ if ( !selectedItem || selectedItem !== items[ i ] ) {
+ items[ i ].$element.addClass( 'oo-ui-element-hidden' );
+ }
+ }
+ if ( selectedItem ) {
+ selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
+ }
+ }
+};
+
+/**
+ * MenuLayouts combine a menu and a content {@link OO.ui.PanelLayout panel}. The menu is positioned relative to the content (after, before, top, or bottom)
+ * and its size is customized with the #menuSize config. The content area will fill all remaining space.
+ *
+ * @example
+ * var menuLayout = new OO.ui.MenuLayout( {
+ * position: 'top'
+ * } ),
+ * menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
+ * contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
+ * select = new OO.ui.SelectWidget( {
+ * items: [
+ * new OO.ui.OptionWidget( {
+ * data: 'before',
+ * label: 'Before',
+ * } ),
+ * new OO.ui.OptionWidget( {
+ * data: 'after',
+ * label: 'After',
+ * } ),
+ * new OO.ui.OptionWidget( {
+ * data: 'top',
+ * label: 'Top',
+ * } ),
+ * new OO.ui.OptionWidget( {
+ * data: 'bottom',
+ * label: 'Bottom',
+ * } )
+ * ]
+ * } ).on( 'select', function ( item ) {
+ * menuLayout.setMenuPosition( item.getData() );
+ * } );
+ *
+ * menuLayout.$menu.append(
+ * menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
+ * );
+ * menuLayout.$content.append(
+ * contentPanel.$element.append( '<b>Content panel</b>', '<p>Note that the menu is positioned relative to the content panel: top, bottom, after, before.</p>')
+ * );
+ * $( 'body' ).append( menuLayout.$element );
+ *
+ * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
+ * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
+ * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
+ * may be omitted.
+ *
+ * .oo-ui-menuLayout-menu {
+ * height: 200px;
+ * width: 200px;
+ * }
+ * .oo-ui-menuLayout-content {
+ * top: 200px;
+ * left: 200px;
+ * right: 200px;
+ * bottom: 200px;
+ * }
+ *
+ * @class
+ * @extends OO.ui.Layout
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [showMenu=true] Show menu
+ * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
+ */
+OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
+ // Configuration initialization
+ config = $.extend( {
+ showMenu: true,
+ menuPosition: 'before'
+ }, config );
+
+ // Parent constructor
+ OO.ui.MenuLayout.parent.call( this, config );
+
+ /**
+ * Menu DOM node
+ *
+ * @property {jQuery}
+ */
+ this.$menu = $( '<div>' );
+ /**
+ * Content DOM node
+ *
+ * @property {jQuery}
+ */
+ this.$content = $( '<div>' );
+
+ // Initialization
+ this.$menu
+ .addClass( 'oo-ui-menuLayout-menu' );
+ this.$content.addClass( 'oo-ui-menuLayout-content' );
+ this.$element
+ .addClass( 'oo-ui-menuLayout' )
+ .append( this.$content, this.$menu );
+ this.setMenuPosition( config.menuPosition );
+ this.toggleMenu( config.showMenu );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
+
+/* Methods */
+
+/**
+ * Toggle menu.
+ *
+ * @param {boolean} showMenu Show menu, omit to toggle
+ * @chainable
+ */
+OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
+ showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
+
+ if ( this.showMenu !== showMenu ) {
+ this.showMenu = showMenu;
+ this.$element
+ .toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu )
+ .toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu );
+ }
+
+ return this;
+};
+
+/**
+ * Check if menu is visible
+ *
+ * @return {boolean} Menu is visible
+ */
+OO.ui.MenuLayout.prototype.isMenuVisible = function () {
+ return this.showMenu;
+};
+
+/**
+ * Set menu position.
+ *
+ * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
+ * @throws {Error} If position value is not supported
+ * @chainable
+ */
+OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
+ this.$element.removeClass( 'oo-ui-menuLayout-' + this.menuPosition );
+ this.menuPosition = position;
+ this.$element.addClass( 'oo-ui-menuLayout-' + position );
+
+ return this;
+};
+
+/**
+ * Get menu position.
+ *
+ * @return {string} Menu position
+ */
+OO.ui.MenuLayout.prototype.getMenuPosition = function () {
+ return this.menuPosition;
+};
+
+/**
+ * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
+ * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
+ * through the pages and select which one to display. By default, only one page is
+ * displayed at a time and the outline is hidden. When a user navigates to a new page,
+ * the booklet layout automatically focuses on the first focusable element, unless the
+ * default setting is changed. Optionally, booklets can be configured to show
+ * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
+ *
+ * @example
+ * // Example of a BookletLayout that contains two PageLayouts.
+ *
+ * function PageOneLayout( name, config ) {
+ * PageOneLayout.parent.call( this, name, config );
+ * this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on the left)</p>' );
+ * }
+ * OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
+ * PageOneLayout.prototype.setupOutlineItem = function () {
+ * this.outlineItem.setLabel( 'Page One' );
+ * };
+ *
+ * function PageTwoLayout( name, config ) {
+ * PageTwoLayout.parent.call( this, name, config );
+ * this.$element.append( '<p>Second page</p>' );
+ * }
+ * OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
+ * PageTwoLayout.prototype.setupOutlineItem = function () {
+ * this.outlineItem.setLabel( 'Page Two' );
+ * };
+ *
+ * var page1 = new PageOneLayout( 'one' ),
+ * page2 = new PageTwoLayout( 'two' );
+ *
+ * var booklet = new OO.ui.BookletLayout( {
+ * outlined: true
+ * } );
+ *
+ * booklet.addPages ( [ page1, page2 ] );
+ * $( 'body' ).append( booklet.$element );
+ *
+ * @class
+ * @extends OO.ui.MenuLayout
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [continuous=false] Show all pages, one after another
+ * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is displayed.
+ * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the pages of the booklet.
+ * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
+ */
+OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.BookletLayout.parent.call( this, config );
+
+ // Properties
+ this.currentPageName = null;
+ this.pages = {};
+ this.ignoreFocus = false;
+ this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
+ this.$content.append( this.stackLayout.$element );
+ this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
+ this.outlineVisible = false;
+ this.outlined = !!config.outlined;
+ if ( this.outlined ) {
+ this.editable = !!config.editable;
+ this.outlineControlsWidget = null;
+ this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
+ this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
+ this.$menu.append( this.outlinePanel.$element );
+ this.outlineVisible = true;
+ if ( this.editable ) {
+ this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
+ this.outlineSelectWidget
+ );
+ }
+ }
+ this.toggleMenu( this.outlined );
+
+ // Events
+ this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
+ if ( this.outlined ) {
+ this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
+ this.scrolling = false;
+ this.stackLayout.connect( this, { visibleItemChange: 'onStackLayoutVisibleItemChange' } );
+ }
+ if ( this.autoFocus ) {
+ // Event 'focus' does not bubble, but 'focusin' does
+ this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
+ }
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-bookletLayout' );
+ this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
+ if ( this.outlined ) {
+ this.outlinePanel.$element
+ .addClass( 'oo-ui-bookletLayout-outlinePanel' )
+ .append( this.outlineSelectWidget.$element );
+ if ( this.editable ) {
+ this.outlinePanel.$element
+ .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
+ .append( this.outlineControlsWidget.$element );
+ }
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
+
+/* Events */
+
+/**
+ * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the booklet layout.
+ * @event set
+ * @param {OO.ui.PageLayout} page Current page
+ */
+
+/**
+ * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
+ *
+ * @event add
+ * @param {OO.ui.PageLayout[]} page Added pages
+ * @param {number} index Index pages were added at
+ */
+
+/**
+ * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
+ * {@link #removePages removed} from the booklet.
+ *
+ * @event remove
+ * @param {OO.ui.PageLayout[]} pages Removed pages
+ */
+
+/* Methods */
+
+/**
+ * Handle stack layout focus.
+ *
+ * @private
+ * @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 visibleItemChange events from the stackLayout
+ *
+ * The next visible page is set as the current page by selecting it
+ * in the outline
+ *
+ * @param {OO.ui.PageLayout} page The next visible page in the layout
+ */
+OO.ui.BookletLayout.prototype.onStackLayoutVisibleItemChange = function ( page ) {
+ // Set a flag to so that the resulting call to #onStackLayoutSet doesn't
+ // try and scroll the item into view again.
+ this.scrolling = true;
+ this.outlineSelectWidget.selectItemByData( page.getName() );
+ this.scrolling = false;
+};
+
+/**
+ * Handle stack layout set events.
+ *
+ * @private
+ * @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 ( !this.scrolling && page ) {
+ page.scrollElementIntoView( { complete: function () {
+ if ( layout.autoFocus ) {
+ layout.focus();
+ }
+ } } );
+ }
+};
+
+/**
+ * Focus the first input in the current page.
+ *
+ * If no page is selected, the first selectable page will be selected.
+ * If the focus is already in an element on the current page, nothing will happen.
+ * @param {number} [itemIndex] A specific item to focus on
+ */
+OO.ui.BookletLayout.prototype.focus = function ( itemIndex ) {
+ var page,
+ items = this.stackLayout.getItems();
+
+ if ( itemIndex !== undefined && items[ itemIndex ] ) {
+ page = items[ itemIndex ];
+ } else {
+ page = this.stackLayout.getCurrentItem();
+ }
+
+ if ( !page && this.outlined ) {
+ this.selectFirstSelectablePage();
+ page = this.stackLayout.getCurrentItem();
+ }
+ if ( !page ) {
+ return;
+ }
+ // Only change the focus if is not already in the current page
+ if ( !OO.ui.contains( page.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
+ page.focus();
+ }
+};
+
+/**
+ * Find the first focusable input in the booklet layout and focus
+ * on it.
+ */
+OO.ui.BookletLayout.prototype.focusFirstFocusable = function () {
+ OO.ui.findFocusable( this.stackLayout.$element ).focus();
+};
+
+/**
+ * Handle outline widget select events.
+ *
+ * @private
+ * @param {OO.ui.OptionWidget|null} item Selected item
+ */
+OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
+ if ( item ) {
+ this.setPage( item.getData() );
+ }
+};
+
+/**
+ * Check if booklet has an outline.
+ *
+ * @return {boolean} Booklet has an outline
+ */
+OO.ui.BookletLayout.prototype.isOutlined = function () {
+ return this.outlined;
+};
+
+/**
+ * Check if booklet has editing controls.
+ *
+ * @return {boolean} Booklet is editable
+ */
+OO.ui.BookletLayout.prototype.isEditable = function () {
+ return this.editable;
+};
+
+/**
+ * Check if booklet has a visible outline.
+ *
+ * @return {boolean} Outline is visible
+ */
+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.toggleMenu( show );
+ }
+
+ return this;
+};
+
+/**
+ * Get the page closest to the specified page.
+ *
+ * @param {OO.ui.PageLayout} page Page to use as a reference point
+ * @return {OO.ui.PageLayout|null} Page closest to the specified page
+ */
+OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
+ var next, prev, level,
+ pages = this.stackLayout.getItems(),
+ index = pages.indexOf( page );
+
+ if ( index !== -1 ) {
+ next = pages[ index + 1 ];
+ prev = pages[ index - 1 ];
+ // Prefer adjacent pages at the same level
+ if ( this.outlined ) {
+ level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
+ if (
+ prev &&
+ level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
+ ) {
+ return prev;
+ }
+ if (
+ next &&
+ level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
+ ) {
+ return next;
+ }
+ }
+ }
+ return prev || next || null;
+};
+
+/**
+ * Get the outline widget.
+ *
+ * If the booklet is not outlined, the method will return `null`.
+ *
+ * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
+ */
+OO.ui.BookletLayout.prototype.getOutline = function () {
+ return this.outlineSelectWidget;
+};
+
+/**
+ * Get the outline controls widget.
+ *
+ * If the outline is not editable, the method will return `null`.
+ *
+ * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
+ */
+OO.ui.BookletLayout.prototype.getOutlineControls = function () {
+ return this.outlineControlsWidget;
+};
+
+/**
+ * Get a page by its symbolic 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.
+ *
+ * @return {OO.ui.PageLayout|undefined} Current page, if found
+ */
+OO.ui.BookletLayout.prototype.getCurrentPage = function () {
+ var name = this.getCurrentPageName();
+ return name ? this.getPage( name ) : undefined;
+};
+
+/**
+ * Get the symbolic name of the current page.
+ *
+ * @return {string|null} Symbolic name of the current page
+ */
+OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
+ return this.currentPageName;
+};
+
+/**
+ * Add pages to the booklet 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 of the insertion point
+ * @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 = stackLayoutPages.indexOf( this.pages[ name ] );
+ 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.OutlineOptionWidget( { data: name } );
+ page.setOutlineItem( item );
+ items.push( item );
+ }
+ }
+
+ if ( this.outlined && items.length ) {
+ this.outlineSelectWidget.addItems( items, index );
+ this.selectFirstSelectablePage();
+ }
+ this.stackLayout.addItems( pages, index );
+ this.emit( 'add', pages, index );
+
+ return this;
+};
+
+/**
+ * Remove the specified pages from the booklet layout.
+ *
+ * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
+ *
+ * @param {OO.ui.PageLayout[]} pages An array of pages to remove
+ * @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.outlineSelectWidget.getItemFromData( name ) );
+ page.setOutlineItem( null );
+ }
+ }
+ if ( this.outlined && items.length ) {
+ this.outlineSelectWidget.removeItems( items );
+ this.selectFirstSelectablePage();
+ }
+ this.stackLayout.removeItems( pages );
+ this.emit( 'remove', pages );
+
+ return this;
+};
+
+/**
+ * Clear all pages from the booklet layout.
+ *
+ * To remove only a subset of pages from the booklet, use the #removePages method.
+ *
+ * @fires remove
+ * @chainable
+ */
+OO.ui.BookletLayout.prototype.clearPages = function () {
+ var i, len,
+ pages = this.stackLayout.getItems();
+
+ this.pages = {};
+ this.currentPageName = null;
+ if ( this.outlined ) {
+ this.outlineSelectWidget.clearItems();
+ for ( i = 0, len = pages.length; i < len; i++ ) {
+ pages[ i ].setOutlineItem( null );
+ }
+ }
+ this.stackLayout.clearItems();
+
+ this.emit( 'remove', pages );
+
+ return this;
+};
+
+/**
+ * Set the current page by symbolic name.
+ *
+ * @fires set
+ * @param {string} name Symbolic name of page
+ */
+OO.ui.BookletLayout.prototype.setPage = function ( name ) {
+ var selectedItem,
+ $focused,
+ page = this.pages[ name ],
+ previousPage = this.currentPageName && this.pages[ this.currentPageName ];
+
+ if ( name !== this.currentPageName ) {
+ if ( this.outlined ) {
+ selectedItem = this.outlineSelectWidget.getSelectedItem();
+ if ( selectedItem && selectedItem.getData() !== name ) {
+ this.outlineSelectWidget.selectItemByData( name );
+ }
+ }
+ if ( page ) {
+ if ( previousPage ) {
+ previousPage.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 the layout is non-continuous, this check is
+ // meaningless because the next page is not visible yet and thus can't hold focus.
+ if (
+ this.autoFocus &&
+ this.stackLayout.continuous &&
+ OO.ui.findFocusable( page.$element ).length !== 0
+ ) {
+ $focused = previousPage.$element.find( ':focus' );
+ if ( $focused.length ) {
+ $focused[ 0 ].blur();
+ }
+ }
+ }
+ this.currentPageName = name;
+ page.setActive( true );
+ this.stackLayout.setItem( page );
+ if ( !this.stackLayout.continuous && previousPage ) {
+ // This should not be necessary, since any inputs on the previous page should have been
+ // blurred when it was hidden, but browsers are not very consistent about this.
+ $focused = previousPage.$element.find( ':focus' );
+ if ( $focused.length ) {
+ $focused[ 0 ].blur();
+ }
+ }
+ this.emit( 'set', page );
+ }
+ }
+};
+
+/**
+ * Select the first selectable page.
+ *
+ * @chainable
+ */
+OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
+ if ( !this.outlineSelectWidget.getSelectedItem() ) {
+ this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
+ }
+
+ return this;
+};
+
+/**
+ * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
+ * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the cards and
+ * select which one to display. By default, only one card is displayed at a time. When a user
+ * navigates to a new card, the index layout automatically focuses on the first focusable element,
+ * unless the default setting is changed.
+ *
+ * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
+ *
+ * @example
+ * // Example of a IndexLayout that contains two CardLayouts.
+ *
+ * function CardOneLayout( name, config ) {
+ * CardOneLayout.parent.call( this, name, config );
+ * this.$element.append( '<p>First card</p>' );
+ * }
+ * OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
+ * CardOneLayout.prototype.setupTabItem = function () {
+ * this.tabItem.setLabel( 'Card one' );
+ * };
+ *
+ * var card1 = new CardOneLayout( 'one' ),
+ * card2 = new CardLayout( 'two', { label: 'Card two' } );
+ *
+ * card2.$element.append( '<p>Second card</p>' );
+ *
+ * var index = new OO.ui.IndexLayout();
+ *
+ * index.addCards ( [ card1, card2 ] );
+ * $( 'body' ).append( index.$element );
+ *
+ * @class
+ * @extends OO.ui.MenuLayout
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [continuous=false] Show all cards, one after another
+ * @cfg {boolean} [expanded=true] Expand the content panel to fill the entire parent element.
+ * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed.
+ */
+OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
+ // Configuration initialization
+ config = $.extend( {}, config, { menuPosition: 'top' } );
+
+ // Parent constructor
+ OO.ui.IndexLayout.parent.call( this, config );
+
+ // Properties
+ this.currentCardName = null;
+ this.cards = {};
+ this.ignoreFocus = false;
+ this.stackLayout = new OO.ui.StackLayout( {
+ continuous: !!config.continuous,
+ expanded: config.expanded
+ } );
+ this.$content.append( this.stackLayout.$element );
+ this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
+
+ this.tabSelectWidget = new OO.ui.TabSelectWidget();
+ this.tabPanel = new OO.ui.PanelLayout();
+ this.$menu.append( this.tabPanel.$element );
+
+ this.toggleMenu( true );
+
+ // Events
+ this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
+ this.tabSelectWidget.connect( this, { select: 'onTabSelectWidgetSelect' } );
+ if ( this.autoFocus ) {
+ // Event 'focus' does not bubble, but 'focusin' does
+ this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
+ }
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-indexLayout' );
+ this.stackLayout.$element.addClass( 'oo-ui-indexLayout-stackLayout' );
+ this.tabPanel.$element
+ .addClass( 'oo-ui-indexLayout-tabPanel' )
+ .append( this.tabSelectWidget.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );
+
+/* Events */
+
+/**
+ * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout.
+ * @event set
+ * @param {OO.ui.CardLayout} card Current card
+ */
+
+/**
+ * An 'add' event is emitted when cards are {@link #addCards added} to the index layout.
+ *
+ * @event add
+ * @param {OO.ui.CardLayout[]} card Added cards
+ * @param {number} index Index cards were added at
+ */
+
+/**
+ * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
+ * {@link #removeCards removed} from the index.
+ *
+ * @event remove
+ * @param {OO.ui.CardLayout[]} cards Removed cards
+ */
+
+/* Methods */
+
+/**
+ * Handle stack layout focus.
+ *
+ * @private
+ * @param {jQuery.Event} e Focusin event
+ */
+OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
+ var name, $target;
+
+ // Find the card that an element was focused within
+ $target = $( e.target ).closest( '.oo-ui-cardLayout' );
+ for ( name in this.cards ) {
+ // Check for card match, exclude current card to find only card changes
+ if ( this.cards[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentCardName ) {
+ this.setCard( name );
+ break;
+ }
+ }
+};
+
+/**
+ * Handle stack layout set events.
+ *
+ * @private
+ * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel
+ */
+OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
+ var layout = this;
+ if ( card ) {
+ card.scrollElementIntoView( { complete: function () {
+ if ( layout.autoFocus ) {
+ layout.focus();
+ }
+ } } );
+ }
+};
+
+/**
+ * Focus the first input in the current card.
+ *
+ * If no card is selected, the first selectable card will be selected.
+ * If the focus is already in an element on the current card, nothing will happen.
+ * @param {number} [itemIndex] A specific item to focus on
+ */
+OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
+ var card,
+ items = this.stackLayout.getItems();
+
+ if ( itemIndex !== undefined && items[ itemIndex ] ) {
+ card = items[ itemIndex ];
+ } else {
+ card = this.stackLayout.getCurrentItem();
+ }
+
+ if ( !card ) {
+ this.selectFirstSelectableCard();
+ card = this.stackLayout.getCurrentItem();
+ }
+ if ( !card ) {
+ return;
+ }
+ // Only change the focus if is not already in the current page
+ if ( !OO.ui.contains( card.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
+ card.focus();
+ }
+};
+
+/**
+ * Find the first focusable input in the index layout and focus
+ * on it.
+ */
+OO.ui.IndexLayout.prototype.focusFirstFocusable = function () {
+ OO.ui.findFocusable( this.stackLayout.$element ).focus();
+};
+
+/**
+ * Handle tab widget select events.
+ *
+ * @private
+ * @param {OO.ui.OptionWidget|null} item Selected item
+ */
+OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
+ if ( item ) {
+ this.setCard( item.getData() );
+ }
+};
+
+/**
+ * Get the card closest to the specified card.
+ *
+ * @param {OO.ui.CardLayout} card Card to use as a reference point
+ * @return {OO.ui.CardLayout|null} Card closest to the specified card
+ */
+OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) {
+ var next, prev, level,
+ cards = this.stackLayout.getItems(),
+ index = cards.indexOf( card );
+
+ if ( index !== -1 ) {
+ next = cards[ index + 1 ];
+ prev = cards[ index - 1 ];
+ // Prefer adjacent cards at the same level
+ level = this.tabSelectWidget.getItemFromData( card.getName() ).getLevel();
+ if (
+ prev &&
+ level === this.tabSelectWidget.getItemFromData( prev.getName() ).getLevel()
+ ) {
+ return prev;
+ }
+ if (
+ next &&
+ level === this.tabSelectWidget.getItemFromData( next.getName() ).getLevel()
+ ) {
+ return next;
+ }
+ }
+ return prev || next || null;
+};
+
+/**
+ * Get the tabs widget.
+ *
+ * @return {OO.ui.TabSelectWidget} Tabs widget
+ */
+OO.ui.IndexLayout.prototype.getTabs = function () {
+ return this.tabSelectWidget;
+};
+
+/**
+ * Get a card by its symbolic name.
+ *
+ * @param {string} name Symbolic name of card
+ * @return {OO.ui.CardLayout|undefined} Card, if found
+ */
+OO.ui.IndexLayout.prototype.getCard = function ( name ) {
+ return this.cards[ name ];
+};
+
+/**
+ * Get the current card.
+ *
+ * @return {OO.ui.CardLayout|undefined} Current card, if found
+ */
+OO.ui.IndexLayout.prototype.getCurrentCard = function () {
+ var name = this.getCurrentCardName();
+ return name ? this.getCard( name ) : undefined;
+};
+
+/**
+ * Get the symbolic name of the current card.
+ *
+ * @return {string|null} Symbolic name of the current card
+ */
+OO.ui.IndexLayout.prototype.getCurrentCardName = function () {
+ return this.currentCardName;
+};
+
+/**
+ * Add cards to the index layout
+ *
+ * When cards are added with the same names as existing cards, the existing cards will be
+ * automatically removed before the new cards are added.
+ *
+ * @param {OO.ui.CardLayout[]} cards Cards to add
+ * @param {number} index Index of the insertion point
+ * @fires add
+ * @chainable
+ */
+OO.ui.IndexLayout.prototype.addCards = function ( cards, index ) {
+ var i, len, name, card, item, currentIndex,
+ stackLayoutCards = this.stackLayout.getItems(),
+ remove = [],
+ items = [];
+
+ // Remove cards with same names
+ for ( i = 0, len = cards.length; i < len; i++ ) {
+ card = cards[ i ];
+ name = card.getName();
+
+ if ( Object.prototype.hasOwnProperty.call( this.cards, name ) ) {
+ // Correct the insertion index
+ currentIndex = stackLayoutCards.indexOf( this.cards[ name ] );
+ if ( currentIndex !== -1 && currentIndex + 1 < index ) {
+ index--;
+ }
+ remove.push( this.cards[ name ] );
+ }
+ }
+ if ( remove.length ) {
+ this.removeCards( remove );
+ }
+
+ // Add new cards
+ for ( i = 0, len = cards.length; i < len; i++ ) {
+ card = cards[ i ];
+ name = card.getName();
+ this.cards[ card.getName() ] = card;
+ item = new OO.ui.TabOptionWidget( { data: name } );
+ card.setTabItem( item );
+ items.push( item );
+ }
+
+ if ( items.length ) {
+ this.tabSelectWidget.addItems( items, index );
+ this.selectFirstSelectableCard();
+ }
+ this.stackLayout.addItems( cards, index );
+ this.emit( 'add', cards, index );
+
+ return this;
+};
+
+/**
+ * Remove the specified cards from the index layout.
+ *
+ * To remove all cards from the index, you may wish to use the #clearCards method instead.
+ *
+ * @param {OO.ui.CardLayout[]} cards An array of cards to remove
+ * @fires remove
+ * @chainable
+ */
+OO.ui.IndexLayout.prototype.removeCards = function ( cards ) {
+ var i, len, name, card,
+ items = [];
+
+ for ( i = 0, len = cards.length; i < len; i++ ) {
+ card = cards[ i ];
+ name = card.getName();
+ delete this.cards[ name ];
+ items.push( this.tabSelectWidget.getItemFromData( name ) );
+ card.setTabItem( null );
+ }
+ if ( items.length ) {
+ this.tabSelectWidget.removeItems( items );
+ this.selectFirstSelectableCard();
+ }
+ this.stackLayout.removeItems( cards );
+ this.emit( 'remove', cards );
+
+ return this;
+};
+
+/**
+ * Clear all cards from the index layout.
+ *
+ * To remove only a subset of cards from the index, use the #removeCards method.
+ *
+ * @fires remove
+ * @chainable
+ */
+OO.ui.IndexLayout.prototype.clearCards = function () {
+ var i, len,
+ cards = this.stackLayout.getItems();
+
+ this.cards = {};
+ this.currentCardName = null;
+ this.tabSelectWidget.clearItems();
+ for ( i = 0, len = cards.length; i < len; i++ ) {
+ cards[ i ].setTabItem( null );
+ }
+ this.stackLayout.clearItems();
+
+ this.emit( 'remove', cards );
+
+ return this;
+};
+
+/**
+ * Set the current card by symbolic name.
+ *
+ * @fires set
+ * @param {string} name Symbolic name of card
+ */
+OO.ui.IndexLayout.prototype.setCard = function ( name ) {
+ var selectedItem,
+ $focused,
+ card = this.cards[ name ],
+ previousCard = this.currentCardName && this.cards[ this.currentCardName ];
+
+ if ( name !== this.currentCardName ) {
+ selectedItem = this.tabSelectWidget.getSelectedItem();
+ if ( selectedItem && selectedItem.getData() !== name ) {
+ this.tabSelectWidget.selectItemByData( name );
+ }
+ if ( card ) {
+ if ( previousCard ) {
+ previousCard.setActive( false );
+ // Blur anything focused if the next card doesn't have anything focusable.
+ // This is not needed if the next card has something focusable (because once it is focused
+ // this blur happens automatically). If the layout is non-continuous, this check is
+ // meaningless because the next card is not visible yet and thus can't hold focus.
+ if (
+ this.autoFocus &&
+ this.stackLayout.continuous &&
+ OO.ui.findFocusable( card.$element ).length !== 0
+ ) {
+ $focused = previousCard.$element.find( ':focus' );
+ if ( $focused.length ) {
+ $focused[ 0 ].blur();
+ }
+ }
+ }
+ this.currentCardName = name;
+ card.setActive( true );
+ this.stackLayout.setItem( card );
+ if ( !this.stackLayout.continuous && previousCard ) {
+ // This should not be necessary, since any inputs on the previous card should have been
+ // blurred when it was hidden, but browsers are not very consistent about this.
+ $focused = previousCard.$element.find( ':focus' );
+ if ( $focused.length ) {
+ $focused[ 0 ].blur();
+ }
+ }
+ this.emit( 'set', card );
+ }
+ }
+};
+
+/**
+ * Select the first selectable card.
+ *
+ * @chainable
+ */
+OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
+ if ( !this.tabSelectWidget.getSelectedItem() ) {
+ this.tabSelectWidget.selectItem( this.tabSelectWidget.getFirstSelectableItem() );
+ }
+
+ return this;
+};
+
+/**
+ * ToggleWidget implements basic behavior of widgets with an on/off state.
+ * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [value=false] The toggle’s initial on/off state.
+ * By default, the toggle is in the 'off' state.
+ */
+OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.ToggleWidget.parent.call( this, config );
+
+ // Properties
+ this.value = null;
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-toggleWidget' );
+ this.setValue( !!config.value );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ToggleWidget, OO.ui.Widget );
+
+/* Events */
+
+/**
+ * @event change
+ *
+ * A change event is emitted when the on/off state of the toggle changes.
+ *
+ * @param {boolean} value Value representing the new state of the toggle
+ */
+
+/* Methods */
+
+/**
+ * Get the value representing the toggle’s state.
+ *
+ * @return {boolean} The on/off state of the toggle
+ */
+OO.ui.ToggleWidget.prototype.getValue = function () {
+ return this.value;
+};
+
+/**
+ * Set the state of the toggle: `true` for 'on', `false' for 'off'.
+ *
+ * @param {boolean} value The state of the toggle
+ * @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 );
+ this.$element.attr( 'aria-checked', value.toString() );
+ }
+ return this;
+};
+
+/**
+ * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
+ * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
+ * configured with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators},
+ * {@link OO.ui.mixin.TitledElement titles}, {@link OO.ui.mixin.FlaggedElement styling flags},
+ * and {@link OO.ui.mixin.LabelElement labels}. Please see
+ * the [OOjs UI documentation][1] on MediaWiki for more information.
+ *
+ * @example
+ * // Toggle buttons in the 'off' and 'on' state.
+ * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
+ * label: 'Toggle Button off'
+ * } );
+ * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
+ * label: 'Toggle Button on',
+ * value: true
+ * } );
+ * // Append the buttons to the DOM.
+ * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
+ *
+ * @class
+ * @extends OO.ui.ToggleWidget
+ * @mixins OO.ui.mixin.ButtonElement
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.IndicatorElement
+ * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.TitledElement
+ * @mixins OO.ui.mixin.FlaggedElement
+ * @mixins OO.ui.mixin.TabIndexedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [value=false] The toggle button’s initial on/off
+ * state. By default, the button is in the 'off' state.
+ */
+OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.ToggleButtonWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.ButtonElement.call( this, config );
+ OO.ui.mixin.IconElement.call( this, config );
+ OO.ui.mixin.IndicatorElement.call( this, config );
+ OO.ui.mixin.LabelElement.call( this, config );
+ OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
+ OO.ui.mixin.FlaggedElement.call( this, config );
+ OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
+
+ // Events
+ this.connect( this, { click: 'onAction' } );
+
+ // Initialization
+ this.$button.append( this.$icon, this.$label, this.$indicator );
+ this.$element
+ .addClass( 'oo-ui-toggleButtonWidget' )
+ .append( this.$button );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
+OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.ButtonElement );
+OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IconElement );
+OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IndicatorElement );
+OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TitledElement );
+OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.FlaggedElement );
+OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TabIndexedElement );
+
+/* Methods */
+
+/**
+ * Handle the button action being triggered.
+ *
+ * @private
+ */
+OO.ui.ToggleButtonWidget.prototype.onAction = function () {
+ this.setValue( !this.value );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
+ value = !!value;
+ if ( value !== this.value ) {
+ // Might be called from parent constructor before ButtonElement constructor
+ if ( this.$button ) {
+ this.$button.attr( 'aria-pressed', value.toString() );
+ }
+ this.setActive( value );
+ }
+
+ // Parent method
+ OO.ui.ToggleButtonWidget.parent.prototype.setValue.call( this, value );
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ToggleButtonWidget.prototype.setButtonElement = function ( $button ) {
+ if ( this.$button ) {
+ this.$button.removeAttr( 'aria-pressed' );
+ }
+ OO.ui.mixin.ButtonElement.prototype.setButtonElement.call( this, $button );
+ this.$button.attr( 'aria-pressed', this.value.toString() );
+};
+
+/**
+ * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
+ * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
+ * visually by a slider in the leftmost position.
+ *
+ * @example
+ * // Toggle switches in the 'off' and 'on' position.
+ * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget();
+ * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
+ * value: true
+ * } );
+ *
+ * // Create a FieldsetLayout to layout and label switches
+ * var fieldset = new OO.ui.FieldsetLayout( {
+ * label: 'Toggle switches'
+ * } );
+ * fieldset.addItems( [
+ * new OO.ui.FieldLayout( toggleSwitch1, { label: 'Off', align: 'top' } ),
+ * new OO.ui.FieldLayout( toggleSwitch2, { label: 'On', align: 'top' } )
+ * ] );
+ * $( 'body' ).append( fieldset.$element );
+ *
+ * @class
+ * @extends OO.ui.ToggleWidget
+ * @mixins OO.ui.mixin.TabIndexedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
+ * By default, the toggle switch is in the 'off' position.
+ */
+OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
+ // Parent constructor
+ OO.ui.ToggleSwitchWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.TabIndexedElement.call( this, config );
+
+ // Properties
+ this.dragging = false;
+ this.dragStart = null;
+ this.sliding = false;
+ this.$glow = $( '<span>' );
+ this.$grip = $( '<span>' );
+
+ // Events
+ this.$element.on( {
+ click: this.onClick.bind( this ),
+ keypress: this.onKeyPress.bind( this )
+ } );
+
+ // Initialization
+ this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
+ this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
+ this.$element
+ .addClass( 'oo-ui-toggleSwitchWidget' )
+ .attr( 'role', 'checkbox' )
+ .append( this.$glow, this.$grip );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
+OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.mixin.TabIndexedElement );
+
+/* Methods */
+
+/**
+ * Handle mouse click events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse click event
+ */
+OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
+ if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
+ this.setValue( !this.value );
+ }
+ return false;
+};
+
+/**
+ * Handle key press events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ */
+OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
+ if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
+ this.setValue( !this.value );
+ return false;
+ }
+};
+
+/**
+ * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
+ * Controls include moving items up and down, removing items, and adding different kinds of items.
+ *
+ * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.GroupElement
+ * @mixins OO.ui.mixin.IconElement
+ *
+ * @constructor
+ * @param {OO.ui.OutlineSelectWidget} outline Outline to control
+ * @param {Object} [config] Configuration options
+ * @cfg {Object} [abilities] List of abilties
+ * @cfg {boolean} [abilities.move=true] Allow moving movable items
+ * @cfg {boolean} [abilities.remove=true] Allow removing removable items
+ */
+OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( outline ) && config === undefined ) {
+ config = outline;
+ outline = config.outline;
+ }
+
+ // Configuration initialization
+ config = $.extend( { icon: 'add' }, config );
+
+ // Parent constructor
+ OO.ui.OutlineControlsWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.GroupElement.call( this, config );
+ OO.ui.mixin.IconElement.call( this, config );
+
+ // Properties
+ this.outline = outline;
+ this.$movers = $( '<div>' );
+ this.upButton = new OO.ui.ButtonWidget( {
+ framed: false,
+ icon: 'collapse',
+ title: OO.ui.msg( 'ooui-outline-control-move-up' )
+ } );
+ this.downButton = new OO.ui.ButtonWidget( {
+ framed: false,
+ icon: 'expand',
+ title: OO.ui.msg( 'ooui-outline-control-move-down' )
+ } );
+ this.removeButton = new OO.ui.ButtonWidget( {
+ framed: false,
+ icon: 'remove',
+ title: OO.ui.msg( 'ooui-outline-control-remove' )
+ } );
+ this.abilities = { move: true, remove: true };
+
+ // 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 );
+ this.setAbilities( config.abilities || {} );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.GroupElement );
+OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.IconElement );
+
+/* Events */
+
+/**
+ * @event move
+ * @param {number} places Number of places to move
+ */
+
+/**
+ * @event remove
+ */
+
+/* Methods */
+
+/**
+ * Set abilities.
+ *
+ * @param {Object} abilities List of abilties
+ * @param {boolean} [abilities.move] Allow moving movable items
+ * @param {boolean} [abilities.remove] Allow removing removable items
+ */
+OO.ui.OutlineControlsWidget.prototype.setAbilities = function ( abilities ) {
+ var ability;
+
+ for ( ability in this.abilities ) {
+ if ( abilities[ ability ] !== undefined ) {
+ this.abilities[ ability ] = !!abilities[ ability ];
+ }
+ }
+
+ this.onOutlineChange();
+};
+
+/**
+ * @private
+ * Handle outline change events.
+ */
+OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
+ var i, len, firstMovable, lastMovable,
+ items = this.outline.getItems(),
+ selectedItem = this.outline.getSelectedItem(),
+ movable = this.abilities.move && selectedItem && selectedItem.isMovable(),
+ removable = this.abilities.remove && 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 );
+};
+
+/**
+ * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}.
+ *
+ * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain
+ * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout}
+ * for an example.
+ *
+ * @class
+ * @extends OO.ui.DecoratedOptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {number} [level] Indentation level
+ * @cfg {boolean} [movable] Allow modification from {@link OO.ui.OutlineControlsWidget outline controls}.
+ */
+OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.OutlineOptionWidget.parent.call( this, config );
+
+ // Properties
+ this.level = 0;
+ this.movable = !!config.movable;
+ this.removable = !!config.removable;
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-outlineOptionWidget' );
+ this.setLevel( config.level );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
+
+/* Static Properties */
+
+OO.ui.OutlineOptionWidget.static.highlightable = false;
+
+OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
+
+OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
+
+OO.ui.OutlineOptionWidget.static.levels = 3;
+
+/* Methods */
+
+/**
+ * Check if item is movable.
+ *
+ * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
+ *
+ * @return {boolean} Item is movable
+ */
+OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
+ return this.movable;
+};
+
+/**
+ * Check if item is removable.
+ *
+ * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
+ *
+ * @return {boolean} Item is removable
+ */
+OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
+ return this.removable;
+};
+
+/**
+ * Get indentation level.
+ *
+ * @return {number} Indentation level
+ */
+OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
+ return this.level;
+};
+
+/**
+ * Set movability.
+ *
+ * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
+ *
+ * @param {boolean} movable Item is movable
+ * @chainable
+ */
+OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
+ this.movable = !!movable;
+ this.updateThemeClasses();
+ return this;
+};
+
+/**
+ * Set removability.
+ *
+ * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
+ *
+ * @param {boolean} removable Item is removable
+ * @chainable
+ */
+OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
+ this.removable = !!removable;
+ this.updateThemeClasses();
+ return this;
+};
+
+/**
+ * Set indentation level.
+ *
+ * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
+ * @chainable
+ */
+OO.ui.OutlineOptionWidget.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 );
+ }
+ }
+ this.updateThemeClasses();
+
+ return this;
+};
+
+/**
+ * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
+ * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
+ *
+ * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ * @mixins OO.ui.mixin.TabIndexedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
+ // Parent constructor
+ OO.ui.OutlineSelectWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.TabIndexedElement.call( this, config );
+
+ // Events
+ this.$element.on( {
+ focus: this.bindKeyDownListener.bind( this ),
+ blur: this.unbindKeyDownListener.bind( this )
+ } );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-outlineSelectWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.mixin.TabIndexedElement );
+
+/**
+ * ButtonOptionWidget is a special type of {@link OO.ui.mixin.ButtonElement button element} that
+ * can be selected and configured with data. The class is
+ * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
+ * [OOjs UI documentation on MediaWiki] [1] for more information.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
+ *
+ * @class
+ * @extends OO.ui.DecoratedOptionWidget
+ * @mixins OO.ui.mixin.ButtonElement
+ * @mixins OO.ui.mixin.TabIndexedElement
+ * @mixins OO.ui.mixin.TitledElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.ButtonOptionWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.ButtonElement.call( this, config );
+ OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
+ OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, {
+ $tabIndexed: this.$button,
+ tabIndex: -1
+ } ) );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-buttonOptionWidget' );
+ this.$button.append( this.$element.contents() );
+ this.$element.append( this.$button );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
+OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.ButtonElement );
+OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TitledElement );
+OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TabIndexedElement );
+
+/* Static Properties */
+
+// 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.ButtonOptionWidget.static.highlightable = false;
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
+ OO.ui.ButtonOptionWidget.parent.prototype.setSelected.call( this, state );
+
+ if ( this.constructor.static.selectable ) {
+ this.setActive( state );
+ }
+
+ return this;
+};
+
+/**
+ * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
+ * button options and is used together with
+ * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
+ * highlighting, choosing, and selecting mutually exclusive options. Please see
+ * the [OOjs UI documentation on MediaWiki] [1] for more information.
+ *
+ * @example
+ * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
+ * var option1 = new OO.ui.ButtonOptionWidget( {
+ * data: 1,
+ * label: 'Option 1',
+ * title: 'Button option 1'
+ * } );
+ *
+ * var option2 = new OO.ui.ButtonOptionWidget( {
+ * data: 2,
+ * label: 'Option 2',
+ * title: 'Button option 2'
+ * } );
+ *
+ * var option3 = new OO.ui.ButtonOptionWidget( {
+ * data: 3,
+ * label: 'Option 3',
+ * title: 'Button option 3'
+ * } );
+ *
+ * var buttonSelect=new OO.ui.ButtonSelectWidget( {
+ * items: [ option1, option2, option3 ]
+ * } );
+ * $( 'body' ).append( buttonSelect.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ * @mixins OO.ui.mixin.TabIndexedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
+ // Parent constructor
+ OO.ui.ButtonSelectWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.TabIndexedElement.call( this, config );
+
+ // Events
+ this.$element.on( {
+ focus: this.bindKeyDownListener.bind( this ),
+ blur: this.unbindKeyDownListener.bind( this )
+ } );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-buttonSelectWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.mixin.TabIndexedElement );
+
+/**
+ * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
+ *
+ * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
+ * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout}
+ * for an example.
+ *
+ * @class
+ * @extends OO.ui.OptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.TabOptionWidget = function OoUiTabOptionWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.TabOptionWidget.parent.call( this, config );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-tabOptionWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.TabOptionWidget, OO.ui.OptionWidget );
+
+/* Static Properties */
+
+OO.ui.TabOptionWidget.static.highlightable = false;
+
+/**
+ * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options}
+ *
+ * **Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.**
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ * @mixins OO.ui.mixin.TabIndexedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.TabSelectWidget = function OoUiTabSelectWidget( config ) {
+ // Parent constructor
+ OO.ui.TabSelectWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.TabIndexedElement.call( this, config );
+
+ // Events
+ this.$element.on( {
+ focus: this.bindKeyDownListener.bind( this ),
+ blur: this.unbindKeyDownListener.bind( this )
+ } );
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-tabSelectWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.TabSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.mixin.TabIndexedElement );
+
+/**
+ * CapsuleItemWidgets are used within a {@link OO.ui.CapsuleMultiSelectWidget
+ * CapsuleMultiSelectWidget} to display the selected items.
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.ItemWidget
+ * @mixins OO.ui.mixin.IndicatorElement
+ * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.FlaggedElement
+ * @mixins OO.ui.mixin.TabIndexedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.CapsuleItemWidget = function OoUiCapsuleItemWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.CapsuleItemWidget.parent.call( this, config );
+
+ // Properties (must be set before mixin constructor calls)
+ this.$indicator = $( '<span>' );
+
+ // Mixin constructors
+ OO.ui.mixin.ItemWidget.call( this );
+ OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$indicator, indicator: 'clear' } ) );
+ OO.ui.mixin.LabelElement.call( this, config );
+ OO.ui.mixin.FlaggedElement.call( this, config );
+ OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
+
+ // Events
+ this.$indicator.on( {
+ keydown: this.onCloseKeyDown.bind( this ),
+ click: this.onCloseClick.bind( this )
+ } );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-capsuleItemWidget' )
+ .append( this.$indicator, this.$label );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.CapsuleItemWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.ItemWidget );
+OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.IndicatorElement );
+OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.FlaggedElement );
+OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.TabIndexedElement );
+
+/* Methods */
+
+/**
+ * Handle close icon clicks
+ * @param {jQuery.Event} event
+ */
+OO.ui.CapsuleItemWidget.prototype.onCloseClick = function () {
+ var element = this.getElementGroup();
+
+ if ( !this.isDisabled() && element && $.isFunction( element.removeItems ) ) {
+ element.removeItems( [ this ] );
+ element.focus();
+ }
+};
+
+/**
+ * Handle close keyboard events
+ * @param {jQuery.Event} event Key down event
+ */
+OO.ui.CapsuleItemWidget.prototype.onCloseKeyDown = function ( e ) {
+ if ( !this.isDisabled() && $.isFunction( this.getElementGroup().removeItems ) ) {
+ switch ( e.which ) {
+ case OO.ui.Keys.ENTER:
+ case OO.ui.Keys.BACKSPACE:
+ case OO.ui.Keys.SPACE:
+ this.getElementGroup().removeItems( [ this ] );
+ return false;
+ }
+ }
+};
+
+/**
+ * CapsuleMultiSelectWidgets are something like a {@link OO.ui.ComboBoxInputWidget combo box widget}
+ * that allows for selecting multiple values.
+ *
+ * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * @example
+ * // Example: A CapsuleMultiSelectWidget.
+ * var capsule = new OO.ui.CapsuleMultiSelectWidget( {
+ * label: 'CapsuleMultiSelectWidget',
+ * selected: [ 'Option 1', 'Option 3' ],
+ * menu: {
+ * items: [
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'Option 1',
+ * label: 'Option One'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'Option 2',
+ * label: 'Option Two'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'Option 3',
+ * label: 'Option Three'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'Option 4',
+ * label: 'Option Four'
+ * } ),
+ * new OO.ui.MenuOptionWidget( {
+ * data: 'Option 5',
+ * label: 'Option Five'
+ * } )
+ * ]
+ * }
+ * } );
+ * $( 'body' ).append( capsule.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.TabIndexedElement
+ * @mixins OO.ui.mixin.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [allowArbitrary=false] Allow data items to be added even if not present in the menu.
+ * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
+ * @cfg {Object} [popup] Configuration options to pass to the {@link OO.ui.PopupWidget popup widget}.
+ * If specified, this popup will be shown instead of the menu (but the menu
+ * will still be used for item labels and allowArbitrary=false). The widgets
+ * in the popup should use this.addItemsFromData() or this.addItems() as necessary.
+ * @cfg {jQuery} [$overlay] Render the menu or popup into a separate layer.
+ * This configuration is useful in cases where the expanded menu is larger than
+ * its containing `<div>`. The specified overlay layer is usually on top of
+ * the containing `<div>` and has a larger area. By default, the menu uses
+ * relative positioning.
+ */
+OO.ui.CapsuleMultiSelectWidget = function OoUiCapsuleMultiSelectWidget( config ) {
+ var $tabFocus;
+
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.CapsuleMultiSelectWidget.parent.call( this, config );
+
+ // Properties (must be set before mixin constructor calls)
+ this.$input = config.popup ? null : $( '<input>' );
+ this.$handle = $( '<div>' );
+
+ // Mixin constructors
+ OO.ui.mixin.GroupElement.call( this, config );
+ if ( config.popup ) {
+ config.popup = $.extend( {}, config.popup, {
+ align: 'forwards',
+ anchor: false
+ } );
+ OO.ui.mixin.PopupElement.call( this, config );
+ $tabFocus = $( '<span>' );
+ OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: $tabFocus } ) );
+ } else {
+ this.popup = null;
+ $tabFocus = null;
+ OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
+ }
+ OO.ui.mixin.IndicatorElement.call( this, config );
+ OO.ui.mixin.IconElement.call( this, config );
+
+ // Properties
+ this.$content = $( '<div>' );
+ this.allowArbitrary = !!config.allowArbitrary;
+ this.$overlay = config.$overlay || this.$element;
+ this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
+ {
+ widget: this,
+ $input: this.$input,
+ $container: this.$element,
+ filterFromInput: true,
+ disabled: this.isDisabled()
+ },
+ config.menu
+ ) );
+
+ // Events
+ if ( this.popup ) {
+ $tabFocus.on( {
+ focus: this.onFocusForPopup.bind( this )
+ } );
+ this.popup.$element.on( 'focusout', this.onPopupFocusOut.bind( this ) );
+ if ( this.popup.$autoCloseIgnore ) {
+ this.popup.$autoCloseIgnore.on( 'focusout', this.onPopupFocusOut.bind( this ) );
+ }
+ this.popup.connect( this, {
+ toggle: function ( visible ) {
+ $tabFocus.toggle( !visible );
+ }
+ } );
+ } else {
+ this.$input.on( {
+ focus: this.onInputFocus.bind( this ),
+ blur: this.onInputBlur.bind( this ),
+ 'propertychange change click mouseup keydown keyup input cut paste select focus':
+ OO.ui.debounce( this.updateInputSize.bind( this ) ),
+ keydown: this.onKeyDown.bind( this ),
+ keypress: this.onKeyPress.bind( this )
+ } );
+ }
+ this.menu.connect( this, {
+ choose: 'onMenuChoose',
+ add: 'onMenuItemsChange',
+ remove: 'onMenuItemsChange'
+ } );
+ this.$handle.on( {
+ mousedown: this.onMouseDown.bind( this )
+ } );
+
+ // Initialization
+ if ( this.$input ) {
+ this.$input.prop( 'disabled', this.isDisabled() );
+ this.$input.attr( {
+ role: 'combobox',
+ 'aria-autocomplete': 'list'
+ } );
+ this.updateInputSize();
+ }
+ if ( config.data ) {
+ this.setItemsFromData( config.data );
+ }
+ this.$content.addClass( 'oo-ui-capsuleMultiSelectWidget-content' )
+ .append( this.$group );
+ this.$group.addClass( 'oo-ui-capsuleMultiSelectWidget-group' );
+ this.$handle.addClass( 'oo-ui-capsuleMultiSelectWidget-handle' )
+ .append( this.$indicator, this.$icon, this.$content );
+ this.$element.addClass( 'oo-ui-capsuleMultiSelectWidget' )
+ .append( this.$handle );
+ if ( this.popup ) {
+ this.$content.append( $tabFocus );
+ this.$overlay.append( this.popup.$element );
+ } else {
+ this.$content.append( this.$input );
+ this.$overlay.append( this.menu.$element );
+ }
+ this.onMenuItemsChange();
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.GroupElement );
+OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.PopupElement );
+OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.TabIndexedElement );
+OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.IndicatorElement );
+OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.IconElement );
+
+/* Events */
+
+/**
+ * @event change
+ *
+ * A change event is emitted when the set of selected items changes.
+ *
+ * @param {Mixed[]} datas Data of the now-selected items
+ */
+
+/* Methods */
+
+/**
+ * Construct a OO.ui.CapsuleItemWidget (or a subclass thereof) from given label and data.
+ *
+ * @protected
+ * @param {Mixed} data Custom data of any type.
+ * @param {string} label The label text.
+ * @return {OO.ui.CapsuleItemWidget}
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.createItemWidget = function ( data, label ) {
+ return new OO.ui.CapsuleItemWidget( { data: data, label: label } );
+};
+
+/**
+ * Get the data of the items in the capsule
+ * @return {Mixed[]}
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.getItemsData = function () {
+ return $.map( this.getItems(), function ( e ) { return e.data; } );
+};
+
+/**
+ * Set the items in the capsule by providing data
+ * @chainable
+ * @param {Mixed[]} datas
+ * @return {OO.ui.CapsuleMultiSelectWidget}
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.setItemsFromData = function ( datas ) {
+ var widget = this,
+ menu = this.menu,
+ items = this.getItems();
+
+ $.each( datas, function ( i, data ) {
+ var j, label,
+ item = menu.getItemFromData( data );
+
+ if ( item ) {
+ label = item.label;
+ } else if ( widget.allowArbitrary ) {
+ label = String( data );
+ } else {
+ return;
+ }
+
+ item = null;
+ for ( j = 0; j < items.length; j++ ) {
+ if ( items[ j ].data === data && items[ j ].label === label ) {
+ item = items[ j ];
+ items.splice( j, 1 );
+ break;
+ }
+ }
+ if ( !item ) {
+ item = widget.createItemWidget( data, label );
+ }
+ widget.addItems( [ item ], i );
+ } );
+
+ if ( items.length ) {
+ widget.removeItems( items );
+ }
+
+ return this;
+};
+
+/**
+ * Add items to the capsule by providing their data
+ * @chainable
+ * @param {Mixed[]} datas
+ * @return {OO.ui.CapsuleMultiSelectWidget}
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.addItemsFromData = function ( datas ) {
+ var widget = this,
+ menu = this.menu,
+ items = [];
+
+ $.each( datas, function ( i, data ) {
+ var item;
+
+ if ( !widget.getItemFromData( data ) ) {
+ item = menu.getItemFromData( data );
+ if ( item ) {
+ items.push( widget.createItemWidget( data, item.label ) );
+ } else if ( widget.allowArbitrary ) {
+ items.push( widget.createItemWidget( data, String( data ) ) );
+ }
+ }
+ } );
+
+ if ( items.length ) {
+ this.addItems( items );
+ }
+
+ return this;
+};
+
+/**
+ * Remove items by data
+ * @chainable
+ * @param {Mixed[]} datas
+ * @return {OO.ui.CapsuleMultiSelectWidget}
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.removeItemsFromData = function ( datas ) {
+ var widget = this,
+ items = [];
+
+ $.each( datas, function ( i, data ) {
+ var item = widget.getItemFromData( data );
+ if ( item ) {
+ items.push( item );
+ }
+ } );
+
+ if ( items.length ) {
+ this.removeItems( items );
+ }
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.addItems = function ( items ) {
+ var same, i, l,
+ oldItems = this.items.slice();
+
+ OO.ui.mixin.GroupElement.prototype.addItems.call( this, items );
+
+ if ( this.items.length !== oldItems.length ) {
+ same = false;
+ } else {
+ same = true;
+ for ( i = 0, l = oldItems.length; same && i < l; i++ ) {
+ same = same && this.items[ i ] === oldItems[ i ];
+ }
+ }
+ if ( !same ) {
+ this.emit( 'change', this.getItemsData() );
+ this.menu.position();
+ }
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.removeItems = function ( items ) {
+ var same, i, l,
+ oldItems = this.items.slice();
+
+ OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
+
+ if ( this.items.length !== oldItems.length ) {
+ same = false;
+ } else {
+ same = true;
+ for ( i = 0, l = oldItems.length; same && i < l; i++ ) {
+ same = same && this.items[ i ] === oldItems[ i ];
+ }
+ }
+ if ( !same ) {
+ this.emit( 'change', this.getItemsData() );
+ this.menu.position();
+ }
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.clearItems = function () {
+ if ( this.items.length ) {
+ OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
+ this.emit( 'change', this.getItemsData() );
+ this.menu.position();
+ }
+ return this;
+};
+
+/**
+ * Get the capsule widget's menu.
+ * @return {OO.ui.MenuSelectWidget} Menu widget
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.getMenu = function () {
+ return this.menu;
+};
+
+/**
+ * Handle focus events
+ *
+ * @private
+ * @param {jQuery.Event} event
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.onInputFocus = function () {
+ if ( !this.isDisabled() ) {
+ this.menu.toggle( true );
+ }
+};
+
+/**
+ * Handle blur events
+ *
+ * @private
+ * @param {jQuery.Event} event
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.onInputBlur = function () {
+ if ( this.allowArbitrary && this.$input.val().trim() !== '' ) {
+ this.addItemsFromData( [ this.$input.val() ] );
+ }
+ this.clearInput();
+};
+
+/**
+ * Handle focus events
+ *
+ * @private
+ * @param {jQuery.Event} event
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.onFocusForPopup = function () {
+ if ( !this.isDisabled() ) {
+ this.popup.setSize( this.$handle.width() );
+ this.popup.toggle( true );
+ this.popup.$element.find( '*' )
+ .filter( function () { return OO.ui.isFocusableElement( $( this ), true ); } )
+ .first()
+ .focus();
+ }
+};
+
+/**
+ * Handles popup focus out events.
+ *
+ * @private
+ * @param {Event} e Focus out event
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.onPopupFocusOut = function () {
+ var widget = this.popup;
+
+ setTimeout( function () {
+ if (
+ widget.isVisible() &&
+ !OO.ui.contains( widget.$element[ 0 ], document.activeElement, true ) &&
+ ( !widget.$autoCloseIgnore || !widget.$autoCloseIgnore.has( document.activeElement ).length )
+ ) {
+ widget.toggle( false );
+ }
+ } );
+};
+
+/**
+ * Handle mouse down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.onMouseDown = function ( e ) {
+ if ( e.which === OO.ui.MouseButtons.LEFT ) {
+ this.focus();
+ return false;
+ } else {
+ this.updateInputSize();
+ }
+};
+
+/**
+ * Handle key press events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.onKeyPress = function ( e ) {
+ var item;
+
+ if ( !this.isDisabled() ) {
+ if ( e.which === OO.ui.Keys.ESCAPE ) {
+ this.clearInput();
+ return false;
+ }
+
+ if ( !this.popup ) {
+ this.menu.toggle( true );
+ if ( e.which === OO.ui.Keys.ENTER ) {
+ item = this.menu.getItemFromLabel( this.$input.val(), true );
+ if ( item ) {
+ this.addItemsFromData( [ item.data ] );
+ this.clearInput();
+ } else if ( this.allowArbitrary && this.$input.val().trim() !== '' ) {
+ this.addItemsFromData( [ this.$input.val() ] );
+ this.clearInput();
+ }
+ return false;
+ }
+
+ // Make sure the input gets resized.
+ setTimeout( this.updateInputSize.bind( this ), 0 );
+ }
+ }
+};
+
+/**
+ * Handle key down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.onKeyDown = function ( e ) {
+ if ( !this.isDisabled() ) {
+ // 'keypress' event is not triggered for Backspace
+ if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.$input.val() === '' ) {
+ if ( this.items.length ) {
+ this.removeItems( this.items.slice( -1 ) );
+ }
+ return false;
+ }
+ }
+};
+
+/**
+ * Update the dimensions of the text input field to encompass all available area.
+ *
+ * @private
+ * @param {jQuery.Event} e Event of some sort
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.updateInputSize = function () {
+ var $lastItem, direction, contentWidth, currentWidth, bestWidth;
+ if ( !this.isDisabled() ) {
+ this.$input.css( 'width', '1em' );
+ $lastItem = this.$group.children().last();
+ direction = OO.ui.Element.static.getDir( this.$handle );
+ contentWidth = this.$input[ 0 ].scrollWidth;
+ currentWidth = this.$input.width();
+
+ if ( contentWidth < currentWidth ) {
+ // All is fine, don't perform expensive calculations
+ return;
+ }
+
+ if ( !$lastItem.length ) {
+ bestWidth = this.$content.innerWidth();
+ } else {
+ bestWidth = direction === 'ltr' ?
+ this.$content.innerWidth() - $lastItem.position().left - $lastItem.outerWidth() :
+ $lastItem.position().left;
+ }
+ // Some safety margin for sanity, because I *really* don't feel like finding out where the few
+ // pixels this is off by are coming from.
+ bestWidth -= 10;
+ if ( contentWidth > bestWidth ) {
+ // This will result in the input getting shifted to the next line
+ bestWidth = this.$content.innerWidth() - 10;
+ }
+ this.$input.width( Math.floor( bestWidth ) );
+
+ this.menu.position();
+ }
+};
+
+/**
+ * Handle menu choose events.
+ *
+ * @private
+ * @param {OO.ui.OptionWidget} item Chosen item
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.onMenuChoose = function ( item ) {
+ if ( item && item.isVisible() ) {
+ this.addItemsFromData( [ item.getData() ] );
+ this.clearInput();
+ }
+};
+
+/**
+ * Handle menu item change events.
+ *
+ * @private
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.onMenuItemsChange = function () {
+ this.setItemsFromData( this.getItemsData() );
+ this.$element.toggleClass( 'oo-ui-capsuleMultiSelectWidget-empty', this.menu.isEmpty() );
+};
+
+/**
+ * Clear the input field
+ * @private
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.clearInput = function () {
+ if ( this.$input ) {
+ this.$input.val( '' );
+ this.updateInputSize();
+ }
+ if ( this.popup ) {
+ this.popup.toggle( false );
+ }
+ this.menu.toggle( false );
+ this.menu.selectItem();
+ this.menu.highlightItem();
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.setDisabled = function ( disabled ) {
+ var i, len;
+
+ // Parent method
+ OO.ui.CapsuleMultiSelectWidget.parent.prototype.setDisabled.call( this, disabled );
+
+ if ( this.$input ) {
+ this.$input.prop( 'disabled', this.isDisabled() );
+ }
+ if ( this.menu ) {
+ this.menu.setDisabled( this.isDisabled() );
+ }
+ if ( this.popup ) {
+ this.popup.setDisabled( this.isDisabled() );
+ }
+
+ if ( this.items ) {
+ for ( i = 0, len = this.items.length; i < len; i++ ) {
+ this.items[ i ].updateDisabled();
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Focus the widget
+ * @chainable
+ * @return {OO.ui.CapsuleMultiSelectWidget}
+ */
+OO.ui.CapsuleMultiSelectWidget.prototype.focus = function () {
+ if ( !this.isDisabled() ) {
+ if ( this.popup ) {
+ this.popup.setSize( this.$handle.width() );
+ this.popup.toggle( true );
+ this.popup.$element.find( '*' )
+ .filter( function () { return OO.ui.isFocusableElement( $( this ), true ); } )
+ .first()
+ .focus();
+ } else {
+ this.updateInputSize();
+ this.menu.toggle( true );
+ this.$input.focus();
+ }
+ }
+ return this;
+};
+
+/**
+ * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
+ * widgets can be configured with {@link OO.ui.mixin.IconElement icons} and {@link
+ * OO.ui.mixin.IndicatorElement indicators}.
+ * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
+ *
+ * @example
+ * // Example of a file select widget
+ * var selectFile = new OO.ui.SelectFileWidget();
+ * $( 'body' ).append( selectFile.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.IconElement
+ * @mixins OO.ui.mixin.IndicatorElement
+ * @mixins OO.ui.mixin.PendingElement
+ * @mixins OO.ui.mixin.LabelElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
+ * @cfg {string} [placeholder] Text to display when no file is selected.
+ * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
+ * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
+ * @cfg {boolean} [showDropTarget=false] Whether to show a drop target. Requires droppable to be true.
+ * @cfg {boolean} [dragDropUI=false] Deprecated alias for showDropTarget
+ */
+OO.ui.SelectFileWidget = function OoUiSelectFileWidget( config ) {
+ var dragHandler;
+
+ // TODO: Remove in next release
+ if ( config && config.dragDropUI ) {
+ config.showDropTarget = true;
+ }
+
+ // Configuration initialization
+ config = $.extend( {
+ accept: null,
+ placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
+ notsupported: OO.ui.msg( 'ooui-selectfile-not-supported' ),
+ droppable: true,
+ showDropTarget: false
+ }, config );
+
+ // Parent constructor
+ OO.ui.SelectFileWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.IconElement.call( this, config );
+ OO.ui.mixin.IndicatorElement.call( this, config );
+ OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$info } ) );
+ OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { autoFitLabel: true } ) );
+
+ // Properties
+ this.$info = $( '<span>' );
+
+ // Properties
+ this.showDropTarget = config.showDropTarget;
+ this.isSupported = this.constructor.static.isSupported();
+ this.currentFile = null;
+ if ( Array.isArray( config.accept ) ) {
+ this.accept = config.accept;
+ } else {
+ this.accept = null;
+ }
+ this.placeholder = config.placeholder;
+ this.notsupported = config.notsupported;
+ this.onFileSelectedHandler = this.onFileSelected.bind( this );
+
+ this.selectButton = new OO.ui.ButtonWidget( {
+ classes: [ 'oo-ui-selectFileWidget-selectButton' ],
+ label: OO.ui.msg( 'ooui-selectfile-button-select' ),
+ disabled: this.disabled || !this.isSupported
+ } );
+
+ this.clearButton = new OO.ui.ButtonWidget( {
+ classes: [ 'oo-ui-selectFileWidget-clearButton' ],
+ framed: false,
+ icon: 'remove',
+ disabled: this.disabled
+ } );
+
+ // Events
+ this.selectButton.$button.on( {
+ keypress: this.onKeyPress.bind( this )
+ } );
+ this.clearButton.connect( this, {
+ click: 'onClearClick'
+ } );
+ if ( config.droppable ) {
+ dragHandler = this.onDragEnterOrOver.bind( this );
+ this.$element.on( {
+ dragenter: dragHandler,
+ dragover: dragHandler,
+ dragleave: this.onDragLeave.bind( this ),
+ drop: this.onDrop.bind( this )
+ } );
+ }
+
+ // Initialization
+ this.addInput();
+ this.updateUI();
+ this.$label.addClass( 'oo-ui-selectFileWidget-label' );
+ this.$info
+ .addClass( 'oo-ui-selectFileWidget-info' )
+ .append( this.$icon, this.$label, this.clearButton.$element, this.$indicator );
+ this.$element
+ .addClass( 'oo-ui-selectFileWidget' )
+ .append( this.$info, this.selectButton.$element );
+ if ( config.droppable && config.showDropTarget ) {
+ this.$dropTarget = $( '<div>' )
+ .addClass( 'oo-ui-selectFileWidget-dropTarget' )
+ .text( OO.ui.msg( 'ooui-selectfile-dragdrop-placeholder' ) )
+ .on( {
+ click: this.onDropTargetClick.bind( this )
+ } );
+ this.$element.prepend( this.$dropTarget );
+ }
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IconElement );
+OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IndicatorElement );
+OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.PendingElement );
+OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.LabelElement );
+
+/* Static Properties */
+
+/**
+ * Check if this widget is supported
+ *
+ * @static
+ * @return {boolean}
+ */
+OO.ui.SelectFileWidget.static.isSupported = function () {
+ var $input;
+ if ( OO.ui.SelectFileWidget.static.isSupportedCache === null ) {
+ $input = $( '<input type="file">' );
+ OO.ui.SelectFileWidget.static.isSupportedCache = $input[ 0 ].files !== undefined;
+ }
+ return OO.ui.SelectFileWidget.static.isSupportedCache;
+};
+
+OO.ui.SelectFileWidget.static.isSupportedCache = null;
+
+/* Events */
+
+/**
+ * @event change
+ *
+ * A change event is emitted when the on/off state of the toggle changes.
+ *
+ * @param {File|null} value New value
+ */
+
+/* Methods */
+
+/**
+ * Get the current value of the field
+ *
+ * @return {File|null}
+ */
+OO.ui.SelectFileWidget.prototype.getValue = function () {
+ return this.currentFile;
+};
+
+/**
+ * Set the current value of the field
+ *
+ * @param {File|null} file File to select
+ */
+OO.ui.SelectFileWidget.prototype.setValue = function ( file ) {
+ if ( this.currentFile !== file ) {
+ this.currentFile = file;
+ this.updateUI();
+ this.emit( 'change', this.currentFile );
+ }
+};
+
+/**
+ * Focus the widget.
+ *
+ * Focusses the select file button.
+ *
+ * @chainable
+ */
+OO.ui.SelectFileWidget.prototype.focus = function () {
+ this.selectButton.$button[ 0 ].focus();
+ return this;
+};
+
+/**
+ * Update the user interface when a file is selected or unselected
+ *
+ * @protected
+ */
+OO.ui.SelectFileWidget.prototype.updateUI = function () {
+ var $label;
+ if ( !this.isSupported ) {
+ this.$element.addClass( 'oo-ui-selectFileWidget-notsupported' );
+ this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
+ this.setLabel( this.notsupported );
+ } else {
+ this.$element.addClass( 'oo-ui-selectFileWidget-supported' );
+ if ( this.currentFile ) {
+ this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
+ $label = $( [] );
+ $label = $label.add(
+ $( '<span>' )
+ .addClass( 'oo-ui-selectFileWidget-fileName' )
+ .text( this.currentFile.name )
+ );
+ if ( this.currentFile.type !== '' ) {
+ $label = $label.add(
+ $( '<span>' )
+ .addClass( 'oo-ui-selectFileWidget-fileType' )
+ .text( this.currentFile.type )
+ );
+ }
+ this.setLabel( $label );
+ } else {
+ this.$element.addClass( 'oo-ui-selectFileWidget-empty' );
+ this.setLabel( this.placeholder );
+ }
+ }
+};
+
+/**
+ * Add the input to the widget
+ *
+ * @private
+ */
+OO.ui.SelectFileWidget.prototype.addInput = function () {
+ if ( this.$input ) {
+ this.$input.remove();
+ }
+
+ if ( !this.isSupported ) {
+ this.$input = null;
+ return;
+ }
+
+ this.$input = $( '<input type="file">' );
+ this.$input.on( 'change', this.onFileSelectedHandler );
+ this.$input.attr( {
+ tabindex: -1
+ } );
+ if ( this.accept ) {
+ this.$input.attr( 'accept', this.accept.join( ', ' ) );
+ }
+ this.selectButton.$button.append( this.$input );
+};
+
+/**
+ * Determine if we should accept this file
+ *
+ * @private
+ * @param {string} File MIME type
+ * @return {boolean}
+ */
+OO.ui.SelectFileWidget.prototype.isAllowedType = function ( mimeType ) {
+ var i, mimeTest;
+
+ if ( !this.accept || !mimeType ) {
+ return true;
+ }
+
+ for ( i = 0; i < this.accept.length; i++ ) {
+ mimeTest = this.accept[ i ];
+ if ( mimeTest === mimeType ) {
+ return true;
+ } else if ( mimeTest.substr( -2 ) === '/*' ) {
+ mimeTest = mimeTest.substr( 0, mimeTest.length - 1 );
+ if ( mimeType.substr( 0, mimeTest.length ) === mimeTest ) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+};
+
+/**
+ * Handle file selection from the input
+ *
+ * @private
+ * @param {jQuery.Event} e
+ */
+OO.ui.SelectFileWidget.prototype.onFileSelected = function ( e ) {
+ var file = OO.getProp( e.target, 'files', 0 ) || null;
+
+ if ( file && !this.isAllowedType( file.type ) ) {
+ file = null;
+ }
+
+ this.setValue( file );
+ this.addInput();
+};
+
+/**
+ * Handle clear button click events.
+ *
+ * @private
+ */
+OO.ui.SelectFileWidget.prototype.onClearClick = function () {
+ this.setValue( null );
+ return false;
+};
+
+/**
+ * Handle key press events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ */
+OO.ui.SelectFileWidget.prototype.onKeyPress = function ( e ) {
+ if ( this.isSupported && !this.isDisabled() && this.$input &&
+ ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+ ) {
+ this.$input.click();
+ return false;
+ }
+};
+
+/**
+ * Handle drop target click events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ */
+OO.ui.SelectFileWidget.prototype.onDropTargetClick = function () {
+ if ( this.isSupported && !this.isDisabled() && this.$input ) {
+ this.$input.click();
+ return false;
+ }
+};
+
+/**
+ * Handle drag enter and over events
+ *
+ * @private
+ * @param {jQuery.Event} e Drag event
+ */
+OO.ui.SelectFileWidget.prototype.onDragEnterOrOver = function ( e ) {
+ var itemOrFile,
+ droppableFile = false,
+ dt = e.originalEvent.dataTransfer;
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ if ( this.isDisabled() || !this.isSupported ) {
+ this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
+ dt.dropEffect = 'none';
+ return false;
+ }
+
+ // DataTransferItem and File both have a type property, but in Chrome files
+ // have no information at this point.
+ itemOrFile = OO.getProp( dt, 'items', 0 ) || OO.getProp( dt, 'files', 0 );
+ if ( itemOrFile ) {
+ if ( this.isAllowedType( itemOrFile.type ) ) {
+ droppableFile = true;
+ }
+ // dt.types is Array-like, but not an Array
+ } else if ( Array.prototype.indexOf.call( OO.getProp( dt, 'types' ) || [], 'Files' ) !== -1 ) {
+ // File information is not available at this point for security so just assume
+ // it is acceptable for now.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=640534
+ droppableFile = true;
+ }
+
+ this.$element.toggleClass( 'oo-ui-selectFileWidget-canDrop', droppableFile );
+ if ( !droppableFile ) {
+ dt.dropEffect = 'none';
+ }
+
+ return false;
+};
+
+/**
+ * Handle drag leave events
+ *
+ * @private
+ * @param {jQuery.Event} e Drag event
+ */
+OO.ui.SelectFileWidget.prototype.onDragLeave = function () {
+ this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
+};
+
+/**
+ * Handle drop events
+ *
+ * @private
+ * @param {jQuery.Event} e Drop event
+ */
+OO.ui.SelectFileWidget.prototype.onDrop = function ( e ) {
+ var file = null,
+ dt = e.originalEvent.dataTransfer;
+
+ e.preventDefault();
+ e.stopPropagation();
+ this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
+
+ if ( this.isDisabled() || !this.isSupported ) {
+ return false;
+ }
+
+ file = OO.getProp( dt, 'files', 0 );
+ if ( file && !this.isAllowedType( file.type ) ) {
+ file = null;
+ }
+ if ( file ) {
+ this.setValue( file );
+ }
+
+ return false;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.SelectFileWidget.prototype.setDisabled = function ( disabled ) {
+ OO.ui.SelectFileWidget.parent.prototype.setDisabled.call( this, disabled );
+ if ( this.selectButton ) {
+ this.selectButton.setDisabled( disabled );
+ }
+ if ( this.clearButton ) {
+ this.clearButton.setDisabled( disabled );
+ }
+ return this;
+};
+
+/**
+ * Progress bars visually display the status of an operation, such as a download,
+ * and can be either determinate or indeterminate:
+ *
+ * - **determinate** process bars show the percent of an operation that is complete.
+ *
+ * - **indeterminate** process bars use a visual display of motion to indicate that an operation
+ * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
+ * not use percentages.
+ *
+ * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
+ *
+ * @example
+ * // Examples of determinate and indeterminate progress bars.
+ * var progressBar1 = new OO.ui.ProgressBarWidget( {
+ * progress: 33
+ * } );
+ * var progressBar2 = new OO.ui.ProgressBarWidget();
+ *
+ * // Create a FieldsetLayout to layout progress bars
+ * var fieldset = new OO.ui.FieldsetLayout;
+ * fieldset.addItems( [
+ * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
+ * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
+ * ] );
+ * $( 'body' ).append( fieldset.$element );
+ *
+ * @class
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
+ * To create a determinate progress bar, specify a number that reflects the initial percent complete.
+ * By default, the progress bar is indeterminate.
+ */
+OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.ProgressBarWidget.parent.call( this, config );
+
+ // Properties
+ this.$bar = $( '<div>' );
+ this.progress = null;
+
+ // Initialization
+ this.setProgress( config.progress !== undefined ? config.progress : false );
+ this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
+ this.$element
+ .attr( {
+ role: 'progressbar',
+ 'aria-valuemin': 0,
+ 'aria-valuemax': 100
+ } )
+ .addClass( 'oo-ui-progressBarWidget' )
+ .append( this.$bar );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
+
+/* Static Properties */
+
+OO.ui.ProgressBarWidget.static.tagName = 'div';
+
+/* Methods */
+
+/**
+ * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
+ *
+ * @return {number|boolean} Progress percent
+ */
+OO.ui.ProgressBarWidget.prototype.getProgress = function () {
+ return this.progress;
+};
+
+/**
+ * Set the percent of the process completed or `false` for an indeterminate process.
+ *
+ * @param {number|boolean} progress Progress percent or `false` for indeterminate
+ */
+OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
+ this.progress = progress;
+
+ if ( progress !== false ) {
+ this.$bar.css( 'width', this.progress + '%' );
+ this.$element.attr( 'aria-valuenow', this.progress );
+ } else {
+ this.$bar.css( 'width', '' );
+ this.$element.removeAttr( 'aria-valuenow' );
+ }
+ this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
+};
+
+/**
+ * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query,
+ * and a menu of search results, which is displayed beneath the query
+ * field. Unlike {@link OO.ui.mixin.LookupElement lookup menus}, search result menus are always visible to the user.
+ * Users can choose an item from the menu or type a query into the text field to search for a matching result item.
+ * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window.
+ *
+ * Each time the query is changed, the search result menu is cleared and repopulated. Please see
+ * the [OOjs UI demos][1] for an example.
+ *
+ * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/#dialogs-mediawiki-vector-ltr
+ *
+ * @class
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string|jQuery} [placeholder] Placeholder text for query input
+ * @cfg {string} [value] Initial query value
+ */
+OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.SearchWidget.parent.call( this, config );
+
+ // Properties
+ this.query = new OO.ui.TextInputWidget( {
+ icon: 'search',
+ placeholder: config.placeholder,
+ value: config.value
+ } );
+ this.results = new OO.ui.SelectWidget();
+ this.$query = $( '<div>' );
+ this.$results = $( '<div>' );
+
+ // Events
+ this.query.connect( this, {
+ change: 'onQueryChange',
+ enter: 'onQueryEnter'
+ } );
+ this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
+
+ // Initialization
+ this.$query
+ .addClass( 'oo-ui-searchWidget-query' )
+ .append( this.query.$element );
+ this.$results
+ .addClass( 'oo-ui-searchWidget-results' )
+ .append( this.results.$element );
+ this.$element
+ .addClass( 'oo-ui-searchWidget' )
+ .append( this.$results, this.$query );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
+
+/* Methods */
+
+/**
+ * Handle query key down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
+ var highlightedItem, nextItem,
+ dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
+
+ if ( dir ) {
+ highlightedItem = this.results.getHighlightedItem();
+ if ( !highlightedItem ) {
+ highlightedItem = this.results.getSelectedItem();
+ }
+ nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
+ this.results.highlightItem( nextItem );
+ nextItem.scrollElementIntoView();
+ }
+};
+
+/**
+ * Handle select widget select events.
+ *
+ * Clears existing results. Subclasses should repopulate items according to new query.
+ *
+ * @private
+ * @param {string} value New value
+ */
+OO.ui.SearchWidget.prototype.onQueryChange = function () {
+ // Reset
+ this.results.clearItems();
+};
+
+/**
+ * Handle select widget enter key events.
+ *
+ * Chooses highlighted item.
+ *
+ * @private
+ * @param {string} value New value
+ */
+OO.ui.SearchWidget.prototype.onQueryEnter = function () {
+ var highlightedItem = this.results.getHighlightedItem();
+ if ( highlightedItem ) {
+ this.results.chooseItem( highlightedItem );
+ }
+};
+
+/**
+ * Get the query input.
+ *
+ * @return {OO.ui.TextInputWidget} Query input
+ */
+OO.ui.SearchWidget.prototype.getQuery = function () {
+ return this.query;
+};
+
+/**
+ * Get the search results menu.
+ *
+ * @return {OO.ui.SelectWidget} Menu of search results
+ */
+OO.ui.SearchWidget.prototype.getResults = function () {
+ return this.results;
+};
+
+/**
+ * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
+ * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
+ * (to adjust the value in increments) to allow the user to enter a number.
+ *
+ * @example
+ * // Example: A NumberInputWidget.
+ * var numberInput = new OO.ui.NumberInputWidget( {
+ * label: 'NumberInputWidget',
+ * input: { value: 5, min: 1, max: 10 }
+ * } );
+ * $( 'body' ).append( numberInput.$element );
+ *
+ * @class
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
+ * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
+ * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
+ * @cfg {boolean} [isInteger=false] Whether the field accepts only integer values.
+ * @cfg {number} [min=-Infinity] Minimum allowed value
+ * @cfg {number} [max=Infinity] Maximum allowed value
+ * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
+ * @cfg {number|null} [pageStep] Delta when using the page-up/page-down keys. Defaults to 10 times #step.
+ */
+OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
+ // Configuration initialization
+ config = $.extend( {
+ isInteger: false,
+ min: -Infinity,
+ max: Infinity,
+ step: 1,
+ pageStep: null
+ }, config );
+
+ // Parent constructor
+ OO.ui.NumberInputWidget.parent.call( this, config );
+
+ // Properties
+ this.input = new OO.ui.TextInputWidget( $.extend(
+ {
+ disabled: this.isDisabled()
+ },
+ config.input
+ ) );
+ this.minusButton = new OO.ui.ButtonWidget( $.extend(
+ {
+ disabled: this.isDisabled(),
+ tabIndex: -1
+ },
+ config.minusButton,
+ {
+ classes: [ 'oo-ui-numberInputWidget-minusButton' ],
+ label: '−'
+ }
+ ) );
+ this.plusButton = new OO.ui.ButtonWidget( $.extend(
+ {
+ disabled: this.isDisabled(),
+ tabIndex: -1
+ },
+ config.plusButton,
+ {
+ classes: [ 'oo-ui-numberInputWidget-plusButton' ],
+ label: '+'
+ }
+ ) );
+
+ // Events
+ this.input.connect( this, {
+ change: this.emit.bind( this, 'change' ),
+ enter: this.emit.bind( this, 'enter' )
+ } );
+ this.input.$input.on( {
+ keydown: this.onKeyDown.bind( this ),
+ 'wheel mousewheel DOMMouseScroll': this.onWheel.bind( this )
+ } );
+ this.plusButton.connect( this, {
+ click: [ 'onButtonClick', +1 ]
+ } );
+ this.minusButton.connect( this, {
+ click: [ 'onButtonClick', -1 ]
+ } );
+
+ // Initialization
+ this.setIsInteger( !!config.isInteger );
+ this.setRange( config.min, config.max );
+ this.setStep( config.step, config.pageStep );
+
+ this.$field = $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' )
+ .append(
+ this.minusButton.$element,
+ this.input.$element,
+ this.plusButton.$element
+ );
+ this.$element.addClass( 'oo-ui-numberInputWidget' ).append( this.$field );
+ this.input.setValidation( this.validateNumber.bind( this ) );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.Widget );
+
+/* Events */
+
+/**
+ * A `change` event is emitted when the value of the input changes.
+ *
+ * @event change
+ */
+
+/**
+ * An `enter` event is emitted when the user presses 'enter' inside the text box.
+ *
+ * @event enter
+ */
+
+/* Methods */
+
+/**
+ * Set whether only integers are allowed
+ * @param {boolean} flag
+ */
+OO.ui.NumberInputWidget.prototype.setIsInteger = function ( flag ) {
+ this.isInteger = !!flag;
+ this.input.setValidityFlag();
+};
+
+/**
+ * Get whether only integers are allowed
+ * @return {boolean} Flag value
+ */
+OO.ui.NumberInputWidget.prototype.getIsInteger = function () {
+ return this.isInteger;
+};
+
+/**
+ * Set the range of allowed values
+ * @param {number} min Minimum allowed value
+ * @param {number} max Maximum allowed value
+ */
+OO.ui.NumberInputWidget.prototype.setRange = function ( min, max ) {
+ if ( min > max ) {
+ throw new Error( 'Minimum (' + min + ') must not be greater than maximum (' + max + ')' );
+ }
+ this.min = min;
+ this.max = max;
+ this.input.setValidityFlag();
+};
+
+/**
+ * Get the current range
+ * @return {number[]} Minimum and maximum values
+ */
+OO.ui.NumberInputWidget.prototype.getRange = function () {
+ return [ this.min, this.max ];
+};
+
+/**
+ * Set the stepping deltas
+ * @param {number} step Normal step
+ * @param {number|null} pageStep Page step. If null, 10 * step will be used.
+ */
+OO.ui.NumberInputWidget.prototype.setStep = function ( step, pageStep ) {
+ if ( step <= 0 ) {
+ throw new Error( 'Step value must be positive' );
+ }
+ if ( pageStep === null ) {
+ pageStep = step * 10;
+ } else if ( pageStep <= 0 ) {
+ throw new Error( 'Page step value must be positive' );
+ }
+ this.step = step;
+ this.pageStep = pageStep;
+};
+
+/**
+ * Get the current stepping values
+ * @return {number[]} Step and page step
+ */
+OO.ui.NumberInputWidget.prototype.getStep = function () {
+ return [ this.step, this.pageStep ];
+};
+
+/**
+ * Get the current value of the widget
+ * @return {string}
+ */
+OO.ui.NumberInputWidget.prototype.getValue = function () {
+ return this.input.getValue();
+};
+
+/**
+ * Get the current value of the widget as a number
+ * @return {number} May be NaN, or an invalid number
+ */
+OO.ui.NumberInputWidget.prototype.getNumericValue = function () {
+ return +this.input.getValue();
+};
+
+/**
+ * Set the value of the widget
+ * @param {string} value Invalid values are allowed
+ */
+OO.ui.NumberInputWidget.prototype.setValue = function ( value ) {
+ this.input.setValue( value );
+};
+
+/**
+ * Adjust the value of the widget
+ * @param {number} delta Adjustment amount
+ */
+OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
+ var n, v = this.getNumericValue();
+
+ delta = +delta;
+ if ( isNaN( delta ) || !isFinite( delta ) ) {
+ throw new Error( 'Delta must be a finite number' );
+ }
+
+ if ( isNaN( v ) ) {
+ n = 0;
+ } else {
+ n = v + delta;
+ n = Math.max( Math.min( n, this.max ), this.min );
+ if ( this.isInteger ) {
+ n = Math.round( n );
+ }
+ }
+
+ if ( n !== v ) {
+ this.setValue( n );
+ }
+};
+
+/**
+ * Validate input
+ * @private
+ * @param {string} value Field value
+ * @return {boolean}
+ */
+OO.ui.NumberInputWidget.prototype.validateNumber = function ( value ) {
+ var n = +value;
+ if ( isNaN( n ) || !isFinite( n ) ) {
+ return false;
+ }
+
+ /*jshint bitwise: false */
+ if ( this.isInteger && ( n | 0 ) !== n ) {
+ return false;
+ }
+ /*jshint bitwise: true */
+
+ if ( n < this.min || n > this.max ) {
+ return false;
+ }
+
+ return true;
+};
+
+/**
+ * Handle mouse click events.
+ *
+ * @private
+ * @param {number} dir +1 or -1
+ */
+OO.ui.NumberInputWidget.prototype.onButtonClick = function ( dir ) {
+ this.adjustValue( dir * this.step );
+};
+
+/**
+ * Handle mouse wheel events.
+ *
+ * @private
+ * @param {jQuery.Event} event
+ */
+OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
+ var delta = 0;
+
+ // Standard 'wheel' event
+ if ( event.originalEvent.deltaMode !== undefined ) {
+ this.sawWheelEvent = true;
+ }
+ if ( event.originalEvent.deltaY ) {
+ delta = -event.originalEvent.deltaY;
+ } else if ( event.originalEvent.deltaX ) {
+ delta = event.originalEvent.deltaX;
+ }
+
+ // Non-standard events
+ if ( !this.sawWheelEvent ) {
+ if ( event.originalEvent.wheelDeltaX ) {
+ delta = -event.originalEvent.wheelDeltaX;
+ } else if ( event.originalEvent.wheelDeltaY ) {
+ delta = event.originalEvent.wheelDeltaY;
+ } else if ( event.originalEvent.wheelDelta ) {
+ delta = event.originalEvent.wheelDelta;
+ } else if ( event.originalEvent.detail ) {
+ delta = -event.originalEvent.detail;
+ }
+ }
+
+ if ( delta ) {
+ delta = delta < 0 ? -1 : 1;
+ this.adjustValue( delta * this.step );
+ }
+
+ return false;
+};
+
+/**
+ * Handle key down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) {
+ if ( !this.isDisabled() ) {
+ switch ( e.which ) {
+ case OO.ui.Keys.UP:
+ this.adjustValue( this.step );
+ return false;
+ case OO.ui.Keys.DOWN:
+ this.adjustValue( -this.step );
+ return false;
+ case OO.ui.Keys.PAGEUP:
+ this.adjustValue( this.pageStep );
+ return false;
+ case OO.ui.Keys.PAGEDOWN:
+ this.adjustValue( -this.pageStep );
+ return false;
+ }
+ }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
+ // Parent method
+ OO.ui.NumberInputWidget.parent.prototype.setDisabled.call( this, disabled );
+
+ if ( this.input ) {
+ this.input.setDisabled( this.isDisabled() );
+ }
+ if ( this.minusButton ) {
+ this.minusButton.setDisabled( this.isDisabled() );
+ }
+ if ( this.plusButton ) {
+ this.plusButton.setDisabled( this.isDisabled() );
+ }
+
+ return this;
+};
+
+}( OO ) );
--- /dev/null
+/*!
+ * OOjs UI v0.15.2
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: 2016-02-02T22:07:06Z
+ */
+@-webkit-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-moz-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-ms-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-o-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+.oo-ui-actionWidget.oo-ui-pendingElement-pending {
+ background-image: /* @embed */ url(themes/apex/images/textures/pending.gif);
+}
+.oo-ui-window {
+ background-color: transparent;
+ background-image: none;
+}
+.oo-ui-window-frame {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-window-content:focus {
+ outline: none;
+}
+.oo-ui-window-head,
+.oo-ui-window-foot {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.oo-ui-window-body {
+ margin: 0;
+ padding: 0;
+ background: none;
+}
+.oo-ui-window-overlay {
+ position: absolute;
+ top: 0;
+ /* @noflip */
+ left: 0;
+}
+.oo-ui-dialog-content > .oo-ui-window-head,
+.oo-ui-dialog-content > .oo-ui-window-body,
+.oo-ui-dialog-content > .oo-ui-window-foot {
+ position: absolute;
+ left: 0;
+ right: 0;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-dialog-content > .oo-ui-window-head {
+ overflow: hidden;
+ z-index: 1;
+ top: 0;
+}
+.oo-ui-dialog-content > .oo-ui-window-body {
+ overflow: auto;
+ z-index: 2;
+ top: 0;
+ bottom: 0;
+}
+.oo-ui-dialog-content > .oo-ui-window-foot {
+ overflow: hidden;
+ z-index: 1;
+ bottom: 0;
+}
+.oo-ui-dialog-content > .oo-ui-window-body {
+ box-shadow: 0 0 0.66em rgba(0, 0, 0, 0.25);
+}
+.oo-ui-messageDialog-actions-horizontal {
+ display: table;
+ table-layout: fixed;
+ width: 100%;
+}
+.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget {
+ display: table-cell;
+ width: 1%;
+}
+.oo-ui-messageDialog-actions-vertical {
+ display: block;
+}
+.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget {
+ position: relative;
+ text-align: center;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-buttonElement-button {
+ display: block;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-labelElement-label {
+ position: relative;
+ top: auto;
+ bottom: auto;
+ display: inline;
+ white-space: nowrap;
+}
+.oo-ui-messageDialog-content .oo-ui-window-body {
+ box-shadow: 0 0 0.33em rgba(0, 0, 0, 0.33);
+}
+.oo-ui-messageDialog-title,
+.oo-ui-messageDialog-message {
+ display: block;
+ text-align: center;
+}
+.oo-ui-messageDialog-title.oo-ui-labelElement,
+.oo-ui-messageDialog-message.oo-ui-labelElement {
+ padding-top: 0.5em;
+}
+.oo-ui-messageDialog-title {
+ font-size: 1.5em;
+ line-height: 1em;
+ color: #000000;
+}
+.oo-ui-messageDialog-message {
+ font-size: 0.9em;
+ line-height: 1.25em;
+ color: #666666;
+}
+.oo-ui-messageDialog-message-verbose {
+ font-size: 1.1em;
+ line-height: 1.5em;
+ text-align: left;
+}
+.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget {
+ border-right: 1px solid #e5e5e5;
+ margin: 0;
+}
+.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget:last-child {
+ border-right-width: 0;
+}
+.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget {
+ border-bottom: 1px solid #e5e5e5;
+ margin: 0;
+}
+.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget:last-child {
+ border-bottom-width: 0;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget {
+ height: 3.4em;
+ margin-right: 0;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
+ text-align: center;
+ line-height: 3.4em;
+ padding: 0 2em;
+}
+.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-flaggedElement-progressive:hover {
+ background-color: rgba(8, 126, 204, 0.05);
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-progressive:active {
+ background-color: rgba(8, 126, 204, 0.1);
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-progressive .oo-ui-labelElement-label {
+ font-weight: bold;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:hover {
+ background-color: rgba(118, 171, 54, 0.05);
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:active {
+ background-color: rgba(118, 171, 54, 0.1);
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:hover {
+ background-color: rgba(212, 83, 83, 0.05);
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:active {
+ background-color: rgba(212, 83, 83, 0.1);
+}
+.oo-ui-processDialog-location {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.oo-ui-processDialog-title {
+ display: inline;
+ padding: 0;
+}
+.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-processDialog-actions-primary {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+}
+.oo-ui-processDialog-actions-safe {
+ left: 0;
+}
+.oo-ui-processDialog-actions-primary {
+ right: 0;
+}
+.oo-ui-processDialog-errors {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 2;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+.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.33em rgba(0, 0, 0, 0.33);
+}
+.oo-ui-processDialog-navigation {
+ position: relative;
+ height: 3.4em;
+ padding: 0 1em;
+}
+.oo-ui-processDialog-location {
+ padding: 0.75em 0;
+ height: 1.875em;
+ cursor: default;
+ text-align: center;
+}
+.oo-ui-processDialog-title {
+ font-weight: bold;
+ line-height: 1.875em;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-buttonElement-button,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-buttonElement-button,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-buttonElement-button {
+ min-width: 1.875em;
+ min-height: 1.875em;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget .oo-ui-labelElement-label,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget .oo-ui-labelElement-label,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget .oo-ui-labelElement-label {
+ line-height: 1.875em;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-iconElement .oo-ui-iconElement-icon,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
+ margin-top: -0.125em;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-framed,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-framed,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-framed {
+ margin: 0.75em;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-framed .oo-ui-buttonElement-button,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-framed .oo-ui-buttonElement-button,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-framed .oo-ui-buttonElement-button {
+ padding: 0 1em;
+ vertical-align: middle;
+ margin: -1px;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-frameless {
+ margin: 0;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button {
+ padding: 0.75em 1em;
+ vertical-align: middle;
+}
+.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-flaggedElement-progressive:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-progressive:hover {
+ background-color: rgba(8, 126, 204, 0.05);
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggedElement-progressive:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-progressive:active {
+ background-color: rgba(8, 126, 204, 0.1);
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggedElement-progressive .oo-ui-labelElement-label,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-progressive .oo-ui-labelElement-label {
+ font-weight: bold;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:hover {
+ background-color: rgba(118, 171, 54, 0.05);
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:active {
+ background-color: rgba(118, 171, 54, 0.1);
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:hover {
+ background-color: rgba(212, 83, 83, 0.05);
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:active {
+ background-color: rgba(212, 83, 83, 0.1);
+}
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement {
+ margin-right: 0;
+}
+.oo-ui-processDialog > .oo-ui-window-frame {
+ min-height: 5em;
+}
+.oo-ui-processDialog-errors {
+ background-color: rgba(255, 255, 255, 0.9);
+ padding: 3em 3em 1.5em 3em;
+ text-align: center;
+}
+.oo-ui-processDialog-errors .oo-ui-buttonWidget {
+ margin: 2em 1em 2em 1em;
+}
+.oo-ui-processDialog-errors-title {
+ font-size: 1.5em;
+ color: #000000;
+ margin-bottom: 2em;
+}
+.oo-ui-processDialog-error {
+ text-align: left;
+ margin: 1em;
+ padding: 1em;
+ border: 1px solid #ff9e9e;
+ background-color: #fff7f7;
+ border-radius: 0.25em;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog {
+ position: fixed;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-active {
+ width: auto;
+ height: auto;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ padding: 1em;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup > .oo-ui-window-frame {
+ position: absolute;
+ right: 0;
+ left: 0;
+ margin: auto;
+ overflow: hidden;
+ max-width: 100%;
+ max-height: 100%;
+}
+.oo-ui-windowManager-fullscreen > .oo-ui-dialog > .oo-ui-window-frame {
+ width: 100%;
+ height: 100%;
+ top: 0;
+ bottom: 0;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog {
+ background-color: rgba(255, 255, 255, 0.5);
+ opacity: 0;
+ -webkit-transition: opacity 250ms ease;
+ -moz-transition: opacity 250ms ease;
+ transition: opacity 250ms ease;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog > .oo-ui-window-frame {
+ background-color: #ffffff;
+ opacity: 0;
+ -webkit-transform: scale(0.5);
+ -moz-transform: scale(0.5);
+ -ms-transform: scale(0.5);
+ transform: scale(0.5);
+ -webkit-transition: all 250ms ease;
+ -moz-transition: all 250ms ease;
+ transition: all 250ms ease;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup {
+ opacity: 1;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-ready > .oo-ui-window-frame {
+ opacity: 1;
+ -webkit-transform: scale(1);
+ -moz-transform: scale(1);
+ -ms-transform: scale(1);
+ transform: scale(1);
+}
+.oo-ui-windowManager-modal.oo-ui-windowManager-floating > .oo-ui-dialog > .oo-ui-window-frame {
+ top: 1em;
+ bottom: 1em;
+ border: 1px solid #cccccc;
+ border-radius: 0.5em;
+ box-shadow: 0 0.2em 1em rgba(0, 0, 0, 0.3);
+}
--- /dev/null
+/*!
+ * OOjs UI v0.15.2
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: 2016-02-02T22:07:06Z
+ */
+@-webkit-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@-moz-keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+@keyframes oo-ui-progressBarWidget-slide {
+ from {
+ margin-left: -40%;
+ }
+ to {
+ margin-left: 100%;
+ }
+}
+.oo-ui-window {
+ background: transparent;
+}
+.oo-ui-window-frame {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-window-content:focus {
+ outline: none;
+}
+.oo-ui-window-head,
+.oo-ui-window-foot {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.oo-ui-window-body {
+ margin: 0;
+ padding: 0;
+ background: none;
+}
+.oo-ui-window-overlay {
+ position: absolute;
+ top: 0;
+ /* @noflip */
+ left: 0;
+}
+.oo-ui-dialog-content > .oo-ui-window-head,
+.oo-ui-dialog-content > .oo-ui-window-body,
+.oo-ui-dialog-content > .oo-ui-window-foot {
+ position: absolute;
+ left: 0;
+ right: 0;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.oo-ui-dialog-content > .oo-ui-window-head {
+ overflow: hidden;
+ z-index: 1;
+ top: 0;
+}
+.oo-ui-dialog-content > .oo-ui-window-body {
+ overflow: auto;
+ z-index: 2;
+ top: 0;
+ bottom: 0;
+}
+.oo-ui-dialog-content > .oo-ui-window-foot {
+ overflow: hidden;
+ z-index: 1;
+ bottom: 0;
+}
+.oo-ui-dialog-content > .oo-ui-window-body {
+ outline: 1px solid #aaaaaa;
+}
+.oo-ui-messageDialog-actions-horizontal {
+ display: table;
+ table-layout: fixed;
+ width: 100%;
+}
+.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget {
+ display: table-cell;
+ width: 1%;
+}
+.oo-ui-messageDialog-actions-vertical {
+ display: block;
+}
+.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget {
+ position: relative;
+ text-align: center;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-buttonElement-button {
+ display: block;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget .oo-ui-labelElement-label {
+ position: relative;
+ top: auto;
+ bottom: auto;
+ display: inline;
+ white-space: nowrap;
+}
+.oo-ui-messageDialog-title,
+.oo-ui-messageDialog-message {
+ display: block;
+ text-align: center;
+}
+.oo-ui-messageDialog-title.oo-ui-labelElement,
+.oo-ui-messageDialog-message.oo-ui-labelElement {
+ padding-top: 0.5em;
+}
+.oo-ui-messageDialog-title {
+ font-size: 1.5em;
+ line-height: 1em;
+ color: #000000;
+}
+.oo-ui-messageDialog-message {
+ font-size: 0.9em;
+ line-height: 1.25em;
+ color: #555555;
+}
+.oo-ui-messageDialog-message-verbose {
+ font-size: 1.1em;
+ line-height: 1.5em;
+ text-align: left;
+}
+.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget {
+ border-right: 1px solid #e5e5e5;
+ margin: 0;
+}
+.oo-ui-messageDialog-actions-horizontal .oo-ui-actionWidget:last-child {
+ border-right-width: 0;
+}
+.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget {
+ border-bottom: 1px solid #e5e5e5;
+ margin: 0;
+}
+.oo-ui-messageDialog-actions-vertical .oo-ui-actionWidget:last-child {
+ border-bottom-width: 0;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget {
+ height: 3.4em;
+ margin-right: 0;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget:last-child {
+ margin-right: 0;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-labelElement .oo-ui-labelElement-label {
+ text-align: center;
+ line-height: 3.4em;
+ padding: 0 2em;
+}
+.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-flaggedElement-progressive:hover {
+ background-color: rgba(8, 126, 204, 0.05);
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-progressive:active {
+ background-color: rgba(8, 126, 204, 0.1);
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-progressive .oo-ui-labelElement-label {
+ font-weight: bold;
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:hover {
+ background-color: rgba(118, 171, 54, 0.05);
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-constructive:active {
+ background-color: rgba(118, 171, 54, 0.1);
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:hover {
+ background-color: rgba(212, 83, 83, 0.05);
+}
+.oo-ui-messageDialog-actions .oo-ui-actionWidget.oo-ui-flaggedElement-destructive:active {
+ background-color: rgba(212, 83, 83, 0.1);
+}
+.oo-ui-processDialog-location {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.oo-ui-processDialog-title {
+ display: inline;
+ padding: 0;
+}
+.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-processDialog-actions-primary {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+}
+.oo-ui-processDialog-actions-safe {
+ left: 0;
+}
+.oo-ui-processDialog-actions-primary {
+ right: 0;
+}
+.oo-ui-processDialog-errors {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 2;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+.oo-ui-processDialog-content .oo-ui-window-head {
+ height: 3.4em;
+}
+.oo-ui-processDialog-content .oo-ui-window-body {
+ top: 3.4em;
+ outline: 1px solid rgba(0, 0, 0, 0.2);
+}
+.oo-ui-processDialog-navigation {
+ position: relative;
+ height: 3.4em;
+ padding: 0 1em;
+}
+.oo-ui-processDialog-location {
+ padding: 0.75em 0;
+ height: 1.875em;
+ cursor: default;
+ text-align: center;
+}
+.oo-ui-processDialog-title {
+ font-weight: bold;
+ line-height: 1.875em;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-framed,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-framed,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-framed {
+ margin: 0.5em;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-frameless {
+ margin: 0;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button {
+ padding: 0.75em 1em;
+ vertical-align: middle;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-labelElement-label,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-labelElement-label,
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement-frameless .oo-ui-labelElement-label {
+ line-height: 1.875em;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless:active {
+ background-color: rgba(0, 0, 0, 0.1);
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive:hover {
+ background-color: rgba(8, 126, 204, 0.05);
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive:active {
+ background-color: rgba(8, 126, 204, 0.1);
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive .oo-ui-labelElement-label,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-progressive .oo-ui-labelElement-label {
+ font-weight: bold;
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-constructive:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-constructive:hover {
+ background-color: rgba(118, 171, 54, 0.05);
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-constructive:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-constructive:active {
+ background-color: rgba(118, 171, 54, 0.1);
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-destructive:hover,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-destructive:hover {
+ background-color: rgba(212, 83, 83, 0.05);
+}
+.oo-ui-processDialog-actions-safe .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-destructive:active,
+.oo-ui-processDialog-actions-primary .oo-ui-actionWidget.oo-ui-buttonElement-frameless.oo-ui-flaggedElement-destructive:active {
+ background-color: rgba(212, 83, 83, 0.1);
+}
+.oo-ui-processDialog-actions-other .oo-ui-actionWidget.oo-ui-buttonElement {
+ margin-right: 0;
+}
+.oo-ui-processDialog > .oo-ui-window-frame {
+ min-height: 5em;
+}
+.oo-ui-processDialog-errors {
+ background-color: rgba(255, 255, 255, 0.9);
+ padding: 3em 3em 1.5em 3em;
+ text-align: center;
+}
+.oo-ui-processDialog-errors .oo-ui-buttonWidget {
+ margin: 2em 1em 2em 1em;
+}
+.oo-ui-processDialog-errors-title {
+ font-size: 1.5em;
+ color: #000000;
+ margin-bottom: 2em;
+}
+.oo-ui-processDialog-error {
+ text-align: left;
+ margin: 1em;
+ padding: 1em;
+ border: 1px solid #ff9e9e;
+ background-color: #fff7f7;
+ border-radius: 2px;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog {
+ position: fixed;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-active {
+ width: auto;
+ height: auto;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ padding: 1em;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup > .oo-ui-window-frame {
+ position: absolute;
+ right: 0;
+ left: 0;
+ margin: auto;
+ overflow: hidden;
+ max-width: 100%;
+ max-height: 100%;
+}
+.oo-ui-windowManager-fullscreen > .oo-ui-dialog > .oo-ui-window-frame {
+ width: 100%;
+ height: 100%;
+ top: 0;
+ bottom: 0;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog {
+ background-color: rgba(255, 255, 255, 0.5);
+ opacity: 0;
+ -webkit-transition: opacity 250ms ease;
+ -moz-transition: opacity 250ms ease;
+ transition: opacity 250ms ease;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog > .oo-ui-window-frame {
+ background-color: #ffffff;
+ opacity: 0;
+ -webkit-transform: scale(0.5);
+ -moz-transform: scale(0.5);
+ -ms-transform: scale(0.5);
+ transform: scale(0.5);
+ -webkit-transition: all 250ms ease;
+ -moz-transition: all 250ms ease;
+ transition: all 250ms ease;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-setup {
+ opacity: 1;
+}
+.oo-ui-windowManager-modal > .oo-ui-dialog.oo-ui-window-ready > .oo-ui-window-frame {
+ opacity: 1;
+ -webkit-transform: scale(1);
+ -moz-transform: scale(1);
+ -ms-transform: scale(1);
+ transform: scale(1);
+}
+.oo-ui-windowManager-modal.oo-ui-windowManager-floating > .oo-ui-dialog > .oo-ui-window-frame {
+ top: 1em;
+ bottom: 1em;
+ border: 1px solid #aaaaaa;
+ border-radius: 2px;
+ box-shadow: 0 0.15em 0 0 rgba(0, 0, 0, 0.15);
+}
--- /dev/null
+/*!
+ * OOjs UI v0.15.2
+ * https://www.mediawiki.org/wiki/OOjs_UI
+ *
+ * Copyright 2011–2016 OOjs UI Team and other contributors.
+ * Released under the MIT license
+ * http://oojs.mit-license.org
+ *
+ * Date: 2016-02-02T22:07:00Z
+ */
+( function ( OO ) {
+
+'use strict';
+
+/**
+ * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
+ * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
+ * of the actions.
+ *
+ * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
+ * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
+ * and examples.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
+ *
+ * @class
+ * @extends OO.ui.ButtonWidget
+ * @mixins OO.ui.mixin.PendingElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
+ * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
+ * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
+ * for more information about setting modes.
+ * @cfg {boolean} [framed=false] Render the action button with a frame
+ */
+OO.ui.ActionWidget = function OoUiActionWidget( config ) {
+ // Configuration initialization
+ config = $.extend( { framed: false }, config );
+
+ // Parent constructor
+ OO.ui.ActionWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.PendingElement.call( this, config );
+
+ // Properties
+ this.action = config.action || '';
+ this.modes = config.modes || [];
+ this.width = 0;
+ this.height = 0;
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-actionWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
+OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
+
+/* Events */
+
+/**
+ * A resize event is emitted when the size of the widget changes.
+ *
+ * @event resize
+ */
+
+/* Methods */
+
+/**
+ * Check if the action is configured to be available in the specified `mode`.
+ *
+ * @param {string} mode Name of mode
+ * @return {boolean} The action is configured with the mode
+ */
+OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
+ return this.modes.indexOf( mode ) !== -1;
+};
+
+/**
+ * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
+ *
+ * @return {string}
+ */
+OO.ui.ActionWidget.prototype.getAction = function () {
+ return this.action;
+};
+
+/**
+ * Get the symbolic name of the mode or modes for which the action is configured to be available.
+ *
+ * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
+ * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
+ * are hidden.
+ *
+ * @return {string[]}
+ */
+OO.ui.ActionWidget.prototype.getModes = function () {
+ return this.modes.slice();
+};
+
+/**
+ * Emit a resize event if the size has changed.
+ *
+ * @private
+ * @chainable
+ */
+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;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ActionWidget.prototype.setIcon = function () {
+ // Mixin method
+ OO.ui.mixin.IconElement.prototype.setIcon.apply( this, arguments );
+ this.propagateResize();
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ActionWidget.prototype.setLabel = function () {
+ // Mixin method
+ OO.ui.mixin.LabelElement.prototype.setLabel.apply( this, arguments );
+ this.propagateResize();
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ActionWidget.prototype.setFlags = function () {
+ // Mixin method
+ OO.ui.mixin.FlaggedElement.prototype.setFlags.apply( this, arguments );
+ this.propagateResize();
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ActionWidget.prototype.clearFlags = function () {
+ // Mixin method
+ OO.ui.mixin.FlaggedElement.prototype.clearFlags.apply( this, arguments );
+ this.propagateResize();
+
+ return this;
+};
+
+/**
+ * Toggle the visibility of the action button.
+ *
+ * @param {boolean} [show] Show button, omit to toggle visibility
+ * @chainable
+ */
+OO.ui.ActionWidget.prototype.toggle = function () {
+ // Parent method
+ OO.ui.ActionWidget.parent.prototype.toggle.apply( this, arguments );
+ this.propagateResize();
+
+ return this;
+};
+
+/**
+ * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them.
+ * Actions can be made available for specific contexts (modes) and circumstances
+ * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
+ *
+ * ActionSets contain two types of actions:
+ *
+ * - Special: Special actions are the first visible actions with special flags, such as 'safe' and 'primary', the default special flags. Additional special flags can be configured in subclasses with the static #specialFlags property.
+ * - Other: Other actions include all non-special visible actions.
+ *
+ * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
+ *
+ * @example
+ * // Example: An action set used in a process dialog
+ * function MyProcessDialog( config ) {
+ * MyProcessDialog.parent.call( this, config );
+ * }
+ * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
+ * MyProcessDialog.static.title = 'An action set in a process dialog';
+ * // An action set that uses modes ('edit' and 'help' mode, in this example).
+ * MyProcessDialog.static.actions = [
+ * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
+ * { action: 'help', modes: 'edit', label: 'Help' },
+ * { modes: 'edit', label: 'Cancel', flags: 'safe' },
+ * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
+ * ];
+ *
+ * MyProcessDialog.prototype.initialize = function () {
+ * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
+ * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
+ * this.panel1.$element.append( '<p>This dialog uses an action set (continue, help, cancel, back) configured with modes. This is edit mode. Click \'help\' to see help mode.</p>' );
+ * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
+ * this.panel2.$element.append( '<p>This is help mode. Only the \'back\' action widget is configured to be visible here. Click \'back\' to return to \'edit\' mode.</p>' );
+ * this.stackLayout = new OO.ui.StackLayout( {
+ * items: [ this.panel1, this.panel2 ]
+ * } );
+ * this.$body.append( this.stackLayout.$element );
+ * };
+ * MyProcessDialog.prototype.getSetupProcess = function ( data ) {
+ * return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data )
+ * .next( function () {
+ * this.actions.setMode( 'edit' );
+ * }, this );
+ * };
+ * MyProcessDialog.prototype.getActionProcess = function ( action ) {
+ * if ( action === 'help' ) {
+ * this.actions.setMode( 'help' );
+ * this.stackLayout.setItem( this.panel2 );
+ * } else if ( action === 'back' ) {
+ * this.actions.setMode( 'edit' );
+ * this.stackLayout.setItem( this.panel1 );
+ * } else if ( action === 'continue' ) {
+ * var dialog = this;
+ * return new OO.ui.Process( function () {
+ * dialog.close();
+ * } );
+ * }
+ * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
+ * };
+ * MyProcessDialog.prototype.getBodyHeight = function () {
+ * return this.panel1.$element.outerHeight( true );
+ * };
+ * var windowManager = new OO.ui.WindowManager();
+ * $( 'body' ).append( windowManager.$element );
+ * var dialog = new MyProcessDialog( {
+ * size: 'medium'
+ * } );
+ * windowManager.addWindows( [ dialog ] );
+ * windowManager.openWindow( dialog );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
+ *
+ * @abstract
+ * @class
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.ActionSet = function OoUiActionSet( config ) {
+ // Configuration initialization
+ 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 the flags used to identify special actions. Special actions are displayed in the
+ * header of a {@link OO.ui.ProcessDialog process dialog}.
+ * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
+ *
+ * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
+ *
+ * @abstract
+ * @static
+ * @inheritable
+ * @property {string}
+ */
+OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
+
+/* Events */
+
+/**
+ * @event click
+ *
+ * A 'click' event is emitted when an action is clicked.
+ *
+ * @param {OO.ui.ActionWidget} action Action that was clicked
+ */
+
+/**
+ * @event resize
+ *
+ * A 'resize' event is emitted when an action widget is resized.
+ *
+ * @param {OO.ui.ActionWidget} action Action that was resized
+ */
+
+/**
+ * @event add
+ *
+ * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
+ *
+ * @param {OO.ui.ActionWidget[]} added Actions added
+ */
+
+/**
+ * @event remove
+ *
+ * A 'remove' event is emitted when actions are {@link #method-remove removed}
+ * or {@link #clear cleared}.
+ *
+ * @param {OO.ui.ActionWidget[]} added Actions removed
+ */
+
+/**
+ * @event change
+ *
+ * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
+ * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
+ *
+ */
+
+/* Methods */
+
+/**
+ * Handle action change events.
+ *
+ * @private
+ * @fires change
+ */
+OO.ui.ActionSet.prototype.onActionChange = function () {
+ this.organized = false;
+ if ( this.changing ) {
+ this.changed = true;
+ } else {
+ this.emit( 'change' );
+ }
+};
+
+/**
+ * Check if an 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 action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
+ * or ‘disabled’.
+ *
+ * @param {Object} [filters] Filters to use, omit to get all actions
+ * @param {string|string[]} [filters.actions] Actions that action widgets must have
+ * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
+ * @param {string|string[]} [filters.modes] Modes that action widgets must have
+ * @param {boolean} [filters.visible] Action widgets must be visible
+ * @param {boolean} [filters.disabled] Action widgets must be disabled
+ * @return {OO.ui.ActionWidget[]} Action widgets 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 action widgets with special flags, such as 'safe' and 'primary'.
+ * Special flags can be configured in subclasses by changing the static #specialFlags property.
+ *
+ * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
+ */
+OO.ui.ActionSet.prototype.getSpecial = function () {
+ this.organize();
+ return $.extend( {}, this.special );
+};
+
+/**
+ * Get 'other' actions.
+ *
+ * Other actions include all non-special visible action widgets.
+ *
+ * @return {OO.ui.ActionWidget[]} 'Other' action widgets
+ */
+OO.ui.ActionSet.prototype.getOthers = function () {
+ this.organize();
+ return this.others.slice();
+};
+
+/**
+ * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
+ * to be available in the specified mode will be made visible. All other actions will be hidden.
+ *
+ * @param {string} mode The mode. Only actions configured to be available in the specified
+ * mode will be made visible.
+ * @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;
+};
+
+/**
+ * Set the abilities of the specified actions.
+ *
+ * Action widgets that are configured with the specified actions will be enabled
+ * or disabled based on the boolean values specified in the `actions`
+ * parameter.
+ *
+ * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
+ * values that indicate whether or not the action should be enabled.
+ * @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 to determine 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 action widgets to the action set.
+ *
+ * @param {OO.ui.ActionWidget[]} actions Action widgets 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 action widgets from the set.
+ *
+ * To remove all actions, you may wish to use the #clear method instead.
+ *
+ * @param {OO.ui.ActionWidget[]} actions Action widgets 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 action widets from the set.
+ *
+ * To remove only specified actions, use the {@link #method-remove remove} method instead.
+ *
+ * @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 whenever 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 categories
+ 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;
+};
+
+/**
+ * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
+ * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
+ * appearance and functionality of the error interface.
+ *
+ * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
+ * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
+ * that initiated the failed process will be disabled.
+ *
+ * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
+ * process again.
+ *
+ * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors
+ *
+ * @class
+ *
+ * @constructor
+ * @param {string|jQuery} message Description of error
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [recoverable=true] Error is recoverable.
+ * By default, errors are recoverable, and users can try the process again.
+ * @cfg {boolean} [warning=false] Error is a warning.
+ * If the error is a warning, the error interface will include a
+ * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
+ * is not triggered a second time if the user chooses to continue.
+ */
+OO.ui.Error = function OoUiError( message, config ) {
+ // Allow passing positional parameters inside the config object
+ if ( OO.isPlainObject( message ) && config === undefined ) {
+ config = message;
+ message = config.message;
+ }
+
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.message = message instanceof jQuery ? message : String( message );
+ this.recoverable = config.recoverable === undefined || !!config.recoverable;
+ this.warning = !!config.warning;
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.Error );
+
+/* Methods */
+
+/**
+ * Check if the error is recoverable.
+ *
+ * If the error is recoverable, users are able to try the process again.
+ *
+ * @return {boolean} Error is recoverable
+ */
+OO.ui.Error.prototype.isRecoverable = function () {
+ return this.recoverable;
+};
+
+/**
+ * Check if the error is a warning.
+ *
+ * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
+ *
+ * @return {boolean} Error is warning
+ */
+OO.ui.Error.prototype.isWarning = function () {
+ return this.warning;
+};
+
+/**
+ * Get error message as DOM nodes.
+ *
+ * @return {jQuery} Error message in DOM nodes
+ */
+OO.ui.Error.prototype.getMessage = function () {
+ return this.message instanceof jQuery ?
+ this.message.clone() :
+ $( '<div>' ).text( this.message ).contents();
+};
+
+/**
+ * Get the error message text.
+ *
+ * @return {string} Error message
+ */
+OO.ui.Error.prototype.getMessageText = function () {
+ return this.message instanceof jQuery ? this.message.text() : this.message;
+};
+
+/**
+ * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
+ * or a function:
+ *
+ * - **number**: the process will wait for the specified number of milliseconds before proceeding.
+ * - **promise**: the process will continue to the next step when the promise is successfully resolved
+ * or stop if the promise is rejected.
+ * - **function**: the process will execute the function. The process will stop if the function returns
+ * either a boolean `false` or a promise that is rejected; if the function returns a number, the process
+ * will wait for that number of milliseconds before proceeding.
+ *
+ * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
+ * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
+ * its remaining steps will not be performed.
+ *
+ * @class
+ *
+ * @constructor
+ * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
+ * that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
+ * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
+ * a number or promise.
+ * @return {Object} Step object, with `callback` and `context` properties
+ */
+OO.ui.Process = function ( step, context ) {
+ // Properties
+ this.steps = [];
+
+ // Initialization
+ if ( step !== undefined ) {
+ this.next( step, context );
+ }
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.Process );
+
+/* Methods */
+
+/**
+ * Start the process.
+ *
+ * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
+ * If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
+ * and any remaining steps are not performed.
+ */
+OO.ui.Process.prototype.execute = function () {
+ var i, len, promise;
+
+ /**
+ * 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 ( Array.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 promise;
+};
+
+/**
+ * Create a process step.
+ *
+ * @private
+ * @param {number|jQuery.Promise|Function} step
+ *
+ * - Number of milliseconds to wait before proceeding
+ * - Promise that must be resolved before proceeding
+ * - Function to execute
+ * - If the function returns a boolean false the process will stop
+ * - If the function returns a promise, the process will continue to the next
+ * step when the promise is resolved or stop if the promise is rejected
+ * - If the function returns a number, the process will wait for that number of
+ * milliseconds before proceeding
+ * @param {Object} [context=null] Execution context of the function. The context is
+ * ignored if the step is a number or 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' );
+};
+
+/**
+ * Add step to the beginning of the process.
+ *
+ * @inheritdoc #createStep
+ * @return {OO.ui.Process} this
+ * @chainable
+ */
+OO.ui.Process.prototype.first = function ( step, context ) {
+ this.steps.unshift( this.createStep( step, context ) );
+ return this;
+};
+
+/**
+ * Add step to the end of the process.
+ *
+ * @inheritdoc #createStep
+ * @return {OO.ui.Process} this
+ * @chainable
+ */
+OO.ui.Process.prototype.next = function ( step, context ) {
+ this.steps.push( this.createStep( step, context ) );
+ return this;
+};
+
+/**
+ * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
+ * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
+ * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
+ * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
+ * pertinent data and reused.
+ *
+ * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
+ * `opened`, and `closing`, which represent the primary stages of the cycle:
+ *
+ * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
+ * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
+ *
+ * - an `opening` event is emitted with an `opening` promise
+ * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
+ * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
+ * window and its result executed
+ * - a `setup` progress notification is emitted from the `opening` promise
+ * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
+ * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
+ * window and its result executed
+ * - a `ready` progress notification is emitted from the `opening` promise
+ * - the `opening` promise is resolved with an `opened` promise
+ *
+ * **Opened**: the window is now open.
+ *
+ * **Closing**: the closing stage begins when the window manager's #closeWindow or the
+ * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
+ * to close the window.
+ *
+ * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
+ * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
+ * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
+ * window and its result executed
+ * - a `hold` progress notification is emitted from the `closing` promise
+ * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
+ * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
+ * window and its result executed
+ * - a `teardown` progress notification is emitted from the `closing` promise
+ * - the `closing` promise is resolved. The window is now closed
+ *
+ * See the [OOjs UI documentation on MediaWiki][1] for more information.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
+ *
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
+ * Note that window classes that are instantiated with a factory must have
+ * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
+ * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
+ */
+OO.ui.WindowManager = function OoUiWindowManager( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.WindowManager.parent.call( this, config );
+
+ // Mixin constructors
+ OO.EventEmitter.call( this );
+
+ // Properties
+ this.factory = config.factory;
+ this.modal = config.modal === undefined || !!config.modal;
+ this.windows = {};
+ this.opening = null;
+ this.opened = null;
+ this.closing = null;
+ this.preparingToOpen = null;
+ this.preparingToClose = null;
+ this.currentWindow = null;
+ this.globalEvents = false;
+ this.$ariaHidden = null;
+ this.onWindowResizeTimeout = null;
+ this.onWindowResizeHandler = this.onWindowResize.bind( this );
+ this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-windowManager' )
+ .toggleClass( 'oo-ui-windowManager-modal', this.modal );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
+OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
+
+/* Events */
+
+/**
+ * An 'opening' event is emitted when the window begins to be opened.
+ *
+ * @event opening
+ * @param {OO.ui.Window} win Window that's being opened
+ * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
+ * When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
+ * is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
+ * @param {Object} data Window opening data
+ */
+
+/**
+ * A 'closing' event is emitted when the window begins to be closed.
+ *
+ * @event closing
+ * @param {OO.ui.Window} win Window that's being closed
+ * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
+ * is closed successfully. The promise emits `hold` and `teardown` notifications when those
+ * processes are complete. When the `closing` promise is resolved, the first argument of its value
+ * is the closing data.
+ * @param {Object} data Window closing data
+ */
+
+/**
+ * A 'resize' event is emitted when a window is resized.
+ *
+ * @event resize
+ * @param {OO.ui.Window} win Window that was resized
+ */
+
+/* Static Properties */
+
+/**
+ * Map of the symbolic name of each window size and its CSS properties.
+ *
+ * @static
+ * @inheritable
+ * @property {Object}
+ */
+OO.ui.WindowManager.static.sizes = {
+ small: {
+ width: 300
+ },
+ medium: {
+ width: 500
+ },
+ large: {
+ width: 700
+ },
+ larger: {
+ width: 900
+ },
+ full: {
+ // These can be non-numeric because they are never used in calculations
+ width: '100%',
+ height: '100%'
+ }
+};
+
+/**
+ * Symbolic name of the default window size.
+ *
+ * The default size is used if the window's requested size is not recognized.
+ *
+ * @static
+ * @inheritable
+ * @property {string}
+ */
+OO.ui.WindowManager.static.defaultSize = 'medium';
+
+/* Methods */
+
+/**
+ * Handle window resize events.
+ *
+ * @private
+ * @param {jQuery.Event} e Window resize event
+ */
+OO.ui.WindowManager.prototype.onWindowResize = function () {
+ clearTimeout( this.onWindowResizeTimeout );
+ this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
+};
+
+/**
+ * Handle window resize events.
+ *
+ * @private
+ * @param {jQuery.Event} e Window resize event
+ */
+OO.ui.WindowManager.prototype.afterWindowResize = function () {
+ if ( this.currentWindow ) {
+ this.updateWindowSize( this.currentWindow );
+ }
+};
+
+/**
+ * Check if window is opening.
+ *
+ * @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.
+ *
+ * @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.
+ *
+ * @return {boolean} Window is opened
+ */
+OO.ui.WindowManager.prototype.isOpened = function ( win ) {
+ return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
+};
+
+/**
+ * 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;
+
+ for ( name in this.windows ) {
+ if ( this.windows[ name ] === win ) {
+ return true;
+ }
+ }
+
+ return false;
+};
+
+/**
+ * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
+ *
+ * @param {OO.ui.Window} win Window being opened
+ * @param {Object} [data] Window opening data
+ * @return {number} Milliseconds to wait
+ */
+OO.ui.WindowManager.prototype.getSetupDelay = function () {
+ return 0;
+};
+
+/**
+ * Get the number of milliseconds to wait after setup has finished before executing the ‘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;
+};
+
+/**
+ * Get the number of milliseconds to wait after closing has begun before executing the '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;
+};
+
+/**
+ * Get the number of milliseconds to wait after the ‘hold’ process has finished before
+ * executing the ‘teardown’ process.
+ *
+ * @param {OO.ui.Window} win Window being closed
+ * @param {Object} [data] Window closing data
+ * @return {number} Milliseconds to wait
+ */
+OO.ui.WindowManager.prototype.getTeardownDelay = function () {
+ return this.modal ? 250 : 0;
+};
+
+/**
+ * Get a window by its symbolic name.
+ *
+ * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
+ * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
+ * for more information about using factories.
+ * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
+ *
+ * @param {string} name Symbolic name of the window
+ * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
+ * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
+ * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
+ */
+OO.ui.WindowManager.prototype.getWindow = function ( name ) {
+ var deferred = $.Deferred(),
+ win = this.windows[ name ];
+
+ 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.addWindows( [ win ] );
+ deferred.resolve( win );
+ }
+ } else {
+ deferred.reject( new OO.ui.Error(
+ 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
+ ) );
+ }
+ } else {
+ deferred.resolve( win );
+ }
+
+ return deferred.promise();
+};
+
+/**
+ * Get current window.
+ *
+ * @return {OO.ui.Window|null} Currently opening/opened/closing window
+ */
+OO.ui.WindowManager.prototype.getCurrentWindow = function () {
+ return this.currentWindow;
+};
+
+/**
+ * Open a window.
+ *
+ * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
+ * @param {Object} [data] Window opening data
+ * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
+ * See {@link #event-opening 'opening' event} for more information about `opening` promises.
+ * @fires opening
+ */
+OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
+ var manager = this,
+ 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'
+ ) );
+ } else if ( this.preparingToOpen || this.opening || this.opened ) {
+ opening.reject( new OO.ui.Error(
+ 'Cannot open window: another window is opening or open'
+ ) );
+ }
+
+ // Window opening
+ if ( opening.state() !== 'rejected' ) {
+ // If a window is currently closing, wait for it to complete
+ this.preparingToOpen = $.when( this.closing );
+ // Ensure handlers get called after preparingToOpen is set
+ this.preparingToOpen.done( function () {
+ if ( manager.modal ) {
+ manager.toggleGlobalEvents( true );
+ manager.toggleAriaIsolation( true );
+ }
+ manager.currentWindow = win;
+ manager.opening = opening;
+ manager.preparingToOpen = null;
+ manager.emit( 'opening', win, opening, data );
+ setTimeout( function () {
+ win.setup( data ).then( function () {
+ manager.updateWindowSize( win );
+ 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 );
+ }, function () {
+ manager.opening = null;
+ manager.opened = $.Deferred();
+ opening.reject();
+ manager.closeWindow( win );
+ } );
+ }, manager.getReadyDelay() );
+ }, function () {
+ manager.opening = null;
+ manager.opened = $.Deferred();
+ opening.reject();
+ manager.closeWindow( win );
+ } );
+ }, manager.getSetupDelay() );
+ } );
+ }
+
+ return opening.promise();
+};
+
+/**
+ * Close a window.
+ *
+ * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
+ * @param {Object} [data] Window closing data
+ * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
+ * See {@link #event-closing 'closing' event} for more information about closing promises.
+ * @throws {Error} An error is thrown if the window is not managed by the window manager.
+ * @fires closing
+ */
+OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
+ var manager = this,
+ closing = $.Deferred(),
+ 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.preparingToClose || this.closing ) {
+ closing.reject( new OO.ui.Error(
+ 'Cannot close window: window already closing with different data'
+ ) );
+ }
+
+ // Window closing
+ if ( closing.state() !== 'rejected' ) {
+ // If the window is currently opening, close it when it's done
+ this.preparingToClose = $.when( this.opening );
+ // Ensure handlers get called after preparingToClose is set
+ this.preparingToClose.always( function () {
+ manager.closing = closing;
+ manager.preparingToClose = null;
+ manager.emit( 'closing', win, closing, data );
+ opened = manager.opened;
+ 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.toggleGlobalEvents( false );
+ manager.toggleAriaIsolation( false );
+ }
+ manager.closing = null;
+ manager.currentWindow = null;
+ closing.resolve( data );
+ } );
+ }, manager.getTeardownDelay() );
+ } );
+ }, manager.getHoldDelay() );
+ } );
+ }
+
+ return closing.promise();
+};
+
+/**
+ * Add windows to the window manager.
+ *
+ * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
+ * See the [OOjs ui documentation on MediaWiki] [2] for examples.
+ * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
+ *
+ * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
+ * by reference, symbolic name, or explicitly defined symbolic names.
+ * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
+ * explicit nor a statically configured symbolic name.
+ */
+OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
+ var i, len, win, name, list;
+
+ if ( Array.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 ( OO.isPlainObject( windows ) ) {
+ list = windows;
+ }
+
+ // Add windows
+ for ( name in list ) {
+ win = list[ name ];
+ this.windows[ name ] = win.toggle( false );
+ this.$element.append( win.$element );
+ win.setManager( this );
+ }
+};
+
+/**
+ * Remove the specified windows from the windows manager.
+ *
+ * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
+ * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
+ * longer listens to events, use the #destroy method.
+ *
+ * @param {string[]} names Symbolic names of windows to remove
+ * @return {jQuery.Promise} Promise resolved when window is closed and removed
+ * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
+ */
+OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
+ var i, len, win, name, cleanupWindow,
+ manager = this,
+ promises = [],
+ cleanup = function ( name, win ) {
+ delete manager.windows[ name ];
+ win.$element.detach();
+ };
+
+ for ( i = 0, len = names.length; i < len; i++ ) {
+ name = names[ i ];
+ win = this.windows[ name ];
+ if ( !win ) {
+ throw new Error( 'Cannot remove window' );
+ }
+ cleanupWindow = cleanup.bind( null, name, win );
+ promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
+ }
+
+ return $.when.apply( $, promises );
+};
+
+/**
+ * Remove all windows from the window manager.
+ *
+ * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
+ * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
+ * To remove just a subset of windows, use the #removeWindows method.
+ *
+ * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
+ */
+OO.ui.WindowManager.prototype.clearWindows = function () {
+ return this.removeWindows( Object.keys( this.windows ) );
+};
+
+/**
+ * Set dialog size. In general, this method should not be called directly.
+ *
+ * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
+ *
+ * @chainable
+ */
+OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
+ var isFullscreen;
+
+ // Bypass for non-current, and thus invisible, windows
+ if ( win !== this.currentWindow ) {
+ return;
+ }
+
+ isFullscreen = win.getSize() === 'full';
+
+ this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen );
+ this.$element.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen );
+ win.setDimensions( win.getSizeProperties() );
+
+ this.emit( 'resize', win );
+
+ return this;
+};
+
+/**
+ * Bind or unbind global events for scrolling.
+ *
+ * @private
+ * @param {boolean} [on] Bind global events
+ * @chainable
+ */
+OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
+ var scrollWidth, bodyMargin,
+ $body = $( this.getElementDocument().body ),
+ // We could have multiple window managers open so only modify
+ // the body css at the bottom of the stack
+ stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0 ;
+
+ on = on === undefined ? !!this.globalEvents : !!on;
+
+ if ( on ) {
+ if ( !this.globalEvents ) {
+ $( this.getElementWindow() ).on( {
+ // Start listening for top-level window dimension changes
+ 'orientationchange resize': this.onWindowResizeHandler
+ } );
+ if ( stackDepth === 0 ) {
+ scrollWidth = window.innerWidth - document.documentElement.clientWidth;
+ bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
+ $body.css( {
+ overflow: 'hidden',
+ 'margin-right': bodyMargin + scrollWidth
+ } );
+ }
+ stackDepth++;
+ this.globalEvents = true;
+ }
+ } else if ( this.globalEvents ) {
+ $( this.getElementWindow() ).off( {
+ // Stop listening for top-level window dimension changes
+ 'orientationchange resize': this.onWindowResizeHandler
+ } );
+ stackDepth--;
+ if ( stackDepth === 0 ) {
+ $body.css( {
+ overflow: '',
+ 'margin-right': ''
+ } );
+ }
+ this.globalEvents = false;
+ }
+ $body.data( 'windowManagerGlobalEvents', stackDepth );
+
+ return this;
+};
+
+/**
+ * Toggle screen reader visibility of content other than the window manager.
+ *
+ * @private
+ * @param {boolean} [isolate] Make only the window manager visible to screen readers
+ * @chainable
+ */
+OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
+ isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
+
+ if ( isolate ) {
+ if ( !this.$ariaHidden ) {
+ // Hide everything other than the window manager from screen readers
+ this.$ariaHidden = $( 'body' )
+ .children()
+ .not( this.$element.parentsUntil( 'body' ).last() )
+ .attr( 'aria-hidden', '' );
+ }
+ } else if ( this.$ariaHidden ) {
+ // Restore screen reader visibility
+ this.$ariaHidden.removeAttr( 'aria-hidden' );
+ this.$ariaHidden = null;
+ }
+
+ return this;
+};
+
+/**
+ * Destroy the window manager.
+ *
+ * Destroying the window manager ensures that it will no longer listen to events. If you would like to
+ * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
+ * instead.
+ */
+OO.ui.WindowManager.prototype.destroy = function () {
+ this.toggleGlobalEvents( false );
+ this.toggleAriaIsolation( false );
+ this.clearWindows();
+ this.$element.remove();
+};
+
+/**
+ * A window is a container for elements that are in a child frame. They are used with
+ * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
+ * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
+ * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
+ * the window manager will choose a sensible fallback.
+ *
+ * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
+ * different processes are executed:
+ *
+ * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
+ * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
+ * the window.
+ *
+ * - {@link #getSetupProcess} method is called and its result executed
+ * - {@link #getReadyProcess} method is called and its result executed
+ *
+ * **opened**: The window is now open
+ *
+ * **closing**: The closing stage begins when the window manager's
+ * {@link OO.ui.WindowManager#closeWindow closeWindow}
+ * or the window's {@link #close} methods are used, and the window manager begins to close the window.
+ *
+ * - {@link #getHoldProcess} method is called and its result executed
+ * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
+ *
+ * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
+ * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
+ * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
+ * processing can complete. Always assume window processes are executed asynchronously.
+ *
+ * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
+ * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
+ */
+OO.ui.Window = function OoUiWindow( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.Window.parent.call( this, config );
+
+ // Mixin constructors
+ OO.EventEmitter.call( this );
+
+ // Properties
+ this.manager = null;
+ this.size = config.size || this.constructor.static.size;
+ this.$frame = $( '<div>' );
+ this.$overlay = $( '<div>' );
+ this.$content = $( '<div>' );
+
+ this.$focusTrapBefore = $( '<div>' ).prop( 'tabIndex', 0 );
+ this.$focusTrapAfter = $( '<div>' ).prop( 'tabIndex', 0 );
+ this.$focusTraps = this.$focusTrapBefore.add( this.$focusTrapAfter );
+
+ // Initialization
+ this.$overlay.addClass( 'oo-ui-window-overlay' );
+ this.$content
+ .addClass( 'oo-ui-window-content' )
+ .attr( 'tabindex', 0 );
+ this.$frame
+ .addClass( 'oo-ui-window-frame' )
+ .append( this.$focusTrapBefore, this.$content, this.$focusTrapAfter );
+
+ this.$element
+ .addClass( 'oo-ui-window' )
+ .append( this.$frame, this.$overlay );
+
+ // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
+ // that reference properties not initialized at that time of parent class construction
+ // TODO: Find a better way to handle post-constructor setup
+ this.visible = false;
+ this.$element.addClass( 'oo-ui-element-hidden' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.Window, OO.ui.Element );
+OO.mixinClass( OO.ui.Window, OO.EventEmitter );
+
+/* Static Properties */
+
+/**
+ * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
+ *
+ * The static size is used if no #size is configured during construction.
+ *
+ * @static
+ * @inheritable
+ * @property {string}
+ */
+OO.ui.Window.static.size = 'medium';
+
+/* Methods */
+
+/**
+ * Handle mouse down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Mouse down event
+ */
+OO.ui.Window.prototype.onMouseDown = function ( e ) {
+ // Prevent clicking on the click-block from stealing focus
+ if ( e.target === this.$element[ 0 ] ) {
+ return false;
+ }
+};
+
+/**
+ * Check if the window has been initialized.
+ *
+ * Initialization occurs when a window is added to a manager.
+ *
+ * @return {boolean} Window has been initialized
+ */
+OO.ui.Window.prototype.isInitialized = function () {
+ return !!this.manager;
+};
+
+/**
+ * Check if the window is visible.
+ *
+ * @return {boolean} Window is visible
+ */
+OO.ui.Window.prototype.isVisible = function () {
+ return this.visible;
+};
+
+/**
+ * Check if the window is opening.
+ *
+ * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
+ * method.
+ *
+ * @return {boolean} Window is opening
+ */
+OO.ui.Window.prototype.isOpening = function () {
+ return this.manager.isOpening( this );
+};
+
+/**
+ * Check if the window is closing.
+ *
+ * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
+ *
+ * @return {boolean} Window is closing
+ */
+OO.ui.Window.prototype.isClosing = function () {
+ return this.manager.isClosing( this );
+};
+
+/**
+ * Check if the window is opened.
+ *
+ * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
+ *
+ * @return {boolean} Window is opened
+ */
+OO.ui.Window.prototype.isOpened = function () {
+ return this.manager.isOpened( this );
+};
+
+/**
+ * Get the window manager.
+ *
+ * All windows must be attached to a window manager, which is used to open
+ * and close the window and control its presentation.
+ *
+ * @return {OO.ui.WindowManager} Manager of window
+ */
+OO.ui.Window.prototype.getManager = function () {
+ return this.manager;
+};
+
+/**
+ * Get the symbolic name of the window size (e.g., `small` or `medium`).
+ *
+ * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
+ */
+OO.ui.Window.prototype.getSize = function () {
+ var viewport = OO.ui.Element.static.getDimensions( this.getElementWindow() ),
+ sizes = this.manager.constructor.static.sizes,
+ size = this.size;
+
+ if ( !sizes[ size ] ) {
+ size = this.manager.constructor.static.defaultSize;
+ }
+ if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
+ size = 'full';
+ }
+
+ return size;
+};
+
+/**
+ * Get the size properties associated with the current window size
+ *
+ * @return {Object} Size properties
+ */
+OO.ui.Window.prototype.getSizeProperties = function () {
+ return this.manager.constructor.static.sizes[ this.getSize() ];
+};
+
+/**
+ * Disable transitions on window's frame for the duration of the callback function, then enable them
+ * back.
+ *
+ * @private
+ * @param {Function} callback Function to call while transitions are disabled
+ */
+OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
+ // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
+ // Disable transitions first, otherwise we'll get values from when the window was animating.
+ var oldTransition,
+ styleObj = this.$frame[ 0 ].style;
+ oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
+ styleObj.MozTransition || styleObj.WebkitTransition;
+ styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
+ styleObj.MozTransition = styleObj.WebkitTransition = 'none';
+ callback();
+ // Force reflow to make sure the style changes done inside callback really are not transitioned
+ this.$frame.height();
+ styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
+ styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
+};
+
+/**
+ * Get the height of the full window contents (i.e., the window head, body and foot together).
+ *
+ * What consistitutes the head, body, and foot varies depending on the window type.
+ * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
+ * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
+ * and special actions in the head, and dialog content in the body.
+ *
+ * To get just the height of the dialog body, use the #getBodyHeight method.
+ *
+ * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
+ */
+OO.ui.Window.prototype.getContentHeight = function () {
+ var bodyHeight,
+ win = this,
+ bodyStyleObj = this.$body[ 0 ].style,
+ frameStyleObj = this.$frame[ 0 ].style;
+
+ // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
+ // Disable transitions first, otherwise we'll get values from when the window was animating.
+ this.withoutSizeTransitions( function () {
+ var oldHeight = frameStyleObj.height,
+ oldPosition = bodyStyleObj.position;
+ frameStyleObj.height = '1px';
+ // Force body to resize to new width
+ bodyStyleObj.position = 'relative';
+ bodyHeight = win.getBodyHeight();
+ frameStyleObj.height = oldHeight;
+ bodyStyleObj.position = oldPosition;
+ } );
+
+ return (
+ // Add buffer for border
+ ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
+ // Use combined heights of children
+ ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
+ );
+};
+
+/**
+ * Get the height of the window body.
+ *
+ * To get the height of the full window contents (the window body, head, and foot together),
+ * use #getContentHeight.
+ *
+ * When this function is called, the window will temporarily have been resized
+ * to height=1px, so .scrollHeight measurements can be taken accurately.
+ *
+ * @return {number} Height of the window body in pixels
+ */
+OO.ui.Window.prototype.getBodyHeight = function () {
+ return this.$body[ 0 ].scrollHeight;
+};
+
+/**
+ * Get the directionality of the frame (right-to-left or left-to-right).
+ *
+ * @return {string} Directionality: `'ltr'` or `'rtl'`
+ */
+OO.ui.Window.prototype.getDir = function () {
+ return OO.ui.Element.static.getDir( this.$content ) || 'ltr';
+};
+
+/**
+ * Get the 'setup' process.
+ *
+ * The setup process is used to set up a window for use in a particular context,
+ * based on the `data` argument. This method is called during the opening phase of the window’s
+ * lifecycle.
+ *
+ * Override this method to add additional steps to the ‘setup’ process the parent method provides
+ * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
+ * of OO.ui.Process.
+ *
+ * To add window content that persists between openings, you may wish to use the #initialize method
+ * instead.
+ *
+ * @param {Object} [data] Window opening data
+ * @return {OO.ui.Process} Setup process
+ */
+OO.ui.Window.prototype.getSetupProcess = function () {
+ return new OO.ui.Process();
+};
+
+/**
+ * Get the ‘ready’ process.
+ *
+ * The ready process is used to ready a window for use in a particular
+ * context, based on the `data` argument. This method is called during the opening phase of
+ * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
+ *
+ * Override this method to add additional steps to the ‘ready’ process the parent method
+ * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
+ * methods of OO.ui.Process.
+ *
+ * @param {Object} [data] Window opening data
+ * @return {OO.ui.Process} Ready process
+ */
+OO.ui.Window.prototype.getReadyProcess = function () {
+ return new OO.ui.Process();
+};
+
+/**
+ * Get the 'hold' process.
+ *
+ * The hold proccess is used to keep a window from being used in a particular context,
+ * based on the `data` argument. This method is called during the closing phase of the window’s
+ * lifecycle.
+ *
+ * Override this method to add additional steps to the 'hold' process the parent method provides
+ * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
+ * of OO.ui.Process.
+ *
+ * @param {Object} [data] Window closing data
+ * @return {OO.ui.Process} Hold process
+ */
+OO.ui.Window.prototype.getHoldProcess = function () {
+ return new OO.ui.Process();
+};
+
+/**
+ * Get the ‘teardown’ process.
+ *
+ * The teardown process is used to teardown a window after use. During teardown,
+ * user interactions within the window are conveyed and the window is closed, based on the `data`
+ * argument. This method is called during the closing phase of the window’s lifecycle.
+ *
+ * Override this method to add additional steps to the ‘teardown’ process the parent method provides
+ * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
+ * of OO.ui.Process.
+ *
+ * @param {Object} [data] Window closing data
+ * @return {OO.ui.Process} Teardown process
+ */
+OO.ui.Window.prototype.getTeardownProcess = function () {
+ return new OO.ui.Process();
+};
+
+/**
+ * Set the window manager.
+ *
+ * This will cause the window to initialize. Calling it more than once will cause an error.
+ *
+ * @param {OO.ui.WindowManager} manager Manager for this window
+ * @throws {Error} An error is thrown if the method is called more than once
+ * @chainable
+ */
+OO.ui.Window.prototype.setManager = function ( manager ) {
+ if ( this.manager ) {
+ throw new Error( 'Cannot set window manager, window already has a manager' );
+ }
+
+ this.manager = manager;
+ this.initialize();
+
+ return this;
+};
+
+/**
+ * Set the window size by symbolic name (e.g., 'small' or 'medium')
+ *
+ * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
+ * `full`
+ * @chainable
+ */
+OO.ui.Window.prototype.setSize = function ( size ) {
+ this.size = size;
+ this.updateSize();
+ return this;
+};
+
+/**
+ * Update the window size.
+ *
+ * @throws {Error} An error is thrown if the window is not attached to a window manager
+ * @chainable
+ */
+OO.ui.Window.prototype.updateSize = function () {
+ if ( !this.manager ) {
+ throw new Error( 'Cannot update window size, must be attached to a manager' );
+ }
+
+ this.manager.updateWindowSize( this );
+
+ return this;
+};
+
+/**
+ * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
+ * when the window is opening. In general, setDimensions should not be called directly.
+ *
+ * To set the size of the window, use the #setSize method.
+ *
+ * @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.Window.prototype.setDimensions = function ( dim ) {
+ var height,
+ win = this,
+ styleObj = this.$frame[ 0 ].style;
+
+ // Calculate the height we need to set using the correct width
+ if ( dim.height === undefined ) {
+ this.withoutSizeTransitions( function () {
+ var oldWidth = styleObj.width;
+ win.$frame.css( 'width', dim.width || '' );
+ height = win.getContentHeight();
+ styleObj.width = oldWidth;
+ } );
+ } else {
+ height = dim.height;
+ }
+
+ this.$frame.css( {
+ width: dim.width || '',
+ minWidth: dim.minWidth || '',
+ maxWidth: dim.maxWidth || '',
+ height: height || '',
+ minHeight: dim.minHeight || '',
+ maxHeight: dim.maxHeight || ''
+ } );
+
+ return this;
+};
+
+/**
+ * Initialize window contents.
+ *
+ * Before the window is opened for the first time, #initialize is called so that content that
+ * persists between openings can be added to the window.
+ *
+ * To set up a window with new content each time the window opens, use #getSetupProcess.
+ *
+ * @throws {Error} An error is thrown if the window is not attached to a window manager
+ * @chainable
+ */
+OO.ui.Window.prototype.initialize = function () {
+ if ( !this.manager ) {
+ throw new Error( 'Cannot initialize window, must be attached to a manager' );
+ }
+
+ // Properties
+ this.$head = $( '<div>' );
+ this.$body = $( '<div>' );
+ this.$foot = $( '<div>' );
+ this.$document = $( this.getElementDocument() );
+
+ // Events
+ this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
+
+ // Initialization
+ this.$head.addClass( 'oo-ui-window-head' );
+ this.$body.addClass( 'oo-ui-window-body' );
+ this.$foot.addClass( 'oo-ui-window-foot' );
+ this.$content.append( this.$head, this.$body, this.$foot );
+
+ return this;
+};
+
+/**
+ * Called when someone tries to focus the hidden element at the end of the dialog.
+ * Sends focus back to the start of the dialog.
+ *
+ * @param {jQuery.Event} event Focus event
+ */
+OO.ui.Window.prototype.onFocusTrapFocused = function ( event ) {
+ if ( this.$focusTrapBefore.is( event.target ) ) {
+ OO.ui.findFocusable( this.$content, true ).focus();
+ } else {
+ // this.$content is the part of the focus cycle, and is the first focusable element
+ this.$content.focus();
+ }
+};
+
+/**
+ * Open the window.
+ *
+ * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
+ * method, which returns a promise resolved when the window is done opening.
+ *
+ * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
+ *
+ * @param {Object} [data] Window opening data
+ * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
+ * if the window fails to open. When the promise is resolved successfully, the first argument of the
+ * value is a new promise, which is resolved when the window begins closing.
+ * @throws {Error} An error is thrown if the window is not attached to a window manager
+ */
+OO.ui.Window.prototype.open = function ( data ) {
+ if ( !this.manager ) {
+ throw new Error( 'Cannot open window, must be attached to a manager' );
+ }
+
+ return this.manager.openWindow( this, data );
+};
+
+/**
+ * Close the window.
+ *
+ * This method is a wrapper around a call to the window
+ * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
+ * which returns a closing promise resolved when the window is done closing.
+ *
+ * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
+ * phase of the window’s lifecycle and can be used to specify closing behavior each time
+ * the window closes.
+ *
+ * @param {Object} [data] Window closing data
+ * @return {jQuery.Promise} Promise resolved when window is closed
+ * @throws {Error} An error is thrown if the window is not attached to a window manager
+ */
+OO.ui.Window.prototype.close = function ( data ) {
+ if ( !this.manager ) {
+ throw new Error( 'Cannot close window, must be attached to a manager' );
+ }
+
+ return this.manager.closeWindow( this, data );
+};
+
+/**
+ * Setup window.
+ *
+ * This is called by OO.ui.WindowManager during 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 setup
+ */
+OO.ui.Window.prototype.setup = function ( data ) {
+ var win = this;
+
+ this.toggle( true );
+
+ this.focusTrapHandler = OO.ui.bind( this.onFocusTrapFocused, this );
+ this.$focusTraps.on( 'focus', this.focusTrapHandler );
+
+ return this.getSetupProcess( data ).execute().then( function () {
+ // Force redraw by asking the browser to measure the elements' widths
+ win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
+ win.$content.addClass( 'oo-ui-window-content-setup' ).width();
+ } );
+};
+
+/**
+ * Ready window.
+ *
+ * This is called by OO.ui.WindowManager during 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.Window.prototype.ready = function ( data ) {
+ var win = this;
+
+ this.$content.focus();
+ return this.getReadyProcess( data ).execute().then( function () {
+ // Force redraw by asking the browser to measure the elements' widths
+ win.$element.addClass( 'oo-ui-window-ready' ).width();
+ win.$content.addClass( 'oo-ui-window-content-ready' ).width();
+ } );
+};
+
+/**
+ * Hold window.
+ *
+ * This is called by OO.ui.WindowManager during window closing, and should not be called directly
+ * by other systems.
+ *
+ * @param {Object} [data] Window closing data
+ * @return {jQuery.Promise} Promise resolved when window is held
+ */
+OO.ui.Window.prototype.hold = function ( data ) {
+ var win = this;
+
+ return this.getHoldProcess( data ).execute().then( function () {
+ // Get the focused element within the window's content
+ var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
+
+ // Blur the focused element
+ if ( $focus.length ) {
+ $focus[ 0 ].blur();
+ }
+
+ // Force redraw by asking the browser to measure the elements' widths
+ win.$element.removeClass( 'oo-ui-window-ready' ).width();
+ win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
+ } );
+};
+
+/**
+ * Teardown window.
+ *
+ * This is called by OO.ui.WindowManager during window closing, and should not be called directly
+ * by other systems.
+ *
+ * @param {Object} [data] Window closing data
+ * @return {jQuery.Promise} Promise resolved when window is torn down
+ */
+OO.ui.Window.prototype.teardown = function ( data ) {
+ var win = this;
+
+ return this.getTeardownProcess( data ).execute().then( function () {
+ // Force redraw by asking the browser to measure the elements' widths
+ win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
+ win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
+ win.$focusTraps.off( 'focus', win.focusTrapHandler );
+ win.toggle( false );
+ } );
+};
+
+/**
+ * The Dialog class serves as the base class for the other types of dialogs.
+ * Unless extended to include controls, the rendered dialog box is a simple window
+ * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
+ * which opens, closes, and controls the presentation of the window. See the
+ * [OOjs UI documentation on MediaWiki] [1] for more information.
+ *
+ * @example
+ * // A simple dialog window.
+ * function MyDialog( config ) {
+ * MyDialog.parent.call( this, config );
+ * }
+ * OO.inheritClass( MyDialog, OO.ui.Dialog );
+ * MyDialog.prototype.initialize = function () {
+ * MyDialog.parent.prototype.initialize.call( this );
+ * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
+ * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
+ * this.$body.append( this.content.$element );
+ * };
+ * MyDialog.prototype.getBodyHeight = function () {
+ * return this.content.$element.outerHeight( true );
+ * };
+ * var myDialog = new MyDialog( {
+ * size: 'medium'
+ * } );
+ * // Create and append a window manager, which opens and closes the window.
+ * var windowManager = new OO.ui.WindowManager();
+ * $( 'body' ).append( windowManager.$element );
+ * windowManager.addWindows( [ myDialog ] );
+ * // Open the window!
+ * windowManager.openWindow( myDialog );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Window
+ * @mixins OO.ui.mixin.PendingElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.Dialog = function OoUiDialog( config ) {
+ // Parent constructor
+ OO.ui.Dialog.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.PendingElement.call( this );
+
+ // Properties
+ this.actions = new OO.ui.ActionSet();
+ this.attachedActions = [];
+ this.currentAction = null;
+ this.onDialogKeyDownHandler = this.onDialogKeyDown.bind( this );
+
+ // Events
+ this.actions.connect( this, {
+ click: 'onActionClick',
+ resize: 'onActionResize',
+ change: 'onActionsChange'
+ } );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-dialog' )
+ .attr( 'role', 'dialog' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
+OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
+
+/* Static Properties */
+
+/**
+ * Symbolic name of dialog.
+ *
+ * The dialog class must have a symbolic name in order to be registered with OO.Factory.
+ * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
+ *
+ * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
+ *
+ * @abstract
+ * @static
+ * @inheritable
+ * @property {string}
+ */
+OO.ui.Dialog.static.name = '';
+
+/**
+ * The dialog title.
+ *
+ * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
+ * that will produce a Label node or string. The title can also be specified with data passed to the
+ * constructor (see #getSetupProcess). In this case, the static value will be overridden.
+ *
+ * @abstract
+ * @static
+ * @inheritable
+ * @property {jQuery|string|Function}
+ */
+OO.ui.Dialog.static.title = '';
+
+/**
+ * An array of configured {@link OO.ui.ActionWidget action widgets}.
+ *
+ * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
+ * value will be overridden.
+ *
+ * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
+ *
+ * @static
+ * @inheritable
+ * @property {Object[]}
+ */
+OO.ui.Dialog.static.actions = [];
+
+/**
+ * Close the dialog when the 'Esc' key is pressed.
+ *
+ * @static
+ * @abstract
+ * @inheritable
+ * @property {boolean}
+ */
+OO.ui.Dialog.static.escapable = true;
+
+/* Methods */
+
+/**
+ * Handle frame document key down events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.Dialog.prototype.onDialogKeyDown = function ( e ) {
+ if ( e.which === OO.ui.Keys.ESCAPE ) {
+ this.executeAction( '' );
+ e.preventDefault();
+ e.stopPropagation();
+ }
+};
+
+/**
+ * Handle action resized events.
+ *
+ * @private
+ * @param {OO.ui.ActionWidget} action Action that was resized
+ */
+OO.ui.Dialog.prototype.onActionResize = function () {
+ // Override in subclass
+};
+
+/**
+ * Handle action click events.
+ *
+ * @private
+ * @param {OO.ui.ActionWidget} action Action that was clicked
+ */
+OO.ui.Dialog.prototype.onActionClick = function ( action ) {
+ if ( !this.isPending() ) {
+ this.executeAction( action.getAction() );
+ }
+};
+
+/**
+ * Handle actions change event.
+ *
+ * @private
+ */
+OO.ui.Dialog.prototype.onActionsChange = function () {
+ this.detachActions();
+ if ( !this.isClosing() ) {
+ this.attachActions();
+ }
+};
+
+/**
+ * Get the set of actions used by the dialog.
+ *
+ * @return {OO.ui.ActionSet}
+ */
+OO.ui.Dialog.prototype.getActions = function () {
+ return this.actions;
+};
+
+/**
+ * Get a process for taking action.
+ *
+ * When you override this method, you can create a new OO.ui.Process and return it, or add additional
+ * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
+ * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
+ *
+ * @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.title] Dialog title, omit to use
+ * the {@link #static-title static title}
+ * @param {Object[]} [data.actions] List of configuration options for each
+ * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
+ */
+OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
+ data = data || {};
+
+ // Parent method
+ return OO.ui.Dialog.parent.prototype.getSetupProcess.call( this, data )
+ .next( function () {
+ var config = this.constructor.static,
+ actions = data.actions !== undefined ? data.actions : config.actions;
+
+ this.title.setLabel(
+ data.title !== undefined ? data.title : this.constructor.static.title
+ );
+ this.actions.add( this.getActionWidgets( actions ) );
+
+ if ( this.constructor.static.escapable ) {
+ this.$element.on( 'keydown', this.onDialogKeyDownHandler );
+ }
+ }, this );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
+ // Parent method
+ return OO.ui.Dialog.parent.prototype.getTeardownProcess.call( this, data )
+ .first( function () {
+ if ( this.constructor.static.escapable ) {
+ this.$element.off( 'keydown', this.onDialogKeyDownHandler );
+ }
+
+ this.actions.clear();
+ this.currentAction = null;
+ }, this );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.Dialog.prototype.initialize = function () {
+ var titleId;
+
+ // Parent method
+ OO.ui.Dialog.parent.prototype.initialize.call( this );
+
+ titleId = OO.ui.generateElementId();
+
+ // Properties
+ this.title = new OO.ui.LabelWidget( {
+ id: titleId
+ } );
+
+ // Initialization
+ this.$content.addClass( 'oo-ui-dialog-content' );
+ this.$element.attr( 'aria-labelledby', titleId );
+ this.setPendingElement( this.$head );
+};
+
+/**
+ * Get action widgets from a list of configs
+ *
+ * @param {Object[]} actions Action widget configs
+ * @return {OO.ui.ActionWidget[]} Action widgets
+ */
+OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
+ var i, len, widgets = [];
+ for ( i = 0, len = actions.length; i < len; i++ ) {
+ widgets.push(
+ new OO.ui.ActionWidget( actions[ i ] )
+ );
+ }
+ return widgets;
+};
+
+/**
+ * Attach action actions.
+ *
+ * @protected
+ */
+OO.ui.Dialog.prototype.attachActions = function () {
+ // Remember the list of potentially attached actions
+ this.attachedActions = this.actions.get();
+};
+
+/**
+ * Detach action actions.
+ *
+ * @protected
+ * @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();
+ this.currentAction = action;
+ return this.getActionProcess( action ).execute()
+ .always( this.popPending.bind( this ) );
+};
+
+/**
+ * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
+ * consists of a header that contains the dialog title, a body with the message, and a footer that
+ * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
+ * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
+ *
+ * There are two basic types of message dialogs, confirmation and alert:
+ *
+ * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
+ * more details about the consequences.
+ * - **alert**: the dialog title describes which event occurred and the message provides more information
+ * about why the event occurred.
+ *
+ * The MessageDialog class specifies two actions: ‘accept’, the primary
+ * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
+ * passing along the selected action.
+ *
+ * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * @example
+ * // Example: Creating and opening a message dialog window.
+ * var messageDialog = new OO.ui.MessageDialog();
+ *
+ * // Create and append a window manager.
+ * var windowManager = new OO.ui.WindowManager();
+ * $( 'body' ).append( windowManager.$element );
+ * windowManager.addWindows( [ messageDialog ] );
+ * // Open the window.
+ * windowManager.openWindow( messageDialog, {
+ * title: 'Basic message dialog',
+ * message: 'This is the message'
+ * } );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
+ *
+ * @class
+ * @extends OO.ui.Dialog
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
+ // Parent constructor
+ OO.ui.MessageDialog.parent.call( this, config );
+
+ // Properties
+ this.verticalActionLayout = null;
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-messageDialog' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
+
+/* Static Properties */
+
+OO.ui.MessageDialog.static.name = 'message';
+
+OO.ui.MessageDialog.static.size = 'small';
+
+OO.ui.MessageDialog.static.verbose = false;
+
+/**
+ * Dialog title.
+ *
+ * The title of a confirmation dialog describes what a progressive action will do. The
+ * title of an alert dialog describes which event occurred.
+ *
+ * @static
+ * @inheritable
+ * @property {jQuery|string|Function|null}
+ */
+OO.ui.MessageDialog.static.title = null;
+
+/**
+ * The message displayed in the dialog body.
+ *
+ * A confirmation message describes the consequences of a progressive action. An alert
+ * message describes why an event occurred.
+ *
+ * @static
+ * @inheritable
+ * @property {jQuery|string|Function|null}
+ */
+OO.ui.MessageDialog.static.message = null;
+
+// Note that OO.ui.alert() and OO.ui.confirm() rely on these.
+OO.ui.MessageDialog.static.actions = [
+ { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
+ { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
+];
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
+ OO.ui.MessageDialog.parent.prototype.setManager.call( this, manager );
+
+ // Events
+ this.manager.connect( this, {
+ resize: 'onResize'
+ } );
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
+ this.fitActions();
+ return OO.ui.MessageDialog.parent.prototype.onActionResize.call( this, action );
+};
+
+/**
+ * Handle window resized events.
+ *
+ * @private
+ */
+OO.ui.MessageDialog.prototype.onResize = function () {
+ var dialog = this;
+ dialog.fitActions();
+ // Wait for CSS transition to finish and do it again :(
+ setTimeout( function () {
+ dialog.fitActions();
+ }, 300 );
+};
+
+/**
+ * Toggle action layout between vertical and horizontal.
+ *
+ * @private
+ * @param {boolean} [value] Layout actions vertically, omit to toggle
+ * @chainable
+ */
+OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
+ value = value === undefined ? !this.verticalActionLayout : !!value;
+
+ if ( value !== this.verticalActionLayout ) {
+ this.verticalActionLayout = value;
+ this.$actions
+ .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
+ .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
+ }
+
+ return this;
+};
+
+/**
+ * @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.parent.prototype.getActionProcess.call( this, action );
+};
+
+/**
+ * @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 || {};
+
+ // Parent method
+ return OO.ui.MessageDialog.parent.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 );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.getReadyProcess = function ( data ) {
+ data = data || {};
+
+ // Parent method
+ return OO.ui.MessageDialog.parent.prototype.getReadyProcess.call( this, data )
+ .next( function () {
+ // Focus the primary action button
+ var actions = this.actions.get();
+ actions = actions.filter( function ( action ) {
+ return action.getFlags().indexOf( 'primary' ) > -1;
+ } );
+ if ( actions.length > 0 ) {
+ actions[ 0 ].$button.focus();
+ }
+ }, this );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.getBodyHeight = function () {
+ var bodyHeight, oldOverflow,
+ $scrollable = this.container.$element;
+
+ oldOverflow = $scrollable[ 0 ].style.overflow;
+ $scrollable[ 0 ].style.overflow = 'hidden';
+
+ OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
+
+ bodyHeight = this.text.$element.outerHeight( true );
+ $scrollable[ 0 ].style.overflow = oldOverflow;
+
+ return bodyHeight;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
+ var $scrollable = this.container.$element;
+ OO.ui.MessageDialog.parent.prototype.setDimensions.call( this, dim );
+
+ // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
+ // Need to do it after transition completes (250ms), add 50ms just in case.
+ setTimeout( function () {
+ var oldOverflow = $scrollable[ 0 ].style.overflow;
+ $scrollable[ 0 ].style.overflow = 'hidden';
+
+ OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
+
+ $scrollable[ 0 ].style.overflow = oldOverflow;
+ }, 300 );
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.initialize = function () {
+ // Parent method
+ OO.ui.MessageDialog.parent.prototype.initialize.call( this );
+
+ // Properties
+ this.$actions = $( '<div>' );
+ this.container = new OO.ui.PanelLayout( {
+ scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
+ } );
+ this.text = new OO.ui.PanelLayout( {
+ padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
+ } );
+ this.message = new OO.ui.LabelWidget( {
+ classes: [ 'oo-ui-messageDialog-message' ]
+ } );
+
+ // Initialization
+ this.title.$element.addClass( 'oo-ui-messageDialog-title' );
+ this.$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 );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.MessageDialog.prototype.attachActions = function () {
+ var i, len, other, special, others;
+
+ // Parent method
+ OO.ui.MessageDialog.parent.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 );
+ }
+
+ if ( !this.isOpening() ) {
+ // If the dialog is currently opening, this will be called automatically soon.
+ // This also calls #fitActions.
+ this.updateSize();
+ }
+};
+
+/**
+ * Fit action actions into columns or rows.
+ *
+ * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
+ *
+ * @private
+ */
+OO.ui.MessageDialog.prototype.fitActions = function () {
+ var i, len, action,
+ previous = this.verticalActionLayout,
+ 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;
+ }
+ }
+
+ // Move the body out of the way of the foot
+ this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
+
+ if ( this.verticalActionLayout !== previous ) {
+ // We changed the layout, window height might need to be updated.
+ this.updateSize();
+ }
+};
+
+/**
+ * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
+ * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
+ * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
+ * relevant. The ProcessDialog class is always extended and customized with the actions and content
+ * required for each process.
+ *
+ * The process dialog box consists of a header that visually represents the ‘working’ state of long
+ * processes with an animation. The header contains the dialog title as well as
+ * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
+ * a ‘primary’ action on the right (e.g., ‘Done’).
+ *
+ * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
+ * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
+ *
+ * @example
+ * // Example: Creating and opening a process dialog window.
+ * function MyProcessDialog( config ) {
+ * MyProcessDialog.parent.call( this, config );
+ * }
+ * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
+ *
+ * MyProcessDialog.static.title = 'Process dialog';
+ * MyProcessDialog.static.actions = [
+ * { action: 'save', label: 'Done', flags: 'primary' },
+ * { label: 'Cancel', flags: 'safe' }
+ * ];
+ *
+ * MyProcessDialog.prototype.initialize = function () {
+ * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
+ * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
+ * this.content.$element.append( '<p>This is a process dialog window. The header contains the title and two buttons: \'Cancel\' (a safe action) on the left and \'Done\' (a primary action) on the right.</p>' );
+ * this.$body.append( this.content.$element );
+ * };
+ * MyProcessDialog.prototype.getActionProcess = function ( action ) {
+ * var dialog = this;
+ * if ( action ) {
+ * return new OO.ui.Process( function () {
+ * dialog.close( { action: action } );
+ * } );
+ * }
+ * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
+ * };
+ *
+ * var windowManager = new OO.ui.WindowManager();
+ * $( 'body' ).append( windowManager.$element );
+ *
+ * var dialog = new MyProcessDialog();
+ * windowManager.addWindows( [ dialog ] );
+ * windowManager.openWindow( dialog );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Dialog
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
+ // Parent constructor
+ OO.ui.ProcessDialog.parent.call( this, config );
+
+ // Properties
+ this.fitOnOpen = false;
+
+ // Initialization
+ this.$element.addClass( 'oo-ui-processDialog' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
+
+/* Methods */
+
+/**
+ * Handle dismiss button click events.
+ *
+ * Hides errors.
+ *
+ * @private
+ */
+OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
+ this.hideErrors();
+};
+
+/**
+ * Handle retry button click events.
+ *
+ * Hides errors and then tries again.
+ *
+ * @private
+ */
+OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
+ this.hideErrors();
+ this.executeAction( this.currentAction );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
+ if ( this.actions.isSpecial( action ) ) {
+ this.fitLabel();
+ }
+ return OO.ui.ProcessDialog.parent.prototype.onActionResize.call( this, action );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ProcessDialog.prototype.initialize = function () {
+ // Parent method
+ OO.ui.ProcessDialog.parent.prototype.initialize.call( this );
+
+ // Properties
+ this.$navigation = $( '<div>' );
+ this.$location = $( '<div>' );
+ this.$safeActions = $( '<div>' );
+ this.$primaryActions = $( '<div>' );
+ this.$otherActions = $( '<div>' );
+ this.dismissButton = new OO.ui.ButtonWidget( {
+ label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
+ } );
+ this.retryButton = new OO.ui.ButtonWidget();
+ this.$errors = $( '<div>' );
+ this.$errorsTitle = $( '<div>' );
+
+ // 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 oo-ui-element-hidden' )
+ .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
+ this.$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 );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ProcessDialog.prototype.getActionWidgets = function ( actions ) {
+ var i, len, widgets = [];
+ for ( i = 0, len = actions.length; i < len; i++ ) {
+ widgets.push(
+ new OO.ui.ActionWidget( $.extend( { framed: true }, actions[ i ] ) )
+ );
+ }
+ return widgets;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ProcessDialog.prototype.attachActions = function () {
+ var i, len, other, special, others;
+
+ // Parent method
+ OO.ui.ProcessDialog.parent.prototype.attachActions.call( this );
+
+ special = this.actions.getSpecial();
+ others = this.actions.getOthers();
+ if ( special.primary ) {
+ this.$primaryActions.append( special.primary.$element );
+ }
+ for ( i = 0, len = others.length; i < len; i++ ) {
+ other = others[ i ];
+ this.$otherActions.append( other.$element );
+ }
+ if ( special.safe ) {
+ this.$safeActions.append( special.safe.$element );
+ }
+
+ this.fitLabel();
+ this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
+ var process = this;
+ return OO.ui.ProcessDialog.parent.prototype.executeAction.call( this, action )
+ .fail( function ( errors ) {
+ process.showErrors( errors || [] );
+ } );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ProcessDialog.prototype.setDimensions = function () {
+ // Parent method
+ OO.ui.ProcessDialog.parent.prototype.setDimensions.apply( this, arguments );
+
+ this.fitLabel();
+};
+
+/**
+ * Fit label between actions.
+ *
+ * @private
+ * @chainable
+ */
+OO.ui.ProcessDialog.prototype.fitLabel = function () {
+ var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth,
+ size = this.getSizeProperties();
+
+ if ( typeof size.width !== 'number' ) {
+ if ( this.isOpened() ) {
+ navigationWidth = this.$head.width() - 20;
+ } else if ( this.isOpening() ) {
+ if ( !this.fitOnOpen ) {
+ // Size is relative and the dialog isn't open yet, so wait.
+ this.manager.opening.done( this.fitLabel.bind( this ) );
+ this.fitOnOpen = true;
+ }
+ return;
+ } else {
+ return;
+ }
+ } else {
+ navigationWidth = size.width - 20;
+ }
+
+ safeWidth = this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0;
+ primaryWidth = this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0;
+ biggerWidth = Math.max( safeWidth, primaryWidth );
+
+ labelWidth = this.title.$element.width();
+
+ if ( 2 * biggerWidth + labelWidth < navigationWidth ) {
+ // We have enough space to center the label
+ leftWidth = rightWidth = biggerWidth;
+ } else {
+ // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
+ if ( this.getDir() === 'ltr' ) {
+ leftWidth = safeWidth;
+ rightWidth = primaryWidth;
+ } else {
+ leftWidth = primaryWidth;
+ rightWidth = safeWidth;
+ }
+ }
+
+ this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
+
+ return this;
+};
+
+/**
+ * Handle errors that occurred during accept or reject processes.
+ *
+ * @private
+ * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
+ */
+OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
+ var i, len, $item, actions,
+ items = [],
+ abilities = {},
+ recoverable = true,
+ warning = false;
+
+ if ( errors instanceof OO.ui.Error ) {
+ errors = [ errors ];
+ }
+
+ for ( i = 0, len = errors.length; i < len; i++ ) {
+ if ( !errors[ i ].isRecoverable() ) {
+ recoverable = false;
+ }
+ if ( errors[ i ].isWarning() ) {
+ warning = true;
+ }
+ $item = $( '<div>' )
+ .addClass( 'oo-ui-processDialog-error' )
+ .append( errors[ i ].getMessage() );
+ items.push( $item[ 0 ] );
+ }
+ this.$errorItems = $( items );
+ if ( recoverable ) {
+ abilities[ this.currentAction ] = true;
+ // Copy the flags from the first matching action
+ actions = this.actions.get( { actions: this.currentAction } );
+ if ( actions.length ) {
+ this.retryButton.clearFlags().setFlags( actions[ 0 ].getFlags() );
+ }
+ } else {
+ abilities[ this.currentAction ] = false;
+ this.actions.setAbilities( abilities );
+ }
+ if ( warning ) {
+ this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
+ } else {
+ this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
+ }
+ this.retryButton.toggle( recoverable );
+ this.$errorsTitle.after( this.$errorItems );
+ this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
+};
+
+/**
+ * Hide errors.
+ *
+ * @private
+ */
+OO.ui.ProcessDialog.prototype.hideErrors = function () {
+ this.$errors.addClass( 'oo-ui-element-hidden' );
+ if ( this.$errorItems ) {
+ this.$errorItems.remove();
+ this.$errorItems = null;
+ }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
+ // Parent method
+ return OO.ui.ProcessDialog.parent.prototype.getTeardownProcess.call( this, data )
+ .first( function () {
+ // Make sure to hide errors
+ this.hideErrors();
+ this.fitOnOpen = false;
+ }, this );
+};
+
+/**
+ * @class OO.ui
+ */
+
+/**
+ * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
+ * OO.ui.confirm.
+ *
+ * @private
+ * @return {OO.ui.WindowManager}
+ */
+OO.ui.getWindowManager = function () {
+ if ( !OO.ui.windowManager ) {
+ OO.ui.windowManager = new OO.ui.WindowManager();
+ $( 'body' ).append( OO.ui.windowManager.$element );
+ OO.ui.windowManager.addWindows( {
+ messageDialog: new OO.ui.MessageDialog()
+ } );
+ }
+ return OO.ui.windowManager;
+};
+
+/**
+ * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
+ * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
+ * has only one action button, labelled "OK", clicking it will simply close the dialog.
+ *
+ * A window manager is created automatically when this function is called for the first time.
+ *
+ * @example
+ * OO.ui.alert( 'Something happened!' ).done( function () {
+ * console.log( 'User closed the dialog.' );
+ * } );
+ *
+ * @param {jQuery|string} text Message text to display
+ * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
+ * @return {jQuery.Promise} Promise resolved when the user closes the dialog
+ */
+OO.ui.alert = function ( text, options ) {
+ return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
+ message: text,
+ verbose: true,
+ actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ]
+ }, options ) ).then( function ( opened ) {
+ return opened.then( function ( closing ) {
+ return closing.then( function () {
+ return $.Deferred().resolve();
+ } );
+ } );
+ } );
+};
+
+/**
+ * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
+ * the rest of the page will be dimmed out and the user won't be able to interact with it. The
+ * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
+ * (labelled "Cancel").
+ *
+ * A window manager is created automatically when this function is called for the first time.
+ *
+ * @example
+ * OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
+ * if ( confirmed ) {
+ * console.log( 'User clicked "OK"!' );
+ * } else {
+ * console.log( 'User clicked "Cancel" or closed the dialog.' );
+ * }
+ * } );
+ *
+ * @param {jQuery|string} text Message text to display
+ * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
+ * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
+ * confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
+ * `false`.
+ */
+OO.ui.confirm = function ( text, options ) {
+ return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
+ message: text,
+ verbose: true
+ }, options ) ).then( function ( opened ) {
+ return opened.then( function ( closing ) {
+ return closing.then( function ( data ) {
+ return $.Deferred().resolve( !!( data && data.action === 'accept' ) );
+ } );
+ } );
+ } );
+};
+
+}( OO ) );
+++ /dev/null
-/*!
- * OOjs UI v0.15.2
- * https://www.mediawiki.org/wiki/OOjs_UI
- *
- * Copyright 2011–2016 OOjs UI Team and other contributors.
- * Released under the MIT license
- * http://oojs.mit-license.org
- *
- * Date: 2016-02-02T22:07:00Z
- */
-( function ( OO ) {
-
-'use strict';
-
-/**
- * Namespace for all classes, static methods and static properties.
- *
- * @class
- * @singleton
- */
-OO.ui = {};
-
-OO.ui.bind = $.proxy;
-
-/**
- * @property {Object}
- */
-OO.ui.Keys = {
- UNDEFINED: 0,
- BACKSPACE: 8,
- DELETE: 46,
- LEFT: 37,
- RIGHT: 39,
- UP: 38,
- DOWN: 40,
- ENTER: 13,
- END: 35,
- HOME: 36,
- TAB: 9,
- PAGEUP: 33,
- PAGEDOWN: 34,
- ESCAPE: 27,
- SHIFT: 16,
- SPACE: 32
-};
-
-/**
- * Constants for MouseEvent.which
- *
- * @property {Object}
- */
-OO.ui.MouseButtons = {
- LEFT: 1,
- MIDDLE: 2,
- RIGHT: 3
-};
-
-/**
- * @property {Number}
- */
-OO.ui.elementId = 0;
-
-/**
- * Generate a unique ID for element
- *
- * @return {String} [id]
- */
-OO.ui.generateElementId = function () {
- OO.ui.elementId += 1;
- return 'oojsui-' + OO.ui.elementId;
-};
-
-/**
- * Check if an element is focusable.
- * Inspired from :focusable in jQueryUI v1.11.4 - 2015-04-14
- *
- * @param {jQuery} element Element to test
- * @return {boolean}
- */
-OO.ui.isFocusableElement = function ( $element ) {
- var nodeName,
- element = $element[ 0 ];
-
- // Anything disabled is not focusable
- if ( element.disabled ) {
- return false;
- }
-
- // Check if the element is visible
- if ( !(
- // This is quicker than calling $element.is( ':visible' )
- $.expr.filters.visible( element ) &&
- // Check that all parents are visible
- !$element.parents().addBack().filter( function () {
- return $.css( this, 'visibility' ) === 'hidden';
- } ).length
- ) ) {
- return false;
- }
-
- // Check if the element is ContentEditable, which is the string 'true'
- if ( element.contentEditable === 'true' ) {
- return true;
- }
-
- // Anything with a non-negative numeric tabIndex is focusable.
- // Use .prop to avoid browser bugs
- if ( $element.prop( 'tabIndex' ) >= 0 ) {
- return true;
- }
-
- // Some element types are naturally focusable
- // (indexOf is much faster than regex in Chrome and about the
- // same in FF: https://jsperf.com/regex-vs-indexof-array2)
- nodeName = element.nodeName.toLowerCase();
- if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName ) !== -1 ) {
- return true;
- }
-
- // Links and areas are focusable if they have an href
- if ( ( nodeName === 'a' || nodeName === 'area' ) && $element.attr( 'href' ) !== undefined ) {
- return true;
- }
-
- return false;
-};
-
-/**
- * Find a focusable child
- *
- * @param {jQuery} $container Container to search in
- * @param {boolean} [backwards] Search backwards
- * @return {jQuery} Focusable child, an empty jQuery object if none found
- */
-OO.ui.findFocusable = function ( $container, backwards ) {
- var $focusable = $( [] ),
- // $focusableCandidates is a superset of things that
- // could get matched by isFocusableElement
- $focusableCandidates = $container
- .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
-
- if ( backwards ) {
- $focusableCandidates = Array.prototype.reverse.call( $focusableCandidates );
- }
-
- $focusableCandidates.each( function () {
- var $this = $( this );
- if ( OO.ui.isFocusableElement( $this ) ) {
- $focusable = $this;
- return false;
- }
- } );
- return $focusable;
-};
-
-/**
- * Get the user's language and any fallback languages.
- *
- * These language codes are used to localize user interface elements in the user's language.
- *
- * In environments that provide a localization system, this function should be overridden to
- * return the user's language(s). The default implementation returns English (en) only.
- *
- * @return {string[]} Language codes, in descending order of priority
- */
-OO.ui.getUserLanguages = function () {
- return [ 'en' ];
-};
-
-/**
- * Get a value in an object keyed by language code.
- *
- * @param {Object.<string,Mixed>} obj Object keyed by language code
- * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
- * @param {string} [fallback] Fallback code, used if no matching language can be found
- * @return {Mixed} Local value
- */
-OO.ui.getLocalValue = function ( obj, lang, fallback ) {
- var i, len, langs;
-
- // Requested language
- if ( obj[ lang ] ) {
- return obj[ lang ];
- }
- // Known user language
- langs = OO.ui.getUserLanguages();
- for ( i = 0, len = langs.length; i < len; i++ ) {
- lang = langs[ i ];
- if ( obj[ lang ] ) {
- return obj[ lang ];
- }
- }
- // Fallback language
- if ( obj[ fallback ] ) {
- return obj[ fallback ];
- }
- // First existing language
- for ( lang in obj ) {
- return obj[ lang ];
- }
-
- return undefined;
-};
-
-/**
- * Check if a node is contained within another node
- *
- * Similar to jQuery#contains except a list of containers can be supplied
- * and a boolean argument allows you to include the container in the match list
- *
- * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
- * @param {HTMLElement} contained Node to find
- * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
- * @return {boolean} The node is in the list of target nodes
- */
-OO.ui.contains = function ( containers, contained, matchContainers ) {
- var i;
- if ( !Array.isArray( containers ) ) {
- containers = [ containers ];
- }
- for ( i = containers.length - 1; i >= 0; i-- ) {
- if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) {
- return true;
- }
- }
- return false;
-};
-
-/**
- * Return a function, that, as long as it continues to be invoked, will not
- * be triggered. The function will be called after it stops being called for
- * N milliseconds. If `immediate` is passed, trigger the function on the
- * leading edge, instead of the trailing.
- *
- * Ported from: http://underscorejs.org/underscore.js
- *
- * @param {Function} func
- * @param {number} wait
- * @param {boolean} immediate
- * @return {Function}
- */
-OO.ui.debounce = function ( func, wait, immediate ) {
- var timeout;
- return function () {
- var context = this,
- args = arguments,
- later = function () {
- timeout = null;
- if ( !immediate ) {
- func.apply( context, args );
- }
- };
- if ( immediate && !timeout ) {
- func.apply( context, args );
- }
- clearTimeout( timeout );
- timeout = setTimeout( later, wait );
- };
-};
-
-/**
- * Proxy for `node.addEventListener( eventName, handler, true )`.
- *
- * @param {HTMLElement} node
- * @param {string} eventName
- * @param {Function} handler
- * @deprecated
- */
-OO.ui.addCaptureEventListener = function ( node, eventName, handler ) {
- node.addEventListener( eventName, handler, true );
-};
-
-/**
- * Proxy for `node.removeEventListener( eventName, handler, true )`.
- *
- * @param {HTMLElement} node
- * @param {string} eventName
- * @param {Function} handler
- * @deprecated
- */
-OO.ui.removeCaptureEventListener = function ( node, eventName, handler ) {
- node.removeEventListener( eventName, handler, true );
-};
-
-/**
- * Reconstitute a JavaScript object corresponding to a widget created by
- * the PHP implementation.
- *
- * This is an alias for `OO.ui.Element.static.infuse()`.
- *
- * @param {string|HTMLElement|jQuery} idOrNode
- * A DOM id (if a string) or node for the widget to infuse.
- * @return {OO.ui.Element}
- * The `OO.ui.Element` corresponding to this (infusable) document node.
- */
-OO.ui.infuse = function ( idOrNode ) {
- return OO.ui.Element.static.infuse( idOrNode );
-};
-
-( function () {
- /**
- * Message store for the default implementation of OO.ui.msg
- *
- * Environments that provide a localization system should not use this, but should override
- * OO.ui.msg altogether.
- *
- * @private
- */
- var messages = {
- // Tool tip for a button that moves items in a list down one place
- 'ooui-outline-control-move-down': 'Move item down',
- // Tool tip for a button that moves items in a list up one place
- 'ooui-outline-control-move-up': 'Move item up',
- // Tool tip for a button that removes items from a list
- '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 fake tool that expands the full list of tools in a toolbar group
- 'ooui-toolgroup-expand': 'More',
- // Label for the fake tool that collapses the full list of tools in a toolbar group
- 'ooui-toolgroup-collapse': 'Fewer',
- // 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 only recoverable errors
- 'ooui-dialog-process-retry': 'Try again',
- // Label for process dialog retry action button, visible when describing only warnings
- 'ooui-dialog-process-continue': 'Continue',
- // Label for the file selection widget's select file button
- 'ooui-selectfile-button-select': 'Select a file',
- // Label for the file selection widget if file selection is not supported
- 'ooui-selectfile-not-supported': 'File selection is not supported',
- // Label for the file selection widget when no file is currently selected
- 'ooui-selectfile-placeholder': 'No file is selected',
- // Label for the file selection widget's drop target
- 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
- };
-
- /**
- * Get a localized message.
- *
- * In environments that provide a localization system, this function should be overridden to
- * return the message translated in the user's language. The default implementation always returns
- * English messages.
- *
- * After the message key, message parameters may optionally be passed. In the default implementation,
- * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
- * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
- * they support unnamed, ordered message parameters.
- *
- * @param {string} key Message key
- * @param {Mixed...} [params] Message parameters
- * @return {string} Translated message with parameters substituted
- */
- OO.ui.msg = function ( key ) {
- var message = messages[ key ],
- params = Array.prototype.slice.call( arguments, 1 );
- if ( typeof message === 'string' ) {
- // Perform $1 substitution
- message = message.replace( /\$(\d+)/g, function ( unused, n ) {
- var i = parseInt( n, 10 );
- return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
- } );
- } else {
- // Return placeholder if message not found
- message = '[' + key + ']';
- }
- return message;
- };
-} )();
-
-/**
- * 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.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();
- }
- return msg;
-};
-
-/**
- * @param {string} url
- * @return {boolean}
- */
-OO.ui.isSafeUrl = function ( url ) {
- // Keep this function in sync with php/Tag.php
- var i, protocolWhitelist;
-
- function stringStartsWith( haystack, needle ) {
- return haystack.substr( 0, needle.length ) === needle;
- }
-
- protocolWhitelist = [
- 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
- 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
- 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
- ];
-
- if ( url === '' ) {
- return true;
- }
-
- for ( i = 0; i < protocolWhitelist.length; i++ ) {
- if ( stringStartsWith( url, protocolWhitelist[ i ] + ':' ) ) {
- return true;
- }
- }
-
- // This matches '//' too
- if ( stringStartsWith( url, '/' ) || stringStartsWith( url, './' ) ) {
- return true;
- }
- if ( stringStartsWith( url, '?' ) || stringStartsWith( url, '#' ) ) {
- return true;
- }
-
- return false;
-};
-
-/*!
- * Mixin namespace.
- */
-
-/**
- * Namespace for OOjs UI mixins.
- *
- * Mixins are named according to the type of object they are intended to
- * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
- * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
- * is intended to be mixed in to an instance of OO.ui.Widget.
- *
- * @class
- * @singleton
- */
-OO.ui.mixin = {};
-
-/**
- * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
- * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
- * connected to them and can't be interacted with.
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
- * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
- * for an example.
- * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
- * @cfg {string} [id] The HTML id attribute used in the rendered tag.
- * @cfg {string} [text] Text to insert
- * @cfg {Array} [content] An array of content elements to append (after #text).
- * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
- * Instances of OO.ui.Element will have their $element appended.
- * @cfg {jQuery} [$content] Content elements to append (after #text).
- * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
- * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
- * Data can also be specified with the #setData method.
- */
-OO.ui.Element = function OoUiElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.$ = $;
- this.visible = true;
- this.data = config.data;
- this.$element = config.$element ||
- $( document.createElement( this.getTagName() ) );
- this.elementGroup = null;
- this.debouncedUpdateThemeClassesHandler = OO.ui.debounce( this.debouncedUpdateThemeClasses );
-
- // Initialization
- if ( Array.isArray( config.classes ) ) {
- this.$element.addClass( config.classes.join( ' ' ) );
- }
- if ( config.id ) {
- this.$element.attr( 'id', config.id );
- }
- if ( config.text ) {
- this.$element.text( config.text );
- }
- if ( config.content ) {
- // The `content` property treats plain strings as text; use an
- // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
- // appropriate $element appended.
- this.$element.append( config.content.map( function ( v ) {
- if ( typeof v === 'string' ) {
- // Escape string so it is properly represented in HTML.
- return document.createTextNode( v );
- } else if ( v instanceof OO.ui.HtmlSnippet ) {
- // Bypass escaping.
- return v.toString();
- } else if ( v instanceof OO.ui.Element ) {
- return v.$element;
- }
- return v;
- } ) );
- }
- if ( config.$content ) {
- // The `$content` property treats plain strings as HTML.
- this.$element.append( config.$content );
- }
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.Element );
-
-/* Static Properties */
-
-/**
- * The name of the HTML tag used by the element.
- *
- * The static value may be ignored if the #getTagName method is overridden.
- *
- * @static
- * @inheritable
- * @property {string}
- */
-OO.ui.Element.static.tagName = 'div';
-
-/* Static Methods */
-
-/**
- * Reconstitute a JavaScript object corresponding to a widget created
- * by the PHP implementation.
- *
- * @param {string|HTMLElement|jQuery} idOrNode
- * A DOM id (if a string) or node for the widget to infuse.
- * @return {OO.ui.Element}
- * The `OO.ui.Element` corresponding to this (infusable) document node.
- * For `Tag` objects emitted on the HTML side (used occasionally for content)
- * the value returned is a newly-created Element wrapping around the existing
- * DOM node.
- */
-OO.ui.Element.static.infuse = function ( idOrNode ) {
- var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, false );
- // Verify that the type matches up.
- // FIXME: uncomment after T89721 is fixed (see T90929)
- /*
- if ( !( obj instanceof this['class'] ) ) {
- throw new Error( 'Infusion type mismatch!' );
- }
- */
- return obj;
-};
-
-/**
- * Implementation helper for `infuse`; skips the type check and has an
- * extra property so that only the top-level invocation touches the DOM.
- * @private
- * @param {string|HTMLElement|jQuery} idOrNode
- * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
- * when the top-level widget of this infusion is inserted into DOM,
- * replacing the original node; or false for top-level invocation.
- * @return {OO.ui.Element}
- */
-OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
- // look for a cached result of a previous infusion.
- var id, $elem, data, cls, parts, parent, obj, top, state;
- if ( typeof idOrNode === 'string' ) {
- id = idOrNode;
- $elem = $( document.getElementById( id ) );
- } else {
- $elem = $( idOrNode );
- id = $elem.attr( 'id' );
- }
- if ( !$elem.length ) {
- throw new Error( 'Widget not found: ' + id );
- }
- data = $elem.data( 'ooui-infused' ) || $elem[ 0 ].oouiInfused;
- if ( data ) {
- // cached!
- if ( data === true ) {
- throw new Error( 'Circular dependency! ' + id );
- }
- return data;
- }
- data = $elem.attr( 'data-ooui' );
- if ( !data ) {
- throw new Error( 'No infusion data found: ' + id );
- }
- try {
- data = $.parseJSON( data );
- } catch ( _ ) {
- data = null;
- }
- if ( !( data && data._ ) ) {
- throw new Error( 'No valid infusion data found: ' + id );
- }
- if ( data._ === 'Tag' ) {
- // Special case: this is a raw Tag; wrap existing node, don't rebuild.
- return new OO.ui.Element( { $element: $elem } );
- }
- parts = data._.split( '.' );
- cls = OO.getProp.apply( OO, [ window ].concat( parts ) );
- if ( cls === undefined ) {
- // The PHP output might be old and not including the "OO.ui" prefix
- // TODO: Remove this back-compat after next major release
- cls = OO.getProp.apply( OO, [ OO.ui ].concat( parts ) );
- if ( cls === undefined ) {
- throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
- }
- }
-
- // Verify that we're creating an OO.ui.Element instance
- parent = cls.parent;
-
- while ( parent !== undefined ) {
- if ( parent === OO.ui.Element ) {
- // Safe
- break;
- }
-
- parent = parent.parent;
- }
-
- if ( parent !== OO.ui.Element ) {
- throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
- }
-
- if ( domPromise === false ) {
- top = $.Deferred();
- domPromise = top.promise();
- }
- $elem.data( 'ooui-infused', true ); // prevent loops
- data.id = id; // implicit
- data = OO.copy( data, null, function deserialize( value ) {
- if ( OO.isPlainObject( value ) ) {
- if ( value.tag ) {
- return OO.ui.Element.static.unsafeInfuse( value.tag, domPromise );
- }
- if ( value.html ) {
- return new OO.ui.HtmlSnippet( value.html );
- }
- }
- } );
- // allow widgets to reuse parts of the DOM
- data = cls.static.reusePreInfuseDOM( $elem[ 0 ], data );
- // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
- state = cls.static.gatherPreInfuseState( $elem[ 0 ], data );
- // rebuild widget
- // jscs:disable requireCapitalizedConstructors
- obj = new cls( data );
- // jscs:enable requireCapitalizedConstructors
- // now replace old DOM with this new DOM.
- if ( top ) {
- // An efficient constructor might be able to reuse the entire DOM tree of the original element,
- // so only mutate the DOM if we need to.
- if ( $elem[ 0 ] !== obj.$element[ 0 ] ) {
- $elem.replaceWith( obj.$element );
- // This element is now gone from the DOM, but if anyone is holding a reference to it,
- // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
- // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
- $elem[ 0 ].oouiInfused = obj;
- }
- top.resolve();
- }
- obj.$element.data( 'ooui-infused', obj );
- // set the 'data-ooui' attribute so we can identify infused widgets
- obj.$element.attr( 'data-ooui', '' );
- // restore dynamic state after the new element is inserted into DOM
- domPromise.done( obj.restorePreInfuseState.bind( obj, state ) );
- return obj;
-};
-
-/**
- * Pick out parts of `node`'s DOM to be reused when infusing a widget.
- *
- * This method **must not** make any changes to the DOM, only find interesting pieces and add them
- * to `config` (which should then be returned). Actual DOM juggling should then be done by the
- * constructor, which will be given the enhanced config.
- *
- * @protected
- * @param {HTMLElement} node
- * @param {Object} config
- * @return {Object}
- */
-OO.ui.Element.static.reusePreInfuseDOM = function ( node, config ) {
- return config;
-};
-
-/**
- * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
- * (and its children) that represent an Element of the same class and the given configuration,
- * generated by the PHP implementation.
- *
- * This method is called just before `node` is detached from the DOM. The return value of this
- * function will be passed to #restorePreInfuseState after the newly created widget's #$element
- * is inserted into DOM to replace `node`.
- *
- * @protected
- * @param {HTMLElement} node
- * @param {Object} config
- * @return {Object}
- */
-OO.ui.Element.static.gatherPreInfuseState = function () {
- return {};
-};
-
-/**
- * Get a jQuery function within a specific document.
- *
- * @static
- * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
- * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
- * not in an iframe
- * @return {Function} Bound jQuery function
- */
-OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
- function wrapper( selector ) {
- return $( selector, wrapper.context );
- }
-
- wrapper.context = this.getDocument( context );
-
- if ( $iframe ) {
- wrapper.$iframe = $iframe;
- }
-
- return wrapper;
-};
-
-/**
- * Get the document of an element.
- *
- * @static
- * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
- * @return {HTMLDocument|null} Document object
- */
-OO.ui.Element.static.getDocument = function ( obj ) {
- // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
- return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
- // Empty jQuery selections might have a context
- obj.context ||
- // HTMLElement
- obj.ownerDocument ||
- // Window
- obj.document ||
- // HTMLDocument
- ( obj.nodeType === 9 && obj ) ||
- null;
-};
-
-/**
- * Get the window of an element or document.
- *
- * @static
- * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
- * @return {Window} Window object
- */
-OO.ui.Element.static.getWindow = function ( obj ) {
- var doc = this.getDocument( obj );
- return doc.defaultView;
-};
-
-/**
- * Get the direction of an element or document.
- *
- * @static
- * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
- * @return {string} Text direction, either 'ltr' or 'rtl'
- */
-OO.ui.Element.static.getDir = function ( obj ) {
- var isDoc, isWin;
-
- if ( obj instanceof jQuery ) {
- obj = obj[ 0 ];
- }
- isDoc = obj.nodeType === 9;
- isWin = obj.document !== undefined;
- if ( isDoc || isWin ) {
- if ( isWin ) {
- obj = obj.document;
- }
- obj = obj.body;
- }
- return $( obj ).css( 'direction' );
-};
-
-/**
- * Get the offset between two frames.
- *
- * TODO: Make this function not use recursion.
- *
- * @static
- * @param {Window} from Window of the child frame
- * @param {Window} [to=window] Window of the parent frame
- * @param {Object} [offset] Offset to start with, used internally
- * @return {Object} Offset object, containing left and top properties
- */
-OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
- var i, len, frames, frame, rect;
-
- if ( !to ) {
- to = window;
- }
- if ( !offset ) {
- offset = { top: 0, left: 0 };
- }
- if ( from.parent === from ) {
- return offset;
- }
-
- // Get iframe element
- frames = from.parent.document.getElementsByTagName( 'iframe' );
- for ( i = 0, len = frames.length; i < len; i++ ) {
- if ( frames[ i ].contentWindow === from ) {
- frame = frames[ i ];
- break;
- }
- }
-
- // Recursively accumulate offset values
- if ( frame ) {
- rect = frame.getBoundingClientRect();
- offset.left += rect.left;
- offset.top += rect.top;
- if ( from !== to ) {
- this.getFrameOffset( from.parent, offset );
- }
- }
- return offset;
-};
-
-/**
- * Get the offset between two elements.
- *
- * The two elements may be in a different frame, but in that case the frame $element is in must
- * be contained in the frame $anchor is in.
- *
- * @static
- * @param {jQuery} $element Element whose position to get
- * @param {jQuery} $anchor Element to get $element's position relative to
- * @return {Object} Translated position coordinates, containing top and left properties
- */
-OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
- var iframe, iframePos,
- pos = $element.offset(),
- anchorPos = $anchor.offset(),
- elementDocument = this.getDocument( $element ),
- anchorDocument = this.getDocument( $anchor );
-
- // If $element isn't in the same document as $anchor, traverse up
- while ( elementDocument !== anchorDocument ) {
- iframe = elementDocument.defaultView.frameElement;
- if ( !iframe ) {
- throw new Error( '$element frame is not contained in $anchor frame' );
- }
- iframePos = $( iframe ).offset();
- pos.left += iframePos.left;
- pos.top += iframePos.top;
- elementDocument = iframe.ownerDocument;
- }
- pos.left -= anchorPos.left;
- pos.top -= anchorPos.top;
- return pos;
-};
-
-/**
- * Get element border sizes.
- *
- * @static
- * @param {HTMLElement} el Element to measure
- * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
- */
-OO.ui.Element.static.getBorders = function ( el ) {
- var doc = el.ownerDocument,
- win = doc.defaultView,
- style = win.getComputedStyle( el, null ),
- $el = $( el ),
- top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
- left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
- bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
- right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
-
- return {
- top: top,
- left: left,
- bottom: bottom,
- right: right
- };
-};
-
-/**
- * Get dimensions of an element or window.
- *
- * @static
- * @param {HTMLElement|Window} el Element to measure
- * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
- */
-OO.ui.Element.static.getDimensions = function ( el ) {
- var $el, $win,
- doc = el.ownerDocument || el.document,
- win = doc.defaultView;
-
- if ( win === el || el === doc.documentElement ) {
- $win = $( win );
- return {
- borders: { top: 0, left: 0, bottom: 0, right: 0 },
- scroll: {
- top: $win.scrollTop(),
- left: $win.scrollLeft()
- },
- scrollbar: { right: 0, bottom: 0 },
- rect: {
- top: 0,
- left: 0,
- bottom: $win.innerHeight(),
- right: $win.innerWidth()
- }
- };
- } else {
- $el = $( el );
- return {
- borders: this.getBorders( el ),
- scroll: {
- top: $el.scrollTop(),
- left: $el.scrollLeft()
- },
- scrollbar: {
- right: $el.innerWidth() - el.clientWidth,
- bottom: $el.innerHeight() - el.clientHeight
- },
- rect: el.getBoundingClientRect()
- };
- }
-};
-
-/**
- * Get scrollable object parent
- *
- * documentElement can't be used to get or set the scrollTop
- * property on Blink. Changing and testing its value lets us
- * use 'body' or 'documentElement' based on what is working.
- *
- * https://code.google.com/p/chromium/issues/detail?id=303131
- *
- * @static
- * @param {HTMLElement} el Element to find scrollable parent for
- * @return {HTMLElement} Scrollable parent
- */
-OO.ui.Element.static.getRootScrollableElement = function ( el ) {
- var scrollTop, body;
-
- if ( OO.ui.scrollableElement === undefined ) {
- body = el.ownerDocument.body;
- scrollTop = body.scrollTop;
- body.scrollTop = 1;
-
- if ( body.scrollTop === 1 ) {
- body.scrollTop = scrollTop;
- OO.ui.scrollableElement = 'body';
- } else {
- OO.ui.scrollableElement = 'documentElement';
- }
- }
-
- return el.ownerDocument[ OO.ui.scrollableElement ];
-};
-
-/**
- * Get closest scrollable container.
- *
- * Traverses up until either a scrollable element or the root is reached, in which case the window
- * will be returned.
- *
- * @static
- * @param {HTMLElement} el Element to find scrollable container for
- * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
- * @return {HTMLElement} Closest scrollable container
- */
-OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
- var i, val,
- // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
- props = [ 'overflow-x', 'overflow-y' ],
- $parent = $( el ).parent();
-
- if ( dimension === 'x' || dimension === 'y' ) {
- props = [ 'overflow-' + dimension ];
- }
-
- while ( $parent.length ) {
- if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
- return $parent[ 0 ];
- }
- i = props.length;
- while ( i-- ) {
- val = $parent.css( props[ i ] );
- if ( val === 'auto' || val === 'scroll' ) {
- return $parent[ 0 ];
- }
- }
- $parent = $parent.parent();
- }
- return this.getDocument( el ).body;
-};
-
-/**
- * Scroll element into view.
- *
- * @static
- * @param {HTMLElement} el Element to scroll into view
- * @param {Object} [config] Configuration options
- * @param {string} [config.duration] jQuery animation duration value
- * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
- * to scroll in both directions
- * @param {Function} [config.complete] Function to call when scrolling completes
- */
-OO.ui.Element.static.scrollIntoView = function ( el, config ) {
- var rel, anim, callback, sc, $sc, eld, scd, $win;
-
- // Configuration initialization
- config = config || {};
-
- anim = {};
- callback = typeof config.complete === 'function' && config.complete;
- sc = this.getClosestScrollableContainer( el, config.direction );
- $sc = $( sc );
- eld = this.getDimensions( el );
- scd = this.getDimensions( sc );
- $win = $( this.getWindow( el ) );
-
- // Compute the distances between the edges of el and the edges of the scroll viewport
- if ( $sc.is( 'html, body' ) ) {
- // If the scrollable container is the root, this is easy
- rel = {
- top: eld.rect.top,
- bottom: $win.innerHeight() - eld.rect.bottom,
- left: eld.rect.left,
- right: $win.innerWidth() - eld.rect.right
- };
- } else {
- // Otherwise, we have to subtract el's coordinates from sc's coordinates
- rel = {
- top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
- bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
- left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
- right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
- };
- }
-
- if ( !config.direction || config.direction === 'y' ) {
- if ( rel.top < 0 ) {
- anim.scrollTop = scd.scroll.top + rel.top;
- } else if ( rel.top > 0 && rel.bottom < 0 ) {
- anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
- }
- }
- if ( !config.direction || config.direction === 'x' ) {
- if ( rel.left < 0 ) {
- anim.scrollLeft = scd.scroll.left + rel.left;
- } else if ( rel.left > 0 && rel.right < 0 ) {
- anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
- }
- }
- if ( !$.isEmptyObject( anim ) ) {
- $sc.stop( true ).animate( anim, config.duration || 'fast' );
- if ( callback ) {
- $sc.queue( function ( next ) {
- callback();
- next();
- } );
- }
- } else {
- if ( callback ) {
- callback();
- }
- }
-};
-
-/**
- * Force the browser to reconsider whether it really needs to render scrollbars inside the element
- * and reserve space for them, because it probably doesn't.
- *
- * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
- * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
- * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
- * and then reattach (or show) them back.
- *
- * @static
- * @param {HTMLElement} el Element to reconsider the scrollbars on
- */
-OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
- var i, len, scrollLeft, scrollTop, nodes = [];
- // Save scroll position
- scrollLeft = el.scrollLeft;
- scrollTop = el.scrollTop;
- // Detach all children
- while ( el.firstChild ) {
- nodes.push( el.firstChild );
- el.removeChild( el.firstChild );
- }
- // Force reflow
- void el.offsetHeight;
- // Reattach all children
- for ( i = 0, len = nodes.length; i < len; i++ ) {
- el.appendChild( nodes[ i ] );
- }
- // Restore scroll position (no-op if scrollbars disappeared)
- el.scrollLeft = scrollLeft;
- el.scrollTop = scrollTop;
-};
-
-/* Methods */
-
-/**
- * Toggle visibility of an element.
- *
- * @param {boolean} [show] Make element visible, omit to toggle visibility
- * @fires visible
- * @chainable
- */
-OO.ui.Element.prototype.toggle = function ( show ) {
- show = show === undefined ? !this.visible : !!show;
-
- if ( show !== this.isVisible() ) {
- this.visible = show;
- this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
- this.emit( 'toggle', show );
- }
-
- return this;
-};
-
-/**
- * Check if element is visible.
- *
- * @return {boolean} element is visible
- */
-OO.ui.Element.prototype.isVisible = function () {
- return this.visible;
-};
-
-/**
- * Get element data.
- *
- * @return {Mixed} Element data
- */
-OO.ui.Element.prototype.getData = function () {
- return this.data;
-};
-
-/**
- * Set element data.
- *
- * @param {Mixed} Element data
- * @chainable
- */
-OO.ui.Element.prototype.setData = function ( data ) {
- this.data = data;
- return this;
-};
-
-/**
- * Check if element supports one or more methods.
- *
- * @param {string|string[]} methods Method or list of methods to check
- * @return {boolean} All methods are supported
- */
-OO.ui.Element.prototype.supports = function ( methods ) {
- var i, len,
- support = 0;
-
- methods = Array.isArray( methods ) ? methods : [ methods ];
- for ( i = 0, len = methods.length; i < len; i++ ) {
- if ( $.isFunction( this[ methods[ i ] ] ) ) {
- support++;
- }
- }
-
- return methods.length === support;
-};
-
-/**
- * Update the theme-provided classes.
- *
- * @localdoc This is called in element mixins and widget classes any time state changes.
- * Updating is debounced, minimizing overhead of changing multiple attributes and
- * guaranteeing that theme updates do not occur within an element's constructor
- */
-OO.ui.Element.prototype.updateThemeClasses = function () {
- this.debouncedUpdateThemeClassesHandler();
-};
-
-/**
- * @private
- * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to
- * make them synchronous.
- */
-OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
- OO.ui.theme.updateElementClasses( this );
-};
-
-/**
- * Get the HTML tag name.
- *
- * Override this method to base the result on instance information.
- *
- * @return {string} HTML tag name
- */
-OO.ui.Element.prototype.getTagName = function () {
- return this.constructor.static.tagName;
-};
-
-/**
- * Check if the element is attached to the DOM
- * @return {boolean} The element is attached to the DOM
- */
-OO.ui.Element.prototype.isElementAttached = function () {
- return $.contains( this.getElementDocument(), this.$element[ 0 ] );
-};
-
-/**
- * Get the DOM document.
- *
- * @return {HTMLDocument} Document object
- */
-OO.ui.Element.prototype.getElementDocument = function () {
- // Don't cache this in other ways either because subclasses could can change this.$element
- return OO.ui.Element.static.getDocument( this.$element );
-};
-
-/**
- * Get the DOM window.
- *
- * @return {Window} Window object
- */
-OO.ui.Element.prototype.getElementWindow = function () {
- return OO.ui.Element.static.getWindow( this.$element );
-};
-
-/**
- * Get closest scrollable container.
- */
-OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
- return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
-};
-
-/**
- * Get group element is in.
- *
- * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
- */
-OO.ui.Element.prototype.getElementGroup = function () {
- return this.elementGroup;
-};
-
-/**
- * Set group element is in.
- *
- * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
- * @chainable
- */
-OO.ui.Element.prototype.setElementGroup = function ( group ) {
- this.elementGroup = group;
- return this;
-};
-
-/**
- * Scroll element into view.
- *
- * @param {Object} [config] Configuration options
- */
-OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
- return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
-};
-
-/**
- * Restore the pre-infusion dynamic state for this widget.
- *
- * This method is called after #$element has been inserted into DOM. The parameter is the return
- * value of #gatherPreInfuseState.
- *
- * @protected
- * @param {Object} state
- */
-OO.ui.Element.prototype.restorePreInfuseState = function () {
-};
-
-/**
- * Wraps an HTML snippet for use with configuration values which default
- * to strings. This bypasses the default html-escaping done to string
- * values.
- *
- * @class
- *
- * @constructor
- * @param {string} [content] HTML content
- */
-OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
- // Properties
- this.content = content;
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.HtmlSnippet );
-
-/* Methods */
-
-/**
- * Render into HTML.
- *
- * @return {string} Unchanged HTML snippet.
- */
-OO.ui.HtmlSnippet.prototype.toString = function () {
- return this.content;
-};
-
-/**
- * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
- * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
- * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
- * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
- * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
- *
- * @abstract
- * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.Layout = function OoUiLayout( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.Layout.parent.call( this, config );
-
- // Mixin constructors
- OO.EventEmitter.call( this );
-
- // Initialization
- this.$element.addClass( 'oo-ui-layout' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.Layout, OO.ui.Element );
-OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
-
-/**
- * Widgets are compositions of one or more OOjs UI elements that users can both view
- * and interact with. All widgets can be configured and modified via a standard API,
- * and their state can change dynamically according to a model.
- *
- * @abstract
- * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
- * appearance reflects this state.
- */
-OO.ui.Widget = function OoUiWidget( config ) {
- // Initialize config
- config = $.extend( { disabled: false }, config );
-
- // Parent constructor
- OO.ui.Widget.parent.call( this, config );
-
- // Mixin constructors
- OO.EventEmitter.call( this );
-
- // Properties
- 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 );
-
-/* Static Properties */
-
-/**
- * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
- * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
- * handling.
- *
- * @static
- * @inheritable
- * @property {boolean}
- */
-OO.ui.Widget.static.supportsSimpleLabel = false;
-
-/* Events */
-
-/**
- * @event disable
- *
- * A 'disable' event is emitted when the disabled state of the widget changes
- * (i.e. on disable **and** enable).
- *
- * @param {boolean} disabled Widget is disabled
- */
-
-/**
- * @event toggle
- *
- * A 'toggle' event is emitted when the visibility of the widget changes.
- *
- * @param {boolean} visible Widget is visible
- */
-
-/* Methods */
-
-/**
- * Check if the widget is disabled.
- *
- * @return {boolean} Widget is disabled
- */
-OO.ui.Widget.prototype.isDisabled = function () {
- return this.disabled;
-};
-
-/**
- * Set the 'disabled' state of the widget.
- *
- * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
- *
- * @param {boolean} disabled Disable widget
- * @chainable
- */
-OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
- var isDisabled;
-
- 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.$element.attr( 'aria-disabled', isDisabled.toString() );
- this.emit( 'disable', isDisabled );
- this.updateThemeClasses();
- }
- this.wasDisabled = isDisabled;
-
- return this;
-};
-
-/**
- * Update the disabled state, in case of changes in parent widget.
- *
- * @chainable
- */
-OO.ui.Widget.prototype.updateDisabled = function () {
- this.setDisabled( this.disabled );
- return this;
-};
-
-/**
- * Theme logic.
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.Theme = function OoUiTheme( config ) {
- // Configuration initialization
- config = config || {};
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.Theme );
-
-/* Methods */
-
-/**
- * Get a list of classes to be applied to a widget.
- *
- * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
- * otherwise state transitions will not work properly.
- *
- * @param {OO.ui.Element} element Element for which to get classes
- * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
- */
-OO.ui.Theme.prototype.getElementClasses = function () {
- return { on: [], off: [] };
-};
-
-/**
- * Update CSS classes provided by the theme.
- *
- * For elements with theme logic hooks, this should be called any time there's a state change.
- *
- * @param {OO.ui.Element} element Element for which to update classes
- * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
- */
-OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
- var $elements = $( [] ),
- classes = this.getElementClasses( element );
-
- if ( element.$icon ) {
- $elements = $elements.add( element.$icon );
- }
- if ( element.$indicator ) {
- $elements = $elements.add( element.$indicator );
- }
-
- $elements
- .removeClass( classes.off.join( ' ' ) )
- .addClass( classes.on.join( ' ' ) );
-};
-
-/**
- * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
- * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
- * order in which users will navigate through the focusable elements via the "tab" key.
- *
- * @example
- * // TabIndexedElement is mixed into the ButtonWidget class
- * // to provide a tabIndex property.
- * var button1 = new OO.ui.ButtonWidget( {
- * label: 'fourth',
- * tabIndex: 4
- * } );
- * var button2 = new OO.ui.ButtonWidget( {
- * label: 'second',
- * tabIndex: 2
- * } );
- * var button3 = new OO.ui.ButtonWidget( {
- * label: 'third',
- * tabIndex: 3
- * } );
- * var button4 = new OO.ui.ButtonWidget( {
- * label: 'first',
- * tabIndex: 1
- * } );
- * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
- * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
- * functionality will be applied to it instead.
- * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
- * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
- * to remove the element from the tab-navigation flow.
- */
-OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
- // Configuration initialization
- config = $.extend( { tabIndex: 0 }, config );
-
- // Properties
- this.$tabIndexed = null;
- this.tabIndex = null;
-
- // Events
- this.connect( this, { disable: 'onTabIndexedElementDisable' } );
-
- // Initialization
- this.setTabIndex( config.tabIndex );
- this.setTabIndexedElement( config.$tabIndexed || this.$element );
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.mixin.TabIndexedElement );
-
-/* Methods */
-
-/**
- * Set the element that should use the tabindex functionality.
- *
- * This method is used to retarget a tabindex mixin so that its functionality applies
- * to the specified element. If an element is currently using the functionality, the mixin’s
- * effect on that element is removed before the new element is set up.
- *
- * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
- * @chainable
- */
-OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
- var tabIndex = this.tabIndex;
- // Remove attributes from old $tabIndexed
- this.setTabIndex( null );
- // Force update of new $tabIndexed
- this.$tabIndexed = $tabIndexed;
- this.tabIndex = tabIndex;
- return this.updateTabIndex();
-};
-
-/**
- * Set the value of the tabindex.
- *
- * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
- * @chainable
- */
-OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
- tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
-
- if ( this.tabIndex !== tabIndex ) {
- this.tabIndex = tabIndex;
- this.updateTabIndex();
- }
-
- return this;
-};
-
-/**
- * Update the `tabindex` attribute, in case of changes to tab index or
- * disabled state.
- *
- * @private
- * @chainable
- */
-OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
- if ( this.$tabIndexed ) {
- if ( this.tabIndex !== null ) {
- // Do not index over disabled elements
- this.$tabIndexed.attr( {
- tabindex: this.isDisabled() ? -1 : this.tabIndex,
- // Support: ChromeVox and NVDA
- // These do not seem to inherit aria-disabled from parent elements
- 'aria-disabled': this.isDisabled().toString()
- } );
- } else {
- this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
- }
- }
- return this;
-};
-
-/**
- * Handle disable events.
- *
- * @private
- * @param {boolean} disabled Element is disabled
- */
-OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
- this.updateTabIndex();
-};
-
-/**
- * Get the value of the tabindex.
- *
- * @return {number|null} Tabindex value
- */
-OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
- return this.tabIndex;
-};
-
-/**
- * ButtonElement is often mixed into other classes to generate a button, which is a clickable
- * interface element that can be configured with access keys for accessibility.
- * See the [OOjs UI documentation on MediaWiki] [1] for examples.
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$button] The button element created by the class.
- * If this configuration is omitted, the button element will use a generated `<a>`.
- * @cfg {boolean} [framed=true] Render the button with a frame
- */
-OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.$button = null;
- this.framed = null;
- this.active = false;
- this.onMouseUpHandler = this.onMouseUp.bind( this );
- this.onMouseDownHandler = this.onMouseDown.bind( this );
- this.onKeyDownHandler = this.onKeyDown.bind( this );
- this.onKeyUpHandler = this.onKeyUp.bind( this );
- this.onClickHandler = this.onClick.bind( this );
- this.onKeyPressHandler = this.onKeyPress.bind( this );
-
- // Initialization
- this.$element.addClass( 'oo-ui-buttonElement' );
- this.toggleFramed( config.framed === undefined || config.framed );
- this.setButtonElement( config.$button || $( '<a>' ) );
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.mixin.ButtonElement );
-
-/* Static Properties */
-
-/**
- * Cancel mouse down events.
- *
- * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
- * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
- * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
- * parent widget.
- *
- * @static
- * @inheritable
- * @property {boolean}
- */
-OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
-
-/* Events */
-
-/**
- * A 'click' event is emitted when the button element is clicked.
- *
- * @event click
- */
-
-/* Methods */
-
-/**
- * Set the button element.
- *
- * This method is used to retarget a button mixin so that its functionality applies to
- * the specified button element instead of the one created by the class. If a button element
- * is already set, the method will remove the mixin’s effect on that element.
- *
- * @param {jQuery} $button Element to use as button
- */
-OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
- if ( this.$button ) {
- this.$button
- .removeClass( 'oo-ui-buttonElement-button' )
- .removeAttr( 'role accesskey' )
- .off( {
- mousedown: this.onMouseDownHandler,
- keydown: this.onKeyDownHandler,
- click: this.onClickHandler,
- keypress: this.onKeyPressHandler
- } );
- }
-
- this.$button = $button
- .addClass( 'oo-ui-buttonElement-button' )
- .attr( { role: 'button' } )
- .on( {
- mousedown: this.onMouseDownHandler,
- keydown: this.onKeyDownHandler,
- click: this.onClickHandler,
- keypress: this.onKeyPressHandler
- } );
-};
-
-/**
- * Handles mouse down events.
- *
- * @protected
- * @param {jQuery.Event} e Mouse down event
- */
-OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
- if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
- return;
- }
- this.$element.addClass( 'oo-ui-buttonElement-pressed' );
- // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
- // reliably 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;
- }
-};
-
-/**
- * Handles mouse up events.
- *
- * @protected
- * @param {jQuery.Event} e Mouse up event
- */
-OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
- if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
- return;
- }
- this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
- // Stop listening for mouseup, since we only needed this once
- this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
-};
-
-/**
- * Handles mouse click events.
- *
- * @protected
- * @param {jQuery.Event} e Mouse click event
- * @fires click
- */
-OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
- if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
- if ( this.emit( 'click' ) ) {
- return false;
- }
- }
-};
-
-/**
- * Handles key down events.
- *
- * @protected
- * @param {jQuery.Event} e Key down event
- */
-OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
- if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
- return;
- }
- this.$element.addClass( 'oo-ui-buttonElement-pressed' );
- // Run the keyup handler no matter where the key is when the button is let go, so we can
- // reliably remove the pressed class
- this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
-};
-
-/**
- * Handles key up events.
- *
- * @protected
- * @param {jQuery.Event} e Key up event
- */
-OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
- if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
- return;
- }
- this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
- // Stop listening for keyup, since we only needed this once
- this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
-};
-
-/**
- * Handles key press events.
- *
- * @protected
- * @param {jQuery.Event} e Key press event
- * @fires click
- */
-OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
- if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
- if ( this.emit( 'click' ) ) {
- return false;
- }
- }
-};
-
-/**
- * Check if button has a frame.
- *
- * @return {boolean} Button is framed
- */
-OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
- return this.framed;
-};
-
-/**
- * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
- *
- * @param {boolean} [framed] Make button framed, omit to toggle
- * @chainable
- */
-OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
- framed = framed === undefined ? !this.framed : !!framed;
- if ( framed !== this.framed ) {
- this.framed = framed;
- this.$element
- .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
- .toggleClass( 'oo-ui-buttonElement-framed', framed );
- this.updateThemeClasses();
- }
-
- return this;
-};
-
-/**
- * Set the button's active state.
- *
- * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
- * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
- * for other button types.
- *
- * @param {boolean} value Make button active
- * @chainable
- */
-OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
- this.active = !!value;
- this.$element.toggleClass( 'oo-ui-buttonElement-active', this.active );
- return this;
-};
-
-/**
- * Check if the button is active
- *
- * @return {boolean} The button is active
- */
-OO.ui.mixin.ButtonElement.prototype.isActive = function () {
- return this.active;
-};
-
-/**
- * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
- * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
- * items from the group is done through the interface the class provides.
- * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$group] The container element created by the class. If this configuration
- * is omitted, the group element will use a generated `<div>`.
- */
-OO.ui.mixin.GroupElement = function OoUiMixinGroupElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.$group = null;
- this.items = [];
- this.aggregateItemEvents = {};
-
- // Initialization
- this.setGroupElement( config.$group || $( '<div>' ) );
-};
-
-/* Methods */
-
-/**
- * Set the group element.
- *
- * If an element is already set, items will be moved to the new element.
- *
- * @param {jQuery} $group Element to use as group
- */
-OO.ui.mixin.GroupElement.prototype.setGroupElement = function ( $group ) {
- var i, len;
-
- this.$group = $group;
- for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.$group.append( this.items[ i ].$element );
- }
-};
-
-/**
- * Check if a group contains no items.
- *
- * @return {boolean} Group is empty
- */
-OO.ui.mixin.GroupElement.prototype.isEmpty = function () {
- return !this.items.length;
-};
-
-/**
- * Get all items in the group.
- *
- * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
- * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
- * from a group).
- *
- * @return {OO.ui.Element[]} An array of items.
- */
-OO.ui.mixin.GroupElement.prototype.getItems = function () {
- return this.items.slice( 0 );
-};
-
-/**
- * Get an item by its data.
- *
- * Only the first item with matching data will be returned. To return all matching items,
- * use the #getItemsFromData method.
- *
- * @param {Object} data Item data to search for
- * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
- */
-OO.ui.mixin.GroupElement.prototype.getItemFromData = function ( data ) {
- var i, len, item,
- hash = OO.getHash( data );
-
- for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[ i ];
- if ( hash === OO.getHash( item.getData() ) ) {
- return item;
- }
- }
-
- return null;
-};
-
-/**
- * Get items by their data.
- *
- * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
- *
- * @param {Object} data Item data to search for
- * @return {OO.ui.Element[]} Items with equivalent data
- */
-OO.ui.mixin.GroupElement.prototype.getItemsFromData = function ( data ) {
- var i, len, item,
- hash = OO.getHash( data ),
- items = [];
-
- for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[ i ];
- if ( hash === OO.getHash( item.getData() ) ) {
- items.push( item );
- }
- }
-
- return items;
-};
-
-/**
- * Aggregate the events emitted by the group.
- *
- * When events are aggregated, the group will listen to all contained items for the event,
- * and then emit the event under a new name. The new event will contain an additional leading
- * parameter containing the item that emitted the original event. Other arguments emitted from
- * the original event are passed through.
- *
- * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
- * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
- * A `null` value will remove aggregated events.
-
- * @throws {Error} An error is thrown if aggregation already exists.
- */
-OO.ui.mixin.GroupElement.prototype.aggregate = function ( events ) {
- var i, len, item, add, remove, itemEvent, groupEvent;
-
- for ( itemEvent in events ) {
- groupEvent = events[ itemEvent ];
-
- // Remove existing aggregated event
- if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
- // 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', this.aggregateItemEvents[ itemEvent ], item ];
- item.disconnect( this, remove );
- }
- }
- // Prevent future items from aggregating event
- delete this.aggregateItemEvents[ itemEvent ];
- }
-
- // 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 );
- }
- }
- }
- }
-};
-
-/**
- * Add items to the group.
- *
- * Items will be added to the end of the group array unless the optional `index` parameter specifies
- * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
- *
- * @param {OO.ui.Element[]} items An array of items to add to the group
- * @param {number} [index] Index of the insertion point
- * @chainable
- */
-OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
- var i, len, item, event, events, currentIndex,
- itemElements = [];
-
- for ( i = 0, len = items.length; i < len; i++ ) {
- item = items[ i ];
-
- // Check if item exists then remove it first, effectively "moving" it
- currentIndex = this.items.indexOf( item );
- if ( currentIndex >= 0 ) {
- this.removeItems( [ item ] );
- // Adjust index to compensate for removal
- if ( currentIndex < index ) {
- index--;
- }
- }
- // Add the item
- if ( 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;
-};
-
-/**
- * Remove the specified items from a group.
- *
- * Removed items are detached (not removed) from the DOM so that they may be reused.
- * To remove all items from a group, you may wish to use the #clearItems method instead.
- *
- * @param {OO.ui.Element[]} items An array of items to remove
- * @chainable
- */
-OO.ui.mixin.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 = this.items.indexOf( item );
- if ( index !== -1 ) {
- if (
- item.connect && item.disconnect &&
- !$.isEmptyObject( this.aggregateItemEvents )
- ) {
- remove = {};
- if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
- remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
- }
- item.disconnect( this, remove );
- }
- item.setElementGroup( null );
- this.items.splice( index, 1 );
- item.$element.detach();
- }
- }
-
- return this;
-};
-
-/**
- * Clear all items from the group.
- *
- * Cleared items are detached from the DOM, not removed, so that they may be reused.
- * To remove only a subset of items from a group, use the #removeItems method.
- *
- * @chainable
- */
-OO.ui.mixin.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 ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
- remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
- }
- item.disconnect( this, remove );
- }
- item.setElementGroup( null );
- item.$element.detach();
- }
-
- this.items = [];
- return this;
-};
-
-/**
- * IconElement is often mixed into other classes to generate an icon.
- * Icons are graphics, about the size of normal text. They are used to aid the user
- * in locating a control or to convey information in a space-efficient way. See the
- * [OOjs UI documentation on MediaWiki] [1] for a list of icons
- * included in the library.
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
- * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
- * the icon element be set to an existing icon instead of the one generated by this class, set a
- * value using a jQuery selection. For example:
- *
- * // Use a <div> tag instead of a <span>
- * $icon: $("<div>")
- * // Use an existing icon element instead of the one generated by the class
- * $icon: this.$element
- * // Use an icon element from a child widget
- * $icon: this.childwidget.$element
- * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
- * symbolic names. A map is used for i18n purposes and contains a `default` icon
- * name and additional names keyed by language code. The `default` name is used when no icon is keyed
- * by the user's language.
- *
- * Example of an i18n map:
- *
- * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
- * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
- * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
- * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
- * text. The icon title is displayed when users move the mouse over the icon.
- */
-OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.$icon = null;
- this.icon = null;
- this.iconTitle = null;
-
- // Initialization
- this.setIcon( config.icon || this.constructor.static.icon );
- this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
- this.setIconElement( config.$icon || $( '<span>' ) );
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.mixin.IconElement );
-
-/* Static Properties */
-
-/**
- * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
- * for i18n purposes and contains a `default` icon name and additional names keyed by
- * language code. The `default` name is used when no icon is keyed by the user's language.
- *
- * Example of an i18n map:
- *
- * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
- *
- * Note: the static property will be overridden if the #icon configuration is used.
- *
- * @static
- * @inheritable
- * @property {Object|string}
- */
-OO.ui.mixin.IconElement.static.icon = null;
-
-/**
- * The icon title, displayed when users move the mouse over the icon. The value can be text, a
- * function that returns title text, or `null` for no title.
- *
- * The static property will be overridden if the #iconTitle configuration is used.
- *
- * @static
- * @inheritable
- * @property {string|Function|null}
- */
-OO.ui.mixin.IconElement.static.iconTitle = null;
-
-/* Methods */
-
-/**
- * Set the icon element. This method is used to retarget an icon mixin so that its functionality
- * applies to the specified icon element instead of the one created by the class. If an icon
- * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
- * and mixin methods will no longer affect the element.
- *
- * @param {jQuery} $icon Element to use as icon
- */
-OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
- if ( this.$icon ) {
- this.$icon
- .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
- .removeAttr( 'title' );
- }
-
- this.$icon = $icon
- .addClass( 'oo-ui-iconElement-icon' )
- .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
- if ( this.iconTitle !== null ) {
- this.$icon.attr( 'title', this.iconTitle );
- }
-
- this.updateThemeClasses();
-};
-
-/**
- * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
- * The icon parameter can also be set to a map of icon names. See the #icon config setting
- * for an example.
- *
- * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
- * by language code, or `null` to remove the icon.
- * @chainable
- */
-OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
- icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
- icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
-
- if ( this.icon !== icon ) {
- if ( this.$icon ) {
- if ( this.icon !== null ) {
- this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
- }
- if ( icon !== null ) {
- this.$icon.addClass( 'oo-ui-icon-' + icon );
- }
- }
- this.icon = icon;
- }
-
- this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
- this.updateThemeClasses();
-
- return this;
-};
-
-/**
- * Set the icon title. Use `null` to remove the title.
- *
- * @param {string|Function|null} iconTitle A text string used as the icon title,
- * a function that returns title text, or `null` for no title.
- * @chainable
- */
-OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
- iconTitle = typeof iconTitle === 'function' ||
- ( typeof iconTitle === 'string' && iconTitle.length ) ?
- OO.ui.resolveMsg( iconTitle ) : null;
-
- if ( this.iconTitle !== iconTitle ) {
- this.iconTitle = iconTitle;
- if ( this.$icon ) {
- if ( this.iconTitle !== null ) {
- this.$icon.attr( 'title', iconTitle );
- } else {
- this.$icon.removeAttr( 'title' );
- }
- }
- }
-
- return this;
-};
-
-/**
- * Get the symbolic name of the icon.
- *
- * @return {string} Icon name
- */
-OO.ui.mixin.IconElement.prototype.getIcon = function () {
- return this.icon;
-};
-
-/**
- * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
- *
- * @return {string} Icon title text
- */
-OO.ui.mixin.IconElement.prototype.getIconTitle = function () {
- return this.iconTitle;
-};
-
-/**
- * IndicatorElement is often mixed into other classes to generate an indicator.
- * Indicators are small graphics that are generally used in two ways:
- *
- * - To draw attention to the status of an item. For example, an indicator might be
- * used to show that an item in a list has errors that need to be resolved.
- * - To clarify the function of a control that acts in an exceptional way (a button
- * that opens a menu instead of performing an action directly, for example).
- *
- * For a list of indicators included in the library, please see the
- * [OOjs UI documentation on MediaWiki] [1].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
- * configuration is omitted, the indicator element will use a generated `<span>`.
- * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
- * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
- * in the library.
- * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
- * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
- * or a function that returns title text. The indicator title is displayed when users move
- * the mouse over the indicator.
- */
-OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.$indicator = null;
- this.indicator = null;
- this.indicatorTitle = null;
-
- // Initialization
- this.setIndicator( config.indicator || this.constructor.static.indicator );
- this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
- this.setIndicatorElement( config.$indicator || $( '<span>' ) );
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.mixin.IndicatorElement );
-
-/* Static Properties */
-
-/**
- * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
- * The static property will be overridden if the #indicator configuration is used.
- *
- * @static
- * @inheritable
- * @property {string|null}
- */
-OO.ui.mixin.IndicatorElement.static.indicator = null;
-
-/**
- * A text string used as the indicator title, a function that returns title text, or `null`
- * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
- *
- * @static
- * @inheritable
- * @property {string|Function|null}
- */
-OO.ui.mixin.IndicatorElement.static.indicatorTitle = null;
-
-/* Methods */
-
-/**
- * Set the indicator element.
- *
- * If an element is already set, it will be cleaned up before setting up the new element.
- *
- * @param {jQuery} $indicator Element to use as indicator
- */
-OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
- if ( this.$indicator ) {
- this.$indicator
- .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
- .removeAttr( 'title' );
- }
-
- this.$indicator = $indicator
- .addClass( 'oo-ui-indicatorElement-indicator' )
- .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
- if ( this.indicatorTitle !== null ) {
- this.$indicator.attr( 'title', this.indicatorTitle );
- }
-
- this.updateThemeClasses();
-};
-
-/**
- * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
- *
- * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
- * @chainable
- */
-OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
- indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
-
- if ( this.indicator !== indicator ) {
- if ( this.$indicator ) {
- if ( this.indicator !== null ) {
- this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
- }
- if ( indicator !== null ) {
- this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
- }
- }
- this.indicator = indicator;
- }
-
- this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
- this.updateThemeClasses();
-
- return this;
-};
-
-/**
- * Set the indicator title.
- *
- * The title is displayed when a user moves the mouse over the indicator.
- *
- * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
- * `null` for no indicator title
- * @chainable
- */
-OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
- indicatorTitle = typeof indicatorTitle === 'function' ||
- ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
- OO.ui.resolveMsg( indicatorTitle ) : null;
-
- if ( this.indicatorTitle !== indicatorTitle ) {
- this.indicatorTitle = indicatorTitle;
- if ( this.$indicator ) {
- if ( this.indicatorTitle !== null ) {
- this.$indicator.attr( 'title', indicatorTitle );
- } else {
- this.$indicator.removeAttr( 'title' );
- }
- }
- }
-
- return this;
-};
-
-/**
- * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
- *
- * @return {string} Symbolic name of indicator
- */
-OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () {
- return this.indicator;
-};
-
-/**
- * Get the indicator title.
- *
- * The title is displayed when a user moves the mouse over the indicator.
- *
- * @return {string} Indicator title text
- */
-OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
- return this.indicatorTitle;
-};
-
-/**
- * LabelElement is often mixed into other classes to generate a label, which
- * helps identify the function of an interface element.
- * See the [OOjs UI documentation on MediaWiki] [1] for more information.
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$label] The label element created by the class. If this
- * configuration is omitted, the label element will use a generated `<span>`.
- * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
- * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
- * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
- * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
- * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
- * The label will be truncated to fit if necessary.
- */
-OO.ui.mixin.LabelElement = function OoUiMixinLabelElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.$label = null;
- this.label = null;
- this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
-
- // Initialization
- this.setLabel( config.label || this.constructor.static.label );
- this.setLabelElement( config.$label || $( '<span>' ) );
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.mixin.LabelElement );
-
-/* Events */
-
-/**
- * @event labelChange
- * @param {string} value
- */
-
-/* Static Properties */
-
-/**
- * The label text. The label can be specified as a plaintext string, a function that will
- * produce a string in the future, or `null` for no label. The static value will
- * be overridden if a label is specified with the #label config option.
- *
- * @static
- * @inheritable
- * @property {string|Function|null}
- */
-OO.ui.mixin.LabelElement.static.label = null;
-
-/* Methods */
-
-/**
- * Set the label element.
- *
- * If an element is already set, it will be cleaned up before setting up the new element.
- *
- * @param {jQuery} $label Element to use as label
- */
-OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
- if ( this.$label ) {
- this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
- }
-
- this.$label = $label.addClass( 'oo-ui-labelElement-label' );
- this.setLabelContent( this.label );
-};
-
-/**
- * Set the label.
- *
- * An empty string will result in the label being hidden. A string containing only whitespace will
- * be converted to a single ` `.
- *
- * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
- * text; or null for no label
- * @chainable
- */
-OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
- label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
- label = ( ( typeof label === 'string' && label.length ) || label instanceof jQuery || label instanceof OO.ui.HtmlSnippet ) ? label : null;
-
- this.$element.toggleClass( 'oo-ui-labelElement', !!label );
-
- if ( this.label !== label ) {
- if ( this.$label ) {
- this.setLabelContent( label );
- }
- this.label = label;
- this.emit( 'labelChange' );
- }
-
- return this;
-};
-
-/**
- * Get the label.
- *
- * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
- * text; or null for no label
- */
-OO.ui.mixin.LabelElement.prototype.getLabel = function () {
- return this.label;
-};
-
-/**
- * Fit the label.
- *
- * @chainable
- */
-OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
- if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
- this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
- }
-
- return this;
-};
-
-/**
- * Set the content of the label.
- *
- * Do not call this method until after the label element has been set by #setLabelElement.
- *
- * @private
- * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
- * text; or null for no label
- */
-OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
- if ( typeof label === 'string' ) {
- if ( label.match( /^\s*$/ ) ) {
- // Convert whitespace only string to a single non-breaking space
- this.$label.html( ' ' );
- } else {
- this.$label.text( label );
- }
- } else if ( label instanceof OO.ui.HtmlSnippet ) {
- this.$label.html( label.toString() );
- } else if ( label instanceof jQuery ) {
- this.$label.empty().append( label );
- } else {
- this.$label.empty();
- }
-};
-
-/**
- * The FlaggedElement class is an attribute mixin, meaning that it is used to add
- * additional functionality to an element created by another class. The class provides
- * a ‘flags’ property assigned the name (or an array of names) of styling flags,
- * which are used to customize the look and feel of a widget to better describe its
- * importance and functionality.
- *
- * The library currently contains the following styling flags for general use:
- *
- * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
- * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
- * - **constructive**: Constructive styling is applied to convey that the widget will create something.
- *
- * The flags affect the appearance of the buttons:
- *
- * @example
- * // FlaggedElement is mixed into ButtonWidget to provide styling flags
- * var button1 = new OO.ui.ButtonWidget( {
- * label: 'Constructive',
- * flags: 'constructive'
- * } );
- * var button2 = new OO.ui.ButtonWidget( {
- * label: 'Destructive',
- * flags: 'destructive'
- * } );
- * var button3 = new OO.ui.ButtonWidget( {
- * label: 'Progressive',
- * flags: 'progressive'
- * } );
- * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
- *
- * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
- * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
- * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
- * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
- * @cfg {jQuery} [$flagged] The flagged element. By default,
- * the flagged functionality is applied to the element created by the class ($element).
- * If a different element is specified, the flagged functionality will be applied to it instead.
- */
-OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.flags = {};
- this.$flagged = null;
-
- // Initialization
- this.setFlags( config.flags );
- this.setFlaggedElement( config.$flagged || this.$element );
-};
-
-/* Events */
-
-/**
- * @event flag
- * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
- * parameter contains the name of each modified flag and indicates whether it was
- * added or removed.
- *
- * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
- * that the flag was added, `false` that the flag was removed.
- */
-
-/* Methods */
-
-/**
- * Set the flagged element.
- *
- * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
- * If an element is already set, the method will remove the mixin’s effect on that element.
- *
- * @param {jQuery} $flagged Element that should be flagged
- */
-OO.ui.mixin.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
- var classNames = Object.keys( this.flags ).map( function ( flag ) {
- return 'oo-ui-flaggedElement-' + flag;
- } ).join( ' ' );
-
- if ( this.$flagged ) {
- this.$flagged.removeClass( classNames );
- }
-
- this.$flagged = $flagged.addClass( classNames );
-};
-
-/**
- * Check if the specified flag is set.
- *
- * @param {string} flag Name of flag
- * @return {boolean} The flag is set
- */
-OO.ui.mixin.FlaggedElement.prototype.hasFlag = function ( flag ) {
- // This may be called before the constructor, thus before this.flags is set
- return this.flags && ( flag in this.flags );
-};
-
-/**
- * Get the names of all flags set.
- *
- * @return {string[]} Flag names
- */
-OO.ui.mixin.FlaggedElement.prototype.getFlags = function () {
- // This may be called before the constructor, thus before this.flags is set
- return Object.keys( this.flags || {} );
-};
-
-/**
- * Clear all flags.
- *
- * @chainable
- * @fires flag
- */
-OO.ui.mixin.FlaggedElement.prototype.clearFlags = function () {
- var flag, className,
- changes = {},
- remove = [],
- classPrefix = 'oo-ui-flaggedElement-';
-
- for ( flag in this.flags ) {
- className = classPrefix + flag;
- changes[ flag ] = false;
- delete this.flags[ flag ];
- remove.push( className );
- }
-
- if ( this.$flagged ) {
- this.$flagged.removeClass( remove.join( ' ' ) );
- }
-
- this.updateThemeClasses();
- this.emit( 'flag', changes );
-
- return this;
-};
-
-/**
- * Add one or more flags.
- *
- * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
- * or an object keyed by flag name with a boolean value that indicates whether the flag should
- * be added (`true`) or removed (`false`).
- * @chainable
- * @fires flag
- */
-OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
- var i, len, flag, className,
- changes = {},
- add = [],
- remove = [],
- classPrefix = 'oo-ui-flaggedElement-';
-
- if ( typeof flags === 'string' ) {
- className = classPrefix + flags;
- // Set
- if ( !this.flags[ flags ] ) {
- this.flags[ flags ] = true;
- add.push( className );
- }
- } else if ( Array.isArray( flags ) ) {
- for ( i = 0, len = flags.length; i < len; i++ ) {
- flag = flags[ i ];
- className = classPrefix + flag;
- // Set
- if ( !this.flags[ flag ] ) {
- changes[ flag ] = true;
- this.flags[ flag ] = true;
- add.push( className );
- }
- }
- } else if ( OO.isPlainObject( flags ) ) {
- for ( flag in flags ) {
- className = classPrefix + flag;
- if ( flags[ flag ] ) {
- // Set
- if ( !this.flags[ flag ] ) {
- changes[ flag ] = true;
- this.flags[ flag ] = true;
- add.push( className );
- }
- } else {
- // Remove
- if ( this.flags[ flag ] ) {
- changes[ flag ] = false;
- delete this.flags[ flag ];
- remove.push( className );
- }
- }
- }
- }
-
- if ( this.$flagged ) {
- this.$flagged
- .addClass( add.join( ' ' ) )
- .removeClass( remove.join( ' ' ) );
- }
-
- this.updateThemeClasses();
- this.emit( 'flag', changes );
-
- return this;
-};
-
-/**
- * TitledElement is mixed into other classes to provide a `title` attribute.
- * Titles are rendered by the browser and are made visible when the user moves
- * the mouse over the element. Titles are not visible on touch devices.
- *
- * @example
- * // TitledElement provides a 'title' attribute to the
- * // ButtonWidget class
- * var button = new OO.ui.ButtonWidget( {
- * label: 'Button with Title',
- * title: 'I am a button'
- * } );
- * $( 'body' ).append( button.$element );
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
- * If this config is omitted, the title functionality is applied to $element, the
- * element created by the class.
- * @cfg {string|Function} [title] The title text or a function that returns text. If
- * this config is omitted, the value of the {@link #static-title static title} property is used.
- */
-OO.ui.mixin.TitledElement = function OoUiMixinTitledElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.$titled = null;
- this.title = null;
-
- // Initialization
- this.setTitle( config.title || this.constructor.static.title );
- this.setTitledElement( config.$titled || this.$element );
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.mixin.TitledElement );
-
-/* Static Properties */
-
-/**
- * The title text, a function that returns text, or `null` for no title. The value of the static property
- * is overridden if the #title config option is used.
- *
- * @static
- * @inheritable
- * @property {string|Function|null}
- */
-OO.ui.mixin.TitledElement.static.title = null;
-
-/* Methods */
-
-/**
- * Set the titled element.
- *
- * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
- * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
- *
- * @param {jQuery} $titled Element that should use the 'titled' functionality
- */
-OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
- if ( this.$titled ) {
- this.$titled.removeAttr( 'title' );
- }
-
- this.$titled = $titled;
- if ( this.title ) {
- this.$titled.attr( 'title', this.title );
- }
-};
-
-/**
- * Set title.
- *
- * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
- * @chainable
- */
-OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
- title = typeof title === 'function' ? OO.ui.resolveMsg( title ) : title;
- title = ( typeof title === 'string' && title.length ) ? title : null;
-
- if ( this.title !== title ) {
- if ( this.$titled ) {
- if ( title !== null ) {
- this.$titled.attr( 'title', title );
- } else {
- this.$titled.removeAttr( 'title' );
- }
- }
- this.title = title;
- }
-
- return this;
-};
-
-/**
- * Get title.
- *
- * @return {string} Title string
- */
-OO.ui.mixin.TitledElement.prototype.getTitle = function () {
- return this.title;
-};
-
-/**
- * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
- * Accesskeys allow an user to go to a specific element by using
- * a shortcut combination of a browser specific keys + the key
- * set to the field.
- *
- * @example
- * // AccessKeyedElement provides an 'accesskey' attribute to the
- * // ButtonWidget class
- * var button = new OO.ui.ButtonWidget( {
- * label: 'Button with Accesskey',
- * accessKey: 'k'
- * } );
- * $( 'body' ).append( button.$element );
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
- * If this config is omitted, the accesskey functionality is applied to $element, the
- * element created by the class.
- * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
- * this config is omitted, no accesskey will be added.
- */
-OO.ui.mixin.AccessKeyedElement = function OoUiMixinAccessKeyedElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.$accessKeyed = null;
- this.accessKey = null;
-
- // Initialization
- this.setAccessKey( config.accessKey || null );
- this.setAccessKeyedElement( config.$accessKeyed || this.$element );
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.mixin.AccessKeyedElement );
-
-/* Static Properties */
-
-/**
- * The access key, a function that returns a key, or `null` for no accesskey.
- *
- * @static
- * @inheritable
- * @property {string|Function|null}
- */
-OO.ui.mixin.AccessKeyedElement.static.accessKey = null;
-
-/* Methods */
-
-/**
- * Set the accesskeyed element.
- *
- * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
- * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
- *
- * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
- */
-OO.ui.mixin.AccessKeyedElement.prototype.setAccessKeyedElement = function ( $accessKeyed ) {
- if ( this.$accessKeyed ) {
- this.$accessKeyed.removeAttr( 'accesskey' );
- }
-
- this.$accessKeyed = $accessKeyed;
- if ( this.accessKey ) {
- this.$accessKeyed.attr( 'accesskey', this.accessKey );
- }
-};
-
-/**
- * Set accesskey.
- *
- * @param {string|Function|null} accesskey Key, a function that returns a key, or `null` for no accesskey
- * @chainable
- */
-OO.ui.mixin.AccessKeyedElement.prototype.setAccessKey = function ( accessKey ) {
- accessKey = typeof accessKey === 'string' ? OO.ui.resolveMsg( accessKey ) : null;
-
- if ( this.accessKey !== accessKey ) {
- if ( this.$accessKeyed ) {
- if ( accessKey !== null ) {
- this.$accessKeyed.attr( 'accesskey', accessKey );
- } else {
- this.$accessKeyed.removeAttr( 'accesskey' );
- }
- }
- this.accessKey = accessKey;
- }
-
- return this;
-};
-
-/**
- * Get accesskey.
- *
- * @return {string} accessKey string
- */
-OO.ui.mixin.AccessKeyedElement.prototype.getAccessKey = function () {
- return this.accessKey;
-};
-
-/**
- * ButtonWidget is a generic widget for buttons. A wide variety of looks,
- * feels, and functionality can be customized via the class’s configuration options
- * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
- * and examples.
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
- *
- * @example
- * // A button widget
- * var button = new OO.ui.ButtonWidget( {
- * label: 'Button with Icon',
- * icon: 'remove',
- * iconTitle: 'Remove'
- * } );
- * $( 'body' ).append( button.$element );
- *
- * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.ButtonElement
- * @mixins OO.ui.mixin.IconElement
- * @mixins OO.ui.mixin.IndicatorElement
- * @mixins OO.ui.mixin.LabelElement
- * @mixins OO.ui.mixin.TitledElement
- * @mixins OO.ui.mixin.FlaggedElement
- * @mixins OO.ui.mixin.TabIndexedElement
- * @mixins OO.ui.mixin.AccessKeyedElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string} [href] Hyperlink to visit when the button is clicked.
- * @cfg {string} [target] The frame or window in which to open the hyperlink.
- * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
- */
-OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.ButtonWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.ButtonElement.call( this, config );
- OO.ui.mixin.IconElement.call( this, config );
- OO.ui.mixin.IndicatorElement.call( this, config );
- OO.ui.mixin.LabelElement.call( this, config );
- OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
- OO.ui.mixin.FlaggedElement.call( this, config );
- OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
- OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$button } ) );
-
- // Properties
- this.href = null;
- this.target = null;
- this.noFollow = false;
-
- // Events
- this.connect( this, { disable: 'onDisable' } );
-
- // Initialization
- 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 );
- this.setNoFollow( config.noFollow );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.ButtonElement );
-OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IconElement );
-OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IndicatorElement );
-OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TitledElement );
-OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
-OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
-OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.AccessKeyedElement );
-
-/* Methods */
-
-/**
- * @inheritdoc
- */
-OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
- if ( !this.isDisabled() ) {
- // Remove the tab-index while the button is down to prevent the button from stealing focus
- this.$button.removeAttr( 'tabindex' );
- }
-
- return OO.ui.mixin.ButtonElement.prototype.onMouseDown.call( this, e );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
- if ( !this.isDisabled() ) {
- // Restore the tab-index after the button is up to restore the button's accessibility
- this.$button.attr( 'tabindex', this.tabIndex );
- }
-
- return OO.ui.mixin.ButtonElement.prototype.onMouseUp.call( this, e );
-};
-
-/**
- * Get hyperlink location.
- *
- * @return {string} Hyperlink location
- */
-OO.ui.ButtonWidget.prototype.getHref = function () {
- return this.href;
-};
-
-/**
- * Get hyperlink target.
- *
- * @return {string} Hyperlink target
- */
-OO.ui.ButtonWidget.prototype.getTarget = function () {
- return this.target;
-};
-
-/**
- * Get search engine traversal hint.
- *
- * @return {boolean} Whether search engines should avoid traversing this hyperlink
- */
-OO.ui.ButtonWidget.prototype.getNoFollow = function () {
- return this.noFollow;
-};
-
-/**
- * Set hyperlink location.
- *
- * @param {string|null} href Hyperlink location, null to remove
- */
-OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
- href = typeof href === 'string' ? href : null;
- if ( href !== null && !OO.ui.isSafeUrl( href ) ) {
- href = './' + href;
- }
-
- if ( href !== this.href ) {
- this.href = href;
- this.updateHref();
- }
-
- return this;
-};
-
-/**
- * Update the `href` attribute, in case of changes to href or
- * disabled state.
- *
- * @private
- * @chainable
- */
-OO.ui.ButtonWidget.prototype.updateHref = function () {
- if ( this.href !== null && !this.isDisabled() ) {
- this.$button.attr( 'href', this.href );
- } else {
- this.$button.removeAttr( 'href' );
- }
-
- return this;
-};
-
-/**
- * Handle disable events.
- *
- * @private
- * @param {boolean} disabled Element is disabled
- */
-OO.ui.ButtonWidget.prototype.onDisable = function () {
- this.updateHref();
-};
-
-/**
- * Set hyperlink target.
- *
- * @param {string|null} target Hyperlink target, null to remove
- */
-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;
-};
-
-/**
- * Set search engine traversal hint.
- *
- * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
- */
-OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
- noFollow = typeof noFollow === 'boolean' ? noFollow : true;
-
- if ( noFollow !== this.noFollow ) {
- this.noFollow = noFollow;
- if ( noFollow ) {
- this.$button.attr( 'rel', 'nofollow' );
- } else {
- this.$button.removeAttr( 'rel' );
- }
- }
-
- return this;
-};
-
-/**
- * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
- * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
- * removed, and cleared from the group.
- *
- * @example
- * // Example: A ButtonGroupWidget with two buttons
- * var button1 = new OO.ui.PopupButtonWidget( {
- * label: 'Select a category',
- * icon: 'menu',
- * popup: {
- * $content: $( '<p>List of categories...</p>' ),
- * padded: true,
- * align: 'left'
- * }
- * } );
- * var button2 = new OO.ui.ButtonWidget( {
- * label: 'Add item'
- * });
- * var buttonGroup = new OO.ui.ButtonGroupWidget( {
- * items: [button1, button2]
- * } );
- * $( 'body' ).append( buttonGroup.$element );
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.GroupElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
- */
-OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.ButtonGroupWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
-
- // Initialization
- this.$element.addClass( 'oo-ui-buttonGroupWidget' );
- if ( Array.isArray( config.items ) ) {
- this.addItems( config.items );
- }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
-
-/**
- * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
- * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
- * for a list of icons included in the library.
- *
- * @example
- * // An icon widget with a label
- * var myIcon = new OO.ui.IconWidget( {
- * icon: 'help',
- * iconTitle: 'Help'
- * } );
- * // Create a label.
- * var iconLabel = new OO.ui.LabelWidget( {
- * label: 'Help'
- * } );
- * $( 'body' ).append( myIcon.$element, iconLabel.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.IconElement
- * @mixins OO.ui.mixin.TitledElement
- * @mixins OO.ui.mixin.FlaggedElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.IconWidget = function OoUiIconWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.IconWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
- OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
- OO.ui.mixin.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) );
-
- // Initialization
- this.$element.addClass( 'oo-ui-iconWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.IconElement );
-OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.TitledElement );
-OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.FlaggedElement );
-
-/* Static Properties */
-
-OO.ui.IconWidget.static.tagName = 'span';
-
-/**
- * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
- * attention to the status of an item or to clarify the function of a control. For a list of
- * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * @example
- * // Example of an indicator widget
- * var indicator1 = new OO.ui.IndicatorWidget( {
- * indicator: 'alert'
- * } );
- *
- * // Create a fieldset layout to add a label
- * var fieldset = new OO.ui.FieldsetLayout();
- * fieldset.addItems( [
- * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
- * ] );
- * $( 'body' ).append( fieldset.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.IndicatorElement
- * @mixins OO.ui.mixin.TitledElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.IndicatorWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
- OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
-
- // Initialization
- this.$element.addClass( 'oo-ui-indicatorWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.IndicatorElement );
-OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.TitledElement );
-
-/* Static Properties */
-
-OO.ui.IndicatorWidget.static.tagName = 'span';
-
-/**
- * LabelWidgets help identify the function of interface elements. Each LabelWidget can
- * be configured with a `label` option that is set to a string, a label node, or a function:
- *
- * - String: a plaintext string
- * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
- * label that includes a link or special styling, such as a gray color or additional graphical elements.
- * - Function: a function that will produce a string in the future. Functions are used
- * in cases where the value of the label is not currently defined.
- *
- * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
- * will come into focus when the label is clicked.
- *
- * @example
- * // Examples of LabelWidgets
- * var label1 = new OO.ui.LabelWidget( {
- * label: 'plaintext label'
- * } );
- * var label2 = new OO.ui.LabelWidget( {
- * label: $( '<a href="default.html">jQuery label</a>' )
- * } );
- * // Create a fieldset layout with fields for each example
- * var fieldset = new OO.ui.FieldsetLayout();
- * fieldset.addItems( [
- * new OO.ui.FieldLayout( label1 ),
- * new OO.ui.FieldLayout( label2 )
- * ] );
- * $( 'body' ).append( fieldset.$element );
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.LabelElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
- * Clicking the label will focus the specified input field.
- */
-OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.LabelWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
- OO.ui.mixin.TitledElement.call( this, config );
-
- // Properties
- this.input = config.input;
-
- // Events
- if ( this.input instanceof OO.ui.InputWidget ) {
- this.$element.on( 'click', this.onClick.bind( this ) );
- }
-
- // Initialization
- this.$element.addClass( 'oo-ui-labelWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement );
-
-/* Static Properties */
-
-OO.ui.LabelWidget.static.tagName = 'span';
-
-/* Methods */
-
-/**
- * Handles label mouse click events.
- *
- * @private
- * @param {jQuery.Event} e Mouse click event
- */
-OO.ui.LabelWidget.prototype.onClick = function () {
- this.input.simulateLabelClick();
- return false;
-};
-
-/**
- * PendingElement is a mixin that is used to create elements that notify users that something is happening
- * and that they should wait before proceeding. The pending state is visually represented with a pending
- * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
- * field of a {@link OO.ui.TextInputWidget text input widget}.
- *
- * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
- * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
- * in process dialogs.
- *
- * @example
- * function MessageDialog( config ) {
- * MessageDialog.parent.call( this, config );
- * }
- * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
- *
- * MessageDialog.static.actions = [
- * { action: 'save', label: 'Done', flags: 'primary' },
- * { label: 'Cancel', flags: 'safe' }
- * ];
- *
- * MessageDialog.prototype.initialize = function () {
- * MessageDialog.parent.prototype.initialize.apply( this, arguments );
- * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
- * this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending state. Note that action widgets can be marked pending in message dialogs but not process dialogs.</p>' );
- * this.$body.append( this.content.$element );
- * };
- * MessageDialog.prototype.getBodyHeight = function () {
- * return 100;
- * }
- * MessageDialog.prototype.getActionProcess = function ( action ) {
- * var dialog = this;
- * if ( action === 'save' ) {
- * dialog.getActions().get({actions: 'save'})[0].pushPending();
- * return new OO.ui.Process()
- * .next( 1000 )
- * .next( function () {
- * dialog.getActions().get({actions: 'save'})[0].popPending();
- * } );
- * }
- * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
- * };
- *
- * var windowManager = new OO.ui.WindowManager();
- * $( 'body' ).append( windowManager.$element );
- *
- * var dialog = new MessageDialog();
- * windowManager.addWindows( [ dialog ] );
- * windowManager.openWindow( dialog );
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
- */
-OO.ui.mixin.PendingElement = function OoUiMixinPendingElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.pending = 0;
- this.$pending = null;
-
- // Initialisation
- this.setPendingElement( config.$pending || this.$element );
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.mixin.PendingElement );
-
-/* Methods */
-
-/**
- * Set the pending element (and clean up any existing one).
- *
- * @param {jQuery} $pending The element to set to pending.
- */
-OO.ui.mixin.PendingElement.prototype.setPendingElement = function ( $pending ) {
- if ( this.$pending ) {
- this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
- }
-
- this.$pending = $pending;
- if ( this.pending > 0 ) {
- this.$pending.addClass( 'oo-ui-pendingElement-pending' );
- }
-};
-
-/**
- * Check if an element is pending.
- *
- * @return {boolean} Element is pending
- */
-OO.ui.mixin.PendingElement.prototype.isPending = function () {
- return !!this.pending;
-};
-
-/**
- * Increase the pending counter. The pending state will remain active until the counter is zero
- * (i.e., the number of calls to #pushPending and #popPending is the same).
- *
- * @chainable
- */
-OO.ui.mixin.PendingElement.prototype.pushPending = function () {
- if ( this.pending === 0 ) {
- this.$pending.addClass( 'oo-ui-pendingElement-pending' );
- this.updateThemeClasses();
- }
- this.pending++;
-
- return this;
-};
-
-/**
- * Decrease the pending counter. The pending state will remain active until the counter is zero
- * (i.e., the number of calls to #pushPending and #popPending is the same).
- *
- * @chainable
- */
-OO.ui.mixin.PendingElement.prototype.popPending = function () {
- if ( this.pending === 1 ) {
- this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
- this.updateThemeClasses();
- }
- this.pending = Math.max( 0, this.pending - 1 );
-
- return this;
-};
-
-/**
- * Element that can be automatically clipped to visible boundaries.
- *
- * Whenever the element's natural height changes, you have to call
- * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
- * clipping correctly.
- *
- * The dimensions of #$clippableContainer will be compared to the boundaries of the
- * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
- * then #$clippable will be given a fixed reduced height and/or width and will be made
- * scrollable. By default, #$clippable and #$clippableContainer are the same element,
- * but you can build a static footer by setting #$clippableContainer to an element that contains
- * #$clippable and the footer.
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
- * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
- * omit to use #$clippable
- */
-OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.$clippable = null;
- this.$clippableContainer = null;
- this.clipping = false;
- this.clippedHorizontally = false;
- this.clippedVertically = false;
- this.$clippableScrollableContainer = null;
- this.$clippableScroller = null;
- this.$clippableWindow = null;
- this.idealWidth = null;
- this.idealHeight = null;
- this.onClippableScrollHandler = this.clip.bind( this );
- this.onClippableWindowResizeHandler = this.clip.bind( this );
-
- // Initialization
- if ( config.$clippableContainer ) {
- this.setClippableContainer( config.$clippableContainer );
- }
- this.setClippableElement( config.$clippable || this.$element );
-};
-
-/* Methods */
-
-/**
- * Set clippable element.
- *
- * If an element is already set, it will be cleaned up before setting up the new element.
- *
- * @param {jQuery} $clippable Element to make clippable
- */
-OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
- if ( this.$clippable ) {
- this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
- this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
- OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
- }
-
- this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
- this.clip();
-};
-
-/**
- * Set clippable container.
- *
- * This is the container that will be measured when deciding whether to clip. When clipping,
- * #$clippable will be resized in order to keep the clippable container fully visible.
- *
- * If the clippable container is unset, #$clippable will be used.
- *
- * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
- */
-OO.ui.mixin.ClippableElement.prototype.setClippableContainer = function ( $clippableContainer ) {
- this.$clippableContainer = $clippableContainer;
- if ( this.$clippable ) {
- this.clip();
- }
-};
-
-/**
- * Toggle clipping.
- *
- * Do not turn clipping on until after the element is attached to the DOM and visible.
- *
- * @param {boolean} [clipping] Enable clipping, omit to toggle
- * @chainable
- */
-OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
- clipping = clipping === undefined ? !this.clipping : !!clipping;
-
- if ( this.clipping !== clipping ) {
- this.clipping = clipping;
- if ( clipping ) {
- this.$clippableScrollableContainer = $( this.getClosestScrollableElementContainer() );
- // If the clippable container is the root, we have to listen to scroll events and check
- // jQuery.scrollTop on the window because of browser inconsistencies
- this.$clippableScroller = this.$clippableScrollableContainer.is( 'html, body' ) ?
- $( OO.ui.Element.static.getWindow( this.$clippableScrollableContainer ) ) :
- this.$clippableScrollableContainer;
- this.$clippableScroller.on( 'scroll', this.onClippableScrollHandler );
- this.$clippableWindow = $( this.getElementWindow() )
- .on( 'resize', this.onClippableWindowResizeHandler );
- // Initial clip after visible
- this.clip();
- } else {
- this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
- OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
-
- this.$clippableScrollableContainer = null;
- this.$clippableScroller.off( 'scroll', this.onClippableScrollHandler );
- this.$clippableScroller = null;
- this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
- this.$clippableWindow = null;
- }
- }
-
- return this;
-};
-
-/**
- * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
- *
- * @return {boolean} Element will be clipped to the visible area
- */
-OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
- return this.clipping;
-};
-
-/**
- * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
- *
- * @return {boolean} Part of the element is being clipped
- */
-OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
- return this.clippedHorizontally || this.clippedVertically;
-};
-
-/**
- * Check if the right of the element is being clipped by the nearest scrollable container.
- *
- * @return {boolean} Part of the element is being clipped
- */
-OO.ui.mixin.ClippableElement.prototype.isClippedHorizontally = function () {
- return this.clippedHorizontally;
-};
-
-/**
- * Check if the bottom of the element is being clipped by the nearest scrollable container.
- *
- * @return {boolean} Part of the element is being clipped
- */
-OO.ui.mixin.ClippableElement.prototype.isClippedVertically = function () {
- return this.clippedVertically;
-};
-
-/**
- * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
- *
- * @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.mixin.ClippableElement.prototype.setIdealSize = function ( width, height ) {
- this.idealWidth = width;
- this.idealHeight = height;
-
- if ( !this.clipping ) {
- // Update dimensions
- this.$clippable.css( { width: width, height: height } );
- }
- // While clipping, idealWidth and idealHeight are not considered
-};
-
-/**
- * Clip element to visible boundaries and allow scrolling when needed. Call this method when
- * the element's natural height changes.
- *
- * 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.
- *
- * @chainable
- */
-OO.ui.mixin.ClippableElement.prototype.clip = function () {
- var $container, extraHeight, extraWidth, ccOffset,
- $scrollableContainer, scOffset, scHeight, scWidth,
- ccWidth, scrollerIsWindow, scrollTop, scrollLeft,
- desiredWidth, desiredHeight, allotedWidth, allotedHeight,
- naturalWidth, naturalHeight, clipWidth, clipHeight,
- buffer = 7; // Chosen by fair dice roll
-
- if ( !this.clipping ) {
- // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
- return this;
- }
-
- $container = this.$clippableContainer || this.$clippable;
- extraHeight = $container.outerHeight() - this.$clippable.outerHeight();
- extraWidth = $container.outerWidth() - this.$clippable.outerWidth();
- ccOffset = $container.offset();
- $scrollableContainer = this.$clippableScrollableContainer.is( 'html, body' ) ?
- this.$clippableWindow : this.$clippableScrollableContainer;
- scOffset = $scrollableContainer.offset() || { top: 0, left: 0 };
- scHeight = $scrollableContainer.innerHeight() - buffer;
- scWidth = $scrollableContainer.innerWidth() - buffer;
- ccWidth = $container.outerWidth() + buffer;
- scrollerIsWindow = this.$clippableScroller[ 0 ] === this.$clippableWindow[ 0 ];
- scrollTop = scrollerIsWindow ? this.$clippableScroller.scrollTop() : 0;
- scrollLeft = scrollerIsWindow ? this.$clippableScroller.scrollLeft() : 0;
- desiredWidth = ccOffset.left < 0 ?
- ccWidth + ccOffset.left :
- ( scOffset.left + scrollLeft + scWidth ) - ccOffset.left;
- desiredHeight = ( scOffset.top + scrollTop + scHeight ) - ccOffset.top;
- allotedWidth = Math.ceil( desiredWidth - extraWidth );
- allotedHeight = Math.ceil( desiredHeight - extraHeight );
- naturalWidth = this.$clippable.prop( 'scrollWidth' );
- naturalHeight = this.$clippable.prop( 'scrollHeight' );
- clipWidth = allotedWidth < naturalWidth;
- clipHeight = allotedHeight < naturalHeight;
-
- if ( clipWidth ) {
- this.$clippable.css( { overflowX: 'scroll', width: Math.max( 0, allotedWidth ) } );
- } else {
- this.$clippable.css( { width: this.idealWidth ? this.idealWidth - extraWidth : '', overflowX: '' } );
- }
- if ( clipHeight ) {
- this.$clippable.css( { overflowY: 'scroll', height: Math.max( 0, allotedHeight ) } );
- } else {
- this.$clippable.css( { height: this.idealHeight ? this.idealHeight - extraHeight : '', overflowY: '' } );
- }
-
- // If we stopped clipping in at least one of the dimensions
- if ( ( this.clippedHorizontally && !clipWidth ) || ( this.clippedVertically && !clipHeight ) ) {
- OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
- }
-
- this.clippedHorizontally = clipWidth;
- this.clippedVertically = clipHeight;
-
- return this;
-};
-
-/**
- * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
- * By default, each popup has an anchor that points toward its origin.
- * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
- *
- * @example
- * // A popup widget.
- * var popup = new OO.ui.PopupWidget( {
- * $content: $( '<p>Hi there!</p>' ),
- * padded: true,
- * width: 300
- * } );
- *
- * $( 'body' ).append( popup.$element );
- * // To display the popup, toggle the visibility to 'true'.
- * popup.toggle( true );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.LabelElement
- * @mixins OO.ui.mixin.ClippableElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {number} [width=320] Width of popup in pixels
- * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
- * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
- * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
- * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
- * popup is leaning towards the right of the screen.
- * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
- * in the given language, which means it will flip to the correct positioning in right-to-left languages.
- * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
- * sentence in the given language.
- * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
- * See the [OOjs UI docs on MediaWiki][3] for an example.
- * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
- * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
- * @cfg {jQuery} [$content] Content to append to the popup's body
- * @cfg {jQuery} [$footer] Content to append to the popup's footer
- * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
- * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
- * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
- * for an example.
- * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
- * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
- * button.
- * @cfg {boolean} [padded] Add padding to the popup's body
- */
-OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.PopupWidget.parent.call( this, config );
-
- // Properties (must be set before ClippableElement constructor call)
- this.$body = $( '<div>' );
- this.$popup = $( '<div>' );
-
- // Mixin constructors
- OO.ui.mixin.LabelElement.call( this, config );
- OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, {
- $clippable: this.$body,
- $clippableContainer: this.$popup
- } ) );
-
- // Properties
- this.$head = $( '<div>' );
- this.$footer = $( '<div>' );
- this.$anchor = $( '<div>' );
- // If undefined, will be computed lazily in updateDimensions()
- this.$container = config.$container;
- this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
- this.autoClose = !!config.autoClose;
- this.$autoCloseIgnore = config.$autoCloseIgnore;
- this.transitionTimeout = null;
- this.anchor = null;
- this.width = config.width !== undefined ? config.width : 320;
- this.height = config.height !== undefined ? config.height : null;
- this.setAlignment( config.align );
- this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
- this.onMouseDownHandler = this.onMouseDown.bind( this );
- this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
-
- // Events
- this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
-
- // Initialization
- this.toggleAnchor( config.anchor === undefined || config.anchor );
- 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 );
- this.$footer.addClass( 'oo-ui-popupWidget-footer' );
- if ( !config.head ) {
- this.$head.addClass( 'oo-ui-element-hidden' );
- }
- if ( !config.$footer ) {
- this.$footer.addClass( 'oo-ui-element-hidden' );
- }
- this.$popup
- .addClass( 'oo-ui-popupWidget-popup' )
- .append( this.$head, this.$body, this.$footer );
- this.$element
- .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.$footer instanceof jQuery ) {
- this.$footer.append( config.$footer );
- }
- if ( config.padded ) {
- this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
- }
-
- // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
- // that reference properties not initialized at that time of parent class construction
- // TODO: Find a better way to handle post-constructor setup
- this.visible = false;
- this.$element.addClass( 'oo-ui-element-hidden' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
-
-/* Methods */
-
-/**
- * Handles mouse down events.
- *
- * @private
- * @param {MouseEvent} 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.
- *
- * @private
- */
-OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
- // Capture clicks outside popup
- this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
-};
-
-/**
- * Handles close button click events.
- *
- * @private
- */
-OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
- if ( this.isVisible() ) {
- this.toggle( false );
- }
-};
-
-/**
- * Unbind mouse down listener.
- *
- * @private
- */
-OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
- this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
-};
-
-/**
- * Handles key down events.
- *
- * @private
- * @param {KeyboardEvent} e Key down event
- */
-OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
- if (
- e.which === OO.ui.Keys.ESCAPE &&
- this.isVisible()
- ) {
- this.toggle( false );
- e.preventDefault();
- e.stopPropagation();
- }
-};
-
-/**
- * Bind key down listener.
- *
- * @private
- */
-OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
- this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
-};
-
-/**
- * Unbind key down listener.
- *
- * @private
- */
-OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
- this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
-};
-
-/**
- * Show, hide, or toggle the visibility of the anchor.
- *
- * @param {boolean} [show] Show anchor, omit to toggle
- */
-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;
- }
-};
-
-/**
- * Check if the anchor is visible.
- *
- * @return {boolean} Anchor is visible
- */
-OO.ui.PopupWidget.prototype.hasAnchor = function () {
- return this.anchor;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.PopupWidget.prototype.toggle = function ( show ) {
- var change;
- show = show === undefined ? !this.isVisible() : !!show;
-
- change = show !== this.isVisible();
-
- // Parent method
- OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
-
- if ( change ) {
- if ( show ) {
- if ( this.autoClose ) {
- this.bindMouseDownListener();
- this.bindKeyDownListener();
- }
- this.updateDimensions();
- this.toggleClipping( true );
- } else {
- this.toggleClipping( false );
- if ( this.autoClose ) {
- this.unbindMouseDownListener();
- this.unbindKeyDownListener();
- }
- }
- }
-
- return this;
-};
-
-/**
- * Set the size of the popup.
- *
- * Changing the size may also change the popup's position depending on the alignment.
- *
- * @param {number} width Width in pixels
- * @param {number} height Height in pixels
- * @param {boolean} [transition=false] Use a smooth transition
- * @chainable
- */
-OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
- this.width = width;
- this.height = height !== undefined ? height : null;
- if ( this.isVisible() ) {
- this.updateDimensions( transition );
- }
-};
-
-/**
- * Update the size and position.
- *
- * 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.PopupWidget.prototype.updateDimensions = function ( transition ) {
- var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
- popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
- align = this.align,
- widget = this;
-
- if ( !this.$container ) {
- // Lazy-initialize $container if not specified in constructor
- this.$container = $( this.getClosestScrollableElementContainer() );
- }
-
- // Set height and width before measuring things, since it might cause our measurements
- // to change (e.g. due to scrollbars appearing or disappearing)
- this.$popup.css( {
- width: this.width,
- height: this.height !== null ? this.height : 'auto'
- } );
-
- // If we are in RTL, we need to flip the alignment, unless it is center
- if ( align === 'forwards' || align === 'backwards' ) {
- if ( this.$container.css( 'direction' ) === 'rtl' ) {
- align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
- } else {
- align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
- }
-
- }
-
- // Compute initial popupOffset based on alignment
- popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
-
- // Figure out if this will cause the popup to go beyond the edge of the container
- originOffset = this.$element.offset().left;
- containerLeft = this.$container.offset().left;
- containerWidth = this.$container.innerWidth();
- containerRight = containerLeft + containerWidth;
- popupLeft = popupOffset - this.containerPadding;
- popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
- overlapLeft = ( originOffset + popupLeft ) - containerLeft;
- overlapRight = containerRight - ( originOffset + popupRight );
-
- // Adjust offset to make the popup not go beyond the edge, if needed
- if ( overlapRight < 0 ) {
- popupOffset += overlapRight;
- } else if ( overlapLeft < 0 ) {
- popupOffset -= overlapLeft;
- }
-
- // Adjust offset to avoid anchor being rendered too close to the edge
- // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
- // TODO: Find a measurement that works for CSS anchors and image anchors
- anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
- if ( popupOffset + this.width < anchorWidth ) {
- popupOffset = anchorWidth - this.width;
- } else if ( -popupOffset < anchorWidth ) {
- popupOffset = -anchorWidth;
- }
-
- // Prevent transition from being interrupted
- clearTimeout( this.transitionTimeout );
- if ( transition ) {
- // Enable transition
- this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
- }
-
- // Position body relative to anchor
- this.$popup.css( 'margin-left', popupOffset );
-
- 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' );
- }
-
- // Reevaluate clipping state since we've relocated and resized the popup
- this.clip();
-
- return this;
-};
-
-/**
- * Set popup alignment
- * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
- * `backwards` or `forwards`.
- */
-OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
- // Validate alignment and transform deprecated values
- if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
- this.align = { left: 'force-right', right: 'force-left' }[ align ] || align;
- } else {
- this.align = 'center';
- }
-};
-
-/**
- * Get popup alignment
- * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
- * `backwards` or `forwards`.
- */
-OO.ui.PopupWidget.prototype.getAlignment = function () {
- return this.align;
-};
-
-/**
- * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
- * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
- * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
- * See {@link OO.ui.PopupWidget PopupWidget} for an example.
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {Object} [popup] Configuration to pass to popup
- * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
- */
-OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.popup = new OO.ui.PopupWidget( $.extend(
- { autoClose: true },
- config.popup,
- { $autoCloseIgnore: this.$element }
- ) );
-};
-
-/* Methods */
-
-/**
- * Get popup.
- *
- * @return {OO.ui.PopupWidget} Popup widget
- */
-OO.ui.mixin.PopupElement.prototype.getPopup = function () {
- return this.popup;
-};
-
-/**
- * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
- * which is used to display additional information or options.
- *
- * @example
- * // Example of a popup button.
- * var popupButton = new OO.ui.PopupButtonWidget( {
- * label: 'Popup button with options',
- * icon: 'menu',
- * popup: {
- * $content: $( '<p>Additional options here.</p>' ),
- * padded: true,
- * align: 'force-left'
- * }
- * } );
- * // Append the button to the DOM.
- * $( 'body' ).append( popupButton.$element );
- *
- * @class
- * @extends OO.ui.ButtonWidget
- * @mixins OO.ui.mixin.PopupElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
- // Parent constructor
- OO.ui.PopupButtonWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.PopupElement.call( this, config );
-
- // Events
- this.connect( this, { click: 'onAction' } );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-popupButtonWidget' )
- .attr( 'aria-haspopup', 'true' )
- .append( this.popup.$element );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
-OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.mixin.PopupElement );
-
-/* Methods */
-
-/**
- * Handle the button action being triggered.
- *
- * @private
- */
-OO.ui.PopupButtonWidget.prototype.onAction = function () {
- this.popup.toggle();
-};
-
-/**
- * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
- *
- * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
- *
- * @private
- * @abstract
- * @class
- * @extends OO.ui.mixin.GroupElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.mixin.GroupWidget = function OoUiMixinGroupWidget( config ) {
- // Parent constructor
- OO.ui.mixin.GroupWidget.parent.call( this, config );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.mixin.GroupWidget, OO.ui.mixin.GroupElement );
-
-/* Methods */
-
-/**
- * Set the disabled state of the widget.
- *
- * This will also update the disabled state of child widgets.
- *
- * @param {boolean} disabled Disable widget
- * @chainable
- */
-OO.ui.mixin.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.mixin.GroupElement constructor
- if ( this.items ) {
- for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.items[ i ].updateDisabled();
- }
- }
-
- return this;
-};
-
-/**
- * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
- *
- * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
- * allows bidirectional communication.
- *
- * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
- *
- * @private
- * @abstract
- * @class
- *
- * @constructor
- */
-OO.ui.mixin.ItemWidget = function OoUiMixinItemWidget() {
- //
-};
-
-/* Methods */
-
-/**
- * Check if widget is disabled.
- *
- * Checks parent if present, making disabled state inheritable.
- *
- * @return {boolean} Widget is disabled
- */
-OO.ui.mixin.ItemWidget.prototype.isDisabled = function () {
- return this.disabled ||
- ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
-};
-
-/**
- * Set group element is in.
- *
- * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
- * @chainable
- */
-OO.ui.mixin.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();
-
- return this;
-};
-
-/**
- * OptionWidgets are special elements that can be selected and configured with data. The
- * data is often unique for each option, but it does not have to be. OptionWidgets are used
- * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
- * and examples, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.LabelElement
- * @mixins OO.ui.mixin.FlaggedElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.OptionWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.ItemWidget.call( this );
- OO.ui.mixin.LabelElement.call( this, config );
- OO.ui.mixin.FlaggedElement.call( this, config );
-
- // Properties
- this.selected = false;
- this.highlighted = false;
- this.pressed = false;
-
- // Initialization
- this.$element
- .data( 'oo-ui-optionWidget', this )
- .attr( 'role', 'option' )
- .attr( 'aria-selected', 'false' )
- .addClass( 'oo-ui-optionWidget' )
- .append( this.$label );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
-
-/* Static Properties */
-
-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 */
-
-/**
- * Check if the option can be selected.
- *
- * @return {boolean} Item is selectable
- */
-OO.ui.OptionWidget.prototype.isSelectable = function () {
- return this.constructor.static.selectable && !this.isDisabled() && this.isVisible();
-};
-
-/**
- * Check if the option can be highlighted. A highlight indicates that the option
- * may be selected when a user presses enter or clicks. Disabled items cannot
- * be highlighted.
- *
- * @return {boolean} Item is highlightable
- */
-OO.ui.OptionWidget.prototype.isHighlightable = function () {
- return this.constructor.static.highlightable && !this.isDisabled() && this.isVisible();
-};
-
-/**
- * Check if the option can be pressed. The pressed state occurs when a user mouses
- * down on an item, but has not yet let go of the mouse.
- *
- * @return {boolean} Item is pressable
- */
-OO.ui.OptionWidget.prototype.isPressable = function () {
- return this.constructor.static.pressable && !this.isDisabled() && this.isVisible();
-};
-
-/**
- * Check if the option is selected.
- *
- * @return {boolean} Item is selected
- */
-OO.ui.OptionWidget.prototype.isSelected = function () {
- return this.selected;
-};
-
-/**
- * Check if the option is highlighted. A highlight indicates that the
- * item may be selected when a user presses enter or clicks.
- *
- * @return {boolean} Item is highlighted
- */
-OO.ui.OptionWidget.prototype.isHighlighted = function () {
- return this.highlighted;
-};
-
-/**
- * Check if the option is pressed. The pressed state occurs when a user mouses
- * down on an item, but has not yet let go of the mouse. The item may appear
- * selected, but it will not be selected until the user releases the mouse.
- *
- * @return {boolean} Item is pressed
- */
-OO.ui.OptionWidget.prototype.isPressed = function () {
- return this.pressed;
-};
-
-/**
- * Set the option’s selected state. In general, all modifications to the selection
- * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
- * method instead of this method.
- *
- * @param {boolean} [state=false] Select option
- * @chainable
- */
-OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
- if ( this.constructor.static.selectable ) {
- this.selected = !!state;
- this.$element
- .toggleClass( 'oo-ui-optionWidget-selected', state )
- .attr( 'aria-selected', state.toString() );
- if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
- this.scrollElementIntoView();
- }
- this.updateThemeClasses();
- }
- return this;
-};
-
-/**
- * Set the option’s highlighted state. In general, all programmatic
- * modifications to the highlight should be handled by the
- * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
- * method instead of this method.
- *
- * @param {boolean} [state=false] Highlight option
- * @chainable
- */
-OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
- if ( this.constructor.static.highlightable ) {
- this.highlighted = !!state;
- this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
- this.updateThemeClasses();
- }
- return this;
-};
-
-/**
- * Set the option’s pressed state. In general, all
- * programmatic modifications to the pressed state should be handled by the
- * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
- * method instead of this method.
- *
- * @param {boolean} [state=false] Press option
- * @chainable
- */
-OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
- if ( this.constructor.static.pressable ) {
- this.pressed = !!state;
- this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
- this.updateThemeClasses();
- }
- return this;
-};
-
-/**
- * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
- * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
- * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
- * menu selects}.
- *
- * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
- * information, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * @example
- * // Example of a select widget with three options
- * var select = new OO.ui.SelectWidget( {
- * items: [
- * new OO.ui.OptionWidget( {
- * data: 'a',
- * label: 'Option One',
- * } ),
- * new OO.ui.OptionWidget( {
- * data: 'b',
- * label: 'Option Two',
- * } ),
- * new OO.ui.OptionWidget( {
- * data: 'c',
- * label: 'Option Three',
- * } )
- * ]
- * } );
- * $( 'body' ).append( select.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
- *
- * @abstract
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.GroupWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
- * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
- * the [OOjs UI documentation on MediaWiki] [2] for examples.
- * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
- */
-OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.SelectWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
-
- // Properties
- this.pressed = false;
- this.selecting = null;
- this.onMouseUpHandler = this.onMouseUp.bind( this );
- this.onMouseMoveHandler = this.onMouseMove.bind( this );
- this.onKeyDownHandler = this.onKeyDown.bind( this );
- this.onKeyPressHandler = this.onKeyPress.bind( this );
- this.keyPressBuffer = '';
- this.keyPressBufferTimer = null;
-
- // Events
- this.connect( this, {
- toggle: 'onToggle'
- } );
- this.$element.on( {
- mousedown: this.onMouseDown.bind( this ),
- mouseover: this.onMouseOver.bind( this ),
- mouseleave: this.onMouseLeave.bind( this )
- } );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
- .attr( 'role', 'listbox' );
- if ( Array.isArray( config.items ) ) {
- this.addItems( config.items );
- }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
-
-// Need to mixin base class as well
-OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupElement );
-OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
-
-/* Static */
-OO.ui.SelectWidget.static.passAllFilter = function () {
- return true;
-};
-
-/* Events */
-
-/**
- * @event highlight
- *
- * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
- *
- * @param {OO.ui.OptionWidget|null} item Highlighted item
- */
-
-/**
- * @event press
- *
- * A `press` event is emitted when the #pressItem method is used to programmatically modify the
- * pressed state of an option.
- *
- * @param {OO.ui.OptionWidget|null} item Pressed item
- */
-
-/**
- * @event select
- *
- * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
- *
- * @param {OO.ui.OptionWidget|null} item Selected item
- */
-
-/**
- * @event choose
- * A `choose` event is emitted when an item is chosen with the #chooseItem method.
- * @param {OO.ui.OptionWidget} item Chosen item
- */
-
-/**
- * @event add
- *
- * An `add` event is emitted when options are added to the select with the #addItems method.
- *
- * @param {OO.ui.OptionWidget[]} items Added items
- * @param {number} index Index of insertion point
- */
-
-/**
- * @event remove
- *
- * A `remove` event is emitted when options are removed from the select with the #clearItems
- * or #removeItems methods.
- *
- * @param {OO.ui.OptionWidget[]} items Removed items
- */
-
-/* Methods */
-
-/**
- * Handle mouse down events.
- *
- * @private
- * @param {jQuery.Event} e Mouse down event
- */
-OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
- var item;
-
- if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
- 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;
-};
-
-/**
- * Handle mouse up events.
- *
- * @private
- * @param {jQuery.Event} e Mouse up event
- */
-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 === OO.ui.MouseButtons.LEFT && 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;
-};
-
-/**
- * Handle mouse move events.
- *
- * @private
- * @param {jQuery.Event} e Mouse move event
- */
-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;
-};
-
-/**
- * Handle mouse over events.
- *
- * @private
- * @param {jQuery.Event} e Mouse over event
- */
-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;
-};
-
-/**
- * Handle mouse leave events.
- *
- * @private
- * @param {jQuery.Event} e Mouse over event
- */
-OO.ui.SelectWidget.prototype.onMouseLeave = function () {
- if ( !this.isDisabled() ) {
- this.highlightItem( null );
- }
- return false;
-};
-
-/**
- * Handle key down events.
- *
- * @protected
- * @param {jQuery.Event} e Key down event
- */
-OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
- var nextItem,
- handled = false,
- currentItem = this.getHighlightedItem() || this.getSelectedItem();
-
- if ( !this.isDisabled() && this.isVisible() ) {
- switch ( e.keyCode ) {
- case OO.ui.Keys.ENTER:
- if ( currentItem && currentItem.constructor.static.highlightable ) {
- // Was only highlighted, now let's select it. No-op if already selected.
- this.chooseItem( currentItem );
- handled = true;
- }
- break;
- case OO.ui.Keys.UP:
- case OO.ui.Keys.LEFT:
- this.clearKeyPressBuffer();
- nextItem = this.getRelativeSelectableItem( currentItem, -1 );
- handled = true;
- break;
- case OO.ui.Keys.DOWN:
- case OO.ui.Keys.RIGHT:
- this.clearKeyPressBuffer();
- nextItem = this.getRelativeSelectableItem( currentItem, 1 );
- handled = true;
- break;
- case OO.ui.Keys.ESCAPE:
- case OO.ui.Keys.TAB:
- if ( currentItem && currentItem.constructor.static.highlightable ) {
- currentItem.setHighlighted( false );
- }
- this.unbindKeyDownListener();
- this.unbindKeyPressListener();
- // Don't prevent tabbing away / defocusing
- handled = false;
- break;
- }
-
- if ( nextItem ) {
- if ( nextItem.constructor.static.highlightable ) {
- this.highlightItem( nextItem );
- } else {
- this.chooseItem( nextItem );
- }
- nextItem.scrollElementIntoView();
- }
-
- if ( handled ) {
- // Can't just return false, because e is not always a jQuery event
- e.preventDefault();
- e.stopPropagation();
- }
- }
-};
-
-/**
- * Bind key down listener.
- *
- * @protected
- */
-OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
- this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
-};
-
-/**
- * Unbind key down listener.
- *
- * @protected
- */
-OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
- this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
-};
-
-/**
- * Clear the key-press buffer
- *
- * @protected
- */
-OO.ui.SelectWidget.prototype.clearKeyPressBuffer = function () {
- if ( this.keyPressBufferTimer ) {
- clearTimeout( this.keyPressBufferTimer );
- this.keyPressBufferTimer = null;
- }
- this.keyPressBuffer = '';
-};
-
-/**
- * Handle key press events.
- *
- * @protected
- * @param {jQuery.Event} e Key press event
- */
-OO.ui.SelectWidget.prototype.onKeyPress = function ( e ) {
- var c, filter, item;
-
- if ( !e.charCode ) {
- if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.keyPressBuffer !== '' ) {
- this.keyPressBuffer = this.keyPressBuffer.substr( 0, this.keyPressBuffer.length - 1 );
- return false;
- }
- return;
- }
- if ( String.fromCodePoint ) {
- c = String.fromCodePoint( e.charCode );
- } else {
- c = String.fromCharCode( e.charCode );
- }
-
- if ( this.keyPressBufferTimer ) {
- clearTimeout( this.keyPressBufferTimer );
- }
- this.keyPressBufferTimer = setTimeout( this.clearKeyPressBuffer.bind( this ), 1500 );
-
- item = this.getHighlightedItem() || this.getSelectedItem();
-
- if ( this.keyPressBuffer === c ) {
- // Common (if weird) special case: typing "xxxx" will cycle through all
- // the items beginning with "x".
- if ( item ) {
- item = this.getRelativeSelectableItem( item, 1 );
- }
- } else {
- this.keyPressBuffer += c;
- }
-
- filter = this.getItemMatcher( this.keyPressBuffer, false );
- if ( !item || !filter( item ) ) {
- item = this.getRelativeSelectableItem( item, 1, filter );
- }
- if ( item ) {
- if ( item.constructor.static.highlightable ) {
- this.highlightItem( item );
- } else {
- this.chooseItem( item );
- }
- item.scrollElementIntoView();
- }
-
- return false;
-};
-
-/**
- * Get a matcher for the specific string
- *
- * @protected
- * @param {string} s String to match against items
- * @param {boolean} [exact=false] Only accept exact matches
- * @return {Function} function ( OO.ui.OptionItem ) => boolean
- */
-OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
- var re;
-
- if ( s.normalize ) {
- s = s.normalize();
- }
- s = exact ? s.trim() : s.replace( /^\s+/, '' );
- re = '^\\s*' + s.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
- if ( exact ) {
- re += '\\s*$';
- }
- re = new RegExp( re, 'i' );
- return function ( item ) {
- var l = item.getLabel();
- if ( typeof l !== 'string' ) {
- l = item.$label.text();
- }
- if ( l.normalize ) {
- l = l.normalize();
- }
- return re.test( l );
- };
-};
-
-/**
- * Bind key press listener.
- *
- * @protected
- */
-OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
- this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler, true );
-};
-
-/**
- * Unbind key down listener.
- *
- * If you override this, be sure to call this.clearKeyPressBuffer() from your
- * implementation.
- *
- * @protected
- */
-OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
- this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler, true );
- this.clearKeyPressBuffer();
-};
-
-/**
- * Visibility change handler
- *
- * @protected
- * @param {boolean} visible
- */
-OO.ui.SelectWidget.prototype.onToggle = function ( visible ) {
- if ( !visible ) {
- this.clearKeyPressBuffer();
- }
-};
-
-/**
- * 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
- */
-OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
- return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || 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;
-};
-
-/**
- * Toggle pressed state.
- *
- * Press is a state that occurs when a user mouses down on an item, but
- * has not yet let go of the mouse. The item may appear selected, but it will not be selected
- * until the user releases the mouse.
- *
- * @param {boolean} pressed An option is being pressed
- */
-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;
- }
-};
-
-/**
- * Highlight an option. If the `item` param is omitted, no options will be highlighted
- * and any existing highlight will be removed. The highlight is mutually exclusive.
- *
- * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
- * @fires highlight
- * @chainable
- */
-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;
-};
-
-/**
- * Fetch an item by its label.
- *
- * @param {string} label Label of the item to select.
- * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
- * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
- */
-OO.ui.SelectWidget.prototype.getItemFromLabel = function ( label, prefix ) {
- var i, item, found,
- len = this.items.length,
- filter = this.getItemMatcher( label, true );
-
- for ( i = 0; i < len; i++ ) {
- item = this.items[ i ];
- if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
- return item;
- }
- }
-
- if ( prefix ) {
- found = null;
- filter = this.getItemMatcher( label, false );
- for ( i = 0; i < len; i++ ) {
- item = this.items[ i ];
- if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
- if ( found ) {
- return null;
- }
- found = item;
- }
- }
- if ( found ) {
- return found;
- }
- }
-
- return null;
-};
-
-/**
- * Programmatically select an option by its label. If the item does not exist,
- * all options will be deselected.
- *
- * @param {string} [label] Label of the item to select.
- * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
- * @fires select
- * @chainable
- */
-OO.ui.SelectWidget.prototype.selectItemByLabel = function ( label, prefix ) {
- var itemFromLabel = this.getItemFromLabel( label, !!prefix );
- if ( label === undefined || !itemFromLabel ) {
- return this.selectItem();
- }
- return this.selectItem( itemFromLabel );
-};
-
-/**
- * Programmatically select an option by its data. If the `data` parameter is omitted,
- * or if the item does not exist, all options will be deselected.
- *
- * @param {Object|string} [data] Value of the item to select, omit to deselect all
- * @fires select
- * @chainable
- */
-OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
- var itemFromData = this.getItemFromData( data );
- if ( data === undefined || !itemFromData ) {
- return this.selectItem();
- }
- return this.selectItem( itemFromData );
-};
-
-/**
- * Programmatically select an option by its reference. If the `item` parameter is omitted,
- * all options will be deselected.
- *
- * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
- * @fires select
- * @chainable
- */
-OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
- var i, len, 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;
-};
-
-/**
- * Press an item.
- *
- * Press is a state that occurs when a user mouses down on an item, but has not
- * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
- * releases the mouse.
- *
- * @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;
-
- 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 );
- }
-
- return this;
-};
-
-/**
- * Choose an item.
- *
- * Note that ‘choose’ should never be modified programmatically. A user can choose
- * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
- * use the #selectItem method.
- *
- * This method is identical to #selectItem, but may vary in subclasses that take additional action
- * when users choose an item with the keyboard or mouse.
- *
- * @param {OO.ui.OptionWidget} item Item to choose
- * @fires choose
- * @chainable
- */
-OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
- if ( item ) {
- this.selectItem( item );
- this.emit( 'choose', item );
- }
-
- return this;
-};
-
-/**
- * Get an option by its position relative to the specified item (or to the start of the option array,
- * if item is `null`). The direction in which to search through the option array is specified with a
- * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
- * `null` if there are no options in the array.
- *
- * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
- * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
- * @param {Function} filter Only consider items for which this function returns
- * true. Function takes an OO.ui.OptionWidget and returns a boolean.
- * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
- */
-OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction, filter ) {
- var currentIndex, nextIndex, i,
- increase = direction > 0 ? 1 : -1,
- len = this.items.length;
-
- if ( !$.isFunction( filter ) ) {
- filter = OO.ui.SelectWidget.static.passAllFilter;
- }
-
- if ( item instanceof OO.ui.OptionWidget ) {
- currentIndex = this.items.indexOf( item );
- nextIndex = ( currentIndex + increase + len ) % len;
- } else {
- // If no item is selected and moving forward, start at the beginning.
- // If moving backward, start at the end.
- nextIndex = direction > 0 ? 0 : len - 1;
- }
-
- for ( i = 0; i < len; i++ ) {
- item = this.items[ nextIndex ];
- if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
- return item;
- }
- nextIndex = ( nextIndex + increase + len ) % len;
- }
- return null;
-};
-
-/**
- * Get the next selectable item or `null` if there are no selectable items.
- * Disabled options and menu-section markers and breaks are not selectable.
- *
- * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
- */
-OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
- var i, len, item;
-
- for ( i = 0, len = this.items.length; i < len; i++ ) {
- item = this.items[ i ];
- if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
- return item;
- }
- }
-
- return null;
-};
-
-/**
- * Add an array of options to the select. Optionally, an index number can be used to
- * specify an insertion point.
- *
- * @param {OO.ui.OptionWidget[]} items Items to add
- * @param {number} [index] Index to insert items after
- * @fires add
- * @chainable
- */
-OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
- // Mixin method
- OO.ui.mixin.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;
-};
-
-/**
- * Remove the specified array of options from the select. Options will be detached
- * from the DOM, not removed, so they can be reused later. To remove all options from
- * the select, you may wish to use the #clearItems method instead.
- *
- * @param {OO.ui.OptionWidget[]} items Items to remove
- * @fires remove
- * @chainable
- */
-OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
- var i, len, item;
-
- // Deselect items being removed
- for ( i = 0, len = items.length; i < len; i++ ) {
- item = items[ i ];
- if ( item.isSelected() ) {
- this.selectItem( null );
- }
- }
-
- // Mixin method
- OO.ui.mixin.GroupWidget.prototype.removeItems.call( this, items );
-
- this.emit( 'remove', items );
-
- return this;
-};
-
-/**
- * Clear all options from the select. Options will be detached from the DOM, not removed,
- * so that they can be reused later. To remove a subset of options from the select, use
- * the #removeItems method.
- *
- * @fires remove
- * @chainable
- */
-OO.ui.SelectWidget.prototype.clearItems = function () {
- var items = this.items.slice();
-
- // Mixin method
- OO.ui.mixin.GroupWidget.prototype.clearItems.call( this );
-
- // Clear selection
- this.selectItem( null );
-
- this.emit( 'remove', items );
-
- return this;
-};
-
-/**
- * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
- * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
- * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
- * options. For more information about options and selects, please see the
- * [OOjs UI documentation on MediaWiki][1].
- *
- * @example
- * // Decorated options in a select widget
- * var select = new OO.ui.SelectWidget( {
- * items: [
- * new OO.ui.DecoratedOptionWidget( {
- * data: 'a',
- * label: 'Option with icon',
- * icon: 'help'
- * } ),
- * new OO.ui.DecoratedOptionWidget( {
- * data: 'b',
- * label: 'Option with indicator',
- * indicator: 'next'
- * } )
- * ]
- * } );
- * $( 'body' ).append( select.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
- *
- * @class
- * @extends OO.ui.OptionWidget
- * @mixins OO.ui.mixin.IconElement
- * @mixins OO.ui.mixin.IndicatorElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
- // Parent constructor
- OO.ui.DecoratedOptionWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.IconElement.call( this, config );
- OO.ui.mixin.IndicatorElement.call( this, config );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-decoratedOptionWidget' )
- .prepend( this.$icon )
- .append( this.$indicator );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
-OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IconElement );
-OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IndicatorElement );
-
-/**
- * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
- * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
- * the [OOjs UI documentation on MediaWiki] [1] for more information.
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
- *
- * @class
- * @extends OO.ui.DecoratedOptionWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
- // Configuration initialization
- config = $.extend( { icon: 'check' }, config );
-
- // Parent constructor
- OO.ui.MenuOptionWidget.parent.call( this, config );
-
- // Initialization
- this.$element
- .attr( 'role', 'menuitem' )
- .addClass( 'oo-ui-menuOptionWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
-
-/* Static Properties */
-
-OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
-
-/**
- * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
- * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
- *
- * @example
- * var myDropdown = new OO.ui.DropdownWidget( {
- * menu: {
- * items: [
- * new OO.ui.MenuSectionOptionWidget( {
- * label: 'Dogs'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'corgi',
- * label: 'Welsh Corgi'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'poodle',
- * label: 'Standard Poodle'
- * } ),
- * new OO.ui.MenuSectionOptionWidget( {
- * label: 'Cats'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'lion',
- * label: 'Lion'
- * } )
- * ]
- * }
- * } );
- * $( 'body' ).append( myDropdown.$element );
- *
- * @class
- * @extends OO.ui.DecoratedOptionWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
- // Parent constructor
- OO.ui.MenuSectionOptionWidget.parent.call( this, config );
-
- // Initialization
- this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
-
-/* Static Properties */
-
-OO.ui.MenuSectionOptionWidget.static.selectable = false;
-
-OO.ui.MenuSectionOptionWidget.static.highlightable = false;
-
-/**
- * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
- * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
- * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
- * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
- * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
- * and customized to be opened, closed, and displayed as needed.
- *
- * By default, menus are clipped to the visible viewport and are not visible when a user presses the
- * mouse outside the menu.
- *
- * Menus also have support for keyboard interaction:
- *
- * - Enter/Return key: choose and select a menu option
- * - Up-arrow key: highlight the previous menu option
- * - Down-arrow key: highlight the next menu option
- * - Esc key: hide the menu
- *
- * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
- *
- * @class
- * @extends OO.ui.SelectWidget
- * @mixins OO.ui.mixin.ClippableElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
- * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
- * and {@link OO.ui.mixin.LookupElement LookupElement}
- * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
- * the text the user types. This config is used by {@link OO.ui.CapsuleMultiSelectWidget CapsuleMultiSelectWidget}
- * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
- * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
- * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
- * that button, unless the button (or its parent widget) is passed in here.
- * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
- * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
- */
-OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.MenuSelectWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
-
- // Properties
- this.newItems = null;
- this.autoHide = config.autoHide === undefined || !!config.autoHide;
- this.filterFromInput = !!config.filterFromInput;
- this.$input = config.$input ? config.$input : config.input ? config.input.$input : null;
- this.$widget = config.widget ? config.widget.$element : null;
- this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
- this.onInputEditHandler = OO.ui.debounce( this.updateItemVisibility.bind( this ), 100 );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-menuSelectWidget' )
- .attr( 'role', 'menu' );
-
- // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
- // that reference properties not initialized at that time of parent class construction
- // TODO: Find a better way to handle post-constructor setup
- this.visible = false;
- this.$element.addClass( 'oo-ui-element-hidden' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
-OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.ClippableElement );
-
-/* Methods */
-
-/**
- * Handles document mouse down events.
- *
- * @protected
- * @param {jQuery.Event} e Key down event
- */
-OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
- if (
- !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
- ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
- ) {
- this.toggle( false );
- }
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
- var currentItem = this.getHighlightedItem() || this.getSelectedItem();
-
- if ( !this.isDisabled() && this.isVisible() ) {
- switch ( e.keyCode ) {
- case OO.ui.Keys.LEFT:
- case OO.ui.Keys.RIGHT:
- // Do nothing if a text field is associated, arrow keys will be handled natively
- if ( !this.$input ) {
- OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
- }
- break;
- case OO.ui.Keys.ESCAPE:
- case OO.ui.Keys.TAB:
- if ( currentItem ) {
- currentItem.setHighlighted( false );
- }
- this.toggle( false );
- // Don't prevent tabbing away, prevent defocusing
- if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
- e.preventDefault();
- e.stopPropagation();
- }
- break;
- default:
- OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
- return;
- }
- }
-};
-
-/**
- * Update menu item visibility after input changes.
- * @protected
- */
-OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
- var i, item,
- len = this.items.length,
- showAll = !this.isVisible(),
- filter = showAll ? null : this.getItemMatcher( this.$input.val() );
-
- for ( i = 0; i < len; i++ ) {
- item = this.items[ i ];
- if ( item instanceof OO.ui.OptionWidget ) {
- item.toggle( showAll || filter( item ) );
- }
- }
-
- // Reevaluate clipping
- this.clip();
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
- if ( this.$input ) {
- this.$input.on( 'keydown', this.onKeyDownHandler );
- } else {
- OO.ui.MenuSelectWidget.parent.prototype.bindKeyDownListener.call( this );
- }
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
- if ( this.$input ) {
- this.$input.off( 'keydown', this.onKeyDownHandler );
- } else {
- OO.ui.MenuSelectWidget.parent.prototype.unbindKeyDownListener.call( this );
- }
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MenuSelectWidget.prototype.bindKeyPressListener = function () {
- if ( this.$input ) {
- if ( this.filterFromInput ) {
- this.$input.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
- }
- } else {
- OO.ui.MenuSelectWidget.parent.prototype.bindKeyPressListener.call( this );
- }
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MenuSelectWidget.prototype.unbindKeyPressListener = function () {
- if ( this.$input ) {
- if ( this.filterFromInput ) {
- this.$input.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
- this.updateItemVisibility();
- }
- } else {
- OO.ui.MenuSelectWidget.parent.prototype.unbindKeyPressListener.call( this );
- }
-};
-
-/**
- * Choose an item.
- *
- * When a user chooses an item, the menu is closed.
- *
- * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
- * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
- * @param {OO.ui.OptionWidget} item Item to choose
- * @chainable
- */
-OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
- OO.ui.MenuSelectWidget.parent.prototype.chooseItem.call( this, item );
- this.toggle( false );
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
- var i, len, item;
-
- // Parent method
- OO.ui.MenuSelectWidget.parent.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 has been attached
- item.fitLabel();
- } else {
- this.newItems.push( item );
- }
- }
-
- // Reevaluate clipping
- this.clip();
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
- // Parent method
- OO.ui.MenuSelectWidget.parent.prototype.removeItems.call( this, items );
-
- // Reevaluate clipping
- this.clip();
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MenuSelectWidget.prototype.clearItems = function () {
- // Parent method
- OO.ui.MenuSelectWidget.parent.prototype.clearItems.call( this );
-
- // Reevaluate clipping
- this.clip();
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
- var i, len, change;
-
- visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
- change = visible !== this.isVisible();
-
- // Parent method
- OO.ui.MenuSelectWidget.parent.prototype.toggle.call( this, visible );
-
- if ( change ) {
- if ( visible ) {
- this.bindKeyDownListener();
- this.bindKeyPressListener();
-
- if ( this.newItems && this.newItems.length ) {
- for ( i = 0, len = this.newItems.length; i < len; i++ ) {
- this.newItems[ i ].fitLabel();
- }
- this.newItems = null;
- }
- this.toggleClipping( true );
-
- // Auto-hide
- if ( this.autoHide ) {
- this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
- }
- } else {
- this.unbindKeyDownListener();
- this.unbindKeyPressListener();
- this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
- this.toggleClipping( false );
- }
- }
-
- return this;
-};
-
-/**
- * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
- * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
- * users can interact with it.
- *
- * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
- * OO.ui.DropdownInputWidget instead.
- *
- * @example
- * // Example: A DropdownWidget with a menu that contains three options
- * var dropDown = new OO.ui.DropdownWidget( {
- * label: 'Dropdown menu: Select a menu option',
- * menu: {
- * items: [
- * new OO.ui.MenuOptionWidget( {
- * data: 'a',
- * label: 'First'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'b',
- * label: 'Second'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'c',
- * label: 'Third'
- * } )
- * ]
- * }
- * } );
- *
- * $( 'body' ).append( dropDown.$element );
- *
- * dropDown.getMenu().selectItemByData( 'b' );
- *
- * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
- *
- * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.IconElement
- * @mixins OO.ui.mixin.IndicatorElement
- * @mixins OO.ui.mixin.LabelElement
- * @mixins OO.ui.mixin.TitledElement
- * @mixins OO.ui.mixin.TabIndexedElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
- * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
- * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
- * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
- */
-OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
- // Configuration initialization
- config = $.extend( { indicator: 'down' }, config );
-
- // Parent constructor
- OO.ui.DropdownWidget.parent.call( this, config );
-
- // Properties (must be set before TabIndexedElement constructor call)
- this.$handle = this.$( '<span>' );
- this.$overlay = config.$overlay || this.$element;
-
- // Mixin constructors
- OO.ui.mixin.IconElement.call( this, config );
- OO.ui.mixin.IndicatorElement.call( this, config );
- OO.ui.mixin.LabelElement.call( this, config );
- OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
- OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
-
- // Properties
- this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend( {
- widget: this,
- $container: this.$element
- }, config.menu ) );
-
- // Events
- this.$handle.on( {
- click: this.onClick.bind( this ),
- keypress: this.onKeyPress.bind( this )
- } );
- this.menu.connect( this, { select: 'onMenuSelect' } );
-
- // Initialization
- this.$handle
- .addClass( 'oo-ui-dropdownWidget-handle' )
- .append( this.$icon, this.$label, this.$indicator );
- this.$element
- .addClass( 'oo-ui-dropdownWidget' )
- .append( this.$handle );
- this.$overlay.append( this.menu.$element );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IconElement );
-OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IndicatorElement );
-OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TitledElement );
-OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TabIndexedElement );
-
-/* Methods */
-
-/**
- * Get the menu.
- *
- * @return {OO.ui.MenuSelectWidget} Menu of widget
- */
-OO.ui.DropdownWidget.prototype.getMenu = function () {
- return this.menu;
-};
-
-/**
- * Handles menu select events.
- *
- * @private
- * @param {OO.ui.MenuOptionWidget} item Selected menu item
- */
-OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
- var selectedLabel;
-
- if ( !item ) {
- this.setLabel( null );
- 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 );
-};
-
-/**
- * Handle mouse click events.
- *
- * @private
- * @param {jQuery.Event} e Mouse click event
- */
-OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
- if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
- this.menu.toggle();
- }
- return false;
-};
-
-/**
- * Handle key press events.
- *
- * @private
- * @param {jQuery.Event} e Key press event
- */
-OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
- if ( !this.isDisabled() &&
- ( ( e.which === OO.ui.Keys.SPACE && !this.menu.isVisible() ) || e.which === OO.ui.Keys.ENTER )
- ) {
- this.menu.toggle();
- return false;
- }
-};
-
-/**
- * RadioOptionWidget is an option widget that looks like a radio button.
- * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
- * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
- *
- * @class
- * @extends OO.ui.OptionWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties (must be done before parent constructor which calls #setDisabled)
- this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
-
- // Parent constructor
- OO.ui.RadioOptionWidget.parent.call( this, config );
-
- // Events
- this.radio.$input.on( 'focus', this.onInputFocus.bind( this ) );
-
- // Initialization
- // Remove implicit role, we're handling it ourselves
- this.radio.$input.attr( 'role', 'presentation' );
- this.$element
- .addClass( 'oo-ui-radioOptionWidget' )
- .attr( 'role', 'radio' )
- .attr( 'aria-checked', 'false' )
- .removeAttr( 'aria-selected' )
- .prepend( this.radio.$element );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
-
-/* Static Properties */
-
-OO.ui.RadioOptionWidget.static.highlightable = false;
-
-OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
-
-OO.ui.RadioOptionWidget.static.pressable = false;
-
-OO.ui.RadioOptionWidget.static.tagName = 'label';
-
-/* Methods */
-
-/**
- * @param {jQuery.Event} e Focus event
- * @private
- */
-OO.ui.RadioOptionWidget.prototype.onInputFocus = function () {
- this.radio.$input.blur();
- this.$element.parent().focus();
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
- OO.ui.RadioOptionWidget.parent.prototype.setSelected.call( this, state );
-
- this.radio.setSelected( state );
- this.$element
- .attr( 'aria-checked', state.toString() )
- .removeAttr( 'aria-selected' );
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
- OO.ui.RadioOptionWidget.parent.prototype.setDisabled.call( this, disabled );
-
- this.radio.setDisabled( this.isDisabled() );
-
- return this;
-};
-
-/**
- * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
- * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
- * an interface for adding, removing and selecting options.
- * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
- *
- * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
- * OO.ui.RadioSelectInputWidget instead.
- *
- * @example
- * // A RadioSelectWidget with RadioOptions.
- * var option1 = new OO.ui.RadioOptionWidget( {
- * data: 'a',
- * label: 'Selected radio option'
- * } );
- *
- * var option2 = new OO.ui.RadioOptionWidget( {
- * data: 'b',
- * label: 'Unselected radio option'
- * } );
- *
- * var radioSelect=new OO.ui.RadioSelectWidget( {
- * items: [ option1, option2 ]
- * } );
- *
- * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
- * radioSelect.selectItem( option1 );
- *
- * $( 'body' ).append( radioSelect.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
-
- *
- * @class
- * @extends OO.ui.SelectWidget
- * @mixins OO.ui.mixin.TabIndexedElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
- // Parent constructor
- OO.ui.RadioSelectWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.TabIndexedElement.call( this, config );
-
- // Events
- this.$element.on( {
- focus: this.bindKeyDownListener.bind( this ),
- blur: this.unbindKeyDownListener.bind( this )
- } );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-radioSelectWidget' )
- .attr( 'role', 'radiogroup' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
-OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
-
-/**
- * Element that will stick under a specified container, even when it is inserted elsewhere in the
- * document (for example, in a OO.ui.Window's $overlay).
- *
- * The elements's position is automatically calculated and maintained when window is resized or the
- * page is scrolled. If you reposition the container manually, you have to call #position to make
- * sure the element is still placed correctly.
- *
- * As positioning is only possible when both the element and the container are attached to the DOM
- * and visible, it's only done after you call #togglePositioning. You might want to do this inside
- * the #toggle method to display a floating popup, for example.
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
- * @cfg {jQuery} [$floatableContainer] Node to position below
- */
-OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.$floatable = null;
- this.$floatableContainer = null;
- this.$floatableWindow = null;
- this.$floatableClosestScrollable = null;
- this.onFloatableScrollHandler = this.position.bind( this );
- this.onFloatableWindowResizeHandler = this.position.bind( this );
-
- // Initialization
- this.setFloatableContainer( config.$floatableContainer );
- this.setFloatableElement( config.$floatable || this.$element );
-};
-
-/* Methods */
-
-/**
- * Set floatable element.
- *
- * If an element is already set, it will be cleaned up before setting up the new element.
- *
- * @param {jQuery} $floatable Element to make floatable
- */
-OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
- if ( this.$floatable ) {
- this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
- this.$floatable.css( { left: '', top: '' } );
- }
-
- this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
- this.position();
-};
-
-/**
- * Set floatable container.
- *
- * The element will be always positioned under the specified container.
- *
- * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
- */
-OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
- this.$floatableContainer = $floatableContainer;
- if ( this.$floatable ) {
- this.position();
- }
-};
-
-/**
- * Toggle positioning.
- *
- * Do not turn positioning on until after the element is attached to the DOM and visible.
- *
- * @param {boolean} [positioning] Enable positioning, omit to toggle
- * @chainable
- */
-OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
- var closestScrollableOfContainer, closestScrollableOfFloatable;
-
- positioning = positioning === undefined ? !this.positioning : !!positioning;
-
- if ( this.positioning !== positioning ) {
- this.positioning = positioning;
-
- closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
- closestScrollableOfFloatable = OO.ui.Element.static.getClosestScrollableContainer( this.$floatable[ 0 ] );
- if ( closestScrollableOfContainer !== closestScrollableOfFloatable ) {
- // If the scrollable is the root, we have to listen to scroll events
- // on the window because of browser inconsistencies (or do we? someone should verify this)
- if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
- closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
- }
- }
-
- if ( positioning ) {
- this.$floatableWindow = $( this.getElementWindow() );
- this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
-
- if ( closestScrollableOfContainer !== closestScrollableOfFloatable ) {
- this.$floatableClosestScrollable = $( closestScrollableOfContainer );
- this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
- }
-
- // Initial position after visible
- this.position();
- } else {
- if ( this.$floatableWindow ) {
- this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
- this.$floatableWindow = null;
- }
-
- if ( this.$floatableClosestScrollable ) {
- this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
- this.$floatableClosestScrollable = null;
- }
-
- this.$floatable.css( { left: '', top: '' } );
- }
- }
-
- return this;
-};
-
-/**
- * Position the floatable below its container.
- *
- * This should only be done when both of them are attached to the DOM and visible.
- *
- * @chainable
- */
-OO.ui.mixin.FloatableElement.prototype.position = function () {
- var pos;
-
- if ( !this.positioning ) {
- return this;
- }
-
- pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
-
- // Position under container
- pos.top += this.$floatableContainer.height();
- this.$floatable.css( pos );
-
- // We updated the position, so re-evaluate the clipping state.
- // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
- // will not notice the need to update itself.)
- // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
- // it not listen to the right events in the right places?
- if ( this.clip ) {
- this.clip();
- }
-
- return this;
-};
-
-/**
- * FloatingMenuSelectWidget is a menu that will stick under a specified
- * container, even when it is inserted elsewhere in the document (for example,
- * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
- * menu from being clipped too aggresively.
- *
- * The menu's position is automatically calculated and maintained when the menu
- * is toggled or the window is resized.
- *
- * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
- *
- * @class
- * @extends OO.ui.MenuSelectWidget
- * @mixins OO.ui.mixin.FloatableElement
- *
- * @constructor
- * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
- * Deprecated, omit this parameter and specify `$container` instead.
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
- */
-OO.ui.FloatingMenuSelectWidget = function OoUiFloatingMenuSelectWidget( inputWidget, config ) {
- // Allow 'inputWidget' parameter and config for backwards compatibility
- if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
- config = inputWidget;
- inputWidget = config.inputWidget;
- }
-
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.FloatingMenuSelectWidget.parent.call( this, config );
-
- // Properties (must be set before mixin constructors)
- this.inputWidget = inputWidget; // For backwards compatibility
- this.$container = config.$container || this.inputWidget.$element;
-
- // Mixins constructors
- OO.ui.mixin.FloatableElement.call( this, $.extend( {}, config, { $floatableContainer: this.$container } ) );
-
- // Initialization
- this.$element.addClass( 'oo-ui-floatingMenuSelectWidget' );
- // For backwards compatibility
- this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.FloatingMenuSelectWidget, OO.ui.MenuSelectWidget );
-OO.mixinClass( OO.ui.FloatingMenuSelectWidget, OO.ui.mixin.FloatableElement );
-
-// For backwards compatibility
-OO.ui.TextInputMenuSelectWidget = OO.ui.FloatingMenuSelectWidget;
-
-/* Methods */
-
-/**
- * @inheritdoc
- */
-OO.ui.FloatingMenuSelectWidget.prototype.toggle = function ( visible ) {
- var change;
- visible = visible === undefined ? !this.isVisible() : !!visible;
- change = visible !== this.isVisible();
-
- if ( change && visible ) {
- // Make sure the width is set before the parent method runs.
- this.setIdealSize( this.$container.width() );
- }
-
- // Parent method
- // This will call this.clip(), which is nonsensical since we're not positioned yet...
- OO.ui.FloatingMenuSelectWidget.parent.prototype.toggle.call( this, visible );
-
- if ( change ) {
- this.togglePositioning( this.isVisible() );
- }
-
- return this;
-};
-
-/**
- * InputWidget is the base class for all input widgets, which
- * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
- * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
- * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
- *
- * @abstract
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.FlaggedElement
- * @mixins OO.ui.mixin.TabIndexedElement
- * @mixins OO.ui.mixin.TitledElement
- * @mixins OO.ui.mixin.AccessKeyedElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
- * @cfg {string} [value=''] The value of the input.
- * @cfg {string} [dir] The directionality of the input (ltr/rtl).
- * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
- * before it is accepted.
- */
-OO.ui.InputWidget = function OoUiInputWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.InputWidget.parent.call( this, config );
-
- // Properties
- this.$input = this.getInputElement( config );
- this.value = '';
- this.inputFilter = config.inputFilter;
-
- // Mixin constructors
- OO.ui.mixin.FlaggedElement.call( this, config );
- OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
- OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
- OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$input } ) );
-
- // Events
- this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
-
- // Initialization
- this.$input
- .addClass( 'oo-ui-inputWidget-input' )
- .attr( 'name', config.name )
- .prop( 'disabled', this.isDisabled() );
- this.$element
- .addClass( 'oo-ui-inputWidget' )
- .append( this.$input );
- this.setValue( config.value );
- this.setAccessKey( config.accessKey );
- if ( config.dir ) {
- this.setDir( config.dir );
- }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.FlaggedElement );
-OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
-OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TitledElement );
-OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.AccessKeyedElement );
-
-/* Static Properties */
-
-OO.ui.InputWidget.static.supportsSimpleLabel = true;
-
-/* Static Methods */
-
-/**
- * @inheritdoc
- */
-OO.ui.InputWidget.static.reusePreInfuseDOM = function ( node, config ) {
- config = OO.ui.InputWidget.parent.static.reusePreInfuseDOM( node, config );
- // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
- config.$input = $( node ).find( '.oo-ui-inputWidget-input' );
- return config;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.InputWidget.static.gatherPreInfuseState = function ( node, config ) {
- var state = OO.ui.InputWidget.parent.static.gatherPreInfuseState( node, config );
- state.value = config.$input.val();
- // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
- state.focus = config.$input.is( ':focus' );
- return state;
-};
-
-/* Events */
-
-/**
- * @event change
- *
- * A change event is emitted when the value of the input changes.
- *
- * @param {string} value
- */
-
-/* Methods */
-
-/**
- * Get input element.
- *
- * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
- * different circumstances. The element must have a `value` property (like form elements).
- *
- * @protected
- * @param {Object} config Configuration options
- * @return {jQuery} Input element
- */
-OO.ui.InputWidget.prototype.getInputElement = function ( config ) {
- // See #reusePreInfuseDOM about config.$input
- return config.$input || $( '<input>' );
-};
-
-/**
- * Handle potentially value-changing events.
- *
- * @private
- * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
- */
-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() );
- } );
- }
-};
-
-/**
- * Get the value of the input.
- *
- * @return {string} Input value
- */
-OO.ui.InputWidget.prototype.getValue = function () {
- // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
- // it, and we won't know unless they're kind enough to trigger a 'change' event.
- var value = this.$input.val();
- if ( this.value !== value ) {
- this.setValue( value );
- }
- return this.value;
-};
-
-/**
- * Set the directionality of the input, either RTL (right-to-left) or LTR (left-to-right).
- *
- * @deprecated since v0.13.1, use #setDir directly
- * @param {boolean} isRTL Directionality is right-to-left
- * @chainable
- */
-OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
- this.setDir( isRTL ? 'rtl' : 'ltr' );
- return this;
-};
-
-/**
- * Set the directionality of the input.
- *
- * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
- * @chainable
- */
-OO.ui.InputWidget.prototype.setDir = function ( dir ) {
- this.$input.prop( 'dir', dir );
- return this;
-};
-
-/**
- * Set the value of the input.
- *
- * @param {string} value New value
- * @fires change
- * @chainable
- */
-OO.ui.InputWidget.prototype.setValue = function ( value ) {
- value = this.cleanUpValue( value );
- // Update the DOM if it has changed. Note that with cleanUpValue, it
- // is possible for the DOM value to change without this.value changing.
- if ( this.$input.val() !== value ) {
- this.$input.val( value );
- }
- if ( this.value !== value ) {
- this.value = value;
- this.emit( 'change', this.value );
- }
- return this;
-};
-
-/**
- * Set the input's access key.
- * FIXME: This is the same code as in OO.ui.mixin.ButtonElement, maybe find a better place for it?
- *
- * @param {string} accessKey Input's access key, use empty string to remove
- * @chainable
- */
-OO.ui.InputWidget.prototype.setAccessKey = function ( accessKey ) {
- accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
-
- if ( this.accessKey !== accessKey ) {
- if ( this.$input ) {
- if ( accessKey !== null ) {
- this.$input.attr( 'accesskey', accessKey );
- } else {
- this.$input.removeAttr( 'accesskey' );
- }
- }
- this.accessKey = accessKey;
- }
-
- return this;
-};
-
-/**
- * Clean up incoming value.
- *
- * Ensures value is a string, and converts undefined and null to empty string.
- *
- * @private
- * @param {string} value Original value
- * @return {string} Cleaned up value
- */
-OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
- if ( value === undefined || value === null ) {
- return '';
- } else if ( this.inputFilter ) {
- return this.inputFilter( String( value ) );
- } else {
- return String( value );
- }
-};
-
-/**
- * Simulate the behavior of clicking on a label bound to this input. This method is only called by
- * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
- * called directly.
- */
-OO.ui.InputWidget.prototype.simulateLabelClick = function () {
- if ( !this.isDisabled() ) {
- if ( this.$input.is( ':checkbox, :radio' ) ) {
- this.$input.click();
- }
- if ( this.$input.is( ':input' ) ) {
- this.$input[ 0 ].focus();
- }
- }
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
- OO.ui.InputWidget.parent.prototype.setDisabled.call( this, state );
- if ( this.$input ) {
- this.$input.prop( 'disabled', this.isDisabled() );
- }
- return this;
-};
-
-/**
- * Focus the input.
- *
- * @chainable
- */
-OO.ui.InputWidget.prototype.focus = function () {
- this.$input[ 0 ].focus();
- return this;
-};
-
-/**
- * Blur the input.
- *
- * @chainable
- */
-OO.ui.InputWidget.prototype.blur = function () {
- this.$input[ 0 ].blur();
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.InputWidget.prototype.restorePreInfuseState = function ( state ) {
- OO.ui.InputWidget.parent.prototype.restorePreInfuseState.call( this, state );
- if ( state.value !== undefined && state.value !== this.getValue() ) {
- this.setValue( state.value );
- }
- if ( state.focus ) {
- this.focus();
- }
-};
-
-/**
- * ButtonInputWidget is used to submit HTML forms and is intended to be used within
- * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
- * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
- * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
- * [OOjs UI documentation on MediaWiki] [1] for more information.
- *
- * @example
- * // A ButtonInputWidget rendered as an HTML button, the default.
- * var button = new OO.ui.ButtonInputWidget( {
- * label: 'Input button',
- * icon: 'check',
- * value: 'check'
- * } );
- * $( 'body' ).append( button.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
- *
- * @class
- * @extends OO.ui.InputWidget
- * @mixins OO.ui.mixin.ButtonElement
- * @mixins OO.ui.mixin.IconElement
- * @mixins OO.ui.mixin.IndicatorElement
- * @mixins OO.ui.mixin.LabelElement
- * @mixins OO.ui.mixin.TitledElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
- * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
- * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
- * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
- * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
- */
-OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
- // Configuration initialization
- config = $.extend( { type: 'button', useInputTag: false }, config );
-
- // Properties (must be set before parent constructor, which calls #setValue)
- this.useInputTag = config.useInputTag;
-
- // Parent constructor
- OO.ui.ButtonInputWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
- OO.ui.mixin.IconElement.call( this, config );
- OO.ui.mixin.IndicatorElement.call( this, config );
- OO.ui.mixin.LabelElement.call( this, config );
- OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
-
- // Initialization
- if ( !config.useInputTag ) {
- this.$input.append( this.$icon, this.$label, this.$indicator );
- }
- this.$element.addClass( 'oo-ui-buttonInputWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
-OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
-OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
-OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
-OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
-
-/* Static Properties */
-
-/**
- * Disable generating `<label>` elements for buttons. One would very rarely need additional label
- * for a button, and it's already a big clickable target, and it causes unexpected rendering.
- */
-OO.ui.ButtonInputWidget.static.supportsSimpleLabel = false;
-
-/* Methods */
-
-/**
- * @inheritdoc
- * @protected
- */
-OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
- var type;
- // See InputWidget#reusePreInfuseDOM about config.$input
- if ( config.$input ) {
- return config.$input.empty();
- }
- type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ? config.type : 'button';
- return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
-};
-
-/**
- * Set label value.
- *
- * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
- *
- * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
- * text, or `null` for no label
- * @chainable
- */
-OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
- OO.ui.mixin.LabelElement.prototype.setLabel.call( this, label );
-
- if ( this.useInputTag ) {
- if ( typeof label === 'function' ) {
- label = OO.ui.resolveMsg( label );
- }
- if ( label instanceof jQuery ) {
- label = label.text();
- }
- if ( !label ) {
- label = '';
- }
- this.$input.val( label );
- }
-
- return this;
-};
-
-/**
- * Set the value of the input.
- *
- * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
- * they do not support {@link #value values}.
- *
- * @param {string} value New value
- * @chainable
- */
-OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
- if ( !this.useInputTag ) {
- OO.ui.ButtonInputWidget.parent.prototype.setValue.call( this, value );
- }
- return this;
-};
-
-/**
- * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
- * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
- * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
- * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
- *
- * @example
- * // An example of selected, unselected, and disabled checkbox inputs
- * var checkbox1=new OO.ui.CheckboxInputWidget( {
- * value: 'a',
- * selected: true
- * } );
- * var checkbox2=new OO.ui.CheckboxInputWidget( {
- * value: 'b'
- * } );
- * var checkbox3=new OO.ui.CheckboxInputWidget( {
- * value:'c',
- * disabled: true
- * } );
- * // Create a fieldset layout with fields for each checkbox.
- * var fieldset = new OO.ui.FieldsetLayout( {
- * label: 'Checkboxes'
- * } );
- * fieldset.addItems( [
- * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
- * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
- * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
- * ] );
- * $( 'body' ).append( fieldset.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
- *
- * @class
- * @extends OO.ui.InputWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
- */
-OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.CheckboxInputWidget.parent.call( this, config );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-checkboxInputWidget' )
- // Required for pretty styling in MediaWiki theme
- .append( $( '<span>' ) );
- this.setSelected( config.selected !== undefined ? config.selected : false );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
-
-/* Static Methods */
-
-/**
- * @inheritdoc
- */
-OO.ui.CheckboxInputWidget.static.gatherPreInfuseState = function ( node, config ) {
- var state = OO.ui.CheckboxInputWidget.parent.static.gatherPreInfuseState( node, config );
- state.checked = config.$input.prop( 'checked' );
- return state;
-};
-
-/* Methods */
-
-/**
- * @inheritdoc
- * @protected
- */
-OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
- return $( '<input type="checkbox" />' );
-};
-
-/**
- * @inheritdoc
- */
-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.setSelected( widget.$input.prop( 'checked' ) );
- } );
- }
-};
-
-/**
- * Set selection state of this checkbox.
- *
- * @param {boolean} state `true` for selected
- * @chainable
- */
-OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
- state = !!state;
- if ( this.selected !== state ) {
- this.selected = state;
- this.$input.prop( 'checked', this.selected );
- this.emit( 'change', this.selected );
- }
- return this;
-};
-
-/**
- * Check if this checkbox is selected.
- *
- * @return {boolean} Checkbox is selected
- */
-OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
- // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
- // it, and we won't know unless they're kind enough to trigger a 'change' event.
- var selected = this.$input.prop( 'checked' );
- if ( this.selected !== selected ) {
- this.setSelected( selected );
- }
- return this.selected;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.CheckboxInputWidget.prototype.restorePreInfuseState = function ( state ) {
- OO.ui.CheckboxInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
- if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
- this.setSelected( state.checked );
- }
-};
-
-/**
- * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
- * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
- * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
- * more information about input widgets.
- *
- * A DropdownInputWidget always has a value (one of the options is always selected), unless there
- * are no options. If no `value` configuration option is provided, the first option is selected.
- * If you need a state representing no value (no option being selected), use a DropdownWidget.
- *
- * This and OO.ui.RadioSelectInputWidget support the same configuration options.
- *
- * @example
- * // Example: A DropdownInputWidget with three options
- * var dropdownInput = new OO.ui.DropdownInputWidget( {
- * options: [
- * { data: 'a', label: 'First' },
- * { data: 'b', label: 'Second'},
- * { data: 'c', label: 'Third' }
- * ]
- * } );
- * $( 'body' ).append( dropdownInput.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
- *
- * @class
- * @extends OO.ui.InputWidget
- * @mixins OO.ui.mixin.TitledElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
- * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
- */
-OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties (must be done before parent constructor which calls #setDisabled)
- this.dropdownWidget = new OO.ui.DropdownWidget( config.dropdown );
-
- // Parent constructor
- OO.ui.DropdownInputWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.TitledElement.call( this, config );
-
- // Events
- this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
-
- // Initialization
- this.setOptions( config.options || [] );
- this.$element
- .addClass( 'oo-ui-dropdownInputWidget' )
- .append( this.dropdownWidget.$element );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
-OO.mixinClass( OO.ui.DropdownInputWidget, OO.ui.mixin.TitledElement );
-
-/* Methods */
-
-/**
- * @inheritdoc
- * @protected
- */
-OO.ui.DropdownInputWidget.prototype.getInputElement = function ( config ) {
- // See InputWidget#reusePreInfuseDOM about config.$input
- if ( config.$input ) {
- return config.$input.addClass( 'oo-ui-element-hidden' );
- }
- return $( '<input type="hidden">' );
-};
-
-/**
- * Handles menu select events.
- *
- * @private
- * @param {OO.ui.MenuOptionWidget} item Selected menu item
- */
-OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
- this.setValue( item.getData() );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
- value = this.cleanUpValue( value );
- this.dropdownWidget.getMenu().selectItemByData( value );
- OO.ui.DropdownInputWidget.parent.prototype.setValue.call( this, value );
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
- this.dropdownWidget.setDisabled( state );
- OO.ui.DropdownInputWidget.parent.prototype.setDisabled.call( this, state );
- return this;
-};
-
-/**
- * Set the options available for this input.
- *
- * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
- * @chainable
- */
-OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
- var
- value = this.getValue(),
- widget = this;
-
- // Rebuild the dropdown menu
- this.dropdownWidget.getMenu()
- .clearItems()
- .addItems( options.map( function ( opt ) {
- var optValue = widget.cleanUpValue( opt.data );
- return new OO.ui.MenuOptionWidget( {
- data: optValue,
- label: opt.label !== undefined ? opt.label : optValue
- } );
- } ) );
-
- // Restore the previous value, or reset to something sensible
- if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
- // Previous value is still available, ensure consistency with the dropdown
- this.setValue( value );
- } else {
- // No longer valid, reset
- if ( options.length ) {
- this.setValue( options[ 0 ].data );
- }
- }
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.DropdownInputWidget.prototype.focus = function () {
- this.dropdownWidget.getMenu().toggle( true );
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.DropdownInputWidget.prototype.blur = function () {
- this.dropdownWidget.getMenu().toggle( false );
- return this;
-};
-
-/**
- * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
- * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
- * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
- * please see the [OOjs UI documentation on MediaWiki][1].
- *
- * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
- *
- * @example
- * // An example of selected, unselected, and disabled radio inputs
- * var radio1 = new OO.ui.RadioInputWidget( {
- * value: 'a',
- * selected: true
- * } );
- * var radio2 = new OO.ui.RadioInputWidget( {
- * value: 'b'
- * } );
- * var radio3 = new OO.ui.RadioInputWidget( {
- * value: 'c',
- * disabled: true
- * } );
- * // Create a fieldset layout with fields for each radio button.
- * var fieldset = new OO.ui.FieldsetLayout( {
- * label: 'Radio inputs'
- * } );
- * fieldset.addItems( [
- * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
- * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
- * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
- * ] );
- * $( 'body' ).append( fieldset.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
- *
- * @class
- * @extends OO.ui.InputWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
- */
-OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.RadioInputWidget.parent.call( this, config );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-radioInputWidget' )
- // Required for pretty styling in MediaWiki theme
- .append( $( '<span>' ) );
- this.setSelected( config.selected !== undefined ? config.selected : false );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
-
-/* Static Methods */
-
-/**
- * @inheritdoc
- */
-OO.ui.RadioInputWidget.static.gatherPreInfuseState = function ( node, config ) {
- var state = OO.ui.RadioInputWidget.parent.static.gatherPreInfuseState( node, config );
- state.checked = config.$input.prop( 'checked' );
- return state;
-};
-
-/* Methods */
-
-/**
- * @inheritdoc
- * @protected
- */
-OO.ui.RadioInputWidget.prototype.getInputElement = function () {
- return $( '<input type="radio" />' );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.RadioInputWidget.prototype.onEdit = function () {
- // RadioInputWidget doesn't track its state.
-};
-
-/**
- * Set selection state of this radio button.
- *
- * @param {boolean} state `true` for selected
- * @chainable
- */
-OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
- // RadioInputWidget doesn't track its state.
- this.$input.prop( 'checked', state );
- return this;
-};
-
-/**
- * Check if this radio button is selected.
- *
- * @return {boolean} Radio is selected
- */
-OO.ui.RadioInputWidget.prototype.isSelected = function () {
- return this.$input.prop( 'checked' );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.RadioInputWidget.prototype.restorePreInfuseState = function ( state ) {
- OO.ui.RadioInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
- if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
- this.setSelected( state.checked );
- }
-};
-
-/**
- * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
- * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
- * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
- * more information about input widgets.
- *
- * This and OO.ui.DropdownInputWidget support the same configuration options.
- *
- * @example
- * // Example: A RadioSelectInputWidget with three options
- * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
- * options: [
- * { data: 'a', label: 'First' },
- * { data: 'b', label: 'Second'},
- * { data: 'c', label: 'Third' }
- * ]
- * } );
- * $( 'body' ).append( radioSelectInput.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
- *
- * @class
- * @extends OO.ui.InputWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
- */
-OO.ui.RadioSelectInputWidget = function OoUiRadioSelectInputWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties (must be done before parent constructor which calls #setDisabled)
- this.radioSelectWidget = new OO.ui.RadioSelectWidget();
-
- // Parent constructor
- OO.ui.RadioSelectInputWidget.parent.call( this, config );
-
- // Events
- this.radioSelectWidget.connect( this, { select: 'onMenuSelect' } );
-
- // Initialization
- this.setOptions( config.options || [] );
- this.$element
- .addClass( 'oo-ui-radioSelectInputWidget' )
- .append( this.radioSelectWidget.$element );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.RadioSelectInputWidget, OO.ui.InputWidget );
-
-/* Static Properties */
-
-OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
-
-/* Static Methods */
-
-/**
- * @inheritdoc
- */
-OO.ui.RadioSelectInputWidget.static.gatherPreInfuseState = function ( node, config ) {
- var state = OO.ui.RadioSelectInputWidget.parent.static.gatherPreInfuseState( node, config );
- state.value = $( node ).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
- return state;
-};
-
-/* Methods */
-
-/**
- * @inheritdoc
- * @protected
- */
-OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
- return $( '<input type="hidden">' );
-};
-
-/**
- * Handles menu select events.
- *
- * @private
- * @param {OO.ui.RadioOptionWidget} item Selected menu item
- */
-OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
- this.setValue( item.getData() );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
- value = this.cleanUpValue( value );
- this.radioSelectWidget.selectItemByData( value );
- OO.ui.RadioSelectInputWidget.parent.prototype.setValue.call( this, value );
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
- this.radioSelectWidget.setDisabled( state );
- OO.ui.RadioSelectInputWidget.parent.prototype.setDisabled.call( this, state );
- return this;
-};
-
-/**
- * Set the options available for this input.
- *
- * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
- * @chainable
- */
-OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
- var
- value = this.getValue(),
- widget = this;
-
- // Rebuild the radioSelect menu
- this.radioSelectWidget
- .clearItems()
- .addItems( options.map( function ( opt ) {
- var optValue = widget.cleanUpValue( opt.data );
- return new OO.ui.RadioOptionWidget( {
- data: optValue,
- label: opt.label !== undefined ? opt.label : optValue
- } );
- } ) );
-
- // Restore the previous value, or reset to something sensible
- if ( this.radioSelectWidget.getItemFromData( value ) ) {
- // Previous value is still available, ensure consistency with the radioSelect
- this.setValue( value );
- } else {
- // No longer valid, reset
- if ( options.length ) {
- this.setValue( options[ 0 ].data );
- }
- }
-
- return this;
-};
-
-/**
- * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
- * size of the field as well as its presentation. In addition, these widgets can be configured
- * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
- * validation-pattern (used to determine if an input value is valid or not) and an input filter,
- * which modifies incoming values rather than validating them.
- * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
- *
- * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
- *
- * @example
- * // Example of a text input widget
- * var textInput = new OO.ui.TextInputWidget( {
- * value: 'Text input'
- * } )
- * $( 'body' ).append( textInput.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
- *
- * @class
- * @extends OO.ui.InputWidget
- * @mixins OO.ui.mixin.IconElement
- * @mixins OO.ui.mixin.IndicatorElement
- * @mixins OO.ui.mixin.PendingElement
- * @mixins OO.ui.mixin.LabelElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
- * 'email' or 'url'. Ignored if `multiline` is true.
- *
- * Some values of `type` result in additional behaviors:
- *
- * - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
- * empties the text field
- * @cfg {string} [placeholder] Placeholder text
- * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
- * instruct the browser to focus this widget.
- * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
- * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
- * @cfg {boolean} [multiline=false] Allow multiple lines of text
- * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
- * specifies minimum number of rows to display.
- * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
- * Use the #maxRows config to specify a maximum number of displayed rows.
- * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
- * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
- * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
- * the value or placeholder text: `'before'` or `'after'`
- * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
- * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
- * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
- * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
- * (the value must contain only numbers); when RegExp, a regular expression that must match the
- * value for it to be considered valid; when Function, a function receiving the value as parameter
- * that must return true, or promise resolving to true, for it to be considered valid.
- */
-OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
- // Configuration initialization
- config = $.extend( {
- type: 'text',
- labelPosition: 'after'
- }, config );
- if ( config.type === 'search' ) {
- if ( config.icon === undefined ) {
- config.icon = 'search';
- }
- // indicator: 'clear' is set dynamically later, depending on value
- }
- if ( config.required ) {
- if ( config.indicator === undefined ) {
- config.indicator = 'required';
- }
- }
-
- // Parent constructor
- OO.ui.TextInputWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.IconElement.call( this, config );
- OO.ui.mixin.IndicatorElement.call( this, config );
- OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$input } ) );
- OO.ui.mixin.LabelElement.call( this, config );
-
- // Properties
- this.type = this.getSaneType( config );
- this.readOnly = false;
- this.multiline = !!config.multiline;
- this.autosize = !!config.autosize;
- this.minRows = config.rows !== undefined ? config.rows : '';
- this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
- this.validate = null;
- this.styleHeight = null;
- this.scrollWidth = null;
-
- // Clone for resizing
- if ( this.autosize ) {
- this.$clone = this.$input
- .clone()
- .insertAfter( this.$input )
- .attr( 'aria-hidden', 'true' )
- .addClass( 'oo-ui-element-hidden' );
- }
-
- this.setValidation( config.validate );
- this.setLabelPosition( config.labelPosition );
-
- // Events
- this.$input.on( {
- keypress: this.onKeyPress.bind( this ),
- blur: this.onBlur.bind( this )
- } );
- this.$input.one( {
- focus: this.onElementAttach.bind( this )
- } );
- this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
- this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
- this.on( 'labelChange', this.updatePosition.bind( this ) );
- this.connect( this, {
- change: 'onChange',
- disable: 'onDisable'
- } );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type )
- .append( this.$icon, this.$indicator );
- this.setReadOnly( !!config.readOnly );
- this.updateSearchIndicator();
- if ( config.placeholder ) {
- this.$input.attr( 'placeholder', config.placeholder );
- }
- if ( config.maxLength !== undefined ) {
- this.$input.attr( 'maxlength', config.maxLength );
- }
- if ( config.autofocus ) {
- this.$input.attr( 'autofocus', 'autofocus' );
- }
- if ( config.required ) {
- this.$input.attr( 'required', 'required' );
- this.$input.attr( 'aria-required', 'true' );
- }
- if ( config.autocomplete === false ) {
- this.$input.attr( 'autocomplete', 'off' );
- // Turning off autocompletion also disables "form caching" when the user navigates to a
- // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
- $( window ).on( {
- beforeunload: function () {
- this.$input.removeAttr( 'autocomplete' );
- }.bind( this ),
- pageshow: function () {
- // Browsers don't seem to actually fire this event on "Back", they instead just reload the
- // whole page... it shouldn't hurt, though.
- this.$input.attr( 'autocomplete', 'off' );
- }.bind( this )
- } );
- }
- if ( this.multiline && config.rows ) {
- this.$input.attr( 'rows', config.rows );
- }
- if ( this.label || config.autosize ) {
- this.installParentChangeDetector();
- }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
-OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
-OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
-OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
-OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
-
-/* Static Properties */
-
-OO.ui.TextInputWidget.static.validationPatterns = {
- 'non-empty': /.+/,
- integer: /^\d+$/
-};
-
-/* Static Methods */
-
-/**
- * @inheritdoc
- */
-OO.ui.TextInputWidget.static.gatherPreInfuseState = function ( node, config ) {
- var state = OO.ui.TextInputWidget.parent.static.gatherPreInfuseState( node, config );
- if ( config.multiline ) {
- state.scrollTop = config.$input.scrollTop();
- }
- return state;
-};
-
-/* Events */
-
-/**
- * An `enter` event is emitted when the user presses 'enter' inside the text box.
- *
- * Not emitted if the input is multiline.
- *
- * @event enter
- */
-
-/**
- * A `resize` event is emitted when autosize is set and the widget resizes
- *
- * @event resize
- */
-
-/* Methods */
-
-/**
- * Handle icon mouse down events.
- *
- * @private
- * @param {jQuery.Event} e Mouse down event
- * @fires icon
- */
-OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
- if ( e.which === OO.ui.MouseButtons.LEFT ) {
- this.$input[ 0 ].focus();
- return false;
- }
-};
-
-/**
- * Handle indicator mouse down events.
- *
- * @private
- * @param {jQuery.Event} e Mouse down event
- * @fires indicator
- */
-OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
- if ( e.which === OO.ui.MouseButtons.LEFT ) {
- if ( this.type === 'search' ) {
- // Clear the text field
- this.setValue( '' );
- }
- this.$input[ 0 ].focus();
- return false;
- }
-};
-
-/**
- * Handle key press events.
- *
- * @private
- * @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', e );
- }
-};
-
-/**
- * Handle blur events.
- *
- * @private
- * @param {jQuery.Event} e Blur event
- */
-OO.ui.TextInputWidget.prototype.onBlur = function () {
- this.setValidityFlag();
-};
-
-/**
- * Handle element attach events.
- *
- * @private
- * @param {jQuery.Event} e Element attach event
- */
-OO.ui.TextInputWidget.prototype.onElementAttach = function () {
- // Any previously calculated size is now probably invalid if we reattached elsewhere
- this.valCache = null;
- this.adjustSize();
- this.positionLabel();
-};
-
-/**
- * Handle change events.
- *
- * @param {string} value
- * @private
- */
-OO.ui.TextInputWidget.prototype.onChange = function () {
- this.updateSearchIndicator();
- this.setValidityFlag();
- this.adjustSize();
-};
-
-/**
- * Handle disable events.
- *
- * @param {boolean} disabled Element is disabled
- * @private
- */
-OO.ui.TextInputWidget.prototype.onDisable = function () {
- this.updateSearchIndicator();
-};
-
-/**
- * Check if the input is {@link #readOnly read-only}.
- *
- * @return {boolean}
- */
-OO.ui.TextInputWidget.prototype.isReadOnly = function () {
- return this.readOnly;
-};
-
-/**
- * Set the {@link #readOnly read-only} state of the input.
- *
- * @param {boolean} state Make input read-only
- * @chainable
- */
-OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
- this.readOnly = !!state;
- this.$input.prop( 'readOnly', this.readOnly );
- this.updateSearchIndicator();
- return this;
-};
-
-/**
- * Support function for making #onElementAttach work across browsers.
- *
- * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
- * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
- *
- * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
- * first time that the element gets attached to the documented.
- */
-OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
- var mutationObserver, onRemove, topmostNode, fakeParentNode,
- MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
- widget = this;
-
- if ( MutationObserver ) {
- // The new way. If only it wasn't so ugly.
-
- if ( this.$element.closest( 'html' ).length ) {
- // Widget is attached already, do nothing. This breaks the functionality of this function when
- // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
- // would require observation of the whole document, which would hurt performance of other,
- // more important code.
- return;
- }
-
- // Find topmost node in the tree
- topmostNode = this.$element[ 0 ];
- while ( topmostNode.parentNode ) {
- topmostNode = topmostNode.parentNode;
- }
-
- // We have no way to detect the $element being attached somewhere without observing the entire
- // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
- // parent node of $element, and instead detect when $element is removed from it (and thus
- // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
- // doesn't get attached, we end up back here and create the parent.
-
- mutationObserver = new MutationObserver( function ( mutations ) {
- var i, j, removedNodes;
- for ( i = 0; i < mutations.length; i++ ) {
- removedNodes = mutations[ i ].removedNodes;
- for ( j = 0; j < removedNodes.length; j++ ) {
- if ( removedNodes[ j ] === topmostNode ) {
- setTimeout( onRemove, 0 );
- return;
- }
- }
- }
- } );
-
- onRemove = function () {
- // If the node was attached somewhere else, report it
- if ( widget.$element.closest( 'html' ).length ) {
- widget.onElementAttach();
- }
- mutationObserver.disconnect();
- widget.installParentChangeDetector();
- };
-
- // Create a fake parent and observe it
- fakeParentNode = $( '<div>' ).append( topmostNode )[ 0 ];
- mutationObserver.observe( fakeParentNode, { childList: true } );
- } else {
- // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
- // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
- this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
- }
-};
-
-/**
- * Automatically adjust the size of the text input.
- *
- * This only affects #multiline inputs that are {@link #autosize autosized}.
- *
- * @chainable
- * @fires resize
- */
-OO.ui.TextInputWidget.prototype.adjustSize = function () {
- var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError,
- idealHeight, newHeight, scrollWidth, property;
-
- if ( this.multiline && this.$input.val() !== this.valCache ) {
- if ( this.autosize ) {
- this.$clone
- .val( this.$input.val() )
- .attr( 'rows', this.minRows )
- // Set inline height property to 0 to measure scroll height
- .css( 'height', 0 );
-
- this.$clone.removeClass( 'oo-ui-element-hidden' );
-
- this.valCache = this.$input.val();
-
- scrollHeight = this.$clone[ 0 ].scrollHeight;
-
- // Remove inline height property to measure natural heights
- this.$clone.css( 'height', '' );
- innerHeight = this.$clone.innerHeight();
- outerHeight = this.$clone.outerHeight();
-
- // Measure max rows height
- this.$clone
- .attr( 'rows', this.maxRows )
- .css( 'height', 'auto' )
- .val( '' );
- maxInnerHeight = this.$clone.innerHeight();
-
- // Difference between reported innerHeight and scrollHeight with no scrollbars present
- // Equals 1 on Blink-based browsers and 0 everywhere else
- measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
- idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
-
- this.$clone.addClass( 'oo-ui-element-hidden' );
-
- // Only apply inline height when expansion beyond natural height is needed
- // Use the difference between the inner and outer height as a buffer
- newHeight = idealHeight > innerHeight ? idealHeight + ( outerHeight - innerHeight ) : '';
- if ( newHeight !== this.styleHeight ) {
- this.$input.css( 'height', newHeight );
- this.styleHeight = newHeight;
- this.emit( 'resize' );
- }
- }
- scrollWidth = this.$input[ 0 ].offsetWidth - this.$input[ 0 ].clientWidth;
- if ( scrollWidth !== this.scrollWidth ) {
- property = this.$element.css( 'direction' ) === 'rtl' ? 'left' : 'right';
- // Reset
- this.$label.css( { right: '', left: '' } );
- this.$indicator.css( { right: '', left: '' } );
-
- if ( scrollWidth ) {
- this.$indicator.css( property, scrollWidth );
- if ( this.labelPosition === 'after' ) {
- this.$label.css( property, scrollWidth );
- }
- }
-
- this.scrollWidth = scrollWidth;
- this.positionLabel();
- }
- }
- return this;
-};
-
-/**
- * @inheritdoc
- * @protected
- */
-OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
- return config.multiline ?
- $( '<textarea>' ) :
- $( '<input type="' + this.getSaneType( config ) + '" />' );
-};
-
-/**
- * Get sanitized value for 'type' for given config.
- *
- * @param {Object} config Configuration options
- * @return {string|null}
- * @private
- */
-OO.ui.TextInputWidget.prototype.getSaneType = function ( config ) {
- var type = [ 'text', 'password', 'search', 'email', 'url' ].indexOf( config.type ) !== -1 ?
- config.type :
- 'text';
- return config.multiline ? 'multiline' : type;
-};
-
-/**
- * Check if the input supports multiple lines.
- *
- * @return {boolean}
- */
-OO.ui.TextInputWidget.prototype.isMultiline = function () {
- return !!this.multiline;
-};
-
-/**
- * Check if the input automatically adjusts its size.
- *
- * @return {boolean}
- */
-OO.ui.TextInputWidget.prototype.isAutosizing = function () {
- return !!this.autosize;
-};
-
-/**
- * Focus the input and select a specified range within the text.
- *
- * @param {number} from Select from offset
- * @param {number} [to] Select to offset, defaults to from
- * @chainable
- */
-OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
- var isBackwards, start, end,
- input = this.$input[ 0 ];
-
- to = to || from;
-
- isBackwards = to < from;
- start = isBackwards ? to : from;
- end = isBackwards ? from : to;
-
- this.focus();
-
- input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
- return this;
-};
-
-/**
- * Get an object describing the current selection range in a directional manner
- *
- * @return {Object} Object containing 'from' and 'to' offsets
- */
-OO.ui.TextInputWidget.prototype.getRange = function () {
- var input = this.$input[ 0 ],
- start = input.selectionStart,
- end = input.selectionEnd,
- isBackwards = input.selectionDirection === 'backward';
-
- return {
- from: isBackwards ? end : start,
- to: isBackwards ? start : end
- };
-};
-
-/**
- * Get the length of the text input value.
- *
- * This could differ from the length of #getValue if the
- * value gets filtered
- *
- * @return {number} Input length
- */
-OO.ui.TextInputWidget.prototype.getInputLength = function () {
- return this.$input[ 0 ].value.length;
-};
-
-/**
- * Focus the input and select the entire text.
- *
- * @chainable
- */
-OO.ui.TextInputWidget.prototype.select = function () {
- return this.selectRange( 0, this.getInputLength() );
-};
-
-/**
- * Focus the input and move the cursor to the start.
- *
- * @chainable
- */
-OO.ui.TextInputWidget.prototype.moveCursorToStart = function () {
- return this.selectRange( 0 );
-};
-
-/**
- * Focus the input and move the cursor to the end.
- *
- * @chainable
- */
-OO.ui.TextInputWidget.prototype.moveCursorToEnd = function () {
- return this.selectRange( this.getInputLength() );
-};
-
-/**
- * Insert new content into the input.
- *
- * @param {string} content Content to be inserted
- * @chainable
- */
-OO.ui.TextInputWidget.prototype.insertContent = function ( content ) {
- var start, end,
- range = this.getRange(),
- value = this.getValue();
-
- start = Math.min( range.from, range.to );
- end = Math.max( range.from, range.to );
-
- this.setValue( value.slice( 0, start ) + content + value.slice( end ) );
- this.selectRange( start + content.length );
- return this;
-};
-
-/**
- * Insert new content either side of a selection.
- *
- * @param {string} pre Content to be inserted before the selection
- * @param {string} post Content to be inserted after the selection
- * @chainable
- */
-OO.ui.TextInputWidget.prototype.encapsulateContent = function ( pre, post ) {
- var start, end,
- range = this.getRange(),
- offset = pre.length;
-
- start = Math.min( range.from, range.to );
- end = Math.max( range.from, range.to );
-
- this.selectRange( start ).insertContent( pre );
- this.selectRange( offset + end ).insertContent( post );
-
- this.selectRange( offset + start, offset + end );
- return this;
-};
-
-/**
- * Set the validation pattern.
- *
- * The validation pattern is either a regular expression, a function, or the symbolic name of a
- * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
- * value must contain only numbers).
- *
- * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
- * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
- */
-OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
- if ( validate instanceof RegExp || validate instanceof Function ) {
- this.validate = validate;
- } else {
- this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
- }
-};
-
-/**
- * Sets the 'invalid' flag appropriately.
- *
- * @param {boolean} [isValid] Optionally override validation result
- */
-OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
- var widget = this,
- setFlag = function ( valid ) {
- if ( !valid ) {
- widget.$input.attr( 'aria-invalid', 'true' );
- } else {
- widget.$input.removeAttr( 'aria-invalid' );
- }
- widget.setFlags( { invalid: !valid } );
- };
-
- if ( isValid !== undefined ) {
- setFlag( isValid );
- } else {
- this.getValidity().then( function () {
- setFlag( true );
- }, function () {
- setFlag( false );
- } );
- }
-};
-
-/**
- * Check if a value is valid.
- *
- * This method returns a promise that resolves with a boolean `true` if the current value is
- * considered valid according to the supplied {@link #validate validation pattern}.
- *
- * @deprecated
- * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
- */
-OO.ui.TextInputWidget.prototype.isValid = function () {
- var result;
-
- if ( this.validate instanceof Function ) {
- result = this.validate( this.getValue() );
- if ( result && $.isFunction( result.promise ) ) {
- return result.promise();
- } else {
- return $.Deferred().resolve( !!result ).promise();
- }
- } else {
- return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
- }
-};
-
-/**
- * Get the validity of current value.
- *
- * This method returns a promise that resolves if the value is valid and rejects if
- * it isn't. Uses the {@link #validate validation pattern} to check for validity.
- *
- * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
- */
-OO.ui.TextInputWidget.prototype.getValidity = function () {
- var result;
-
- function rejectOrResolve( valid ) {
- if ( valid ) {
- return $.Deferred().resolve().promise();
- } else {
- return $.Deferred().reject().promise();
- }
- }
-
- if ( this.validate instanceof Function ) {
- result = this.validate( this.getValue() );
- if ( result && $.isFunction( result.promise ) ) {
- return result.promise().then( function ( valid ) {
- return rejectOrResolve( valid );
- } );
- } else {
- return rejectOrResolve( result );
- }
- } else {
- return rejectOrResolve( this.getValue().match( this.validate ) );
- }
-};
-
-/**
- * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
- *
- * @param {string} labelPosition Label position, 'before' or 'after'
- * @chainable
- */
-OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
- this.labelPosition = labelPosition;
- this.updatePosition();
- return this;
-};
-
-/**
- * Update the position of the inline label.
- *
- * This method is called by #setLabelPosition, and can also be called on its own if
- * something causes the label to be mispositioned.
- *
- * @chainable
- */
-OO.ui.TextInputWidget.prototype.updatePosition = function () {
- var after = this.labelPosition === 'after';
-
- this.$element
- .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
- .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
-
- this.valCache = null;
- this.scrollWidth = null;
- this.adjustSize();
- this.positionLabel();
-
- return this;
-};
-
-/**
- * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
- * already empty or when it's not editable.
- */
-OO.ui.TextInputWidget.prototype.updateSearchIndicator = function () {
- if ( this.type === 'search' ) {
- if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
- this.setIndicator( null );
- } else {
- this.setIndicator( 'clear' );
- }
- }
-};
-
-/**
- * Position the label by setting the correct padding on the input.
- *
- * @private
- * @chainable
- */
-OO.ui.TextInputWidget.prototype.positionLabel = function () {
- var after, rtl, property;
- // Clear old values
- this.$input
- // Clear old values if present
- .css( {
- 'padding-right': '',
- 'padding-left': ''
- } );
-
- if ( this.label ) {
- this.$element.append( this.$label );
- } else {
- this.$label.detach();
- return;
- }
-
- after = this.labelPosition === 'after';
- rtl = this.$element.css( 'direction' ) === 'rtl';
- property = after === rtl ? 'padding-left' : 'padding-right';
-
- this.$input.css( property, this.$label.outerWidth( true ) + ( after ? this.scrollWidth : 0 ) );
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.TextInputWidget.prototype.restorePreInfuseState = function ( state ) {
- OO.ui.TextInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
- if ( state.scrollTop !== undefined ) {
- this.$input.scrollTop( state.scrollTop );
- }
-};
-
-/**
- * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
- * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
- * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
- *
- * - by typing a value in the text input field. If the value exactly matches the value of a menu
- * option, that option will appear to be selected.
- * - by choosing a value from the menu. The value of the chosen option will then appear in the text
- * input field.
- *
- * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
- *
- * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * @example
- * // Example: A ComboBoxInputWidget.
- * var comboBox = new OO.ui.ComboBoxInputWidget( {
- * label: 'ComboBoxInputWidget',
- * value: 'Option 1',
- * menu: {
- * items: [
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 1',
- * label: 'Option One'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 2',
- * label: 'Option Two'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 3',
- * label: 'Option Three'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 4',
- * label: 'Option Four'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 5',
- * label: 'Option Five'
- * } )
- * ]
- * }
- * } );
- * $( 'body' ).append( comboBox.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
- *
- * @class
- * @extends OO.ui.TextInputWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
- * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
- * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
- * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
- * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
- */
-OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
- // Configuration initialization
- config = $.extend( {
- indicator: 'down'
- }, config );
- // For backwards-compatibility with ComboBoxWidget config
- $.extend( config, config.input );
-
- // Parent constructor
- OO.ui.ComboBoxInputWidget.parent.call( this, config );
-
- // Properties
- this.$overlay = config.$overlay || this.$element;
- this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
- {
- widget: this,
- input: this,
- $container: this.$element,
- disabled: this.isDisabled()
- },
- config.menu
- ) );
- // For backwards-compatibility with ComboBoxWidget
- this.input = this;
-
- // Events
- this.$indicator.on( {
- click: this.onIndicatorClick.bind( this ),
- keypress: this.onIndicatorKeyPress.bind( this )
- } );
- this.connect( this, {
- change: 'onInputChange',
- enter: 'onInputEnter'
- } );
- this.menu.connect( this, {
- choose: 'onMenuChoose',
- add: 'onMenuItemsChange',
- remove: 'onMenuItemsChange'
- } );
-
- // Initialization
- this.$input.attr( {
- role: 'combobox',
- 'aria-autocomplete': 'list'
- } );
- // Do not override options set via config.menu.items
- if ( config.options !== undefined ) {
- this.setOptions( config.options );
- }
- // Extra class for backwards-compatibility with ComboBoxWidget
- this.$element.addClass( 'oo-ui-comboBoxInputWidget oo-ui-comboBoxWidget' );
- this.$overlay.append( this.menu.$element );
- this.onMenuItemsChange();
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ComboBoxInputWidget, OO.ui.TextInputWidget );
-
-/* Methods */
-
-/**
- * Get the combobox's menu.
- * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
- */
-OO.ui.ComboBoxInputWidget.prototype.getMenu = function () {
- return this.menu;
-};
-
-/**
- * Get the combobox's text input widget.
- * @return {OO.ui.TextInputWidget} Text input widget
- */
-OO.ui.ComboBoxInputWidget.prototype.getInput = function () {
- return this;
-};
-
-/**
- * Handle input change events.
- *
- * @private
- * @param {string} value New value
- */
-OO.ui.ComboBoxInputWidget.prototype.onInputChange = function ( value ) {
- var match = this.menu.getItemFromData( value );
-
- this.menu.selectItem( match );
- if ( this.menu.getHighlightedItem() ) {
- this.menu.highlightItem( match );
- }
-
- if ( !this.isDisabled() ) {
- this.menu.toggle( true );
- }
-};
-
-/**
- * Handle mouse click events.
- *
- * @private
- * @param {jQuery.Event} e Mouse click event
- */
-OO.ui.ComboBoxInputWidget.prototype.onIndicatorClick = function ( e ) {
- if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
- this.menu.toggle();
- this.$input[ 0 ].focus();
- }
- return false;
-};
-
-/**
- * Handle key press events.
- *
- * @private
- * @param {jQuery.Event} e Key press event
- */
-OO.ui.ComboBoxInputWidget.prototype.onIndicatorKeyPress = function ( e ) {
- if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
- this.menu.toggle();
- this.$input[ 0 ].focus();
- return false;
- }
-};
-
-/**
- * Handle input enter events.
- *
- * @private
- */
-OO.ui.ComboBoxInputWidget.prototype.onInputEnter = function () {
- if ( !this.isDisabled() ) {
- this.menu.toggle( false );
- }
-};
-
-/**
- * Handle menu choose events.
- *
- * @private
- * @param {OO.ui.OptionWidget} item Chosen item
- */
-OO.ui.ComboBoxInputWidget.prototype.onMenuChoose = function ( item ) {
- this.setValue( item.getData() );
-};
-
-/**
- * Handle menu item change events.
- *
- * @private
- */
-OO.ui.ComboBoxInputWidget.prototype.onMenuItemsChange = function () {
- var match = this.menu.getItemFromData( this.getValue() );
- this.menu.selectItem( match );
- if ( this.menu.getHighlightedItem() ) {
- this.menu.highlightItem( match );
- }
- this.$element.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu.isEmpty() );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ComboBoxInputWidget.prototype.setDisabled = function ( disabled ) {
- // Parent method
- OO.ui.ComboBoxInputWidget.parent.prototype.setDisabled.call( this, disabled );
-
- if ( this.menu ) {
- this.menu.setDisabled( this.isDisabled() );
- }
-
- return this;
-};
-
-/**
- * Set the options available for this input.
- *
- * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
- * @chainable
- */
-OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) {
- this.getMenu()
- .clearItems()
- .addItems( options.map( function ( opt ) {
- return new OO.ui.MenuOptionWidget( {
- data: opt.data,
- label: opt.label !== undefined ? opt.label : opt.data
- } );
- } ) );
-
- return this;
-};
-
-/**
- * @class
- * @deprecated Use OO.ui.ComboBoxInputWidget instead.
- */
-OO.ui.ComboBoxWidget = OO.ui.ComboBoxInputWidget;
-
-/**
- * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
- * which is a widget that is specified by reference before any optional configuration settings.
- *
- * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
- *
- * - **left**: The label is placed before the field-widget and aligned with the left margin.
- * A left-alignment is used for forms with many fields.
- * - **right**: The label is placed before the field-widget and aligned to the right margin.
- * A right-alignment is used for long but familiar forms which users tab through,
- * verifying the current field with a quick glance at the label.
- * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
- * that users fill out from top to bottom.
- * - **inline**: The label is placed after the field-widget and aligned to the left.
- * An inline-alignment is best used with checkboxes or radio buttons.
- *
- * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
- * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
- * @class
- * @extends OO.ui.Layout
- * @mixins OO.ui.mixin.LabelElement
- * @mixins OO.ui.mixin.TitledElement
- *
- * @constructor
- * @param {OO.ui.Widget} fieldWidget Field widget
- * @param {Object} [config] Configuration options
- * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
- * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
- * The array may contain strings or OO.ui.HtmlSnippet instances.
- * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
- * The array may contain strings or OO.ui.HtmlSnippet instances.
- * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
- * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
- * For important messages, you are advised to use `notices`, as they are always shown.
- *
- * @throws {Error} An error is thrown if no widget is specified
- */
-OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
- var hasInputWidget, div;
-
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
- config = fieldWidget;
- fieldWidget = config.fieldWidget;
- }
-
- // Make sure we have required constructor arguments
- if ( fieldWidget === undefined ) {
- throw new Error( 'Widget not found' );
- }
-
- hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel;
-
- // Configuration initialization
- config = $.extend( { align: 'left' }, config );
-
- // Parent constructor
- OO.ui.FieldLayout.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.LabelElement.call( this, config );
- OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
-
- // Properties
- this.fieldWidget = fieldWidget;
- this.errors = [];
- this.notices = [];
- this.$field = $( '<div>' );
- this.$messages = $( '<ul>' );
- this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
- this.align = null;
- if ( config.help ) {
- this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
- classes: [ 'oo-ui-fieldLayout-help' ],
- framed: false,
- icon: 'info'
- } );
-
- div = $( '<div>' );
- if ( config.help instanceof OO.ui.HtmlSnippet ) {
- div.html( config.help.toString() );
- } else {
- div.text( config.help );
- }
- this.popupButtonWidget.getPopup().$body.append(
- div.addClass( 'oo-ui-fieldLayout-help-content' )
- );
- this.$help = this.popupButtonWidget.$element;
- } else {
- this.$help = $( [] );
- }
-
- // Events
- if ( hasInputWidget ) {
- this.$label.on( 'click', this.onLabelClick.bind( this ) );
- }
- this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-fieldLayout' )
- .append( this.$help, this.$body );
- this.$body.addClass( 'oo-ui-fieldLayout-body' );
- this.$messages.addClass( 'oo-ui-fieldLayout-messages' );
- this.$field
- .addClass( 'oo-ui-fieldLayout-field' )
- .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
- .append( this.fieldWidget.$element );
-
- this.setErrors( config.errors || [] );
- this.setNotices( config.notices || [] );
- this.setAlignment( config.align );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
-OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.TitledElement );
-
-/* Methods */
-
-/**
- * Handle field disable events.
- *
- * @private
- * @param {boolean} value Field is disabled
- */
-OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
- this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
-};
-
-/**
- * Handle label mouse click events.
- *
- * @private
- * @param {jQuery.Event} e Mouse click event
- */
-OO.ui.FieldLayout.prototype.onLabelClick = function () {
- this.fieldWidget.simulateLabelClick();
- return false;
-};
-
-/**
- * Get the widget contained by the field.
- *
- * @return {OO.ui.Widget} Field widget
- */
-OO.ui.FieldLayout.prototype.getField = function () {
- return this.fieldWidget;
-};
-
-/**
- * @protected
- * @param {string} kind 'error' or 'notice'
- * @param {string|OO.ui.HtmlSnippet} text
- * @return {jQuery}
- */
-OO.ui.FieldLayout.prototype.makeMessage = function ( kind, text ) {
- var $listItem, $icon, message;
- $listItem = $( '<li>' );
- if ( kind === 'error' ) {
- $icon = new OO.ui.IconWidget( { icon: 'alert', flags: [ 'warning' ] } ).$element;
- } else if ( kind === 'notice' ) {
- $icon = new OO.ui.IconWidget( { icon: 'info' } ).$element;
- } else {
- $icon = '';
- }
- message = new OO.ui.LabelWidget( { label: text } );
- $listItem
- .append( $icon, message.$element )
- .addClass( 'oo-ui-fieldLayout-messages-' + kind );
- return $listItem;
-};
-
-/**
- * Set the field alignment mode.
- *
- * @private
- * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
- * @chainable
- */
-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.$body.append( this.$field, this.$label );
- } else {
- this.$body.append( this.$label, this.$field );
- }
- // Set classes. The following classes can be used here:
- // * oo-ui-fieldLayout-align-left
- // * oo-ui-fieldLayout-align-right
- // * oo-ui-fieldLayout-align-top
- // * oo-ui-fieldLayout-align-inline
- if ( this.align ) {
- this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
- }
- this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
- this.align = value;
- }
-
- return this;
-};
-
-/**
- * Set the list of error messages.
- *
- * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
- * The array may contain strings or OO.ui.HtmlSnippet instances.
- * @chainable
- */
-OO.ui.FieldLayout.prototype.setErrors = function ( errors ) {
- this.errors = errors.slice();
- this.updateMessages();
- return this;
-};
-
-/**
- * Set the list of notice messages.
- *
- * @param {Array} notices Notices about the widget, which will be displayed below the widget.
- * The array may contain strings or OO.ui.HtmlSnippet instances.
- * @chainable
- */
-OO.ui.FieldLayout.prototype.setNotices = function ( notices ) {
- this.notices = notices.slice();
- this.updateMessages();
- return this;
-};
-
-/**
- * Update the rendering of error and notice messages.
- *
- * @private
- */
-OO.ui.FieldLayout.prototype.updateMessages = function () {
- var i;
- this.$messages.empty();
-
- if ( this.errors.length || this.notices.length ) {
- this.$body.after( this.$messages );
- } else {
- this.$messages.remove();
- return;
- }
-
- for ( i = 0; i < this.notices.length; i++ ) {
- this.$messages.append( this.makeMessage( 'notice', this.notices[ i ] ) );
- }
- for ( i = 0; i < this.errors.length; i++ ) {
- this.$messages.append( this.makeMessage( 'error', this.errors[ i ] ) );
- }
-};
-
-/**
- * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
- * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
- * is required and is specified before any optional configuration settings.
- *
- * Labels can be aligned in one of four ways:
- *
- * - **left**: The label is placed before the field-widget and aligned with the left margin.
- * A left-alignment is used for forms with many fields.
- * - **right**: The label is placed before the field-widget and aligned to the right margin.
- * A right-alignment is used for long but familiar forms which users tab through,
- * verifying the current field with a quick glance at the label.
- * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
- * that users fill out from top to bottom.
- * - **inline**: The label is placed after the field-widget and aligned to the left.
- * An inline-alignment is best used with checkboxes or radio buttons.
- *
- * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
- * text is specified.
- *
- * @example
- * // Example of an ActionFieldLayout
- * var actionFieldLayout = new OO.ui.ActionFieldLayout(
- * new OO.ui.TextInputWidget( {
- * placeholder: 'Field widget'
- * } ),
- * new OO.ui.ButtonWidget( {
- * label: 'Button'
- * } ),
- * {
- * label: 'An ActionFieldLayout. This label is aligned top',
- * align: 'top',
- * help: 'This is help text'
- * }
- * );
- *
- * $( 'body' ).append( actionFieldLayout.$element );
- *
- * @class
- * @extends OO.ui.FieldLayout
- *
- * @constructor
- * @param {OO.ui.Widget} fieldWidget Field widget
- * @param {OO.ui.ButtonWidget} buttonWidget Button widget
- */
-OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
- config = fieldWidget;
- fieldWidget = config.fieldWidget;
- buttonWidget = config.buttonWidget;
- }
-
- // Parent constructor
- OO.ui.ActionFieldLayout.parent.call( this, fieldWidget, config );
-
- // Properties
- this.buttonWidget = buttonWidget;
- this.$button = $( '<div>' );
- this.$input = $( '<div>' );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-actionFieldLayout' );
- this.$button
- .addClass( 'oo-ui-actionFieldLayout-button' )
- .append( this.buttonWidget.$element );
- this.$input
- .addClass( 'oo-ui-actionFieldLayout-input' )
- .append( this.fieldWidget.$element );
- this.$field
- .append( this.$input, this.$button );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
-
-/**
- * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
- * which each contain an individual widget and, optionally, a label. Each Fieldset can be
- * configured with a label as well. For more information and examples,
- * please see the [OOjs UI documentation on MediaWiki][1].
- *
- * @example
- * // Example of a fieldset layout
- * var input1 = new OO.ui.TextInputWidget( {
- * placeholder: 'A text input field'
- * } );
- *
- * var input2 = new OO.ui.TextInputWidget( {
- * placeholder: 'A text input field'
- * } );
- *
- * var fieldset = new OO.ui.FieldsetLayout( {
- * label: 'Example of a fieldset layout'
- * } );
- *
- * fieldset.addItems( [
- * new OO.ui.FieldLayout( input1, {
- * label: 'Field One'
- * } ),
- * new OO.ui.FieldLayout( input2, {
- * label: 'Field Two'
- * } )
- * ] );
- * $( 'body' ).append( fieldset.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
- *
- * @class
- * @extends OO.ui.Layout
- * @mixins OO.ui.mixin.IconElement
- * @mixins OO.ui.mixin.LabelElement
- * @mixins OO.ui.mixin.GroupElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
- */
-OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.FieldsetLayout.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.IconElement.call( this, config );
- OO.ui.mixin.LabelElement.call( this, config );
- OO.ui.mixin.GroupElement.call( this, config );
-
- if ( config.help ) {
- this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
- classes: [ 'oo-ui-fieldsetLayout-help' ],
- framed: false,
- icon: 'info'
- } );
-
- this.popupButtonWidget.getPopup().$body.append(
- $( '<div>' )
- .text( config.help )
- .addClass( 'oo-ui-fieldsetLayout-help-content' )
- );
- this.$help = this.popupButtonWidget.$element;
- } else {
- this.$help = $( [] );
- }
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-fieldsetLayout' )
- .prepend( this.$help, this.$icon, this.$label, this.$group );
- if ( Array.isArray( config.items ) ) {
- this.addItems( config.items );
- }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
-OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
-OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
-
-/**
- * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
- * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
- * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
- * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
- *
- * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
- * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
- * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
- * some fancier controls. Some controls have both regular and InputWidget variants, for example
- * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
- * often have simplified APIs to match the capabilities of HTML forms.
- * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
- * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
- *
- * @example
- * // Example of a form layout that wraps a fieldset layout
- * var input1 = new OO.ui.TextInputWidget( {
- * placeholder: 'Username'
- * } );
- * var input2 = new OO.ui.TextInputWidget( {
- * placeholder: 'Password',
- * type: 'password'
- * } );
- * var submit = new OO.ui.ButtonInputWidget( {
- * label: 'Submit'
- * } );
- *
- * var fieldset = new OO.ui.FieldsetLayout( {
- * label: 'A form layout'
- * } );
- * fieldset.addItems( [
- * new OO.ui.FieldLayout( input1, {
- * label: 'Username',
- * align: 'top'
- * } ),
- * new OO.ui.FieldLayout( input2, {
- * label: 'Password',
- * align: 'top'
- * } ),
- * new OO.ui.FieldLayout( submit )
- * ] );
- * var form = new OO.ui.FormLayout( {
- * items: [ fieldset ],
- * action: '/api/formhandler',
- * method: 'get'
- * } )
- * $( 'body' ).append( form.$element );
- *
- * @class
- * @extends OO.ui.Layout
- * @mixins OO.ui.mixin.GroupElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string} [method] HTML form `method` attribute
- * @cfg {string} [action] HTML form `action` attribute
- * @cfg {string} [enctype] HTML form `enctype` attribute
- * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
- */
-OO.ui.FormLayout = function OoUiFormLayout( config ) {
- var action;
-
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.FormLayout.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
-
- // Events
- this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
-
- // Make sure the action is safe
- action = config.action;
- if ( action !== undefined && !OO.ui.isSafeUrl( action ) ) {
- action = './' + action;
- }
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-formLayout' )
- .attr( {
- method: config.method,
- action: action,
- enctype: config.enctype
- } );
- if ( Array.isArray( config.items ) ) {
- this.addItems( config.items );
- }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
-OO.mixinClass( OO.ui.FormLayout, OO.ui.mixin.GroupElement );
-
-/* Events */
-
-/**
- * A 'submit' event is emitted when the form is submitted.
- *
- * @event submit
- */
-
-/* Static Properties */
-
-OO.ui.FormLayout.static.tagName = 'form';
-
-/* Methods */
-
-/**
- * Handle form submit events.
- *
- * @private
- * @param {jQuery.Event} e Submit event
- * @fires submit
- */
-OO.ui.FormLayout.prototype.onFormSubmit = function () {
- if ( this.emit( 'submit' ) ) {
- return false;
- }
-};
-
-/**
- * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
- * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
- *
- * @example
- * // Example of a panel layout
- * var panel = new OO.ui.PanelLayout( {
- * expanded: false,
- * framed: true,
- * padded: true,
- * $content: $( '<p>A panel layout with padding and a frame.</p>' )
- * } );
- * $( 'body' ).append( panel.$element );
- *
- * @class
- * @extends OO.ui.Layout
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [scrollable=false] Allow vertical scrolling
- * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
- * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
- * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
- */
-OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
- // Configuration initialization
- config = $.extend( {
- scrollable: false,
- padded: false,
- expanded: true,
- framed: false
- }, config );
-
- // Parent constructor
- OO.ui.PanelLayout.parent.call( this, config );
-
- // Initialization
- this.$element.addClass( 'oo-ui-panelLayout' );
- if ( config.scrollable ) {
- this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
- }
- if ( config.padded ) {
- this.$element.addClass( 'oo-ui-panelLayout-padded' );
- }
- if ( config.expanded ) {
- this.$element.addClass( 'oo-ui-panelLayout-expanded' );
- }
- if ( config.framed ) {
- this.$element.addClass( 'oo-ui-panelLayout-framed' );
- }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
-
-/* Methods */
-
-/**
- * Focus the panel layout
- *
- * The default implementation just focuses the first focusable element in the panel
- */
-OO.ui.PanelLayout.prototype.focus = function () {
- OO.ui.findFocusable( this.$element ).focus();
-};
-
-/**
- * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
- * items), with small margins between them. Convenient when you need to put a number of block-level
- * widgets on a single line next to each other.
- *
- * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
- *
- * @example
- * // HorizontalLayout with a text input and a label
- * var layout = new OO.ui.HorizontalLayout( {
- * items: [
- * new OO.ui.LabelWidget( { label: 'Label' } ),
- * new OO.ui.TextInputWidget( { value: 'Text' } )
- * ]
- * } );
- * $( 'body' ).append( layout.$element );
- *
- * @class
- * @extends OO.ui.Layout
- * @mixins OO.ui.mixin.GroupElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
- */
-OO.ui.HorizontalLayout = function OoUiHorizontalLayout( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.HorizontalLayout.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
-
- // Initialization
- this.$element.addClass( 'oo-ui-horizontalLayout' );
- if ( Array.isArray( config.items ) ) {
- this.addItems( config.items );
- }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.HorizontalLayout, OO.ui.Layout );
-OO.mixinClass( OO.ui.HorizontalLayout, OO.ui.mixin.GroupElement );
-
-}( OO ) );
-
-/*!
- * OOjs UI v0.15.2
- * https://www.mediawiki.org/wiki/OOjs_UI
- *
- * Copyright 2011–2016 OOjs UI Team and other contributors.
- * Released under the MIT license
- * http://oojs.mit-license.org
- *
- * Date: 2016-02-02T22:07:00Z
- */
-( function ( OO ) {
-
-'use strict';
-
-/**
- * DraggableElement is a mixin class used to create elements that can be clicked
- * and dragged by a mouse to a new position within a group. This class must be used
- * in conjunction with OO.ui.mixin.DraggableGroupElement, which provides a container for
- * the draggable elements.
- *
- * @abstract
- * @class
- *
- * @constructor
- */
-OO.ui.mixin.DraggableElement = function OoUiMixinDraggableElement() {
- // Properties
- this.index = null;
-
- // Initialize and events
- this.$element
- .attr( 'draggable', true )
- .addClass( 'oo-ui-draggableElement' )
- .on( {
- dragstart: this.onDragStart.bind( this ),
- dragover: this.onDragOver.bind( this ),
- dragend: this.onDragEnd.bind( this ),
- drop: this.onDrop.bind( this )
- } );
-};
-
-OO.initClass( OO.ui.mixin.DraggableElement );
-
-/* Events */
-
-/**
- * @event dragstart
- *
- * A dragstart event is emitted when the user clicks and begins dragging an item.
- * @param {OO.ui.mixin.DraggableElement} item The item the user has clicked and is dragging with the mouse.
- */
-
-/**
- * @event dragend
- * A dragend event is emitted when the user drags an item and releases the mouse,
- * thus terminating the drag operation.
- */
-
-/**
- * @event drop
- * A drop event is emitted when the user drags an item and then releases the mouse button
- * over a valid target.
- */
-
-/* Static Properties */
-
-/**
- * @inheritdoc OO.ui.mixin.ButtonElement
- */
-OO.ui.mixin.DraggableElement.static.cancelButtonMouseDownEvents = false;
-
-/* Methods */
-
-/**
- * Respond to dragstart event.
- *
- * @private
- * @param {jQuery.Event} event jQuery event
- * @fires dragstart
- */
-OO.ui.mixin.DraggableElement.prototype.onDragStart = function ( e ) {
- var dataTransfer = e.originalEvent.dataTransfer;
- // Define drop effect
- dataTransfer.dropEffect = 'none';
- dataTransfer.effectAllowed = 'move';
- // Support: Firefox
- // We must set up a dataTransfer data property or Firefox seems to
- // ignore the fact the element is draggable.
- try {
- dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
- } catch ( err ) {
- // The above is only for Firefox. Move on if it fails.
- }
- // Add dragging class
- this.$element.addClass( 'oo-ui-draggableElement-dragging' );
- // Emit event
- this.emit( 'dragstart', this );
- return true;
-};
-
-/**
- * Respond to dragend event.
- *
- * @private
- * @fires dragend
- */
-OO.ui.mixin.DraggableElement.prototype.onDragEnd = function () {
- this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
- this.emit( 'dragend' );
-};
-
-/**
- * Handle drop event.
- *
- * @private
- * @param {jQuery.Event} event jQuery event
- * @fires drop
- */
-OO.ui.mixin.DraggableElement.prototype.onDrop = function ( e ) {
- e.preventDefault();
- this.emit( 'drop', e );
-};
-
-/**
- * In order for drag/drop to work, the dragover event must
- * return false and stop propogation.
- *
- * @private
- */
-OO.ui.mixin.DraggableElement.prototype.onDragOver = function ( e ) {
- e.preventDefault();
-};
-
-/**
- * Set item index.
- * Store it in the DOM so we can access from the widget drag event
- *
- * @private
- * @param {number} Item index
- */
-OO.ui.mixin.DraggableElement.prototype.setIndex = function ( index ) {
- if ( this.index !== index ) {
- this.index = index;
- this.$element.data( 'index', index );
- }
-};
-
-/**
- * Get item index
- *
- * @private
- * @return {number} Item index
- */
-OO.ui.mixin.DraggableElement.prototype.getIndex = function () {
- return this.index;
-};
-
-/**
- * DraggableGroupElement is a mixin class used to create a group element to
- * contain draggable elements, which are items that can be clicked and dragged by a mouse.
- * The class is used with OO.ui.mixin.DraggableElement.
- *
- * @abstract
- * @class
- * @mixins OO.ui.mixin.GroupElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation
- * should match the layout of the items. Items displayed in a single row
- * or in several rows should use horizontal orientation. The vertical orientation should only be
- * used when the items are displayed in a single column. Defaults to 'vertical'
- */
-OO.ui.mixin.DraggableGroupElement = function OoUiMixinDraggableGroupElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.mixin.GroupElement.call( this, config );
-
- // Properties
- this.orientation = config.orientation || 'vertical';
- this.dragItem = null;
- this.itemDragOver = null;
- this.itemKeys = {};
- this.sideInsertion = '';
-
- // Events
- this.aggregate( {
- dragstart: 'itemDragStart',
- dragend: 'itemDragEnd',
- drop: 'itemDrop'
- } );
- this.connect( this, {
- itemDragStart: 'onItemDragStart',
- itemDrop: 'onItemDrop',
- itemDragEnd: 'onItemDragEnd'
- } );
- this.$element.on( {
- dragover: this.onDragOver.bind( this ),
- dragleave: this.onDragLeave.bind( this )
- } );
-
- // Initialize
- if ( Array.isArray( config.items ) ) {
- this.addItems( config.items );
- }
- this.$placeholder = $( '<div>' )
- .addClass( 'oo-ui-draggableGroupElement-placeholder' );
- this.$element
- .addClass( 'oo-ui-draggableGroupElement' )
- .append( this.$status )
- .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
- .prepend( this.$placeholder );
-};
-
-/* Setup */
-OO.mixinClass( OO.ui.mixin.DraggableGroupElement, OO.ui.mixin.GroupElement );
-
-/* Events */
-
-/**
- * A 'reorder' event is emitted when the order of items in the group changes.
- *
- * @event reorder
- * @param {OO.ui.mixin.DraggableElement} item Reordered item
- * @param {number} [newIndex] New index for the item
- */
-
-/* Methods */
-
-/**
- * Respond to item drag start event
- *
- * @private
- * @param {OO.ui.mixin.DraggableElement} item Dragged item
- */
-OO.ui.mixin.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
- var i, len;
-
- // Map the index of each object
- for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.items[ i ].setIndex( i );
- }
-
- if ( this.orientation === 'horizontal' ) {
- // Set the height of the indicator
- this.$placeholder.css( {
- height: item.$element.outerHeight(),
- width: 2
- } );
- } else {
- // Set the width of the indicator
- this.$placeholder.css( {
- height: 2,
- width: item.$element.outerWidth()
- } );
- }
- this.setDragItem( item );
-};
-
-/**
- * Respond to item drag end event
- *
- * @private
- */
-OO.ui.mixin.DraggableGroupElement.prototype.onItemDragEnd = function () {
- this.unsetDragItem();
- return false;
-};
-
-/**
- * Handle drop event and switch the order of the items accordingly
- *
- * @private
- * @param {OO.ui.mixin.DraggableElement} item Dropped item
- * @fires reorder
- */
-OO.ui.mixin.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
- var toIndex = item.getIndex();
- // Check if the dropped item is from the current group
- // TODO: Figure out a way to configure a list of legally droppable
- // elements even if they are not yet in the list
- if ( this.getDragItem() ) {
- // If the insertion point is 'after', the insertion index
- // is shifted to the right (or to the left in RTL, hence 'after')
- if ( this.sideInsertion === 'after' ) {
- toIndex++;
- }
- // Emit change event
- this.emit( 'reorder', this.getDragItem(), toIndex );
- }
- this.unsetDragItem();
- // Return false to prevent propogation
- return false;
-};
-
-/**
- * Handle dragleave event.
- *
- * @private
- */
-OO.ui.mixin.DraggableGroupElement.prototype.onDragLeave = function () {
- // This means the item was dragged outside the widget
- this.$placeholder
- .css( 'left', 0 )
- .addClass( 'oo-ui-element-hidden' );
-};
-
-/**
- * Respond to dragover event
- *
- * @private
- * @param {jQuery.Event} event Event details
- */
-OO.ui.mixin.DraggableGroupElement.prototype.onDragOver = function ( e ) {
- var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
- itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
- clientX = e.originalEvent.clientX,
- clientY = e.originalEvent.clientY;
-
- // Get the OptionWidget item we are dragging over
- dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
- $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
- if ( $optionWidget[ 0 ] ) {
- itemOffset = $optionWidget.offset();
- itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
- itemPosition = $optionWidget.position();
- itemIndex = $optionWidget.data( 'index' );
- }
-
- if (
- itemOffset &&
- this.isDragging() &&
- itemIndex !== this.getDragItem().getIndex()
- ) {
- if ( this.orientation === 'horizontal' ) {
- // Calculate where the mouse is relative to the item width
- itemSize = itemBoundingRect.width;
- itemMidpoint = itemBoundingRect.left + itemSize / 2;
- dragPosition = clientX;
- // Which side of the item we hover over will dictate
- // where the placeholder will appear, on the left or
- // on the right
- cssOutput = {
- left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
- top: itemPosition.top
- };
- } else {
- // Calculate where the mouse is relative to the item height
- itemSize = itemBoundingRect.height;
- itemMidpoint = itemBoundingRect.top + itemSize / 2;
- dragPosition = clientY;
- // Which side of the item we hover over will dictate
- // where the placeholder will appear, on the top or
- // on the bottom
- cssOutput = {
- top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
- left: itemPosition.left
- };
- }
- // Store whether we are before or after an item to rearrange
- // For horizontal layout, we need to account for RTL, as this is flipped
- if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
- this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
- } else {
- this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
- }
- // Add drop indicator between objects
- this.$placeholder
- .css( cssOutput )
- .removeClass( 'oo-ui-element-hidden' );
- } else {
- // This means the item was dragged outside the widget
- this.$placeholder
- .css( 'left', 0 )
- .addClass( 'oo-ui-element-hidden' );
- }
- // Prevent default
- e.preventDefault();
-};
-
-/**
- * Set a dragged item
- *
- * @param {OO.ui.mixin.DraggableElement} item Dragged item
- */
-OO.ui.mixin.DraggableGroupElement.prototype.setDragItem = function ( item ) {
- this.dragItem = item;
-};
-
-/**
- * Unset the current dragged item
- */
-OO.ui.mixin.DraggableGroupElement.prototype.unsetDragItem = function () {
- this.dragItem = null;
- this.itemDragOver = null;
- this.$placeholder.addClass( 'oo-ui-element-hidden' );
- this.sideInsertion = '';
-};
-
-/**
- * Get the item that is currently being dragged.
- *
- * @return {OO.ui.mixin.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged
- */
-OO.ui.mixin.DraggableGroupElement.prototype.getDragItem = function () {
- return this.dragItem;
-};
-
-/**
- * Check if an item in the group is currently being dragged.
- *
- * @return {Boolean} Item is being dragged
- */
-OO.ui.mixin.DraggableGroupElement.prototype.isDragging = function () {
- return this.getDragItem() !== null;
-};
-
-/**
- * RequestManager is a mixin that manages the lifecycle of a promise-backed request for a widget, such as
- * the {@link OO.ui.mixin.LookupElement}.
- *
- * @class
- * @abstract
- *
- * @constructor
- */
-OO.ui.mixin.RequestManager = function OoUiMixinRequestManager() {
- this.requestCache = {};
- this.requestQuery = null;
- this.requestRequest = null;
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.mixin.RequestManager );
-
-/**
- * Get request results for the current query.
- *
- * @return {jQuery.Promise} Promise object which will be passed response data as the first argument of
- * the done event. If the request was aborted to make way for a subsequent request, this promise
- * may not be rejected, depending on what jQuery feels like doing.
- */
-OO.ui.mixin.RequestManager.prototype.getRequestData = function () {
- var widget = this,
- value = this.getRequestQuery(),
- deferred = $.Deferred(),
- ourRequest;
-
- this.abortRequest();
- if ( Object.prototype.hasOwnProperty.call( this.requestCache, value ) ) {
- deferred.resolve( this.requestCache[ value ] );
- } else {
- if ( this.pushPending ) {
- this.pushPending();
- }
- this.requestQuery = value;
- ourRequest = this.requestRequest = this.getRequest();
- ourRequest
- .always( function () {
- // We need to pop pending even if this is an old request, otherwise
- // the widget will remain pending forever.
- // TODO: this assumes that an aborted request will fail or succeed soon after
- // being aborted, or at least eventually. It would be nice if we could popPending()
- // at abort time, but only if we knew that we hadn't already called popPending()
- // for that request.
- if ( widget.popPending ) {
- widget.popPending();
- }
- } )
- .done( function ( response ) {
- // If this is an old request (and aborting it somehow caused it to still succeed),
- // ignore its success completely
- if ( ourRequest === widget.requestRequest ) {
- widget.requestQuery = null;
- widget.requestRequest = null;
- widget.requestCache[ value ] = widget.getRequestCacheDataFromResponse( response );
- deferred.resolve( widget.requestCache[ value ] );
- }
- } )
- .fail( function () {
- // If this is an old request (or a request failing because it's being aborted),
- // ignore its failure completely
- if ( ourRequest === widget.requestRequest ) {
- widget.requestQuery = null;
- widget.requestRequest = null;
- deferred.reject();
- }
- } );
- }
- return deferred.promise();
-};
-
-/**
- * Abort the currently pending request, if any.
- *
- * @private
- */
-OO.ui.mixin.RequestManager.prototype.abortRequest = function () {
- var oldRequest = this.requestRequest;
- if ( oldRequest ) {
- // First unset this.requestRequest to the fail handler will notice
- // that the request is no longer current
- this.requestRequest = null;
- this.requestQuery = null;
- oldRequest.abort();
- }
-};
-
-/**
- * Get the query to be made.
- *
- * @protected
- * @method
- * @abstract
- * @return {string} query to be used
- */
-OO.ui.mixin.RequestManager.prototype.getRequestQuery = null;
-
-/**
- * Get a new request object of the current query value.
- *
- * @protected
- * @method
- * @abstract
- * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
- */
-OO.ui.mixin.RequestManager.prototype.getRequest = null;
-
-/**
- * Pre-process data returned by the request from #getRequest.
- *
- * The return value of this function will be cached, and any further queries for the given value
- * will use the cache rather than doing API requests.
- *
- * @protected
- * @method
- * @abstract
- * @param {Mixed} response Response from server
- * @return {Mixed} Cached result data
- */
-OO.ui.mixin.RequestManager.prototype.getRequestCacheDataFromResponse = null;
-
-/**
- * LookupElement is a mixin that creates a {@link OO.ui.FloatingMenuSelectWidget menu} of suggested values for
- * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
- * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
- * from the lookup menu, that value becomes the value of the input field.
- *
- * Note that a new menu of suggested items is displayed when a value is chosen from the lookup menu. If this is
- * not the desired behavior, disable lookup menus with the #setLookupsDisabled method, then set the value, then
- * re-enable lookups.
- *
- * See the [OOjs UI demos][1] for an example.
- *
- * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/index.html#widgets-apex-vector-ltr
- *
- * @class
- * @abstract
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
- * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
- * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
- * By default, the lookup menu is not generated and displayed until the user begins to type.
- * @cfg {boolean} [highlightFirst=true] Whether the first lookup result should be highlighted (so, that the user can
- * take it over into the input with simply pressing return) automatically or not.
- */
-OO.ui.mixin.LookupElement = function OoUiMixinLookupElement( config ) {
- // Configuration initialization
- config = $.extend( { highlightFirst: true }, config );
-
- // Mixin constructors
- OO.ui.mixin.RequestManager.call( this, config );
-
- // Properties
- this.$overlay = config.$overlay || this.$element;
- this.lookupMenu = new OO.ui.FloatingMenuSelectWidget( {
- widget: this,
- input: this,
- $container: config.$container || this.$element
- } );
-
- this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
-
- this.lookupsDisabled = false;
- this.lookupInputFocused = false;
- this.lookupHighlightFirstItem = config.highlightFirst;
-
- // Events
- this.$input.on( {
- focus: this.onLookupInputFocus.bind( this ),
- blur: this.onLookupInputBlur.bind( this ),
- mousedown: this.onLookupInputMouseDown.bind( this )
- } );
- this.connect( this, { change: 'onLookupInputChange' } );
- this.lookupMenu.connect( this, {
- toggle: 'onLookupMenuToggle',
- choose: 'onLookupMenuItemChoose'
- } );
-
- // Initialization
- this.$element.addClass( 'oo-ui-lookupElement' );
- this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
- this.$overlay.append( this.lookupMenu.$element );
-};
-
-/* Setup */
-
-OO.mixinClass( OO.ui.mixin.LookupElement, OO.ui.mixin.RequestManager );
-
-/* Methods */
-
-/**
- * Handle input focus event.
- *
- * @protected
- * @param {jQuery.Event} e Input focus event
- */
-OO.ui.mixin.LookupElement.prototype.onLookupInputFocus = function () {
- this.lookupInputFocused = true;
- this.populateLookupMenu();
-};
-
-/**
- * Handle input blur event.
- *
- * @protected
- * @param {jQuery.Event} e Input blur event
- */
-OO.ui.mixin.LookupElement.prototype.onLookupInputBlur = function () {
- this.closeLookupMenu();
- this.lookupInputFocused = false;
-};
-
-/**
- * Handle input mouse down event.
- *
- * @protected
- * @param {jQuery.Event} e Input mouse down event
- */
-OO.ui.mixin.LookupElement.prototype.onLookupInputMouseDown = function () {
- // Only open the menu if the input was already focused.
- // This way we allow the user to open the menu again after closing it with Esc
- // by clicking in the input. Opening (and populating) the menu when initially
- // clicking into the input is handled by the focus handler.
- if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
- this.populateLookupMenu();
- }
-};
-
-/**
- * Handle input change event.
- *
- * @protected
- * @param {string} value New input value
- */
-OO.ui.mixin.LookupElement.prototype.onLookupInputChange = function () {
- if ( this.lookupInputFocused ) {
- this.populateLookupMenu();
- }
-};
-
-/**
- * Handle the lookup menu being shown/hidden.
- *
- * @protected
- * @param {boolean} visible Whether the lookup menu is now visible.
- */
-OO.ui.mixin.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
- if ( !visible ) {
- // When the menu is hidden, abort any active request and clear the menu.
- // This has to be done here in addition to closeLookupMenu(), because
- // MenuSelectWidget will close itself when the user presses Esc.
- this.abortLookupRequest();
- this.lookupMenu.clearItems();
- }
-};
-
-/**
- * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
- *
- * @protected
- * @param {OO.ui.MenuOptionWidget} item Selected item
- */
-OO.ui.mixin.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
- this.setValue( item.getData() );
-};
-
-/**
- * Get lookup menu.
- *
- * @private
- * @return {OO.ui.FloatingMenuSelectWidget}
- */
-OO.ui.mixin.LookupElement.prototype.getLookupMenu = function () {
- return this.lookupMenu;
-};
-
-/**
- * Disable or re-enable lookups.
- *
- * When lookups are disabled, calls to #populateLookupMenu will be ignored.
- *
- * @param {boolean} disabled Disable lookups
- */
-OO.ui.mixin.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
- this.lookupsDisabled = !!disabled;
-};
-
-/**
- * Open the menu. If there are no entries in the menu, this does nothing.
- *
- * @private
- * @chainable
- */
-OO.ui.mixin.LookupElement.prototype.openLookupMenu = function () {
- if ( !this.lookupMenu.isEmpty() ) {
- this.lookupMenu.toggle( true );
- }
- return this;
-};
-
-/**
- * Close the menu, empty it, and abort any pending request.
- *
- * @private
- * @chainable
- */
-OO.ui.mixin.LookupElement.prototype.closeLookupMenu = function () {
- this.lookupMenu.toggle( false );
- this.abortLookupRequest();
- this.lookupMenu.clearItems();
- return this;
-};
-
-/**
- * Request menu items based on the input's current value, and when they arrive,
- * populate the menu with these items and show the menu.
- *
- * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
- *
- * @private
- * @chainable
- */
-OO.ui.mixin.LookupElement.prototype.populateLookupMenu = function () {
- var widget = this,
- value = this.getValue();
-
- if ( this.lookupsDisabled || this.isReadOnly() ) {
- return;
- }
-
- // If the input is empty, clear the menu, unless suggestions when empty are allowed.
- if ( !this.allowSuggestionsWhenEmpty && value === '' ) {
- this.closeLookupMenu();
- // Skip population if there is already a request pending for the current value
- } else if ( value !== this.lookupQuery ) {
- this.getLookupMenuItems()
- .done( function ( items ) {
- widget.lookupMenu.clearItems();
- if ( items.length ) {
- widget.lookupMenu
- .addItems( items )
- .toggle( true );
- widget.initializeLookupMenuSelection();
- } else {
- widget.lookupMenu.toggle( false );
- }
- } )
- .fail( function () {
- widget.lookupMenu.clearItems();
- } );
- }
-
- return this;
-};
-
-/**
- * Highlight the first selectable item in the menu, if configured.
- *
- * @private
- * @chainable
- */
-OO.ui.mixin.LookupElement.prototype.initializeLookupMenuSelection = function () {
- if ( this.lookupHighlightFirstItem && !this.lookupMenu.getSelectedItem() ) {
- this.lookupMenu.highlightItem( this.lookupMenu.getFirstSelectableItem() );
- }
-};
-
-/**
- * Get lookup menu items for the current query.
- *
- * @private
- * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
- * the done event. If the request was aborted to make way for a subsequent request, this promise
- * will not be rejected: it will remain pending forever.
- */
-OO.ui.mixin.LookupElement.prototype.getLookupMenuItems = function () {
- return this.getRequestData().then( function ( data ) {
- return this.getLookupMenuOptionsFromData( data );
- }.bind( this ) );
-};
-
-/**
- * Abort the currently pending lookup request, if any.
- *
- * @private
- */
-OO.ui.mixin.LookupElement.prototype.abortLookupRequest = function () {
- this.abortRequest();
-};
-
-/**
- * Get a new request object of the current lookup query value.
- *
- * @protected
- * @method
- * @abstract
- * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
- */
-OO.ui.mixin.LookupElement.prototype.getLookupRequest = null;
-
-/**
- * Pre-process data returned by the request from #getLookupRequest.
- *
- * The return value of this function will be cached, and any further queries for the given value
- * will use the cache rather than doing API requests.
- *
- * @protected
- * @method
- * @abstract
- * @param {Mixed} response Response from server
- * @return {Mixed} Cached result data
- */
-OO.ui.mixin.LookupElement.prototype.getLookupCacheDataFromResponse = null;
-
-/**
- * Get a list of menu option widgets from the (possibly cached) data returned by
- * #getLookupCacheDataFromResponse.
- *
- * @protected
- * @method
- * @abstract
- * @param {Mixed} data Cached result data, usually an array
- * @return {OO.ui.MenuOptionWidget[]} Menu items
- */
-OO.ui.mixin.LookupElement.prototype.getLookupMenuOptionsFromData = null;
-
-/**
- * Set the read-only state of the widget.
- *
- * This will also disable/enable the lookups functionality.
- *
- * @param {boolean} readOnly Make input read-only
- * @chainable
- */
-OO.ui.mixin.LookupElement.prototype.setReadOnly = function ( readOnly ) {
- // Parent method
- // Note: Calling #setReadOnly this way assumes this is mixed into an OO.ui.TextInputWidget
- OO.ui.TextInputWidget.prototype.setReadOnly.call( this, readOnly );
-
- // During construction, #setReadOnly is called before the OO.ui.mixin.LookupElement constructor
- if ( this.isReadOnly() && this.lookupMenu ) {
- this.closeLookupMenu();
- }
-
- return this;
-};
-
-/**
- * @inheritdoc OO.ui.mixin.RequestManager
- */
-OO.ui.mixin.LookupElement.prototype.getRequestQuery = function () {
- return this.getValue();
-};
-
-/**
- * @inheritdoc OO.ui.mixin.RequestManager
- */
-OO.ui.mixin.LookupElement.prototype.getRequest = function () {
- return this.getLookupRequest();
-};
-
-/**
- * @inheritdoc OO.ui.mixin.RequestManager
- */
-OO.ui.mixin.LookupElement.prototype.getRequestCacheDataFromResponse = function ( response ) {
- return this.getLookupCacheDataFromResponse( response );
-};
-
-/**
- * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
- * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
- * rather extended to include the required content and functionality.
- *
- * Each card must have a unique symbolic name, which is passed to the constructor. In addition, the card's tab
- * item is customized (with a label) using the #setupTabItem method. See
- * {@link OO.ui.IndexLayout IndexLayout} for an example.
- *
- * @class
- * @extends OO.ui.PanelLayout
- *
- * @constructor
- * @param {string} name Unique symbolic name of card
- * @param {Object} [config] Configuration options
- * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] Label for card's tab
- */
-OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( name ) && config === undefined ) {
- config = name;
- name = config.name;
- }
-
- // Configuration initialization
- config = $.extend( { scrollable: true }, config );
-
- // Parent constructor
- OO.ui.CardLayout.parent.call( this, config );
-
- // Properties
- this.name = name;
- this.label = config.label;
- this.tabItem = null;
- this.active = false;
-
- // Initialization
- this.$element.addClass( 'oo-ui-cardLayout' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.CardLayout, OO.ui.PanelLayout );
-
-/* Events */
-
-/**
- * An 'active' event is emitted when the card becomes active. Cards become active when they are
- * shown in a index layout that is configured to display only one card at a time.
- *
- * @event active
- * @param {boolean} active Card is active
- */
-
-/* Methods */
-
-/**
- * Get the symbolic name of the card.
- *
- * @return {string} Symbolic name of card
- */
-OO.ui.CardLayout.prototype.getName = function () {
- return this.name;
-};
-
-/**
- * Check if card is active.
- *
- * Cards become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to display
- * only one card at a time. Additional CSS is applied to the card's tab item to reflect the active state.
- *
- * @return {boolean} Card is active
- */
-OO.ui.CardLayout.prototype.isActive = function () {
- return this.active;
-};
-
-/**
- * Get tab item.
- *
- * The tab item allows users to access the card from the index's tab
- * navigation. The tab item itself can be customized (with a label, level, etc.) using the #setupTabItem method.
- *
- * @return {OO.ui.TabOptionWidget|null} Tab option widget
- */
-OO.ui.CardLayout.prototype.getTabItem = function () {
- return this.tabItem;
-};
-
-/**
- * Set or unset the tab item.
- *
- * Specify a {@link OO.ui.TabOptionWidget tab option} to set it,
- * or `null` to clear the tab item. To customize the tab item itself (e.g., to set a label or tab
- * level), use #setupTabItem instead of this method.
- *
- * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
- * @chainable
- */
-OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) {
- this.tabItem = tabItem || null;
- if ( tabItem ) {
- this.setupTabItem();
- }
- return this;
-};
-
-/**
- * Set up the tab item.
- *
- * Use this method to customize the tab item (e.g., to add a label or tab level). To set or unset
- * the tab item itself (with a {@link OO.ui.TabOptionWidget tab option} or `null`), use
- * the #setTabItem method instead.
- *
- * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
- * @chainable
- */
-OO.ui.CardLayout.prototype.setupTabItem = function () {
- if ( this.label ) {
- this.tabItem.setLabel( this.label );
- }
- return this;
-};
-
-/**
- * Set the card to its 'active' state.
- *
- * Cards become active when they are shown in a index layout that is configured to display only one card at a time. Additional
- * CSS is applied to the tab item to reflect the card's active state. Outside of the index
- * context, setting the active state on a card does nothing.
- *
- * @param {boolean} value Card is active
- * @fires active
- */
-OO.ui.CardLayout.prototype.setActive = function ( active ) {
- active = !!active;
-
- if ( active !== this.active ) {
- this.active = active;
- this.$element.toggleClass( 'oo-ui-cardLayout-active', this.active );
- this.emit( 'active', this.active );
- }
-};
-
-/**
- * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
- * from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
- * rather extended to include the required content and functionality.
- *
- * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the page's outline
- * item is customized (with a label, outline level, etc.) using the #setupOutlineItem method. See
- * {@link OO.ui.BookletLayout BookletLayout} for an example.
- *
- * @class
- * @extends OO.ui.PanelLayout
- *
- * @constructor
- * @param {string} name Unique symbolic name of page
- * @param {Object} [config] Configuration options
- */
-OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( name ) && config === undefined ) {
- config = name;
- name = config.name;
- }
-
- // Configuration initialization
- config = $.extend( { scrollable: true }, config );
-
- // Parent constructor
- OO.ui.PageLayout.parent.call( this, config );
-
- // Properties
- this.name = name;
- this.outlineItem = null;
- this.active = false;
-
- // Initialization
- this.$element.addClass( 'oo-ui-pageLayout' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
-
-/* Events */
-
-/**
- * An 'active' event is emitted when the page becomes active. Pages become active when they are
- * shown in a booklet layout that is configured to display only one page at a time.
- *
- * @event active
- * @param {boolean} active Page is active
- */
-
-/* Methods */
-
-/**
- * Get the symbolic name of the page.
- *
- * @return {string} Symbolic name of page
- */
-OO.ui.PageLayout.prototype.getName = function () {
- return this.name;
-};
-
-/**
- * Check if page is active.
- *
- * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is configured to display
- * only one page at a time. Additional CSS is applied to the page's outline item to reflect the active state.
- *
- * @return {boolean} Page is active
- */
-OO.ui.PageLayout.prototype.isActive = function () {
- return this.active;
-};
-
-/**
- * Get outline item.
- *
- * The outline item allows users to access the page from the booklet's outline
- * navigation. The outline item itself can be customized (with a label, level, etc.) using the #setupOutlineItem method.
- *
- * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
- */
-OO.ui.PageLayout.prototype.getOutlineItem = function () {
- return this.outlineItem;
-};
-
-/**
- * Set or unset the outline item.
- *
- * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
- * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label or outline
- * level), use #setupOutlineItem instead of this method.
- *
- * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
- * @chainable
- */
-OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
- this.outlineItem = outlineItem || null;
- if ( outlineItem ) {
- this.setupOutlineItem();
- }
- return this;
-};
-
-/**
- * Set up the outline item.
- *
- * Use this method to customize the outline item (e.g., to add a label or outline level). To set or unset
- * the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or `null`), use
- * the #setOutlineItem method instead.
- *
- * @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
- * @chainable
- */
-OO.ui.PageLayout.prototype.setupOutlineItem = function () {
- return this;
-};
-
-/**
- * Set the page to its 'active' state.
- *
- * Pages become active when they are shown in a booklet layout that is configured to display only one page at a time. Additional
- * CSS is applied to the outline item to reflect the page's active state. Outside of the booklet
- * context, setting the active state on a page does nothing.
- *
- * @param {boolean} value Page is active
- * @fires active
- */
-OO.ui.PageLayout.prototype.setActive = function ( active ) {
- active = !!active;
-
- if ( active !== this.active ) {
- this.active = active;
- this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
- this.emit( 'active', this.active );
- }
-};
-
-/**
- * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one panel is displayed
- * at a time, though the stack layout can also be configured to show all contained panels, one after another,
- * by setting the #continuous option to 'true'.
- *
- * @example
- * // A stack layout with two panels, configured to be displayed continously
- * var myStack = new OO.ui.StackLayout( {
- * items: [
- * new OO.ui.PanelLayout( {
- * $content: $( '<p>Panel One</p>' ),
- * padded: true,
- * framed: true
- * } ),
- * new OO.ui.PanelLayout( {
- * $content: $( '<p>Panel Two</p>' ),
- * padded: true,
- * framed: true
- * } )
- * ],
- * continuous: true
- * } );
- * $( 'body' ).append( myStack.$element );
- *
- * @class
- * @extends OO.ui.PanelLayout
- * @mixins OO.ui.mixin.GroupElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel is displayed at a time.
- * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
- */
-OO.ui.StackLayout = function OoUiStackLayout( config ) {
- // Configuration initialization
- config = $.extend( { scrollable: true }, config );
-
- // Parent constructor
- OO.ui.StackLayout.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
-
- // Properties
- this.currentItem = null;
- this.continuous = !!config.continuous;
-
- // Initialization
- this.$element.addClass( 'oo-ui-stackLayout' );
- if ( this.continuous ) {
- this.$element.addClass( 'oo-ui-stackLayout-continuous' );
- this.$element.on( 'scroll', OO.ui.debounce( this.onScroll.bind( this ), 250 ) );
- }
- if ( Array.isArray( config.items ) ) {
- this.addItems( config.items );
- }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
-OO.mixinClass( OO.ui.StackLayout, OO.ui.mixin.GroupElement );
-
-/* Events */
-
-/**
- * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
- * {@link #clearItems cleared} or {@link #setItem displayed}.
- *
- * @event set
- * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
- */
-
-/**
- * When used in continuous mode, this event is emitted when the user scrolls down
- * far enough such that currentItem is no longer visible.
- *
- * @event visibleItemChange
- * @param {OO.ui.PanelLayout} panel The next visible item in the layout
- */
-
-/* Methods */
-
-/**
- * Handle scroll events from the layout element
- *
- * @param {jQuery.Event} e
- * @fires visibleItemChange
- */
-OO.ui.StackLayout.prototype.onScroll = function () {
- var currentRect,
- len = this.items.length,
- currentIndex = this.items.indexOf( this.currentItem ),
- newIndex = currentIndex,
- containerRect = this.$element[ 0 ].getBoundingClientRect();
-
- if ( !containerRect || ( !containerRect.top && !containerRect.bottom ) ) {
- // Can't get bounding rect, possibly not attached.
- return;
- }
-
- function getRect( item ) {
- return item.$element[ 0 ].getBoundingClientRect();
- }
-
- function isVisible( item ) {
- var rect = getRect( item );
- return rect.bottom > containerRect.top && rect.top < containerRect.bottom;
- }
-
- currentRect = getRect( this.currentItem );
-
- if ( currentRect.bottom < containerRect.top ) {
- // Scrolled down past current item
- while ( ++newIndex < len ) {
- if ( isVisible( this.items[ newIndex ] ) ) {
- break;
- }
- }
- } else if ( currentRect.top > containerRect.bottom ) {
- // Scrolled up past current item
- while ( --newIndex >= 0 ) {
- if ( isVisible( this.items[ newIndex ] ) ) {
- break;
- }
- }
- }
-
- if ( newIndex !== currentIndex ) {
- this.emit( 'visibleItemChange', this.items[ newIndex ] );
- }
-};
-
-/**
- * Get the current panel.
- *
- * @return {OO.ui.Layout|null}
- */
-OO.ui.StackLayout.prototype.getCurrentItem = function () {
- return this.currentItem;
-};
-
-/**
- * Unset the current item.
- *
- * @private
- * @param {OO.ui.StackLayout} layout
- * @fires set
- */
-OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
- var prevItem = this.currentItem;
- if ( prevItem === null ) {
- return;
- }
-
- this.currentItem = null;
- this.emit( 'set', null );
-};
-
-/**
- * Add panel layouts to the stack layout.
- *
- * Panels will be added to the end of the stack layout array unless the optional index parameter specifies a different
- * insertion point. Adding a panel that is already in the stack will move it to the end of the array or the point specified
- * by the index.
- *
- * @param {OO.ui.Layout[]} items Panels to add
- * @param {number} [index] Index of the insertion point
- * @chainable
- */
-OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
- // Update the visibility
- this.updateHiddenState( items, this.currentItem );
-
- // Mixin method
- OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
-
- if ( !this.currentItem && items.length ) {
- this.setItem( items[ 0 ] );
- }
-
- return this;
-};
-
-/**
- * Remove the specified panels from the stack layout.
- *
- * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all panels,
- * you may wish to use the #clearItems method instead.
- *
- * @param {OO.ui.Layout[]} items Panels to remove
- * @chainable
- * @fires set
- */
-OO.ui.StackLayout.prototype.removeItems = function ( items ) {
- // Mixin method
- OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
-
- if ( items.indexOf( this.currentItem ) !== -1 ) {
- if ( this.items.length ) {
- this.setItem( this.items[ 0 ] );
- } else {
- this.unsetCurrentItem();
- }
- }
-
- return this;
-};
-
-/**
- * Clear all panels from the stack layout.
- *
- * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
- * a subset of panels, use the #removeItems method.
- *
- * @chainable
- * @fires set
- */
-OO.ui.StackLayout.prototype.clearItems = function () {
- this.unsetCurrentItem();
- OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
-
- return this;
-};
-
-/**
- * Show the specified panel.
- *
- * If another panel is currently displayed, it will be hidden.
- *
- * @param {OO.ui.Layout} item Panel to show
- * @chainable
- * @fires set
- */
-OO.ui.StackLayout.prototype.setItem = function ( item ) {
- if ( item !== this.currentItem ) {
- this.updateHiddenState( this.items, item );
-
- if ( this.items.indexOf( item ) !== -1 ) {
- this.currentItem = item;
- this.emit( 'set', item );
- } else {
- this.unsetCurrentItem();
- }
- }
-
- return this;
-};
-
-/**
- * Update the visibility of all items in case of non-continuous view.
- *
- * Ensure all items are hidden except for the selected one.
- * This method does nothing when the stack is continuous.
- *
- * @private
- * @param {OO.ui.Layout[]} items Item list iterate over
- * @param {OO.ui.Layout} [selectedItem] Selected item to show
- */
-OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
- var i, len;
-
- if ( !this.continuous ) {
- for ( i = 0, len = items.length; i < len; i++ ) {
- if ( !selectedItem || selectedItem !== items[ i ] ) {
- items[ i ].$element.addClass( 'oo-ui-element-hidden' );
- }
- }
- if ( selectedItem ) {
- selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
- }
- }
-};
-
-/**
- * MenuLayouts combine a menu and a content {@link OO.ui.PanelLayout panel}. The menu is positioned relative to the content (after, before, top, or bottom)
- * and its size is customized with the #menuSize config. The content area will fill all remaining space.
- *
- * @example
- * var menuLayout = new OO.ui.MenuLayout( {
- * position: 'top'
- * } ),
- * menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
- * contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
- * select = new OO.ui.SelectWidget( {
- * items: [
- * new OO.ui.OptionWidget( {
- * data: 'before',
- * label: 'Before',
- * } ),
- * new OO.ui.OptionWidget( {
- * data: 'after',
- * label: 'After',
- * } ),
- * new OO.ui.OptionWidget( {
- * data: 'top',
- * label: 'Top',
- * } ),
- * new OO.ui.OptionWidget( {
- * data: 'bottom',
- * label: 'Bottom',
- * } )
- * ]
- * } ).on( 'select', function ( item ) {
- * menuLayout.setMenuPosition( item.getData() );
- * } );
- *
- * menuLayout.$menu.append(
- * menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
- * );
- * menuLayout.$content.append(
- * contentPanel.$element.append( '<b>Content panel</b>', '<p>Note that the menu is positioned relative to the content panel: top, bottom, after, before.</p>')
- * );
- * $( 'body' ).append( menuLayout.$element );
- *
- * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
- * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
- * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
- * may be omitted.
- *
- * .oo-ui-menuLayout-menu {
- * height: 200px;
- * width: 200px;
- * }
- * .oo-ui-menuLayout-content {
- * top: 200px;
- * left: 200px;
- * right: 200px;
- * bottom: 200px;
- * }
- *
- * @class
- * @extends OO.ui.Layout
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [showMenu=true] Show menu
- * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
- */
-OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
- // Configuration initialization
- config = $.extend( {
- showMenu: true,
- menuPosition: 'before'
- }, config );
-
- // Parent constructor
- OO.ui.MenuLayout.parent.call( this, config );
-
- /**
- * Menu DOM node
- *
- * @property {jQuery}
- */
- this.$menu = $( '<div>' );
- /**
- * Content DOM node
- *
- * @property {jQuery}
- */
- this.$content = $( '<div>' );
-
- // Initialization
- this.$menu
- .addClass( 'oo-ui-menuLayout-menu' );
- this.$content.addClass( 'oo-ui-menuLayout-content' );
- this.$element
- .addClass( 'oo-ui-menuLayout' )
- .append( this.$content, this.$menu );
- this.setMenuPosition( config.menuPosition );
- this.toggleMenu( config.showMenu );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
-
-/* Methods */
-
-/**
- * Toggle menu.
- *
- * @param {boolean} showMenu Show menu, omit to toggle
- * @chainable
- */
-OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
- showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
-
- if ( this.showMenu !== showMenu ) {
- this.showMenu = showMenu;
- this.$element
- .toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu )
- .toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu );
- }
-
- return this;
-};
-
-/**
- * Check if menu is visible
- *
- * @return {boolean} Menu is visible
- */
-OO.ui.MenuLayout.prototype.isMenuVisible = function () {
- return this.showMenu;
-};
-
-/**
- * Set menu position.
- *
- * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
- * @throws {Error} If position value is not supported
- * @chainable
- */
-OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
- this.$element.removeClass( 'oo-ui-menuLayout-' + this.menuPosition );
- this.menuPosition = position;
- this.$element.addClass( 'oo-ui-menuLayout-' + position );
-
- return this;
-};
-
-/**
- * Get menu position.
- *
- * @return {string} Menu position
- */
-OO.ui.MenuLayout.prototype.getMenuPosition = function () {
- return this.menuPosition;
-};
-
-/**
- * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
- * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
- * through the pages and select which one to display. By default, only one page is
- * displayed at a time and the outline is hidden. When a user navigates to a new page,
- * the booklet layout automatically focuses on the first focusable element, unless the
- * default setting is changed. Optionally, booklets can be configured to show
- * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
- *
- * @example
- * // Example of a BookletLayout that contains two PageLayouts.
- *
- * function PageOneLayout( name, config ) {
- * PageOneLayout.parent.call( this, name, config );
- * this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on the left)</p>' );
- * }
- * OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
- * PageOneLayout.prototype.setupOutlineItem = function () {
- * this.outlineItem.setLabel( 'Page One' );
- * };
- *
- * function PageTwoLayout( name, config ) {
- * PageTwoLayout.parent.call( this, name, config );
- * this.$element.append( '<p>Second page</p>' );
- * }
- * OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
- * PageTwoLayout.prototype.setupOutlineItem = function () {
- * this.outlineItem.setLabel( 'Page Two' );
- * };
- *
- * var page1 = new PageOneLayout( 'one' ),
- * page2 = new PageTwoLayout( 'two' );
- *
- * var booklet = new OO.ui.BookletLayout( {
- * outlined: true
- * } );
- *
- * booklet.addPages ( [ page1, page2 ] );
- * $( 'body' ).append( booklet.$element );
- *
- * @class
- * @extends OO.ui.MenuLayout
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [continuous=false] Show all pages, one after another
- * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is displayed.
- * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the pages of the booklet.
- * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
- */
-OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.BookletLayout.parent.call( this, config );
-
- // Properties
- this.currentPageName = null;
- this.pages = {};
- this.ignoreFocus = false;
- this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
- this.$content.append( this.stackLayout.$element );
- this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
- this.outlineVisible = false;
- this.outlined = !!config.outlined;
- if ( this.outlined ) {
- this.editable = !!config.editable;
- this.outlineControlsWidget = null;
- this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
- this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
- this.$menu.append( this.outlinePanel.$element );
- this.outlineVisible = true;
- if ( this.editable ) {
- this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
- this.outlineSelectWidget
- );
- }
- }
- this.toggleMenu( this.outlined );
-
- // Events
- this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
- if ( this.outlined ) {
- this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
- this.scrolling = false;
- this.stackLayout.connect( this, { visibleItemChange: 'onStackLayoutVisibleItemChange' } );
- }
- if ( this.autoFocus ) {
- // Event 'focus' does not bubble, but 'focusin' does
- this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
- }
-
- // Initialization
- this.$element.addClass( 'oo-ui-bookletLayout' );
- this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
- if ( this.outlined ) {
- this.outlinePanel.$element
- .addClass( 'oo-ui-bookletLayout-outlinePanel' )
- .append( this.outlineSelectWidget.$element );
- if ( this.editable ) {
- this.outlinePanel.$element
- .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
- .append( this.outlineControlsWidget.$element );
- }
- }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
-
-/* Events */
-
-/**
- * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the booklet layout.
- * @event set
- * @param {OO.ui.PageLayout} page Current page
- */
-
-/**
- * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
- *
- * @event add
- * @param {OO.ui.PageLayout[]} page Added pages
- * @param {number} index Index pages were added at
- */
-
-/**
- * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
- * {@link #removePages removed} from the booklet.
- *
- * @event remove
- * @param {OO.ui.PageLayout[]} pages Removed pages
- */
-
-/* Methods */
-
-/**
- * Handle stack layout focus.
- *
- * @private
- * @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 visibleItemChange events from the stackLayout
- *
- * The next visible page is set as the current page by selecting it
- * in the outline
- *
- * @param {OO.ui.PageLayout} page The next visible page in the layout
- */
-OO.ui.BookletLayout.prototype.onStackLayoutVisibleItemChange = function ( page ) {
- // Set a flag to so that the resulting call to #onStackLayoutSet doesn't
- // try and scroll the item into view again.
- this.scrolling = true;
- this.outlineSelectWidget.selectItemByData( page.getName() );
- this.scrolling = false;
-};
-
-/**
- * Handle stack layout set events.
- *
- * @private
- * @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 ( !this.scrolling && page ) {
- page.scrollElementIntoView( { complete: function () {
- if ( layout.autoFocus ) {
- layout.focus();
- }
- } } );
- }
-};
-
-/**
- * Focus the first input in the current page.
- *
- * If no page is selected, the first selectable page will be selected.
- * If the focus is already in an element on the current page, nothing will happen.
- * @param {number} [itemIndex] A specific item to focus on
- */
-OO.ui.BookletLayout.prototype.focus = function ( itemIndex ) {
- var page,
- items = this.stackLayout.getItems();
-
- if ( itemIndex !== undefined && items[ itemIndex ] ) {
- page = items[ itemIndex ];
- } else {
- page = this.stackLayout.getCurrentItem();
- }
-
- if ( !page && this.outlined ) {
- this.selectFirstSelectablePage();
- page = this.stackLayout.getCurrentItem();
- }
- if ( !page ) {
- return;
- }
- // Only change the focus if is not already in the current page
- if ( !OO.ui.contains( page.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
- page.focus();
- }
-};
-
-/**
- * Find the first focusable input in the booklet layout and focus
- * on it.
- */
-OO.ui.BookletLayout.prototype.focusFirstFocusable = function () {
- OO.ui.findFocusable( this.stackLayout.$element ).focus();
-};
-
-/**
- * Handle outline widget select events.
- *
- * @private
- * @param {OO.ui.OptionWidget|null} item Selected item
- */
-OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
- if ( item ) {
- this.setPage( item.getData() );
- }
-};
-
-/**
- * Check if booklet has an outline.
- *
- * @return {boolean} Booklet has an outline
- */
-OO.ui.BookletLayout.prototype.isOutlined = function () {
- return this.outlined;
-};
-
-/**
- * Check if booklet has editing controls.
- *
- * @return {boolean} Booklet is editable
- */
-OO.ui.BookletLayout.prototype.isEditable = function () {
- return this.editable;
-};
-
-/**
- * Check if booklet has a visible outline.
- *
- * @return {boolean} Outline is visible
- */
-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.toggleMenu( show );
- }
-
- return this;
-};
-
-/**
- * Get the page closest to the specified page.
- *
- * @param {OO.ui.PageLayout} page Page to use as a reference point
- * @return {OO.ui.PageLayout|null} Page closest to the specified page
- */
-OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
- var next, prev, level,
- pages = this.stackLayout.getItems(),
- index = pages.indexOf( page );
-
- if ( index !== -1 ) {
- next = pages[ index + 1 ];
- prev = pages[ index - 1 ];
- // Prefer adjacent pages at the same level
- if ( this.outlined ) {
- level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
- if (
- prev &&
- level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
- ) {
- return prev;
- }
- if (
- next &&
- level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
- ) {
- return next;
- }
- }
- }
- return prev || next || null;
-};
-
-/**
- * Get the outline widget.
- *
- * If the booklet is not outlined, the method will return `null`.
- *
- * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
- */
-OO.ui.BookletLayout.prototype.getOutline = function () {
- return this.outlineSelectWidget;
-};
-
-/**
- * Get the outline controls widget.
- *
- * If the outline is not editable, the method will return `null`.
- *
- * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
- */
-OO.ui.BookletLayout.prototype.getOutlineControls = function () {
- return this.outlineControlsWidget;
-};
-
-/**
- * Get a page by its symbolic 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.
- *
- * @return {OO.ui.PageLayout|undefined} Current page, if found
- */
-OO.ui.BookletLayout.prototype.getCurrentPage = function () {
- var name = this.getCurrentPageName();
- return name ? this.getPage( name ) : undefined;
-};
-
-/**
- * Get the symbolic name of the current page.
- *
- * @return {string|null} Symbolic name of the current page
- */
-OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
- return this.currentPageName;
-};
-
-/**
- * Add pages to the booklet 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 of the insertion point
- * @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 = stackLayoutPages.indexOf( this.pages[ name ] );
- 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.OutlineOptionWidget( { data: name } );
- page.setOutlineItem( item );
- items.push( item );
- }
- }
-
- if ( this.outlined && items.length ) {
- this.outlineSelectWidget.addItems( items, index );
- this.selectFirstSelectablePage();
- }
- this.stackLayout.addItems( pages, index );
- this.emit( 'add', pages, index );
-
- return this;
-};
-
-/**
- * Remove the specified pages from the booklet layout.
- *
- * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
- *
- * @param {OO.ui.PageLayout[]} pages An array of pages to remove
- * @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.outlineSelectWidget.getItemFromData( name ) );
- page.setOutlineItem( null );
- }
- }
- if ( this.outlined && items.length ) {
- this.outlineSelectWidget.removeItems( items );
- this.selectFirstSelectablePage();
- }
- this.stackLayout.removeItems( pages );
- this.emit( 'remove', pages );
-
- return this;
-};
-
-/**
- * Clear all pages from the booklet layout.
- *
- * To remove only a subset of pages from the booklet, use the #removePages method.
- *
- * @fires remove
- * @chainable
- */
-OO.ui.BookletLayout.prototype.clearPages = function () {
- var i, len,
- pages = this.stackLayout.getItems();
-
- this.pages = {};
- this.currentPageName = null;
- if ( this.outlined ) {
- this.outlineSelectWidget.clearItems();
- for ( i = 0, len = pages.length; i < len; i++ ) {
- pages[ i ].setOutlineItem( null );
- }
- }
- this.stackLayout.clearItems();
-
- this.emit( 'remove', pages );
-
- return this;
-};
-
-/**
- * Set the current page by symbolic name.
- *
- * @fires set
- * @param {string} name Symbolic name of page
- */
-OO.ui.BookletLayout.prototype.setPage = function ( name ) {
- var selectedItem,
- $focused,
- page = this.pages[ name ],
- previousPage = this.currentPageName && this.pages[ this.currentPageName ];
-
- if ( name !== this.currentPageName ) {
- if ( this.outlined ) {
- selectedItem = this.outlineSelectWidget.getSelectedItem();
- if ( selectedItem && selectedItem.getData() !== name ) {
- this.outlineSelectWidget.selectItemByData( name );
- }
- }
- if ( page ) {
- if ( previousPage ) {
- previousPage.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 the layout is non-continuous, this check is
- // meaningless because the next page is not visible yet and thus can't hold focus.
- if (
- this.autoFocus &&
- this.stackLayout.continuous &&
- OO.ui.findFocusable( page.$element ).length !== 0
- ) {
- $focused = previousPage.$element.find( ':focus' );
- if ( $focused.length ) {
- $focused[ 0 ].blur();
- }
- }
- }
- this.currentPageName = name;
- page.setActive( true );
- this.stackLayout.setItem( page );
- if ( !this.stackLayout.continuous && previousPage ) {
- // This should not be necessary, since any inputs on the previous page should have been
- // blurred when it was hidden, but browsers are not very consistent about this.
- $focused = previousPage.$element.find( ':focus' );
- if ( $focused.length ) {
- $focused[ 0 ].blur();
- }
- }
- this.emit( 'set', page );
- }
- }
-};
-
-/**
- * Select the first selectable page.
- *
- * @chainable
- */
-OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
- if ( !this.outlineSelectWidget.getSelectedItem() ) {
- this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
- }
-
- return this;
-};
-
-/**
- * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
- * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the cards and
- * select which one to display. By default, only one card is displayed at a time. When a user
- * navigates to a new card, the index layout automatically focuses on the first focusable element,
- * unless the default setting is changed.
- *
- * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
- *
- * @example
- * // Example of a IndexLayout that contains two CardLayouts.
- *
- * function CardOneLayout( name, config ) {
- * CardOneLayout.parent.call( this, name, config );
- * this.$element.append( '<p>First card</p>' );
- * }
- * OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
- * CardOneLayout.prototype.setupTabItem = function () {
- * this.tabItem.setLabel( 'Card one' );
- * };
- *
- * var card1 = new CardOneLayout( 'one' ),
- * card2 = new CardLayout( 'two', { label: 'Card two' } );
- *
- * card2.$element.append( '<p>Second card</p>' );
- *
- * var index = new OO.ui.IndexLayout();
- *
- * index.addCards ( [ card1, card2 ] );
- * $( 'body' ).append( index.$element );
- *
- * @class
- * @extends OO.ui.MenuLayout
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [continuous=false] Show all cards, one after another
- * @cfg {boolean} [expanded=true] Expand the content panel to fill the entire parent element.
- * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed.
- */
-OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
- // Configuration initialization
- config = $.extend( {}, config, { menuPosition: 'top' } );
-
- // Parent constructor
- OO.ui.IndexLayout.parent.call( this, config );
-
- // Properties
- this.currentCardName = null;
- this.cards = {};
- this.ignoreFocus = false;
- this.stackLayout = new OO.ui.StackLayout( {
- continuous: !!config.continuous,
- expanded: config.expanded
- } );
- this.$content.append( this.stackLayout.$element );
- this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
-
- this.tabSelectWidget = new OO.ui.TabSelectWidget();
- this.tabPanel = new OO.ui.PanelLayout();
- this.$menu.append( this.tabPanel.$element );
-
- this.toggleMenu( true );
-
- // Events
- this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
- this.tabSelectWidget.connect( this, { select: 'onTabSelectWidgetSelect' } );
- if ( this.autoFocus ) {
- // Event 'focus' does not bubble, but 'focusin' does
- this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
- }
-
- // Initialization
- this.$element.addClass( 'oo-ui-indexLayout' );
- this.stackLayout.$element.addClass( 'oo-ui-indexLayout-stackLayout' );
- this.tabPanel.$element
- .addClass( 'oo-ui-indexLayout-tabPanel' )
- .append( this.tabSelectWidget.$element );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );
-
-/* Events */
-
-/**
- * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout.
- * @event set
- * @param {OO.ui.CardLayout} card Current card
- */
-
-/**
- * An 'add' event is emitted when cards are {@link #addCards added} to the index layout.
- *
- * @event add
- * @param {OO.ui.CardLayout[]} card Added cards
- * @param {number} index Index cards were added at
- */
-
-/**
- * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
- * {@link #removeCards removed} from the index.
- *
- * @event remove
- * @param {OO.ui.CardLayout[]} cards Removed cards
- */
-
-/* Methods */
-
-/**
- * Handle stack layout focus.
- *
- * @private
- * @param {jQuery.Event} e Focusin event
- */
-OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
- var name, $target;
-
- // Find the card that an element was focused within
- $target = $( e.target ).closest( '.oo-ui-cardLayout' );
- for ( name in this.cards ) {
- // Check for card match, exclude current card to find only card changes
- if ( this.cards[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentCardName ) {
- this.setCard( name );
- break;
- }
- }
-};
-
-/**
- * Handle stack layout set events.
- *
- * @private
- * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel
- */
-OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
- var layout = this;
- if ( card ) {
- card.scrollElementIntoView( { complete: function () {
- if ( layout.autoFocus ) {
- layout.focus();
- }
- } } );
- }
-};
-
-/**
- * Focus the first input in the current card.
- *
- * If no card is selected, the first selectable card will be selected.
- * If the focus is already in an element on the current card, nothing will happen.
- * @param {number} [itemIndex] A specific item to focus on
- */
-OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
- var card,
- items = this.stackLayout.getItems();
-
- if ( itemIndex !== undefined && items[ itemIndex ] ) {
- card = items[ itemIndex ];
- } else {
- card = this.stackLayout.getCurrentItem();
- }
-
- if ( !card ) {
- this.selectFirstSelectableCard();
- card = this.stackLayout.getCurrentItem();
- }
- if ( !card ) {
- return;
- }
- // Only change the focus if is not already in the current page
- if ( !OO.ui.contains( card.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
- card.focus();
- }
-};
-
-/**
- * Find the first focusable input in the index layout and focus
- * on it.
- */
-OO.ui.IndexLayout.prototype.focusFirstFocusable = function () {
- OO.ui.findFocusable( this.stackLayout.$element ).focus();
-};
-
-/**
- * Handle tab widget select events.
- *
- * @private
- * @param {OO.ui.OptionWidget|null} item Selected item
- */
-OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
- if ( item ) {
- this.setCard( item.getData() );
- }
-};
-
-/**
- * Get the card closest to the specified card.
- *
- * @param {OO.ui.CardLayout} card Card to use as a reference point
- * @return {OO.ui.CardLayout|null} Card closest to the specified card
- */
-OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) {
- var next, prev, level,
- cards = this.stackLayout.getItems(),
- index = cards.indexOf( card );
-
- if ( index !== -1 ) {
- next = cards[ index + 1 ];
- prev = cards[ index - 1 ];
- // Prefer adjacent cards at the same level
- level = this.tabSelectWidget.getItemFromData( card.getName() ).getLevel();
- if (
- prev &&
- level === this.tabSelectWidget.getItemFromData( prev.getName() ).getLevel()
- ) {
- return prev;
- }
- if (
- next &&
- level === this.tabSelectWidget.getItemFromData( next.getName() ).getLevel()
- ) {
- return next;
- }
- }
- return prev || next || null;
-};
-
-/**
- * Get the tabs widget.
- *
- * @return {OO.ui.TabSelectWidget} Tabs widget
- */
-OO.ui.IndexLayout.prototype.getTabs = function () {
- return this.tabSelectWidget;
-};
-
-/**
- * Get a card by its symbolic name.
- *
- * @param {string} name Symbolic name of card
- * @return {OO.ui.CardLayout|undefined} Card, if found
- */
-OO.ui.IndexLayout.prototype.getCard = function ( name ) {
- return this.cards[ name ];
-};
-
-/**
- * Get the current card.
- *
- * @return {OO.ui.CardLayout|undefined} Current card, if found
- */
-OO.ui.IndexLayout.prototype.getCurrentCard = function () {
- var name = this.getCurrentCardName();
- return name ? this.getCard( name ) : undefined;
-};
-
-/**
- * Get the symbolic name of the current card.
- *
- * @return {string|null} Symbolic name of the current card
- */
-OO.ui.IndexLayout.prototype.getCurrentCardName = function () {
- return this.currentCardName;
-};
-
-/**
- * Add cards to the index layout
- *
- * When cards are added with the same names as existing cards, the existing cards will be
- * automatically removed before the new cards are added.
- *
- * @param {OO.ui.CardLayout[]} cards Cards to add
- * @param {number} index Index of the insertion point
- * @fires add
- * @chainable
- */
-OO.ui.IndexLayout.prototype.addCards = function ( cards, index ) {
- var i, len, name, card, item, currentIndex,
- stackLayoutCards = this.stackLayout.getItems(),
- remove = [],
- items = [];
-
- // Remove cards with same names
- for ( i = 0, len = cards.length; i < len; i++ ) {
- card = cards[ i ];
- name = card.getName();
-
- if ( Object.prototype.hasOwnProperty.call( this.cards, name ) ) {
- // Correct the insertion index
- currentIndex = stackLayoutCards.indexOf( this.cards[ name ] );
- if ( currentIndex !== -1 && currentIndex + 1 < index ) {
- index--;
- }
- remove.push( this.cards[ name ] );
- }
- }
- if ( remove.length ) {
- this.removeCards( remove );
- }
-
- // Add new cards
- for ( i = 0, len = cards.length; i < len; i++ ) {
- card = cards[ i ];
- name = card.getName();
- this.cards[ card.getName() ] = card;
- item = new OO.ui.TabOptionWidget( { data: name } );
- card.setTabItem( item );
- items.push( item );
- }
-
- if ( items.length ) {
- this.tabSelectWidget.addItems( items, index );
- this.selectFirstSelectableCard();
- }
- this.stackLayout.addItems( cards, index );
- this.emit( 'add', cards, index );
-
- return this;
-};
-
-/**
- * Remove the specified cards from the index layout.
- *
- * To remove all cards from the index, you may wish to use the #clearCards method instead.
- *
- * @param {OO.ui.CardLayout[]} cards An array of cards to remove
- * @fires remove
- * @chainable
- */
-OO.ui.IndexLayout.prototype.removeCards = function ( cards ) {
- var i, len, name, card,
- items = [];
-
- for ( i = 0, len = cards.length; i < len; i++ ) {
- card = cards[ i ];
- name = card.getName();
- delete this.cards[ name ];
- items.push( this.tabSelectWidget.getItemFromData( name ) );
- card.setTabItem( null );
- }
- if ( items.length ) {
- this.tabSelectWidget.removeItems( items );
- this.selectFirstSelectableCard();
- }
- this.stackLayout.removeItems( cards );
- this.emit( 'remove', cards );
-
- return this;
-};
-
-/**
- * Clear all cards from the index layout.
- *
- * To remove only a subset of cards from the index, use the #removeCards method.
- *
- * @fires remove
- * @chainable
- */
-OO.ui.IndexLayout.prototype.clearCards = function () {
- var i, len,
- cards = this.stackLayout.getItems();
-
- this.cards = {};
- this.currentCardName = null;
- this.tabSelectWidget.clearItems();
- for ( i = 0, len = cards.length; i < len; i++ ) {
- cards[ i ].setTabItem( null );
- }
- this.stackLayout.clearItems();
-
- this.emit( 'remove', cards );
-
- return this;
-};
-
-/**
- * Set the current card by symbolic name.
- *
- * @fires set
- * @param {string} name Symbolic name of card
- */
-OO.ui.IndexLayout.prototype.setCard = function ( name ) {
- var selectedItem,
- $focused,
- card = this.cards[ name ],
- previousCard = this.currentCardName && this.cards[ this.currentCardName ];
-
- if ( name !== this.currentCardName ) {
- selectedItem = this.tabSelectWidget.getSelectedItem();
- if ( selectedItem && selectedItem.getData() !== name ) {
- this.tabSelectWidget.selectItemByData( name );
- }
- if ( card ) {
- if ( previousCard ) {
- previousCard.setActive( false );
- // Blur anything focused if the next card doesn't have anything focusable.
- // This is not needed if the next card has something focusable (because once it is focused
- // this blur happens automatically). If the layout is non-continuous, this check is
- // meaningless because the next card is not visible yet and thus can't hold focus.
- if (
- this.autoFocus &&
- this.stackLayout.continuous &&
- OO.ui.findFocusable( card.$element ).length !== 0
- ) {
- $focused = previousCard.$element.find( ':focus' );
- if ( $focused.length ) {
- $focused[ 0 ].blur();
- }
- }
- }
- this.currentCardName = name;
- card.setActive( true );
- this.stackLayout.setItem( card );
- if ( !this.stackLayout.continuous && previousCard ) {
- // This should not be necessary, since any inputs on the previous card should have been
- // blurred when it was hidden, but browsers are not very consistent about this.
- $focused = previousCard.$element.find( ':focus' );
- if ( $focused.length ) {
- $focused[ 0 ].blur();
- }
- }
- this.emit( 'set', card );
- }
- }
-};
-
-/**
- * Select the first selectable card.
- *
- * @chainable
- */
-OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
- if ( !this.tabSelectWidget.getSelectedItem() ) {
- this.tabSelectWidget.selectItem( this.tabSelectWidget.getFirstSelectableItem() );
- }
-
- return this;
-};
-
-/**
- * ToggleWidget implements basic behavior of widgets with an on/off state.
- * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
- *
- * @abstract
- * @class
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [value=false] The toggle’s initial on/off state.
- * By default, the toggle is in the 'off' state.
- */
-OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.ToggleWidget.parent.call( this, config );
-
- // Properties
- this.value = null;
-
- // Initialization
- this.$element.addClass( 'oo-ui-toggleWidget' );
- this.setValue( !!config.value );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ToggleWidget, OO.ui.Widget );
-
-/* Events */
-
-/**
- * @event change
- *
- * A change event is emitted when the on/off state of the toggle changes.
- *
- * @param {boolean} value Value representing the new state of the toggle
- */
-
-/* Methods */
-
-/**
- * Get the value representing the toggle’s state.
- *
- * @return {boolean} The on/off state of the toggle
- */
-OO.ui.ToggleWidget.prototype.getValue = function () {
- return this.value;
-};
-
-/**
- * Set the state of the toggle: `true` for 'on', `false' for 'off'.
- *
- * @param {boolean} value The state of the toggle
- * @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 );
- this.$element.attr( 'aria-checked', value.toString() );
- }
- return this;
-};
-
-/**
- * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
- * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
- * configured with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators},
- * {@link OO.ui.mixin.TitledElement titles}, {@link OO.ui.mixin.FlaggedElement styling flags},
- * and {@link OO.ui.mixin.LabelElement labels}. Please see
- * the [OOjs UI documentation][1] on MediaWiki for more information.
- *
- * @example
- * // Toggle buttons in the 'off' and 'on' state.
- * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
- * label: 'Toggle Button off'
- * } );
- * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
- * label: 'Toggle Button on',
- * value: true
- * } );
- * // Append the buttons to the DOM.
- * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
- *
- * @class
- * @extends OO.ui.ToggleWidget
- * @mixins OO.ui.mixin.ButtonElement
- * @mixins OO.ui.mixin.IconElement
- * @mixins OO.ui.mixin.IndicatorElement
- * @mixins OO.ui.mixin.LabelElement
- * @mixins OO.ui.mixin.TitledElement
- * @mixins OO.ui.mixin.FlaggedElement
- * @mixins OO.ui.mixin.TabIndexedElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [value=false] The toggle button’s initial on/off
- * state. By default, the button is in the 'off' state.
- */
-OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.ToggleButtonWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.ButtonElement.call( this, config );
- OO.ui.mixin.IconElement.call( this, config );
- OO.ui.mixin.IndicatorElement.call( this, config );
- OO.ui.mixin.LabelElement.call( this, config );
- OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
- OO.ui.mixin.FlaggedElement.call( this, config );
- OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
-
- // Events
- this.connect( this, { click: 'onAction' } );
-
- // Initialization
- this.$button.append( this.$icon, this.$label, this.$indicator );
- this.$element
- .addClass( 'oo-ui-toggleButtonWidget' )
- .append( this.$button );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
-OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.ButtonElement );
-OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IconElement );
-OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IndicatorElement );
-OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TitledElement );
-OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.FlaggedElement );
-OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TabIndexedElement );
-
-/* Methods */
-
-/**
- * Handle the button action being triggered.
- *
- * @private
- */
-OO.ui.ToggleButtonWidget.prototype.onAction = function () {
- this.setValue( !this.value );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
- value = !!value;
- if ( value !== this.value ) {
- // Might be called from parent constructor before ButtonElement constructor
- if ( this.$button ) {
- this.$button.attr( 'aria-pressed', value.toString() );
- }
- this.setActive( value );
- }
-
- // Parent method
- OO.ui.ToggleButtonWidget.parent.prototype.setValue.call( this, value );
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ToggleButtonWidget.prototype.setButtonElement = function ( $button ) {
- if ( this.$button ) {
- this.$button.removeAttr( 'aria-pressed' );
- }
- OO.ui.mixin.ButtonElement.prototype.setButtonElement.call( this, $button );
- this.$button.attr( 'aria-pressed', this.value.toString() );
-};
-
-/**
- * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
- * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
- * visually by a slider in the leftmost position.
- *
- * @example
- * // Toggle switches in the 'off' and 'on' position.
- * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget();
- * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
- * value: true
- * } );
- *
- * // Create a FieldsetLayout to layout and label switches
- * var fieldset = new OO.ui.FieldsetLayout( {
- * label: 'Toggle switches'
- * } );
- * fieldset.addItems( [
- * new OO.ui.FieldLayout( toggleSwitch1, { label: 'Off', align: 'top' } ),
- * new OO.ui.FieldLayout( toggleSwitch2, { label: 'On', align: 'top' } )
- * ] );
- * $( 'body' ).append( fieldset.$element );
- *
- * @class
- * @extends OO.ui.ToggleWidget
- * @mixins OO.ui.mixin.TabIndexedElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
- * By default, the toggle switch is in the 'off' position.
- */
-OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
- // Parent constructor
- OO.ui.ToggleSwitchWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.TabIndexedElement.call( this, config );
-
- // Properties
- this.dragging = false;
- this.dragStart = null;
- this.sliding = false;
- this.$glow = $( '<span>' );
- this.$grip = $( '<span>' );
-
- // Events
- this.$element.on( {
- click: this.onClick.bind( this ),
- keypress: this.onKeyPress.bind( this )
- } );
-
- // Initialization
- this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
- this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
- this.$element
- .addClass( 'oo-ui-toggleSwitchWidget' )
- .attr( 'role', 'checkbox' )
- .append( this.$glow, this.$grip );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
-OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.mixin.TabIndexedElement );
-
-/* Methods */
-
-/**
- * Handle mouse click events.
- *
- * @private
- * @param {jQuery.Event} e Mouse click event
- */
-OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
- if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
- this.setValue( !this.value );
- }
- return false;
-};
-
-/**
- * Handle key press events.
- *
- * @private
- * @param {jQuery.Event} e Key press event
- */
-OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
- if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
- this.setValue( !this.value );
- return false;
- }
-};
-
-/**
- * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
- * Controls include moving items up and down, removing items, and adding different kinds of items.
- *
- * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.GroupElement
- * @mixins OO.ui.mixin.IconElement
- *
- * @constructor
- * @param {OO.ui.OutlineSelectWidget} outline Outline to control
- * @param {Object} [config] Configuration options
- * @cfg {Object} [abilities] List of abilties
- * @cfg {boolean} [abilities.move=true] Allow moving movable items
- * @cfg {boolean} [abilities.remove=true] Allow removing removable items
- */
-OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( outline ) && config === undefined ) {
- config = outline;
- outline = config.outline;
- }
-
- // Configuration initialization
- config = $.extend( { icon: 'add' }, config );
-
- // Parent constructor
- OO.ui.OutlineControlsWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.GroupElement.call( this, config );
- OO.ui.mixin.IconElement.call( this, config );
-
- // Properties
- this.outline = outline;
- this.$movers = $( '<div>' );
- this.upButton = new OO.ui.ButtonWidget( {
- framed: false,
- icon: 'collapse',
- title: OO.ui.msg( 'ooui-outline-control-move-up' )
- } );
- this.downButton = new OO.ui.ButtonWidget( {
- framed: false,
- icon: 'expand',
- title: OO.ui.msg( 'ooui-outline-control-move-down' )
- } );
- this.removeButton = new OO.ui.ButtonWidget( {
- framed: false,
- icon: 'remove',
- title: OO.ui.msg( 'ooui-outline-control-remove' )
- } );
- this.abilities = { move: true, remove: true };
-
- // 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 );
- this.setAbilities( config.abilities || {} );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.GroupElement );
-OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.IconElement );
-
-/* Events */
-
-/**
- * @event move
- * @param {number} places Number of places to move
- */
-
-/**
- * @event remove
- */
-
-/* Methods */
-
-/**
- * Set abilities.
- *
- * @param {Object} abilities List of abilties
- * @param {boolean} [abilities.move] Allow moving movable items
- * @param {boolean} [abilities.remove] Allow removing removable items
- */
-OO.ui.OutlineControlsWidget.prototype.setAbilities = function ( abilities ) {
- var ability;
-
- for ( ability in this.abilities ) {
- if ( abilities[ ability ] !== undefined ) {
- this.abilities[ ability ] = !!abilities[ ability ];
- }
- }
-
- this.onOutlineChange();
-};
-
-/**
- * @private
- * Handle outline change events.
- */
-OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
- var i, len, firstMovable, lastMovable,
- items = this.outline.getItems(),
- selectedItem = this.outline.getSelectedItem(),
- movable = this.abilities.move && selectedItem && selectedItem.isMovable(),
- removable = this.abilities.remove && 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 );
-};
-
-/**
- * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}.
- *
- * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain
- * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout}
- * for an example.
- *
- * @class
- * @extends OO.ui.DecoratedOptionWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {number} [level] Indentation level
- * @cfg {boolean} [movable] Allow modification from {@link OO.ui.OutlineControlsWidget outline controls}.
- */
-OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.OutlineOptionWidget.parent.call( this, config );
-
- // Properties
- this.level = 0;
- this.movable = !!config.movable;
- this.removable = !!config.removable;
-
- // Initialization
- this.$element.addClass( 'oo-ui-outlineOptionWidget' );
- this.setLevel( config.level );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
-
-/* Static Properties */
-
-OO.ui.OutlineOptionWidget.static.highlightable = false;
-
-OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
-
-OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
-
-OO.ui.OutlineOptionWidget.static.levels = 3;
-
-/* Methods */
-
-/**
- * Check if item is movable.
- *
- * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
- *
- * @return {boolean} Item is movable
- */
-OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
- return this.movable;
-};
-
-/**
- * Check if item is removable.
- *
- * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
- *
- * @return {boolean} Item is removable
- */
-OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
- return this.removable;
-};
-
-/**
- * Get indentation level.
- *
- * @return {number} Indentation level
- */
-OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
- return this.level;
-};
-
-/**
- * Set movability.
- *
- * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
- *
- * @param {boolean} movable Item is movable
- * @chainable
- */
-OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
- this.movable = !!movable;
- this.updateThemeClasses();
- return this;
-};
-
-/**
- * Set removability.
- *
- * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
- *
- * @param {boolean} removable Item is removable
- * @chainable
- */
-OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
- this.removable = !!removable;
- this.updateThemeClasses();
- return this;
-};
-
-/**
- * Set indentation level.
- *
- * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
- * @chainable
- */
-OO.ui.OutlineOptionWidget.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 );
- }
- }
- this.updateThemeClasses();
-
- return this;
-};
-
-/**
- * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
- * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
- *
- * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
- *
- * @class
- * @extends OO.ui.SelectWidget
- * @mixins OO.ui.mixin.TabIndexedElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
- // Parent constructor
- OO.ui.OutlineSelectWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.TabIndexedElement.call( this, config );
-
- // Events
- this.$element.on( {
- focus: this.bindKeyDownListener.bind( this ),
- blur: this.unbindKeyDownListener.bind( this )
- } );
-
- // Initialization
- this.$element.addClass( 'oo-ui-outlineSelectWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
-OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.mixin.TabIndexedElement );
-
-/**
- * ButtonOptionWidget is a special type of {@link OO.ui.mixin.ButtonElement button element} that
- * can be selected and configured with data. The class is
- * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
- * [OOjs UI documentation on MediaWiki] [1] for more information.
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
- *
- * @class
- * @extends OO.ui.DecoratedOptionWidget
- * @mixins OO.ui.mixin.ButtonElement
- * @mixins OO.ui.mixin.TabIndexedElement
- * @mixins OO.ui.mixin.TitledElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.ButtonOptionWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.ButtonElement.call( this, config );
- OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
- OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, {
- $tabIndexed: this.$button,
- tabIndex: -1
- } ) );
-
- // Initialization
- this.$element.addClass( 'oo-ui-buttonOptionWidget' );
- this.$button.append( this.$element.contents() );
- this.$element.append( this.$button );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
-OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.ButtonElement );
-OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TitledElement );
-OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TabIndexedElement );
-
-/* Static Properties */
-
-// 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.ButtonOptionWidget.static.highlightable = false;
-
-/* Methods */
-
-/**
- * @inheritdoc
- */
-OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
- OO.ui.ButtonOptionWidget.parent.prototype.setSelected.call( this, state );
-
- if ( this.constructor.static.selectable ) {
- this.setActive( state );
- }
-
- return this;
-};
-
-/**
- * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
- * button options and is used together with
- * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
- * highlighting, choosing, and selecting mutually exclusive options. Please see
- * the [OOjs UI documentation on MediaWiki] [1] for more information.
- *
- * @example
- * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
- * var option1 = new OO.ui.ButtonOptionWidget( {
- * data: 1,
- * label: 'Option 1',
- * title: 'Button option 1'
- * } );
- *
- * var option2 = new OO.ui.ButtonOptionWidget( {
- * data: 2,
- * label: 'Option 2',
- * title: 'Button option 2'
- * } );
- *
- * var option3 = new OO.ui.ButtonOptionWidget( {
- * data: 3,
- * label: 'Option 3',
- * title: 'Button option 3'
- * } );
- *
- * var buttonSelect=new OO.ui.ButtonSelectWidget( {
- * items: [ option1, option2, option3 ]
- * } );
- * $( 'body' ).append( buttonSelect.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
- *
- * @class
- * @extends OO.ui.SelectWidget
- * @mixins OO.ui.mixin.TabIndexedElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
- // Parent constructor
- OO.ui.ButtonSelectWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.TabIndexedElement.call( this, config );
-
- // Events
- this.$element.on( {
- focus: this.bindKeyDownListener.bind( this ),
- blur: this.unbindKeyDownListener.bind( this )
- } );
-
- // Initialization
- this.$element.addClass( 'oo-ui-buttonSelectWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
-OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.mixin.TabIndexedElement );
-
-/**
- * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
- *
- * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
- * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout}
- * for an example.
- *
- * @class
- * @extends OO.ui.OptionWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.TabOptionWidget = function OoUiTabOptionWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.TabOptionWidget.parent.call( this, config );
-
- // Initialization
- this.$element.addClass( 'oo-ui-tabOptionWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.TabOptionWidget, OO.ui.OptionWidget );
-
-/* Static Properties */
-
-OO.ui.TabOptionWidget.static.highlightable = false;
-
-/**
- * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options}
- *
- * **Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.**
- *
- * @class
- * @extends OO.ui.SelectWidget
- * @mixins OO.ui.mixin.TabIndexedElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.TabSelectWidget = function OoUiTabSelectWidget( config ) {
- // Parent constructor
- OO.ui.TabSelectWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.TabIndexedElement.call( this, config );
-
- // Events
- this.$element.on( {
- focus: this.bindKeyDownListener.bind( this ),
- blur: this.unbindKeyDownListener.bind( this )
- } );
-
- // Initialization
- this.$element.addClass( 'oo-ui-tabSelectWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.TabSelectWidget, OO.ui.SelectWidget );
-OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.mixin.TabIndexedElement );
-
-/**
- * CapsuleItemWidgets are used within a {@link OO.ui.CapsuleMultiSelectWidget
- * CapsuleMultiSelectWidget} to display the selected items.
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.ItemWidget
- * @mixins OO.ui.mixin.IndicatorElement
- * @mixins OO.ui.mixin.LabelElement
- * @mixins OO.ui.mixin.FlaggedElement
- * @mixins OO.ui.mixin.TabIndexedElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.CapsuleItemWidget = function OoUiCapsuleItemWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.CapsuleItemWidget.parent.call( this, config );
-
- // Properties (must be set before mixin constructor calls)
- this.$indicator = $( '<span>' );
-
- // Mixin constructors
- OO.ui.mixin.ItemWidget.call( this );
- OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$indicator, indicator: 'clear' } ) );
- OO.ui.mixin.LabelElement.call( this, config );
- OO.ui.mixin.FlaggedElement.call( this, config );
- OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
-
- // Events
- this.$indicator.on( {
- keydown: this.onCloseKeyDown.bind( this ),
- click: this.onCloseClick.bind( this )
- } );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-capsuleItemWidget' )
- .append( this.$indicator, this.$label );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.CapsuleItemWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.ItemWidget );
-OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.IndicatorElement );
-OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.FlaggedElement );
-OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.TabIndexedElement );
-
-/* Methods */
-
-/**
- * Handle close icon clicks
- * @param {jQuery.Event} event
- */
-OO.ui.CapsuleItemWidget.prototype.onCloseClick = function () {
- var element = this.getElementGroup();
-
- if ( !this.isDisabled() && element && $.isFunction( element.removeItems ) ) {
- element.removeItems( [ this ] );
- element.focus();
- }
-};
-
-/**
- * Handle close keyboard events
- * @param {jQuery.Event} event Key down event
- */
-OO.ui.CapsuleItemWidget.prototype.onCloseKeyDown = function ( e ) {
- if ( !this.isDisabled() && $.isFunction( this.getElementGroup().removeItems ) ) {
- switch ( e.which ) {
- case OO.ui.Keys.ENTER:
- case OO.ui.Keys.BACKSPACE:
- case OO.ui.Keys.SPACE:
- this.getElementGroup().removeItems( [ this ] );
- return false;
- }
- }
-};
-
-/**
- * CapsuleMultiSelectWidgets are something like a {@link OO.ui.ComboBoxInputWidget combo box widget}
- * that allows for selecting multiple values.
- *
- * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * @example
- * // Example: A CapsuleMultiSelectWidget.
- * var capsule = new OO.ui.CapsuleMultiSelectWidget( {
- * label: 'CapsuleMultiSelectWidget',
- * selected: [ 'Option 1', 'Option 3' ],
- * menu: {
- * items: [
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 1',
- * label: 'Option One'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 2',
- * label: 'Option Two'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 3',
- * label: 'Option Three'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 4',
- * label: 'Option Four'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 5',
- * label: 'Option Five'
- * } )
- * ]
- * }
- * } );
- * $( 'body' ).append( capsule.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.TabIndexedElement
- * @mixins OO.ui.mixin.GroupElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [allowArbitrary=false] Allow data items to be added even if not present in the menu.
- * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
- * @cfg {Object} [popup] Configuration options to pass to the {@link OO.ui.PopupWidget popup widget}.
- * If specified, this popup will be shown instead of the menu (but the menu
- * will still be used for item labels and allowArbitrary=false). The widgets
- * in the popup should use this.addItemsFromData() or this.addItems() as necessary.
- * @cfg {jQuery} [$overlay] Render the menu or popup into a separate layer.
- * This configuration is useful in cases where the expanded menu is larger than
- * its containing `<div>`. The specified overlay layer is usually on top of
- * the containing `<div>` and has a larger area. By default, the menu uses
- * relative positioning.
- */
-OO.ui.CapsuleMultiSelectWidget = function OoUiCapsuleMultiSelectWidget( config ) {
- var $tabFocus;
-
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.CapsuleMultiSelectWidget.parent.call( this, config );
-
- // Properties (must be set before mixin constructor calls)
- this.$input = config.popup ? null : $( '<input>' );
- this.$handle = $( '<div>' );
-
- // Mixin constructors
- OO.ui.mixin.GroupElement.call( this, config );
- if ( config.popup ) {
- config.popup = $.extend( {}, config.popup, {
- align: 'forwards',
- anchor: false
- } );
- OO.ui.mixin.PopupElement.call( this, config );
- $tabFocus = $( '<span>' );
- OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: $tabFocus } ) );
- } else {
- this.popup = null;
- $tabFocus = null;
- OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
- }
- OO.ui.mixin.IndicatorElement.call( this, config );
- OO.ui.mixin.IconElement.call( this, config );
-
- // Properties
- this.$content = $( '<div>' );
- this.allowArbitrary = !!config.allowArbitrary;
- this.$overlay = config.$overlay || this.$element;
- this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
- {
- widget: this,
- $input: this.$input,
- $container: this.$element,
- filterFromInput: true,
- disabled: this.isDisabled()
- },
- config.menu
- ) );
-
- // Events
- if ( this.popup ) {
- $tabFocus.on( {
- focus: this.onFocusForPopup.bind( this )
- } );
- this.popup.$element.on( 'focusout', this.onPopupFocusOut.bind( this ) );
- if ( this.popup.$autoCloseIgnore ) {
- this.popup.$autoCloseIgnore.on( 'focusout', this.onPopupFocusOut.bind( this ) );
- }
- this.popup.connect( this, {
- toggle: function ( visible ) {
- $tabFocus.toggle( !visible );
- }
- } );
- } else {
- this.$input.on( {
- focus: this.onInputFocus.bind( this ),
- blur: this.onInputBlur.bind( this ),
- 'propertychange change click mouseup keydown keyup input cut paste select focus':
- OO.ui.debounce( this.updateInputSize.bind( this ) ),
- keydown: this.onKeyDown.bind( this ),
- keypress: this.onKeyPress.bind( this )
- } );
- }
- this.menu.connect( this, {
- choose: 'onMenuChoose',
- add: 'onMenuItemsChange',
- remove: 'onMenuItemsChange'
- } );
- this.$handle.on( {
- mousedown: this.onMouseDown.bind( this )
- } );
-
- // Initialization
- if ( this.$input ) {
- this.$input.prop( 'disabled', this.isDisabled() );
- this.$input.attr( {
- role: 'combobox',
- 'aria-autocomplete': 'list'
- } );
- this.updateInputSize();
- }
- if ( config.data ) {
- this.setItemsFromData( config.data );
- }
- this.$content.addClass( 'oo-ui-capsuleMultiSelectWidget-content' )
- .append( this.$group );
- this.$group.addClass( 'oo-ui-capsuleMultiSelectWidget-group' );
- this.$handle.addClass( 'oo-ui-capsuleMultiSelectWidget-handle' )
- .append( this.$indicator, this.$icon, this.$content );
- this.$element.addClass( 'oo-ui-capsuleMultiSelectWidget' )
- .append( this.$handle );
- if ( this.popup ) {
- this.$content.append( $tabFocus );
- this.$overlay.append( this.popup.$element );
- } else {
- this.$content.append( this.$input );
- this.$overlay.append( this.menu.$element );
- }
- this.onMenuItemsChange();
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.GroupElement );
-OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.PopupElement );
-OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.TabIndexedElement );
-OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.IndicatorElement );
-OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.IconElement );
-
-/* Events */
-
-/**
- * @event change
- *
- * A change event is emitted when the set of selected items changes.
- *
- * @param {Mixed[]} datas Data of the now-selected items
- */
-
-/* Methods */
-
-/**
- * Construct a OO.ui.CapsuleItemWidget (or a subclass thereof) from given label and data.
- *
- * @protected
- * @param {Mixed} data Custom data of any type.
- * @param {string} label The label text.
- * @return {OO.ui.CapsuleItemWidget}
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.createItemWidget = function ( data, label ) {
- return new OO.ui.CapsuleItemWidget( { data: data, label: label } );
-};
-
-/**
- * Get the data of the items in the capsule
- * @return {Mixed[]}
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.getItemsData = function () {
- return $.map( this.getItems(), function ( e ) { return e.data; } );
-};
-
-/**
- * Set the items in the capsule by providing data
- * @chainable
- * @param {Mixed[]} datas
- * @return {OO.ui.CapsuleMultiSelectWidget}
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.setItemsFromData = function ( datas ) {
- var widget = this,
- menu = this.menu,
- items = this.getItems();
-
- $.each( datas, function ( i, data ) {
- var j, label,
- item = menu.getItemFromData( data );
-
- if ( item ) {
- label = item.label;
- } else if ( widget.allowArbitrary ) {
- label = String( data );
- } else {
- return;
- }
-
- item = null;
- for ( j = 0; j < items.length; j++ ) {
- if ( items[ j ].data === data && items[ j ].label === label ) {
- item = items[ j ];
- items.splice( j, 1 );
- break;
- }
- }
- if ( !item ) {
- item = widget.createItemWidget( data, label );
- }
- widget.addItems( [ item ], i );
- } );
-
- if ( items.length ) {
- widget.removeItems( items );
- }
-
- return this;
-};
-
-/**
- * Add items to the capsule by providing their data
- * @chainable
- * @param {Mixed[]} datas
- * @return {OO.ui.CapsuleMultiSelectWidget}
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.addItemsFromData = function ( datas ) {
- var widget = this,
- menu = this.menu,
- items = [];
-
- $.each( datas, function ( i, data ) {
- var item;
-
- if ( !widget.getItemFromData( data ) ) {
- item = menu.getItemFromData( data );
- if ( item ) {
- items.push( widget.createItemWidget( data, item.label ) );
- } else if ( widget.allowArbitrary ) {
- items.push( widget.createItemWidget( data, String( data ) ) );
- }
- }
- } );
-
- if ( items.length ) {
- this.addItems( items );
- }
-
- return this;
-};
-
-/**
- * Remove items by data
- * @chainable
- * @param {Mixed[]} datas
- * @return {OO.ui.CapsuleMultiSelectWidget}
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.removeItemsFromData = function ( datas ) {
- var widget = this,
- items = [];
-
- $.each( datas, function ( i, data ) {
- var item = widget.getItemFromData( data );
- if ( item ) {
- items.push( item );
- }
- } );
-
- if ( items.length ) {
- this.removeItems( items );
- }
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.addItems = function ( items ) {
- var same, i, l,
- oldItems = this.items.slice();
-
- OO.ui.mixin.GroupElement.prototype.addItems.call( this, items );
-
- if ( this.items.length !== oldItems.length ) {
- same = false;
- } else {
- same = true;
- for ( i = 0, l = oldItems.length; same && i < l; i++ ) {
- same = same && this.items[ i ] === oldItems[ i ];
- }
- }
- if ( !same ) {
- this.emit( 'change', this.getItemsData() );
- this.menu.position();
- }
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.removeItems = function ( items ) {
- var same, i, l,
- oldItems = this.items.slice();
-
- OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
-
- if ( this.items.length !== oldItems.length ) {
- same = false;
- } else {
- same = true;
- for ( i = 0, l = oldItems.length; same && i < l; i++ ) {
- same = same && this.items[ i ] === oldItems[ i ];
- }
- }
- if ( !same ) {
- this.emit( 'change', this.getItemsData() );
- this.menu.position();
- }
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.clearItems = function () {
- if ( this.items.length ) {
- OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
- this.emit( 'change', this.getItemsData() );
- this.menu.position();
- }
- return this;
-};
-
-/**
- * Get the capsule widget's menu.
- * @return {OO.ui.MenuSelectWidget} Menu widget
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.getMenu = function () {
- return this.menu;
-};
-
-/**
- * Handle focus events
- *
- * @private
- * @param {jQuery.Event} event
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.onInputFocus = function () {
- if ( !this.isDisabled() ) {
- this.menu.toggle( true );
- }
-};
-
-/**
- * Handle blur events
- *
- * @private
- * @param {jQuery.Event} event
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.onInputBlur = function () {
- if ( this.allowArbitrary && this.$input.val().trim() !== '' ) {
- this.addItemsFromData( [ this.$input.val() ] );
- }
- this.clearInput();
-};
-
-/**
- * Handle focus events
- *
- * @private
- * @param {jQuery.Event} event
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.onFocusForPopup = function () {
- if ( !this.isDisabled() ) {
- this.popup.setSize( this.$handle.width() );
- this.popup.toggle( true );
- this.popup.$element.find( '*' )
- .filter( function () { return OO.ui.isFocusableElement( $( this ), true ); } )
- .first()
- .focus();
- }
-};
-
-/**
- * Handles popup focus out events.
- *
- * @private
- * @param {Event} e Focus out event
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.onPopupFocusOut = function () {
- var widget = this.popup;
-
- setTimeout( function () {
- if (
- widget.isVisible() &&
- !OO.ui.contains( widget.$element[ 0 ], document.activeElement, true ) &&
- ( !widget.$autoCloseIgnore || !widget.$autoCloseIgnore.has( document.activeElement ).length )
- ) {
- widget.toggle( false );
- }
- } );
-};
-
-/**
- * Handle mouse down events.
- *
- * @private
- * @param {jQuery.Event} e Mouse down event
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.onMouseDown = function ( e ) {
- if ( e.which === OO.ui.MouseButtons.LEFT ) {
- this.focus();
- return false;
- } else {
- this.updateInputSize();
- }
-};
-
-/**
- * Handle key press events.
- *
- * @private
- * @param {jQuery.Event} e Key press event
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.onKeyPress = function ( e ) {
- var item;
-
- if ( !this.isDisabled() ) {
- if ( e.which === OO.ui.Keys.ESCAPE ) {
- this.clearInput();
- return false;
- }
-
- if ( !this.popup ) {
- this.menu.toggle( true );
- if ( e.which === OO.ui.Keys.ENTER ) {
- item = this.menu.getItemFromLabel( this.$input.val(), true );
- if ( item ) {
- this.addItemsFromData( [ item.data ] );
- this.clearInput();
- } else if ( this.allowArbitrary && this.$input.val().trim() !== '' ) {
- this.addItemsFromData( [ this.$input.val() ] );
- this.clearInput();
- }
- return false;
- }
-
- // Make sure the input gets resized.
- setTimeout( this.updateInputSize.bind( this ), 0 );
- }
- }
-};
-
-/**
- * Handle key down events.
- *
- * @private
- * @param {jQuery.Event} e Key down event
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.onKeyDown = function ( e ) {
- if ( !this.isDisabled() ) {
- // 'keypress' event is not triggered for Backspace
- if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.$input.val() === '' ) {
- if ( this.items.length ) {
- this.removeItems( this.items.slice( -1 ) );
- }
- return false;
- }
- }
-};
-
-/**
- * Update the dimensions of the text input field to encompass all available area.
- *
- * @private
- * @param {jQuery.Event} e Event of some sort
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.updateInputSize = function () {
- var $lastItem, direction, contentWidth, currentWidth, bestWidth;
- if ( !this.isDisabled() ) {
- this.$input.css( 'width', '1em' );
- $lastItem = this.$group.children().last();
- direction = OO.ui.Element.static.getDir( this.$handle );
- contentWidth = this.$input[ 0 ].scrollWidth;
- currentWidth = this.$input.width();
-
- if ( contentWidth < currentWidth ) {
- // All is fine, don't perform expensive calculations
- return;
- }
-
- if ( !$lastItem.length ) {
- bestWidth = this.$content.innerWidth();
- } else {
- bestWidth = direction === 'ltr' ?
- this.$content.innerWidth() - $lastItem.position().left - $lastItem.outerWidth() :
- $lastItem.position().left;
- }
- // Some safety margin for sanity, because I *really* don't feel like finding out where the few
- // pixels this is off by are coming from.
- bestWidth -= 10;
- if ( contentWidth > bestWidth ) {
- // This will result in the input getting shifted to the next line
- bestWidth = this.$content.innerWidth() - 10;
- }
- this.$input.width( Math.floor( bestWidth ) );
-
- this.menu.position();
- }
-};
-
-/**
- * Handle menu choose events.
- *
- * @private
- * @param {OO.ui.OptionWidget} item Chosen item
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.onMenuChoose = function ( item ) {
- if ( item && item.isVisible() ) {
- this.addItemsFromData( [ item.getData() ] );
- this.clearInput();
- }
-};
-
-/**
- * Handle menu item change events.
- *
- * @private
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.onMenuItemsChange = function () {
- this.setItemsFromData( this.getItemsData() );
- this.$element.toggleClass( 'oo-ui-capsuleMultiSelectWidget-empty', this.menu.isEmpty() );
-};
-
-/**
- * Clear the input field
- * @private
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.clearInput = function () {
- if ( this.$input ) {
- this.$input.val( '' );
- this.updateInputSize();
- }
- if ( this.popup ) {
- this.popup.toggle( false );
- }
- this.menu.toggle( false );
- this.menu.selectItem();
- this.menu.highlightItem();
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.setDisabled = function ( disabled ) {
- var i, len;
-
- // Parent method
- OO.ui.CapsuleMultiSelectWidget.parent.prototype.setDisabled.call( this, disabled );
-
- if ( this.$input ) {
- this.$input.prop( 'disabled', this.isDisabled() );
- }
- if ( this.menu ) {
- this.menu.setDisabled( this.isDisabled() );
- }
- if ( this.popup ) {
- this.popup.setDisabled( this.isDisabled() );
- }
-
- if ( this.items ) {
- for ( i = 0, len = this.items.length; i < len; i++ ) {
- this.items[ i ].updateDisabled();
- }
- }
-
- return this;
-};
-
-/**
- * Focus the widget
- * @chainable
- * @return {OO.ui.CapsuleMultiSelectWidget}
- */
-OO.ui.CapsuleMultiSelectWidget.prototype.focus = function () {
- if ( !this.isDisabled() ) {
- if ( this.popup ) {
- this.popup.setSize( this.$handle.width() );
- this.popup.toggle( true );
- this.popup.$element.find( '*' )
- .filter( function () { return OO.ui.isFocusableElement( $( this ), true ); } )
- .first()
- .focus();
- } else {
- this.updateInputSize();
- this.menu.toggle( true );
- this.$input.focus();
- }
- }
- return this;
-};
-
-/**
- * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
- * widgets can be configured with {@link OO.ui.mixin.IconElement icons} and {@link
- * OO.ui.mixin.IndicatorElement indicators}.
- * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
- *
- * @example
- * // Example of a file select widget
- * var selectFile = new OO.ui.SelectFileWidget();
- * $( 'body' ).append( selectFile.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets
- *
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.IconElement
- * @mixins OO.ui.mixin.IndicatorElement
- * @mixins OO.ui.mixin.PendingElement
- * @mixins OO.ui.mixin.LabelElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
- * @cfg {string} [placeholder] Text to display when no file is selected.
- * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
- * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
- * @cfg {boolean} [showDropTarget=false] Whether to show a drop target. Requires droppable to be true.
- * @cfg {boolean} [dragDropUI=false] Deprecated alias for showDropTarget
- */
-OO.ui.SelectFileWidget = function OoUiSelectFileWidget( config ) {
- var dragHandler;
-
- // TODO: Remove in next release
- if ( config && config.dragDropUI ) {
- config.showDropTarget = true;
- }
-
- // Configuration initialization
- config = $.extend( {
- accept: null,
- placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
- notsupported: OO.ui.msg( 'ooui-selectfile-not-supported' ),
- droppable: true,
- showDropTarget: false
- }, config );
-
- // Parent constructor
- OO.ui.SelectFileWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.IconElement.call( this, config );
- OO.ui.mixin.IndicatorElement.call( this, config );
- OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$info } ) );
- OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { autoFitLabel: true } ) );
-
- // Properties
- this.$info = $( '<span>' );
-
- // Properties
- this.showDropTarget = config.showDropTarget;
- this.isSupported = this.constructor.static.isSupported();
- this.currentFile = null;
- if ( Array.isArray( config.accept ) ) {
- this.accept = config.accept;
- } else {
- this.accept = null;
- }
- this.placeholder = config.placeholder;
- this.notsupported = config.notsupported;
- this.onFileSelectedHandler = this.onFileSelected.bind( this );
-
- this.selectButton = new OO.ui.ButtonWidget( {
- classes: [ 'oo-ui-selectFileWidget-selectButton' ],
- label: OO.ui.msg( 'ooui-selectfile-button-select' ),
- disabled: this.disabled || !this.isSupported
- } );
-
- this.clearButton = new OO.ui.ButtonWidget( {
- classes: [ 'oo-ui-selectFileWidget-clearButton' ],
- framed: false,
- icon: 'remove',
- disabled: this.disabled
- } );
-
- // Events
- this.selectButton.$button.on( {
- keypress: this.onKeyPress.bind( this )
- } );
- this.clearButton.connect( this, {
- click: 'onClearClick'
- } );
- if ( config.droppable ) {
- dragHandler = this.onDragEnterOrOver.bind( this );
- this.$element.on( {
- dragenter: dragHandler,
- dragover: dragHandler,
- dragleave: this.onDragLeave.bind( this ),
- drop: this.onDrop.bind( this )
- } );
- }
-
- // Initialization
- this.addInput();
- this.updateUI();
- this.$label.addClass( 'oo-ui-selectFileWidget-label' );
- this.$info
- .addClass( 'oo-ui-selectFileWidget-info' )
- .append( this.$icon, this.$label, this.clearButton.$element, this.$indicator );
- this.$element
- .addClass( 'oo-ui-selectFileWidget' )
- .append( this.$info, this.selectButton.$element );
- if ( config.droppable && config.showDropTarget ) {
- this.$dropTarget = $( '<div>' )
- .addClass( 'oo-ui-selectFileWidget-dropTarget' )
- .text( OO.ui.msg( 'ooui-selectfile-dragdrop-placeholder' ) )
- .on( {
- click: this.onDropTargetClick.bind( this )
- } );
- this.$element.prepend( this.$dropTarget );
- }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IconElement );
-OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IndicatorElement );
-OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.PendingElement );
-OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.LabelElement );
-
-/* Static Properties */
-
-/**
- * Check if this widget is supported
- *
- * @static
- * @return {boolean}
- */
-OO.ui.SelectFileWidget.static.isSupported = function () {
- var $input;
- if ( OO.ui.SelectFileWidget.static.isSupportedCache === null ) {
- $input = $( '<input type="file">' );
- OO.ui.SelectFileWidget.static.isSupportedCache = $input[ 0 ].files !== undefined;
- }
- return OO.ui.SelectFileWidget.static.isSupportedCache;
-};
-
-OO.ui.SelectFileWidget.static.isSupportedCache = null;
-
-/* Events */
-
-/**
- * @event change
- *
- * A change event is emitted when the on/off state of the toggle changes.
- *
- * @param {File|null} value New value
- */
-
-/* Methods */
-
-/**
- * Get the current value of the field
- *
- * @return {File|null}
- */
-OO.ui.SelectFileWidget.prototype.getValue = function () {
- return this.currentFile;
-};
-
-/**
- * Set the current value of the field
- *
- * @param {File|null} file File to select
- */
-OO.ui.SelectFileWidget.prototype.setValue = function ( file ) {
- if ( this.currentFile !== file ) {
- this.currentFile = file;
- this.updateUI();
- this.emit( 'change', this.currentFile );
- }
-};
-
-/**
- * Focus the widget.
- *
- * Focusses the select file button.
- *
- * @chainable
- */
-OO.ui.SelectFileWidget.prototype.focus = function () {
- this.selectButton.$button[ 0 ].focus();
- return this;
-};
-
-/**
- * Update the user interface when a file is selected or unselected
- *
- * @protected
- */
-OO.ui.SelectFileWidget.prototype.updateUI = function () {
- var $label;
- if ( !this.isSupported ) {
- this.$element.addClass( 'oo-ui-selectFileWidget-notsupported' );
- this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
- this.setLabel( this.notsupported );
- } else {
- this.$element.addClass( 'oo-ui-selectFileWidget-supported' );
- if ( this.currentFile ) {
- this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
- $label = $( [] );
- $label = $label.add(
- $( '<span>' )
- .addClass( 'oo-ui-selectFileWidget-fileName' )
- .text( this.currentFile.name )
- );
- if ( this.currentFile.type !== '' ) {
- $label = $label.add(
- $( '<span>' )
- .addClass( 'oo-ui-selectFileWidget-fileType' )
- .text( this.currentFile.type )
- );
- }
- this.setLabel( $label );
- } else {
- this.$element.addClass( 'oo-ui-selectFileWidget-empty' );
- this.setLabel( this.placeholder );
- }
- }
-};
-
-/**
- * Add the input to the widget
- *
- * @private
- */
-OO.ui.SelectFileWidget.prototype.addInput = function () {
- if ( this.$input ) {
- this.$input.remove();
- }
-
- if ( !this.isSupported ) {
- this.$input = null;
- return;
- }
-
- this.$input = $( '<input type="file">' );
- this.$input.on( 'change', this.onFileSelectedHandler );
- this.$input.attr( {
- tabindex: -1
- } );
- if ( this.accept ) {
- this.$input.attr( 'accept', this.accept.join( ', ' ) );
- }
- this.selectButton.$button.append( this.$input );
-};
-
-/**
- * Determine if we should accept this file
- *
- * @private
- * @param {string} File MIME type
- * @return {boolean}
- */
-OO.ui.SelectFileWidget.prototype.isAllowedType = function ( mimeType ) {
- var i, mimeTest;
-
- if ( !this.accept || !mimeType ) {
- return true;
- }
-
- for ( i = 0; i < this.accept.length; i++ ) {
- mimeTest = this.accept[ i ];
- if ( mimeTest === mimeType ) {
- return true;
- } else if ( mimeTest.substr( -2 ) === '/*' ) {
- mimeTest = mimeTest.substr( 0, mimeTest.length - 1 );
- if ( mimeType.substr( 0, mimeTest.length ) === mimeTest ) {
- return true;
- }
- }
- }
-
- return false;
-};
-
-/**
- * Handle file selection from the input
- *
- * @private
- * @param {jQuery.Event} e
- */
-OO.ui.SelectFileWidget.prototype.onFileSelected = function ( e ) {
- var file = OO.getProp( e.target, 'files', 0 ) || null;
-
- if ( file && !this.isAllowedType( file.type ) ) {
- file = null;
- }
-
- this.setValue( file );
- this.addInput();
-};
-
-/**
- * Handle clear button click events.
- *
- * @private
- */
-OO.ui.SelectFileWidget.prototype.onClearClick = function () {
- this.setValue( null );
- return false;
-};
-
-/**
- * Handle key press events.
- *
- * @private
- * @param {jQuery.Event} e Key press event
- */
-OO.ui.SelectFileWidget.prototype.onKeyPress = function ( e ) {
- if ( this.isSupported && !this.isDisabled() && this.$input &&
- ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
- ) {
- this.$input.click();
- return false;
- }
-};
-
-/**
- * Handle drop target click events.
- *
- * @private
- * @param {jQuery.Event} e Key press event
- */
-OO.ui.SelectFileWidget.prototype.onDropTargetClick = function () {
- if ( this.isSupported && !this.isDisabled() && this.$input ) {
- this.$input.click();
- return false;
- }
-};
-
-/**
- * Handle drag enter and over events
- *
- * @private
- * @param {jQuery.Event} e Drag event
- */
-OO.ui.SelectFileWidget.prototype.onDragEnterOrOver = function ( e ) {
- var itemOrFile,
- droppableFile = false,
- dt = e.originalEvent.dataTransfer;
-
- e.preventDefault();
- e.stopPropagation();
-
- if ( this.isDisabled() || !this.isSupported ) {
- this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
- dt.dropEffect = 'none';
- return false;
- }
-
- // DataTransferItem and File both have a type property, but in Chrome files
- // have no information at this point.
- itemOrFile = OO.getProp( dt, 'items', 0 ) || OO.getProp( dt, 'files', 0 );
- if ( itemOrFile ) {
- if ( this.isAllowedType( itemOrFile.type ) ) {
- droppableFile = true;
- }
- // dt.types is Array-like, but not an Array
- } else if ( Array.prototype.indexOf.call( OO.getProp( dt, 'types' ) || [], 'Files' ) !== -1 ) {
- // File information is not available at this point for security so just assume
- // it is acceptable for now.
- // https://bugzilla.mozilla.org/show_bug.cgi?id=640534
- droppableFile = true;
- }
-
- this.$element.toggleClass( 'oo-ui-selectFileWidget-canDrop', droppableFile );
- if ( !droppableFile ) {
- dt.dropEffect = 'none';
- }
-
- return false;
-};
-
-/**
- * Handle drag leave events
- *
- * @private
- * @param {jQuery.Event} e Drag event
- */
-OO.ui.SelectFileWidget.prototype.onDragLeave = function () {
- this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
-};
-
-/**
- * Handle drop events
- *
- * @private
- * @param {jQuery.Event} e Drop event
- */
-OO.ui.SelectFileWidget.prototype.onDrop = function ( e ) {
- var file = null,
- dt = e.originalEvent.dataTransfer;
-
- e.preventDefault();
- e.stopPropagation();
- this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
-
- if ( this.isDisabled() || !this.isSupported ) {
- return false;
- }
-
- file = OO.getProp( dt, 'files', 0 );
- if ( file && !this.isAllowedType( file.type ) ) {
- file = null;
- }
- if ( file ) {
- this.setValue( file );
- }
-
- return false;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.SelectFileWidget.prototype.setDisabled = function ( disabled ) {
- OO.ui.SelectFileWidget.parent.prototype.setDisabled.call( this, disabled );
- if ( this.selectButton ) {
- this.selectButton.setDisabled( disabled );
- }
- if ( this.clearButton ) {
- this.clearButton.setDisabled( disabled );
- }
- return this;
-};
-
-/**
- * Progress bars visually display the status of an operation, such as a download,
- * and can be either determinate or indeterminate:
- *
- * - **determinate** process bars show the percent of an operation that is complete.
- *
- * - **indeterminate** process bars use a visual display of motion to indicate that an operation
- * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
- * not use percentages.
- *
- * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
- *
- * @example
- * // Examples of determinate and indeterminate progress bars.
- * var progressBar1 = new OO.ui.ProgressBarWidget( {
- * progress: 33
- * } );
- * var progressBar2 = new OO.ui.ProgressBarWidget();
- *
- * // Create a FieldsetLayout to layout progress bars
- * var fieldset = new OO.ui.FieldsetLayout;
- * fieldset.addItems( [
- * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
- * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
- * ] );
- * $( 'body' ).append( fieldset.$element );
- *
- * @class
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
- * To create a determinate progress bar, specify a number that reflects the initial percent complete.
- * By default, the progress bar is indeterminate.
- */
-OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.ProgressBarWidget.parent.call( this, config );
-
- // Properties
- this.$bar = $( '<div>' );
- this.progress = null;
-
- // Initialization
- this.setProgress( config.progress !== undefined ? config.progress : false );
- this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
- this.$element
- .attr( {
- role: 'progressbar',
- 'aria-valuemin': 0,
- 'aria-valuemax': 100
- } )
- .addClass( 'oo-ui-progressBarWidget' )
- .append( this.$bar );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
-
-/* Static Properties */
-
-OO.ui.ProgressBarWidget.static.tagName = 'div';
-
-/* Methods */
-
-/**
- * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
- *
- * @return {number|boolean} Progress percent
- */
-OO.ui.ProgressBarWidget.prototype.getProgress = function () {
- return this.progress;
-};
-
-/**
- * Set the percent of the process completed or `false` for an indeterminate process.
- *
- * @param {number|boolean} progress Progress percent or `false` for indeterminate
- */
-OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
- this.progress = progress;
-
- if ( progress !== false ) {
- this.$bar.css( 'width', this.progress + '%' );
- this.$element.attr( 'aria-valuenow', this.progress );
- } else {
- this.$bar.css( 'width', '' );
- this.$element.removeAttr( 'aria-valuenow' );
- }
- this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
-};
-
-/**
- * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query,
- * and a menu of search results, which is displayed beneath the query
- * field. Unlike {@link OO.ui.mixin.LookupElement lookup menus}, search result menus are always visible to the user.
- * Users can choose an item from the menu or type a query into the text field to search for a matching result item.
- * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window.
- *
- * Each time the query is changed, the search result menu is cleared and repopulated. Please see
- * the [OOjs UI demos][1] for an example.
- *
- * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/#dialogs-mediawiki-vector-ltr
- *
- * @class
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string|jQuery} [placeholder] Placeholder text for query input
- * @cfg {string} [value] Initial query value
- */
-OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.SearchWidget.parent.call( this, config );
-
- // Properties
- this.query = new OO.ui.TextInputWidget( {
- icon: 'search',
- placeholder: config.placeholder,
- value: config.value
- } );
- this.results = new OO.ui.SelectWidget();
- this.$query = $( '<div>' );
- this.$results = $( '<div>' );
-
- // Events
- this.query.connect( this, {
- change: 'onQueryChange',
- enter: 'onQueryEnter'
- } );
- this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
-
- // Initialization
- this.$query
- .addClass( 'oo-ui-searchWidget-query' )
- .append( this.query.$element );
- this.$results
- .addClass( 'oo-ui-searchWidget-results' )
- .append( this.results.$element );
- this.$element
- .addClass( 'oo-ui-searchWidget' )
- .append( this.$results, this.$query );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
-
-/* Methods */
-
-/**
- * Handle query key down events.
- *
- * @private
- * @param {jQuery.Event} e Key down event
- */
-OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
- var highlightedItem, nextItem,
- dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
-
- if ( dir ) {
- highlightedItem = this.results.getHighlightedItem();
- if ( !highlightedItem ) {
- highlightedItem = this.results.getSelectedItem();
- }
- nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
- this.results.highlightItem( nextItem );
- nextItem.scrollElementIntoView();
- }
-};
-
-/**
- * Handle select widget select events.
- *
- * Clears existing results. Subclasses should repopulate items according to new query.
- *
- * @private
- * @param {string} value New value
- */
-OO.ui.SearchWidget.prototype.onQueryChange = function () {
- // Reset
- this.results.clearItems();
-};
-
-/**
- * Handle select widget enter key events.
- *
- * Chooses highlighted item.
- *
- * @private
- * @param {string} value New value
- */
-OO.ui.SearchWidget.prototype.onQueryEnter = function () {
- var highlightedItem = this.results.getHighlightedItem();
- if ( highlightedItem ) {
- this.results.chooseItem( highlightedItem );
- }
-};
-
-/**
- * Get the query input.
- *
- * @return {OO.ui.TextInputWidget} Query input
- */
-OO.ui.SearchWidget.prototype.getQuery = function () {
- return this.query;
-};
-
-/**
- * Get the search results menu.
- *
- * @return {OO.ui.SelectWidget} Menu of search results
- */
-OO.ui.SearchWidget.prototype.getResults = function () {
- return this.results;
-};
-
-/**
- * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
- * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
- * (to adjust the value in increments) to allow the user to enter a number.
- *
- * @example
- * // Example: A NumberInputWidget.
- * var numberInput = new OO.ui.NumberInputWidget( {
- * label: 'NumberInputWidget',
- * input: { value: 5, min: 1, max: 10 }
- * } );
- * $( 'body' ).append( numberInput.$element );
- *
- * @class
- * @extends OO.ui.Widget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
- * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
- * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
- * @cfg {boolean} [isInteger=false] Whether the field accepts only integer values.
- * @cfg {number} [min=-Infinity] Minimum allowed value
- * @cfg {number} [max=Infinity] Maximum allowed value
- * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
- * @cfg {number|null} [pageStep] Delta when using the page-up/page-down keys. Defaults to 10 times #step.
- */
-OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
- // Configuration initialization
- config = $.extend( {
- isInteger: false,
- min: -Infinity,
- max: Infinity,
- step: 1,
- pageStep: null
- }, config );
-
- // Parent constructor
- OO.ui.NumberInputWidget.parent.call( this, config );
-
- // Properties
- this.input = new OO.ui.TextInputWidget( $.extend(
- {
- disabled: this.isDisabled()
- },
- config.input
- ) );
- this.minusButton = new OO.ui.ButtonWidget( $.extend(
- {
- disabled: this.isDisabled(),
- tabIndex: -1
- },
- config.minusButton,
- {
- classes: [ 'oo-ui-numberInputWidget-minusButton' ],
- label: '−'
- }
- ) );
- this.plusButton = new OO.ui.ButtonWidget( $.extend(
- {
- disabled: this.isDisabled(),
- tabIndex: -1
- },
- config.plusButton,
- {
- classes: [ 'oo-ui-numberInputWidget-plusButton' ],
- label: '+'
- }
- ) );
-
- // Events
- this.input.connect( this, {
- change: this.emit.bind( this, 'change' ),
- enter: this.emit.bind( this, 'enter' )
- } );
- this.input.$input.on( {
- keydown: this.onKeyDown.bind( this ),
- 'wheel mousewheel DOMMouseScroll': this.onWheel.bind( this )
- } );
- this.plusButton.connect( this, {
- click: [ 'onButtonClick', +1 ]
- } );
- this.minusButton.connect( this, {
- click: [ 'onButtonClick', -1 ]
- } );
-
- // Initialization
- this.setIsInteger( !!config.isInteger );
- this.setRange( config.min, config.max );
- this.setStep( config.step, config.pageStep );
-
- this.$field = $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' )
- .append(
- this.minusButton.$element,
- this.input.$element,
- this.plusButton.$element
- );
- this.$element.addClass( 'oo-ui-numberInputWidget' ).append( this.$field );
- this.input.setValidation( this.validateNumber.bind( this ) );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.Widget );
-
-/* Events */
-
-/**
- * A `change` event is emitted when the value of the input changes.
- *
- * @event change
- */
-
-/**
- * An `enter` event is emitted when the user presses 'enter' inside the text box.
- *
- * @event enter
- */
-
-/* Methods */
-
-/**
- * Set whether only integers are allowed
- * @param {boolean} flag
- */
-OO.ui.NumberInputWidget.prototype.setIsInteger = function ( flag ) {
- this.isInteger = !!flag;
- this.input.setValidityFlag();
-};
-
-/**
- * Get whether only integers are allowed
- * @return {boolean} Flag value
- */
-OO.ui.NumberInputWidget.prototype.getIsInteger = function () {
- return this.isInteger;
-};
-
-/**
- * Set the range of allowed values
- * @param {number} min Minimum allowed value
- * @param {number} max Maximum allowed value
- */
-OO.ui.NumberInputWidget.prototype.setRange = function ( min, max ) {
- if ( min > max ) {
- throw new Error( 'Minimum (' + min + ') must not be greater than maximum (' + max + ')' );
- }
- this.min = min;
- this.max = max;
- this.input.setValidityFlag();
-};
-
-/**
- * Get the current range
- * @return {number[]} Minimum and maximum values
- */
-OO.ui.NumberInputWidget.prototype.getRange = function () {
- return [ this.min, this.max ];
-};
-
-/**
- * Set the stepping deltas
- * @param {number} step Normal step
- * @param {number|null} pageStep Page step. If null, 10 * step will be used.
- */
-OO.ui.NumberInputWidget.prototype.setStep = function ( step, pageStep ) {
- if ( step <= 0 ) {
- throw new Error( 'Step value must be positive' );
- }
- if ( pageStep === null ) {
- pageStep = step * 10;
- } else if ( pageStep <= 0 ) {
- throw new Error( 'Page step value must be positive' );
- }
- this.step = step;
- this.pageStep = pageStep;
-};
-
-/**
- * Get the current stepping values
- * @return {number[]} Step and page step
- */
-OO.ui.NumberInputWidget.prototype.getStep = function () {
- return [ this.step, this.pageStep ];
-};
-
-/**
- * Get the current value of the widget
- * @return {string}
- */
-OO.ui.NumberInputWidget.prototype.getValue = function () {
- return this.input.getValue();
-};
-
-/**
- * Get the current value of the widget as a number
- * @return {number} May be NaN, or an invalid number
- */
-OO.ui.NumberInputWidget.prototype.getNumericValue = function () {
- return +this.input.getValue();
-};
-
-/**
- * Set the value of the widget
- * @param {string} value Invalid values are allowed
- */
-OO.ui.NumberInputWidget.prototype.setValue = function ( value ) {
- this.input.setValue( value );
-};
-
-/**
- * Adjust the value of the widget
- * @param {number} delta Adjustment amount
- */
-OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
- var n, v = this.getNumericValue();
-
- delta = +delta;
- if ( isNaN( delta ) || !isFinite( delta ) ) {
- throw new Error( 'Delta must be a finite number' );
- }
-
- if ( isNaN( v ) ) {
- n = 0;
- } else {
- n = v + delta;
- n = Math.max( Math.min( n, this.max ), this.min );
- if ( this.isInteger ) {
- n = Math.round( n );
- }
- }
-
- if ( n !== v ) {
- this.setValue( n );
- }
-};
-
-/**
- * Validate input
- * @private
- * @param {string} value Field value
- * @return {boolean}
- */
-OO.ui.NumberInputWidget.prototype.validateNumber = function ( value ) {
- var n = +value;
- if ( isNaN( n ) || !isFinite( n ) ) {
- return false;
- }
-
- /*jshint bitwise: false */
- if ( this.isInteger && ( n | 0 ) !== n ) {
- return false;
- }
- /*jshint bitwise: true */
-
- if ( n < this.min || n > this.max ) {
- return false;
- }
-
- return true;
-};
-
-/**
- * Handle mouse click events.
- *
- * @private
- * @param {number} dir +1 or -1
- */
-OO.ui.NumberInputWidget.prototype.onButtonClick = function ( dir ) {
- this.adjustValue( dir * this.step );
-};
-
-/**
- * Handle mouse wheel events.
- *
- * @private
- * @param {jQuery.Event} event
- */
-OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
- var delta = 0;
-
- // Standard 'wheel' event
- if ( event.originalEvent.deltaMode !== undefined ) {
- this.sawWheelEvent = true;
- }
- if ( event.originalEvent.deltaY ) {
- delta = -event.originalEvent.deltaY;
- } else if ( event.originalEvent.deltaX ) {
- delta = event.originalEvent.deltaX;
- }
-
- // Non-standard events
- if ( !this.sawWheelEvent ) {
- if ( event.originalEvent.wheelDeltaX ) {
- delta = -event.originalEvent.wheelDeltaX;
- } else if ( event.originalEvent.wheelDeltaY ) {
- delta = event.originalEvent.wheelDeltaY;
- } else if ( event.originalEvent.wheelDelta ) {
- delta = event.originalEvent.wheelDelta;
- } else if ( event.originalEvent.detail ) {
- delta = -event.originalEvent.detail;
- }
- }
-
- if ( delta ) {
- delta = delta < 0 ? -1 : 1;
- this.adjustValue( delta * this.step );
- }
-
- return false;
-};
-
-/**
- * Handle key down events.
- *
- * @private
- * @param {jQuery.Event} e Key down event
- */
-OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) {
- if ( !this.isDisabled() ) {
- switch ( e.which ) {
- case OO.ui.Keys.UP:
- this.adjustValue( this.step );
- return false;
- case OO.ui.Keys.DOWN:
- this.adjustValue( -this.step );
- return false;
- case OO.ui.Keys.PAGEUP:
- this.adjustValue( this.pageStep );
- return false;
- case OO.ui.Keys.PAGEDOWN:
- this.adjustValue( -this.pageStep );
- return false;
- }
- }
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
- // Parent method
- OO.ui.NumberInputWidget.parent.prototype.setDisabled.call( this, disabled );
-
- if ( this.input ) {
- this.input.setDisabled( this.isDisabled() );
- }
- if ( this.minusButton ) {
- this.minusButton.setDisabled( this.isDisabled() );
- }
- if ( this.plusButton ) {
- this.plusButton.setDisabled( this.isDisabled() );
- }
-
- return this;
-};
-
-}( OO ) );
-
-/*!
- * OOjs UI v0.15.2
- * https://www.mediawiki.org/wiki/OOjs_UI
- *
- * Copyright 2011–2016 OOjs UI Team and other contributors.
- * Released under the MIT license
- * http://oojs.mit-license.org
- *
- * Date: 2016-02-02T22:07:00Z
- */
-( function ( OO ) {
-
-'use strict';
-
-/**
- * Toolbars are complex interface components that permit users to easily access a variety
- * of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional commands that are
- * part of the toolbar, but not configured as tools.
- *
- * Individual tools are customized and then registered with a {@link OO.ui.ToolFactory tool factory}, which creates
- * the tools on demand. Each tool has a symbolic name (used when registering the tool), a title (e.g., ‘Insert
- * image’), and an icon.
- *
- * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be {@link OO.ui.MenuToolGroup menus}
- * of tools, {@link OO.ui.ListToolGroup lists} of tools, or a single {@link OO.ui.BarToolGroup bar} of tools.
- * The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
- * any order, but each can only appear once in the toolbar.
- *
- * The toolbar can be synchronized with the state of the external "application", like a text
- * editor's editing area, marking tools as active/inactive (e.g. a 'bold' tool would be shown as
- * active when the text cursor was inside bolded text) or enabled/disabled (e.g. a table caption
- * tool would be disabled while the user is not editing a table). A state change is signalled by
- * emitting the {@link #event-updateState 'updateState' event}, which calls Tools'
- * {@link OO.ui.Tool#onUpdateState onUpdateState method}.
- *
- * The following is an example of a basic toolbar.
- *
- * @example
- * // Example of a toolbar
- * // Create the toolbar
- * var toolFactory = new OO.ui.ToolFactory();
- * var toolGroupFactory = new OO.ui.ToolGroupFactory();
- * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
- *
- * // We will be placing status text in this element when tools are used
- * var $area = $( '<p>' ).text( 'Toolbar example' );
- *
- * // Define the tools that we're going to place in our toolbar
- *
- * // Create a class inheriting from OO.ui.Tool
- * function SearchTool() {
- * SearchTool.parent.apply( this, arguments );
- * }
- * OO.inheritClass( SearchTool, OO.ui.Tool );
- * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
- * // of 'icon' and 'title' (displayed icon and text).
- * SearchTool.static.name = 'search';
- * SearchTool.static.icon = 'search';
- * SearchTool.static.title = 'Search...';
- * // Defines the action that will happen when this tool is selected (clicked).
- * SearchTool.prototype.onSelect = function () {
- * $area.text( 'Search tool clicked!' );
- * // Never display this tool as "active" (selected).
- * this.setActive( false );
- * };
- * SearchTool.prototype.onUpdateState = function () {};
- * // Make this tool available in our toolFactory and thus our toolbar
- * toolFactory.register( SearchTool );
- *
- * // Register two more tools, nothing interesting here
- * function SettingsTool() {
- * SettingsTool.parent.apply( this, arguments );
- * }
- * OO.inheritClass( SettingsTool, OO.ui.Tool );
- * SettingsTool.static.name = 'settings';
- * SettingsTool.static.icon = 'settings';
- * SettingsTool.static.title = 'Change settings';
- * SettingsTool.prototype.onSelect = function () {
- * $area.text( 'Settings tool clicked!' );
- * this.setActive( false );
- * };
- * SettingsTool.prototype.onUpdateState = function () {};
- * toolFactory.register( SettingsTool );
- *
- * // Register two more tools, nothing interesting here
- * function StuffTool() {
- * StuffTool.parent.apply( this, arguments );
- * }
- * OO.inheritClass( StuffTool, OO.ui.Tool );
- * StuffTool.static.name = 'stuff';
- * StuffTool.static.icon = 'ellipsis';
- * StuffTool.static.title = 'More stuff';
- * StuffTool.prototype.onSelect = function () {
- * $area.text( 'More stuff tool clicked!' );
- * this.setActive( false );
- * };
- * StuffTool.prototype.onUpdateState = function () {};
- * toolFactory.register( StuffTool );
- *
- * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
- * // little popup window (a PopupWidget).
- * function HelpTool( toolGroup, config ) {
- * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
- * padded: true,
- * label: 'Help',
- * head: true
- * } }, config ) );
- * this.popup.$body.append( '<p>I am helpful!</p>' );
- * }
- * OO.inheritClass( HelpTool, OO.ui.PopupTool );
- * HelpTool.static.name = 'help';
- * HelpTool.static.icon = 'help';
- * HelpTool.static.title = 'Help';
- * toolFactory.register( HelpTool );
- *
- * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
- * // used once (but not all defined tools must be used).
- * toolbar.setup( [
- * {
- * // 'bar' tool groups display tools' icons only, side-by-side.
- * type: 'bar',
- * include: [ 'search', 'help' ]
- * },
- * {
- * // 'list' tool groups display both the titles and icons, in a dropdown list.
- * type: 'list',
- * indicator: 'down',
- * label: 'More',
- * include: [ 'settings', 'stuff' ]
- * }
- * // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
- * // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
- * // since it's more complicated to use. (See the next example snippet on this page.)
- * ] );
- *
- * // Create some UI around the toolbar and place it in the document
- * var frame = new OO.ui.PanelLayout( {
- * expanded: false,
- * framed: true
- * } );
- * var contentFrame = new OO.ui.PanelLayout( {
- * expanded: false,
- * padded: true
- * } );
- * frame.$element.append(
- * toolbar.$element,
- * contentFrame.$element.append( $area )
- * );
- * $( 'body' ).append( frame.$element );
- *
- * // Here is where the toolbar is actually built. This must be done after inserting it into the
- * // document.
- * toolbar.initialize();
- * toolbar.emit( 'updateState' );
- *
- * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
- * {@link #event-updateState 'updateState' event}.
- *
- * @example
- * // Create the toolbar
- * var toolFactory = new OO.ui.ToolFactory();
- * var toolGroupFactory = new OO.ui.ToolGroupFactory();
- * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
- *
- * // We will be placing status text in this element when tools are used
- * var $area = $( '<p>' ).text( 'Toolbar example' );
- *
- * // Define the tools that we're going to place in our toolbar
- *
- * // Create a class inheriting from OO.ui.Tool
- * function SearchTool() {
- * SearchTool.parent.apply( this, arguments );
- * }
- * OO.inheritClass( SearchTool, OO.ui.Tool );
- * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
- * // of 'icon' and 'title' (displayed icon and text).
- * SearchTool.static.name = 'search';
- * SearchTool.static.icon = 'search';
- * SearchTool.static.title = 'Search...';
- * // Defines the action that will happen when this tool is selected (clicked).
- * SearchTool.prototype.onSelect = function () {
- * $area.text( 'Search tool clicked!' );
- * // Never display this tool as "active" (selected).
- * this.setActive( false );
- * };
- * SearchTool.prototype.onUpdateState = function () {};
- * // Make this tool available in our toolFactory and thus our toolbar
- * toolFactory.register( SearchTool );
- *
- * // Register two more tools, nothing interesting here
- * function SettingsTool() {
- * SettingsTool.parent.apply( this, arguments );
- * this.reallyActive = false;
- * }
- * OO.inheritClass( SettingsTool, OO.ui.Tool );
- * SettingsTool.static.name = 'settings';
- * SettingsTool.static.icon = 'settings';
- * SettingsTool.static.title = 'Change settings';
- * SettingsTool.prototype.onSelect = function () {
- * $area.text( 'Settings tool clicked!' );
- * // Toggle the active state on each click
- * this.reallyActive = !this.reallyActive;
- * this.setActive( this.reallyActive );
- * // To update the menu label
- * this.toolbar.emit( 'updateState' );
- * };
- * SettingsTool.prototype.onUpdateState = function () {};
- * toolFactory.register( SettingsTool );
- *
- * // Register two more tools, nothing interesting here
- * function StuffTool() {
- * StuffTool.parent.apply( this, arguments );
- * this.reallyActive = false;
- * }
- * OO.inheritClass( StuffTool, OO.ui.Tool );
- * StuffTool.static.name = 'stuff';
- * StuffTool.static.icon = 'ellipsis';
- * StuffTool.static.title = 'More stuff';
- * StuffTool.prototype.onSelect = function () {
- * $area.text( 'More stuff tool clicked!' );
- * // Toggle the active state on each click
- * this.reallyActive = !this.reallyActive;
- * this.setActive( this.reallyActive );
- * // To update the menu label
- * this.toolbar.emit( 'updateState' );
- * };
- * StuffTool.prototype.onUpdateState = function () {};
- * toolFactory.register( StuffTool );
- *
- * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
- * // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
- * function HelpTool( toolGroup, config ) {
- * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
- * padded: true,
- * label: 'Help',
- * head: true
- * } }, config ) );
- * this.popup.$body.append( '<p>I am helpful!</p>' );
- * }
- * OO.inheritClass( HelpTool, OO.ui.PopupTool );
- * HelpTool.static.name = 'help';
- * HelpTool.static.icon = 'help';
- * HelpTool.static.title = 'Help';
- * toolFactory.register( HelpTool );
- *
- * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
- * // used once (but not all defined tools must be used).
- * toolbar.setup( [
- * {
- * // 'bar' tool groups display tools' icons only, side-by-side.
- * type: 'bar',
- * include: [ 'search', 'help' ]
- * },
- * {
- * // 'menu' tool groups display both the titles and icons, in a dropdown menu.
- * // Menu label indicates which items are selected.
- * type: 'menu',
- * indicator: 'down',
- * include: [ 'settings', 'stuff' ]
- * }
- * ] );
- *
- * // Create some UI around the toolbar and place it in the document
- * var frame = new OO.ui.PanelLayout( {
- * expanded: false,
- * framed: true
- * } );
- * var contentFrame = new OO.ui.PanelLayout( {
- * expanded: false,
- * padded: true
- * } );
- * frame.$element.append(
- * toolbar.$element,
- * contentFrame.$element.append( $area )
- * );
- * $( 'body' ).append( frame.$element );
- *
- * // Here is where the toolbar is actually built. This must be done after inserting it into the
- * // document.
- * toolbar.initialize();
- * toolbar.emit( 'updateState' );
- *
- * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
- * @mixins OO.ui.mixin.GroupElement
- *
- * @constructor
- * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
- * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [actions] Add an actions section to the toolbar. Actions are commands that are included
- * in the toolbar, but are not configured as tools. By default, actions are displayed on the right side of
- * the toolbar.
- * @cfg {boolean} [shadow] Add a shadow below the toolbar.
- */
-OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
- config = toolFactory;
- toolFactory = config.toolFactory;
- toolGroupFactory = config.toolGroupFactory;
- }
-
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.Toolbar.parent.call( this, config );
-
- // Mixin constructors
- OO.EventEmitter.call( this );
- OO.ui.mixin.GroupElement.call( this, config );
-
- // Properties
- this.toolFactory = toolFactory;
- this.toolGroupFactory = toolGroupFactory;
- this.groups = [];
- this.tools = {};
- this.$bar = $( '<div>' );
- this.$actions = $( '<div>' );
- this.initialized = false;
- this.onWindowResizeHandler = this.onWindowResize.bind( this );
-
- // Events
- this.$element
- .add( this.$bar ).add( this.$group ).add( this.$actions )
- .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
-
- // Initialization
- this.$group.addClass( 'oo-ui-toolbar-tools' );
- if ( config.actions ) {
- this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
- }
- this.$bar
- .addClass( 'oo-ui-toolbar-bar' )
- .append( this.$group, '<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.Toolbar, OO.ui.Element );
-OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
-OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
-
-/* Events */
-
-/**
- * @event updateState
- *
- * An 'updateState' event must be emitted on the Toolbar (by calling `toolbar.emit( 'updateState' )`)
- * every time the state of the application using the toolbar changes, and an update to the state of
- * tools is required.
- *
- * @param {Mixed...} data Application-defined parameters
- */
-
-/* Methods */
-
-/**
- * Get the tool factory.
- *
- * @return {OO.ui.ToolFactory} Tool factory
- */
-OO.ui.Toolbar.prototype.getToolFactory = function () {
- return this.toolFactory;
-};
-
-/**
- * Get the toolgroup factory.
- *
- * @return {OO.Factory} Toolgroup factory
- */
-OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
- return this.toolGroupFactory;
-};
-
-/**
- * Handles mouse down events.
- *
- * @private
- * @param {jQuery.Event} e Mouse down event
- */
-OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
- var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
- $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
- if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
- return false;
- }
-};
-
-/**
- * Handle window resize event.
- *
- * @private
- * @param {jQuery.Event} e Window resize event
- */
-OO.ui.Toolbar.prototype.onWindowResize = function () {
- this.$element.toggleClass(
- 'oo-ui-toolbar-narrow',
- this.$bar.width() <= this.narrowThreshold
- );
-};
-
-/**
- * Sets up handles and preloads required information for the toolbar to work.
- * This must be called after it is attached to a visible document and before doing anything else.
- */
-OO.ui.Toolbar.prototype.initialize = function () {
- if ( !this.initialized ) {
- this.initialized = true;
- this.narrowThreshold = this.$group.width() + this.$actions.width();
- $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
- this.onWindowResize();
- }
-};
-
-/**
- * Set up the toolbar.
- *
- * The toolbar is set up with a list of toolgroup configurations that specify the type of
- * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or {@link OO.ui.ListToolGroup list})
- * to add and which tools to include, exclude, promote, or demote within that toolgroup. Please
- * see {@link OO.ui.ToolGroup toolgroups} for more information about including tools in toolgroups.
- *
- * @param {Object.<string,Array>} groups List of toolgroup configurations
- * @param {Array|string} [groups.include] Tools to include in the toolgroup
- * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
- * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
- * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
- */
-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 = OO.ui.msg( 'ooui-toolbar-more' );
- }
- }
- // Check type has been registered
- type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
- items.push(
- this.getToolGroupFactory().create( type, this, group )
- );
- }
- this.addItems( items );
-};
-
-/**
- * Remove all tools and toolgroups 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();
-};
-
-/**
- * Destroy the toolbar.
- *
- * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar. Call
- * this method whenever you are done using a toolbar.
- */
-OO.ui.Toolbar.prototype.destroy = function () {
- $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
- this.reset();
- this.$element.remove();
-};
-
-/**
- * Check if the tool is available.
- *
- * Available tools are ones that have not yet been added to the toolbar.
- *
- * @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.
- *
- * The OOjs UI library does not contain an accelerator system, but this is the hook for one. To
- * use an accelerator system, subclass the toolbar and override this method, which is meant to return a label
- * that describes the accelerator keys for the tool passed (by symbolic name) to the method.
- *
- * @param {string} name Symbolic name of tool
- * @return {string|undefined} Tool accelerator label if available
- */
-OO.ui.Toolbar.prototype.getToolAccelerator = function () {
- return undefined;
-};
-
-/**
- * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute {@link OO.ui.Toolbar toolbars}.
- * Each tool is configured with a static name, title, and icon and is customized with the command to carry
- * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
- * which creates the tools on demand.
- *
- * Every Tool subclass must implement two methods:
- *
- * - {@link #onUpdateState}
- * - {@link #onSelect}
- *
- * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
- * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
- * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
- *
- * For more information, please see the [OOjs UI documentation on MediaWiki][1].
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
- *
- * @abstract
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.IconElement
- * @mixins OO.ui.mixin.FlaggedElement
- * @mixins OO.ui.mixin.TabIndexedElement
- *
- * @constructor
- * @param {OO.ui.ToolGroup} toolGroup
- * @param {Object} [config] Configuration options
- * @cfg {string|Function} [title] Title text or a function that returns text. If this config is omitted, the value of
- * the {@link #static-title static title} property is used.
- *
- * The title is used in different ways depending on the type of toolgroup that contains the tool. The
- * title is used as a tooltip if the tool is part of a {@link OO.ui.BarToolGroup bar} toolgroup, or as the label text if the tool is
- * part of a {@link OO.ui.ListToolGroup list} or {@link OO.ui.MenuToolGroup menu} toolgroup.
- *
- * For bar toolgroups, a description of the accelerator key is appended to the title if an accelerator key
- * is associated with an action by the same name as the tool and accelerator functionality has been added to the application.
- * To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
- */
-OO.ui.Tool = function OoUiTool( toolGroup, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
- config = toolGroup;
- toolGroup = config.toolGroup;
- }
-
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.Tool.parent.call( this, config );
-
- // Properties
- this.toolGroup = toolGroup;
- this.toolbar = this.toolGroup.getToolbar();
- this.active = false;
- this.$title = $( '<span>' );
- this.$accel = $( '<span>' );
- this.$link = $( '<a>' );
- this.title = null;
-
- // Mixin constructors
- OO.ui.mixin.IconElement.call( this, config );
- OO.ui.mixin.FlaggedElement.call( this, config );
- OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$link } ) );
-
- // Events
- this.toolbar.connect( this, { updateState: 'onUpdateState' } );
-
- // Initialization
- this.$title.addClass( 'oo-ui-tool-title' );
- this.$accel
- .addClass( 'oo-ui-tool-accel' )
- .prop( {
- // This may need to be changed if the key names are ever localized,
- // but for now they are essentially written in English
- dir: 'ltr',
- lang: 'en'
- } );
- this.$link
- .addClass( 'oo-ui-tool-link' )
- .append( this.$icon, this.$title, this.$accel )
- .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' )
- )
- .toggleClass( 'oo-ui-tool-with-label', this.constructor.static.displayBothIconAndLabel )
- .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.mixin.IconElement );
-OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
-OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
-
-/* Static Properties */
-
-/**
- * @static
- * @inheritdoc
- */
-OO.ui.Tool.static.tagName = 'span';
-
-/**
- * Symbolic name of tool.
- *
- * The symbolic name is used internally to register the tool with a {@link OO.ui.ToolFactory ToolFactory}. It can
- * also be used when adding tools to toolgroups.
- *
- * @abstract
- * @static
- * @inheritable
- * @property {string}
- */
-OO.ui.Tool.static.name = '';
-
-/**
- * Symbolic name of the group.
- *
- * The group name is used to associate tools with each other so that they can be selected later by
- * a {@link OO.ui.ToolGroup toolgroup}.
- *
- * @abstract
- * @static
- * @inheritable
- * @property {string}
- */
-OO.ui.Tool.static.group = '';
-
-/**
- * Tool title text or a function that returns title text. The value of the static property is overridden if the #title config option is used.
- *
- * @abstract
- * @static
- * @inheritable
- * @property {string|Function}
- */
-OO.ui.Tool.static.title = '';
-
-/**
- * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
- * Normally only the icon is displayed, or only the label if no icon is given.
- *
- * @static
- * @inheritable
- * @property {boolean}
- */
-OO.ui.Tool.static.displayBothIconAndLabel = false;
-
-/**
- * Add tool to catch-all groups automatically.
- *
- * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
- * can be included in a toolgroup using the wildcard selector, an asterisk (*).
- *
- * @static
- * @inheritable
- * @property {boolean}
- */
-OO.ui.Tool.static.autoAddToCatchall = true;
-
-/**
- * Add tool to named groups automatically.
- *
- * By default, tools that are configured with a static ‘group’ property are added
- * to that group and will be selected when the symbolic name of the group is specified (e.g., when
- * toolgroups include tools by group name).
- *
- * @static
- * @property {boolean}
- * @inheritable
- */
-OO.ui.Tool.static.autoAddToGroup = true;
-
-/**
- * Check if this tool is compatible with given data.
- *
- * This is a stub that can be overridden to provide support for filtering tools based on an
- * arbitrary piece of information (e.g., where the cursor is in a document). The implementation
- * must also call this method so that the compatibility check can be performed.
- *
- * @static
- * @inheritable
- * @param {Mixed} data Data to check
- * @return {boolean} Tool can be used with data
- */
-OO.ui.Tool.static.isCompatibleWith = function () {
- return false;
-};
-
-/* Methods */
-
-/**
- * Handle the toolbar state being updated. This method is called when the
- * {@link OO.ui.Toolbar#event-updateState 'updateState' event} is emitted on the
- * {@link OO.ui.Toolbar Toolbar} that uses this tool, and should set the state of this tool
- * depending on application state (usually by calling #setDisabled to enable or disable the tool,
- * or #setActive to mark is as currently in-use or not).
- *
- * This is an abstract method that must be overridden in a concrete subclass.
- *
- * @method
- * @protected
- * @abstract
- */
-OO.ui.Tool.prototype.onUpdateState = null;
-
-/**
- * Handle the tool being selected. This method is called when the user triggers this tool,
- * usually by clicking on its label/icon.
- *
- * This is an abstract method that must be overridden in a concrete subclass.
- *
- * @method
- * @protected
- * @abstract
- */
-OO.ui.Tool.prototype.onSelect = null;
-
-/**
- * Check if the tool is active.
- *
- * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
- * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
- *
- * @return {boolean} Tool is active
- */
-OO.ui.Tool.prototype.isActive = function () {
- return this.active;
-};
-
-/**
- * Make the tool appear active or inactive.
- *
- * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
- * appear pressed or not.
- *
- * @param {boolean} state Make tool 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' );
- }
-};
-
-/**
- * Set the tool #title.
- *
- * @param {string|Function} title Title text or a function that returns text
- * @chainable
- */
-OO.ui.Tool.prototype.setTitle = function ( title ) {
- this.title = OO.ui.resolveMsg( title );
- this.updateTitle();
- return this;
-};
-
-/**
- * Get the tool #title.
- *
- * @return {string} Title text
- */
-OO.ui.Tool.prototype.getTitle = function () {
- return this.title;
-};
-
-/**
- * Get the tool's symbolic name.
- *
- * @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.text( this.title );
- this.$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.
- *
- * Destroying the tool removes all event handlers and the tool’s DOM elements.
- * Call this method whenever you are done using a tool.
- */
-OO.ui.Tool.prototype.destroy = function () {
- this.toolbar.disconnect( this );
- this.$element.remove();
-};
-
-/**
- * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a {@link OO.ui.Toolbar toolbar}.
- * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or {@link OO.ui.MenuToolGroup menu})
- * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
- * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
- *
- * Toolgroups can contain individual tools, groups of tools, or all available tools, as specified
- * using the `include` config option. See OO.ui.ToolFactory#extract on documentation of the format.
- * The options `exclude`, `promote`, and `demote` support the same formats.
- *
- * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
- * please see the [OOjs UI documentation on MediaWiki][1].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
- *
- * @abstract
- * @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.GroupElement
- *
- * @constructor
- * @param {OO.ui.Toolbar} toolbar
- * @param {Object} [config] Configuration options
- * @cfg {Array|string} [include] List of tools to include in the toolgroup, see above.
- * @cfg {Array|string} [exclude] List of tools to exclude from the toolgroup, see above.
- * @cfg {Array|string} [promote] List of tools to promote to the beginning of the toolgroup, see above.
- * @cfg {Array|string} [demote] List of tools to demote to the end of the toolgroup, see above.
- * This setting is particularly useful when tools have been added to the toolgroup
- * en masse (e.g., via the catch-all selector).
- */
-OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( toolbar ) && config === undefined ) {
- config = toolbar;
- toolbar = config.toolbar;
- }
-
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.ToolGroup.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.GroupElement.call( this, 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.onCapturedMouseKeyUpHandler = this.onCapturedMouseKeyUp.bind( this );
-
- // Events
- this.$element.on( {
- mousedown: this.onMouseKeyDown.bind( this ),
- mouseup: this.onMouseKeyUp.bind( this ),
- keydown: this.onMouseKeyDown.bind( this ),
- keyup: this.onMouseKeyUp.bind( this ),
- focus: this.onMouseOverFocus.bind( this ),
- blur: this.onMouseOutBlur.bind( this ),
- mouseover: this.onMouseOverFocus.bind( this ),
- mouseout: this.onMouseOutBlur.bind( 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.mixin.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.
- *
- * Note: The OOjs UI library does not include an accelerator system, but does contain
- * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
- * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
- * meant to return a label that describes the accelerator keys for a given tool (e.g., 'Ctrl + M').
- *
- * @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.parent.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.parent.prototype.updateDisabled.apply( this, arguments );
-};
-
-/**
- * Handle mouse down and key down events.
- *
- * @protected
- * @param {jQuery.Event} e Mouse down or key down event
- */
-OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
- if (
- !this.isDisabled() &&
- ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
- ) {
- this.pressed = this.getTargetTool( e );
- if ( this.pressed ) {
- this.pressed.setActive( true );
- this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
- this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
- }
- return false;
- }
-};
-
-/**
- * Handle captured mouse up and key up events.
- *
- * @protected
- * @param {Event} e Mouse up or key up event
- */
-OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function ( e ) {
- this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
- this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
- // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
- // released, but since `this.pressed` will no longer be true, the second call will be ignored.
- this.onMouseKeyUp( e );
-};
-
-/**
- * Handle mouse up and key up events.
- *
- * @protected
- * @param {jQuery.Event} e Mouse up or key up event
- */
-OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
- var tool = this.getTargetTool( e );
-
- if (
- !this.isDisabled() && this.pressed && this.pressed === tool &&
- ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
- ) {
- this.pressed.onSelect();
- this.pressed = null;
- return false;
- }
-
- this.pressed = null;
-};
-
-/**
- * Handle mouse over and focus events.
- *
- * @protected
- * @param {jQuery.Event} e Mouse over or focus event
- */
-OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
- var tool = this.getTargetTool( e );
-
- if ( this.pressed && this.pressed === tool ) {
- this.pressed.setActive( true );
- }
-};
-
-/**
- * Handle mouse out and blur events.
- *
- * @protected
- * @param {jQuery.Event} e Mouse out or blur event
- */
-OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
- var tool = this.getTargetTool( e );
-
- if ( this.pressed && this.pressed === tool ) {
- this.pressed.setActive( false );
- }
-};
-
-/**
- * Get the closest tool to a jQuery.Event.
- *
- * Only tool links are considered, which prevents other elements in the tool such as popups from
- * triggering tool group interactions.
- *
- * @private
- * @param {jQuery.Event} e
- * @return {OO.ui.Tool|null} Tool, `null` if none was found
- */
-OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
- var tool,
- $item = $( e.target ).closest( '.oo-ui-tool-link' );
-
- if ( $item.length ) {
- tool = $item.parent().data( 'oo-ui-tool' );
- }
-
- return tool && !tool.isDisabled() ? tool : null;
-};
-
-/**
- * Handle tool registry register events.
- *
- * If a tool is registered after the group is created, we must repopulate the list to account for:
- *
- * - a tool being added that may be included
- * - a tool already included being overridden
- *
- * @protected
- * @param {string} name Symbolic name of tool
- */
-OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
- this.populate();
-};
-
-/**
- * Get the toolbar that contains the toolgroup.
- *
- * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
- */
-OO.ui.ToolGroup.prototype.getToolbar = function () {
- return this.toolbar;
-};
-
-/**
- * Add and remove tools based on configuration.
- */
-OO.ui.ToolGroup.prototype.populate = function () {
- var i, len, name, tool,
- toolFactory = this.toolbar.getToolFactory(),
- names = {},
- add = [],
- remove = [],
- list = this.toolbar.getToolFactory().getTools(
- this.include, this.exclude, this.promote, this.demote
- );
-
- // Build a list of needed tools
- for ( i = 0, len = list.length; i < len; i++ ) {
- name = list[ i ];
- if (
- // Tool exists
- toolFactory.lookup( name ) &&
- // Tool is available or is already in this group
- ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
- ) {
- // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
- // creating it, but we can't call reserveTool() yet because we haven't created the tool.
- this.toolbar.tools[ name ] = true;
- tool = this.tools[ name ];
- if ( !tool ) {
- // Auto-initialize tools on first use
- this.tools[ name ] = tool = toolFactory.create( name, this );
- tool.updateTitle();
- }
- this.toolbar.reserveTool( tool );
- add.push( tool );
- names[ name ] = true;
- }
- }
- // Remove tools that are no longer needed
- for ( name in this.tools ) {
- if ( !names[ name ] ) {
- this.tools[ name ].destroy();
- this.toolbar.releaseTool( this.tools[ name ] );
- remove.push( this.tools[ name ] );
- delete this.tools[ name ];
- }
- }
- if ( remove.length ) {
- this.removeItems( remove );
- }
- // Update emptiness state
- if ( add.length ) {
- this.$element.removeClass( 'oo-ui-toolGroup-empty' );
- } else {
- this.$element.addClass( 'oo-ui-toolGroup-empty' );
- }
- // Re-add tools (moving existing ones to new locations)
- this.addItems( add );
- // Disabled state may depend on items
- this.updateDisabled();
-};
-
-/**
- * Destroy toolgroup.
- */
-OO.ui.ToolGroup.prototype.destroy = function () {
- var name;
-
- this.clearItems();
- this.toolbar.getToolFactory().disconnect( this );
- for ( name in this.tools ) {
- this.toolbar.releaseTool( this.tools[ name ] );
- this.tools[ name ].disconnect( this ).destroy();
- delete this.tools[ name ];
- }
- this.$element.remove();
-};
-
-/**
- * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools}, {@link OO.ui.PopupTool PopupTools},
- * and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be registered with a tool factory. Tools are
- * registered by their symbolic name. See {@link OO.ui.Toolbar toolbars} for an example.
- *
- * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
- *
- * @class
- * @extends OO.Factory
- * @constructor
- */
-OO.ui.ToolFactory = function OoUiToolFactory() {
- // Parent constructor
- OO.ui.ToolFactory.parent.call( this );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
-
-/* Methods */
-
-/**
- * Get tools from the factory
- *
- * @param {Array|string} [include] Included tools, see #extract for format
- * @param {Array|string} [exclude] Excluded tools, see #extract for format
- * @param {Array|string} [promote] Promoted tools, see #extract for format
- * @param {Array|string} [demote] Demoted tools, see #extract for format
- * @return {string[]} List of tools
- */
-OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
- var i, len, included, promoted, demoted,
- auto = [],
- used = {};
-
- // Collect included and not excluded tools
- included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
-
- // Promotion
- promoted = this.extract( promote, used );
- demoted = this.extract( demote, used );
-
- // Auto
- for ( i = 0, len = included.length; i < len; i++ ) {
- if ( !used[ included[ i ] ] ) {
- auto.push( included[ i ] );
- }
- }
-
- return promoted.concat( auto ).concat( demoted );
-};
-
-/**
- * Get a flat list of names from a list of names or groups.
- *
- * Normally, `collection` is an array of tool specifications. Tools can be specified in the
- * following ways:
- *
- * - To include an individual tool, use the symbolic name: `{ name: 'tool-name' }` or `'tool-name'`.
- * - To include all tools in a group, use the group name: `{ group: 'group-name' }`. (To assign the
- * tool to a group, use OO.ui.Tool.static.group.)
- *
- * Alternatively, to include all tools that are not yet assigned to any other toolgroup, use the
- * catch-all selector `'*'`.
- *
- * If `used` is passed, tool names that appear as properties in this object will be considered
- * already assigned, and will not be returned even if specified otherwise. The tool names extracted
- * by this function call will be added as new properties in the object.
- *
- * @private
- * @param {Array|string} collection List of tools, see above
- * @param {Object} [used] Object containing information about used tools, see above
- * @return {string[]} List of extracted tool names
- */
-OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
- var i, len, item, name, tool,
- names = [];
-
- if ( collection === '*' ) {
- for ( name in this.registry ) {
- tool = this.registry[ name ];
- if (
- // Only add tools by group name when auto-add is enabled
- tool.static.autoAddToCatchall &&
- // Exclude already used tools
- ( !used || !used[ name ] )
- ) {
- names.push( name );
- if ( used ) {
- used[ name ] = true;
- }
- }
- }
- } else if ( Array.isArray( collection ) ) {
- for ( i = 0, len = collection.length; i < len; i++ ) {
- item = collection[ i ];
- // Allow plain strings as shorthand for named tools
- if ( typeof item === 'string' ) {
- item = { name: item };
- }
- if ( OO.isPlainObject( item ) ) {
- if ( item.group ) {
- for ( name in this.registry ) {
- tool = this.registry[ name ];
- if (
- // Include tools with matching group
- tool.static.group === item.group &&
- // Only add tools by group name when auto-add is enabled
- tool.static.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;
-};
-
-/**
- * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes must
- * specify a symbolic name and be registered with the factory. The following classes are registered by
- * default:
- *
- * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
- * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
- * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
- *
- * See {@link OO.ui.Toolbar toolbars} for an example.
- *
- * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
- * @class
- * @extends OO.Factory
- * @constructor
- */
-OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
- var i, l, defaultClasses;
- // Parent constructor
- OO.Factory.call( this );
-
- defaultClasses = this.constructor.static.getDefaultClasses();
-
- // Register default toolgroups
- for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
- this.register( defaultClasses[ i ] );
- }
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
-
-/* Static Methods */
-
-/**
- * Get a default set of classes to be registered on construction.
- *
- * @return {Function[]} Default classes
- */
-OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
- return [
- OO.ui.BarToolGroup,
- OO.ui.ListToolGroup,
- OO.ui.MenuToolGroup
- ];
-};
-
-/**
- * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}. Each popup tool is configured
- * with a static name, title, and icon, as well with as any popup configurations. Unlike other tools, popup tools do not require that developers specify
- * an #onSelect or #onUpdateState method, as these methods have been implemented already.
- *
- * // Example of a popup tool. When selected, a popup tool displays
- * // a popup window.
- * function HelpTool( toolGroup, config ) {
- * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
- * padded: true,
- * label: 'Help',
- * head: true
- * } }, config ) );
- * this.popup.$body.append( '<p>I am helpful!</p>' );
- * };
- * OO.inheritClass( HelpTool, OO.ui.PopupTool );
- * HelpTool.static.name = 'help';
- * HelpTool.static.icon = 'help';
- * HelpTool.static.title = 'Help';
- * toolFactory.register( HelpTool );
- *
- * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}. For more information about
- * toolbars in genreral, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
- *
- * @abstract
- * @class
- * @extends OO.ui.Tool
- * @mixins OO.ui.mixin.PopupElement
- *
- * @constructor
- * @param {OO.ui.ToolGroup} toolGroup
- * @param {Object} [config] Configuration options
- */
-OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
- config = toolGroup;
- toolGroup = config.toolGroup;
- }
-
- // Parent constructor
- OO.ui.PopupTool.parent.call( this, toolGroup, config );
-
- // Mixin constructors
- OO.ui.mixin.PopupElement.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.mixin.PopupElement );
-
-/* Methods */
-
-/**
- * Handle the tool being selected.
- *
- * @inheritdoc
- */
-OO.ui.PopupTool.prototype.onSelect = function () {
- if ( !this.isDisabled() ) {
- this.popup.toggle();
- }
- this.setActive( false );
- return false;
-};
-
-/**
- * Handle the toolbar state being updated.
- *
- * @inheritdoc
- */
-OO.ui.PopupTool.prototype.onUpdateState = function () {
- this.setActive( false );
-};
-
-/**
- * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
- * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
- * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
- * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
- * when the ToolGroupTool is selected.
- *
- * // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2', defined elsewhere.
- *
- * function SettingsTool() {
- * SettingsTool.parent.apply( this, arguments );
- * };
- * OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
- * SettingsTool.static.name = 'settings';
- * SettingsTool.static.title = 'Change settings';
- * SettingsTool.static.groupConfig = {
- * icon: 'settings',
- * label: 'ToolGroupTool',
- * include: [ 'setting1', 'setting2' ]
- * };
- * toolFactory.register( SettingsTool );
- *
- * For more information, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * Please note that this implementation is subject to change per [T74159] [2].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars#ToolGroupTool
- * [2]: https://phabricator.wikimedia.org/T74159
- *
- * @abstract
- * @class
- * @extends OO.ui.Tool
- *
- * @constructor
- * @param {OO.ui.ToolGroup} toolGroup
- * @param {Object} [config] Configuration options
- */
-OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
- config = toolGroup;
- toolGroup = config.toolGroup;
- }
-
- // Parent constructor
- OO.ui.ToolGroupTool.parent.call( this, toolGroup, config );
-
- // Properties
- this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
-
- // Events
- this.innerToolGroup.connect( this, { disable: 'onToolGroupDisable' } );
-
- // Initialization
- this.$link.remove();
- this.$element
- .addClass( 'oo-ui-toolGroupTool' )
- .append( this.innerToolGroup.$element );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
-
-/* Static Properties */
-
-/**
- * Toolgroup configuration.
- *
- * The toolgroup configuration consists of the tools to include, as well as an icon and label
- * to use for the bar item. Tools can be included by symbolic name, group, or with the
- * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
- *
- * @property {Object.<string,Array>}
- */
-OO.ui.ToolGroupTool.static.groupConfig = {};
-
-/* Methods */
-
-/**
- * Handle the tool being selected.
- *
- * @inheritdoc
- */
-OO.ui.ToolGroupTool.prototype.onSelect = function () {
- this.innerToolGroup.setActive( !this.innerToolGroup.active );
- return false;
-};
-
-/**
- * Synchronize disabledness state of the tool with the inner toolgroup.
- *
- * @private
- * @param {boolean} disabled Element is disabled
- */
-OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
- this.setDisabled( disabled );
-};
-
-/**
- * Handle the toolbar state being updated.
- *
- * @inheritdoc
- */
-OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
- this.setActive( false );
-};
-
-/**
- * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
- *
- * @param {Object.<string,Array>} group Toolgroup configuration. Please see {@link OO.ui.ToolGroup toolgroup} for
- * more information.
- * @return {OO.ui.ListToolGroup}
- */
-OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
- if ( group.include === '*' ) {
- // Apply defaults to catch-all groups
- if ( group.label === undefined ) {
- group.label = OO.ui.msg( 'ooui-toolbar-more' );
- }
- }
-
- return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
-};
-
-/**
- * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
- * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
- * and {@link OO.ui.ListToolGroup ListToolGroup}). The {@link OO.ui.Tool tools} in a BarToolGroup are
- * displayed by icon in a single row. The title of the tool is displayed when users move the mouse over
- * the tool.
- *
- * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar is
- * set up.
- *
- * @example
- * // Example of a BarToolGroup with two tools
- * var toolFactory = new OO.ui.ToolFactory();
- * var toolGroupFactory = new OO.ui.ToolGroupFactory();
- * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
- *
- * // We will be placing status text in this element when tools are used
- * var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
- *
- * // Define the tools that we're going to place in our toolbar
- *
- * // Create a class inheriting from OO.ui.Tool
- * function SearchTool() {
- * SearchTool.parent.apply( this, arguments );
- * }
- * OO.inheritClass( SearchTool, OO.ui.Tool );
- * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
- * // of 'icon' and 'title' (displayed icon and text).
- * SearchTool.static.name = 'search';
- * SearchTool.static.icon = 'search';
- * SearchTool.static.title = 'Search...';
- * // Defines the action that will happen when this tool is selected (clicked).
- * SearchTool.prototype.onSelect = function () {
- * $area.text( 'Search tool clicked!' );
- * // Never display this tool as "active" (selected).
- * this.setActive( false );
- * };
- * SearchTool.prototype.onUpdateState = function () {};
- * // Make this tool available in our toolFactory and thus our toolbar
- * toolFactory.register( SearchTool );
- *
- * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
- * // little popup window (a PopupWidget).
- * function HelpTool( toolGroup, config ) {
- * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
- * padded: true,
- * label: 'Help',
- * head: true
- * } }, config ) );
- * this.popup.$body.append( '<p>I am helpful!</p>' );
- * }
- * OO.inheritClass( HelpTool, OO.ui.PopupTool );
- * HelpTool.static.name = 'help';
- * HelpTool.static.icon = 'help';
- * HelpTool.static.title = 'Help';
- * toolFactory.register( HelpTool );
- *
- * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
- * // used once (but not all defined tools must be used).
- * toolbar.setup( [
- * {
- * // 'bar' tool groups display tools by icon only
- * type: 'bar',
- * include: [ 'search', 'help' ]
- * }
- * ] );
- *
- * // Create some UI around the toolbar and place it in the document
- * var frame = new OO.ui.PanelLayout( {
- * expanded: false,
- * framed: true
- * } );
- * var contentFrame = new OO.ui.PanelLayout( {
- * expanded: false,
- * padded: true
- * } );
- * frame.$element.append(
- * toolbar.$element,
- * contentFrame.$element.append( $area )
- * );
- * $( 'body' ).append( frame.$element );
- *
- * // Here is where the toolbar is actually built. This must be done after inserting it into the
- * // document.
- * toolbar.initialize();
- *
- * For more information about how to add tools to a bar tool group, please see {@link OO.ui.ToolGroup toolgroup}.
- * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
- *
- * @class
- * @extends OO.ui.ToolGroup
- *
- * @constructor
- * @param {OO.ui.Toolbar} toolbar
- * @param {Object} [config] Configuration options
- */
-OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( toolbar ) && config === undefined ) {
- config = toolbar;
- toolbar = config.toolbar;
- }
-
- // Parent constructor
- OO.ui.BarToolGroup.parent.call( this, toolbar, config );
-
- // Initialization
- this.$element.addClass( 'oo-ui-barToolGroup' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
-
-/* Static Properties */
-
-OO.ui.BarToolGroup.static.titleTooltips = true;
-
-OO.ui.BarToolGroup.static.accelTooltips = true;
-
-OO.ui.BarToolGroup.static.name = 'bar';
-
-/**
- * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
- * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup--an overlaid menu or list of tools with an
- * optional icon and label. This class can be used for other base classes that also use this functionality.
- *
- * @abstract
- * @class
- * @extends OO.ui.ToolGroup
- * @mixins OO.ui.mixin.IconElement
- * @mixins OO.ui.mixin.IndicatorElement
- * @mixins OO.ui.mixin.LabelElement
- * @mixins OO.ui.mixin.TitledElement
- * @mixins OO.ui.mixin.ClippableElement
- * @mixins OO.ui.mixin.TabIndexedElement
- *
- * @constructor
- * @param {OO.ui.Toolbar} toolbar
- * @param {Object} [config] Configuration options
- * @cfg {string} [header] Text to display at the top of the popup
- */
-OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( toolbar ) && config === undefined ) {
- config = toolbar;
- toolbar = config.toolbar;
- }
-
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.PopupToolGroup.parent.call( this, toolbar, config );
-
- // Properties
- this.active = false;
- this.dragging = false;
- this.onBlurHandler = this.onBlur.bind( this );
- this.$handle = $( '<span>' );
-
- // Mixin constructors
- OO.ui.mixin.IconElement.call( this, config );
- OO.ui.mixin.IndicatorElement.call( this, config );
- OO.ui.mixin.LabelElement.call( this, config );
- OO.ui.mixin.TitledElement.call( this, config );
- OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
- OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
-
- // Events
- this.$handle.on( {
- keydown: this.onHandleMouseKeyDown.bind( this ),
- keyup: this.onHandleMouseKeyUp.bind( this ),
- mousedown: this.onHandleMouseKeyDown.bind( this ),
- mouseup: this.onHandleMouseKeyUp.bind( this )
- } );
-
- // Initialization
- this.$handle
- .addClass( 'oo-ui-popupToolGroup-handle' )
- .append( this.$icon, this.$label, this.$indicator );
- // If the pop-up should have a header, add it to the top of the toolGroup.
- // Note: If this feature is useful for other widgets, we could abstract it into an
- // OO.ui.HeaderedElement mixin constructor.
- if ( config.header !== undefined ) {
- this.$group
- .prepend( $( '<span>' )
- .addClass( 'oo-ui-popupToolGroup-header' )
- .text( config.header )
- );
- }
- this.$element
- .addClass( 'oo-ui-popupToolGroup' )
- .prepend( this.$handle );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
-OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IconElement );
-OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IndicatorElement );
-OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TitledElement );
-OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.ClippableElement );
-OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TabIndexedElement );
-
-/* Methods */
-
-/**
- * @inheritdoc
- */
-OO.ui.PopupToolGroup.prototype.setDisabled = function () {
- // Parent method
- OO.ui.PopupToolGroup.parent.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/keyup, so it is not a normal blur event object.
- *
- * @protected
- * @param {jQuery.Event} e Mouse up or key up event
- */
-OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
- // Only deactivate when clicking outside the dropdown element
- if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
- this.setActive( false );
- }
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
- // Only close toolgroup when a tool was actually selected
- if (
- !this.isDisabled() && this.pressed && this.pressed === this.getTargetTool( e ) &&
- ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
- ) {
- this.setActive( false );
- }
- return OO.ui.PopupToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
-};
-
-/**
- * Handle mouse up and key up events.
- *
- * @protected
- * @param {jQuery.Event} e Mouse up or key up event
- */
-OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
- if (
- !this.isDisabled() &&
- ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
- ) {
- return false;
- }
-};
-
-/**
- * Handle mouse down and key down events.
- *
- * @protected
- * @param {jQuery.Event} e Mouse down or key down event
- */
-OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
- if (
- !this.isDisabled() &&
- ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
- ) {
- this.setActive( !this.active );
- return false;
- }
-};
-
-/**
- * Switch into 'active' mode.
- *
- * When active, the popup is visible. A mouseup event anywhere in the document will trigger
- * deactivation.
- */
-OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
- var containerWidth, containerLeft;
- value = !!value;
- if ( this.active !== value ) {
- this.active = value;
- if ( value ) {
- this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
- this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler, true );
-
- this.$clippable.css( 'left', '' );
- // Try anchoring the popup to the left first
- this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
- this.toggleClipping( true );
- if ( this.isClippedHorizontally() ) {
- // Anchoring to the left caused the popup to clip, so anchor it to the right instead
- this.toggleClipping( false );
- this.$element
- .removeClass( 'oo-ui-popupToolGroup-left' )
- .addClass( 'oo-ui-popupToolGroup-right' );
- this.toggleClipping( true );
- }
- if ( this.isClippedHorizontally() ) {
- // Anchoring to the right also caused the popup to clip, so just make it fill the container
- containerWidth = this.$clippableScrollableContainer.width();
- containerLeft = this.$clippableScrollableContainer.offset().left;
-
- this.toggleClipping( false );
- this.$element.removeClass( 'oo-ui-popupToolGroup-right' );
-
- this.$clippable.css( {
- left: -( this.$element.offset().left - containerLeft ),
- width: containerWidth
- } );
- }
- } else {
- this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
- this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler, true );
- this.$element.removeClass(
- 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
- );
- this.toggleClipping( false );
- }
- }
-};
-
-/**
- * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
- * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
- * and {@link OO.ui.BarToolGroup BarToolGroup}). The {@link OO.ui.Tool tools} in a ListToolGroup are displayed
- * by label in a dropdown menu. The title of the tool is used as the label text. The menu itself can be configured
- * with a label, icon, indicator, header, and title.
- *
- * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a ‘More’ option that
- * users can select to see the full list of tools. If a collapsed toolgroup is expanded, a ‘Fewer’ option permits
- * users to collapse the list again.
- *
- * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the toolbar is set up. The factory
- * requires the ListToolGroup's symbolic name, 'list', which is specified along with the other configurations. For more
- * information about how to add tools to a ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
- *
- * @example
- * // Example of a ListToolGroup
- * var toolFactory = new OO.ui.ToolFactory();
- * var toolGroupFactory = new OO.ui.ToolGroupFactory();
- * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
- *
- * // Configure and register two tools
- * function SettingsTool() {
- * SettingsTool.parent.apply( this, arguments );
- * }
- * OO.inheritClass( SettingsTool, OO.ui.Tool );
- * SettingsTool.static.name = 'settings';
- * SettingsTool.static.icon = 'settings';
- * SettingsTool.static.title = 'Change settings';
- * SettingsTool.prototype.onSelect = function () {
- * this.setActive( false );
- * };
- * SettingsTool.prototype.onUpdateState = function () {};
- * toolFactory.register( SettingsTool );
- * // Register two more tools, nothing interesting here
- * function StuffTool() {
- * StuffTool.parent.apply( this, arguments );
- * }
- * OO.inheritClass( StuffTool, OO.ui.Tool );
- * StuffTool.static.name = 'stuff';
- * StuffTool.static.icon = 'search';
- * StuffTool.static.title = 'Change the world';
- * StuffTool.prototype.onSelect = function () {
- * this.setActive( false );
- * };
- * StuffTool.prototype.onUpdateState = function () {};
- * toolFactory.register( StuffTool );
- * toolbar.setup( [
- * {
- * // Configurations for list toolgroup.
- * type: 'list',
- * label: 'ListToolGroup',
- * indicator: 'down',
- * icon: 'ellipsis',
- * title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
- * header: 'This is the header',
- * include: [ 'settings', 'stuff' ],
- * allowCollapse: ['stuff']
- * }
- * ] );
- *
- * // Create some UI around the toolbar and place it in the document
- * var frame = new OO.ui.PanelLayout( {
- * expanded: false,
- * framed: true
- * } );
- * frame.$element.append(
- * toolbar.$element
- * );
- * $( 'body' ).append( frame.$element );
- * // Build the toolbar. This must be done after the toolbar has been appended to the document.
- * toolbar.initialize();
- *
- * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
- *
- * @class
- * @extends OO.ui.PopupToolGroup
- *
- * @constructor
- * @param {OO.ui.Toolbar} toolbar
- * @param {Object} [config] Configuration options
- * @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible tools
- * will only be displayed if users click the ‘More’ option displayed at the bottom of the list. If
- * the list is expanded, a ‘Fewer’ option permits users to collapse the list again. Any tools that
- * are included in the toolgroup, but are not designated as collapsible, will always be displayed.
- * To open a collapsible list in its expanded state, set #expanded to 'true'.
- * @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as collapsible.
- * Unless #expanded is set to true, the collapsible tools will be collapsed when the list is first opened.
- * @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools have
- * been designated as collapsible. When expanded is set to true, all tools in the group will be displayed
- * when the list is first opened. Users can collapse the list with a ‘Fewer’ option at the bottom.
- */
-OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( toolbar ) && config === undefined ) {
- config = toolbar;
- toolbar = config.toolbar;
- }
-
- // Configuration initialization
- config = config || {};
-
- // Properties (must be set before parent constructor, which calls #populate)
- this.allowCollapse = config.allowCollapse;
- this.forceExpand = config.forceExpand;
- this.expanded = config.expanded !== undefined ? config.expanded : false;
- this.collapsibleTools = [];
-
- // Parent constructor
- OO.ui.ListToolGroup.parent.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.name = 'list';
-
-/* Methods */
-
-/**
- * @inheritdoc
- */
-OO.ui.ListToolGroup.prototype.populate = function () {
- var i, len, allowCollapse = [];
-
- OO.ui.ListToolGroup.parent.prototype.populate.call( this );
-
- // Update the list of collapsible tools
- if ( this.allowCollapse !== undefined ) {
- allowCollapse = this.allowCollapse;
- } else if ( this.forceExpand !== undefined ) {
- allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
- }
-
- this.collapsibleTools = [];
- for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
- if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
- this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
- }
- }
-
- // Keep at the end, even when tools are added
- this.$group.append( this.getExpandCollapseTool().$element );
-
- this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
- this.updateCollapsibleState();
-};
-
-OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
- var ExpandCollapseTool;
- if ( this.expandCollapseTool === undefined ) {
- ExpandCollapseTool = function () {
- ExpandCollapseTool.parent.apply( this, arguments );
- };
-
- OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
-
- ExpandCollapseTool.prototype.onSelect = function () {
- this.toolGroup.expanded = !this.toolGroup.expanded;
- this.toolGroup.updateCollapsibleState();
- this.setActive( false );
- };
- ExpandCollapseTool.prototype.onUpdateState = function () {
- // Do nothing. Tool interface requires an implementation of this function.
- };
-
- ExpandCollapseTool.static.name = 'more-fewer';
-
- this.expandCollapseTool = new ExpandCollapseTool( this );
- }
- return this.expandCollapseTool;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
- // Do not close the popup when the user wants to show more/fewer tools
- if (
- $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
- ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
- ) {
- // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
- // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
- return OO.ui.ListToolGroup.parent.parent.prototype.onMouseKeyUp.call( this, e );
- } else {
- return OO.ui.ListToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
- }
-};
-
-OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
- var i, len;
-
- this.getExpandCollapseTool()
- .setIcon( this.expanded ? 'collapse' : 'expand' )
- .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
-
- for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
- this.collapsibleTools[ i ].toggle( this.expanded );
- }
-};
-
-/**
- * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
- * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.BarToolGroup BarToolGroup}
- * and {@link OO.ui.ListToolGroup ListToolGroup}). MenuToolGroups contain selectable {@link OO.ui.Tool tools},
- * which are displayed by label in a dropdown menu. The tool's title is used as the label text, and the
- * menu label is updated to reflect which tool or tools are currently selected. If no tools are selected,
- * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
- *
- * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
- * is set up.
- *
- * @example
- * // Example of a MenuToolGroup
- * var toolFactory = new OO.ui.ToolFactory();
- * var toolGroupFactory = new OO.ui.ToolGroupFactory();
- * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
- *
- * // We will be placing status text in this element when tools are used
- * var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the dropdown menu.' );
- *
- * // Define the tools that we're going to place in our toolbar
- *
- * function SettingsTool() {
- * SettingsTool.parent.apply( this, arguments );
- * this.reallyActive = false;
- * }
- * OO.inheritClass( SettingsTool, OO.ui.Tool );
- * SettingsTool.static.name = 'settings';
- * SettingsTool.static.icon = 'settings';
- * SettingsTool.static.title = 'Change settings';
- * SettingsTool.prototype.onSelect = function () {
- * $area.text( 'Settings tool clicked!' );
- * // Toggle the active state on each click
- * this.reallyActive = !this.reallyActive;
- * this.setActive( this.reallyActive );
- * // To update the menu label
- * this.toolbar.emit( 'updateState' );
- * };
- * SettingsTool.prototype.onUpdateState = function () {};
- * toolFactory.register( SettingsTool );
- *
- * function StuffTool() {
- * StuffTool.parent.apply( this, arguments );
- * this.reallyActive = false;
- * }
- * OO.inheritClass( StuffTool, OO.ui.Tool );
- * StuffTool.static.name = 'stuff';
- * StuffTool.static.icon = 'ellipsis';
- * StuffTool.static.title = 'More stuff';
- * StuffTool.prototype.onSelect = function () {
- * $area.text( 'More stuff tool clicked!' );
- * // Toggle the active state on each click
- * this.reallyActive = !this.reallyActive;
- * this.setActive( this.reallyActive );
- * // To update the menu label
- * this.toolbar.emit( 'updateState' );
- * };
- * StuffTool.prototype.onUpdateState = function () {};
- * toolFactory.register( StuffTool );
- *
- * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
- * // used once (but not all defined tools must be used).
- * toolbar.setup( [
- * {
- * type: 'menu',
- * header: 'This is the (optional) header',
- * title: 'This is the (optional) title',
- * indicator: 'down',
- * include: [ 'settings', 'stuff' ]
- * }
- * ] );
- *
- * // Create some UI around the toolbar and place it in the document
- * var frame = new OO.ui.PanelLayout( {
- * expanded: false,
- * framed: true
- * } );
- * var contentFrame = new OO.ui.PanelLayout( {
- * expanded: false,
- * padded: true
- * } );
- * frame.$element.append(
- * toolbar.$element,
- * contentFrame.$element.append( $area )
- * );
- * $( 'body' ).append( frame.$element );
- *
- * // Here is where the toolbar is actually built. This must be done after inserting it into the
- * // document.
- * toolbar.initialize();
- * toolbar.emit( 'updateState' );
- *
- * For more information about how to add tools to a MenuToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
- * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki] [1].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
- *
- * @class
- * @extends OO.ui.PopupToolGroup
- *
- * @constructor
- * @param {OO.ui.Toolbar} toolbar
- * @param {Object} [config] Configuration options
- */
-OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( toolbar ) && config === undefined ) {
- config = toolbar;
- toolbar = config.toolbar;
- }
-
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.MenuToolGroup.parent.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.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.
- *
- * @private
- */
-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( ', ' ) || ' ' );
-};
-
-}( OO ) );
-
-/*!
- * OOjs UI v0.15.2
- * https://www.mediawiki.org/wiki/OOjs_UI
- *
- * Copyright 2011–2016 OOjs UI Team and other contributors.
- * Released under the MIT license
- * http://oojs.mit-license.org
- *
- * Date: 2016-02-02T22:07:00Z
- */
-( function ( OO ) {
-
-'use strict';
-
-/**
- * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
- * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
- * of the actions.
- *
- * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
- * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
- * and examples.
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
- *
- * @class
- * @extends OO.ui.ButtonWidget
- * @mixins OO.ui.mixin.PendingElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
- * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
- * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
- * for more information about setting modes.
- * @cfg {boolean} [framed=false] Render the action button with a frame
- */
-OO.ui.ActionWidget = function OoUiActionWidget( config ) {
- // Configuration initialization
- config = $.extend( { framed: false }, config );
-
- // Parent constructor
- OO.ui.ActionWidget.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.PendingElement.call( this, config );
-
- // Properties
- this.action = config.action || '';
- this.modes = config.modes || [];
- this.width = 0;
- this.height = 0;
-
- // Initialization
- this.$element.addClass( 'oo-ui-actionWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
-OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
-
-/* Events */
-
-/**
- * A resize event is emitted when the size of the widget changes.
- *
- * @event resize
- */
-
-/* Methods */
-
-/**
- * Check if the action is configured to be available in the specified `mode`.
- *
- * @param {string} mode Name of mode
- * @return {boolean} The action is configured with the mode
- */
-OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
- return this.modes.indexOf( mode ) !== -1;
-};
-
-/**
- * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
- *
- * @return {string}
- */
-OO.ui.ActionWidget.prototype.getAction = function () {
- return this.action;
-};
-
-/**
- * Get the symbolic name of the mode or modes for which the action is configured to be available.
- *
- * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
- * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
- * are hidden.
- *
- * @return {string[]}
- */
-OO.ui.ActionWidget.prototype.getModes = function () {
- return this.modes.slice();
-};
-
-/**
- * Emit a resize event if the size has changed.
- *
- * @private
- * @chainable
- */
-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;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ActionWidget.prototype.setIcon = function () {
- // Mixin method
- OO.ui.mixin.IconElement.prototype.setIcon.apply( this, arguments );
- this.propagateResize();
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ActionWidget.prototype.setLabel = function () {
- // Mixin method
- OO.ui.mixin.LabelElement.prototype.setLabel.apply( this, arguments );
- this.propagateResize();
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ActionWidget.prototype.setFlags = function () {
- // Mixin method
- OO.ui.mixin.FlaggedElement.prototype.setFlags.apply( this, arguments );
- this.propagateResize();
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ActionWidget.prototype.clearFlags = function () {
- // Mixin method
- OO.ui.mixin.FlaggedElement.prototype.clearFlags.apply( this, arguments );
- this.propagateResize();
-
- return this;
-};
-
-/**
- * Toggle the visibility of the action button.
- *
- * @param {boolean} [show] Show button, omit to toggle visibility
- * @chainable
- */
-OO.ui.ActionWidget.prototype.toggle = function () {
- // Parent method
- OO.ui.ActionWidget.parent.prototype.toggle.apply( this, arguments );
- this.propagateResize();
-
- return this;
-};
-
-/**
- * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them.
- * Actions can be made available for specific contexts (modes) and circumstances
- * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
- *
- * ActionSets contain two types of actions:
- *
- * - Special: Special actions are the first visible actions with special flags, such as 'safe' and 'primary', the default special flags. Additional special flags can be configured in subclasses with the static #specialFlags property.
- * - Other: Other actions include all non-special visible actions.
- *
- * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
- *
- * @example
- * // Example: An action set used in a process dialog
- * function MyProcessDialog( config ) {
- * MyProcessDialog.parent.call( this, config );
- * }
- * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
- * MyProcessDialog.static.title = 'An action set in a process dialog';
- * // An action set that uses modes ('edit' and 'help' mode, in this example).
- * MyProcessDialog.static.actions = [
- * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
- * { action: 'help', modes: 'edit', label: 'Help' },
- * { modes: 'edit', label: 'Cancel', flags: 'safe' },
- * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
- * ];
- *
- * MyProcessDialog.prototype.initialize = function () {
- * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
- * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
- * this.panel1.$element.append( '<p>This dialog uses an action set (continue, help, cancel, back) configured with modes. This is edit mode. Click \'help\' to see help mode.</p>' );
- * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
- * this.panel2.$element.append( '<p>This is help mode. Only the \'back\' action widget is configured to be visible here. Click \'back\' to return to \'edit\' mode.</p>' );
- * this.stackLayout = new OO.ui.StackLayout( {
- * items: [ this.panel1, this.panel2 ]
- * } );
- * this.$body.append( this.stackLayout.$element );
- * };
- * MyProcessDialog.prototype.getSetupProcess = function ( data ) {
- * return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data )
- * .next( function () {
- * this.actions.setMode( 'edit' );
- * }, this );
- * };
- * MyProcessDialog.prototype.getActionProcess = function ( action ) {
- * if ( action === 'help' ) {
- * this.actions.setMode( 'help' );
- * this.stackLayout.setItem( this.panel2 );
- * } else if ( action === 'back' ) {
- * this.actions.setMode( 'edit' );
- * this.stackLayout.setItem( this.panel1 );
- * } else if ( action === 'continue' ) {
- * var dialog = this;
- * return new OO.ui.Process( function () {
- * dialog.close();
- * } );
- * }
- * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
- * };
- * MyProcessDialog.prototype.getBodyHeight = function () {
- * return this.panel1.$element.outerHeight( true );
- * };
- * var windowManager = new OO.ui.WindowManager();
- * $( 'body' ).append( windowManager.$element );
- * var dialog = new MyProcessDialog( {
- * size: 'medium'
- * } );
- * windowManager.addWindows( [ dialog ] );
- * windowManager.openWindow( dialog );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
- *
- * @abstract
- * @class
- * @mixins OO.EventEmitter
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.ActionSet = function OoUiActionSet( config ) {
- // Configuration initialization
- 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 the flags used to identify special actions. Special actions are displayed in the
- * header of a {@link OO.ui.ProcessDialog process dialog}.
- * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
- *
- * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
- *
- * @abstract
- * @static
- * @inheritable
- * @property {string}
- */
-OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
-
-/* Events */
-
-/**
- * @event click
- *
- * A 'click' event is emitted when an action is clicked.
- *
- * @param {OO.ui.ActionWidget} action Action that was clicked
- */
-
-/**
- * @event resize
- *
- * A 'resize' event is emitted when an action widget is resized.
- *
- * @param {OO.ui.ActionWidget} action Action that was resized
- */
-
-/**
- * @event add
- *
- * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
- *
- * @param {OO.ui.ActionWidget[]} added Actions added
- */
-
-/**
- * @event remove
- *
- * A 'remove' event is emitted when actions are {@link #method-remove removed}
- * or {@link #clear cleared}.
- *
- * @param {OO.ui.ActionWidget[]} added Actions removed
- */
-
-/**
- * @event change
- *
- * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
- * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
- *
- */
-
-/* Methods */
-
-/**
- * Handle action change events.
- *
- * @private
- * @fires change
- */
-OO.ui.ActionSet.prototype.onActionChange = function () {
- this.organized = false;
- if ( this.changing ) {
- this.changed = true;
- } else {
- this.emit( 'change' );
- }
-};
-
-/**
- * Check if an 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 action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
- * or ‘disabled’.
- *
- * @param {Object} [filters] Filters to use, omit to get all actions
- * @param {string|string[]} [filters.actions] Actions that action widgets must have
- * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
- * @param {string|string[]} [filters.modes] Modes that action widgets must have
- * @param {boolean} [filters.visible] Action widgets must be visible
- * @param {boolean} [filters.disabled] Action widgets must be disabled
- * @return {OO.ui.ActionWidget[]} Action widgets 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 action widgets with special flags, such as 'safe' and 'primary'.
- * Special flags can be configured in subclasses by changing the static #specialFlags property.
- *
- * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
- */
-OO.ui.ActionSet.prototype.getSpecial = function () {
- this.organize();
- return $.extend( {}, this.special );
-};
-
-/**
- * Get 'other' actions.
- *
- * Other actions include all non-special visible action widgets.
- *
- * @return {OO.ui.ActionWidget[]} 'Other' action widgets
- */
-OO.ui.ActionSet.prototype.getOthers = function () {
- this.organize();
- return this.others.slice();
-};
-
-/**
- * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
- * to be available in the specified mode will be made visible. All other actions will be hidden.
- *
- * @param {string} mode The mode. Only actions configured to be available in the specified
- * mode will be made visible.
- * @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;
-};
-
-/**
- * Set the abilities of the specified actions.
- *
- * Action widgets that are configured with the specified actions will be enabled
- * or disabled based on the boolean values specified in the `actions`
- * parameter.
- *
- * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
- * values that indicate whether or not the action should be enabled.
- * @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 to determine 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 action widgets to the action set.
- *
- * @param {OO.ui.ActionWidget[]} actions Action widgets 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 action widgets from the set.
- *
- * To remove all actions, you may wish to use the #clear method instead.
- *
- * @param {OO.ui.ActionWidget[]} actions Action widgets 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 action widets from the set.
- *
- * To remove only specified actions, use the {@link #method-remove remove} method instead.
- *
- * @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 whenever 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 categories
- 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;
-};
-
-/**
- * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
- * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
- * appearance and functionality of the error interface.
- *
- * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
- * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
- * that initiated the failed process will be disabled.
- *
- * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
- * process again.
- *
- * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors
- *
- * @class
- *
- * @constructor
- * @param {string|jQuery} message Description of error
- * @param {Object} [config] Configuration options
- * @cfg {boolean} [recoverable=true] Error is recoverable.
- * By default, errors are recoverable, and users can try the process again.
- * @cfg {boolean} [warning=false] Error is a warning.
- * If the error is a warning, the error interface will include a
- * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
- * is not triggered a second time if the user chooses to continue.
- */
-OO.ui.Error = function OoUiError( message, config ) {
- // Allow passing positional parameters inside the config object
- if ( OO.isPlainObject( message ) && config === undefined ) {
- config = message;
- message = config.message;
- }
-
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.message = message instanceof jQuery ? message : String( message );
- this.recoverable = config.recoverable === undefined || !!config.recoverable;
- this.warning = !!config.warning;
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.Error );
-
-/* Methods */
-
-/**
- * Check if the error is recoverable.
- *
- * If the error is recoverable, users are able to try the process again.
- *
- * @return {boolean} Error is recoverable
- */
-OO.ui.Error.prototype.isRecoverable = function () {
- return this.recoverable;
-};
-
-/**
- * Check if the error is a warning.
- *
- * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
- *
- * @return {boolean} Error is warning
- */
-OO.ui.Error.prototype.isWarning = function () {
- return this.warning;
-};
-
-/**
- * Get error message as DOM nodes.
- *
- * @return {jQuery} Error message in DOM nodes
- */
-OO.ui.Error.prototype.getMessage = function () {
- return this.message instanceof jQuery ?
- this.message.clone() :
- $( '<div>' ).text( this.message ).contents();
-};
-
-/**
- * Get the error message text.
- *
- * @return {string} Error message
- */
-OO.ui.Error.prototype.getMessageText = function () {
- return this.message instanceof jQuery ? this.message.text() : this.message;
-};
-
-/**
- * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
- * or a function:
- *
- * - **number**: the process will wait for the specified number of milliseconds before proceeding.
- * - **promise**: the process will continue to the next step when the promise is successfully resolved
- * or stop if the promise is rejected.
- * - **function**: the process will execute the function. The process will stop if the function returns
- * either a boolean `false` or a promise that is rejected; if the function returns a number, the process
- * will wait for that number of milliseconds before proceeding.
- *
- * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
- * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
- * its remaining steps will not be performed.
- *
- * @class
- *
- * @constructor
- * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
- * that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
- * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
- * a number or promise.
- * @return {Object} Step object, with `callback` and `context` properties
- */
-OO.ui.Process = function ( step, context ) {
- // Properties
- this.steps = [];
-
- // Initialization
- if ( step !== undefined ) {
- this.next( step, context );
- }
-};
-
-/* Setup */
-
-OO.initClass( OO.ui.Process );
-
-/* Methods */
-
-/**
- * Start the process.
- *
- * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
- * If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
- * and any remaining steps are not performed.
- */
-OO.ui.Process.prototype.execute = function () {
- var i, len, promise;
-
- /**
- * 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 ( Array.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 promise;
-};
-
-/**
- * Create a process step.
- *
- * @private
- * @param {number|jQuery.Promise|Function} step
- *
- * - Number of milliseconds to wait before proceeding
- * - Promise that must be resolved before proceeding
- * - Function to execute
- * - If the function returns a boolean false the process will stop
- * - If the function returns a promise, the process will continue to the next
- * step when the promise is resolved or stop if the promise is rejected
- * - If the function returns a number, the process will wait for that number of
- * milliseconds before proceeding
- * @param {Object} [context=null] Execution context of the function. The context is
- * ignored if the step is a number or 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' );
-};
-
-/**
- * Add step to the beginning of the process.
- *
- * @inheritdoc #createStep
- * @return {OO.ui.Process} this
- * @chainable
- */
-OO.ui.Process.prototype.first = function ( step, context ) {
- this.steps.unshift( this.createStep( step, context ) );
- return this;
-};
-
-/**
- * Add step to the end of the process.
- *
- * @inheritdoc #createStep
- * @return {OO.ui.Process} this
- * @chainable
- */
-OO.ui.Process.prototype.next = function ( step, context ) {
- this.steps.push( this.createStep( step, context ) );
- return this;
-};
-
-/**
- * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
- * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
- * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
- * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
- * pertinent data and reused.
- *
- * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
- * `opened`, and `closing`, which represent the primary stages of the cycle:
- *
- * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
- * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
- *
- * - an `opening` event is emitted with an `opening` promise
- * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
- * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
- * window and its result executed
- * - a `setup` progress notification is emitted from the `opening` promise
- * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
- * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
- * window and its result executed
- * - a `ready` progress notification is emitted from the `opening` promise
- * - the `opening` promise is resolved with an `opened` promise
- *
- * **Opened**: the window is now open.
- *
- * **Closing**: the closing stage begins when the window manager's #closeWindow or the
- * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
- * to close the window.
- *
- * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
- * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
- * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
- * window and its result executed
- * - a `hold` progress notification is emitted from the `closing` promise
- * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
- * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
- * window and its result executed
- * - a `teardown` progress notification is emitted from the `closing` promise
- * - the `closing` promise is resolved. The window is now closed
- *
- * See the [OOjs UI documentation on MediaWiki][1] for more information.
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
- *
- * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
- * Note that window classes that are instantiated with a factory must have
- * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
- * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
- */
-OO.ui.WindowManager = function OoUiWindowManager( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.WindowManager.parent.call( this, config );
-
- // Mixin constructors
- OO.EventEmitter.call( this );
-
- // Properties
- this.factory = config.factory;
- this.modal = config.modal === undefined || !!config.modal;
- this.windows = {};
- this.opening = null;
- this.opened = null;
- this.closing = null;
- this.preparingToOpen = null;
- this.preparingToClose = null;
- this.currentWindow = null;
- this.globalEvents = false;
- this.$ariaHidden = null;
- this.onWindowResizeTimeout = null;
- this.onWindowResizeHandler = this.onWindowResize.bind( this );
- this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-windowManager' )
- .toggleClass( 'oo-ui-windowManager-modal', this.modal );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
-OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
-
-/* Events */
-
-/**
- * An 'opening' event is emitted when the window begins to be opened.
- *
- * @event opening
- * @param {OO.ui.Window} win Window that's being opened
- * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
- * When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
- * is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
- * @param {Object} data Window opening data
- */
-
-/**
- * A 'closing' event is emitted when the window begins to be closed.
- *
- * @event closing
- * @param {OO.ui.Window} win Window that's being closed
- * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
- * is closed successfully. The promise emits `hold` and `teardown` notifications when those
- * processes are complete. When the `closing` promise is resolved, the first argument of its value
- * is the closing data.
- * @param {Object} data Window closing data
- */
-
-/**
- * A 'resize' event is emitted when a window is resized.
- *
- * @event resize
- * @param {OO.ui.Window} win Window that was resized
- */
-
-/* Static Properties */
-
-/**
- * Map of the symbolic name of each window size and its CSS properties.
- *
- * @static
- * @inheritable
- * @property {Object}
- */
-OO.ui.WindowManager.static.sizes = {
- small: {
- width: 300
- },
- medium: {
- width: 500
- },
- large: {
- width: 700
- },
- larger: {
- width: 900
- },
- full: {
- // These can be non-numeric because they are never used in calculations
- width: '100%',
- height: '100%'
- }
-};
-
-/**
- * Symbolic name of the default window size.
- *
- * The default size is used if the window's requested size is not recognized.
- *
- * @static
- * @inheritable
- * @property {string}
- */
-OO.ui.WindowManager.static.defaultSize = 'medium';
-
-/* Methods */
-
-/**
- * Handle window resize events.
- *
- * @private
- * @param {jQuery.Event} e Window resize event
- */
-OO.ui.WindowManager.prototype.onWindowResize = function () {
- clearTimeout( this.onWindowResizeTimeout );
- this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
-};
-
-/**
- * Handle window resize events.
- *
- * @private
- * @param {jQuery.Event} e Window resize event
- */
-OO.ui.WindowManager.prototype.afterWindowResize = function () {
- if ( this.currentWindow ) {
- this.updateWindowSize( this.currentWindow );
- }
-};
-
-/**
- * Check if window is opening.
- *
- * @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.
- *
- * @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.
- *
- * @return {boolean} Window is opened
- */
-OO.ui.WindowManager.prototype.isOpened = function ( win ) {
- return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
-};
-
-/**
- * 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;
-
- for ( name in this.windows ) {
- if ( this.windows[ name ] === win ) {
- return true;
- }
- }
-
- return false;
-};
-
-/**
- * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
- *
- * @param {OO.ui.Window} win Window being opened
- * @param {Object} [data] Window opening data
- * @return {number} Milliseconds to wait
- */
-OO.ui.WindowManager.prototype.getSetupDelay = function () {
- return 0;
-};
-
-/**
- * Get the number of milliseconds to wait after setup has finished before executing the ‘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;
-};
-
-/**
- * Get the number of milliseconds to wait after closing has begun before executing the '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;
-};
-
-/**
- * Get the number of milliseconds to wait after the ‘hold’ process has finished before
- * executing the ‘teardown’ process.
- *
- * @param {OO.ui.Window} win Window being closed
- * @param {Object} [data] Window closing data
- * @return {number} Milliseconds to wait
- */
-OO.ui.WindowManager.prototype.getTeardownDelay = function () {
- return this.modal ? 250 : 0;
-};
-
-/**
- * Get a window by its symbolic name.
- *
- * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
- * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
- * for more information about using factories.
- * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
- *
- * @param {string} name Symbolic name of the window
- * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
- * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
- * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
- */
-OO.ui.WindowManager.prototype.getWindow = function ( name ) {
- var deferred = $.Deferred(),
- win = this.windows[ name ];
-
- 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.addWindows( [ win ] );
- deferred.resolve( win );
- }
- } else {
- deferred.reject( new OO.ui.Error(
- 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
- ) );
- }
- } else {
- deferred.resolve( win );
- }
-
- return deferred.promise();
-};
-
-/**
- * Get current window.
- *
- * @return {OO.ui.Window|null} Currently opening/opened/closing window
- */
-OO.ui.WindowManager.prototype.getCurrentWindow = function () {
- return this.currentWindow;
-};
-
-/**
- * Open a window.
- *
- * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
- * @param {Object} [data] Window opening data
- * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
- * See {@link #event-opening 'opening' event} for more information about `opening` promises.
- * @fires opening
- */
-OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
- var manager = this,
- 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'
- ) );
- } else if ( this.preparingToOpen || this.opening || this.opened ) {
- opening.reject( new OO.ui.Error(
- 'Cannot open window: another window is opening or open'
- ) );
- }
-
- // Window opening
- if ( opening.state() !== 'rejected' ) {
- // If a window is currently closing, wait for it to complete
- this.preparingToOpen = $.when( this.closing );
- // Ensure handlers get called after preparingToOpen is set
- this.preparingToOpen.done( function () {
- if ( manager.modal ) {
- manager.toggleGlobalEvents( true );
- manager.toggleAriaIsolation( true );
- }
- manager.currentWindow = win;
- manager.opening = opening;
- manager.preparingToOpen = null;
- manager.emit( 'opening', win, opening, data );
- setTimeout( function () {
- win.setup( data ).then( function () {
- manager.updateWindowSize( win );
- 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 );
- }, function () {
- manager.opening = null;
- manager.opened = $.Deferred();
- opening.reject();
- manager.closeWindow( win );
- } );
- }, manager.getReadyDelay() );
- }, function () {
- manager.opening = null;
- manager.opened = $.Deferred();
- opening.reject();
- manager.closeWindow( win );
- } );
- }, manager.getSetupDelay() );
- } );
- }
-
- return opening.promise();
-};
-
-/**
- * Close a window.
- *
- * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
- * @param {Object} [data] Window closing data
- * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
- * See {@link #event-closing 'closing' event} for more information about closing promises.
- * @throws {Error} An error is thrown if the window is not managed by the window manager.
- * @fires closing
- */
-OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
- var manager = this,
- closing = $.Deferred(),
- 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.preparingToClose || this.closing ) {
- closing.reject( new OO.ui.Error(
- 'Cannot close window: window already closing with different data'
- ) );
- }
-
- // Window closing
- if ( closing.state() !== 'rejected' ) {
- // If the window is currently opening, close it when it's done
- this.preparingToClose = $.when( this.opening );
- // Ensure handlers get called after preparingToClose is set
- this.preparingToClose.always( function () {
- manager.closing = closing;
- manager.preparingToClose = null;
- manager.emit( 'closing', win, closing, data );
- opened = manager.opened;
- 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.toggleGlobalEvents( false );
- manager.toggleAriaIsolation( false );
- }
- manager.closing = null;
- manager.currentWindow = null;
- closing.resolve( data );
- } );
- }, manager.getTeardownDelay() );
- } );
- }, manager.getHoldDelay() );
- } );
- }
-
- return closing.promise();
-};
-
-/**
- * Add windows to the window manager.
- *
- * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
- * See the [OOjs ui documentation on MediaWiki] [2] for examples.
- * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
- *
- * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
- * by reference, symbolic name, or explicitly defined symbolic names.
- * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
- * explicit nor a statically configured symbolic name.
- */
-OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
- var i, len, win, name, list;
-
- if ( Array.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 ( OO.isPlainObject( windows ) ) {
- list = windows;
- }
-
- // Add windows
- for ( name in list ) {
- win = list[ name ];
- this.windows[ name ] = win.toggle( false );
- this.$element.append( win.$element );
- win.setManager( this );
- }
-};
-
-/**
- * Remove the specified windows from the windows manager.
- *
- * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
- * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
- * longer listens to events, use the #destroy method.
- *
- * @param {string[]} names Symbolic names of windows to remove
- * @return {jQuery.Promise} Promise resolved when window is closed and removed
- * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
- */
-OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
- var i, len, win, name, cleanupWindow,
- manager = this,
- promises = [],
- cleanup = function ( name, win ) {
- delete manager.windows[ name ];
- win.$element.detach();
- };
-
- for ( i = 0, len = names.length; i < len; i++ ) {
- name = names[ i ];
- win = this.windows[ name ];
- if ( !win ) {
- throw new Error( 'Cannot remove window' );
- }
- cleanupWindow = cleanup.bind( null, name, win );
- promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
- }
-
- return $.when.apply( $, promises );
-};
-
-/**
- * Remove all windows from the window manager.
- *
- * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
- * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
- * To remove just a subset of windows, use the #removeWindows method.
- *
- * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
- */
-OO.ui.WindowManager.prototype.clearWindows = function () {
- return this.removeWindows( Object.keys( this.windows ) );
-};
-
-/**
- * Set dialog size. In general, this method should not be called directly.
- *
- * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
- *
- * @chainable
- */
-OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
- var isFullscreen;
-
- // Bypass for non-current, and thus invisible, windows
- if ( win !== this.currentWindow ) {
- return;
- }
-
- isFullscreen = win.getSize() === 'full';
-
- this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen );
- this.$element.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen );
- win.setDimensions( win.getSizeProperties() );
-
- this.emit( 'resize', win );
-
- return this;
-};
-
-/**
- * Bind or unbind global events for scrolling.
- *
- * @private
- * @param {boolean} [on] Bind global events
- * @chainable
- */
-OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
- var scrollWidth, bodyMargin,
- $body = $( this.getElementDocument().body ),
- // We could have multiple window managers open so only modify
- // the body css at the bottom of the stack
- stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0 ;
-
- on = on === undefined ? !!this.globalEvents : !!on;
-
- if ( on ) {
- if ( !this.globalEvents ) {
- $( this.getElementWindow() ).on( {
- // Start listening for top-level window dimension changes
- 'orientationchange resize': this.onWindowResizeHandler
- } );
- if ( stackDepth === 0 ) {
- scrollWidth = window.innerWidth - document.documentElement.clientWidth;
- bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
- $body.css( {
- overflow: 'hidden',
- 'margin-right': bodyMargin + scrollWidth
- } );
- }
- stackDepth++;
- this.globalEvents = true;
- }
- } else if ( this.globalEvents ) {
- $( this.getElementWindow() ).off( {
- // Stop listening for top-level window dimension changes
- 'orientationchange resize': this.onWindowResizeHandler
- } );
- stackDepth--;
- if ( stackDepth === 0 ) {
- $body.css( {
- overflow: '',
- 'margin-right': ''
- } );
- }
- this.globalEvents = false;
- }
- $body.data( 'windowManagerGlobalEvents', stackDepth );
-
- return this;
-};
-
-/**
- * Toggle screen reader visibility of content other than the window manager.
- *
- * @private
- * @param {boolean} [isolate] Make only the window manager visible to screen readers
- * @chainable
- */
-OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
- isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
-
- if ( isolate ) {
- if ( !this.$ariaHidden ) {
- // Hide everything other than the window manager from screen readers
- this.$ariaHidden = $( 'body' )
- .children()
- .not( this.$element.parentsUntil( 'body' ).last() )
- .attr( 'aria-hidden', '' );
- }
- } else if ( this.$ariaHidden ) {
- // Restore screen reader visibility
- this.$ariaHidden.removeAttr( 'aria-hidden' );
- this.$ariaHidden = null;
- }
-
- return this;
-};
-
-/**
- * Destroy the window manager.
- *
- * Destroying the window manager ensures that it will no longer listen to events. If you would like to
- * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
- * instead.
- */
-OO.ui.WindowManager.prototype.destroy = function () {
- this.toggleGlobalEvents( false );
- this.toggleAriaIsolation( false );
- this.clearWindows();
- this.$element.remove();
-};
-
-/**
- * A window is a container for elements that are in a child frame. They are used with
- * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
- * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
- * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
- * the window manager will choose a sensible fallback.
- *
- * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
- * different processes are executed:
- *
- * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
- * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
- * the window.
- *
- * - {@link #getSetupProcess} method is called and its result executed
- * - {@link #getReadyProcess} method is called and its result executed
- *
- * **opened**: The window is now open
- *
- * **closing**: The closing stage begins when the window manager's
- * {@link OO.ui.WindowManager#closeWindow closeWindow}
- * or the window's {@link #close} methods are used, and the window manager begins to close the window.
- *
- * - {@link #getHoldProcess} method is called and its result executed
- * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
- *
- * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
- * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
- * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
- * processing can complete. Always assume window processes are executed asynchronously.
- *
- * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
- *
- * @abstract
- * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
- * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
- */
-OO.ui.Window = function OoUiWindow( config ) {
- // Configuration initialization
- config = config || {};
-
- // Parent constructor
- OO.ui.Window.parent.call( this, config );
-
- // Mixin constructors
- OO.EventEmitter.call( this );
-
- // Properties
- this.manager = null;
- this.size = config.size || this.constructor.static.size;
- this.$frame = $( '<div>' );
- this.$overlay = $( '<div>' );
- this.$content = $( '<div>' );
-
- this.$focusTrapBefore = $( '<div>' ).prop( 'tabIndex', 0 );
- this.$focusTrapAfter = $( '<div>' ).prop( 'tabIndex', 0 );
- this.$focusTraps = this.$focusTrapBefore.add( this.$focusTrapAfter );
-
- // Initialization
- this.$overlay.addClass( 'oo-ui-window-overlay' );
- this.$content
- .addClass( 'oo-ui-window-content' )
- .attr( 'tabindex', 0 );
- this.$frame
- .addClass( 'oo-ui-window-frame' )
- .append( this.$focusTrapBefore, this.$content, this.$focusTrapAfter );
-
- this.$element
- .addClass( 'oo-ui-window' )
- .append( this.$frame, this.$overlay );
-
- // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
- // that reference properties not initialized at that time of parent class construction
- // TODO: Find a better way to handle post-constructor setup
- this.visible = false;
- this.$element.addClass( 'oo-ui-element-hidden' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.Window, OO.ui.Element );
-OO.mixinClass( OO.ui.Window, OO.EventEmitter );
-
-/* Static Properties */
-
-/**
- * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
- *
- * The static size is used if no #size is configured during construction.
- *
- * @static
- * @inheritable
- * @property {string}
- */
-OO.ui.Window.static.size = 'medium';
-
-/* Methods */
-
-/**
- * Handle mouse down events.
- *
- * @private
- * @param {jQuery.Event} e Mouse down event
- */
-OO.ui.Window.prototype.onMouseDown = function ( e ) {
- // Prevent clicking on the click-block from stealing focus
- if ( e.target === this.$element[ 0 ] ) {
- return false;
- }
-};
-
-/**
- * Check if the window has been initialized.
- *
- * Initialization occurs when a window is added to a manager.
- *
- * @return {boolean} Window has been initialized
- */
-OO.ui.Window.prototype.isInitialized = function () {
- return !!this.manager;
-};
-
-/**
- * Check if the window is visible.
- *
- * @return {boolean} Window is visible
- */
-OO.ui.Window.prototype.isVisible = function () {
- return this.visible;
-};
-
-/**
- * Check if the window is opening.
- *
- * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
- * method.
- *
- * @return {boolean} Window is opening
- */
-OO.ui.Window.prototype.isOpening = function () {
- return this.manager.isOpening( this );
-};
-
-/**
- * Check if the window is closing.
- *
- * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
- *
- * @return {boolean} Window is closing
- */
-OO.ui.Window.prototype.isClosing = function () {
- return this.manager.isClosing( this );
-};
-
-/**
- * Check if the window is opened.
- *
- * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
- *
- * @return {boolean} Window is opened
- */
-OO.ui.Window.prototype.isOpened = function () {
- return this.manager.isOpened( this );
-};
-
-/**
- * Get the window manager.
- *
- * All windows must be attached to a window manager, which is used to open
- * and close the window and control its presentation.
- *
- * @return {OO.ui.WindowManager} Manager of window
- */
-OO.ui.Window.prototype.getManager = function () {
- return this.manager;
-};
-
-/**
- * Get the symbolic name of the window size (e.g., `small` or `medium`).
- *
- * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
- */
-OO.ui.Window.prototype.getSize = function () {
- var viewport = OO.ui.Element.static.getDimensions( this.getElementWindow() ),
- sizes = this.manager.constructor.static.sizes,
- size = this.size;
-
- if ( !sizes[ size ] ) {
- size = this.manager.constructor.static.defaultSize;
- }
- if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
- size = 'full';
- }
-
- return size;
-};
-
-/**
- * Get the size properties associated with the current window size
- *
- * @return {Object} Size properties
- */
-OO.ui.Window.prototype.getSizeProperties = function () {
- return this.manager.constructor.static.sizes[ this.getSize() ];
-};
-
-/**
- * Disable transitions on window's frame for the duration of the callback function, then enable them
- * back.
- *
- * @private
- * @param {Function} callback Function to call while transitions are disabled
- */
-OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
- // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
- // Disable transitions first, otherwise we'll get values from when the window was animating.
- var oldTransition,
- styleObj = this.$frame[ 0 ].style;
- oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
- styleObj.MozTransition || styleObj.WebkitTransition;
- styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
- styleObj.MozTransition = styleObj.WebkitTransition = 'none';
- callback();
- // Force reflow to make sure the style changes done inside callback really are not transitioned
- this.$frame.height();
- styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
- styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
-};
-
-/**
- * Get the height of the full window contents (i.e., the window head, body and foot together).
- *
- * What consistitutes the head, body, and foot varies depending on the window type.
- * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
- * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
- * and special actions in the head, and dialog content in the body.
- *
- * To get just the height of the dialog body, use the #getBodyHeight method.
- *
- * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
- */
-OO.ui.Window.prototype.getContentHeight = function () {
- var bodyHeight,
- win = this,
- bodyStyleObj = this.$body[ 0 ].style,
- frameStyleObj = this.$frame[ 0 ].style;
-
- // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
- // Disable transitions first, otherwise we'll get values from when the window was animating.
- this.withoutSizeTransitions( function () {
- var oldHeight = frameStyleObj.height,
- oldPosition = bodyStyleObj.position;
- frameStyleObj.height = '1px';
- // Force body to resize to new width
- bodyStyleObj.position = 'relative';
- bodyHeight = win.getBodyHeight();
- frameStyleObj.height = oldHeight;
- bodyStyleObj.position = oldPosition;
- } );
-
- return (
- // Add buffer for border
- ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
- // Use combined heights of children
- ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
- );
-};
-
-/**
- * Get the height of the window body.
- *
- * To get the height of the full window contents (the window body, head, and foot together),
- * use #getContentHeight.
- *
- * When this function is called, the window will temporarily have been resized
- * to height=1px, so .scrollHeight measurements can be taken accurately.
- *
- * @return {number} Height of the window body in pixels
- */
-OO.ui.Window.prototype.getBodyHeight = function () {
- return this.$body[ 0 ].scrollHeight;
-};
-
-/**
- * Get the directionality of the frame (right-to-left or left-to-right).
- *
- * @return {string} Directionality: `'ltr'` or `'rtl'`
- */
-OO.ui.Window.prototype.getDir = function () {
- return OO.ui.Element.static.getDir( this.$content ) || 'ltr';
-};
-
-/**
- * Get the 'setup' process.
- *
- * The setup process is used to set up a window for use in a particular context,
- * based on the `data` argument. This method is called during the opening phase of the window’s
- * lifecycle.
- *
- * Override this method to add additional steps to the ‘setup’ process the parent method provides
- * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
- * of OO.ui.Process.
- *
- * To add window content that persists between openings, you may wish to use the #initialize method
- * instead.
- *
- * @param {Object} [data] Window opening data
- * @return {OO.ui.Process} Setup process
- */
-OO.ui.Window.prototype.getSetupProcess = function () {
- return new OO.ui.Process();
-};
-
-/**
- * Get the ‘ready’ process.
- *
- * The ready process is used to ready a window for use in a particular
- * context, based on the `data` argument. This method is called during the opening phase of
- * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
- *
- * Override this method to add additional steps to the ‘ready’ process the parent method
- * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
- * methods of OO.ui.Process.
- *
- * @param {Object} [data] Window opening data
- * @return {OO.ui.Process} Ready process
- */
-OO.ui.Window.prototype.getReadyProcess = function () {
- return new OO.ui.Process();
-};
-
-/**
- * Get the 'hold' process.
- *
- * The hold proccess is used to keep a window from being used in a particular context,
- * based on the `data` argument. This method is called during the closing phase of the window’s
- * lifecycle.
- *
- * Override this method to add additional steps to the 'hold' process the parent method provides
- * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
- * of OO.ui.Process.
- *
- * @param {Object} [data] Window closing data
- * @return {OO.ui.Process} Hold process
- */
-OO.ui.Window.prototype.getHoldProcess = function () {
- return new OO.ui.Process();
-};
-
-/**
- * Get the ‘teardown’ process.
- *
- * The teardown process is used to teardown a window after use. During teardown,
- * user interactions within the window are conveyed and the window is closed, based on the `data`
- * argument. This method is called during the closing phase of the window’s lifecycle.
- *
- * Override this method to add additional steps to the ‘teardown’ process the parent method provides
- * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
- * of OO.ui.Process.
- *
- * @param {Object} [data] Window closing data
- * @return {OO.ui.Process} Teardown process
- */
-OO.ui.Window.prototype.getTeardownProcess = function () {
- return new OO.ui.Process();
-};
-
-/**
- * Set the window manager.
- *
- * This will cause the window to initialize. Calling it more than once will cause an error.
- *
- * @param {OO.ui.WindowManager} manager Manager for this window
- * @throws {Error} An error is thrown if the method is called more than once
- * @chainable
- */
-OO.ui.Window.prototype.setManager = function ( manager ) {
- if ( this.manager ) {
- throw new Error( 'Cannot set window manager, window already has a manager' );
- }
-
- this.manager = manager;
- this.initialize();
-
- return this;
-};
-
-/**
- * Set the window size by symbolic name (e.g., 'small' or 'medium')
- *
- * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
- * `full`
- * @chainable
- */
-OO.ui.Window.prototype.setSize = function ( size ) {
- this.size = size;
- this.updateSize();
- return this;
-};
-
-/**
- * Update the window size.
- *
- * @throws {Error} An error is thrown if the window is not attached to a window manager
- * @chainable
- */
-OO.ui.Window.prototype.updateSize = function () {
- if ( !this.manager ) {
- throw new Error( 'Cannot update window size, must be attached to a manager' );
- }
-
- this.manager.updateWindowSize( this );
-
- return this;
-};
-
-/**
- * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
- * when the window is opening. In general, setDimensions should not be called directly.
- *
- * To set the size of the window, use the #setSize method.
- *
- * @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.Window.prototype.setDimensions = function ( dim ) {
- var height,
- win = this,
- styleObj = this.$frame[ 0 ].style;
-
- // Calculate the height we need to set using the correct width
- if ( dim.height === undefined ) {
- this.withoutSizeTransitions( function () {
- var oldWidth = styleObj.width;
- win.$frame.css( 'width', dim.width || '' );
- height = win.getContentHeight();
- styleObj.width = oldWidth;
- } );
- } else {
- height = dim.height;
- }
-
- this.$frame.css( {
- width: dim.width || '',
- minWidth: dim.minWidth || '',
- maxWidth: dim.maxWidth || '',
- height: height || '',
- minHeight: dim.minHeight || '',
- maxHeight: dim.maxHeight || ''
- } );
-
- return this;
-};
-
-/**
- * Initialize window contents.
- *
- * Before the window is opened for the first time, #initialize is called so that content that
- * persists between openings can be added to the window.
- *
- * To set up a window with new content each time the window opens, use #getSetupProcess.
- *
- * @throws {Error} An error is thrown if the window is not attached to a window manager
- * @chainable
- */
-OO.ui.Window.prototype.initialize = function () {
- if ( !this.manager ) {
- throw new Error( 'Cannot initialize window, must be attached to a manager' );
- }
-
- // Properties
- this.$head = $( '<div>' );
- this.$body = $( '<div>' );
- this.$foot = $( '<div>' );
- this.$document = $( this.getElementDocument() );
-
- // Events
- this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
-
- // Initialization
- this.$head.addClass( 'oo-ui-window-head' );
- this.$body.addClass( 'oo-ui-window-body' );
- this.$foot.addClass( 'oo-ui-window-foot' );
- this.$content.append( this.$head, this.$body, this.$foot );
-
- return this;
-};
-
-/**
- * Called when someone tries to focus the hidden element at the end of the dialog.
- * Sends focus back to the start of the dialog.
- *
- * @param {jQuery.Event} event Focus event
- */
-OO.ui.Window.prototype.onFocusTrapFocused = function ( event ) {
- if ( this.$focusTrapBefore.is( event.target ) ) {
- OO.ui.findFocusable( this.$content, true ).focus();
- } else {
- // this.$content is the part of the focus cycle, and is the first focusable element
- this.$content.focus();
- }
-};
-
-/**
- * Open the window.
- *
- * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
- * method, which returns a promise resolved when the window is done opening.
- *
- * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
- *
- * @param {Object} [data] Window opening data
- * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
- * if the window fails to open. When the promise is resolved successfully, the first argument of the
- * value is a new promise, which is resolved when the window begins closing.
- * @throws {Error} An error is thrown if the window is not attached to a window manager
- */
-OO.ui.Window.prototype.open = function ( data ) {
- if ( !this.manager ) {
- throw new Error( 'Cannot open window, must be attached to a manager' );
- }
-
- return this.manager.openWindow( this, data );
-};
-
-/**
- * Close the window.
- *
- * This method is a wrapper around a call to the window
- * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
- * which returns a closing promise resolved when the window is done closing.
- *
- * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
- * phase of the window’s lifecycle and can be used to specify closing behavior each time
- * the window closes.
- *
- * @param {Object} [data] Window closing data
- * @return {jQuery.Promise} Promise resolved when window is closed
- * @throws {Error} An error is thrown if the window is not attached to a window manager
- */
-OO.ui.Window.prototype.close = function ( data ) {
- if ( !this.manager ) {
- throw new Error( 'Cannot close window, must be attached to a manager' );
- }
-
- return this.manager.closeWindow( this, data );
-};
-
-/**
- * Setup window.
- *
- * This is called by OO.ui.WindowManager during 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 setup
- */
-OO.ui.Window.prototype.setup = function ( data ) {
- var win = this;
-
- this.toggle( true );
-
- this.focusTrapHandler = OO.ui.bind( this.onFocusTrapFocused, this );
- this.$focusTraps.on( 'focus', this.focusTrapHandler );
-
- return this.getSetupProcess( data ).execute().then( function () {
- // Force redraw by asking the browser to measure the elements' widths
- win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
- win.$content.addClass( 'oo-ui-window-content-setup' ).width();
- } );
-};
-
-/**
- * Ready window.
- *
- * This is called by OO.ui.WindowManager during 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.Window.prototype.ready = function ( data ) {
- var win = this;
-
- this.$content.focus();
- return this.getReadyProcess( data ).execute().then( function () {
- // Force redraw by asking the browser to measure the elements' widths
- win.$element.addClass( 'oo-ui-window-ready' ).width();
- win.$content.addClass( 'oo-ui-window-content-ready' ).width();
- } );
-};
-
-/**
- * Hold window.
- *
- * This is called by OO.ui.WindowManager during window closing, and should not be called directly
- * by other systems.
- *
- * @param {Object} [data] Window closing data
- * @return {jQuery.Promise} Promise resolved when window is held
- */
-OO.ui.Window.prototype.hold = function ( data ) {
- var win = this;
-
- return this.getHoldProcess( data ).execute().then( function () {
- // Get the focused element within the window's content
- var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
-
- // Blur the focused element
- if ( $focus.length ) {
- $focus[ 0 ].blur();
- }
-
- // Force redraw by asking the browser to measure the elements' widths
- win.$element.removeClass( 'oo-ui-window-ready' ).width();
- win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
- } );
-};
-
-/**
- * Teardown window.
- *
- * This is called by OO.ui.WindowManager during window closing, and should not be called directly
- * by other systems.
- *
- * @param {Object} [data] Window closing data
- * @return {jQuery.Promise} Promise resolved when window is torn down
- */
-OO.ui.Window.prototype.teardown = function ( data ) {
- var win = this;
-
- return this.getTeardownProcess( data ).execute().then( function () {
- // Force redraw by asking the browser to measure the elements' widths
- win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
- win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
- win.$focusTraps.off( 'focus', win.focusTrapHandler );
- win.toggle( false );
- } );
-};
-
-/**
- * The Dialog class serves as the base class for the other types of dialogs.
- * Unless extended to include controls, the rendered dialog box is a simple window
- * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
- * which opens, closes, and controls the presentation of the window. See the
- * [OOjs UI documentation on MediaWiki] [1] for more information.
- *
- * @example
- * // A simple dialog window.
- * function MyDialog( config ) {
- * MyDialog.parent.call( this, config );
- * }
- * OO.inheritClass( MyDialog, OO.ui.Dialog );
- * MyDialog.prototype.initialize = function () {
- * MyDialog.parent.prototype.initialize.call( this );
- * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
- * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
- * this.$body.append( this.content.$element );
- * };
- * MyDialog.prototype.getBodyHeight = function () {
- * return this.content.$element.outerHeight( true );
- * };
- * var myDialog = new MyDialog( {
- * size: 'medium'
- * } );
- * // Create and append a window manager, which opens and closes the window.
- * var windowManager = new OO.ui.WindowManager();
- * $( 'body' ).append( windowManager.$element );
- * windowManager.addWindows( [ myDialog ] );
- * // Open the window!
- * windowManager.openWindow( myDialog );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
- *
- * @abstract
- * @class
- * @extends OO.ui.Window
- * @mixins OO.ui.mixin.PendingElement
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.Dialog = function OoUiDialog( config ) {
- // Parent constructor
- OO.ui.Dialog.parent.call( this, config );
-
- // Mixin constructors
- OO.ui.mixin.PendingElement.call( this );
-
- // Properties
- this.actions = new OO.ui.ActionSet();
- this.attachedActions = [];
- this.currentAction = null;
- this.onDialogKeyDownHandler = this.onDialogKeyDown.bind( this );
-
- // Events
- this.actions.connect( this, {
- click: 'onActionClick',
- resize: 'onActionResize',
- change: 'onActionsChange'
- } );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-dialog' )
- .attr( 'role', 'dialog' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
-OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
-
-/* Static Properties */
-
-/**
- * Symbolic name of dialog.
- *
- * The dialog class must have a symbolic name in order to be registered with OO.Factory.
- * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
- *
- * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
- *
- * @abstract
- * @static
- * @inheritable
- * @property {string}
- */
-OO.ui.Dialog.static.name = '';
-
-/**
- * The dialog title.
- *
- * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
- * that will produce a Label node or string. The title can also be specified with data passed to the
- * constructor (see #getSetupProcess). In this case, the static value will be overridden.
- *
- * @abstract
- * @static
- * @inheritable
- * @property {jQuery|string|Function}
- */
-OO.ui.Dialog.static.title = '';
-
-/**
- * An array of configured {@link OO.ui.ActionWidget action widgets}.
- *
- * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
- * value will be overridden.
- *
- * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
- *
- * @static
- * @inheritable
- * @property {Object[]}
- */
-OO.ui.Dialog.static.actions = [];
-
-/**
- * Close the dialog when the 'Esc' key is pressed.
- *
- * @static
- * @abstract
- * @inheritable
- * @property {boolean}
- */
-OO.ui.Dialog.static.escapable = true;
-
-/* Methods */
-
-/**
- * Handle frame document key down events.
- *
- * @private
- * @param {jQuery.Event} e Key down event
- */
-OO.ui.Dialog.prototype.onDialogKeyDown = function ( e ) {
- if ( e.which === OO.ui.Keys.ESCAPE ) {
- this.executeAction( '' );
- e.preventDefault();
- e.stopPropagation();
- }
-};
-
-/**
- * Handle action resized events.
- *
- * @private
- * @param {OO.ui.ActionWidget} action Action that was resized
- */
-OO.ui.Dialog.prototype.onActionResize = function () {
- // Override in subclass
-};
-
-/**
- * Handle action click events.
- *
- * @private
- * @param {OO.ui.ActionWidget} action Action that was clicked
- */
-OO.ui.Dialog.prototype.onActionClick = function ( action ) {
- if ( !this.isPending() ) {
- this.executeAction( action.getAction() );
- }
-};
-
-/**
- * Handle actions change event.
- *
- * @private
- */
-OO.ui.Dialog.prototype.onActionsChange = function () {
- this.detachActions();
- if ( !this.isClosing() ) {
- this.attachActions();
- }
-};
-
-/**
- * Get the set of actions used by the dialog.
- *
- * @return {OO.ui.ActionSet}
- */
-OO.ui.Dialog.prototype.getActions = function () {
- return this.actions;
-};
-
-/**
- * Get a process for taking action.
- *
- * When you override this method, you can create a new OO.ui.Process and return it, or add additional
- * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
- * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
- *
- * @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.title] Dialog title, omit to use
- * the {@link #static-title static title}
- * @param {Object[]} [data.actions] List of configuration options for each
- * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
- */
-OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
- data = data || {};
-
- // Parent method
- return OO.ui.Dialog.parent.prototype.getSetupProcess.call( this, data )
- .next( function () {
- var config = this.constructor.static,
- actions = data.actions !== undefined ? data.actions : config.actions;
-
- this.title.setLabel(
- data.title !== undefined ? data.title : this.constructor.static.title
- );
- this.actions.add( this.getActionWidgets( actions ) );
-
- if ( this.constructor.static.escapable ) {
- this.$element.on( 'keydown', this.onDialogKeyDownHandler );
- }
- }, this );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
- // Parent method
- return OO.ui.Dialog.parent.prototype.getTeardownProcess.call( this, data )
- .first( function () {
- if ( this.constructor.static.escapable ) {
- this.$element.off( 'keydown', this.onDialogKeyDownHandler );
- }
-
- this.actions.clear();
- this.currentAction = null;
- }, this );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.Dialog.prototype.initialize = function () {
- var titleId;
-
- // Parent method
- OO.ui.Dialog.parent.prototype.initialize.call( this );
-
- titleId = OO.ui.generateElementId();
-
- // Properties
- this.title = new OO.ui.LabelWidget( {
- id: titleId
- } );
-
- // Initialization
- this.$content.addClass( 'oo-ui-dialog-content' );
- this.$element.attr( 'aria-labelledby', titleId );
- this.setPendingElement( this.$head );
-};
-
-/**
- * Get action widgets from a list of configs
- *
- * @param {Object[]} actions Action widget configs
- * @return {OO.ui.ActionWidget[]} Action widgets
- */
-OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
- var i, len, widgets = [];
- for ( i = 0, len = actions.length; i < len; i++ ) {
- widgets.push(
- new OO.ui.ActionWidget( actions[ i ] )
- );
- }
- return widgets;
-};
-
-/**
- * Attach action actions.
- *
- * @protected
- */
-OO.ui.Dialog.prototype.attachActions = function () {
- // Remember the list of potentially attached actions
- this.attachedActions = this.actions.get();
-};
-
-/**
- * Detach action actions.
- *
- * @protected
- * @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();
- this.currentAction = action;
- return this.getActionProcess( action ).execute()
- .always( this.popPending.bind( this ) );
-};
-
-/**
- * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
- * consists of a header that contains the dialog title, a body with the message, and a footer that
- * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
- * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
- *
- * There are two basic types of message dialogs, confirmation and alert:
- *
- * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
- * more details about the consequences.
- * - **alert**: the dialog title describes which event occurred and the message provides more information
- * about why the event occurred.
- *
- * The MessageDialog class specifies two actions: ‘accept’, the primary
- * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
- * passing along the selected action.
- *
- * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
- *
- * @example
- * // Example: Creating and opening a message dialog window.
- * var messageDialog = new OO.ui.MessageDialog();
- *
- * // Create and append a window manager.
- * var windowManager = new OO.ui.WindowManager();
- * $( 'body' ).append( windowManager.$element );
- * windowManager.addWindows( [ messageDialog ] );
- * // Open the window.
- * windowManager.openWindow( messageDialog, {
- * title: 'Basic message dialog',
- * message: 'This is the message'
- * } );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
- *
- * @class
- * @extends OO.ui.Dialog
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
- // Parent constructor
- OO.ui.MessageDialog.parent.call( this, config );
-
- // Properties
- this.verticalActionLayout = null;
-
- // Initialization
- this.$element.addClass( 'oo-ui-messageDialog' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
-
-/* Static Properties */
-
-OO.ui.MessageDialog.static.name = 'message';
-
-OO.ui.MessageDialog.static.size = 'small';
-
-OO.ui.MessageDialog.static.verbose = false;
-
-/**
- * Dialog title.
- *
- * The title of a confirmation dialog describes what a progressive action will do. The
- * title of an alert dialog describes which event occurred.
- *
- * @static
- * @inheritable
- * @property {jQuery|string|Function|null}
- */
-OO.ui.MessageDialog.static.title = null;
-
-/**
- * The message displayed in the dialog body.
- *
- * A confirmation message describes the consequences of a progressive action. An alert
- * message describes why an event occurred.
- *
- * @static
- * @inheritable
- * @property {jQuery|string|Function|null}
- */
-OO.ui.MessageDialog.static.message = null;
-
-// Note that OO.ui.alert() and OO.ui.confirm() rely on these.
-OO.ui.MessageDialog.static.actions = [
- { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
- { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
-];
-
-/* Methods */
-
-/**
- * @inheritdoc
- */
-OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
- OO.ui.MessageDialog.parent.prototype.setManager.call( this, manager );
-
- // Events
- this.manager.connect( this, {
- resize: 'onResize'
- } );
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
- this.fitActions();
- return OO.ui.MessageDialog.parent.prototype.onActionResize.call( this, action );
-};
-
-/**
- * Handle window resized events.
- *
- * @private
- */
-OO.ui.MessageDialog.prototype.onResize = function () {
- var dialog = this;
- dialog.fitActions();
- // Wait for CSS transition to finish and do it again :(
- setTimeout( function () {
- dialog.fitActions();
- }, 300 );
-};
-
-/**
- * Toggle action layout between vertical and horizontal.
- *
- * @private
- * @param {boolean} [value] Layout actions vertically, omit to toggle
- * @chainable
- */
-OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
- value = value === undefined ? !this.verticalActionLayout : !!value;
-
- if ( value !== this.verticalActionLayout ) {
- this.verticalActionLayout = value;
- this.$actions
- .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
- .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
- }
-
- return this;
-};
-
-/**
- * @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.parent.prototype.getActionProcess.call( this, action );
-};
-
-/**
- * @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 || {};
-
- // Parent method
- return OO.ui.MessageDialog.parent.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 );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MessageDialog.prototype.getReadyProcess = function ( data ) {
- data = data || {};
-
- // Parent method
- return OO.ui.MessageDialog.parent.prototype.getReadyProcess.call( this, data )
- .next( function () {
- // Focus the primary action button
- var actions = this.actions.get();
- actions = actions.filter( function ( action ) {
- return action.getFlags().indexOf( 'primary' ) > -1;
- } );
- if ( actions.length > 0 ) {
- actions[ 0 ].$button.focus();
- }
- }, this );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MessageDialog.prototype.getBodyHeight = function () {
- var bodyHeight, oldOverflow,
- $scrollable = this.container.$element;
-
- oldOverflow = $scrollable[ 0 ].style.overflow;
- $scrollable[ 0 ].style.overflow = 'hidden';
-
- OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
-
- bodyHeight = this.text.$element.outerHeight( true );
- $scrollable[ 0 ].style.overflow = oldOverflow;
-
- return bodyHeight;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
- var $scrollable = this.container.$element;
- OO.ui.MessageDialog.parent.prototype.setDimensions.call( this, dim );
-
- // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
- // Need to do it after transition completes (250ms), add 50ms just in case.
- setTimeout( function () {
- var oldOverflow = $scrollable[ 0 ].style.overflow;
- $scrollable[ 0 ].style.overflow = 'hidden';
-
- OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
-
- $scrollable[ 0 ].style.overflow = oldOverflow;
- }, 300 );
-
- return this;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MessageDialog.prototype.initialize = function () {
- // Parent method
- OO.ui.MessageDialog.parent.prototype.initialize.call( this );
-
- // Properties
- this.$actions = $( '<div>' );
- this.container = new OO.ui.PanelLayout( {
- scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
- } );
- this.text = new OO.ui.PanelLayout( {
- padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
- } );
- this.message = new OO.ui.LabelWidget( {
- classes: [ 'oo-ui-messageDialog-message' ]
- } );
-
- // Initialization
- this.title.$element.addClass( 'oo-ui-messageDialog-title' );
- this.$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 );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MessageDialog.prototype.attachActions = function () {
- var i, len, other, special, others;
-
- // Parent method
- OO.ui.MessageDialog.parent.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 );
- }
-
- if ( !this.isOpening() ) {
- // If the dialog is currently opening, this will be called automatically soon.
- // This also calls #fitActions.
- this.updateSize();
- }
-};
-
-/**
- * Fit action actions into columns or rows.
- *
- * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
- *
- * @private
- */
-OO.ui.MessageDialog.prototype.fitActions = function () {
- var i, len, action,
- previous = this.verticalActionLayout,
- 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;
- }
- }
-
- // Move the body out of the way of the foot
- this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
-
- if ( this.verticalActionLayout !== previous ) {
- // We changed the layout, window height might need to be updated.
- this.updateSize();
- }
-};
-
-/**
- * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
- * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
- * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
- * relevant. The ProcessDialog class is always extended and customized with the actions and content
- * required for each process.
- *
- * The process dialog box consists of a header that visually represents the ‘working’ state of long
- * processes with an animation. The header contains the dialog title as well as
- * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
- * a ‘primary’ action on the right (e.g., ‘Done’).
- *
- * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
- * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
- *
- * @example
- * // Example: Creating and opening a process dialog window.
- * function MyProcessDialog( config ) {
- * MyProcessDialog.parent.call( this, config );
- * }
- * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
- *
- * MyProcessDialog.static.title = 'Process dialog';
- * MyProcessDialog.static.actions = [
- * { action: 'save', label: 'Done', flags: 'primary' },
- * { label: 'Cancel', flags: 'safe' }
- * ];
- *
- * MyProcessDialog.prototype.initialize = function () {
- * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
- * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
- * this.content.$element.append( '<p>This is a process dialog window. The header contains the title and two buttons: \'Cancel\' (a safe action) on the left and \'Done\' (a primary action) on the right.</p>' );
- * this.$body.append( this.content.$element );
- * };
- * MyProcessDialog.prototype.getActionProcess = function ( action ) {
- * var dialog = this;
- * if ( action ) {
- * return new OO.ui.Process( function () {
- * dialog.close( { action: action } );
- * } );
- * }
- * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
- * };
- *
- * var windowManager = new OO.ui.WindowManager();
- * $( 'body' ).append( windowManager.$element );
- *
- * var dialog = new MyProcessDialog();
- * windowManager.addWindows( [ dialog ] );
- * windowManager.openWindow( dialog );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
- *
- * @abstract
- * @class
- * @extends OO.ui.Dialog
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
- // Parent constructor
- OO.ui.ProcessDialog.parent.call( this, config );
-
- // Properties
- this.fitOnOpen = false;
-
- // Initialization
- this.$element.addClass( 'oo-ui-processDialog' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
-
-/* Methods */
-
-/**
- * Handle dismiss button click events.
- *
- * Hides errors.
- *
- * @private
- */
-OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
- this.hideErrors();
-};
-
-/**
- * Handle retry button click events.
- *
- * Hides errors and then tries again.
- *
- * @private
- */
-OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
- this.hideErrors();
- this.executeAction( this.currentAction );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
- if ( this.actions.isSpecial( action ) ) {
- this.fitLabel();
- }
- return OO.ui.ProcessDialog.parent.prototype.onActionResize.call( this, action );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ProcessDialog.prototype.initialize = function () {
- // Parent method
- OO.ui.ProcessDialog.parent.prototype.initialize.call( this );
-
- // Properties
- this.$navigation = $( '<div>' );
- this.$location = $( '<div>' );
- this.$safeActions = $( '<div>' );
- this.$primaryActions = $( '<div>' );
- this.$otherActions = $( '<div>' );
- this.dismissButton = new OO.ui.ButtonWidget( {
- label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
- } );
- this.retryButton = new OO.ui.ButtonWidget();
- this.$errors = $( '<div>' );
- this.$errorsTitle = $( '<div>' );
-
- // 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 oo-ui-element-hidden' )
- .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
- this.$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 );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ProcessDialog.prototype.getActionWidgets = function ( actions ) {
- var i, len, widgets = [];
- for ( i = 0, len = actions.length; i < len; i++ ) {
- widgets.push(
- new OO.ui.ActionWidget( $.extend( { framed: true }, actions[ i ] ) )
- );
- }
- return widgets;
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ProcessDialog.prototype.attachActions = function () {
- var i, len, other, special, others;
-
- // Parent method
- OO.ui.ProcessDialog.parent.prototype.attachActions.call( this );
-
- special = this.actions.getSpecial();
- others = this.actions.getOthers();
- if ( special.primary ) {
- this.$primaryActions.append( special.primary.$element );
- }
- for ( i = 0, len = others.length; i < len; i++ ) {
- other = others[ i ];
- this.$otherActions.append( other.$element );
- }
- if ( special.safe ) {
- this.$safeActions.append( special.safe.$element );
- }
-
- this.fitLabel();
- this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
- var process = this;
- return OO.ui.ProcessDialog.parent.prototype.executeAction.call( this, action )
- .fail( function ( errors ) {
- process.showErrors( errors || [] );
- } );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ProcessDialog.prototype.setDimensions = function () {
- // Parent method
- OO.ui.ProcessDialog.parent.prototype.setDimensions.apply( this, arguments );
-
- this.fitLabel();
-};
-
-/**
- * Fit label between actions.
- *
- * @private
- * @chainable
- */
-OO.ui.ProcessDialog.prototype.fitLabel = function () {
- var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth,
- size = this.getSizeProperties();
-
- if ( typeof size.width !== 'number' ) {
- if ( this.isOpened() ) {
- navigationWidth = this.$head.width() - 20;
- } else if ( this.isOpening() ) {
- if ( !this.fitOnOpen ) {
- // Size is relative and the dialog isn't open yet, so wait.
- this.manager.opening.done( this.fitLabel.bind( this ) );
- this.fitOnOpen = true;
- }
- return;
- } else {
- return;
- }
- } else {
- navigationWidth = size.width - 20;
- }
-
- safeWidth = this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0;
- primaryWidth = this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0;
- biggerWidth = Math.max( safeWidth, primaryWidth );
-
- labelWidth = this.title.$element.width();
-
- if ( 2 * biggerWidth + labelWidth < navigationWidth ) {
- // We have enough space to center the label
- leftWidth = rightWidth = biggerWidth;
- } else {
- // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
- if ( this.getDir() === 'ltr' ) {
- leftWidth = safeWidth;
- rightWidth = primaryWidth;
- } else {
- leftWidth = primaryWidth;
- rightWidth = safeWidth;
- }
- }
-
- this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
-
- return this;
-};
-
-/**
- * Handle errors that occurred during accept or reject processes.
- *
- * @private
- * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
- */
-OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
- var i, len, $item, actions,
- items = [],
- abilities = {},
- recoverable = true,
- warning = false;
-
- if ( errors instanceof OO.ui.Error ) {
- errors = [ errors ];
- }
-
- for ( i = 0, len = errors.length; i < len; i++ ) {
- if ( !errors[ i ].isRecoverable() ) {
- recoverable = false;
- }
- if ( errors[ i ].isWarning() ) {
- warning = true;
- }
- $item = $( '<div>' )
- .addClass( 'oo-ui-processDialog-error' )
- .append( errors[ i ].getMessage() );
- items.push( $item[ 0 ] );
- }
- this.$errorItems = $( items );
- if ( recoverable ) {
- abilities[ this.currentAction ] = true;
- // Copy the flags from the first matching action
- actions = this.actions.get( { actions: this.currentAction } );
- if ( actions.length ) {
- this.retryButton.clearFlags().setFlags( actions[ 0 ].getFlags() );
- }
- } else {
- abilities[ this.currentAction ] = false;
- this.actions.setAbilities( abilities );
- }
- if ( warning ) {
- this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
- } else {
- this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
- }
- this.retryButton.toggle( recoverable );
- this.$errorsTitle.after( this.$errorItems );
- this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
-};
-
-/**
- * Hide errors.
- *
- * @private
- */
-OO.ui.ProcessDialog.prototype.hideErrors = function () {
- this.$errors.addClass( 'oo-ui-element-hidden' );
- if ( this.$errorItems ) {
- this.$errorItems.remove();
- this.$errorItems = null;
- }
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
- // Parent method
- return OO.ui.ProcessDialog.parent.prototype.getTeardownProcess.call( this, data )
- .first( function () {
- // Make sure to hide errors
- this.hideErrors();
- this.fitOnOpen = false;
- }, this );
-};
-
-/**
- * @class OO.ui
- */
-
-/**
- * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
- * OO.ui.confirm.
- *
- * @private
- * @return {OO.ui.WindowManager}
- */
-OO.ui.getWindowManager = function () {
- if ( !OO.ui.windowManager ) {
- OO.ui.windowManager = new OO.ui.WindowManager();
- $( 'body' ).append( OO.ui.windowManager.$element );
- OO.ui.windowManager.addWindows( {
- messageDialog: new OO.ui.MessageDialog()
- } );
- }
- return OO.ui.windowManager;
-};
-
-/**
- * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
- * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
- * has only one action button, labelled "OK", clicking it will simply close the dialog.
- *
- * A window manager is created automatically when this function is called for the first time.
- *
- * @example
- * OO.ui.alert( 'Something happened!' ).done( function () {
- * console.log( 'User closed the dialog.' );
- * } );
- *
- * @param {jQuery|string} text Message text to display
- * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
- * @return {jQuery.Promise} Promise resolved when the user closes the dialog
- */
-OO.ui.alert = function ( text, options ) {
- return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
- message: text,
- verbose: true,
- actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ]
- }, options ) ).then( function ( opened ) {
- return opened.then( function ( closing ) {
- return closing.then( function () {
- return $.Deferred().resolve();
- } );
- } );
- } );
-};
-
-/**
- * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
- * the rest of the page will be dimmed out and the user won't be able to interact with it. The
- * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
- * (labelled "Cancel").
- *
- * A window manager is created automatically when this function is called for the first time.
- *
- * @example
- * OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
- * if ( confirmed ) {
- * console.log( 'User clicked "OK"!' );
- * } else {
- * console.log( 'User clicked "Cancel" or closed the dialog.' );
- * }
- * } );
- *
- * @param {jQuery|string} text Message text to display
- * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
- * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
- * confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
- * `false`.
- */
-OO.ui.confirm = function ( text, options ) {
- return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
- message: text,
- verbose: true
- }, options ) ).then( function ( opened ) {
- return opened.then( function ( closing ) {
- return closing.then( function ( data ) {
- return $.Deferred().resolve( !!( data && data.action === 'accept' ) );
- } );
- } );
- } );
-};
-
-}( OO ) );