jenkins-bot has submitted this change and it was merged.

Change subject: Implemented EditableTemplatedWidget
......................................................................


Implemented EditableTemplatedWidget

EdtableTemplatedWidget is supposed to be the common base constructor for
all wikibase.*view widgets. ->72666

Change-Id: Ib21c2d50d1acd0c32024ad226956cd0840fe3e13
---
M lib/resources/jquery.wikibase/jquery.wikibase.aliasesview.js
M lib/resources/jquery.wikibase/jquery.wikibase.sitelinkgroupview.js
M lib/resources/jquery.wikibase/jquery.wikibase.sitelinklistview.js
M lib/resources/jquery.wikibase/resources.php
M lib/tests/qunit/jquery.wikibase/jquery.wikibase.aliasesview.tests.js
M lib/tests/qunit/jquery.wikibase/jquery.wikibase.sitelinkgroupview.tests.js
M lib/tests/qunit/jquery.wikibase/jquery.wikibase.sitelinklistview.tests.js
M repo/resources/Resources.php
A repo/resources/jquery.ui/jquery.ui.EditableTemplatedWidget.js
A repo/tests/qunit/jquery.ui/jquery.ui.EditableTemplatedWidget.tests.js
M repo/tests/qunit/resources.php
11 files changed, 851 insertions(+), 465 deletions(-)

Approvals:
  Adrian Lang: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/lib/resources/jquery.wikibase/jquery.wikibase.aliasesview.js 
b/lib/resources/jquery.wikibase/jquery.wikibase.aliasesview.js
index f94ded8..bbb07c7 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(
@@ -64,34 +57,19 @@
                this.$label.text( mw.msg( 'wikibase-aliases-label' ) );
 
                if( this.$list.children( 'li' ).length !== 
this.options.value.getTexts().length ) {
-                       this._draw();
+                       this.draw();
                }
        },
 
        /**
-        * @see jQuery.ui.TemplatedWidget.destroy
+        * @see jQuery.ui.EditableTemplatedWidget.draw
         */
-       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.
-        */
-       _draw: function() {
+       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/jquery.wikibase.sitelinkgroupview.js 
b/lib/resources/jquery.wikibase/jquery.wikibase.sitelinkgroupview.js
index a02979b..d8f4616 100644
--- a/lib/resources/jquery.wikibase/jquery.wikibase.sitelinkgroupview.js
+++ b/lib/resources/jquery.wikibase/jquery.wikibase.sitelinkgroupview.js
@@ -5,12 +5,12 @@
 ( function( $, mw, wb ) {
        'use strict';
 
-       var PARENT = $.ui.TemplatedWidget;
+       var PARENT = $.ui.EditableTemplatedWidget;
 
 /**
  * Manages a sitelinklistview widget specific to a particular site link group.
  * @since 0.5
- * @extends jQuery.ui.TemplatedWidget
+ * @extends jQuery.ui.EditableTemplatedWidget
  *
  * @option {Object} value
  *         Object representing the widget's value.
@@ -23,24 +23,6 @@
  * @option {string} [helpMessage]
  *                  Default: 'Add a site link by specifying a site and a page 
of that site, edit or
  *                  remove existing site links.'
- *
- * @event change
- *        - {jQuery.Event}
- *
- * @event afterstartediting
- *       - {jQuery.Event}
- *
- * @event stopediting
- *        - {jQuery.Event}
- *        - {boolean} Whether to drop the value.
- *
- * @event afterstopediting
- *        - {jQuery.Event}
- *        - {boolean} Whether to drop the value.
- *
- * @event toggleerror
- *        - {jQuery.Event}
- *        - {Error|null}
  */
 $.widget( 'wikibase.sitelinkgroupview', PARENT, {
        /**
@@ -67,19 +49,12 @@
        },
 
        /**
-        * @type {boolean}
-        */
-       _isInEditMode: false,
-
-       /**
         * @type {jQuery}
         */
        $sitelinklistview: null,
 
        /**
         * @see jQuery.ui.TemplatedWidget._create
-        *
-        * @throws {Error} if required parameters are not specified properly.
         */
        _create: function() {
                if( !this.options.siteLinksChanger || !this.options.entityStore 
) {
@@ -99,19 +74,43 @@
                        this.$sitelinklistview = $( '<table/>' ).appendTo( 
this.element );
                }
 
-               this._createSitelinklistview();
-
-               this._update();
+               this.draw();
        },
 
        /**
-        * @see jQuery.ui.TemplatedWidget.destroy
+        * @see jQuery.ui.EditableTemplatedWidget.destroy
         */
        destroy: function() {
                if( this.$sitelinklistview ) {
                        this.$sitelinklistview.data( 'sitelinklistview' 
).destroy();
                }
                PARENT.prototype.destroy.call( this );
+       },
+
+       /**
+        * @see jQuery.ui.EditableTemplatedWidget.draw
+        */
+       draw: function() {
+               var deferred = $.Deferred();
+
+               this.element.data( 'group', this.options.value.group );
+
+               this.$h
+               .attr( 'id', 'sitelinks-' + this.options.value.group )
+//             .text( mw.msg( 'wikibase-sitelinks-' + this.options.value.group 
) )
+               .text( this.__headingText )
+               .append( this.$counter );
+
+               if( !this.$sitelinklistview.data( 'sitelinklistview' ) ) {
+                       this._createSitelinklistview();
+                       deferred.resolve();
+               } else {
+                       this.$sitelinklistview.data( 'sitelinklistview' ).draw()
+                               .done( deferred.resolve )
+                               .fail( deferred.reject );
+               }
+
+               return deferred.promise();
        },
 
        /**
@@ -151,8 +150,6 @@
                        siteLinksChanger: this.options.siteLinksChanger,
                        $counter: this.$counter
                } );
-
-               this._update();
        },
 
        /**
@@ -194,43 +191,33 @@
        },
 
        /**
-        * @return {boolean}
+        * @see jQuery.ui.EditableTemplatedWidget.startEditing
         */
-       isValid: function() {
-               return this.$sitelinklistview.data( 'sitelinklistview' 
).isValid();
-       },
-
-       /**
-        * @return {boolean}
-        */
-       isInitialValue: function() {
-               return this.$sitelinklistview.data( 'sitelinklistview' 
).isInitialValue();
-       },
-
        startEditing: function() {
-               if( this._isInEditMode ) {
-                       return;
-               }
+               var self = this,
+                       deferred = $.Deferred();
 
-               this._isInEditMode = true;
-               this.element.addClass( 'wb-edit' );
+               this.$sitelinklistview.one( 
'sitelinklistviewafterstartediting', function() {
+                       PARENT.prototype.startEditing.call( self )
+                               .done( deferred.resolve )
+                               .fail( deferred.reject );
+               } );
 
                this.$sitelinklistview.data( 'sitelinklistview' 
).startEditing();
 
-               this._trigger( 'afterstartediting' );
+               return deferred.promise();
        },
 
        /**
-        * @param {boolean} [dropValue]
+        * @see jQuery.ui.EditableTemplatedWidget.stopEditing
         */
        stopEditing: function( dropValue ) {
-               var self = this;
+               var self = this,
+                       deferred = $.Deferred();
 
-               if( !this._isInEditMode || ( !this.isValid() || 
this.isInitialValue() ) && !dropValue ) {
-                       return;
+               if( !this.isInEditMode() || ( !this.isValid() || 
this.isInitialValue() ) && !dropValue ) {
+                       return deferred.resolve().promise();
                }
-
-               dropValue = !!dropValue;
 
                this._trigger( 'stopediting', null, [dropValue] );
 
@@ -242,57 +229,22 @@
                        function( event, dropValue ) {
                                self._afterStopEditing( dropValue );
                                self.$sitelinklistview.off( 
'.sitelinkgroupviewstopediting' );
+                               deferred.resolve();
                        }
                )
-               .one( 
'sitelinklistviewtoggleerror.sitelinkgroupviewstopediting', function( event ) {
+               .one( 
'sitelinklistviewtoggleerror.sitelinkgroupviewstopediting', function( event, 
error ) {
                        self.enable();
                        self.$sitelinklistview.off( 
'.sitelinkgroupviewstopediting' );
+                       deferred.reject( error );
                } );
 
                this.$sitelinklistview.data( 'sitelinklistview' ).stopEditing( 
dropValue );
+
+               return deferred.promise();
        },
 
        /**
-        * @param {boolean} dropValue
-        */
-       _afterStopEditing: function( dropValue ) {
-               if( !dropValue ) {
-                       this.options.value = this.value();
-               }
-               this._isInEditMode = false;
-               this.enable();
-               this.element.removeClass( 'wb-edit' );
-               this._trigger( 'afterstopediting', null, [dropValue] );
-       },
-
-       cancelEditing: function() {
-               this.stopEditing( true );
-       },
-
-       /**
-        * @see jQuery.ui.TemplatedWidget.focus
-        */
-       focus: function() {
-               this.$sitelinklistview.data( 'sitelinklistview' ).focus();
-       },
-
-       /**
-        * Applies/Removes error state.
-        *
-        * @param {Error} [error]
-        */
-       setError: function( error ) {
-               if( error ) {
-                       this.element.addClass( 'wb-error' );
-                       this._trigger( 'toggleerror', null, [error] );
-               } else if( this.element.hasClass( 'wb-error' ) ) {
-                       this.element.removeClass( 'wb-error' );
-                       this._trigger( 'toggleerror' );
-               }
-       },
-
-       /**
-        * Sets/Gets the widget's value.
+        * @see jQuery.ui.EditableTemplatedWidget.value
         *
         * @param {Object} [value]
         * @return {Object|*}
@@ -302,6 +254,34 @@
                        return this.option( 'value' );
                }
                return this.option( 'value', value );
+       },
+
+       /**
+        * @see jQuery.ui.EditableTemplatedWidget.isEmpty
+        */
+       isEmpty: function() {
+               return !this.value().siteLinks.length;
+       },
+
+       /**
+        * @see jQuery.ui.EditableTemplatedWidget.isValid
+        */
+       isValid: function() {
+               return this.$sitelinklistview.data( 'sitelinklistview' 
).isValid();
+       },
+
+       /**
+        * @see jQuery.ui.EditableTemplatedWidget.isInitialValue
+        */
+       isInitialValue: function() {
+               return this.$sitelinklistview.data( 'sitelinklistview' 
).isInitialValue();
+       },
+
+       /**
+        * @see jQuery.ui.TemplatedWidget.focus
+        */
+       focus: function() {
+               this.$sitelinklistview.data( 'sitelinklistview' ).focus();
        },
 
        /**
@@ -319,25 +299,12 @@
                        .option( 'allowedSiteIds', getSiteIdsOfGroup( 
this.options.value.group ) )
                        .value( this.options.value.siteLinks );
 
-                       this._update();
+                       this.draw();
                } else if( key === 'disabled' ) {
                        this.$sitelinklistview.data( 'sitelinklistview' 
).option( key, value );
                }
 
                return response;
-       },
-
-       /**
-        * Updates the widget's group references.
-        */
-       _update: function() {
-               this.element.data( 'group', this.options.value.group );
-
-               this.$h
-               .attr( 'id', 'sitelinks-' + this.options.value.group )
-//             .text( mw.msg( 'wikibase-sitelinks-' + this.options.value.group 
) )
-               .text( this.__headingText )
-               .append( this.$counter );
        }
 } );
 
@@ -438,7 +405,7 @@
                                sitelinklistview = $sitelinklistview.data( 
'sitelinklistview' ),
                                sitelinklistviewListview = 
sitelinklistview.$listview.data( 'listview' );
 
-                       if( !$sitelinkgroupview.length || 
!sitelinkgroupview._isInEditMode ) {
+                       if( !$sitelinkgroupview.length || 
!sitelinkgroupview.isInEditMode() ) {
                                return;
                        }
 
diff --git a/lib/resources/jquery.wikibase/jquery.wikibase.sitelinklistview.js 
b/lib/resources/jquery.wikibase/jquery.wikibase.sitelinklistview.js
index 76bdce5..3274630 100644
--- a/lib/resources/jquery.wikibase/jquery.wikibase.sitelinklistview.js
+++ b/lib/resources/jquery.wikibase/jquery.wikibase.sitelinklistview.js
@@ -5,12 +5,12 @@
 ( function( mw, wb, $ ) {
        'use strict';
 
-       var PARENT = $.ui.TemplatedWidget;
+       var PARENT = $.ui.EditableTemplatedWidget;
 
 /**
  * Displays and allows editing multiple site links.
  * @since 0.5
- * @extends jQuery.ui.TemplatedWidget
+ * @extends jQuery.ui.EditableTemplatedWidget
  *
  * @option {wikibase.datamodel.SiteLink[]} [value]
  *         Default: []
@@ -29,27 +29,6 @@
  *         Whether to automatically show and add new input fields to add a new 
value when in edit
  *         mode.
  *         Default: true
- *
- * @event change
- *        - {jQuery.Event}
- *
- * @event afterstartediting
- *       - {jQuery.Event}
- *
- * @event stopediting
- *        - {jQuery.Event}
- *        - {boolean} Whether to drop the value.
- *
- * @event afterstopediting
- *        - {jQuery.Event}
- *        - {boolean} Whether to drop the value.
- *
- * @event afterremove
- *        - {jQuery.Event}
- *
- * @event toggleerror
- *        - {jQuery.Event}
- *        - {Error|null}
  */
 $.widget( 'wikibase.sitelinklistview', PARENT, {
        options: {
@@ -78,11 +57,6 @@
        },
 
        /**
-        * @type {boolean}
-        */
-       _isInEditMode: false,
-
-       /**
         * @see jQuery.ui.TemplatedWidget._create
         */
        _create: function() {
@@ -92,35 +66,16 @@
 
                PARENT.prototype._create.call( this );
 
-               this._createListView();
-
-               this.element.addClass( 'wikibase-sitelinklistview' );
-
                if( this.element.children( 'thead' ).children().length ) {
                        // Initially sort on the site id column.
                        this.element.tablesorter( { sortList: [{ 1: 'asc' }] } 
);
                }
 
-               this._refreshCounter();
-
-               if( this.options.autoInput && !this.isFull() ) {
-                       var self = this;
-
-                       this.element
-                       .on( this.widgetEventPrefix + 'afterstartediting', 
function() {
-                               self._updateAutoInput();
-                       } );
-               }
-
-               this.$thead.sticknode( {
-                       $container: this.element
-               } );
-
-               this._applyStickiness();
+               this.draw();
        },
 
        /**
-        * @see jQuery.ui.TemplatedWidget.destroy
+        * @see jQuery.ui.EditableTemplatedWidget.destroy
         */
        destroy: function() {
                this.$thead.data( 'sticknode' ).destroy();
@@ -129,6 +84,39 @@
                this.element.removeData( 'tablesorter' );
                this.element.removeClass( 'wikibase-sitelinklistview' );
                PARENT.prototype.destroy.call( this );
+       },
+
+       /**
+        * @see jQuery.ui.EditableTemplatedWidget.draw
+        */
+       draw: function() {
+               if( !this.$listview.data( 'listview' ) ) {
+                       this._createListView();
+               }
+
+               this._refreshCounter();
+
+               if( this.options.autoInput && !this.isFull() ) {
+                       var self = this,
+                               event = this.widgetEventPrefix + 
'afterstartediting.' + this.widgetName,
+                               updateAutoInput = function() {
+                                       self._updateAutoInput();
+                               };
+
+                       this.element
+                       .off( event, updateAutoInput )
+                       .on( event, updateAutoInput );
+               }
+
+               if( !this.$thead.data( 'sticknode' ) ) {
+                       this.$thead.sticknode( {
+                               $container: this.element
+                       } );
+
+                       this._applyStickiness();
+               }
+
+               return $.Deferred().resolve().promise();
        },
 
        /**
@@ -191,31 +179,6 @@
                                event.stopPropagation();
                        }
                )
-               .on( 'sitelinkviewstopediting.' + this.widgetName, function( 
event, dropValue, callback ) {
-                       event.stopPropagation();
-
-                       var $sitelinkview = $( event.target ),
-                               sitelinkview = $sitelinkview.data( 
'sitelinkview' ),
-                               value = sitelinkview.value();
-
-                       if( dropValue || sitelinkview.isInitialValue() ) {
-                               callback();
-                       } else {
-                               sitelinkview.disable();
-
-                               self._saveSiteLink( value )
-                               .done( function( newSiteLink ) {
-                                       sitelinkview.value( newSiteLink );
-                                       callback();
-                               } )
-                               .fail( function( error ) {
-                                       sitelinkview.setError( error );
-                               } )
-                               .always( function() {
-                                       sitelinkview.enable();
-                               } );
-                       }
-               } )
                .on(
                        'listviewitemremoved.' + this.widgetName
                        + ' listviewitemadded.' + this.widgetName,
@@ -245,11 +208,11 @@
                        self.$thead.find( 'th' ).each( function( i ) {
                                var $th = $( this );
 
-                               if( !self._isInEditMode ) {
+                               if( !self.isInEditMode() ) {
                                        $th.removeAttr( 'style' );
                                }
 
-                               if( i === 2 && !self._isInEditMode ) {
+                               if( i === 2 && !self.isInEditMode() ) {
                                        return;
                                }
 
@@ -408,6 +371,13 @@
        },
 
        /**
+        * @see jQuery.ui.EditableTemplatedWidget.isEmpty
+        */
+       isEmpty: function() {
+               return !this.$listview.data( 'listview' ).items().length;
+       },
+
+       /**
         * @return {boolean}
         */
        isValid: function() {
@@ -434,7 +404,7 @@
        },
 
        /**
-        * @return {boolean}
+        * @see jQuery.ui.EditableTemplatedWidget.isInitialValue
         */
        isInitialValue: function() {
                var currentValue = this.value();
@@ -464,19 +434,15 @@
                return true;
        },
 
+       /**
+        * @see jQuery.ui.EditableTemplatedWidget.startEditing
+        */
        startEditing: function() {
-               if( this._isInEditMode ) {
-                       return;
-               }
+               var self = this;
 
-               // Reset sort order before actually switching to edit mode 
since it should not be done while
-               // in edit mode (see comment in this._refreshTablesorter()).
                this._refreshTablesorter();
 
-               this._isInEditMode = true;
-               this.element.addClass( 'wb-edit' );
-
-               var listview = this.$listview.data( 'listview' ),
+               var listview = self.$listview.data( 'listview' ),
                        lia = listview.listItemAdapter();
 
                listview.items().each( function() {
@@ -484,27 +450,34 @@
                        sitelinkview.startEditing();
                } );
 
-               this._trigger( 'afterstartediting' );
+               return PARENT.prototype.startEditing.call( this );
        },
 
        /**
-        * @param {boolean} [dropValue]
+        * @see jQuery.ui.EditableTemplatedWidget.stopEditing
         */
        stopEditing: function( dropValue ) {
                var self = this;
 
-               if( !this._isInEditMode || ( !this.isValid() || 
this.isInitialValue() ) && !dropValue ) {
-                       return;
+               if( dropValue ) {
+                       self.$listview.data( 'listview' ).value( 
self.options.value );
+               } else {
+                       this._removeIncompleteSiteLinks();
                }
 
-               dropValue = !!dropValue;
+               return PARENT.prototype.stopEditing.call( this, dropValue )
+                       .done( function() {
+                               self.$listview.data( 'listview' ).value( 
self.value() );
+                       } );
+       },
 
-               this._trigger( 'stopediting', null, [dropValue] );
-
-               this.disable();
-               this._removeIncompleteSiteLinks();
-
-               var listview = this.$listview.data( 'listview' ),
+       /**
+        * @see jQuery.ui.EditableTemplatedWidget._save
+        */
+       _save: function() {
+               var self = this,
+                       deferred = $.Deferred(),
+                       listview = this.$listview.data( 'listview' ),
                        lia = listview.listItemAdapter();
 
                var $queue = $( {} );
@@ -525,52 +498,95 @@
                                        } )
                                        .fail( function( error ) {
                                                $queue.clearQueue( 
'stopediting' );
-                                               self.setError( error );
                                                self._resetEditMode();
+                                               deferred.reject( error );
                                        } );
                        } );
                }
 
-               if( !dropValue ) {
-                       var removedSiteLinkIds = this._getRemovedSiteLinkIds();
+               var removedSiteLinkIds = this._getRemovedSiteLinkIds();
 
-                       for( var i = 0; i < removedSiteLinkIds.length; i++ ) {
-                               addRemoveToQueue( $queue, removedSiteLinkIds[i] 
);
-                       }
+               for( var i = 0; i < removedSiteLinkIds.length; i++ ) {
+                       addRemoveToQueue( $queue, removedSiteLinkIds[i] );
                }
 
                /**
                 * @param {jQuery} $queue
                 * @param {jQuery.wikibase.sitelinkview} sitelinkview
-                * @param {boolean} dropValue
                 */
-               function addStopEditToQueue( $queue, sitelinkview, dropValue ) {
+               function addStopEditToQueue( $queue, sitelinkview ) {
                        $queue.queue( 'stopediting', function( next ) {
                                sitelinkview.element
+                               .one( 
'sitelinkviewstopediting.sitelinklistviewstopediting', function( event, 
dropValue, callback ) {
+                                       event.stopPropagation();
+
+                                       var $sitelinkview = $( event.target ),
+                                               sitelinkview = 
$sitelinkview.data( 'sitelinkview' ),
+                                               value = sitelinkview.value();
+
+                                       if( !dropValue && 
!sitelinkview.isInitialValue() ) {
+                                               sitelinkview.disable();
+
+                                               self._saveSiteLink( value )
+                                               .done( function( newSiteLink ) {
+                                                       sitelinkview.value( 
newSiteLink );
+                                                       callback();
+                                               } )
+                                               .fail( function( error ) {
+                                                       sitelinkview.setError( 
error );
+                                               } )
+                                               .always( function() {
+                                                       sitelinkview.enable();
+                                               } );
+                                       }
+                               } )
                                .one( 
'sitelinkviewafterstopediting.sitelinklistviewstopediting', function( event ) {
                                        sitelinkview.element.off( 
'.sitelinklistviewstopediting' );
                                        // Avoid exceeding call stack size.
                                        setTimeout( next, 0 );
                                } )
-                               .one( 
'sitelinkviewtoggleerror.sitelinklistviewstopediting', function() {
-                                       sitelinkview.element.off( 
'.sitelinklistviewstopediting' );
-                                       $queue.clearQueue( 'stopediting' );
-                                       self._resetEditMode();
-                               } );
-                               sitelinkview.stopEditing( dropValue );
+                               .one(
+                                       
'sitelinkviewtoggleerror.sitelinklistviewstopediting',
+                                       function( event, error ) {
+                                               sitelinkview.element.off( 
'.sitelinklistviewstopediting' );
+                                               $queue.clearQueue( 
'stopediting' );
+                                               self._resetEditMode();
+                                               deferred.reject( error );
+                                       }
+                               );
+                               sitelinkview.stopEditing();
                        } );
                }
 
                listview.items().each( function() {
                        var sitelinkview = lia.liInstance( $( this ) );
-                       addStopEditToQueue( $queue, sitelinkview, dropValue || 
sitelinkview.isInitialValue() );
+
+                       if( sitelinkview.isInitialValue() ) {
+                               sitelinkview.stopEditing( true );
+                       } else {
+                               addStopEditToQueue( $queue, sitelinkview );
+                       }
                } );
 
                $queue.queue( 'stopediting', function() {
-                       self._afterStopEditing( dropValue );
+                       deferred.resolve();
                } );
 
                $queue.dequeue( 'stopediting' );
+
+               return deferred.promise();
+       },
+
+       /**
+        * @see jQuery.ui.EditableTemplatedWidget._afterStopEditing
+        */
+       _afterStopEditing: function( dropValue ) {
+               var self = this;
+
+               return PARENT.prototype._afterStopEditing.call( this, dropValue 
)
+                       .done( function() {
+                               self.$listview.data( 'listview' ).value( 
self.options.value );
+                       } );
        },
 
        _removeIncompleteSiteLinks: function() {
@@ -610,26 +626,6 @@
                }
 
                return removedSiteLinkIds;
-       },
-
-       /**
-        * @param {boolean} dropValue
-        */
-       _afterStopEditing: function( dropValue ) {
-               if( !dropValue ) {
-                       this.options.value = this.value();
-               }
-               this.$listview.data( 'listview' ).value( this.options.value );
-               this._refreshCounter();
-               this._refreshTableHeader();
-               this._isInEditMode = false;
-               this.enable();
-               this.element.removeClass( 'wb-edit' );
-               this._trigger( 'afterstopediting', null, [dropValue] );
-       },
-
-       cancelEditing: function() {
-               this.stopEditing( true );
        },
 
        /**
@@ -681,22 +677,7 @@
        },
 
        /**
-        * Applies/Removes error state.
-        *
-        * @param {Error} [error]
-        */
-       setError: function( error ) {
-               if( error ) {
-                       this.element.addClass( 'wb-error' );
-                       this._trigger( 'toggleerror', null, [error] );
-               } else if( this.element.hasClass( 'wb-error' ) ) {
-                       this.element.removeClass( 'wb-error' );
-                       this._trigger( 'toggleerror' );
-               }
-       },
-
-       /**
-        * Sets/Gets the widget's value.
+        * @see jQuery.ui.EditableTemplatedWidget.value
         *
         * @param {wikibase.datamodel.SiteLink[]} [value]
         * @return {wikibase.datamodel.SiteLink[]|*}
@@ -852,7 +833,7 @@
                        self._refreshTableHeader();
                        self._refreshCounter();
 
-                       if( !self._isInEditMode ) {
+                       if( !self.isInEditMode() ) {
                                self.startEditing();
                        } else {
                                sitelinkview.startEditing();
@@ -865,7 +846,7 @@
        },
 
        _refreshTablesorter: function() {
-               if( this._isInEditMode ) {
+               if( this.isInEditMode() ) {
                        // Re-sorting the table involves reconstructing the 
table's DOM. This may lead to
                        // interference with focusing (IE, though it does not 
lose focus, is unable to reflect
                        // character input into the input box).
diff --git a/lib/resources/jquery.wikibase/resources.php 
b/lib/resources/jquery.wikibase/resources.php
index 312fb0b..b75eca7 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',
@@ -351,7 +351,7 @@
                                'jquery.event.special.eachchange',
                                'jquery.sticknode',
                                'jquery.tablesorter',
-                               'jquery.ui.TemplatedWidget',
+                               'jquery.ui.EditableTemplatedWidget',
                                'jquery.wikibase.addtoolbar',
                                'jquery.wikibase.edittoolbar',
                                'jquery.wikibase.listview',
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..52bed6e 100644
--- a/lib/tests/qunit/jquery.wikibase/jquery.wikibase.aliasesview.tests.js
+++ b/lib/tests/qunit/jquery.wikibase/jquery.wikibase.aliasesview.tests.js
@@ -68,7 +68,31 @@
        );
 } );
 
-QUnit.test( 'startEditing() & stopEditing()', 7, function( assert ) {
+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()', 6, 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/jquery.wikibase/jquery.wikibase.sitelinkgroupview.tests.js 
b/lib/tests/qunit/jquery.wikibase/jquery.wikibase.sitelinkgroupview.tests.js
index baa03e6..54ab99d 100644
--- a/lib/tests/qunit/jquery.wikibase/jquery.wikibase.sitelinkgroupview.tests.js
+++ b/lib/tests/qunit/jquery.wikibase/jquery.wikibase.sitelinkgroupview.tests.js
@@ -129,38 +129,94 @@
                );
        } );
 
-       function p1() {
-               $sitelinkgroupview.one( 'sitelinkgroupviewafterstartediting', 
p2 );
-               sitelinkgroupview.startEditing();
-               sitelinkgroupview.startEditing(); // should not trigger event
+       /**
+        * @param {Function} func
+        * @param {boolean} expectingEvent
+        * @return {Object} jQuery.Promise
+        */
+       function testEditModeChange( func, expectingEvent ) {
+               var deferred = $.Deferred();
+
+               if( !expectingEvent ) {
+                       func();
+                       return deferred.resolve().promise();
+               }
+
+               $sitelinkgroupview
+               .one( 
'sitelinkgroupviewafterstartediting.sitelinkgroupviewtest', function( event ) {
+                       $sitelinkgroupview.off( '.sitelinkgroupviewtest' );
+                       deferred.resolve();
+               } )
+               .one(
+                       
'sitelinkgroupviewafterstopediting.sitelinkgroupviewtest',
+                       function( event, dropValue ) {
+                               $sitelinkgroupview.off( 
'.sitelinkgroupviewtest' );
+                               deferred.resolve();
+                       }
+               );
+
+               func();
+
+               return deferred.promise();
        }
 
-       function p2() {
-               $sitelinkgroupview.one( 'sitelinkgroupviewafterstopediting', p3 
);
+       var $queue = $( {} );
+
+       /**
+        * @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();
+                       } );
+               } );
+       }
+
+       addToQueue( $queue, function() {
+               sitelinkgroupview.startEditing();
+       } );
+
+       addToQueue( $queue, function() {
+               sitelinkgroupview.startEditing();
+       }, false );
+
+       addToQueue( $queue, function() {
                sitelinkgroupview.stopEditing( true );
-               sitelinkgroupview.stopEditing( true ); // should not trigger 
event
-               sitelinkgroupview.stopEditing(); // should not trigger event
-       }
+       } );
 
-       function p3() {
-               $sitelinkgroupview.one( 'sitelinkgroupviewafterstartediting', 
p4 );
+       addToQueue( $queue, function() {
+               sitelinkgroupview.stopEditing( true );
+       }, false );
+
+       addToQueue( $queue, function() {
+               sitelinkgroupview.stopEditing();
+       }, false );
+
+       addToQueue( $queue, function() {
                sitelinkgroupview.startEditing();
+       } );
 
+       addToQueue( $queue, function() {
                // Mock adding a new item:
                var sitelinklistview = 
sitelinkgroupview.$sitelinklistview.data( 'sitelinklistview' ),
                        listview = sitelinklistview.$listview.data( 'listview' 
),
                        lia = listview.listItemAdapter(),
-                       $sitelinkview = listview.addItem( new 
wb.datamodel.SiteLink( 'aawiki', 'aawiki-page' ) );
+                       $sitelinkview = listview.addItem(
+                               new wb.datamodel.SiteLink( 'aawiki', 
'aawiki-page' )
+                       );
                lia.liInstance( $sitelinkview ).startEditing();
-       }
-
-       function p4() {
-               QUnit.start();
                sitelinkgroupview.stopEditing();
-       }
+       } );
 
-       p1();
-       QUnit.stop();
+       $queue.dequeue( 'tests' );
 } );
 
 QUnit.test( 'setError()', 1, function( assert ) {
diff --git 
a/lib/tests/qunit/jquery.wikibase/jquery.wikibase.sitelinklistview.tests.js 
b/lib/tests/qunit/jquery.wikibase/jquery.wikibase.sitelinklistview.tests.js
index fafedbe..8520ae3 100644
--- a/lib/tests/qunit/jquery.wikibase/jquery.wikibase.sitelinklistview.tests.js
+++ b/lib/tests/qunit/jquery.wikibase/jquery.wikibase.sitelinklistview.tests.js
@@ -220,41 +220,91 @@
                );
        } );
 
-       function p1() {
-               $sitelinklistview.one( 'sitelinklistviewafterstartediting', p2 
);
-               sitelinklistview.startEditing();
-               sitelinklistview.startEditing(); // should not trigger event
+       /**
+        * @param {Function} func
+        * @param {boolean} expectingEvent
+        * @return {Object} jQuery.Promise
+        */
+       function testEditModeChange( func, expectingEvent ) {
+               var deferred = $.Deferred();
+
+               if( !expectingEvent ) {
+                       func();
+                       return deferred.resolve().promise();
+               }
+
+               $sitelinklistview
+               .one( 'sitelinklistviewafterstartediting.sitelinklistviewtest', 
function( event ) {
+                       $sitelinklistview.off( '.sitelinklistviewtest' );
+                       deferred.resolve();
+               } )
+               .one(
+                       'sitelinklistviewafterstopediting.sitelinklistviewtest',
+                       function( event, dropValue ) {
+                               $sitelinklistview.off( '.sitelinklistviewtest' 
);
+                               deferred.resolve();
+                       }
+               );
+
+               func();
+
+               return deferred.promise();
        }
 
-       function p2() {
-               $sitelinklistview.one( 'sitelinklistviewafterstopediting', p3 );
+       var $queue = $( {} );
+
+       /**
+        * @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();
+                       } );
+               } );
+       }
+
+       addToQueue( $queue, function() {
+               sitelinklistview.startEditing();
+       } );
+
+       addToQueue( $queue, function() {
+               sitelinklistview.startEditing();
+       }, false );
+
+       addToQueue( $queue, function() {
                sitelinklistview.stopEditing( true );
-               sitelinklistview.stopEditing( true ); // should not trigger 
event
-               sitelinklistview.stopEditing(); // should not trigger event
-       }
+       } );
 
-       function p3() {
-               $sitelinklistview.one( 'sitelinklistviewafterstartediting', p4 
);
+       addToQueue( $queue, function() {
+               sitelinklistview.stopEditing( true );
+       }, false );
+
+       addToQueue( $queue, function() {
+               sitelinklistview.stopEditing();
+       }, false );
+
+       addToQueue( $queue, function() {
                sitelinklistview.startEditing();
+       } );
 
+       addToQueue( $queue, function() {
                // Mock adding a new item:
                var listview = sitelinklistview.$listview.data( 'listview' ),
                        lia = listview.listItemAdapter(),
                        $sitelinkview = listview.addItem( new 
wb.datamodel.SiteLink( 'aawiki', 'aawiki-page' ) );
                lia.liInstance( $sitelinkview ).startEditing();
-       }
+               sitelinklistview.stopEditing( true );
+       } );
 
-       function p4() {
-               $sitelinklistview.one( 'sitelinklistviewafterstopediting', p5 );
-               sitelinklistview.stopEditing( true ); // Have to drop item 
added above
-       }
-
-       function p5() {
-               QUnit.start();
-       }
-
-       p1();
-       QUnit.stop();
+       $queue.dequeue( 'tests' );
 } );
 
 QUnit.test( 'setError()', 1, function( assert ) {
diff --git a/repo/resources/Resources.php b/repo/resources/Resources.php
index 2fc36a9..531cc76 100644
--- a/repo/resources/Resources.php
+++ b/repo/resources/Resources.php
@@ -25,6 +25,17 @@
 
        $modules = array(
 
+               'jquery.ui.EditableTemplatedWidget' => $moduleTemplate + array(
+                       'scripts' => array(
+                               
'jquery.ui/jquery.ui.EditableTemplatedWidget.js',
+                       ),
+                       'dependencies' => array(
+                               'jquery.ui.TemplatedWidget',
+                               'util.inherit',
+                               'wikibase.templates',
+                       ),
+               ),
+
                'jquery.ui.TemplatedWidget' => $moduleTemplate + array(
                        'scripts' => array(
                                'jquery.ui/jquery.ui.TemplatedWidget.js',
diff --git a/repo/resources/jquery.ui/jquery.ui.EditableTemplatedWidget.js 
b/repo/resources/jquery.ui/jquery.ui.EditableTemplatedWidget.js
new file mode 100644
index 0000000..a994f12
--- /dev/null
+++ b/repo/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/repo/tests/qunit/jquery.ui/jquery.ui.EditableTemplatedWidget.tests.js 
b/repo/tests/qunit/jquery.ui/jquery.ui.EditableTemplatedWidget.tests.js
new file mode 100644
index 0000000..0f1301d
--- /dev/null
+++ b/repo/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/repo/tests/qunit/resources.php b/repo/tests/qunit/resources.php
index b2730c7..750a851 100644
--- a/repo/tests/qunit/resources.php
+++ b/repo/tests/qunit/resources.php
@@ -17,6 +17,16 @@
 
        $modules = array(
 
+               'jquery.ui.EditableTemplatedWidget.tests' => $moduleBase + 
array(
+                       'scripts' => array(
+                               
'jquery.ui/jquery.ui.EditableTemplatedWidget.tests.js',
+                       ),
+                       'dependencies' => array(
+                               'jquery.ui.EditableTemplatedWidget',
+                               'wikibase.templates',
+                       ),
+               ),
+
                'jquery.ui.TemplatedWidget.tests' => $moduleBase + array(
                        'scripts' => array(
                                'jquery.ui/jquery.ui.TemplatedWidget.tests.js',

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

Gerrit-MessageType: merged
Gerrit-Change-Id: Ib21c2d50d1acd0c32024ad226956cd0840fe3e13
Gerrit-PatchSet: 8
Gerrit-Project: mediawiki/extensions/Wikibase
Gerrit-Branch: master
Gerrit-Owner: Henning Snater <[email protected]>
Gerrit-Reviewer: Adrian Lang <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

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

Reply via email to