From 55b5f30edb0874387c975e4ad9f5e70c30bbbf40 Mon Sep 17 00:00:00 2001 From: Trevor Parscal Date: Wed, 15 May 2013 16:31:02 -0700 Subject: [PATCH] ve.ui.Context: Add embedding feature Objectives: * Make the context menu display in the top right corner of the currently focused inspectable node (if there is one) * Prevent clicking on anything to do with the toolbar or popup from doing anything at all, ever Bonus: * While we are using the clever feature in jQuery's on method which allows passing boolean false to cancel the event - may as well do that in ve.ui.Dialog as well Changes: ve.ui.FocusableNode * Add ability to specify the focusable element so that dimensions can be derived from it ve.ce.Surface * Add quotes to object keys ve.ui.MediaDialog * Change association from being MW specific to handling images in general ve.ui.Context * Add embedded styles for context * Add embedded mode, which is triggered when the context is a single focusable node, and the node is large enough to fit the context reasonably ve.ui.Dialog * Inline mousedown handler ve.ui.Toolbar, ve.ui.PopupWidget * Cancel stray mousedown events Change-Id: I4b25d33f64b4bcb8a3ecfd7e9728f54a2d4886f3 --- modules/ve/ce/ve.ce.FocusableNode.js | 11 ++++++- modules/ve/ce/ve.ce.Surface.js | 4 +-- modules/ve/ui/dialogs/ve.ui.MediaDialog.js | 4 ++- modules/ve/ui/styles/ve.ui.Context.css | 13 ++++++++ modules/ve/ui/ve.ui.Context.js | 35 ++++++++++++++++++---- modules/ve/ui/ve.ui.Dialog.js | 12 +------- modules/ve/ui/ve.ui.Toolbar.js | 8 ++++- modules/ve/ui/widgets/ve.ui.PopupWidget.js | 7 +++-- 8 files changed, 71 insertions(+), 23 deletions(-) diff --git a/modules/ve/ce/ve.ce.FocusableNode.js b/modules/ve/ce/ve.ce.FocusableNode.js index d3ebe49945..8175be97a5 100644 --- a/modules/ve/ce/ve.ce.FocusableNode.js +++ b/modules/ve/ce/ve.ce.FocusableNode.js @@ -8,14 +8,23 @@ /** * ContentEditable resizable node. * + * Focusable elements have a special treatment by ve.ce.Surface. When the user selects only a single + * node, if it is focusable, the surface will set the focusable node's focused state. Other systems, + * such as the context, may also use a focusable node's $focusable property as a hint of where the + * primary element in the node is. Typically, and by default, the primary element is the root + * element, but in some cases it may need to be configured to be a specific child element within the + * node's DOM rendering. + * * @class * @abstract * * @constructor + * @param {jQuery} [$focusable] Primary element user is focusing on */ -ve.ce.FocusableNode = function VeCeFocusableNode() { +ve.ce.FocusableNode = function VeCeFocusableNode( $focusable ) { // Properties this.focused = false; + this.$focusable = $focusable || this.$; }; /* Events */ diff --git a/modules/ve/ce/ve.ce.Surface.js b/modules/ve/ce/ve.ce.Surface.js index 0f2f418dd9..87b070fa1e 100644 --- a/modules/ve/ce/ve.ce.Surface.js +++ b/modules/ve/ce/ve.ce.Surface.js @@ -177,8 +177,8 @@ ve.ce.Surface.getSelectionRect = function () { }; } else { return { - start: sel.getStartDocumentPos(), - end: sel.getEndDocumentPos() + 'start': sel.getStartDocumentPos(), + 'end': sel.getEndDocumentPos() }; } }; diff --git a/modules/ve/ui/dialogs/ve.ui.MediaDialog.js b/modules/ve/ui/dialogs/ve.ui.MediaDialog.js index 506a813335..ed3983bca7 100644 --- a/modules/ve/ui/dialogs/ve.ui.MediaDialog.js +++ b/modules/ve/ui/dialogs/ve.ui.MediaDialog.js @@ -30,7 +30,9 @@ ve.ui.MediaDialog.static.titleMessage = 'visualeditor-dialog-media-title'; ve.ui.MediaDialog.static.icon = 'picture'; -ve.ui.MediaDialog.static.modelClasses = [ ve.dm.MWInlineImageNode ]; +ve.ui.MediaDialog.static.modelClasses = [ ve.dm.ImageNode ]; + +/* Methods */ /* Registration */ diff --git a/modules/ve/ui/styles/ve.ui.Context.css b/modules/ve/ui/styles/ve.ui.Context.css index 7f2e0c117f..096a8441c0 100644 --- a/modules/ve/ui/styles/ve.ui.Context.css +++ b/modules/ve/ui/styles/ve.ui.Context.css @@ -35,3 +35,16 @@ .ve-ui-context-menu .ve-ui-buttonTool-active { background-image: none; } + +.ve-ui-context-embed .ve-ui-popupWidget-callout { + display: none; +} + +.ve-ui-context-embed .ve-ui-popupWidget-body { + margin-top: 0.25em; + margin-left: -1.3em; +} + +.ve-ui-context-embed .ve-ui-context-menu { + right: 0; +} diff --git a/modules/ve/ui/ve.ui.Context.js b/modules/ve/ui/ve.ui.Context.js index 51f1940039..68fad32cd5 100644 --- a/modules/ve/ui/ve.ui.Context.js +++ b/modules/ve/ui/ve.ui.Context.js @@ -21,6 +21,7 @@ ve.ui.Context = function VeUiContext( surface ) { this.showing = false; this.selecting = false; this.relocating = false; + this.embedded = false; this.selection = null; this.toolbar = null; this.$ = $( '
' ); @@ -53,6 +54,8 @@ ve.ui.Context = function VeUiContext( surface ) { 'resize': ve.bind( this.update, this ), 'focus': ve.bind( this.onWindowFocus, this ) } ); + this.$.add( this.$menu ) + .on( 'mousedown', false ); }; /* Methods */ @@ -246,14 +249,25 @@ ve.ui.Context.prototype.update = function () { * @chainable */ ve.ui.Context.prototype.updateDimensions = function ( transition ) { - var position, $container, - inspector = this.inspectors.getCurrent(); + var position, $container, focusableOffset, focusableWidth, + inspector = this.inspectors.getCurrent(), + focusedNode = this.surface.getView().getFocusedNode(); // Get cursor position position = ve.ce.Surface.getSelectionRect(); - position = position && position.end; + if ( position ) { - $container = inspector ? this.inspectors.$ : this.$menu; + if ( this.embedded ) { + focusableOffset = focusedNode.$focusable.offset(); + focusableWidth = focusedNode.$focusable.outerWidth(); + $container = this.$menu; + position = { 'x': focusableOffset.left + focusableWidth, 'y': focusableOffset.top }; + this.popup.align = 'right'; + } else { + position = position && position.end; + $container = inspector ? this.inspectors.$ : this.$menu; + this.popup.align = 'center'; + } this.$.css( { 'left': position.x, 'top': position.y } ); this.popup.display( position.x, @@ -274,7 +288,8 @@ ve.ui.Context.prototype.updateDimensions = function ( transition ) { * @chainable */ ve.ui.Context.prototype.show = function ( transition ) { - var inspector = this.inspectors.getCurrent(); + var inspector = this.inspectors.getCurrent(), + focusedNode = this.surface.getView().getFocusedNode(); if ( !this.showing ) { this.showing = true; @@ -294,6 +309,16 @@ ve.ui.Context.prototype.show = function ( transition ) { }, this ), 200 ); } else { this.inspectors.$.hide(); + if ( + focusedNode && + focusedNode.$focusable.outerHeight() > this.$menu.outerHeight() * 2 + ) { + this.$.addClass( 've-ui-context-embed' ); + this.embedded = true; + } else { + this.$.removeClass( 've-ui-context-embed' ); + this.embedded = false; + } this.$menu.show(); } diff --git a/modules/ve/ui/ve.ui.Dialog.js b/modules/ve/ui/ve.ui.Dialog.js index d29fbe62ba..85d8feb448 100644 --- a/modules/ve/ui/ve.ui.Dialog.js +++ b/modules/ve/ui/ve.ui.Dialog.js @@ -24,7 +24,7 @@ ve.ui.Dialog = function VeUiDialog( surface ) { // Initialization this.$.addClass( 've-ui-dialog' ); - this.$.on( 'mousedown', ve.bind( this.onMouseDown, this ) ); + this.$.on( 'mousedown', false ); }; /* Inheritance */ @@ -33,16 +33,6 @@ ve.inheritClass( ve.ui.Dialog, ve.ui.Window ); /* Methods */ -/** - * Handle mouse down events. - * - * @method - * @param {jQuery.Event} e Mouse down event - */ -ve.ui.Dialog.prototype.onMouseDown = function () { - return false; -}; - /** * Handle close button click events. * diff --git a/modules/ve/ui/ve.ui.Toolbar.js b/modules/ve/ui/ve.ui.Toolbar.js index ac6f15fce9..f4b802e086 100644 --- a/modules/ve/ui/ve.ui.Toolbar.js +++ b/modules/ve/ui/ve.ui.Toolbar.js @@ -32,6 +32,7 @@ ve.ui.Toolbar = function VeUiToolbar( surface, options ) { this.surface = surface; this.$bar = this.$$( '
' ); this.$tools = this.$$( '
' ); + this.$actions = this.$$( '
' ); this.floating = false; this.$window = null; this.windowEvents = { @@ -39,11 +40,15 @@ ve.ui.Toolbar = function VeUiToolbar( surface, options ) { 'scroll': ve.bind( this.onWindowScroll, this ) }; + // Events + this.$ + .add( this.$bar ).add( this.$tools ).add( this.$actions ) + .on( 'mousedown', false ); + // Initialization this.$tools.addClass( 've-ui-toolbar-tools' ); this.$bar.addClass( 've-ui-toolbar-bar' ).append( this.$tools ); if ( options.actions ) { - this.$actions = this.$$( '
' ); this.$actions.addClass( 've-ui-toolbar-actions' ); this.$bar.append( this.$actions ); } @@ -160,6 +165,7 @@ ve.ui.Toolbar.prototype.addTools = function ( tools ) { group = tools[i]; // Create group $group = this.$$( '
' ) + .on( 'mousedown', false ) .addClass( 've-ui-toolbar-group-' + group.name ); if ( group.label ) { $group.append( diff --git a/modules/ve/ui/widgets/ve.ui.PopupWidget.js b/modules/ve/ui/widgets/ve.ui.PopupWidget.js index 2db41a3919..74f5d4515b 100644 --- a/modules/ve/ui/widgets/ve.ui.PopupWidget.js +++ b/modules/ve/ui/widgets/ve.ui.PopupWidget.js @@ -29,6 +29,11 @@ ve.ui.PopupWidget = function VeUiPopupWidget( config ) { this.transitionTimeout = null; this.align = config.align || 'center'; + // Events + this.$body.on( 'blur', ve.bind( this.onPopupBlur, this ) ); + this.$.add( this.$body ).add( this.$callout ) + .on( 'mousedown', false ); + // Initialization this.$ .addClass( 've-ui-popupWidget' ) @@ -37,8 +42,6 @@ ve.ui.PopupWidget = function VeUiPopupWidget( config ) { this.$body.addClass( 've-ui-popupWidget-body' ) ); - // Auto hide popup - this.$body.on( 'blur', ve.bind( this.onPopupBlur, this ) ); }; /* Inheritance */