Daniel Werner has submitted this change and it was merged.
Change subject: (bug 48145) Introducing listrotator widget to set time precision
......................................................................
(bug 48145) Introducing listrotator widget to set time precision
The new listrotator jQuery widget is used to set the time value's precision.
Apart from that, the input extender widget is enhanced to better comply with
the additional content.
Change-Id: Iaca1465915ce874bfb864e48dfaaa309a599202b
---
M ValueView/ValueView.i18n.php
M ValueView/ValueView.resources.mw.php
M ValueView/ValueView.resources.php
M ValueView/ValueView.tests.qunit.php
M ValueView/resources/jquery.ui/jquery.ui.inputextender.css
M ValueView/resources/jquery.ui/jquery.ui.inputextender.js
A ValueView/resources/jquery.ui/jquery.ui.listrotator.css
A ValueView/resources/jquery.ui/jquery.ui.listrotator.js
M ValueView/resources/jquery.valueview/valueview.experts/experts.TimeInput.js
A ValueView/tests/qunit/jquery.ui/jquery.ui.listrotator.tests.js
10 files changed, 931 insertions(+), 31 deletions(-)
Approvals:
Daniel Werner: Verified; Looks good to me, approved
jenkins-bot: Verified
diff --git a/ValueView/ValueView.i18n.php b/ValueView/ValueView.i18n.php
index a492cb8..0e625d2 100644
--- a/ValueView/ValueView.i18n.php
+++ b/ValueView/ValueView.i18n.php
@@ -48,6 +48,8 @@
'valueview-inputextender-showoptions' => 'show options',
'valueview-inputextender-hideoptions' => 'hide options',
+
+ 'valueview-listrotator-auto' => 'auto',
);
/** Message documentation (Message documentation)
@@ -65,6 +67,7 @@
'valueview-preview-novalue' => 'Message displayed instead of an input
value\'s preview when no value is specified yet or when the specified value
could not be interpreted by the system.',
'valueview-inputextender-showoptions' => 'Message of the link displayed
next to an input element if there are detailed options for inputting a value.
This message is shown, when the options are currently invisible. By clicking
the link, the options are shown and can be adjusted.',
'valueview-inputextender-hideoptions' => 'Message of the link displayed
next to an input element if there are detailed options for inputting a value.
This message is shown, when the options are currently visible. By clicking the
link, the options will be hidden.',
+ 'valueview-listrotator-auto' => 'Label of the link to have the system
automatically select the most appropriate value from a "listrotator" widget.
The "listrotator" basically is a facade for a drop-down select box allowing to
pick a value from a list of values. In addition to the defined values, an
"automatic" option may be selected that makes the system pick the most
appropriate value according to an associated input element.',
);
/** Asturian (asturianu)
diff --git a/ValueView/ValueView.resources.mw.php
b/ValueView/ValueView.resources.mw.php
index 8b92f6d..462c1a5 100644
--- a/ValueView/ValueView.resources.mw.php
+++ b/ValueView/ValueView.resources.mw.php
@@ -125,6 +125,23 @@
),
),
+
+ 'jquery.ui.listrotator' => $moduleTemplate + array(
+ 'scripts' => array(
+ 'jquery.ui/jquery.ui.listrotator.js',
+ ),
+ 'styles' => array(
+ 'jquery.ui/jquery.ui.listrotator.css',
+ ),
+ 'dependencies' => array(
+ 'jquery.ui.widget',
+ 'jquery.ui.position',
+ 'jquery.ui.autocomplete', // needs
jquery.ui.menu
+ ),
+ 'messages' => array(
+ 'valueview-listrotator-auto',
+ ),
+ ),
);
// return jQuery.valueview's native resources plus those required by
the MW extension:
diff --git a/ValueView/ValueView.resources.php
b/ValueView/ValueView.resources.php
index 93587a4..f9a294d 100644
--- a/ValueView/ValueView.resources.php
+++ b/ValueView/ValueView.resources.php
@@ -200,6 +200,7 @@
'dependencies' => array(
'jquery.time.timeinput',
'jquery.ui.inputextender',
+ 'jquery.ui.listrotator',
),
'messages' => array(
'valueview-preview-label',
diff --git a/ValueView/ValueView.tests.qunit.php
b/ValueView/ValueView.tests.qunit.php
index 94a211e..bbce6f5 100644
--- a/ValueView/ValueView.tests.qunit.php
+++ b/ValueView/ValueView.tests.qunit.php
@@ -71,6 +71,15 @@
),
),
+ 'jquery.ui.listrotator.tests' => array(
+ 'scripts' => array(
+ "$bp/jquery.ui/jquery.ui.listrotator.tests.js",
+ ),
+ 'dependencies' => array(
+ 'jquery.ui.listrotator',
+ ),
+ ),
+
'jquery.ui.suggester.tests' => array(
'scripts' => array(
"$bp/jquery.ui/jquery.ui.suggester.tests.js",
diff --git a/ValueView/resources/jquery.ui/jquery.ui.inputextender.css
b/ValueView/resources/jquery.ui/jquery.ui.inputextender.css
index 8f6e1ac..4554dff 100644
--- a/ValueView/resources/jquery.ui/jquery.ui.inputextender.css
+++ b/ValueView/resources/jquery.ui/jquery.ui.inputextender.css
@@ -21,3 +21,9 @@
padding: 2px;
box-shadow: 2px 2px 6px -1px grey;
}
+
+.ui-inputextener .ui-inputextender-extendedcontent {
+ margin-top: 5px;
+ padding-top: 5px;
+ border-top: 1px dashed #CCC;
+}
diff --git a/ValueView/resources/jquery.ui/jquery.ui.inputextender.js
b/ValueView/resources/jquery.ui/jquery.ui.inputextender.js
index 2d4956d..04e081e 100644
--- a/ValueView/resources/jquery.ui/jquery.ui.inputextender.js
+++ b/ValueView/resources/jquery.ui/jquery.ui.inputextender.js
@@ -13,11 +13,16 @@
* clicking on the extender link.
* Default value: []
*
+ * @option {Function} [initCallback] Function triggered after the widget has
been initialized but
+ * before the widget contents get hidden initially. This may be used
to init some widgets
+ * that need to be visible on initialization for measuring dimension
according to their
+ * container's styles.
+ *
* @option {boolean} [hideWhenInputEmpty] Whether all of the input extender's
contents shall be
* hidden when the associated input element is empty.
* Default value: true
*
- * @option [messages] {Object} Strings used within the widget.
+ * @option {Object} [messages] Strings used within the widget.
* Messages should be specified using mwMsgOrString(<resource loader
module message key>,
* <fallback message>) in order to use the messages specified in the
resource loader module
* (if loaded).
@@ -27,6 +32,9 @@
* messages['hide options'] {String} (optional) Label of the link
hiding any additional
* contents.
* Default value: 'hide options'
+ *
+ * @event toggle: Triggered when the visibility of the extended content is
toggled.
+ * (1) {jQuery.Event}
*
* @dependency jQuery.Widget
* @dependency jQuery.eachchange
@@ -41,7 +49,7 @@
var IS_MW_CONTEXT = ( typeof mw !== 'undefined' && mw.msg );
/**
- * Whether actual entity selector resource loader module is loaded.
+ * Whether actual inputextender resource loader module is loaded.
* @type {boolean}
*/
var IS_MODULE_LOADED = (
@@ -69,6 +77,7 @@
options: {
content: [],
extendedContent: [],
+ initCallback: null,
hideWhenInputEmpty: true,
messages: {
'show options': mwMsgOrString(
'valueview-inputextender-showoptions', 'show options' ),
@@ -119,6 +128,12 @@
$extendedContent: null,
/**
+ * Caches the timeout when the actual "blur" action should kick
in.
+ * @type {Object}
+ */
+ _blurTimeout: null,
+
+ /**
* @see jQuery.Widget._create
*/
_create: function() {
@@ -132,7 +147,6 @@
this.$container = $( '<div/>' )
.addClass( this.widgetBaseClass )
- .data( this.widgetName, this )
.appendTo( this.$parent );
this.$inputContainer = $( '<div />' )
@@ -146,14 +160,26 @@
.text( this.options.messages['show options'] )
.appendTo( this.$inputContainer )
.on( 'click', function( event ) {
+ clearTimeout( self._blurTimeout );
self._toggleExtension();
+ } )
+ .on( 'keydown', function( event ) {
+ if( event.keyCode === $.ui.keyCode.ENTER ) {
+ clearTimeout( self._blurTimeout );
+ self._toggleExtension();
+ }
+ } )
+ .on( 'focus', function( event ) {
+ clearTimeout( self._blurTimeout );
} )
.hide();
this.$contentContainer = $( '<div/>' )
.addClass( this.widgetBaseClass + '-contentcontainer
ui-widget-content' )
.appendTo( this.$container )
- .hide();
+ .on( 'click.' + this.widgetName, function( event ) {
+ clearTimeout( self._blurTimeout );
+ } );
this.$content = $( '<div/>' )
.addClass( this.widgetBaseClass + '-content' )
@@ -161,22 +187,27 @@
this.$extendedContent = $( '<div/>' )
.addClass( this.widgetBaseClass + '-extendedcontent' )
- .appendTo( this.$contentContainer )
- .hide();
+ .appendTo( this.$contentContainer );
this.element.add( this.$extender )
.on( 'focus.' + this.widgetName, function( event ) {
- if( !self.options.hideWhenInputEmpty ||
self.element.val() !== '' || self._extended ) {
+ if( !self.options.hideWhenInputEmpty ||
self.element.val() !== '' ) {
+ clearTimeout( self._blurTimeout );
self.showContent();
}
} )
+ // TODO: Do not hide when tabbing into the
inputextender's contents
.on( 'blur.' + this.widgetName, function( event ) {
- self.hideContent();
+ self._blurTimeout = setTimeout( function() {
+ self.hideContent( function() {
+ self._toggleExtension( {
forceHide: true } );
+ } );
+ }, 150 );
} );
if( this.options.hideWhenInputEmpty ) {
this.element.eachchange( function( event,
oldValue ) {
- if( self.element.val() === '' &&
!self._extended ) {
+ if( self.element.val() === '' &&
!self.$extendedContent.is( ':visible' ) ) {
self.hideContent();
} else if ( oldValue === '' ) {
self.showContent();
@@ -184,7 +215,33 @@
} );
}
+ // Blurring by clicking away from the widget (one
handler is sufficient):
+ if( $( ':' + this.widgetBaseClass ).length === 1 ) {
+ $( 'html' ).on( 'click.' + this.widgetName,
function( event ) {
+ // Loop through all widgets and hide
content when having clicked out of it:
+ var $widgetNodes = $( ':' +
self.widgetBaseClass );
+ $widgetNodes.each( function( i,
widgetNode ) {
+ var widget = $( widgetNode
).data( self.widgetName );
+ if(
+ $( event.target
).closest( widget.$container ).length === 0
+ && !widget.element.is(
':focus' )
+ ) {
+ widget.hideContent(
function() {
+
widget._toggleExtension( { forceHide: true } );
+ } );
+ }
+ } );
+ } );
+ }
+
this._draw();
+
+ if( $.isFunction( this.options.initCallback ) ) {
+ this.options.initCallback();
+ }
+
+ this.$contentContainer.hide();
+ this.$extendedContent.hide();
},
/**
@@ -194,6 +251,9 @@
var $input = this.element.detach();
this.$container.remove();
this.$parent.append( $input );
+ if( $( ':' + this.widgetBaseClass ).length === 0 ) {
+ $( 'html' ).off( '.' + this.widgetName );
+ }
$.Widget.prototype.destroy.call( this );
},
@@ -219,23 +279,38 @@
/**
* Toggles the visibility of the additional options.
+ *
+ * @param {Object|undefined} customOptions
*/
- _toggleExtension: function() {
- var self = this;
+ _toggleExtension: function( customOptions ) {
+ var self = this,
+ options = {
+ moveFocus: true,
+ forceHide: false
+ };
- if( this.$extendedContent.is( ':visible' ) ) {
- this.$extendedContent.slideUp( 150, function() {
- self._extended = false;
- self.element.focus();
+ $.extend( options, customOptions );
+
+ function hideExtendedContent() {
+ self.$extendedContent.slideUp( 150, function() {
self.$extender.text(
self.options.messages['show options'] );
self._trigger( 'toggle' );
} );
+ }
+
+ if( options.forceHide ) {
+ hideExtendedContent();
+ return;
+ }
+
+ if( this.$extendedContent.is( ':visible' ) ) {
+ this.showContent( hideExtendedContent );
} else {
- this.$extendedContent.slideDown( 150,
function() {
- self._extended = true;
- self.element.focus();
- self.$extender.text(
self.options.messages['hide options'] );
- self._trigger( 'toggle' );
+ this.showContent( function() {
+ self.$extendedContent.slideDown( 150,
function() {
+ self.$extender.text(
self.options.messages['hide options'] );
+ self._trigger( 'toggle' );
+ } );
} );
}
@@ -247,10 +322,7 @@
* @param {Function} [callback] Invoked as soon as the contents
are visible.
*/
showContent: function( callback ) {
- if( this.$contentContainer.is( ':visible' ) ) {
- return;
- }
- this.$contentContainer.fadeIn( 150, function() {
+ this.$contentContainer.stop( true, true ).fadeIn( 150,
function() {
if( $.isFunction( callback ) ) {
callback();
}
@@ -263,10 +335,7 @@
* @param {Function} [callback] Invoked as soon as the contents
are hidden.
*/
hideContent: function( callback ) {
- if( !this.$contentContainer.is( ':visible' ) ||
this._extended ) {
- return;
- }
- this.$contentContainer.fadeOut( 150, function() {
+ this.$contentContainer.stop( true, true ).fadeOut( 150,
function() {
if( $.isFunction( callback ) ) {
callback();
}
diff --git a/ValueView/resources/jquery.ui/jquery.ui.listrotator.css
b/ValueView/resources/jquery.ui/jquery.ui.listrotator.css
new file mode 100644
index 0000000..383ef20
--- /dev/null
+++ b/ValueView/resources/jquery.ui/jquery.ui.listrotator.css
@@ -0,0 +1,73 @@
+/**
+ * Default styles for listrotator widget
+ *
+ * @license GNU GPL v2+
+ * @author H. Snater < [email protected] >
+ */
+
+.ui-listrotator {
+ white-space: nowrap;
+ border: none;
+}
+
+.ui-listrotator .ui-state-active a {
+ text-decoration: underline;
+}
+
+.ui-listrotator .ui-listrotator-auto {
+ float: left;
+ margin-right: 10px;
+ border: none;
+ background: none;
+}
+
+.ui-listrotator .ui-listrotator-prev {
+ float: left;
+ margin-right: 5px;
+ border: none;
+ background: none;
+ text-align: right;
+}
+.ui-listrotator .ui-listrotator-prev a {
+ float: left;
+}
+
+.ui-listrotator .ui-listrotator-curr {
+ float: left;
+ text-align: center;
+ padding: 0 5px 0 10px;
+ background: none;
+ border-color: #CCCCCC;
+}
+.ui-listrotator .ui-listrotator-curr a {
+ float: left;
+}
+
+.ui-listrotator .ui-listrotator-next {
+ float: right;
+ margin-left: 5px;
+ border: none;
+ background: none;
+}
+.ui-listrotator .ui-listrotator-next a {
+ float :right;
+}
+
+.ui-listrotator .ui-icon {
+ display: inline-block;
+}
+
+.ui-listrotator-menu {
+ font-size: 84%;
+ padding: 0;
+ z-index: 1;
+}
+
+.ui-listrotator-menu .ui-menu-item {
+ font-size: 84%;
+}
+
+.ui-listrotator-menu li.ui-state-active {
+ border-left: none;
+ border-right: none;
+}
diff --git a/ValueView/resources/jquery.ui/jquery.ui.listrotator.js
b/ValueView/resources/jquery.ui/jquery.ui.listrotator.js
new file mode 100644
index 0000000..1d139aa
--- /dev/null
+++ b/ValueView/resources/jquery.ui/jquery.ui.listrotator.js
@@ -0,0 +1,586 @@
+/**
+ * List rotator widget
+ *
+ * The list rotator may be used to rotate through a list of values.
+ * @licence GNU GPL v2+
+ * @author H. Snater < [email protected] >
+ *
+ * @option {Object[]} values Array of objects containing the values to rotate.
+ * Single object structure:
+ * { value: <actual value (being returned on value())>, label: <the
value's label> }
+ *
+ * @option {Object} [menu] Options for the jQuery.menu widget used as
drop-down menu:
+ * [menu.position] {Object} Default object passed to
jQuery.ui.position when positioning the
+ * menu.
+ *
+ * @option {boolean} [auto] Whether to display the "auto" link.
+ * Default value: true
+ *
+ * @option {string[]} [animationMargins] Defines how far the sections should
be shifted when
+ * animating the rotation. First value when shifting to the left and
vice versa. Values will
+ * be flipped in rtl context.
+ * Default value: ['-15px', '15px']
+ *
+ * @option {boolean} [deferInit] Whether to defer initializing the section
widths until initWidths()
+ * is called "manually".
+ * Default value: false
+ *
+ * @event auto: Triggered when "auto" options is selected.
+ * (1) {jQuery.Event}
+ *
+ * @event selected: Triggered when a specific value is selected.
+ * (1) {jQuery.Event}
+ * (2) {*} Value as specified in the "values" option.
+ *
+ * @dependency jQuery.ui.Widget
+ * @dependency jQuery.ui.menu
+ * @dependency jQuery.ui.position
+ */
+( function( $ ) {
+ 'use strict';
+
+ /**
+ * Whether loaded in MediaWiki context.
+ * @type {boolean}
+ */
+ var IS_MW_CONTEXT = ( typeof mw !== 'undefined' && mw.msg );
+
+ /**
+ * Whether actual listrotator resource loader module is loaded.
+ * @type {boolean}
+ */
+ var IS_MODULE_LOADED = (
+ IS_MW_CONTEXT
+ && $.inArray( 'jquery.ui.listrotator',
mw.loader.getModuleNames() ) !== -1
+ );
+
+ /**
+ * Returns a message from the MediaWiki context if the listrotator
module has been loaded.
+ * If it has not been loaded, the corresponding string defined in the
options will be returned.
+ *
+ * @param {String} msgKey
+ * @param {String} string
+ * @return {String}
+ */
+ function mwMsgOrString( msgKey, string ) {
+ return ( IS_MODULE_LOADED ) ? mw.msg( msgKey ) : string;
+ }
+
+ /**
+ * Caches whether the widget is used in a rtl context. This, however,
depends on using an "rtl"
+ * class on the document body like it is done in MediaWiki.
+ * @type {boolean}
+ */
+ var isRtl = $( 'body' ).hasClass( 'rtl' );
+
+ /**
+ * Measures the maximum width of a container according to a list of
strings. The width is
+ * determined by the widest string.
+ *
+ * @param {jQuery} $container
+ * @param {string[]} strings
+ * @returns {number} The container's maximum width in pixel
+ */
+ function measureMaximumStringWidths( $container, strings ) {
+ var widths = [];
+ $.each( strings, function( i, string ) {
+ $container.empty().text( string );
+ widths.push( $container.width() );
+ } );
+ $container.empty();
+ return widths;
+ }
+
+ $.widget( 'ui.listrotator', {
+ /**
+ * Additional options
+ * @type {Object}
+ */
+ options: {
+ values: [],
+ menu: {
+ position: {
+ my: ( isRtl ) ? 'right top' : 'left
top',
+ at: ( isRtl ) ? 'right bottom' : 'left
bottom',
+ collision: 'none'
+ }
+ },
+ auto: true,
+ animationMargins: ['-15px', '15px'],
+ deferInit: false,
+ messages: {
+ 'auto': mwMsgOrString(
'valueview-listrotator-auto', 'auto' )
+ }
+ },
+
+ $auto: null,
+
+ /**
+ * Node of the previous list item section.
+ * @type {jQuery}
+ */
+ $prev: null,
+
+ /**
+ * Node of the current list item section.
+ * @type {jQuery}
+ */
+ $curr: null,
+
+ /**
+ * Node of the next list item section.
+ * @type {jQuery}
+ */
+ $next: null,
+
+ /**
+ * Node of the menu opening when clicking on the "current"
section.
+ * @type {jQuery}
+ */
+ $menu: null,
+
+ /**
+ * Temporarily caching the value the rotator is rotating to
while the animation is being
+ * performed.
+ * @type {*}
+ */
+ _rotatingTo: null,
+
+ /**
+ * @see $.ui.Widget._create
+ */
+ _create: function() {
+ var self = this,
+ iconClasses = ['ui-icon ui-icon-triangle-1-w',
'ui-icon ui-icon-triangle-1-e'];
+
+ // Flip triangle arrows in rtl context:
+ if ( isRtl ) {
+ iconClasses.reverse();
+ }
+
+ if ( this.options.values.length === 0 ) {
+ throw new Error( 'List of values required to
initialize list rotator.' );
+ }
+
+ this.element.addClass( this.widgetBaseClass + '
ui-widget-content' );
+
+ // Construct "auto" link:
+ this.$auto = this._createSection( 'auto', function(
event ) {
+ if( self.$auto.hasClass( 'ui-state-active' ) ) {
+ return;
+ }
+ self.activate( self.$auto );
+ self._trigger( 'auto' );
+ } )
+ .addClass( 'ui-state-active' );
+ this.$auto.children( 'a' ).text(
this.options.messages['show options'] );
+
+ // Construct the basic sections:
+ this.$curr = this._createSection( 'curr', function(
event ) {
+ if ( !self.$menu.is( ':visible' ) ) {
+ self._showMenu();
+ } else {
+ self._hideMenu();
+ }
+ } )
+ .append( $( '<a/>' ).addClass( 'ui-icon
ui-icon-triangle-1-s' ) );
+
+ this.$prev = this._createSection( 'prev', function(
event ) {
+ self.prev();
+ } )
+ .append( $( '<a/>' ).addClass( iconClasses[0] ) );
+
+ this.$next = this._createSection( 'next', function(
event ) {
+ self.next();
+ } )
+ .append( $( '<a/>' ).addClass( iconClasses[1] ) );
+
+ if( this.$auto ) {
+ this.element.append( this.$auto );
+ }
+ this.element.append( this.$prev ).append( this.$curr
).append( this.$next );
+
+ // Apply hover functionality:
+ $.each( [ this.$auto, this.$curr, this.$prev,
this.$next ], function( i, $node ) {
+ $node
+ .addClass( 'ui-state-default' )
+ .on( 'mouseover', function( event ) {
+ var $this = $( this );
+ if( $this.hasClass( 'ui-state-disabled'
) ) {
+ return;
+ }
+ $this.addClass( 'ui-state-hover' );
+ } )
+ .on( 'mouseout', function( event ) {
+ var $this = $( this );
+ if( $this.hasClass( 'ui-state-disabled'
) ) {
+ return;
+ }
+ $this.removeClass( 'ui-state-hover' );
+ } )
+ .find( 'a' ).attr( 'href',
'javascript:void(0);' );
+ } );
+
+ // Construct and initialize menu widget:
+ this._createMenu();
+
+ // Attach event to html node to detect click outside of
the menu closing the menu:
+ if ( $( ':' + self.widgetBaseClass ).length === 1 ) {
+ $( 'html' ).on( 'click.' +
this.widgetBaseClass, function( event ) {
+ $( ':' + self.widgetBaseClass ).each(
function( i, node ) {
+ $( node ).data( 'listrotator'
).$menu.hide();
+ } );
+ } );
+ }
+
+ // Prevent propagation of clicking on the "current"
section as well as on the menu in
+ // order to not trigger the event handler assigned to
the html element.
+ this.$menu.add( this.$curr ).on( 'click.' +
this.widgetBaseClass, function( event ) {
+ event.stopPropagation();
+ } );
+
+ // Focus on first element:
+ this.value( this.options.values[0].value );
+
+ if( !this.options.deferInit ) {
+ this.initWidths();
+ }
+
+ },
+
+ /**
+ * @see $.Widget.destroy
+ */
+ destroy: function() {
+ this.$menu.data( 'menu' ).destroy();
+ this.$menu.remove();
+
+ this.$auto.remove();
+ this.$curr.remove();
+ this.$prev.remove();
+ this.$next.remove();
+
+ this.element.removeClass( this.widgetBaseClass + '
ui-widget-content' );
+
+ // Remove event attached to the html node if no
instances of the widget exist anymore:
+ if ( $( ':' + this.widgetBaseClass ).length === 0 ) {
+ $( 'html' ).off( '.' + this.widgetBaseClass );
+ }
+
+ $.Widget.prototype.destroy.call( this );
+ },
+
+ /**
+ * Init the section widths.
+ */
+ initWidths: function() {
+ // Determine the maximum width a label may have and
apply that width to each section:
+ var currentLabel = this.$curr.children( '.' +
this.widgetBaseClass + '-label' ).text(),
+ labels = [],
+ stringWidths = [],
+ currMaxWidth = 0,
+ prevMaxWidth = 0,
+ nextMaxWidth = 0;
+
+ $.each( this.options.values, function( i, v ) {
+ labels.push( v.label );
+ } );
+
+ stringWidths = measureMaximumStringWidths(
+ this.$curr.children( '.' + this.widgetBaseClass
+ '-label' ),
+ labels
+ );
+ $.each( stringWidths, function( i, width ) {
+ if( width > currMaxWidth ) {
+ currMaxWidth = width;
+ }
+ if( i > 0 && width > prevMaxWidth ) {
+ prevMaxWidth = width;
+ }
+ if( i < stringWidths.length && width >
nextMaxWidth ) {
+ nextMaxWidth = width;
+ }
+ } );
+
+ this.$curr.children( '.' + this.widgetBaseClass +
'-label' ).width( currMaxWidth );
+ // The "previous" section will not be filled with the
last string while the "next"
+ // section will never be filled with the first string.
+ this.$prev.children( '.' + this.widgetBaseClass +
'-label' ).width( prevMaxWidth );
+ this.$next.children( '.' + this.widgetBaseClass +
'-label' ).width( nextMaxWidth );
+
+ // Make menu width comply to the "current" section:
+ var menuSpacing = this.$menu.outerWidth() -
this.$menu.width();
+ this.$menu.width( this.$curr.outerWidth() - menuSpacing
);
+
+ // Reset "current" section's label:
+ this.$curr.children( '.' + this.widgetBaseClass +
'-label' ).text( currentLabel );
+ },
+
+ /**
+ * Creates a widget section.
+ *
+ * @param {string} classSuffix
+ * @param {Function} clickCallback
+ * @return {jQuery}
+ */
+ _createSection: function( classSuffix, clickCallback ) {
+ return $( '<span/>' )
+ .addClass( this.widgetBaseClass + '-' + classSuffix )
+ .on( 'click.' + this.widgetBaseClass, function( event )
{
+ if( !$( this ).hasClass( 'ui-state-disabled' )
) {
+ clickCallback( event );
+ }
+ } )
+ .append( $( '<a/>' ).addClass( this.widgetBaseClass +
'-label' ) );
+ },
+
+ /**
+ * Create the drop-down menu assigned to the "current" section.
+ */
+ _createMenu: function() {
+ var self = this;
+
+ this.$menu = $( '<ul/>' )
+ .addClass( this.widgetBaseClass + '-menu' )
+ .appendTo( $( 'body' ) ).hide();
+
+ $.each( this.options.values, function( i, v ) {
+ self.$menu.append(
+ $( '<li/>' )
+ .append(
+ $( '<a/>' )
+ .attr( 'href',
'javascript:void(0);')
+ .text( v.label )
+ .on( 'click', function( event )
{
+ self._trigger(
'selected', null, [ self.value( v.value ) ] );
+ self.$menu.hide();
+ } )
+ )
+ .data( 'value', v.value )
+ );
+ } );
+
+ this.$menu.menu();
+ },
+
+ /**
+ * Sets/Gets the widget's value.
+ *
+ * TODO: Change behavior: value as setter should return "this"
for allowing chaining calls
+ * to the widget.
+ *
+ * @param [value] The value to assign. (Has to match a value
actually existing in the
+ * widget's options.)
+ * @return {string} Current value.
+ */
+ value: function( value ) {
+ // Get the current value:
+ if ( value === undefined || value === this.$curr.data(
'value' ) ) {
+ return this.$curr.data( 'value' );
+ }
+
+ var index = 0;
+
+ this.$prev.add( this.$curr ).add( this.$next )
+ .children( '.' + this.widgetBaseClass + '-label'
).empty();
+
+ $.each( this.options.values, function( i, v ) {
+ if ( value === v.value ) {
+ index = i;
+ return false;
+ }
+ } );
+
+ this.$curr
+ .data( 'value', this.options.values[index].value )
+ .children( '.' + this.widgetBaseClass + '-label' )
+ .text( this.options.values[index].label );
+
+ if ( index > 0 ) {
+ this.$prev
+ .data( 'value', this.options.values[index -
1].value )
+ .children( '.' + this.widgetBaseClass +
'-label' )
+ .text( this.options.values[index - 1].label );
+ }
+
+ if ( index < this.options.values.length - 1 ) {
+ this.$next
+ .data( 'value', this.options.values[index +
1].value )
+ .children( '.' + this.widgetBaseClass +
'-label' )
+ .text( this.options.values[index + 1].label );
+ }
+
+ this.$prev.css( 'visibility', 'visible' );
+ this.$next.css( 'visibility', 'visible' );
+ if ( index === 0 ) {
+ this.$prev.css( 'visibility', 'hidden' );
+ } else if ( index === this.options.values.length - 1 ) {
+ this.$next.css( 'visibility', 'hidden' );
+ }
+
+ // Alter menu item states:
+ this.$menu.children( 'li' ).each( function( i, li ) {
+ var $li = $( li );
+ $li.removeClass( 'ui-state-active' );
+ if( $li.data( 'value' ) === value ) {
+ $li.addClass( 'ui-state-active' );
+ }
+ } );
+
+ return value;
+ },
+
+ /**
+ * Sets a new value rotating to the new value.
+ *
+ * @param {*} newValue
+ */
+ _setValue: function( newValue ) {
+ var self = this;
+
+ if( this.$curr.data( 'value' ) === newValue ) {
+ // Value is set already.
+ return;
+ }
+
+ this.rotate( newValue, function() {
+ self.activate();
+ } );
+ },
+
+ /**
+ * Rotates the widget to the next value.
+ */
+ next: function() {
+ this._setValue( this.$next.data( 'value' ) );
+ },
+
+ /**
+ * Rotates the widget to the previous value.
+ */
+ prev: function() {
+ this._setValue( this.$prev.data( 'value' ) );
+ },
+
+ /**
+ * Performs the rotation of the widget.
+ *
+ * @param {string} newValue
+ * @param {Function} [callback]
+ */
+ rotate: function( newValue, callback ) {
+ if( newValue === this._rotatingTo || !this._rotatingTo
&& newValue === this.$curr.data( 'value' ) ) {
+ return;
+ }
+
+ this._rotatingTo = newValue;
+
+ var self = this,
+ margins = $.merge( [],
this.options.animationMargins ),
+ s = '.' + this.widgetBaseClass + '-label';
+
+ var $nodes = this.$prev.children( s )
+ .add( this.$curr.children( s ) )
+ .add( this.$next.children( s ) );
+
+ // Figure out whether rotating to the right or to the
left:
+ var beforeCurrent = true;
+ $.each( this.options.values, function( i, v ) {
+ if( v.value === newValue ) {
+ return false;
+ }
+ if( v.value === self.$curr.data( 'value' ) ) {
+ beforeCurrent = false;
+ return false;
+ }
+ } );
+
+ if( beforeCurrent ) {
+ margins.reverse();
+ }
+
+ if ( isRtl ) {
+ margins.reverse();
+ }
+
+ $nodes.animate(
+ {
+ marginLeft: margins[0],
+ marginRight: margins[1],
+ opacity: 0
+ }, {
+ done: function() {
+ $nodes.css( {
+ marginLeft: '0',
+ marginRight: '0',
+ opacity: 1
+ } );
+ self._trigger( 'selected',
null, [ self.value( newValue ) ] );
+ self._rotatingTo = null;
+ if( $.isFunction( callback ) ) {
+ callback();
+ }
+ },
+ duration: 150
+ }
+ );
+ },
+
+ /**
+ * Activates the widget.
+ *
+ * @param {jQuery} [$section] Section to activate. "Current"
section by default.
+ */
+ activate: function( $section ) {
+ this.$curr.add( this.$auto ).removeClass(
'ui-state-hover' ).removeClass( 'ui-state-active' );
+ if( $section === undefined ) {
+ $section = this.$curr;
+ }
+ $section.addClass( 'ui-state-active' );
+ },
+
+ /**
+ * De-activates the widget.
+ */
+ deactivate: function() {
+ this.$curr.add( this.$auto ).removeClass(
'ui-state-active' );
+ },
+
+ /**
+ * Shows the drop-down menu.
+ */
+ _showMenu: function() {
+ this.$menu.slideDown( 150 );
+ this.$menu.position( $.extend( {
+ of: this.$curr
+ }, this.options.menu.position ) );
+ this.activate();
+ },
+
+ /**
+ * Hides the drop-down menu.
+ */
+ _hideMenu: function() {
+ this.$menu.slideUp( 150 );
+ this.activate();
+ },
+
+ /**
+ * Disables the widget.
+ */
+ disable: function() {
+ this.$prev.add( this.$curr ).add( this.$next )
+ .addClass( 'ui-state-disabled' );
+ },
+
+ /**
+ * Enables the widget.
+ */
+ enable: function() {
+ this.$prev.add( this.$curr ).add( this.$next )
+ .removeClass( 'ui-state-disabled' );
+ }
+
+ } );
+
+} )( jQuery );
diff --git
a/ValueView/resources/jquery.valueview/valueview.experts/experts.TimeInput.js
b/ValueView/resources/jquery.valueview/valueview.experts/experts.TimeInput.js
index 479e4c0..4525de1 100644
---
a/ValueView/resources/jquery.valueview/valueview.experts/experts.TimeInput.js
+++
b/ValueView/resources/jquery.valueview/valueview.experts/experts.TimeInput.js
@@ -6,8 +6,11 @@
* @author H. Snater < [email protected] >
*/
// TODO: Remove mediaWiki dependency
-( function( dv, vp, $, vv, Time, mw ) {
+( function( dv, vp, $, vv, time, mw ) {
'use strict';
+
+ var Time = time.Time,
+ timeSettings = time.settings;
var PARENT = vv.Expert;
@@ -64,7 +67,29 @@
this.$previewValue = $( '<div/>' )
.addClass( 'valueview-preview-value' )
+ .text( mw.msg( 'valueview-preview-novalue' ) )
.appendTo( this.$preview );
+
+ var precisionValues = [];
+ $.each( timeSettings.precisiontexts, function( i, text
) {
+ precisionValues.push( { value: i, label: text }
);
+ } );
+
+ this.$precision = $( '<div/>' )
+ .listrotator( { values: precisionValues.reverse(),
deferInit: true } )
+ .on( 'listrotatorauto', function( event ) {
+ var value = new Time( self.$input.val() );
+ $( this ).data( 'listrotator' ).rotate(
value.precision() );
+ self._setRawValue( value );
+ self._updatePreview( value );
+ self._viewNotifier.notify( 'change' );
+ } )
+ .on( 'listrotatorselected', function( event, precision
) {
+ var value = new Time( self.$input.val(),
precision );
+ self._setRawValue( value );
+ self._updatePreview( value );
+ self._viewNotifier.notify( 'change' );
+ } );
this.$input = $( '<input/>', {
type: 'text',
@@ -80,11 +105,21 @@
.timeinput()
// TODO: Move input extender out of here to a more
generic place since it is not
// TimeInput specific.
- .inputextender( { content: [ this.$preview ] } )
+ .inputextender( {
+ content: [ this.$preview ],
+ extendedContent: [ this.$precision ],
+ initCallback: function() {
+ self.$precision.data( 'listrotator'
).initWidths();
+ }
+ } )
.on( 'timeinputupdate', function( event, value ) {
self._updatePreview( value );
+ if( value && value.isValid() ) {
+ self.$precision.data( 'listrotator'
).rotate( value.precision() );
+ }
self._viewNotifier.notify( 'change' );
} );
+
},
/**
@@ -175,6 +210,9 @@
if( this._newValue !== false ) {
this.$input.data( 'timeinput' ).value(
this._newValue );
this._updatePreview( this._newValue );
+ if( this._newValue !== null ) {
+ this.$precision.data( 'listrotator'
).value( this._newValue.precision() );
+ }
this._newValue = false;
}
},
@@ -195,4 +233,4 @@
} );
-}( dataValues, valueParsers, jQuery, jQuery.valueview, time.Time, mediaWiki )
);
+}( dataValues, valueParsers, jQuery, jQuery.valueview, time, mediaWiki ) );
diff --git a/ValueView/tests/qunit/jquery.ui/jquery.ui.listrotator.tests.js
b/ValueView/tests/qunit/jquery.ui/jquery.ui.listrotator.tests.js
new file mode 100644
index 0000000..8543bec
--- /dev/null
+++ b/ValueView/tests/qunit/jquery.ui/jquery.ui.listrotator.tests.js
@@ -0,0 +1,98 @@
+/**
+ * @since 0.1
+ * @ingroup ValueView
+ *
+ * @licence GNU GPL v2+
+ * @author H. Snater < [email protected] >
+ */
+
+( function( $, QUnit ) {
+ 'use strict';
+
+ /**
+ * Factory for creating an listrotator widget suitable for testing.
+ */
+ var newTestListrotator = function( options ) {
+ options = $.extend( {
+ values: [
+ { value: 1, label: 'one' },
+ { value: 2, label: 'two' },
+ { value: 3, label: 'three' }
+ ]
+ }, options || {} );
+
+ var $div = $( '<div/>' )
+ .addClass( 'test_listrotator' )
+ .appendTo( $( 'body' ) )
+ .listrotator( options );
+
+ return $div.data( 'listrotator' );
+ };
+
+ QUnit.module( 'jquery.ui.listrotator', QUnit.newMwEnvironment( {
+ teardown: function() {
+ $( '.test_listrotator' ).each( function( i, node ) {
+ var $node = $( node ),
+ listRotator = $node.data( 'listrotator'
);
+ if( listRotator ) {
+ listRotator.destroy();
+ }
+ $node.remove();
+ } );
+ }
+ } ) );
+
+ QUnit.test( 'Initialize and destroy', 4, function( assert ) {
+ var listrotator = newTestListrotator(),
+ widgetBaseClass = listrotator.widgetBaseClass;
+
+ assert.equal(
+ $( '.test_listrotator' ).data( 'listrotator' ),
+ listrotator,
+ 'Initialized widget.'
+ );
+
+ assert.equal(
+ $( '.' + widgetBaseClass + '-menu' ).length,
+ 1,
+ 'Appended menu element to DOM.'
+ );
+
+ listrotator.destroy();
+
+ assert.ok(
+ $( '.test_listrotator' ).data( 'listrotator' ) ===
undefined,
+ 'Destroyed widget.'
+ );
+
+ assert.equal(
+ $( '.' + widgetBaseClass + '-menu' ).length,
+ 0,
+ 'Remove menu element from DOM.'
+ );
+ } );
+
+ QUnit.test( 'value()', 3, function( assert ) {
+ var listrotator = newTestListrotator();
+
+ assert.strictEqual(
+ listrotator.value(),
+ 1,
+ 'Listrotator\'s value is set to first value on
initialization.'
+ );
+
+ assert.strictEqual(
+ listrotator.value( 3 ),
+ 3,
+ 'Set listrotator\'s value.'
+ );
+
+ assert.strictEqual(
+ listrotator.value(),
+ 3,
+ 'Confirmed listrotator\'s value.'
+ );
+
+ } );
+
+}( jQuery, QUnit ) );
--
To view, visit https://gerrit.wikimedia.org/r/62585
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: Iaca1465915ce874bfb864e48dfaaa309a599202b
Gerrit-PatchSet: 12
Gerrit-Project: mediawiki/extensions/DataValues
Gerrit-Branch: master
Gerrit-Owner: Henning Snater <[email protected]>
Gerrit-Reviewer: Daniel Werner <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits