Daniel Werner has submitted this change and it was merged. Change subject: (bug 48145) Implemented input extender jQuery widget ......................................................................
(bug 48145) Implemented input extender jQuery widget The input extender widget may be used on an input box in order to display additional information/options in an overlay beneath the input element. The basic implementation will be upgraded in future commits. Change-Id: I96af69359f555b61c48c638a483d756213e86856 --- M ValueView/ValueView.i18n.php M ValueView/ValueView.resources.mw.php M ValueView/ValueView.tests.qunit.php M ValueView/resources/jquery.time/jquery.time.timeinput.js A ValueView/resources/jquery.ui/jquery.ui.inputextender.css A ValueView/resources/jquery.ui/jquery.ui.inputextender.js M ValueView/tests/qunit/jquery.time/jquery.time.timeinput.tests.js A ValueView/tests/qunit/jquery.ui/jquery.ui.inputextender.tests.js 8 files changed, 438 insertions(+), 4 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 9ad74f8..bdb5cbc 100644 --- a/ValueView/ValueView.i18n.php +++ b/ValueView/ValueView.i18n.php @@ -31,6 +31,7 @@ /** English * @author Daniel Werner < [email protected] > + * @author H. Snater < [email protected] > */ $messages['en'] = array( 'valueview-desc' => 'UI components for displaying and editing data values', @@ -40,12 +41,16 @@ 'valueview-expert-unsupportedvalue-unsupporteddatatype' => 'Handling of values for "$1" data type is not yet supported.', // EmptyValue expert: - 'valueview-expert-emptyvalue-empty' => 'empty' + 'valueview-expert-emptyvalue-empty' => 'empty', + + 'valueview-inputextender-showoptions' => 'show options', + 'valueview-inputextender-hideoptions' => 'hide options', ); /** Message documentation (Message documentation) * @author Daniel Werner < [email protected] > * @author Shirayuki + * @author H. Snater < [email protected] > */ $messages['qqq'] = array( 'valueview-desc' => '{{desc|name=ValueView|url=http://www.mediawiki.org/wiki/Extension:ValueView}}', @@ -53,6 +58,8 @@ 'valueview-expert-unsupportedvalue-unsupporteddatatype' => 'Error shown if a data value for a certain data type (see [[d:Wikidata:Glossary]]) should be displayed or a form for creating one should be offered while this is not yet possible from a technical point of view (e.g. because a valueview widget expert handling data values for that data type has not yet been implemented). Parameter $1 is the name of the data type which lacks support', 'valueview-expert-emptyvalue-empty' => 'Message expressing that there is currently no value set in a jQuery valueview. {{Identical|Empty}}', + '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.', ); /** Asturian (asturianu) diff --git a/ValueView/ValueView.resources.mw.php b/ValueView/ValueView.resources.mw.php index 8f72e37..cf167bf 100644 --- a/ValueView/ValueView.resources.mw.php +++ b/ValueView/ValueView.resources.mw.php @@ -96,6 +96,22 @@ ), ), + 'jquery.ui.inputextender' => $moduleTemplate + array( + 'scripts' => array( + 'jquery.ui/jquery.ui.inputextender.js', + ), + 'styles' => array( + 'jquery.ui/jquery.ui.inputextender.css', + ), + 'dependencies' => array( + 'jquery.ui.widget', + ), + 'messages' => array( + 'valueview-inputextender-showoptions', + 'valueview-inputextender-hideoptions', + ), + ), + 'jquery.ui.toggler' => $moduleTemplate + array( 'scripts' => array( 'jquery.ui/jquery.ui.toggler.js', diff --git a/ValueView/ValueView.tests.qunit.php b/ValueView/ValueView.tests.qunit.php index 048e6b5..94a211e 100644 --- a/ValueView/ValueView.tests.qunit.php +++ b/ValueView/ValueView.tests.qunit.php @@ -62,6 +62,15 @@ ), ), + 'jquery.ui.inputextender.tests' => array( + 'scripts' => array( + "$bp/jquery.ui/jquery.ui.inputextender.tests.js", + ), + 'dependencies' => array( + 'jquery.ui.inputextender', + ), + ), + 'jquery.ui.suggester.tests' => array( 'scripts' => array( "$bp/jquery.ui/jquery.ui.suggester.tests.js", diff --git a/ValueView/resources/jquery.time/jquery.time.timeinput.js b/ValueView/resources/jquery.time/jquery.time.timeinput.js index 4e061c5..d1f6265 100644 --- a/ValueView/resources/jquery.time/jquery.time.timeinput.js +++ b/ValueView/resources/jquery.time/jquery.time.timeinput.js @@ -4,7 +4,7 @@ * @licence GNU GPL v2+ * @author H. Snater < [email protected] > * - * @event change: Triggered whenever the widget's value has changed. + * @event update: Triggered whenever the widget's value is updated. * (1) {jQuery.Event} * (2) {time.Time|null} New value (null for no or an invalid value) the widget's value has * been changed to. @@ -35,7 +35,7 @@ var value = self._parse(); if( value !== self._value ) { self._value = value; - self._trigger( 'change', null, [self._value] ); + self._trigger( 'update', null, [self._value] ); } } ); }, diff --git a/ValueView/resources/jquery.ui/jquery.ui.inputextender.css b/ValueView/resources/jquery.ui/jquery.ui.inputextender.css new file mode 100644 index 0000000..8f6e1ac --- /dev/null +++ b/ValueView/resources/jquery.ui/jquery.ui.inputextender.css @@ -0,0 +1,23 @@ +/** + * Default styles for input extender widget + * + * @license GNU GPL v2+ + * @author H. Snater < [email protected] > + */ + +.ui-inputextender .ui-inputextender-input { + display: inline; +} + +.ui-inputextender .ui-inputextender-extender { + display: inline; +} + +.ui-inputextender .ui-inputextender-contentcontainer { + position: absolute; + z-index: 1; + left: 2px; + font-size: 84%; + padding: 2px; + box-shadow: 2px 2px 6px -1px grey; +} diff --git a/ValueView/resources/jquery.ui/jquery.ui.inputextender.js b/ValueView/resources/jquery.ui/jquery.ui.inputextender.js new file mode 100644 index 0000000..a97ce51 --- /dev/null +++ b/ValueView/resources/jquery.ui/jquery.ui.inputextender.js @@ -0,0 +1,249 @@ +/** + * Input extender widget + * + * The input extender extends an input element with additional contents displayed underneath the. + * @licence GNU GPL v2+ + * @author H. Snater < [email protected] > + * + * @option {jQuery[]} [content] Default/"fixed" extender contents that always should be visible as + * long as the extension itself is visible. + * + * @option {jQuery[]} [extendedContent] Additional content that should only be displayed after + * clicking on the extender link. + * + * @option [messages] {Object} 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). + * messages['show options'] {String} (optional) Label of the link showing any additional + * contents. + * Default value: 'show options' + * messages['hide options'] {String} (optional) Label of the link hiding any additional + * contents. + * Default value: 'hide options' + * + * @dependency jQuery.Widget + */ +( function( $ ) { + 'use strict'; + + /** + * Whether loaded in MediaWiki context. + * @type {boolean} + */ + var IS_MW_CONTEXT = ( typeof mw !== 'undefined' && mw.msg ); + + /** + * Whether actual entity selector resource loader module is loaded. + * @type {boolean} + */ + var IS_MODULE_LOADED = ( + IS_MW_CONTEXT + && $.inArray( 'jquery.wikibase.entityselector', mw.loader.getModuleNames() ) !== -1 + ); + + /** + * Returns a message from the MediaWiki context if the input extender 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; + } + + $.widget( 'ui.inputextender', { + /** + * Additional options + * @type {Object} + */ + options: { + content: [], + extendedContent: [], + messages: { + 'show options': mwMsgOrString( 'valueview-inputextender-showoptions', 'show options' ), + 'hide options': mwMsgOrString( 'valueview-inputextender-hideoptions', 'hide options' ) + } + }, + + /** + * The widget parent's node. + * @type {jQuery} + */ + $parent: null, + + /** + * Container node wrapping the widget's whole DOM structure. + * @type {jQuery} + */ + $container: null, + + /** + * Container node containing the input element and the extender. + * @type {jQuery} + */ + $inputContainer: null, + + /** + * Node of the link to extended the extenders additional content. + * @type {jQuery} + */ + $extender: null, + + /** + * Node containing all the extension content. + * @type {jQuery} + */ + $contentContainer: null, + + /** + * Node of the default/"fixed" extension content. + * @type {jQuery} + */ + $content: null, + + /** + * Node of the additional extension content shown/hidden by the extender link. + * @type {jQuery} + */ + $extendedContent: null, + + /** + * @see jQuery.Widget._create + */ + _create: function() { + var self = this; + + this.$parent = this.element.parent(); + + if( !this.$parent.length ) { + throw new Error( 'Input extender widget needs to be in the DOM when initializing.' ); + } + + this.$container = $( '<div/>' ) + .addClass( this.widgetBaseClass ) + .data( this.widgetName, this ) + .appendTo( this.$parent ); + + this.$inputContainer = $( '<div />' ) + .addClass( this.widgetBaseClass + '-inputcontainer' ) + .append( this.element.addClass( this.widgetBaseClass + '-input' ).detach() ) + .appendTo( this.$container ); + + this.$extender = $( '<a/>' ) + .addClass( this.widgetBaseClass + '-extender' ) + .attr( 'href', 'javascript:void(0);' ) + .text( this.options.messages['show options'] ) + .appendTo( this.$inputContainer ) + .on( 'click', function( event ) { + self._toggleExtension(); + } ) + .hide(); + + this.$contentContainer = $( '<div/>' ) + .addClass( this.widgetBaseClass + '-contentcontainer ui-widget-content' ) + .appendTo( this.$container ) + .hide(); + + this.$content = $( '<div/>' ) + .addClass( this.widgetBaseClass + '-content' ) + .appendTo( this.$contentContainer ); + + this.$extendedContent = $( '<div/>' ) + .addClass( this.widgetBaseClass + '-extendedcontent' ) + .appendTo( this.$contentContainer ) + .hide(); + + this.element.add( this.$extender ) + .on( 'focus.' + this.widgetName, function( event ) { + self.showContent(); + } ) + .on( 'blur.' + this.widgetName, function( event ) { + self.hideContent(); + } ); + + this._draw(); + }, + + /** + * @see jQuery.Widget.destroy + */ + destroy: function() { + var $input = this.element.detach(); + this.$container.remove(); + this.$parent.append( $input ); + $.Widget.prototype.destroy.call( this ); + }, + + /** + * Draws the widget according to its current state. + */ + _draw: function() { + var self = this; + + this.$content.empty(); + + // Only show the extender when there are any additional options to extend: + this.$extender[ ( this.options.extendedContent.length ) ? 'show' : 'hide' ](); + + $.each( this.options.content, function( i, $node ) { + self.$content.append( $node ); + } ); + + $.each( this.options.extendedContent, function( i, $node ) { + self.$extendedContent.append( $node ); + } ); + }, + + /** + * Toggles the visibility of the additional options. + */ + _toggleExtension: function() { + var self = this; + + if( this.$extendedContent.is( ':visible' ) ) { + this.$extendedContent.slideUp( 150, function() { + self.$extender.text( self.options.messages['show options'] ); + self._trigger( 'toggle' ); + } ); + } else { + this.element.focus(); + this.$extendedContent.slideDown( 150, function() { + self.$extender.text( self.options.messages['hide options'] ); + self._trigger( 'toggle' ); + } ); + } + + }, + + /** + * Shows all the extension contents. + * + * @param {Function} [callback] Invoked as soon as the contents are visible. + */ + showContent: function( callback ) { + this.$contentContainer.fadeIn( 150, function() { + if( $.isFunction( callback ) ) { + callback(); + } + } ); + }, + + /** + * Hides all the extension contents. + * + * @param {Function} [callback] Invoked as soon as the contents are hidden. + */ + hideContent: function( callback ) { + this.$contentContainer.fadeOut( 150, function() { + if( $.isFunction( callback ) ) { + callback(); + } + } ); + } + + } ); + +} )( jQuery ); diff --git a/ValueView/tests/qunit/jquery.time/jquery.time.timeinput.tests.js b/ValueView/tests/qunit/jquery.time/jquery.time.timeinput.tests.js index 78d4f5a..80c46c3 100644 --- a/ValueView/tests/qunit/jquery.time/jquery.time.timeinput.tests.js +++ b/ValueView/tests/qunit/jquery.time/jquery.time.timeinput.tests.js @@ -34,7 +34,7 @@ ); }; - $input.on( 'timeinputchange', function( event, value ) { + $input.on( 'timeinputupdate', function( event, value ) { assertValue( value ); } ); diff --git a/ValueView/tests/qunit/jquery.ui/jquery.ui.inputextender.tests.js b/ValueView/tests/qunit/jquery.ui/jquery.ui.inputextender.tests.js new file mode 100644 index 0000000..4e6f739 --- /dev/null +++ b/ValueView/tests/qunit/jquery.ui/jquery.ui.inputextender.tests.js @@ -0,0 +1,130 @@ +/** + * @since 0.1 + * @ingroup ValueView + * + * @licence GNU GPL v2+ + * @author H. Snater < [email protected] > + */ + +( function( $, QUnit ) { + 'use strict'; + + /** + * Factory for creating an input extender widget suitable for testing. + */ + var newTestInputextender = function( options ) { + if( !options ) { + options = { + content: [ $( '<span/>' ).addClass( 'defaultContent' ).text( 'default content' ) ], + extendedContent: [ $( '<span/>' ).addClass( 'extendedContent' ).text( 'extended content' ) ] + } + } + + return $( '<input/>' ) + .addClass( 'test_inputextender' ) + .appendTo( $( 'body' ) ) + .inputextender( options ); + }; + + QUnit.module( 'jquery.ui.inputextender', QUnit.newMwEnvironment( { + teardown: function() { + $( '.test_inputextender' ).each( function( i, node ) { + $( node ).data( 'inputextender' ).destroy(); + $( node ).remove(); + } ); + } + } ) ); + + QUnit.test( 'Initialization', 1, function( assert ) { + var $input = newTestInputextender(), + extender = $input.data( 'inputextender' ); + + assert.ok( + !extender.$contentContainer.is( ':visible' ), + 'Content is not visible.' + ); + } ); + + QUnit.test( 'Show/Hide basic content', 4, function( assert ) { + var $input = newTestInputextender(), + extender = $input.data( 'inputextender' ); + + extender.showContent( function() { + assert.ok( + extender.$contentContainer.is( ':visible' ), + 'Content visible after focusing input element.' + ); + + assert.ok( + extender.$content.is( ':visible' ), + 'Default content is visible.' + ); + + assert.ok( + !extender.$extendedContent.is( ':visible' ), + 'Additional content is hidden.' + ); + } ); + + QUnit.stop(); + + extender.hideContent( function() { + assert.ok( + !extender.$contentContainer.is( ':visible' ), + 'Content is hidden after blurring the input element.' + ); + + QUnit.start(); + } ); + + } ); + + QUnit.test( 'Toggle additional content', 4, function( assert ) { + var $input = newTestInputextender(), + extender = $input.data( 'inputextender' ); + + var assertions = [ + function() { + assert.ok( + extender.$content.is( ':visible' ), + 'Default content is still visible after having clicked the extender link.' + ); + + assert.ok( + extender.$extendedContent.is( ':visible' ), + 'Additional content is visible after having clicked the extender link.' + ); + }, + function() { + assert.ok( + extender.$content.is( ':visible' ), + 'Default content is still visible after having clicked the extender link the second time.' + ); + + assert.ok( + !extender.$extendedContent.is( ':visible' ), + 'Additional content is hidden again after having clicked the extender link the second time.' + ); + } + ]; + + $input.on( 'inputextendertoggle', function( event ) { + assertions[0](); + $input + .off( 'inputextendertoggle' ) + .on( 'inputextendertoggle', function( event ) { + assertions[1](); + QUnit.start(); + } ); + QUnit.start(); + } ); + + // clicks will result into above event listeners being triggered + QUnit.stop(); + extender.$extender.click(); + + QUnit.stop(); + extender.$extender.click(); + } ); + +}( jQuery, QUnit ) ); -- To view, visit https://gerrit.wikimedia.org/r/62151 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I96af69359f555b61c48c638a483d756213e86856 Gerrit-PatchSet: 10 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
