Henning Snater has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/62585


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/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
8 files changed, 800 insertions(+), 31 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/DataValues 
refs/changes/85/62585/1

diff --git a/ValueView/ValueView.i18n.php b/ValueView/ValueView.i18n.php
index d75adbc..ea585ac 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.',
 );
 
 /** Belarusian (Taraškievica orthography) (беларуская (тарашкевіца)‎)
diff --git a/ValueView/ValueView.resources.mw.php 
b/ValueView/ValueView.resources.mw.php
index 705daad..145f856 100644
--- a/ValueView/ValueView.resources.mw.php
+++ b/ValueView/ValueView.resources.mw.php
@@ -112,6 +112,23 @@
                                'valueview-inputextender-hideoptions',
                        ),
                ),
+
+               '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 f842a6e..43efc11 100644
--- a/ValueView/ValueView.resources.php
+++ b/ValueView/ValueView.resources.php
@@ -182,6 +182,7 @@
                        'dependencies' => array(
                                'jquery.time.timeinput',
                                'jquery.ui.inputextender',
+                               'jquery.ui.listrotator',
                                'jquery.valueview.experts.staticdom',
                                'jquery.valueview.BifidExpert',
                        ),
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..b925a15 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,30 @@
                                } );
                        }
 
+                       // 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.hideContent( 
function() {
+                                                               
widget._toggleExtension( { forceHide: true } );
+                                                       } );
+                                               }
+                                       } );
+                               } );
+                       }
+
                        this._draw();
+
+                       if( $.isFunction( this.options.initCallback ) ) {
+                               this.options.initCallback();
+                       }
+
+                       this.$contentContainer.hide();
+                       this.$extendedContent.hide();
                },
 
                /**
@@ -194,6 +248,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 +276,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 +319,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 +332,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..86411ab
--- /dev/null
+++ b/ValueView/resources/jquery.ui/jquery.ui.listrotator.js
@@ -0,0 +1,565 @@
+/**
+ * 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 )
+                       .addClass( '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() {
+                       // Remove event attached to the html node if no 
instances of the widget exist anymore:
+                       if ( $( ':' + self.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.value( v.value );
+                                                       self.$menu.hide();
+                                               } )
+                                       )
+                                       .data( 'value', v.value )
+                               );
+                       } );
+
+                       this.$menu.menu();
+               },
+
+               /**
+                * Sets/Gets the widget's value.
+                *
+                * @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' );
+                               }
+                       } );
+
+                       this._trigger( 'selected', null, [value] );
+
+                       return value;
+               },
+
+               _setValue: function( newValue ) {
+                       var self = this;
+                       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;
+                               } else 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.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 6a14890..9a63885 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,10 +210,13 @@
                        if( this._newValue !== false ) {
                                this.$input.data( 'timeinput' ).value( 
this._newValue );
                                this._updatePreview( this._newValue );
+                               if( this._newValue !== null ) {
+                                       this.$precision.data( 'listrotator' 
).rotate( this._newValue.precision() );
+                               }
                                this._newValue = false;
                        }
                }
 
        } );
 
-}( dataValues, valueParsers, jQuery, jQuery.valueview, time.Time, mediaWiki ) 
);
+}( dataValues, valueParsers, jQuery, jQuery.valueview, time, mediaWiki ) );

-- 
To view, visit https://gerrit.wikimedia.org/r/62585
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Iaca1465915ce874bfb864e48dfaaa309a599202b
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/DataValues
Gerrit-Branch: master
Gerrit-Owner: Henning Snater <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to