Bartosz Dziewoński has uploaded a new change for review.
https://gerrit.wikimedia.org/r/291708
Change subject: Implement MultiselectWidget, CheckboxMultiselectWidget and
CheckboxMultiselectInputWidget
......................................................................
Implement MultiselectWidget, CheckboxMultiselectWidget and
CheckboxMultiselectInputWidget
* MultiselectWidget is an abstract class representing a widget which
allows the user to select multiple items from a list of predefined
options.
* CheckboxMultiselectWidget is an implementation of it which uses a
set of checkboxes for the interface.
* CheckboxMultiselectInputWidget is a wrapper around it which allows
the selected checkbox values to be submitted with an HTML form.
It's also available in the PHP version of OOUI.
Stylesheets and the PHP code are adapted from RadioSelectWidget and
RadioSelectInputWidget.
I am quite unhappy how many new classes and how much duplicated code
it took to implement this, but I did not find a sensible and
consistent way to do it more simply. Hopefully this will pay off at
least a little when we make CapsuleMultiselectWidget conform to the
MultiselectWidget interface (T108489).
SelectWidget/OptionWidget and MultiselectWidget/MultioptionWidget are
pretty much unrelated. SelectWidget is coded with only a single
selected option in mind and it didn't make sense to reuse it.
MultioptionWidget is a very silly name.
Bug: T117782
Change-Id: I6f9fad0a462b43ac45c117cd3a3e11c36781cf11
---
M bin/testsuitegenerator.rb
M build/modules.yaml
M demos/pages/widgets.js
M demos/pages/widgets.php
A php/widgets/CheckboxMultiselectInputWidget.php
M src/styles/core.less
M src/styles/empty-theme.less
A src/styles/widgets/CheckboxMultioptionWidget.less
A src/styles/widgets/CheckboxMultiselectInputWidget.less
A src/styles/widgets/CheckboxMultiselectWidget.less
A src/styles/widgets/MultioptionWidget.less
A src/styles/widgets/MultiselectWidget.less
M src/themes/apex/widgets.less
M src/themes/blank/widgets.less
M src/themes/mediawiki/widgets.less
A src/widgets/CheckboxMultioptionWidget.js
A src/widgets/CheckboxMultiselectInputWidget.js
A src/widgets/CheckboxMultiselectWidget.js
A src/widgets/MultioptionWidget.js
A src/widgets/MultiselectWidget.js
20 files changed, 933 insertions(+), 1 deletion(-)
git pull ssh://gerrit.wikimedia.org:29418/oojs/ui refs/changes/08/291708/1
diff --git a/bin/testsuitegenerator.rb b/bin/testsuitegenerator.rb
index dffeb98..c925187 100644
--- a/bin/testsuitegenerator.rb
+++ b/bin/testsuitegenerator.rb
@@ -14,7 +14,8 @@
tests = []
classes = php.select{|c| class_names.include? c[:name] }
- untestable_classes = %w[DropdownInputWidget ComboBoxInputWidget
RadioSelectInputWidget]
+ untestable_classes = %w[DropdownInputWidget ComboBoxInputWidget
+ RadioSelectInputWidget CheckboxMultiselectInputWidget]
testable_classes = classes
.reject{|c| c[:abstract] } # can't test abstract classes
.reject{|c| !c[:parent] || c[:trait] || c[:parent] == 'Theme' }
# can't test abstract
diff --git a/build/modules.yaml b/build/modules.yaml
index c5eafdc..795c62c 100644
--- a/build/modules.yaml
+++ b/build/modules.yaml
@@ -56,6 +56,11 @@
# RadioSelectInputWidget
"src/widgets/RadioOptionWidget.js",
"src/widgets/RadioSelectWidget.js",
+ # CheckboxMultiselectInputWidget
+ "src/widgets/MultioptionWidget.js",
+ "src/widgets/MultiselectWidget.js",
+ "src/widgets/CheckboxMultioptionWidget.js",
+ "src/widgets/CheckboxMultiselectWidget.js",
# ComboBoxInputWidget
"src/mixins/FloatableElement.js",
"src/widgets/FloatingMenuSelectWidget.js",
@@ -67,6 +72,7 @@
"src/widgets/DropdownInputWidget.js",
"src/widgets/RadioInputWidget.js",
"src/widgets/RadioSelectInputWidget.js",
+ "src/widgets/CheckboxMultiselectInputWidget.js",
"src/widgets/TextInputWidget.js",
"src/widgets/ComboBoxInputWidget.js",
"src/layouts/FieldLayout.js",
diff --git a/demos/pages/widgets.js b/demos/pages/widgets.js
index 29a33d5..17c822d 100644
--- a/demos/pages/widgets.js
+++ b/demos/pages/widgets.js
@@ -727,6 +727,29 @@
}
),
new OO.ui.FieldLayout(
+ new OO.ui.CheckboxMultiselectWidget( {
+ items: [
+ new
OO.ui.CheckboxMultioptionWidget( {
+ data: 'cat',
+ label: 'Cat'
+ } ),
+ new
OO.ui.CheckboxMultioptionWidget( {
+ data: 'dog',
+ label: 'Dog'
+ } ),
+ new
OO.ui.CheckboxMultioptionWidget( {
+ data:
'goldfish',
+ label:
'Goldfish',
+ disabled: true
+ } )
+ ]
+ } ),
+ {
+ align: 'top',
+ label:
'CheckboxMultiselectWidget'
+ }
+ ),
+ new OO.ui.FieldLayout(
new OO.ui.RadioSelectInputWidget( {
value: 'dog',
options: [
@@ -750,6 +773,29 @@
}
),
new OO.ui.FieldLayout(
+ new
OO.ui.CheckboxMultiselectInputWidget( {
+ value: [ 'dog', 'cat' ],
+ options: [
+ {
+ data: 'cat',
+ label: 'Cat'
+ },
+ {
+ data: 'dog',
+ label: 'Dog'
+ },
+ {
+ data:
'goldfish',
+ label:
'Goldfish'
+ }
+ ]
+ } ),
+ {
+ align: 'top',
+ label:
'CheckboxMultiselectInputWidget'
+ }
+ ),
+ new OO.ui.FieldLayout(
new OO.ui.NumberInputWidget(),
{
label: 'NumberInputWidget',
diff --git a/demos/pages/widgets.php b/demos/pages/widgets.php
index 903ff25..d34d30a 100644
--- a/demos/pages/widgets.php
+++ b/demos/pages/widgets.php
@@ -451,6 +451,29 @@
]
),
new OOUI\FieldLayout(
+ new OOUI\CheckboxMultiselectInputWidget( [
+ 'value' => [ 'dog', 'cat' ],
+ 'options' => [
+ [
+ 'data' => 'cat',
+ 'label' => 'Cat'
+ ],
+ [
+ 'data' => 'dog',
+ 'label' => 'Dog'
+ ],
+ [
+ 'data' => 'goldfish',
+ 'label' => 'Goldfish'
+ ],
+ ]
+ ] ),
+ [
+ 'align' => 'top',
+ 'label' => 'CheckboxMultiselectInputWidget',
+ ]
+ ),
+ new OOUI\FieldLayout(
new OOUI\TextInputWidget( [ 'value' => 'Text input' ] ),
[
'label' => "TextInputWidget\xE2\x80\x8E",
diff --git a/php/widgets/CheckboxMultiselectInputWidget.php
b/php/widgets/CheckboxMultiselectInputWidget.php
new file mode 100644
index 0000000..a9b8da4
--- /dev/null
+++ b/php/widgets/CheckboxMultiselectInputWidget.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace OOUI;
+
+/**
+ * Multiple checkbox input widget. Intended to be used within a
OO.ui.FormLayout.
+ */
+class CheckboxMultiselectInputWidget extends InputWidget {
+
+ /* Static Properties */
+
+ public static $supportsSimpleLabel = false;
+
+ /* Properties */
+
+ /**
+ * @var string|null
+ */
+ protected $name = null;
+
+ /**
+ * Input value.
+ *
+ * @var string[]
+ */
+ protected $value = [];
+
+ /**
+ * Layouts for this input, as FieldLayouts.
+ *
+ * @var array
+ */
+ protected $fields = [];
+
+ /**
+ * @param array $config Configuration options
+ * @param array[] $config['options'] Array of menu options in the format
+ * `array( 'data' => …, 'label' => … )`
+ */
+ public function __construct( array $config = [] ) {
+ // Parent constructor
+ parent::__construct( $config );
+
+ if ( isset( $config['name'] ) ) {
+ $this->name = $config['name'];
+ }
+
+ // Initialization
+ $this->setOptions( isset( $config['options'] ) ?
$config['options'] : [] );
+ // Have to repeat this from parent, as we need options to be
set up for this to make sense
+ $this->setValue( isset( $config['value'] ) ? $config['value'] :
null );
+ $this->addClasses( [ 'oo-ui-checkboxMultiselectInputWidget' ] );
+ }
+
+ protected function getInputElement( $config ) {
+ // Actually unused
+ return new Tag( 'div' );
+ }
+
+ /**
+ * Set the value of the input.
+ *
+ * @param string[] $value New value
+ * @return $this
+ */
+ public function setValue( $value ) {
+ $this->value = $this->cleanUpValue( $value );
+ // Deselect all options
+ foreach ( $this->fields as $field ) {
+ $field->getField()->setSelected( false );
+ }
+ // Select the requested ones
+ foreach ( $this->value as $key ) {
+ $this->fields[ $key ]->getField()->setSelected( true );
+ }
+ return $this;
+ }
+
+ /**
+ * Clean up incoming value.
+ *
+ * @param string[] $value Original value
+ * @return string[] Cleaned up value
+ */
+ protected function cleanUpValue( $value ) {
+ $cleanValue = [];
+ if ( !is_array( $value ) ) {
+ return $cleanValue;
+ }
+ foreach ( $value as $singleValue ) {
+ $singleValue = parent::cleanUpValue( $singleValue );
+ // Remove options that we don't have here
+ if ( !isset( $this->fields[ $singleValue ] ) ) {
+ continue;
+ }
+ $cleanValue[] = $singleValue;
+ }
+ return $cleanValue;
+ }
+
+ /**
+ * Set the options available for this input.
+ *
+ * @param array[] $options Array of menu options in the format
+ * `array( 'data' => …, 'label' => … )`
+ * @return $this
+ */
+ public function setOptions( $options ) {
+ $this->fields = [];
+
+ // Rebuild the checkboxes
+ $this->clearContent();
+ $name = $this->name;
+ foreach ( $options as $opt ) {
+ $optValue = parent::cleanUpValue( $opt['data'] );
+ $field = new FieldLayout(
+ new CheckboxInputWidget( [
+ 'name' => $name,
+ 'value' => $optValue,
+ 'disabled' => $this->isDisabled(),
+ ] ),
+ [
+ 'label' => isset( $opt['label'] ) ?
$opt['label'] : $optValue,
+ 'align' => 'inline',
+ ]
+ );
+
+ $this->fields[ $optValue ] = $field;
+ $this->appendContent( $field );
+ }
+
+ // Re-set the value, checking the checkboxes as needed.
+ // This will also get rid of any stale options that we just
removed.
+ $this->setValue( $this->getValue() );
+
+ return $this;
+ }
+
+ public function setDisabled( $state ) {
+ parent::setDisabled( $state );
+ foreach ( $this->fields as $field ) {
+ $field->getField()->setDisabled( $this->isDisabled() );
+ }
+ return $this;
+ }
+
+ public function getConfig( &$config ) {
+ $o = [];
+ foreach ( $this->fields as $field ) {
+ $label = $field->getLabel();
+ $data = $field->getField()->getValue();
+ $o[] = [ 'data' => $data, 'label' => $label ];
+ }
+ $config['options'] = $o;
+ return parent::getConfig( $config );
+ }
+}
diff --git a/src/styles/core.less b/src/styles/core.less
index 7ac2d02..d17c3bd 100644
--- a/src/styles/core.less
+++ b/src/styles/core.less
@@ -42,6 +42,7 @@
@import 'widgets/InputWidget.less';
@import 'widgets/ButtonInputWidget.less';
@import 'widgets/CheckboxInputWidget.less';
+@import 'widgets/CheckboxMultiselectInputWidget.less';
@import 'widgets/DropdownInputWidget.less';
@import 'widgets/RadioInputWidget.less';
@import 'widgets/RadioSelectInputWidget.less';
@@ -52,3 +53,7 @@
@import 'widgets/FloatingMenuSelectWidget.less';
@import 'widgets/DropdownWidget.less';
@import 'widgets/ComboBoxInputWidget.less';
+@import 'widgets/MultiselectWidget.less';
+@import 'widgets/MultioptionWidget.less';
+@import 'widgets/CheckboxMultiselectWidget.less';
+@import 'widgets/CheckboxMultioptionWidget.less';
diff --git a/src/styles/empty-theme.less b/src/styles/empty-theme.less
index 966865d..3134697 100644
--- a/src/styles/empty-theme.less
+++ b/src/styles/empty-theme.less
@@ -69,6 +69,7 @@
.theme-oo-ui-inputWidget () {}
.theme-oo-ui-buttonInputWidget () {}
.theme-oo-ui-checkboxInputWidget () {}
+.theme-oo-ui-checkboxMultiselectInputWidget () {}
.theme-oo-ui-dropdownInputWidget () {}
.theme-oo-ui-radioInputWidget () {}
.theme-oo-ui-radioSelectInputWidget () {}
@@ -96,3 +97,7 @@
.theme-oo-ui-tabSelectWidget () {}
.theme-oo-ui-numberInputWidget () {}
.theme-oo-ui-toggleSwitchWidget () {}
+.theme-oo-ui-multiselectWidget () {}
+.theme-oo-ui-multioptionWidget () {}
+.theme-oo-ui-checkboxMultiselectWidget () {}
+.theme-oo-ui-checkboxMultioptionWidget () {}
diff --git a/src/styles/widgets/CheckboxMultioptionWidget.less
b/src/styles/widgets/CheckboxMultioptionWidget.less
new file mode 100644
index 0000000..9314071
--- /dev/null
+++ b/src/styles/widgets/CheckboxMultioptionWidget.less
@@ -0,0 +1,13 @@
+@import '../common';
+
+.oo-ui-checkboxMultioptionWidget {
+ cursor: default;
+
+ .oo-ui-checkboxInputWidget,
+ &.oo-ui-labelElement .oo-ui-labelElement-label {
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ .theme-oo-ui-checkboxMultioptionWidget();
+}
diff --git a/src/styles/widgets/CheckboxMultiselectInputWidget.less
b/src/styles/widgets/CheckboxMultiselectInputWidget.less
new file mode 100644
index 0000000..7de12e4
--- /dev/null
+++ b/src/styles/widgets/CheckboxMultiselectInputWidget.less
@@ -0,0 +1,5 @@
+@import '../common';
+
+.oo-ui-checkboxMultiselectInputWidget {
+ .theme-oo-ui-checkboxMultiselectInputWidget();
+}
diff --git a/src/styles/widgets/CheckboxMultiselectWidget.less
b/src/styles/widgets/CheckboxMultiselectWidget.less
new file mode 100644
index 0000000..125140b
--- /dev/null
+++ b/src/styles/widgets/CheckboxMultiselectWidget.less
@@ -0,0 +1,5 @@
+@import '../common';
+
+.oo-ui-checkboxMultiselectWidget {
+ .theme-oo-ui-checkboxMultiselectWidget();
+}
diff --git a/src/styles/widgets/MultioptionWidget.less
b/src/styles/widgets/MultioptionWidget.less
new file mode 100644
index 0000000..d97b418
--- /dev/null
+++ b/src/styles/widgets/MultioptionWidget.less
@@ -0,0 +1,22 @@
+@import '../common';
+
+.oo-ui-multioptionWidget {
+ position: relative;
+ display: block;
+
+ &.oo-ui-widget-enabled {
+ cursor: pointer;
+ }
+ &.oo-ui-widget-disabled {
+ cursor: default;
+ }
+
+ &.oo-ui-labelElement .oo-ui-labelElement-label {
+ display: block;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ .theme-oo-ui-multioptionWidget();
+}
diff --git a/src/styles/widgets/MultiselectWidget.less
b/src/styles/widgets/MultiselectWidget.less
new file mode 100644
index 0000000..d1611ab
--- /dev/null
+++ b/src/styles/widgets/MultiselectWidget.less
@@ -0,0 +1,5 @@
+@import '../common';
+
+.oo-ui-multiselectWidget {
+ .theme-oo-ui-multiselectWidget();
+}
diff --git a/src/themes/apex/widgets.less b/src/themes/apex/widgets.less
index 8f30907..b429eb7 100644
--- a/src/themes/apex/widgets.less
+++ b/src/themes/apex/widgets.less
@@ -325,6 +325,12 @@
.theme-oo-ui-checkboxInputWidget () {}
+.theme-oo-ui-checkboxMultiselectInputWidget () {
+ .oo-ui-fieldLayout {
+ margin-bottom: 0;
+ }
+}
+
.theme-oo-ui-dropdownInputWidget () {
width: 100%;
max-width: @max-width-input-default;
@@ -1067,3 +1073,29 @@
.oo-ui-progressBarWidget-slide-frames;
}
}
+
+.theme-oo-ui-multiselectWidget () {}
+
+.theme-oo-ui-multioptionWidget () {
+ .oo-ui-labelElement-label {
+ line-height: 1.5em;
+ }
+
+ &.oo-ui-widget-disabled {
+ color: #ccc;
+ }
+}
+
+.theme-oo-ui-checkboxMultiselectWidget () {}
+
+.theme-oo-ui-checkboxMultioptionWidget () {
+ padding: 0;
+
+ &.oo-ui-labelElement .oo-ui-labelElement-label {
+ padding-left: 0.5em;
+ }
+
+ .oo-ui-checkboxInputWidget {
+ margin-right: 0;
+ }
+}
diff --git a/src/themes/blank/widgets.less b/src/themes/blank/widgets.less
index cbc3daa..5c10ca1 100644
--- a/src/themes/blank/widgets.less
+++ b/src/themes/blank/widgets.less
@@ -30,6 +30,8 @@
.theme-oo-ui-checkboxInputWidget () {}
+.theme-oo-ui-checkboxMultiselectInputWidget () {}
+
.theme-oo-ui-dropdownInputWidget () {}
.theme-oo-ui-radioInputWidget () {}
@@ -85,3 +87,11 @@
.theme-oo-ui-toggleSwitchWidget () {}
.theme-oo-ui-progressBarWidget () {}
+
+.theme-oo-ui-multiselectWidget () {}
+
+.theme-oo-ui-multioptionWidget () {}
+
+.theme-oo-ui-checkboxMultiselectWidget () {}
+
+.theme-oo-ui-checkboxMultioptionWidget () {}
diff --git a/src/themes/mediawiki/widgets.less
b/src/themes/mediawiki/widgets.less
index bf80133..c314a6e 100644
--- a/src/themes/mediawiki/widgets.less
+++ b/src/themes/mediawiki/widgets.less
@@ -424,6 +424,20 @@
}
}
+.theme-oo-ui-checkboxMultiselectInputWidget () {
+ .oo-ui-fieldLayout {
+ margin-bottom: 0;
+
+ .oo-ui-fieldLayout-body {
+ padding: 0.25em 0;
+
+ .oo-ui-labelElement-label {
+ line-height: 1.5em;
+ }
+ }
+ }
+}
+
.theme-oo-ui-dropdownInputWidget () {
width: 100%;
max-width: @max-width-input-default;
@@ -1369,3 +1383,29 @@
.oo-ui-progressBarWidget-slide-frames;
}
}
+
+.theme-oo-ui-multiselectWidget () {}
+
+.theme-oo-ui-multioptionWidget () {
+ .oo-ui-labelElement-label {
+ line-height: 1.5em;
+ }
+
+ &.oo-ui-widget-disabled {
+ color: @color-disabled;
+ }
+}
+
+.theme-oo-ui-checkboxMultiselectWidget () {}
+
+.theme-oo-ui-checkboxMultioptionWidget () {
+ padding: 0.25em 0;
+
+ &.oo-ui-labelElement .oo-ui-labelElement-label {
+ padding: 0.25em 0.25em 0.25em 1em;
+ }
+
+ .oo-ui-checkboxInputWidget {
+ margin-right: 0;
+ }
+}
diff --git a/src/widgets/CheckboxMultioptionWidget.js
b/src/widgets/CheckboxMultioptionWidget.js
new file mode 100644
index 0000000..d570476
--- /dev/null
+++ b/src/widgets/CheckboxMultioptionWidget.js
@@ -0,0 +1,99 @@
+/**
+ * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
+ * The class is used with OO.ui.CheckboxMultiselectWidget to create a
selection of checkbox 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.MultioptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.CheckboxMultioptionWidget = function OoUiCheckboxMultioptionWidget(
config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties (must be done before parent constructor which calls
#setDisabled)
+ this.checkbox = new OO.ui.CheckboxInputWidget();
+
+ // Parent constructor
+ OO.ui.CheckboxMultioptionWidget.parent.call( this, config );
+
+ // Events
+ this.checkbox.on( 'change', this.onCheckboxChange.bind( this ) );
+ this.$element.on( 'keydown', this.onKeyDown.bind( this ) );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-checkboxMultioptionWidget' )
+ .prepend( this.checkbox.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.CheckboxMultioptionWidget, OO.ui.MultioptionWidget );
+
+/* Static Properties */
+
+OO.ui.CheckboxMultioptionWidget.static.tagName = 'label';
+
+/* Methods */
+
+/**
+ * Handle checkbox selected state change.
+ *
+ * @private
+ */
+OO.ui.CheckboxMultioptionWidget.prototype.onCheckboxChange = function () {
+ this.setSelected( this.checkbox.isSelected() );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CheckboxMultioptionWidget.prototype.setSelected = function ( state ) {
+ OO.ui.CheckboxMultioptionWidget.parent.prototype.setSelected.call(
this, state );
+ this.checkbox.setSelected( state );
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CheckboxMultioptionWidget.prototype.setDisabled = function ( disabled ) {
+ OO.ui.CheckboxMultioptionWidget.parent.prototype.setDisabled.call(
this, disabled );
+ this.checkbox.setDisabled( this.isDisabled() );
+ return this;
+};
+
+/**
+ * Focus the widget.
+ */
+OO.ui.CheckboxMultioptionWidget.prototype.focus = function () {
+ this.checkbox.focus();
+};
+
+/**
+ * Handle key down events.
+ *
+ * @protected
+ * @param {jQuery.Event} e
+ */
+OO.ui.CheckboxMultioptionWidget.prototype.onKeyDown = function ( e ) {
+ var
+ element = this.getElementGroup(),
+ nextItem;
+
+ if ( e.keyCode === OO.ui.Keys.LEFT || e.keyCode === OO.ui.Keys.UP ) {
+ nextItem = element.getRelativeFocusableItem( this, -1 );
+ } else if ( e.keyCode === OO.ui.Keys.RIGHT || e.keyCode ===
OO.ui.Keys.DOWN ) {
+ nextItem = element.getRelativeFocusableItem( this, 1 );
+ }
+
+ if ( nextItem ) {
+ e.preventDefault();
+ nextItem.focus();
+ }
+};
diff --git a/src/widgets/CheckboxMultiselectInputWidget.js
b/src/widgets/CheckboxMultiselectInputWidget.js
new file mode 100644
index 0000000..f79762f
--- /dev/null
+++ b/src/widgets/CheckboxMultiselectInputWidget.js
@@ -0,0 +1,179 @@
+/**
+ * CheckboxMultiselectInputWidget is a
+ * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended
to be used within a
+ * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized
with the value of
+ * HTML `<input type=checkbox>` tags. Please see the [OOjs UI documentation on
MediaWiki][1] for
+ * more information about input widgets.
+ *
+ * @example
+ * // Example: A CheckboxMultiselectInputWidget with three options
+ * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
+ * options: [
+ * { data: 'a', label: 'First' },
+ * { data: 'b', label: 'Second'},
+ * { data: 'c', label: 'Third' }
+ * ]
+ * } );
+ * $( 'body' ).append( multiselectInput.$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.CheckboxMultiselectInputWidget = function
OoUiCheckboxMultiselectInputWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties (must be done before parent constructor which calls
#setDisabled)
+ this.checkboxMultiselectWidget = new OO.ui.CheckboxMultiselectWidget();
+
+ // Parent constructor
+ OO.ui.CheckboxMultiselectInputWidget.parent.call( this, config );
+
+ // Properties
+ this.inputName = config.name;
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
+ .append( this.checkboxMultiselectWidget.$element );
+ // We don't use this.$input, but rather the CheckboxInputWidgets inside
each option
+ this.$input.detach();
+ this.setOptions( config.options || [] );
+ // Have to repeat this from parent, as we need options to be set up for
this to make sense
+ this.setValue( config.value );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.CheckboxMultiselectInputWidget, OO.ui.InputWidget );
+
+/* Static Properties */
+
+OO.ui.CheckboxMultiselectInputWidget.static.supportsSimpleLabel = false;
+
+/* Static Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CheckboxMultiselectInputWidget.static.gatherPreInfuseState = function (
node, config ) {
+ var state =
OO.ui.CheckboxMultiselectInputWidget.parent.static.gatherPreInfuseState( node,
config );
+ state.value = $( node ).find( '.oo-ui-checkboxInputWidget
.oo-ui-inputWidget-input:checked' )
+ .toArray().map( function ( el ) { return el.value; } );
+ return state;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CheckboxMultiselectInputWidget.static.reusePreInfuseDOM = function (
node, config ) {
+ config =
OO.ui.CheckboxMultiselectInputWidget.parent.static.reusePreInfuseDOM( node,
config );
+ // Cannot reuse the `<input type=checkbox>` set
+ delete config.$input;
+ return config;
+};
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ * @protected
+ */
+OO.ui.CheckboxMultiselectInputWidget.prototype.getInputElement = function () {
+ // Actually unused
+ return $( '<div>' );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CheckboxMultiselectInputWidget.prototype.getValue = function () {
+ var value = this.$element.find( '.oo-ui-checkboxInputWidget
.oo-ui-inputWidget-input:checked' )
+ .toArray().map( function ( el ) { return el.value; } );
+ if ( this.value !== value ) {
+ this.setValue( value );
+ }
+ return this.value;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CheckboxMultiselectInputWidget.prototype.setValue = function ( value ) {
+ value = this.cleanUpValue( value );
+ this.checkboxMultiselectWidget.selectItemsByData( value );
+ OO.ui.CheckboxMultiselectInputWidget.parent.prototype.setValue.call(
this, value );
+ return this;
+};
+
+/**
+ * Clean up incoming value.
+ *
+ * @param {string[]} value Original value
+ * @return {string[]} Cleaned up value
+ */
+OO.ui.CheckboxMultiselectInputWidget.prototype.cleanUpValue = function ( value
) {
+ var i, singleValue,
+ cleanValue = [];
+ if ( !Array.isArray( value ) ) {
+ return cleanValue;
+ }
+ for ( i = 0; i < value.length; i++ ) {
+ singleValue =
+
OO.ui.CheckboxMultiselectInputWidget.parent.prototype.cleanUpValue.call( this,
value[ i ] );
+ // Remove options that we don't have here
+ if ( !this.checkboxMultiselectWidget.getItemFromData(
singleValue ) ) {
+ continue;
+ }
+ cleanValue.push( singleValue );
+ }
+ return cleanValue;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CheckboxMultiselectInputWidget.prototype.setDisabled = function ( state
) {
+ this.checkboxMultiselectWidget.setDisabled( state );
+ OO.ui.CheckboxMultiselectInputWidget.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.CheckboxMultiselectInputWidget.prototype.setOptions = function ( options
) {
+ var widget = this;
+
+ // Rebuild the checkboxMultiselectWidget menu
+ this.checkboxMultiselectWidget
+ .clearItems()
+ .addItems( options.map( function ( opt ) {
+ var optValue, item;
+ optValue =
+
OO.ui.CheckboxMultiselectInputWidget.parent.prototype.cleanUpValue.call(
widget, opt.data );
+ item = new OO.ui.CheckboxMultioptionWidget( {
+ data: optValue,
+ label: opt.label !== undefined ? opt.label :
optValue
+ } );
+ // Set the 'name' and 'value' for form submission
+ item.checkbox.$input.attr( 'name', widget.inputName );
+ item.checkbox.setValue( optValue );
+ return item;
+ } ) );
+
+ // Re-set the value, checking the checkboxes as needed.
+ // This will also get rid of any stale options that we just removed.
+ this.setValue( this.getValue() );
+
+ return this;
+};
diff --git a/src/widgets/CheckboxMultiselectWidget.js
b/src/widgets/CheckboxMultiselectWidget.js
new file mode 100644
index 0000000..bd79a28
--- /dev/null
+++ b/src/widgets/CheckboxMultiselectWidget.js
@@ -0,0 +1,84 @@
+/**
+ * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect
widget} that contains
+ * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
+ * CheckboxMultiselectWidget 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.CheckboxMultiselectInputWidget instead.
+ *
+ * @example
+ * // A CheckboxMultiselectWidget with CheckboxMultioptions.
+ * var option1 = new OO.ui.CheckboxMultioptionWidget( {
+ * data: 'a',
+ * selected: true,
+ * label: 'Selected checkbox'
+ * } );
+ *
+ * var option2 = new OO.ui.CheckboxMultioptionWidget( {
+ * data: 'b',
+ * label: 'Unselected checkbox'
+ * } );
+ *
+ * var multiselect=new OO.ui.CheckboxMultiselectWidget( {
+ * items: [ option1, option2 ]
+ * } );
+ *
+ * $( 'body' ).append( multiselect.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
+ *
+ * @class
+ * @extends OO.ui.MultiselectWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.CheckboxMultiselectWidget = function OoUiCheckboxMultiselectWidget(
config ) {
+ // Parent constructor
+ OO.ui.CheckboxMultiselectWidget.parent.call( this, config );
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-checkboxMultiselectWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.CheckboxMultiselectWidget, OO.ui.MultiselectWidget );
+
+/* Methods */
+
+/**
+ * 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.CheckboxMultioptionWidget|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
+ * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if
there are no items in the select
+ */
+OO.ui.CheckboxMultiselectWidget.prototype.getRelativeFocusableItem = function
( item, direction ) {
+ var currentIndex, nextIndex, i,
+ increase = direction > 0 ? 1 : -1,
+ len = this.items.length;
+
+ if ( item ) {
+ 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 && !item.isDisabled() ) {
+ return item;
+ }
+ nextIndex = ( nextIndex + increase + len ) % len;
+ }
+ return null;
+};
diff --git a/src/widgets/MultioptionWidget.js b/src/widgets/MultioptionWidget.js
new file mode 100644
index 0000000..646dc75
--- /dev/null
+++ b/src/widgets/MultioptionWidget.js
@@ -0,0 +1,82 @@
+/**
+ * MultioptionWidgets 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.
MultioptionWidgets 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_Multioptions
+ *
+ * @class
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.ItemWidget
+ * @mixins OO.ui.mixin.LabelElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [selected=false] Whether the option is initially selected
+ */
+OO.ui.MultioptionWidget = function OoUiMultioptionWidget( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Parent constructor
+ OO.ui.MultioptionWidget.parent.call( this, config );
+
+ // Mixin constructors
+ OO.ui.mixin.ItemWidget.call( this );
+ OO.ui.mixin.LabelElement.call( this, config );
+
+ // Properties
+ this.selected = null;
+
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-multioptionWidget' )
+ .append( this.$label );
+ this.setSelected( config.selected );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.MultioptionWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.ItemWidget );
+OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.LabelElement );
+
+/* Events */
+
+/**
+ * @event change
+ *
+ * A change event is emitted when the selected state of the option changes.
+ *
+ * @param {boolean} selected Whether the option is now selected
+ */
+
+/* Methods */
+
+/**
+ * Check if the option is selected.
+ *
+ * @return {boolean} Item is selected
+ */
+OO.ui.MultioptionWidget.prototype.isSelected = function () {
+ return this.selected;
+};
+
+/**
+ * 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.MultioptionWidget.prototype.setSelected = function ( state ) {
+ state = !!state;
+ if ( this.selected !== state ) {
+ this.selected = state;
+ this.emit( 'change', state );
+ this.$element.toggleClass( 'oo-ui-multioptionWidget-selected',
state );
+ }
+ return this;
+};
diff --git a/src/widgets/MultiselectWidget.js b/src/widgets/MultiselectWidget.js
new file mode 100644
index 0000000..98c1645
--- /dev/null
+++ b/src/widgets/MultiselectWidget.js
@@ -0,0 +1,113 @@
+/**
+ * MultiselectWidget allows selecting multiple options from a list.
+ *
+ * For more information about menus and options, 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
+ * @abstract
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.GroupWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the
multiselect.
+ */
+OO.ui.MultiselectWidget = function OoUiMultiselectWidget( config ) {
+ // Parent constructor
+ OO.ui.MultiselectWidget.parent.call( this, config );
+
+ // Configuration initialization
+ config = config || {};
+
+ // Mixin constructors
+ OO.ui.mixin.GroupWidget.call( this, config );
+
+ // Events
+ this.aggregate( { change: 'select' } );
+ // This is mostly for compatibility with CapsuleMultiselectWidget...
normally, 'change' is emitted
+ // by GroupElement only when items are added/removed
+ this.connect( this, { select: [ 'emit', 'change' ] } );
+
+ // Initialization
+ if ( config.items ) {
+ this.addItems( config.items );
+ }
+ this.$group.addClass( 'oo-ui-multiselectWidget-group' );
+ this.$element.addClass( 'oo-ui-multiselectWidget' )
+ .append( this.$group );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.MultiselectWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.MultiselectWidget, OO.ui.mixin.GroupWidget );
+
+/* Events */
+
+/**
+ * @event change
+ *
+ * A change event is emitted when the set of items changes, or an item is
selected or deselected.
+ */
+
+/**
+ * @event select
+ *
+ * A select event is emitted when an item is selected or deselected.
+ */
+
+/* Methods */
+
+/**
+ * Get options that are selected.
+ *
+ * @return {OO.ui.MultioptionWidget[]} Selected options
+ */
+OO.ui.MultiselectWidget.prototype.getSelectedItems = function () {
+ return this.items.filter( function ( item ) {
+ return item.isSelected();
+ } );
+};
+
+/**
+ * Get the data of options that are selected.
+ *
+ * @return {Object[]|string[]} Values of selected options
+ */
+OO.ui.MultiselectWidget.prototype.getSelectedItemsData = function () {
+ return this.getSelectedItems().map( function ( item ) {
+ return item.data;
+ } );
+};
+
+/**
+ * Select options by reference. Options not mentioned in the `items` array
will be deselected.
+ *
+ * @param {OO.ui.MultioptionWidget[]} items Items to select
+ * @chainable
+ */
+OO.ui.MultiselectWidget.prototype.selectItems = function ( items ) {
+ this.items.forEach( function ( item ) {
+ var selected = items.indexOf( item ) !== -1;
+ item.setSelected( selected );
+ } );
+ return this;
+};
+
+/**
+ * Select items by their data. Options not mentioned in the `datas` array will
be deselected.
+ *
+ * @param {Object[]|string[]} datas Values of items to select
+ * @chainable
+ */
+OO.ui.MultiselectWidget.prototype.selectItemsByData = function ( datas ) {
+ var items,
+ widget = this;
+ items = datas.map( function ( data ) {
+ return widget.getItemFromData( data );
+ } );
+ this.selectItems( items );
+ return this;
+};
--
To view, visit https://gerrit.wikimedia.org/r/291708
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I6f9fad0a462b43ac45c117cd3a3e11c36781cf11
Gerrit-PatchSet: 1
Gerrit-Project: oojs/ui
Gerrit-Branch: master
Gerrit-Owner: Bartosz Dziewoński <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits