Allow node relocation

*.php
* Added links to new file

ve.ce.ImageNode.js
* Added relocatable node mixin
* Added $image reference to the actual img element, so if it's wrapped
  in a sub class the functionality in the parent class  doesn't break.
* Moved drag start event handling to relocatable node
* Removed drag end binding, not needed.

ve.ce.MWImageNode.js
* Moved addClass to initialization section of constructor.
* Copied 'view' data prop from image element to keep stuff working after
  the wrapping.

ve.ce.Node.css
* Switched to default (arrow) cursor for images.

ve.ce.RelocatableNode.js
* New mixing for nodes that should be relocatable
* Added implementation for drag start, which tells the surface to allow
  dragging this node.

ve.ce.Surface.js
* Added relocation support, which is used by relocatable nodes
* Split onDocumentDragDrop into onDocumentDragOver and onDocumentDrop
  which now have implementations that support relocation of nodes

ve.ui.Context.js
* Added relocation tracking to prevent context being shown while
  relocating

Change-Id: I8703adfb707af2c3224431afc3418356ac2c686c
This commit is contained in:
Trevor Parscal 2013-04-15 10:54:49 -07:00
parent 832137de50
commit 1878c7c5a8
10 changed files with 219 additions and 53 deletions

View file

@ -303,6 +303,7 @@ $wgResourceModules += array(
've/ce/ve.ce.ContentBranchNode.js',
've/ce/ve.ce.LeafNode.js',
've/ce/ve.ce.FocusableNode.js',
've/ce/ve.ce.RelocatableNode.js',
've/ce/ve.ce.Surface.js',
've/ce/ve.ce.SurfaceObserver.js',

View file

@ -182,6 +182,7 @@ $html = file_get_contents( $page );
<script src="../../modules/ve/ce/ve.ce.ContentBranchNode.js"></script>
<script src="../../modules/ve/ce/ve.ce.LeafNode.js"></script>
<script src="../../modules/ve/ce/ve.ce.FocusableNode.js"></script>
<script src="../../modules/ve/ce/ve.ce.RelocatableNode.js"></script>
<script src="../../modules/ve/ce/ve.ce.Surface.js"></script>
<script src="../../modules/ve/ce/ve.ce.SurfaceObserver.js"></script>
<script src="../../modules/ve/ce/nodes/ve.ce.GeneratedContentNode.js"></script>

View file

@ -11,6 +11,7 @@
* @class
* @extends ve.ce.LeafNode
* @mixins ve.ce.FocusableNode
* @mixins ve.ce.RelocatableNode
*
* @constructor
* @param {ve.dm.ImageNode} model Model to observe
@ -21,18 +22,18 @@ ve.ce.ImageNode = function VeCeImageNode( model ) {
// Mixin constructors
ve.ce.FocusableNode.call( this );
ve.ce.RelocatableNode.call( this );
// Properties
this.$image = this.$;
// Events
this.model.addListenerMethod( this, 'update', 'onUpdate' );
this.$.on( {
'click': ve.bind( this.onClick, this ),
'dragstart': ve.bind( this.onDragStart, this ),
'dragend': ve.bind( this.onDragEnd, this )
} );
this.$image.on( 'click', ve.bind( this.onClick, this ) );
// Initialization
ve.setDomAttributes( this.$[0], this.model.getAttributes(), ['src', 'width', 'height'] );
this.$.addClass( 've-ce-imageNode' );
ve.setDomAttributes( this.$image[0], this.model.getAttributes(), ['src', 'width', 'height'] );
this.$image.addClass( 've-ce-imageNode' );
};
/* Inheritance */
@ -40,6 +41,7 @@ ve.ce.ImageNode = function VeCeImageNode( model ) {
ve.inheritClass( ve.ce.ImageNode, ve.ce.LeafNode );
ve.mixinClass( ve.ce.ImageNode, ve.ce.FocusableNode );
ve.mixinClass( ve.ce.ImageNode, ve.ce.RelocatableNode );
/* Static Properties */
@ -54,40 +56,17 @@ ve.ce.ImageNode.static.name = 'image';
* @param {jQuery.Event} e Click event
*/
ve.ce.ImageNode.prototype.onClick = function ( e ) {
var range,
surfaceModel = this.getRoot().getSurface().getModel(),
selection = surfaceModel.getSelection();
var surfaceModel = this.getRoot().getSurface().getModel(),
selectionRange = surfaceModel.getSelection(),
nodeRange = this.model.getOuterRange();
range = new ve.Range(
this.model.getOffset(),
this.model.getOffset() + this.model.getOuterLength()
);
if ( e.shiftKey ) {
range = ve.Range.newCoveringRange( [ selection, range ], selection.from > range.from );
}
this.getRoot().getSurface().getModel().change( null, range );
};
/**
* Handle the dragstart.
*
* @method
* @param {jQuery.Event} e Dragstart event
*/
ve.ce.ImageNode.prototype.onDragStart = function () {
return false;
};
/**
* Handle the dragend.
*
* @method
* @param {jQuery.Event} e Dragstart event
*/
ve.ce.ImageNode.prototype.onDragEnd = function () {
return false;
surfaceModel.getFragment(
e.shiftKey ?
ve.Range.newCoveringRange(
[ selectionRange, nodeRange ], selectionRange.from > nodeRange.from
) :
nodeRange
).select();
};
/* Registration */

View file

@ -17,13 +17,16 @@
// Parent constructor
ve.ce.ImageNode.call( this, model );
// Initialization
this.$.addClass( 've-ce-MWImageNode' );
// Properties
this.$image = this.$;
this.$ = $( '<' + ( model.getAttribute( 'isLinked' ) ? 'a' : 'span' ) + '>' );
// Initialization
this.$.attr( 'contenteditable', false ).append( this.$image );
this.$
.attr( 'contenteditable', false )
.addClass( 've-ce-mwImageNode' )
.append( this.$image )
.data( 'view', this.$image.data( 'view' ) );
this.onUpdate();
};

View file

@ -16,6 +16,10 @@
right: 0 !important;
}
.ve-ce-imageNode {
cursor: default;
}
.ve-ce-alienNode {
z-index: 0;
}

View file

@ -0,0 +1,58 @@
/*!
* VisualEditor ContentEditable RelocatableNode class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* ContentEditable relocatable node.
*
* @class
* @abstract
*
* @constructor
* @param {jQuery} [$draggable=this.$] Draggable DOM element
*/
ve.ce.RelocatableNode = function VeCeRelocatableNode( $draggable ) {
// Properties
this.$draggable = $draggable || this.$;
this.surface = null;
// Events
this.$draggable.on( {
'dragstart': ve.bind( this.onRelocatableDragStart, this ),
'dragend': ve.bind( this.onRelocatableDragEnd, this )
} );
};
/* Methods */
/**
* Handle element drag start.
*
* @method
* @param {jQuery.Event} e Drag start event
*/
ve.ce.RelocatableNode.prototype.onRelocatableDragStart = function () {
// Store a copy of the surface, when dragend occurs the node will be detached
this.surface = this.getRoot().getSurface();
if ( this.surface ) {
// Allow dragging this node in the surface
this.surface.startRelocation( this );
}
};
/**
* Handle element drag end.
*
* @method
* @param {jQuery.Event} e Drag end event
*/
ve.ce.RelocatableNode.prototype.onRelocatableDragEnd = function () {
if ( this.surface ) {
this.surface.endRelocation();
this.surface = null;
}
};

View file

@ -32,6 +32,7 @@ ve.ce.Surface = function VeCeSurface( $container, model, surface ) {
this.clipboard = {};
this.renderingEnabled = true;
this.dragging = false;
this.relocating = false;
this.selecting = false;
this.$phantoms = $( '<div>' );
this.$pasteTarget = $( '<div>' );
@ -54,7 +55,8 @@ ve.ce.Surface = function VeCeSurface( $container, model, surface ) {
'cut': ve.bind( this.onCut, this ),
'copy': ve.bind( this.onCopy, this ),
'paste': ve.bind( this.onPaste, this ),
'dragover drop': ve.bind( this.onDocumentDragoverDrop, this )
'dragover': ve.bind( this.onDocumentDragOver, this ),
'drop': ve.bind( this.onDocumentDrop, this )
} );
if ( $.browser.msie ) {
this.$.on( 'beforepaste', ve.bind( this.onPaste, this ) );
@ -83,6 +85,14 @@ ve.inheritClass( ve.ce.Surface, ve.EventEmitter );
* @event selectionEnd
*/
/**
* @event relocationStart
*/
/**
* @event relocationEnd
*/
/* Static Properties */
/**
@ -259,14 +269,70 @@ ve.ce.Surface.prototype.onDocumentMouseMove = function () {
};
/**
* Handle document dragover and drop events.
* Handle document dragover events.
*
* Prevents native dragging and dropping of content.
* Limits native drag and drop behavior.
*
* @method
* @param {jQuery.Event} e Drag over/drop event
* @param {jQuery.Event} e Drag over event
*/
ve.ce.Surface.prototype.onDocumentDragoverDrop = function () {
ve.ce.Surface.prototype.onDocumentDragOver = function () {
if ( !this.relocating ) {
return false;
} else if ( this.selecting ) {
this.emit( 'selectionEnd' );
this.selecting = false;
this.dragging = false;
}
};
/**
* Handle document drop events.
*
* Limits native drag and drop behavior.
*
* TODO: Look into using drag and drop data transfer to embed the dragged element's original range
* (for dragging within one document) and serialized linear model data (for dragging between
* multiple documents) and use a special mimetype, like application-x/VisualEditor, to allow
* dragover and drop events on the surface, removing the need to give the surface explicit
* instructions to allow and prevent dragging and dropping a certain node.
*
* @method
* @param {jQuery.Event} e Drag drop event
*/
ve.ce.Surface.prototype.onDocumentDrop = function ( e ) {
var node = this.relocating;
if ( node ) {
// Process drop operation after native drop has been prevented below
setTimeout( ve.bind( function () {
var dropPoint, nodeData, originFragment, targetFragment,
nodeRange = node.getModel().getOuterRange();
// Get a fragment from the drop point
dropPoint = rangy.positionFromPoint( e.originalEvent.pageX, e.originalEvent.pageY );
if ( !dropPoint ) {
// Getting position from point supported
return false;
}
targetFragment = this.model.getFragment(
new ve.Range( ve.ce.getOffset( dropPoint.node, dropPoint.offset ) ), false
);
// Get a fragment and data of the node being dragged
originFragment = this.model.getFragment( nodeRange, false );
nodeData = originFragment.getData();
// Remove node from old location (auto-updates targetFragment's range)
originFragment.removeContent().destroy();
// Re-insert node at new location and re-select it
targetFragment.insertContent( nodeData );
targetFragment.adjustRange( -nodeData.length, 0 ).select().destroy();
targetFragment.destroy();
}, this ) );
}
return false;
};
@ -735,6 +801,34 @@ ve.ce.Surface.prototype.onUnlock = function () {
this.surfaceObserver.start();
};
/*! Relocation */
/**
* Start a relocation action.
*
* @see ve.ce.RelocatableNode
*
* @method
* @param {ve.ce.Node} node Node being relocated
*/
ve.ce.Surface.prototype.startRelocation = function ( node ) {
this.relocating = node;
this.emit( 'relocationStart', node );
};
/**
* Complete a relocation action.
*
* @see ve.ce.RelocatableNode
*
* @method
* @param {ve.ce.Node} node Node being relocated
*/
ve.ce.Surface.prototype.endRelocation = function () {
this.emit( 'relocationEnd', this.relocating );
this.relocating = null;
};
/*! Utilities */
/**

View file

@ -217,7 +217,7 @@ ve.ce.getOffsetFromElementNode = function ( domNode, domOffset, addOuterLength )
if ( domOffset === 0 ) {
node = $domNode.data( 'view' );
if ( node ) {
if ( node && node instanceof ve.ce.Node ) {
nodeModel = $domNode.data( 'view' ).getModel();
if ( addOuterLength === true ) {
return nodeModel.getOffset() + nodeModel.getOuterLength();

View file

@ -125,6 +125,7 @@
<script src="../../ve/ce/ve.ce.ContentBranchNode.js"></script>
<script src="../../ve/ce/ve.ce.LeafNode.js"></script>
<script src="../../ve/ce/ve.ce.FocusableNode.js"></script>
<script src="../../ve/ce/ve.ce.RelocatableNode.js"></script>
<script src="../../ve/ce/ve.ce.Surface.js"></script>
<script src="../../ve/ce/ve.ce.SurfaceObserver.js"></script>
<script src="../../ve/ce/nodes/ve.ce.GeneratedContentNode.js"></script>

View file

@ -20,6 +20,7 @@ ve.ui.Context = function VeUiContext( surface ) {
this.visible = false;
this.showing = false;
this.selecting = false;
this.relocating = false;
this.selection = null;
this.toolbar = null;
this.$ = $( '<div>' );
@ -41,7 +42,9 @@ ve.ui.Context = function VeUiContext( surface ) {
} );
this.surface.getView().addListenerMethods( this, {
'selectionStart': 'onSelectionStart',
'selectionEnd': 'onSelectionEnd'
'selectionEnd': 'onSelectionEnd',
'relocationStart': 'onRelocationStart',
'relocationEnd': 'onRelocationEnd'
} );
this.inspectors.addListenerMethods( this, {
'setup': 'onInspectorSetup',
@ -62,14 +65,14 @@ ve.ui.Context = function VeUiContext( surface ) {
* Changes are ignored while the user is selecting text.
*
* @method
* @param {ve.dm.Transaction} tx Change transaction
* @param {ve.dm.Transaction[]} transactions Change transactions
* @param {ve.Range} selection Change selection
*/
ve.ui.Context.prototype.onChange = function ( tx, selection ) {
ve.ui.Context.prototype.onChange = function ( transactions, selection ) {
if ( selection && selection.start === 0 ) {
return;
}
if ( selection && !this.selecting ) {
if ( selection && !this.selecting && !this.draggingAndDropping ) {
this.update();
}
};
@ -91,6 +94,28 @@ ve.ui.Context.prototype.onSelectionStart = function () {
*/
ve.ui.Context.prototype.onSelectionEnd = function () {
this.selecting = false;
if ( !this.relocating ) {
this.update();
}
};
/**
* Handle selection start events on the view.
*
* @method
*/
ve.ui.Context.prototype.onRelocationStart = function () {
this.relocating = true;
this.hide();
};
/**
* Handle selection end events on the view.
*
* @method
*/
ve.ui.Context.prototype.onRelocationEnd = function () {
this.relocating = false;
this.update();
};