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

Change subject: Overhauled snakview
......................................................................


Overhauled snakview

- value() accepts and returns plain objects (incomplete or complete 
serialization).
- Serialization returned by value() does not contain fields with "null" values 
anymore; Instead,
  those fields are omitted.
- snak() accepts and returns Snak objects or "null".
- Actual value setting logic is moved to _setOption to comply with widget 
standards.
- As to the current "value" option specification, options.value may be a Snak 
object or an
  (in)complete Snak serialization.
- Some useless functions are removed. isInitialSnak() is removed in favour of 
isInitialValue()
  which complies to the common widget standard.
- The initial value/Snak may be retrieved using .option( 'value' ).
- Removed nontransparent caching of current values (_propertyId, _snakType). 
Instead, those
  values are acquired from respective components (in edit mode) or extracted 
from the initial
  value (in non-edit mode).
- Discrepancy between Variation returning a deserialized DataValue and the 
SnakSerializer
  returning a serialized DataValue is not resolved in this change.

Change-Id: I2a3107644a8bf95c4b3f4678a9a51858c0676bce
---
M lib/resources/jquery.wikibase/jquery.wikibase.entityselector.js
M lib/resources/jquery.wikibase/jquery.wikibase.snaklistview.js
M lib/resources/jquery.wikibase/jquery.wikibase.statementview.js
M lib/resources/jquery.wikibase/snakview/resources.php
M lib/resources/jquery.wikibase/snakview/snakview.js
M lib/resources/jquery.wikibase/snakview/snakview.variations.Value.js
M lib/resources/jquery.wikibase/snakview/snakview.variations.Variation.js
M 
lib/resources/jquery.wikibase/toolbar/controller/definitions/removetoolbar/referenceview-snakview.js
M lib/tests/qunit/jquery.wikibase/jquery.wikibase.snaklistview.tests.js
M 
lib/tests/qunit/jquery.wikibase/jquery.wikibase.statementgrouplistview.tests.js
M lib/tests/qunit/jquery.wikibase/snakview/resources.php
M lib/tests/qunit/jquery.wikibase/snakview/snakview.tests.js
12 files changed, 474 insertions(+), 261 deletions(-)

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



diff --git a/lib/resources/jquery.wikibase/jquery.wikibase.entityselector.js 
b/lib/resources/jquery.wikibase/jquery.wikibase.entityselector.js
index ec81297..2d444d6 100644
--- a/lib/resources/jquery.wikibase/jquery.wikibase.entityselector.js
+++ b/lib/resources/jquery.wikibase/jquery.wikibase.entityselector.js
@@ -186,7 +186,7 @@
                                        return;
                                }
 
-                               if( self._termMatchesLabel( requestTerm, 
suggestions[0] ) ) {
+                               if( self._termMatchesSuggestion( requestTerm, 
suggestions[0] ) ) {
                                        self._select( suggestions[0] );
                                }
                        } );
@@ -201,10 +201,11 @@
         * @param {Object} suggestion
         * @return {boolean}
         */
-       _termMatchesLabel: function( term, suggestion ) {
+       _termMatchesSuggestion: function( term, suggestion ) {
                var label = suggestion.label || suggestion.id;
                return label === term
-                       || !this.options.caseSensitive && label.toLowerCase() 
=== term.toLowerCase();
+                       || !this.options.caseSensitive && label.toLowerCase() 
=== term.toLowerCase()
+                       || term === suggestion.id;
        },
 
        /**
@@ -453,7 +454,7 @@
        /**
         * Gets the selected entity.
         *
-        * @return {Object} Plain object featuring `` Entity``` stub data.
+        * @return {Object} Plain object featuring `Entity` stub data.
         */
        selectedEntity: function() {
                // TODO: Implement setter.
diff --git a/lib/resources/jquery.wikibase/jquery.wikibase.snaklistview.js 
b/lib/resources/jquery.wikibase/jquery.wikibase.snaklistview.js
index 2bfb07c..7474e77 100644
--- a/lib/resources/jquery.wikibase/jquery.wikibase.snaklistview.js
+++ b/lib/resources/jquery.wikibase/jquery.wikibase.snaklistview.js
@@ -296,7 +296,7 @@
                var self = this;
 
                this.$listview.one( this._lia.prefixedEvent( 'stopediting.' + 
this.widgetName ),
-                       function( event, dropValue, newSnak ) {
+                       function( event, dropValue ) {
                                event.stopImmediatePropagation();
                                event.preventDefault();
                                self._detachEditModeEventHandlers();
@@ -381,10 +381,6 @@
                var $snakview = this._listview.addItem();
 
                this.startEditing();
-
-               // Since the new snakview will be initialized empty which 
invalidates the snaklistview,
-               // external components using the snaklistview will be noticed 
via the "change" event.
-               this._trigger( 'change' );
 
                return $.Deferred().resolve( $snakview ).promise();
        },
diff --git a/lib/resources/jquery.wikibase/jquery.wikibase.statementview.js 
b/lib/resources/jquery.wikibase/jquery.wikibase.statementview.js
index 3662004..5c77baf 100644
--- a/lib/resources/jquery.wikibase/jquery.wikibase.statementview.js
+++ b/lib/resources/jquery.wikibase/jquery.wikibase.statementview.js
@@ -235,7 +235,7 @@
                } );
 
                this.$mainSnak.snakview( {
-                       value: snak || null,
+                       value: snak || undefined,
                        locked: this.options.locked.mainSnak,
                        autoStartEditing: false,
                        dataTypeStore: this.options.dataTypeStore,
@@ -523,7 +523,7 @@
                        }
                }
 
-               return this.$mainSnak.data( 'snakview' ).isInitialSnak();
+               return this.$mainSnak.data( 'snakview' ).isInitialValue();
        },
 
        /**
diff --git a/lib/resources/jquery.wikibase/snakview/resources.php 
b/lib/resources/jquery.wikibase/snakview/resources.php
index e4cf269..f6f391b 100644
--- a/lib/resources/jquery.wikibase/snakview/resources.php
+++ b/lib/resources/jquery.wikibase/snakview/resources.php
@@ -25,6 +25,7 @@
                                'themes/default/snakview.SnakTypeSelector.css',
                        ),
                        'dependencies' => array(
+                               'dataValues.DataValue',
                                'jquery.event.special.eachchange',
                                'jquery.ui.position',
                                'jquery.ui.TemplatedWidget',
diff --git a/lib/resources/jquery.wikibase/snakview/snakview.js 
b/lib/resources/jquery.wikibase/snakview/snakview.js
index 570e4b3..1635e2d 100644
--- a/lib/resources/jquery.wikibase/snakview/snakview.js
+++ b/lib/resources/jquery.wikibase/snakview/snakview.js
@@ -1,4 +1,4 @@
-( function( mw, wb, $ ) {
+( function( mw, wb, $, dv ) {
        'use strict';
 
        // Back-up components already initialized in the namespace to re-apply 
them after initializing
@@ -20,14 +20,14 @@
  * @author Daniel Werner < [email protected] >
  * @author H. Snater < [email protected] >
  *
- * @costructor
+ * @constructor
  *
  * @param {Object} options
  * @param {Object|wikibase.datamodel.Snak|null} [options.value]
  *        The `Snak` this `snakview` should represent initially. If omitted, 
an empty view will be
  *        served, ready to take some input by the user. The value may be 
overwritten later, by using
  *        the `value()` or the `snak()` function.
- *        Default: `{ property: null, snaktype: 
wikibase.datamodel.PropertyValueSnak.TYPE }`
+ *        Default: `{ snaktype: wikibase.datamodel.PropertyValueSnak.TYPE }`
  * @param {Object|boolean} [options.locked=false]
  *        Key-value pairs determining which `snakview` elements to lock from 
being edited by the
  *        user. May also be a boolean value enabling/disabling all elements. 
If `false`, no elements
@@ -53,10 +53,6 @@
  * Triggered when stopping the widget's edit mode.
  * @param {jQuery.Event} event
  * @param {boolean} dropValue
- * @param {wikibase.datamodel.Snak|null} newSnak
- *        The `Snak` which will be displayed after editing has stopped. 
Normally, this is the `Snak`
- *        representing the last state of the `snakview` during edit mode but 
can also be the `Snak`
- *        from before edit mode in case editing has been cancelled.
  */
 /**
  * @event afterstopediting
@@ -83,7 +79,6 @@
                        '$snakTypeSelector': '.wikibase-snakview-typeselector'
                },
                value: {
-                       property: null,
                        snaktype: wb.datamodel.PropertyValueSnak.TYPE
                },
                locked: {
@@ -138,27 +133,6 @@
        _cachedValues: null,
 
        /**
-        * The `Property` id of the `Snak` currently represented by the 
`snakview`.
-        * @property {string}
-        * @private
-        */
-       _propertyId: null,
-
-       /**
-        * The `Snak` type of the `Snak` currently represented by the 
`snakvview`.
-        * @property {string}
-        * @private
-        */
-       _snakType: null,
-
-       /**
-        * The `Snak` from before edit mode was started.
-        * @property {wikibase.datamodel.Snak|null}
-        * @private
-        */
-       _initialSnak: null,
-
-       /**
         * @property {boolean}
         * @private
         */
@@ -188,7 +162,7 @@
 
                this._cachedValues = {};
 
-               this.value( this.option( 'value' ) || {} );
+               this.value( this.options.value );
 
                if( this.option( 'autoStartEditing' ) && !this.snak() ) {
                        // If no Snak is represented, offer UI to build one.
@@ -200,9 +174,19 @@
        /**
         * @inheritdoc
         * @protected
+        *
+        * @throws {Error} when trying to set an invalid value.
         */
        _setOption: function( key, value ) {
-               if ( key === 'locked' && typeof value === 'boolean' ) {
+               if( key === 'value' ) {
+                       if(
+                               value !== null
+                               && !$.isPlainObject( value ) && !( value 
instanceof wb.datamodel.Snak )
+                       ) {
+                               throw new Error( 'The given value has to be a 
plain object, an instance of '
+                                       + 'wikibase.datamodel.Snak, or null' );
+                       }
+               } else if( key === 'locked' && typeof value === 'boolean' ) {
                        var locked = value;
                        value = $.extend( {}, 
$.wikibase.snakview.prototype.options.locked );
                        $.each( $.wikibase.snakview.prototype.options.locked, 
function( k, v ) {
@@ -212,8 +196,27 @@
 
                var response = PARENT.prototype._setOption.apply( this, 
arguments );
 
-               if( key === 'disabled' ) {
+               if( key === 'value' ) {
+                       value = this.value();
+
+                       this._updateVariation( value );
+
                        this.draw();
+               } else if( key === 'disabled' ) {
+                       var propertySelector = this._getPropertySelector(),
+                               snakTypeSelector = this._getSnakTypeSelector();
+
+                       if( propertySelector ) {
+                               propertySelector.option( 'disabled', key );
+                       }
+
+                       if( snakTypeSelector ) {
+                               snakTypeSelector.option( 'disabled', key );
+                       }
+
+                       if( this._variation ) {
+                               this._variation[value ? 'disable' : 'enable']();
+                       }
                }
 
                return response;
@@ -241,26 +244,22 @@
                .on( 'eachchange', function( event, oldValue ) {
                        // remove out-dated variations
                        if( self._variation ) {
-                               self.propertyId( null );
+                               self.drawSnakTypeSelector();
+                               self._updateVariation( self.value() );
+                               self.drawVariation();
                                self._trigger( 'change' );
                        }
                } )
                .on( 'entityselectorselected', function( e, entityId ) {
-                       // Display spinner as long as the value view is 
loading. There is no need to display the
-                       // spinner when the selected item actually has not 
changed since the variation will stay
-                       // in place.
-                       if( !self._propertyId || self._propertyId !== entityId 
) {
-                               // Reset the cached property id for 
re-rendering being triggered as soon as the new
-                               // property's attributes have been received:
-                               self.propertyId( null );
-
-                               self.$snakValue.empty().append(
-                                       $( '<div/>' ).append( $( '<span/>' 
).addClass( 'mw-small-spinner' ) )
-                               );
-                       }
+                       // Display spinner as long as the ValueView is loading:
+                       self.$snakValue.empty().append(
+                               $( '<div/>' ).append( $( '<span/>' ).addClass( 
'mw-small-spinner' ) )
+                       );
 
                        self.options.entityStore.get( entityId ).done( 
function( entity ) {
-                               self.propertyId( entityId );
+                               self._updateVariation( self.value() );
+                               self.drawSnakTypeSelector();
+                               self.drawVariation();
 
                                self._trigger( 'change' );
 
@@ -298,7 +297,6 @@
 
                var self = this;
 
-               this._initialSnak = this.snak();
                this._isInEditMode = true;
 
                this.element.on( 'keydown.' + this.widgetName, function( event 
) {
@@ -363,32 +361,32 @@
                        return;
                }
 
-               var newSnak = null;
+               var snak = this.snak();
 
-               if( dropValue ) {
-                       newSnak = this._initialSnak;
-               } else if( this._variation ) {
-                       newSnak = this.snak();
-               }
-
-               this._trigger( 'stopediting', [!!dropValue, newSnak] );
+               this._trigger( 'stopediting', null, [dropValue] );
 
                this._isInEditMode = false;
-               this._initialSnak = null;
 
                if( this._variation ) {
                        this._variation.stopEditing( dropValue );
+
+                       if( !dropValue ) {
+                               // TODO: "this.snak( this.snak() )" is supposed 
to work to update the Snak. However,
+                               // the Variation asking the ValueView returns 
null as soon as edit mode is left.
+                               this.snak( snak );
+                       }
                }
 
-               // update view; will remove edit interfaces and represent value 
statically
-               this._setValue( newSnak !== null ? this._serializeSnak( newSnak 
) : {} );
+               if( !this._variation || dropValue ) {
+                       this.value( this.options.value );
+               }
+
                // TODO: Should throw an error somewhere when trying to leave 
edit mode while this.snak()
-               //  still returns null. For now, setting {} is a simple 
solution for non-existent error
-               //  handling in the snak UI.
+               //  still returns null.
 
                this.element.off( 'keydown.' + this.widgetName );
 
-               this._trigger( 'afterStopEditing', null, [dropValue, newSnak] );
+               this._trigger( 'afterStopEditing', null, [dropValue] );
        },
 
        /**
@@ -426,20 +424,31 @@
        },
 
        /**
-        * Returns whether the current `Snak` matches the one the `snakview` 
has been initialized with.
-        * @since 0.4
+        * Returns whether the current value matches the one the `snakview` was 
initialized with by
+        * comparing the (deserialized) `Snak` objects of that stages.
+        * @since 0.5
         *
         * @return {boolean}
         */
-       isInitialSnak: function() {
-               var snak = this.snak(),
-                       initialSnak = this.initialSnak();
+       isInitialValue: function() {
+               var currentSnak = this.snak(),
+                       initialSnak;
 
-               if( !initialSnak && !snak ) {
-                       // No snaks at all, but we consider this situation as 
having same Snaks anyhow.
+               if( this.options.value instanceof wb.datamodel.Snak ) {
+                       initialSnak = this.options.value;
+               } else {
+                       var snakDeserializer = new 
wb.serialization.SnakDeserializer();
+                       try {
+                               initialSnak = snakDeserializer.deserialize( 
this.options.value );
+                       } catch( e ) {
+                               initialSnak = null;
+                       }
+               }
+
+               if( !initialSnak && !currentSnak ) {
                        return true;
                }
-               return snak && snak.equals( initialSnak );
+               return currentSnak && currentSnak.equals( initialSnak );
        },
 
        /**
@@ -478,209 +487,137 @@
        },
 
        /**
-        * Returns the initial value from before edit mode was started. If not 
in edit mode, this will
-        * return the same as `value()`.
-        *
-        * @return {Object}
-        */
-       initialValue: function() {
-               return this.isInEditMode() ? this._serializeSnak( 
this.initialSnak() ) : this._getValue();
-       },
-
-       /**
-        * Just like `initialValue()`, but returns a `Snak` object or `null` if 
there was no `Snak` set
-        * before starting edit mode.
-        *
-        * @return {wikibase.datamodel.Snak|null}
-        */
-       initialSnak: function() {
-               return this.isInEditMode() ? this._initialSnak : this.snak();
-       },
-
-       /**
         * Returns an object representing the currently displayed `Snak`. This 
is equivalent to the JSON
         * structure of a `Snak`, except that it does not have to be complete. 
For example, for a
         * `PropertyValueSnak` where only the `Property` and `Snak` type are 
specified, but the value
         * has not yet been supplied, the returned object would not have a 
field for the value either.
         *
         * @param {Object|wikibase.datamodel.Snak|null} [value]
-        * @return {wikibase.datamodel.Snak|null|undefined} `undefined` in case 
`value()` is called to
+        * @return {wikibase.datamodel.Snak|Object|undefined} `undefined` in 
case `value()` is called to
         *         set the value.
         */
        value: function( value ) {
-               if( value === undefined ) {
-                       return this._getValue();
-               }
-               if( value !== null && typeof value !== 'object' ) {
-                       throw new Error( 'The given value has to be a plain 
object, an instance of '
-                               + 'wikibase.datamodel.Snak, or null' );
-               }
-               this._setValue( value instanceof wb.datamodel.Snak ? 
this._serializeSnak( value ) : value );
-       },
-
-       /**
-        * @private
-        *
-        * @param {wikibase.datamodel.Snak} snak
-        * @return {Object}
-        */
-       _serializeSnak: function( snak ) {
-               var snakSerializer = new 
wikibase.serialization.SnakSerializer();
-               return snakSerializer.serialize( snak );
-       },
-
-       /**
-        * @private
-        *
-        * @return {Object}
-        */
-       _getValue: function() {
-               var value = {
-                       property: this.propertyId(),
-                       snaktype: this.snakType()
-               };
-
-               if( !this._variation ) {
-                       return value;
+               if( value !== undefined ) {
+                       this.option( 'value', value );
+                       return;
                }
 
-               return $.extend( this._variation.value(), value );
-       },
+               var snakSerializer = new 
wikibase.serialization.SnakSerializer(),
+                       serialization = this.options.value instanceof 
wb.datamodel.Snak
+                               ? snakSerializer.serialize( this.options.value )
+                               : this.options.value;
 
-       /**
-        * Updates the `snakview` to represent a given `Snak` in form of a 
plain object. The given
-        * object can have all (or a subset of) fields a serialized 
`wikibase.datamodel.Snak` features.
-        * @private
-        * @since 0.4
-        *
-        * @param {Object|null} value
-        */
-       _setValue: function( value ) {
-               if( this._snakType && this._variation ) {
-                       this._cachedValues[this._snakType] = 
this._variation.value();
+               if( !this.isInEditMode() ) {
+                       return serialization;
                }
 
-               value = value || {};
+               value = {};
 
-               this._propertyId = value.property || null;
-               this._snakType = value.snaktype || null ;
-
-               this._updateVariation();
-
-               if( this._variation ) {
-                       // give other Snak information to variation object. 
Remove basic info since these should
-                       // rather be accessed via the variation's ViewState 
object. Also, use a fresh object so
-                       // the given object doesn't change for outside world.
-                       var valueCopy = $.extend( {}, value );
-                       delete valueCopy.property;
-                       delete valueCopy.snaktype;
-
-                       this._variation.value( valueCopy );
+               if( this.options.locked.property && serialization.property !== 
undefined ) {
+                       value.property = serialization.property;
+               } else if( !this.options.locked.property ) {
+                       var propertySelector = this._getPropertySelector(),
+                               propertyStub = propertySelector && 
propertySelector.selectedEntity();
+                       if( propertyStub && propertyStub.id !== undefined ) {
+                               value.property = propertyStub.id;
+                       }
                }
 
-               this.draw();
-       },
+               if( this.options.locked.snaktype && serialization.snaktype !== 
undefined ) {
+                       value.snaktype = serialization.snaktype;
+               } else if( !this.options.locked.snaktype ) {
+                       var snakTypeSelector = this._getSnakTypeSelector(),
+                               snakType = snakTypeSelector && 
snakTypeSelector.snakType();
+                       if( snakType ) {
+                               value.snaktype = snakType;
+                       }
+               }
 
-       /**
-        * Updates specifics of the value.
-        * @private
-        *
-        * @param {Object} changes
-        */
-       _updateValue: function( changes ) {
-               this._setValue( $.extend( this._getValue(), changes ) );
+               return this._variation ? $.extend( this._variation.value(), 
value ) : value;
        },
 
        /**
         * If a `wikibase.datamodel.Snak` instance is passed, the `snakview` is 
updated to represent the
-        * `Snak`. If no parameter is supplied, the current `Snak` represented 
by the `snakview` or
-        * `null` if the `snakview is in edit mode is returned.
+        * `Snak`. If no parameter is supplied, the current `Snak` represented 
by the `snakview` is
+        * returned.
         * @since 0.4
         *
         * @param {wikibase.datamodel.Snak|null} [snak]
-        * @return {wikibase.datamodel.Snak|null}
+        * @return {wikibase.datamodel.Snak|null|undefined}
         */
        snak: function( snak ) {
-               if( snak === undefined ) {
-                       // factory method will fail when essential data is not 
yet defined!
-                       // TODO: variations should have a function to ask 
whether fully defined yet
-                       try {
-                               // NOTE: can still be null if user didn't enter 
essential information in variation's UI
-                               var value = this.value();
-                               if( value.datavalue ) {
-                                       value.datavalue = {
-                                               type: value.datavalue.getType(),
-                                               value: value.datavalue.toJSON()
-                                       };
-                               }
-                               return ( new 
wb.serialization.SnakDeserializer() ).deserialize( value );
-                       } catch( e ) {
-                               return null;
-                       }
-                       // TODO: have a cached version of that snak! Not only 
for performance, but also to allow
-                       //  x.snak() === x.snak() which would return false 
because a new instance of wb.datamodel.Snak
-                       //  would be factored on each call. On the other hand, 
wb.datamodel.Snak.equals() should be used.
-                       // NOTE: One possibility would be to use the Flyweight 
pattern in wb.datamodel.Snak factories.
+               if( snak !== undefined ) {
+                       this.value( snak || {} );
+                       return;
                }
-               if( snak !== null && !( snak instanceof wb.datamodel.Snak ) ) {
-                       throw new Error( 'The given value has to be null or an 
instance of wikibase.datamodel.Snak' );
+
+               var value = this.value();
+               if( value.datavalue instanceof dv.DataValue ) {
+                       value.datavalue = {
+                               type: value.datavalue.getType(),
+                               value: value.datavalue.toJSON()
+                       };
                }
-               return this.value( snak );
+
+               var snakDeserializer =  new wb.serialization.SnakDeserializer();
+               try {
+                       return snakDeserializer.deserialize( value );
+               } catch( e ) {
+                       return null;
+               }
        },
 
        /**
-        * Returns the `Property` ID of the `Property` chosen for this `Snak` 
or `null` if none is set.
-        * Equal to `.value().getPropertyId()`, but might be set while 
`.value()` still returns `null`,
-        * e.g. if `Property` has been selected or pre-defined while value or 
`Snak` type are not set
-        * yet.
-        * If a `Property` is passed, the `snakview` `Snak`'s `Property` 
reference will be updated.
+        * Sets/Gets the ID of the `Property` for the `Snak` represented by the 
`snakview`. If no
+        * `Property` is set, `null` is returned.
         * @since 0.3 (setter since 0.4)
         *
+        * @param {string|null} [propertyId]
         * @return {string|null|undefined}
         */
        propertyId: function( propertyId ) {
                if( propertyId === undefined ) {
-                       return this._propertyId;
-               }
-               if( propertyId !== this._propertyId ) {
-                       this._updateValue( {
-                               property: propertyId
-                       } );
+                       return this.value().property || null;
+               } else {
+                       var value = this.value();
+
+                       if( propertyId !== value.property ) {
+                               if( propertyId === null ) {
+                                       delete value.property;
+                               } else {
+                                       value.property = propertyId;
+                               }
+                               this.option( 'value', value );
+                       }
                }
        },
 
        /**
-        * Returns the `Snak` type ID in use for the `Snak` represented by the 
`snakview` or `null` if
-        * not defined. Equal to `.value().getType()`, but might be set while 
`.value()` still returns
-        * `null`, e.g. if `Snak` type has been selected or pre-defined while 
other required information
-        * for constructing the `Snak` object has not been defined yet.
-        * If a `Snak` type is passed, the `snakview` `Snak`'s type will be 
updated.
+        * Sets/Gets the ID of the `Snak` type for the `Snak` represented by 
the `snakview`. If no
+        * `Snak` type is set, `null` is returned.
+        * @see wikibase.datamodel.Snak.TYPE
         * @since 0.4
         *
         * @param {string|null} [snakType]
         * @return {string|null|undefined}
         */
        snakType: function( snakType ) {
+               var value = this.value();
+
                if( snakType === undefined ) {
-                       return this._snakType;
+                       return value.snaktype || null;
+               } else if( snakType === value.snaktype ) {
+                       return;
                }
-               if( snakType !== this._snakType ) {
+
+               if( snakType === null ) {
+                       delete value.snaktype;
+               } else {
                        // TODO: check whether given snak type is actually 
valid!
-                       var changes = {
-                               snaktype: snakType
-                       };
-
-                       if( this._cachedValues[snakType] && 
this._cachedValues[snakType].datavalue ) {
-                               $.extend( changes, {
-                                       datavalue: {
-                                               type: 
this._cachedValues[snakType].datavalue.getType(),
-                                               value: 
this._cachedValues[snakType].datavalue.toJSON()
-                                       }
-                               } );
-                       }
-
-                       this._updateValue( changes );
+                       value.snaktype = snakType;
                }
+
+               this.option( 'value', value );
        },
 
        /**
@@ -699,15 +636,28 @@
         * object for that type if necessary.
         * @private
         * @since 0.4
+        *
+        * @param {Object} value (In)complete `Snak` serialization.
         */
-       _updateVariation: function() {
+       _updateVariation: function( value ) {
                var variationsFactory = $.wikibase.snakview.variations,
-                       snakType = this._snakType,
-                       VariationConstructor = variationsFactory.getVariation( 
snakType );
+                       snakType = value ? value.snaktype : null,
+                       VariationConstructor = snakType ? 
variationsFactory.getVariation( snakType ) : null,
+                       propertyId = value ? value.property : null;
 
                if( this._variation
-                       && ( !this._propertyId || this._variation.constructor 
!== VariationConstructor )
+                       && ( !propertyId || this._variation.constructor !== 
VariationConstructor )
                ) {
+                       var variationValue = this._variation.value();
+
+                       if( variationValue.datavalue ) {
+                               variationValue.datavalue = {
+                                       type: 
variationValue.datavalue.getType(),
+                                       value: variationValue.datavalue.toJSON()
+                               };
+                       }
+
+                       
this._cachedValues[this._variation.variationSnakConstructor.TYPE] = 
variationValue;
 
                        this.$snakValue.empty();
 
@@ -716,7 +666,7 @@
                        this._variation = null;
                }
 
-               if( !this._variation && this._propertyId && 
VariationConstructor ) {
+               if( !this._variation && propertyId && VariationConstructor ) {
                        // Snak type has changed so we need another variation 
Object!
                        this._variation = new VariationConstructor(
                                new $.wikibase.snakview.ViewState( this ),
@@ -725,6 +675,20 @@
                                this.options.valueViewBuilder,
                                this.options.dataTypeStore
                        );
+
+                       if( !value.datavalue
+                               && this._cachedValues[snakType] && 
this._cachedValues[snakType].datavalue
+                       ) {
+                               value.datavalue = $.extend( {}, 
this._cachedValues[snakType].datavalue );
+                       }
+
+                       // Update Variation with fields not directly managed by 
the snakview. If necessary
+                       // within the Variation, those fields should be 
accessed via the Variation's
+                       // ViewState object.
+                       var serializationCopy = $.extend( {}, value );
+                       delete serializationCopy.property;
+                       delete serializationCopy.snaktype;
+                       this._variation.value( serializationCopy );
                }
        },
 
@@ -733,12 +697,14 @@
         * @since 0.4
         */
        draw: function() {
-               var self = this;
+               var self = this,
+                       value = this.value(),
+                       propertyId = value ? value.property : null;
 
                // NOTE: Order of these shouldn't matter; If for any reasons 
draw functions start changing
                //  the outcome of the variation (or Snak type), then something 
must be incredibly wrong!
-               if( this._propertyId ) {
-                       this.options.entityStore.get( this._propertyId ).done( 
function( fetchedProperty ) {
+               if( propertyId ) {
+                       this.options.entityStore.get( propertyId ).done( 
function( fetchedProperty ) {
                                self.drawProperty(
                                        fetchedProperty ? 
fetchedProperty.getContent() : null,
                                        fetchedProperty ? 
fetchedProperty.getTitle() : null
@@ -760,7 +726,9 @@
         * @param {mediawiki.Title|null} title Only supposed to be `null` if 
`property` is `null`.
         */
        drawProperty: function( property, title ) {
-               var $propertyDom, propertyId = this._propertyId;
+               var $propertyDom,
+                       value = this.value(),
+                       propertyId = value ? value.property : null;
 
                if( this.options.locked.property || !this.isInEditMode() ) {
                        // property set and can't be changed afterwards, only 
display label
@@ -823,10 +791,14 @@
                }
 
                // mark current Snak type as chosen one in the menu:
-               selector.snakType( this.snakType() );
+               selector.snakType(
+                       this.options.value instanceof wb.datamodel.Snak
+                               ? this.options.value.getType()
+                               : this.options.value.snaktype
+               );
 
                // only show selector if a property is chosen:
-               this.$snakTypeSelector[ ( this._propertyId ? 'show' : 'hide' ) 
]();
+               this.$snakTypeSelector[ ( this.value().property ? 'show' : 
'hide' ) ]();
 
                // propagate snakview state:
                if ( this.options.disabled ) {
@@ -843,8 +815,9 @@
        drawVariation: function() {
                // property ID will be null if not in edit mode and no Snak set 
or if in edit mode and user
                // didn't choose property yet.
-               var propertyId = this._propertyId,
-                       self = this;
+               var self = this,
+                       value = this.value(),
+                       propertyId = value ? value.property : null;
 
                if( propertyId && this._variation ) {
                        $( this._variation ).one( 'afterdraw', function() {
@@ -890,13 +863,12 @@
 
                // bind user interaction on selector to snakview's state:
                $anchor.on( changeEvent + '.' + this.widgetName, function( 
event ) {
-                       self.snakType( selector.snakType() );
+                       self._updateVariation( self.value() );
+                       self.drawVariation();
                        if( self._variation ) {
                                self._variation.focus();
                        }
-                       if ( self.snak() ) {
-                               self._trigger( 'change' );
-                       }
+                       self._trigger( 'change' );
                } );
 
                return $anchor;
@@ -927,4 +899,4 @@
 
 $.extend( $.wikibase.snakview, existingSnakview );
 
-}( mediaWiki, wikibase, jQuery ) );
+}( mediaWiki, wikibase, jQuery, dataValues ) );
diff --git 
a/lib/resources/jquery.wikibase/snakview/snakview.variations.Value.js 
b/lib/resources/jquery.wikibase/snakview/snakview.variations.Value.js
index e0ae443..426f5af 100644
--- a/lib/resources/jquery.wikibase/snakview/snakview.variations.Value.js
+++ b/lib/resources/jquery.wikibase/snakview/snakview.variations.Value.js
@@ -357,6 +357,24 @@
                        return true;
                },
 
+               /**
+                * @inheritdoc
+                */
+               disable: function() {
+                       if( this._valueView ) {
+                               this._valueView.disable();
+                       }
+               },
+
+               /**
+                * @inheritdoc
+                */
+               enable: function() {
+                       if( this._valueView ) {
+                               this._valueView.enable();
+                       }
+               },
+
                /*
                 * @inheritdoc
                 */
diff --git 
a/lib/resources/jquery.wikibase/snakview/snakview.variations.Variation.js 
b/lib/resources/jquery.wikibase/snakview/snakview.variations.Variation.js
index d2d0569..5cdcae3 100644
--- a/lib/resources/jquery.wikibase/snakview/snakview.variations.Variation.js
+++ b/lib/resources/jquery.wikibase/snakview/snakview.variations.Variation.js
@@ -207,6 +207,16 @@
 
                /**
                 * @since 0.5
+                */
+               disable: function() {},
+
+               /**
+                * @since 0.5
+                */
+               enable: function() {},
+
+               /**
+                * @since 0.5
                 *
                 * @return {boolean}
                 */
diff --git 
a/lib/resources/jquery.wikibase/toolbar/controller/definitions/removetoolbar/referenceview-snakview.js
 
b/lib/resources/jquery.wikibase/toolbar/controller/definitions/removetoolbar/referenceview-snakview.js
index bce99f7..e580958 100644
--- 
a/lib/resources/jquery.wikibase/toolbar/controller/definitions/removetoolbar/referenceview-snakview.js
+++ 
b/lib/resources/jquery.wikibase/toolbar/controller/definitions/removetoolbar/referenceview-snakview.js
@@ -93,12 +93,15 @@
                        }
 
                        // If there is only one snakview widget, disable its 
"remove" link:
-                       if( referenceview._listview.items().length === 0 ) {
+                       var $listview = referenceview.$listview,
+                               listview = $listview.data( 'listview' ),
+                               $snaklistviews = listview.items();
+
+                       if( !$snaklistviews.length ) {
                                return;
                        }
 
-                       var $snaklistviews = referenceview._listview.items(),
-                               $firstSnaklistview = $snaklistviews.first(),
+                       var $firstSnaklistview = $snaklistviews.first(),
                                referenceviewLia = 
referenceview.options.listItemAdapter,
                                firstSnaklistview = 
referenceviewLia.liInstance( $firstSnaklistview ),
                                $firstSnakview = 
firstSnaklistview.$listview.data( 'listview' ).items().first(),
@@ -107,9 +110,9 @@
 
                        for( var i = 0; i < $snaklistviews.length; i++ ) {
                                var snaklistviewWidget = 
referenceviewLia.liInstance( $snaklistviews.eq( i ) ),
-                                       snaklistviewListview = 
snaklistviewWidget._listview,
+                                       snaklistviewListview = 
snaklistviewWidget.$listview.data( 'listview' ),
                                        snaklistviewListviewLia = 
snaklistviewListview.listItemAdapter(),
-                                       $snakviews = 
snaklistviewWidget._listview.items();
+                                       $snakviews = 
snaklistviewListview.items();
 
                                for( var j = 0; j < $snakviews.length; j++ ) {
                                        var snakview = 
snaklistviewListviewLia.liInstance( $snakviews.eq( j ) );
diff --git 
a/lib/tests/qunit/jquery.wikibase/jquery.wikibase.snaklistview.tests.js 
b/lib/tests/qunit/jquery.wikibase/jquery.wikibase.snaklistview.tests.js
index a3abf4e..6cc4db2 100644
--- a/lib/tests/qunit/jquery.wikibase/jquery.wikibase.snaklistview.tests.js
+++ b/lib/tests/qunit/jquery.wikibase/jquery.wikibase.snaklistview.tests.js
@@ -466,7 +466,7 @@
                assert.strictEqual(
                        snaklistview.isInitialValue(),
                        true,
-                       'Snaklistview is still empty.'
+                       'Snaklistview still features initial value.'
                );
 
                // Should not trigger any events since not in edit mode:
diff --git 
a/lib/tests/qunit/jquery.wikibase/jquery.wikibase.statementgrouplistview.tests.js
 
b/lib/tests/qunit/jquery.wikibase/jquery.wikibase.statementgrouplistview.tests.js
index d413eec..3d9af8c 100644
--- 
a/lib/tests/qunit/jquery.wikibase/jquery.wikibase.statementgrouplistview.tests.js
+++ 
b/lib/tests/qunit/jquery.wikibase/jquery.wikibase.statementgrouplistview.tests.js
@@ -174,9 +174,10 @@
                statementgroupview = 
statementgrouplistviewListviewLia.liInstance( $statementgroupview ),
                $statementlistview = statementgroupview.$statementlistview;
 
-       $statementlistview.find( ':wikibase-snakview' ).data( 'snakview' 
).value(
-               new wb.datamodel.PropertyNoValueSnak( 'P1' )
-       );
+       // Simulate having altered snakview's value:
+       $statementlistview.find( ':wikibase-snakview' ).data( 'snakview' ).snak 
= function() {
+               return new wb.datamodel.PropertyNoValueSnak( 'P1' );
+       };
 
        assert.ok(
                $statementgroupview.hasClass( 'wb-new' ),
diff --git a/lib/tests/qunit/jquery.wikibase/snakview/resources.php 
b/lib/tests/qunit/jquery.wikibase/snakview/resources.php
index d3cf6d0..fd7e6f5 100644
--- a/lib/tests/qunit/jquery.wikibase/snakview/resources.php
+++ b/lib/tests/qunit/jquery.wikibase/snakview/resources.php
@@ -32,6 +32,7 @@
                                'wikibase.datamodel.PropertyValueSnak',
                                'wikibase.datamodel.Term',
                                'wikibase.datamodel.TermMap',
+                               'wikibase.serialization.SnakDeserializer',
                                'wikibase.serialization.SnakSerializer',
                                'wikibase.store.FetchedContent',
                        ),
diff --git a/lib/tests/qunit/jquery.wikibase/snakview/snakview.tests.js 
b/lib/tests/qunit/jquery.wikibase/snakview/snakview.tests.js
index b8b83c4..02c9d9d 100644
--- a/lib/tests/qunit/jquery.wikibase/snakview/snakview.tests.js
+++ b/lib/tests/qunit/jquery.wikibase/snakview/snakview.tests.js
@@ -35,7 +35,8 @@
        }
 };
 
-var snakSerializer = new wb.serialization.SnakSerializer();
+var snakSerializer = new wb.serialization.SnakSerializer(),
+       snakDeserializer = new wb.serialization.SnakDeserializer();
 
 /**
  * @param {Object} [options={}]
@@ -44,6 +45,7 @@
  */
 var createSnakview = function( options, $node ) {
        options = $.extend( {
+               autoStartEditing: false,
                entityStore: entityStore,
                valueViewBuilder: 'I am a ValueViewBuilder',
                dataTypeStore: new dt.DataTypeStore()
@@ -114,7 +116,6 @@
        assert.deepEqual(
                snakview.value(),
                {
-                       property: null,
                        snaktype: wb.datamodel.PropertyValueSnak.TYPE
                },
                'Verified default value.'
@@ -133,6 +134,11 @@
                'Set Snak serialization value.'
        );
 
+       assert.ok(
+               snakview.snak().equals( snakDeserializer.deserialize( newValue 
) ),
+               'Verified Snak object returned by snak().'
+       );
+
        newValue = new wb.datamodel.PropertyNoValueSnak( 'P1' );
 
        snakview.value( newValue );
@@ -143,8 +149,12 @@
                'Set wikibase.datamodel.Snak value.'
        );
 
+       assert.ok(
+               snakview.snak().equals( newValue ),
+               'Verified Snak object returned by snak().'
+       );
+
        newValue = {
-               property: 'P1',
                snaktype: wb.datamodel.PropertyValueSnak.TYPE
        };
 
@@ -155,6 +165,206 @@
                newValue,
                'Set incomplete Snak serialization value.'
        );
+
+       assert.strictEqual(
+               snakview.snak(),
+               null,
+               'Verified snak() returning "null".'
+       );
+} );
+
+QUnit.test( 'snak()', function( assert ) {
+       var $snakview = createSnakview(),
+               snakview = $snakview.data( 'snakview' );
+
+       assert.strictEqual(
+               snakview.snak(),
+               null,
+               'Returning "null" since default value is an incomplete 
serialization.'
+       );
+
+       var snak = new wb.datamodel.PropertySomeValueSnak( 'P1' );
+
+       snakview.snak( snak );
+
+       assert.ok(
+               snakview.snak().equals( snak ),
+               'Set Snak value.'
+       );
+
+       assert.deepEqual(
+               snakview.value(),
+               snakSerializer.serialize( snak ),
+               'Verified serialization returned by value().'
+       );
+
+       snakview.snak( null );
+
+       assert.strictEqual(
+               snakview.snak(),
+               null,
+               'Reset value by passing "null" to snak().'
+       );
+
+       assert.deepEqual(
+               snakview.value(),
+               {},
+               'Verified serialization returned by value().'
+       );
+} );
+
+QUnit.test( 'propertyId()', function( assert ) {
+       var $snakview = createSnakview(),
+               snakview = $snakview.data( 'snakview' );
+
+       assert.strictEqual(
+               snakview.propertyId(),
+               null,
+               'By default, the Property ID is "null".'
+       );
+
+       snakview.propertyId( 'P1' );
+
+       assert.equal(
+               snakview.propertyId(),
+               'P1',
+               'Set Property ID.'
+       );
+
+       snakview.propertyId( null );
+
+       assert.strictEqual(
+               snakview.propertyId(),
+               null,
+               'Reset Property ID.'
+       );
+
+       snakview.snak( new wb.datamodel.PropertyNoValueSnak( 'P1' ) );
+
+       assert.equal(
+               snakview.propertyId(),
+               'P1',
+               'Property ID is updated when setting a Snak.'
+       );
+
+       snakview.propertyId( 'P2' );
+
+       assert.ok(
+               snakview.snak().equals( new wb.datamodel.PropertyNoValueSnak( 
'P2' ) ),
+               'Updated Property ID of Snak.'
+       );
+} );
+
+QUnit.test( 'snakType()', function( assert ) {
+       var $snakview = createSnakview(),
+               snakview = $snakview.data( 'snakview' );
+
+       assert.strictEqual(
+               snakview.snakType(),
+               'value',
+               'By default, the Snak type is "value".'
+       );
+
+       snakview.snakType( 'novalue' );
+
+       assert.equal(
+               snakview.snakType(),
+               'novalue',
+               'Set Snak type.'
+       );
+
+       snakview.snakType( null );
+
+       assert.strictEqual(
+               snakview.snakType(),
+               null,
+               'Reset Snak type.'
+       );
+
+       snakview.snak( new wb.datamodel.PropertySomeValueSnak( 'P1' ) );
+
+       assert.equal(
+               snakview.snakType(),
+               'somevalue',
+               'Snak type is updated when setting a Snak.'
+       );
+
+       snakview.snakType( 'novalue' );
+
+       assert.ok(
+               snakview.snak().equals( new wb.datamodel.PropertyNoValueSnak( 
'P1' ) ),
+               'Updated Snak type of Snak.'
+       );
+} );
+
+QUnit.test( 'isInitialValue()', function( assert ) {
+       var $snakview = createSnakview(),
+               snakview = $snakview.data( 'snakview' );
+
+       assert.ok(
+               snakview.isInitialValue(),
+               'Verified returning TRUE after default initialization.'
+       );
+
+       // Simulate change of value by overwriting output of value():
+       snakview.value = function() {
+               return $.extend( this.options.value, {
+                       snaktype: 'novalue'
+               } );
+       };
+
+       assert.ok(
+               snakview.isInitialValue(),
+               'No proper Snak currently and on initialization is regarded 
FALSE.'
+       );
+
+       snakview.value = function() {
+               return snakSerializer.serialize( new 
wb.datamodel.PropertyNoValueSnak( 'P1' ) );
+       };
+
+       assert.ok(
+               !snakview.isInitialValue(),
+               'Returning FALSE after setting a proper Snak.'
+       );
+
+       $snakview = createSnakview( {
+               value: new wb.datamodel.PropertyNoValueSnak( 'P1' )
+       } );
+       snakview = $snakview.data( 'snakview' );
+
+       assert.ok(
+               snakview.isInitialValue(),
+               'Verified returning TRUE after initialization with a proper 
Snak object.'
+       );
+
+       snakview.value = function() {
+               var value = this.options.value;
+               delete value.propertyId;
+               return value;
+       };
+
+       assert.ok(
+               !snakview.isInitialValue(),
+               'Returning FALSE after breaking serialization.'
+       );
+
+       snakview.value = function() {
+               return snakSerializer.serialize( new 
wb.datamodel.PropertySomeValueSnak( 'P1' ) );
+       };
+
+       assert.ok(
+               !snakview.isInitialValue(),
+               'Returning FALSE after setting another Snak.'
+       );
+
+       snakview.value = function() {
+               return snakSerializer.serialize( new 
wb.datamodel.PropertyNoValueSnak( 'P1' ) );
+       };
+
+       assert.ok(
+               snakview.isInitialValue(),
+               'Returning TRUE after resetting to initial Snak.'
+       );
 } );
 
 }( jQuery, QUnit, wikibase, dataTypes, mediaWiki ) );

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

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

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

Reply via email to