Henning Snater has uploaded a new change for review.
https://gerrit.wikimedia.org/r/172507
Change subject: Implemented EditableTemplated widget
......................................................................
Implemented EditableTemplated widget
TemplatedWidget is supposed to be the common base constructor for all
wikibase.*view
widgets. ->72666
Change-Id: Ib21c2d50d1acd0c32024ad226956cd0840fe3e13
---
M lib/resources/Resources.php
A lib/resources/jquery.ui/jquery.ui.EditableTemplatedWidget.js
M lib/resources/jquery.wikibase/jquery.wikibase.aliasesview.js
M lib/resources/jquery.wikibase/resources.php
A lib/tests/qunit/jquery.ui/jquery.ui.EditableTemplatedWidget.tests.js
M lib/tests/qunit/jquery.wikibase/jquery.wikibase.aliasesview.tests.js
M lib/tests/qunit/resources.php
7 files changed, 487 insertions(+), 155 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Wikibase
refs/changes/07/172507/1
diff --git a/lib/resources/Resources.php b/lib/resources/Resources.php
index bd7ce2a..689f73a 100644
--- a/lib/resources/Resources.php
+++ b/lib/resources/Resources.php
@@ -140,6 +140,17 @@
),
),
+ 'jquery.ui.EditableTemplatedWidget' => $moduleTemplate + array(
+ 'scripts' => array(
+
'jquery.ui/jquery.ui.EditableTemplatedWidget.js',
+ ),
+ 'dependencies' => array(
+ 'jquery.ui.TemplatedWidget',
+ 'util.inherit',
+ 'wikibase.templates',
+ ),
+ ),
+
'jquery.ui.tagadata' => $moduleTemplate + array(
'scripts' => array(
'jquery.ui/jquery.ui.tagadata.js',
diff --git a/lib/resources/jquery.ui/jquery.ui.EditableTemplatedWidget.js
b/lib/resources/jquery.ui/jquery.ui.EditableTemplatedWidget.js
new file mode 100644
index 0000000..5d9daa2
--- /dev/null
+++ b/lib/resources/jquery.ui/jquery.ui.EditableTemplatedWidget.js
@@ -0,0 +1,257 @@
+/**
+ * @licence GNU GPL v2+
+ * @author H. Snater < [email protected] >
+ */
+( function( $ ) {
+ 'use strict';
+
+ var PARENT = $.ui.TemplatedWidget;
+
+/**
+ * TemplatedWidget enhanced with editing capabilities.
+ * @constructor
+ * @abstract
+ * @extends jQuery.ui.TemplatedWidget
+ * @since 0.5
+ *
+ * @option {*} [value]
+ *
+ * @event afterstartediting
+ * Triggered after having started the widget's edit mode and edit mode
has been rendered.
+ * - {jQuery.Event}
+ *
+ * @event stopediting
+ * Triggered when stopping the widget's edit mode, immediately before
re-drawing.
+ * - {jQuery.Event}
+ * - {boolean} dropValue
+ * Whether the widget's value will be reset to the one from before
starting edit mode.
+ *
+ * @event afterstopediting
+ * Triggered after having stopped the widget's edit mode and non-edit
mode is redrawn.
+ * - {jQuery.Event}
+ * - {boolean} dropValue
+ * Whether the widget's value has been reset to the one from before
starting edit mode.
+ *
+ * @event change
+ * Triggered whenever the widget's value is changed.
+ * - {jQuery.Event} event
+ *
+ * @event toggleerror
+ * Triggered when an error occurred or has been resolved.
+ * - {jQuery.Event}
+ * - {Error|undefined}
+ */
+$.widget( 'ui.EditableTemplatedWidget', PARENT, {
+ /**
+ * @see jQuery.ui.TemplatedWidget.options
+ */
+ options: $.extend( true, {}, PARENT.prototype.options, {
+ value: null
+ } ),
+
+ /**
+ * @see jQuery.ui.TemplatedWidget._create
+ */
+ _create: function() {
+ PARENT.prototype._create.call( this );
+ },
+
+ /**
+ * @see jQuery.ui.TemplatedWidget.destroy
+ */
+ destroy: function() {
+ this.element.removeClass( 'wb-edit' );
+ PARENT.prototype.destroy.call( this );
+ },
+
+ /**
+ * Draws the widget according to whether it is in edit mode or not.
+ *
+ * @return {Object} jQuery.Promise
+ * No resolved parameters.
+ * Rejected parameters:
+ * - {Error}
+ */
+ _draw: util.abstractMember,
+
+ /**
+ * Starts the widget's edit mode.
+ *
+ * @return {Object} jQuery.Promise
+ * No resolved parameters.
+ * Rejected parameters:
+ * - {Error}
+ */
+ startEditing: function() {
+ var deferred = $.Deferred();
+
+ if( this.isInEditMode() ) {
+ return deferred.resolve().promise();
+ }
+
+ var self = this;
+
+ self.element.addClass( 'wb-edit' );
+
+ this._draw()
+ .done( function() {
+ self._trigger( 'afterstartediting' );
+ deferred.resolve();
+ } )
+ .fail( function( error ) {
+ deferred.reject( error );
+ } );
+
+ return deferred.promise();
+ },
+
+ /**
+ * Stops the widget's edit mode.
+ *
+ * @param {boolean} dropValue
+ * @return {Object} jQuery.Promise
+ * Resolved parameters:
+ * - {boolean} dropValue
+ * Rejected parameters:
+ * - {Error}
+ */
+ stopEditing: function( dropValue ) {
+ var self = this,
+ deferred = $.Deferred();
+
+ if( !this.isInEditMode() || ( !this.isValid() ||
this.isInitialValue() ) && !dropValue ) {
+ return deferred.resolve().promise();
+ }
+
+ this._trigger( 'stopediting', null, [dropValue] );
+
+ this.disable();
+
+ if( dropValue ) {
+ return this._afterStopEditing( dropValue );
+ } else {
+ this._save()
+ .done( function() {
+ self.options.value = self.value();
+ self._afterStopEditing( dropValue )
+ .done( function() {
+ deferred.resolve( dropValue );
+ } )
+ .fail( function( error ) {
+ deferred.reject( error );
+ } );
+ } )
+ .fail( function( error ) {
+ self.setError( error );
+ deferred.reject( error );
+ } );
+ }
+
+ return deferred.promise();
+ },
+
+ /**
+ * @return {Object} jQuery.Promise
+ * No resolved parameters.
+ * Rejected parameters:
+ * - {Error}
+ */
+ _save: util.abstractMember,
+
+ /**
+ * @param {boolean} dropValue
+ * @return {Object} jQuery.Promise
+ * No resolved parameters.
+ * Rejected parameters:
+ * - {Error}
+ */
+ _afterStopEditing: function( dropValue ) {
+ var self = this,
+ deferred = $.Deferred();
+
+ self.element.removeClass( 'wb-edit' );
+
+ this._draw()
+ .done( function() {
+ self.enable();
+ self._trigger( 'afterstopediting', null, [dropValue] );
+ deferred.resolve( dropValue );
+ } )
+ .fail( function( error ) {
+ self.setError( error );
+ deferred.reject( error );
+ } );
+
+ return deferred.promise();
+ },
+
+ /**
+ * Cancels the widget's edit mode.
+ */
+ cancelEditing: function() {
+ this.stopEditing( true );
+ },
+
+ /**
+ * Returns whether the widget is in edit mode.
+ */
+ isInEditMode: function() {
+ return this.element.hasClass( 'wb-edit' );
+ },
+
+ /**
+ * Sets/Gets the widget's current value.
+ * When the widget is in edit mode, this.option( 'value' ) may be used
to retrieve the widget's
+ * value from before edit mode has been started.
+ *
+ * @param {*} [value]
+ * @return {*|undefined}
+ */
+ value: util.abstractMember,
+
+ /**
+ * Returns whether the widget features any value (may it be valid or
invalid).
+ *
+ * @return {boolean}
+ */
+ isEmpty: util.abstractMember,
+
+ /**
+ * Returns whether the widget's value is valid.
+ *
+ * @return {boolean}
+ */
+ isValid: util.abstractMember,
+
+ /**
+ * Returns whether the widget's value is the widget's value from before
starting edit mode.
+ * (Always returns "true" in non-edit mode.)
+ *
+ * @return {boolean}
+ */
+ isInitialValue: util.abstractMember,
+
+ /**
+ * Toggles error state.
+ *
+ * @param {Error} [error]
+ */
+ setError: function( error ) {
+ if( error ) {
+ this.element.addClass( 'wb-error' );
+ this._trigger( 'toggleerror', null, [error] );
+ } else {
+ this.removeError();
+ this._trigger( 'toggleerror', null, [null] );
+ }
+ },
+
+ /**
+ * Removes error state without triggering an event.
+ */
+ removeError: function() {
+ this.element.removeClass( 'wb-error' );
+ }
+} );
+
+}( jQuery ) );
diff --git a/lib/resources/jquery.wikibase/jquery.wikibase.aliasesview.js
b/lib/resources/jquery.wikibase/jquery.wikibase.aliasesview.js
index f94ded8..635ea8d 100644
--- a/lib/resources/jquery.wikibase/jquery.wikibase.aliasesview.js
+++ b/lib/resources/jquery.wikibase/jquery.wikibase.aliasesview.js
@@ -5,12 +5,12 @@
( function( $, mw, wb ) {
'use strict';
- var PARENT = $.ui.TemplatedWidget;
+ var PARENT = $.ui.EditableTemplatedWidget;
/**
- * Manages a aliases.
+ * Manages aliases.
* @since 0.5
- * @extends jQuery.ui.TemplatedWidget
+ * @extends jQuery.ui.EditableTemplatedWidget
*
* @option {wikibase.datamodel.MultiTerm} value
*
@@ -21,7 +21,7 @@
*/
$.widget( 'wikibase.aliasesview', PARENT, {
/**
- * @see jQuery.ui.TemplatedWidget.options
+ * @see jQuery.ui.EditableTemplatedWidget.options
*/
options: {
template: 'wikibase-aliasesview',
@@ -41,14 +41,7 @@
},
/**
- * @type {boolean}
- */
- _isInEditMode: false,
-
- /**
* @see jQuery.ui.TemplatedWidget._create
- *
- * @throws {Error} if required parameters are not specified properly.
*/
_create: function() {
if(
@@ -69,29 +62,14 @@
},
/**
- * @see jQuery.ui.TemplatedWidget.destroy
- */
- destroy: function() {
- if( this._isInEditMode ) {
- var self = this;
-
- this.element.one( this.widgetEventPrefix +
'afterstopediting', function( event ) {
- PARENT.prototype.destroy.call( self );
- } );
-
- this.cancelEditing();
- } else {
- PARENT.prototype.destroy.call( this );
- }
- },
-
- /**
- * Main draw routine.
+ * @see jQuery.ui.EditableTemplatedWidget._draw
*/
_draw: function() {
this.$list.off( '.' + this.widgetName );
- if( !this._isInEditMode ) {
+ if( this.isInEditMode() ) {
+ this._initTagadata();
+ } else {
var self = this,
tagadata = this.$list.data( 'tagadata' );
@@ -99,22 +77,14 @@
tagadata.destroy();
}
- this.element.removeClass( 'wb-edit' );
-
this.$list.empty();
- if( this.options.value ) {
- $.each( this.options.value.getTexts(),
function() {
- self.$list.append( mw.wbTemplate(
'wikibase-aliasesview-list-item', this ) );
- } );
- }
-
- return;
+ $.each( this.options.value.getTexts(), function() {
+ self.$list.append( mw.wbTemplate(
'wikibase-aliasesview-list-item', this ) );
+ } );
}
- this.element.addClass( 'wb-edit' );
-
- this._initTagadata();
+ return $.Deferred().resolve().promise();
},
/**
@@ -172,114 +142,24 @@
},
/**
- * Starts the widget's edit mode.
- */
- startEditing: function() {
- if( this._isInEditMode ) {
- return;
- }
-
- this._isInEditMode = true;
- this._draw();
-
- this._trigger( 'afterstartediting' );
- },
-
- /**
- * Stops the widget's edit mode.
- *
- * @param {boolean} dropValue
- */
- stopEditing: function( dropValue ) {
- var self = this;
-
- if( !this._isInEditMode || ( !this.isValid() ||
this.isInitialValue() ) && !dropValue ) {
- return;
- }
-
- if( dropValue ) {
- this._afterStopEditing( dropValue );
- return;
- }
-
- this.disable();
-
- this._trigger( 'stopediting', null, [dropValue] );
-
- // TODO: Performing API interaction should be managed in parent
component (probably
- // entityview)
- this._save()
- .done( function() {
- self.enable();
- self._afterStopEditing( dropValue );
- } )
- .fail( function( error ) {
- self.setError( error );
- } );
- },
-
- /**
- * @return {jQuery.Promise}
+ * @see jQuery.ui.EditableTemplatedWidget.save
*/
_save: function() {
return this.options.aliasesChanger.setAliases( this.value() );
},
/**
- * Cancels the widget's edit mode.
- */
- cancelEditing: function() {
- this.stopEditing( true );
- },
-
- /**
- * Callback tearing down edit mode.
- *
- * @param {boolean} dropValue
- */
- _afterStopEditing: function( dropValue ) {
- if( !dropValue ) {
- this.options.value = this.value();
- }
-
- this._isInEditMode = false;
- this._draw();
-
- this._trigger( 'afterstopediting', null, [dropValue] );
- },
-
- /**
- * @return {boolean}
+ * @see jQuery.ui.EditableTemplatedWidget.isValid
*/
isValid: function() {
- // Function required by edittoolbar.
return true;
},
/**
- * @return {boolean}
+ * @see jQuery.ui.EditableTemplatedWidget.isValid
*/
isInitialValue: function() {
return this.value().equals( this.options.value );
- },
-
- /**
- * Toggles error state.
- *
- * @param {Error} error
- */
- setError: function( error ) {
- if( error ) {
- this.element.addClass( 'wb-error' );
- this._trigger( 'toggleerror', null, [error] );
- } else {
- this.removeError();
- this._trigger( 'toggleerror' );
- }
- },
-
- removeError: function() {
- this.element.removeClass( 'wb-error' );
},
/**
@@ -292,7 +172,7 @@
var response = PARENT.prototype._setOption.call( this, key,
value );
- if( key === 'disabled' && this._isInEditMode ) {
+ if( key === 'disabled' && this.isInEditMode() ) {
this.$list.data( 'tagadata' ).option( 'disabled', value
);
}
@@ -311,7 +191,7 @@
return;
}
- if( !this._isInEditMode ) {
+ if( !this.isInEditMode() ) {
return this.option( 'value' );
}
@@ -329,7 +209,7 @@
* @see jQuery.ui.TemplatedWidget.focus
*/
focus: function() {
- if( this._isInEditMode ) {
+ if( this.isInEditMode() ) {
this.$list.data( 'tagadata' ).getHelperTag().find(
'input' ).focus();
} else {
this.element.focus();
diff --git a/lib/resources/jquery.wikibase/resources.php
b/lib/resources/jquery.wikibase/resources.php
index dbf4971..5add62c 100644
--- a/lib/resources/jquery.wikibase/resources.php
+++ b/lib/resources/jquery.wikibase/resources.php
@@ -29,7 +29,7 @@
'dependencies' => array(
'jquery.inputautoexpand',
'jquery.ui.tagadata',
- 'jquery.ui.TemplatedWidget',
+ 'jquery.ui.EditableTemplatedWidget',
'jquery.wikibase.edittoolbar',
'jquery.wikibase.toolbarcontroller',
'wikibase.datamodel.MultiTerm',
diff --git
a/lib/tests/qunit/jquery.ui/jquery.ui.EditableTemplatedWidget.tests.js
b/lib/tests/qunit/jquery.ui/jquery.ui.EditableTemplatedWidget.tests.js
new file mode 100644
index 0000000..0f1301d
--- /dev/null
+++ b/lib/tests/qunit/jquery.ui/jquery.ui.EditableTemplatedWidget.tests.js
@@ -0,0 +1,85 @@
+/**
+ * @licence GNU GPL v2+
+ * @author H. Snater < [email protected] >
+ */
+( function( mw, $, QUnit ) {
+ 'use strict';
+
+QUnit.module( 'jquery.ui.EditableTemplatedWidget', QUnit.newMwEnvironment( {
+ setup: function() {
+ $.widget( 'test.editablewidget', {
+ _create: function() {
+ this._initialValue = this.options.value;
+ },
+ _draw: function() {},
+ _save: function() {
+ return $.Deferred().resolve().promise();
+ },
+ value: function( value ) {
+ if( value === undefined ) {
+ this.option( 'value', value );
+ } else {
+ return this.option( 'value' );
+ }
+ },
+ isEmpty: function() {
+ return !this.option( 'value' );
+ },
+ isValid: function() {
+ return !!this.option( 'value' );
+ },
+ isInitialValue: function() {
+ return this.option( 'value' ) ===
this._initialValue;
+ }
+ } );
+ },
+ teardown: function() {
+ delete( $.test.editablewidget );
+
+ $( '.test_edittoolbar' ).each( function() {
+ var $edittoolbar = $( this ),
+ edittoolbar = $edittoolbar.data( 'edittoolbar'
);
+
+ if( edittoolbar ) {
+ edittoolbar.destroy();
+ }
+
+ $edittoolbar.remove();
+ } );
+ }
+} ) );
+
+QUnit.test( 'Create & destroy', function( assert ) {
+ var testSets = [
+ [
+ '<div><span>$1</span></div>',
+ {
+ templateParams: ['test']
+ }
+ ]
+ ];
+
+ for( var i = 0; i < testSets.length; i++ ) {
+ mw.wbTemplates.store.set( 'templatedWidget-test',
testSets[i][0] );
+
+ var $subject = $( '<div/>' );
+
+ $subject.editablewidget( $.extend( {
+ template: 'templatedWidget-test'
+ }, testSets[i][1] ) );
+
+ assert.ok(
+ $subject.data( 'editablewidget' ) instanceof
$.test.editablewidget,
+ 'Test set #' + i + ': Initialized widget.'
+ );
+
+ $subject.data( 'editablewidget' ).destroy();
+
+ assert.ok(
+ $subject.data( 'editablewidget' ) === undefined,
+ 'Destroyed widget.'
+ );
+ }
+} );
+
+}( mediaWiki, jQuery, QUnit ) );
diff --git
a/lib/tests/qunit/jquery.wikibase/jquery.wikibase.aliasesview.tests.js
b/lib/tests/qunit/jquery.wikibase/jquery.wikibase.aliasesview.tests.js
index 6b037b0..8108d8c 100644
--- a/lib/tests/qunit/jquery.wikibase/jquery.wikibase.aliasesview.tests.js
+++ b/lib/tests/qunit/jquery.wikibase/jquery.wikibase.aliasesview.tests.js
@@ -68,6 +68,30 @@
);
} );
+QUnit.test( 'Instantiating tagadata widget on startEditing()', function(
assert ) {
+ var $aliasesview = createAliasesview(),
+ aliasesview = $aliasesview.data( 'aliasesview' );
+
+ QUnit.stop();
+
+ aliasesview.startEditing()
+ .done( function() {
+ assert.ok(
+ aliasesview.$list.data( 'tagadata' ) !== undefined,
+ 'Instantiated tagadata widget.'
+ );
+ } )
+ .fail( function() {
+ assert.ok(
+ false,
+ 'Failed to start edit mode.'
+ );
+ } )
+ .always( function() {
+ QUnit.start();
+ } );
+} );
+
QUnit.test( 'startEditing() & stopEditing()', 7, function( assert ) {
var $aliasesview = createAliasesview(),
aliasesview = $aliasesview.data( 'aliasesview' );
@@ -86,29 +110,94 @@
);
} );
- aliasesview.startEditing();
+ /**
+ * @param {Function} func
+ * @param {boolean} expectingEvent
+ * @return {Object} jQuery.Promise
+ */
+ function testEditModeChange( func, expectingEvent ) {
+ var deferred = $.Deferred();
- assert.ok(
- aliasesview.$list.data( 'tagadata' ) !== undefined,
- 'Instantiated tagadata widget.'
- );
+ if( !expectingEvent ) {
+ func();
+ return deferred.resolve().promise();
+ }
- aliasesview.startEditing(); // should not trigger event
- aliasesview.stopEditing( true );
- aliasesview.stopEditing( true ); // should not trigger event
- aliasesview.stopEditing(); // should not trigger event
+ $aliasesview
+ .one( 'aliasesviewafterstartediting.aliasesviewtest', function(
event ) {
+ $aliasesview.off( '.aliasesviewtest' );
+ deferred.resolve();
+ } )
+ .one( 'aliasesviewafterstopediting.aliasesviewtest', function(
event, dropValue ) {
+ $aliasesview.off( '.aliasesviewtest' );
+ deferred.resolve();
+ } );
- aliasesview.startEditing();
+ func();
- aliasesview.$list.data( 'tagadata' ).getTags().first().find( 'input'
).val( 'b' );
+ return deferred.promise();
+ }
- aliasesview.stopEditing();
- aliasesview.startEditing();
+ var $queue = $( {} );
- aliasesview.$list.data( 'tagadata' ).getTags().first().removeClass(
'tagadata-choice-equal' )
- .find( 'input' ).val( 'd' );
+ /**
+ * @param {jQuery} $queue
+ * @param {Function} func
+ * @param {boolean} [expectingEvent]
+ */
+ function addToQueue( $queue, func, expectingEvent ) {
+ if( expectingEvent === undefined ) {
+ expectingEvent = true;
+ }
+ $queue.queue( 'tests', function( next ) {
+ QUnit.stop();
+ testEditModeChange( func, expectingEvent ).always(
function() {
+ QUnit.start();
+ next();
+ } );
+ } );
+ }
- aliasesview.stopEditing();
+ addToQueue( $queue, function() {
+ aliasesview.startEditing();
+ } );
+
+ addToQueue( $queue, function() {
+ aliasesview.startEditing();
+ }, false );
+
+ addToQueue( $queue, function() {
+ aliasesview.stopEditing( true );
+ } );
+
+ addToQueue( $queue, function() {
+ aliasesview.stopEditing( true );
+ }, false );
+
+ addToQueue( $queue, function() {
+ aliasesview.stopEditing();
+ }, false );
+
+ addToQueue( $queue, function() {
+ aliasesview.startEditing();
+ } );
+
+ addToQueue( $queue, function() {
+ aliasesview.$list.data( 'tagadata' ).getTags().first().find(
'input' ).val( 'b' );
+ aliasesview.stopEditing();
+ } );
+
+ addToQueue( $queue, function() {
+ aliasesview.startEditing();
+ } );
+
+ addToQueue( $queue, function() {
+ aliasesview.$list.data( 'tagadata' ).getTags().first()
+ .removeClass( 'tagadata-choice-equal' ).find( 'input'
).val( 'd' );
+ aliasesview.stopEditing();
+ } );
+
+ $queue.dequeue( 'tests' );
} );
QUnit.test( 'isInitialValue()', function( assert ) {
diff --git a/lib/tests/qunit/resources.php b/lib/tests/qunit/resources.php
index b3356e3..d030b5b 100644
--- a/lib/tests/qunit/resources.php
+++ b/lib/tests/qunit/resources.php
@@ -53,6 +53,16 @@
),
),
+ 'jquery.ui.EditableTemplatedWidget.tests' => $moduleBase +
array(
+ 'scripts' => array(
+
'jquery.ui/jquery.ui.EditableTemplatedWidget.tests.js',
+ ),
+ 'dependencies' => array(
+ 'jquery.ui.EditableTemplatedWidget',
+ 'wikibase.templates',
+ ),
+ ),
+
'jquery.ui.tagadata.tests' => $moduleBase + array(
'scripts' => array(
'jquery.ui/jquery.ui.tagadata.tests.js',
--
To view, visit https://gerrit.wikimedia.org/r/172507
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Ib21c2d50d1acd0c32024ad226956cd0840fe3e13
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Wikibase
Gerrit-Branch: master
Gerrit-Owner: Henning Snater <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits