From 1878c7c5a89e690ccfe75875949432a669c6b907 Mon Sep 17 00:00:00 2001 From: Trevor Parscal Date: Mon, 15 Apr 2013 10:54:49 -0700 Subject: [PATCH] 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 --- VisualEditor.php | 1 + demos/ve/index.php | 1 + modules/ve/ce/nodes/ve.ce.ImageNode.js | 59 +++++-------- modules/ve/ce/nodes/ve.ce.MWImageNode.js | 9 +- modules/ve/ce/styles/ve.ce.Node.css | 4 + modules/ve/ce/ve.ce.RelocatableNode.js | 58 +++++++++++++ modules/ve/ce/ve.ce.Surface.js | 104 +++++++++++++++++++++-- modules/ve/ce/ve.ce.js | 2 +- modules/ve/test/index.php | 1 + modules/ve/ui/ve.ui.Context.js | 33 ++++++- 10 files changed, 219 insertions(+), 53 deletions(-) create mode 100644 modules/ve/ce/ve.ce.RelocatableNode.js diff --git a/VisualEditor.php b/VisualEditor.php index a3f5a963f5..0f439096eb 100644 --- a/VisualEditor.php +++ b/VisualEditor.php @@ -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', diff --git a/demos/ve/index.php b/demos/ve/index.php index fecb8fe522..ee66e057ee 100644 --- a/demos/ve/index.php +++ b/demos/ve/index.php @@ -182,6 +182,7 @@ $html = file_get_contents( $page ); + diff --git a/modules/ve/ce/nodes/ve.ce.ImageNode.js b/modules/ve/ce/nodes/ve.ce.ImageNode.js index 503eaec8e7..6c32e9cc28 100644 --- a/modules/ve/ce/nodes/ve.ce.ImageNode.js +++ b/modules/ve/ce/nodes/ve.ce.ImageNode.js @@ -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 */ diff --git a/modules/ve/ce/nodes/ve.ce.MWImageNode.js b/modules/ve/ce/nodes/ve.ce.MWImageNode.js index ede3591add..0fea71654a 100644 --- a/modules/ve/ce/nodes/ve.ce.MWImageNode.js +++ b/modules/ve/ce/nodes/ve.ce.MWImageNode.js @@ -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(); }; diff --git a/modules/ve/ce/styles/ve.ce.Node.css b/modules/ve/ce/styles/ve.ce.Node.css index 47d5686528..37f301052b 100644 --- a/modules/ve/ce/styles/ve.ce.Node.css +++ b/modules/ve/ce/styles/ve.ce.Node.css @@ -16,6 +16,10 @@ right: 0 !important; } +.ve-ce-imageNode { + cursor: default; +} + .ve-ce-alienNode { z-index: 0; } diff --git a/modules/ve/ce/ve.ce.RelocatableNode.js b/modules/ve/ce/ve.ce.RelocatableNode.js new file mode 100644 index 0000000000..e51abba44b --- /dev/null +++ b/modules/ve/ce/ve.ce.RelocatableNode.js @@ -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; + } +}; diff --git a/modules/ve/ce/ve.ce.Surface.js b/modules/ve/ce/ve.ce.Surface.js index 68b899b520..fbf1f0c9e2 100644 --- a/modules/ve/ce/ve.ce.Surface.js +++ b/modules/ve/ce/ve.ce.Surface.js @@ -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 = $( '
' ); this.$pasteTarget = $( '
' ); @@ -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 */ /** diff --git a/modules/ve/ce/ve.ce.js b/modules/ve/ce/ve.ce.js index 1c7511f4f5..1192f35d6c 100644 --- a/modules/ve/ce/ve.ce.js +++ b/modules/ve/ce/ve.ce.js @@ -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(); diff --git a/modules/ve/test/index.php b/modules/ve/test/index.php index 03740e4345..5eb2d91d67 100644 --- a/modules/ve/test/index.php +++ b/modules/ve/test/index.php @@ -125,6 +125,7 @@ + diff --git a/modules/ve/ui/ve.ui.Context.js b/modules/ve/ui/ve.ui.Context.js index af12b1981e..4e3d34e525 100644 --- a/modules/ve/ui/ve.ui.Context.js +++ b/modules/ve/ui/ve.ui.Context.js @@ -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.$ = $( '
' ); @@ -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(); };