jenkins-bot has submitted this change and it was merged.
Change subject: Split ve.ui.Context into DesktopContext and MobileContext
......................................................................
Split ve.ui.Context into DesktopContext and MobileContext
DesktopContext handles more advanced positioning of the context menu and
inspectors on desktop. MobileContext is more basic and can be used on
platforms where context menus are not present (mobile/tablets).
Change-Id: Id3f5f3f39978836f084f7c7e15505e4b6aeb9b44
---
M .docs/categories.json
M .docs/eg-iframe.html
M build/modules.json
M demos/ve/index.html
M modules/ve/test/index.html
D modules/ve/ui/styles/ve.ui.Context.css
A modules/ve/ui/styles/ve.ui.DesktopContext.css
M modules/ve/ui/ve.ui.Context.js
A modules/ve/ui/ve.ui.DesktopContext.js
M modules/ve/ui/ve.ui.DesktopSurface.js
A modules/ve/ui/ve.ui.MobileContext.js
M modules/ve/ui/ve.ui.MobileSurface.js
M modules/ve/ui/ve.ui.Surface.js
13 files changed, 571 insertions(+), 479 deletions(-)
Approvals:
Trevor Parscal: Looks good to me, approved
jenkins-bot: Verified
diff --git a/.docs/categories.json b/.docs/categories.json
index f2e5457..abd0366 100644
--- a/.docs/categories.json
+++ b/.docs/categories.json
@@ -110,6 +110,8 @@
"ve.ui.DesktopSurface",
"ve.ui.MobileSurface",
"ve.ui.Context",
+ "ve.ui.DesktopContext",
+ "ve.ui.MobileContext",
"ve.ui.Toolbar",
"ve.ui.TargetToolbar",
"ve.ui.WindowSet",
diff --git a/.docs/eg-iframe.html b/.docs/eg-iframe.html
index 8a96c1f..809c332 100644
--- a/.docs/eg-iframe.html
+++ b/.docs/eg-iframe.html
@@ -14,7 +14,7 @@
<!-- visualEditor.core -->
<link rel=stylesheet
href="../modules/ve/ce/styles/ve.ce.Node.css">
<link rel=stylesheet
href="../modules/ve/ce/styles/ve.ce.Surface.css">
- <link rel=stylesheet
href="../modules/ve/ui/styles/ve.ui.Context.css">
+ <link rel=stylesheet
href="../modules/ve/ui/styles/ve.ui.DesktopContext.css">
<link rel=stylesheet
href="../modules/ve/ui/styles/ve.ui.Dialog.css">
<link rel=stylesheet
href="../modules/ve/ui/styles/ve.ui.Inspector.css">
<link rel=stylesheet
href="../modules/ve/ui/styles/ve.ui.Surface.css">
@@ -200,6 +200,7 @@
<script src="../modules/ve/ui/ve.ui.Surface.js"></script>
<script src="../modules/ve/ui/ve.ui.DesktopSurface.js"></script>
<script src="../modules/ve/ui/ve.ui.Context.js"></script>
+ <script src="../modules/ve/ui/ve.ui.DesktopContext.js"></script>
<script src="../modules/ve/ui/ve.ui.Dialog.js"></script>
<script src="../modules/ve/ui/ve.ui.Inspector.js"></script>
<script src="../modules/ve/ui/ve.ui.WindowSet.js"></script>
@@ -240,6 +241,7 @@
<!-- visualEditor.mobile -->
<script src="../modules/ve/ui/ve.ui.MobileSurface.js"></script>
+ <script src="../modules/ve/ui/ve.ui.MobileContext.js"></script>
<script>ve.init.platform.setModulesUrl( '../modules' );</script>
diff --git a/build/modules.json b/build/modules.json
index 3a90931..54beba7 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -189,6 +189,7 @@
"modules/ve/ui/ve.ui.Surface.js",
"modules/ve/ui/ve.ui.DesktopSurface.js",
"modules/ve/ui/ve.ui.Context.js",
+ "modules/ve/ui/ve.ui.DesktopContext.js",
"modules/ve/ui/ve.ui.Dialog.js",
"modules/ve/ui/ve.ui.Inspector.js",
"modules/ve/ui/ve.ui.WindowSet.js",
@@ -230,7 +231,7 @@
"styles": [
"modules/ve/ce/styles/ve.ce.Node.css",
"modules/ve/ce/styles/ve.ce.Surface.css",
- "modules/ve/ui/styles/ve.ui.Context.css",
+ "modules/ve/ui/styles/ve.ui.DesktopContext.css",
"modules/ve/ui/styles/ve.ui.Dialog.css",
"modules/ve/ui/styles/ve.ui.Inspector.css",
"modules/ve/ui/styles/ve.ui.Surface.css",
@@ -242,7 +243,8 @@
},
"visualEditor.mobile": {
"scripts": [
- "modules/ve/ui/ve.ui.MobileSurface.js"
+ "modules/ve/ui/ve.ui.MobileSurface.js",
+ "modules/ve/ui/ve.ui.MobileContext.js"
]
}
}
diff --git a/demos/ve/index.html b/demos/ve/index.html
index dfb5b58..61d058b 100644
--- a/demos/ve/index.html
+++ b/demos/ve/index.html
@@ -22,7 +22,7 @@
<!-- visualEditor.core -->
<link rel=stylesheet
href="../../modules/ve/ce/styles/ve.ce.Node.css">
<link rel=stylesheet
href="../../modules/ve/ce/styles/ve.ce.Surface.css">
- <link rel=stylesheet
href="../../modules/ve/ui/styles/ve.ui.Context.css">
+ <link rel=stylesheet
href="../../modules/ve/ui/styles/ve.ui.DesktopContext.css">
<link rel=stylesheet
href="../../modules/ve/ui/styles/ve.ui.Dialog.css">
<link rel=stylesheet
href="../../modules/ve/ui/styles/ve.ui.Inspector.css">
<link rel=stylesheet
href="../../modules/ve/ui/styles/ve.ui.Surface.css">
@@ -232,6 +232,7 @@
<script src="../../modules/ve/ui/ve.ui.Surface.js"></script>
<script
src="../../modules/ve/ui/ve.ui.DesktopSurface.js"></script>
<script src="../../modules/ve/ui/ve.ui.Context.js"></script>
+ <script
src="../../modules/ve/ui/ve.ui.DesktopContext.js"></script>
<script src="../../modules/ve/ui/ve.ui.Dialog.js"></script>
<script src="../../modules/ve/ui/ve.ui.Inspector.js"></script>
<script src="../../modules/ve/ui/ve.ui.WindowSet.js"></script>
@@ -272,6 +273,7 @@
<!-- visualEditor.mobile -->
<script
src="../../modules/ve/ui/ve.ui.MobileSurface.js"></script>
+ <script
src="../../modules/ve/ui/ve.ui.MobileContext.js"></script>
<script>ve.init.platform.setModulesUrl( '../../modules'
);</script>
diff --git a/modules/ve/test/index.html b/modules/ve/test/index.html
index be8d06a..d4a444d 100644
--- a/modules/ve/test/index.html
+++ b/modules/ve/test/index.html
@@ -182,6 +182,7 @@
<script src="../../../modules/ve/ui/ve.ui.Surface.js"></script>
<script
src="../../../modules/ve/ui/ve.ui.DesktopSurface.js"></script>
<script src="../../../modules/ve/ui/ve.ui.Context.js"></script>
+ <script
src="../../../modules/ve/ui/ve.ui.DesktopContext.js"></script>
<script src="../../../modules/ve/ui/ve.ui.Dialog.js"></script>
<script
src="../../../modules/ve/ui/ve.ui.Inspector.js"></script>
<script
src="../../../modules/ve/ui/ve.ui.WindowSet.js"></script>
@@ -222,6 +223,7 @@
<!-- visualEditor.mobile -->
<script
src="../../../modules/ve/ui/ve.ui.MobileSurface.js"></script>
+ <script
src="../../../modules/ve/ui/ve.ui.MobileContext.js"></script>
<script>ve.init.platform.setModulesUrl( '../../../modules'
);</script>
diff --git a/modules/ve/ui/styles/ve.ui.Context.css
b/modules/ve/ui/styles/ve.ui.Context.css
deleted file mode 100644
index 4463686..0000000
--- a/modules/ve/ui/styles/ve.ui.Context.css
+++ /dev/null
@@ -1,56 +0,0 @@
-/*!
- * VisualEditor UserInterface Context styles.
- *
- * @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
- * @license The MIT License (MIT); see LICENSE.txt
- */
-
-.ve-ui-context,
-.ve-ui-context-inspectors,
-.ve-ui-context-menu {
- position: absolute;
-}
-
-.ve-ui-context-menu .oo-ui-toolbar-bar {
- white-space: nowrap;
- border: none;
- background: none;
-}
-
-.ve-ui-context-menu .oo-ui-toolGroup {
- border: none;
- margin: 0;
-}
-
-.ve-ui-context-menu .oo-ui-tool,
-.ve-ui-context-menu .oo-ui-tool:hover {
- border: none;
-}
-
-.ve-ui-context-menu .oo-ui-tool:active,
-.ve-ui-context-menu .oo-ui-tool-active {
- background-image: none;
-}
-
-.ve-ui-context > .oo-ui-popupWidget:not(.oo-ui-popupWidget-tailed)
.oo-ui-popupWidget-popup {
- margin-top: 0.25em;
-}
-
-/* @noflip */
-.ltr .ve-ui-context > .oo-ui-popupWidget:not(.oo-ui-popupWidget-tailed)
.oo-ui-popupWidget-popup {
- margin-left: -0.5em;
-}
-
-/* @noflip */
-.rtl .ve-ui-context > .oo-ui-popupWidget:not(.oo-ui-popupWidget-tailed)
.oo-ui-popupWidget-popup {
- margin-left: 0.5em;
-}
-
-.ve-ui-context > .oo-ui-popupWidget:not(.oo-ui-popupWidget-tailed)
.oo-ui-context-menu {
- right: 0;
-}
-
-/* HACK: Compensate for oo-ui-frame-content setting the font size to 0.8em */
-.oo-ui-frame-content .ve-ui-context {
- font-size: 1.25em;
-}
diff --git a/modules/ve/ui/styles/ve.ui.DesktopContext.css
b/modules/ve/ui/styles/ve.ui.DesktopContext.css
new file mode 100644
index 0000000..a00fb4c
--- /dev/null
+++ b/modules/ve/ui/styles/ve.ui.DesktopContext.css
@@ -0,0 +1,56 @@
+/*!
+ * VisualEditor UserInterface Context styles.
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+.ve-ui-desktopContext,
+.ve-ui-desktopContext-inspectors,
+.ve-ui-desktopContext-menu {
+ position: absolute;
+}
+
+.ve-ui-desktopContext-menu .oo-ui-toolbar-bar {
+ white-space: nowrap;
+ border: none;
+ background: none;
+}
+
+.ve-ui-desktopContext-menu .oo-ui-toolGroup {
+ border: none;
+ margin: 0;
+}
+
+.ve-ui-desktopContext-menu .oo-ui-tool,
+.ve-ui-desktopContext-menu .oo-ui-tool:hover {
+ border: none;
+}
+
+.ve-ui-desktopContext-menu .oo-ui-tool:active,
+.ve-ui-desktopContext-menu .oo-ui-tool-active {
+ background-image: none;
+}
+
+.ve-ui-desktopContext > .oo-ui-popupWidget:not(.oo-ui-popupWidget-tailed)
.oo-ui-popupWidget-popup {
+ margin-top: 0.25em;
+}
+
+/* @noflip */
+.ltr .ve-ui-desktopContext > .oo-ui-popupWidget:not(.oo-ui-popupWidget-tailed)
.oo-ui-popupWidget-popup {
+ margin-left: -0.5em;
+}
+
+/* @noflip */
+.rtl .ve-ui-desktopContext > .oo-ui-popupWidget:not(.oo-ui-popupWidget-tailed)
.oo-ui-popupWidget-popup {
+ margin-left: 0.5em;
+}
+
+.ve-ui-desktopContext > .oo-ui-popupWidget:not(.oo-ui-popupWidget-tailed)
.oo-ui-context-menu {
+ right: 0;
+}
+
+/* HACK: Compensate for oo-ui-frame-content setting the font size to 0.8em */
+.oo-ui-frame-content .ve-ui-desktopContext .oo-ui-popupWidget {
+ font-size: 1.25em;
+}
diff --git a/modules/ve/ui/ve.ui.Context.js b/modules/ve/ui/ve.ui.Context.js
index 0684780..7148e9c 100644
--- a/modules/ve/ui/ve.ui.Context.js
+++ b/modules/ve/ui/ve.ui.Context.js
@@ -9,6 +9,7 @@
* UserInterface context.
*
* @class
+ * @abstract
* @extends OO.ui.Element
*
* @constructor
@@ -21,53 +22,7 @@
// Properties
this.surface = surface;
- this.visible = false;
- this.showing = false;
- this.hiding = false;
- this.selecting = false;
- this.inspectorOpening = false;
- this.inspectorClosing = false;
- this.relocating = false;
- this.embedded = false;
- this.selection = null;
- this.toolbar = null;
- this.afterModelSelectTimeout = null;
- this.popup = new OO.ui.PopupWidget( {
- '$': this.$,
- '$container': this.surface.getView().$element
- } );
- this.$menu = this.$( '<div>' );
this.inspectors = new ve.ui.WindowSet( surface, ve.ui.inspectorFactory
);
-
- // Initialization
- this.$element.addClass( 've-ui-context' ).append( this.popup.$element );
- this.inspectors.$element.addClass( 've-ui-context-inspectors' );
- this.popup.$body.append(
- this.$menu.addClass( 've-ui-context-menu' ),
- this.inspectors.$element.addClass( 've-ui-context-inspectors' )
- );
-
- // Events
- this.surface.getModel().connect( this, { 'select': 'onModelSelect' } );
- this.surface.getView().connect( this, {
- 'selectionStart': 'onSelectionStart',
- 'selectionEnd': 'onSelectionEnd',
- 'relocationStart': 'onRelocationStart',
- 'relocationEnd': 'onRelocationEnd',
- 'focus': 'onSurfaceFocus'
- } );
- this.inspectors.connect( this, {
- 'opening': 'onInspectorOpening',
- 'open': 'onInspectorOpen',
- 'closing': 'onInspectorClosing',
- 'close': 'onInspectorClose'
- } );
-
- this.$( this.getElementWindow() ).on( {
- 'resize': ve.bind( this.update, this )
- } );
- this.$element.add( this.$menu )
- .on( 'mousedown', false );
};
/* Inheritance */
@@ -77,163 +32,7 @@
/* Methods */
/**
- * Handle selection changes in the model.
- *
- * Changes are ignored while the user is selecting text or relocating content,
apart from closing
- * the popup if it's open. While an inspector is opening or closing, all
changes are ignored so as
- * to prevent inspectors that change the selection from within their
open/close handlers from
- * causing issues.
- *
- * The response to selection changes is deferred to prevent close handlers
that process
- * changes from causing this function to recurse. These responses are also
batched for efficiency,
- * so that if there are three selection changes in the same tick,
afterModelSelect() only runs once.
- *
- * @method
- * @see #afterModelSelect
- */
-ve.ui.Context.prototype.onModelSelect = function () {
- if ( this.showing || this.hiding || this.inspectorOpening ||
this.inspectorClosing ) {
- clearTimeout( this.afterModelSelectTimeout );
- } else {
- if ( this.afterModelSelectTimeout === null ) {
- this.afterModelSelectTimeout = setTimeout( ve.bind(
this.afterModelSelect, this ) );
- }
- }
-};
-
-/**
- * Deferred response to one or more select events.
- *
- * Update the context menu for the new selection, except if the user is
selecting or relocating
- * content. If the popup is open, close it, even while selecting or relocating.
- */
-ve.ui.Context.prototype.afterModelSelect = function () {
- this.afterModelSelectTimeout = null;
- if ( this.popup.isVisible() ) {
- this.hide();
- }
- // Bypass while dragging
- if ( this.selecting || this.relocating ) {
- return;
- }
- this.update();
-};
-
-/**
- * Respond to focus events on the surfaceView by hiding the context.
- *
- * If there's an inspector open and the user manages to drop the cursor in the
surface such that
- * the selection doesn't change (i.e. the resulting model selection is equal
to the previous model
- * selection), then #onModelSelect won't cause the inspector to be closed, so
we do that here.
- *
- * Hiding the context immediately on focus also avoids flickering phenomena
where the inspector
- * remains open or the context remains visible in the wrong place while the
selection is visually
- * already moving somewhere else. We deliberately don't call #update to avoid
drawing the context
- * in a place that the selection is about to move away from.
- *
- * However, we only do this when clicking out of an inspector. Hiding the
context when the document
- * is focused through other means than closing an inspector is actually
harmful.
- *
- * We don't have to defer the response to this event because there is no
danger that inspectors'
- * close handlers will end up invoking this handler again.
- */
-ve.ui.Context.prototype.onSurfaceFocus = function () {
- if ( this.inspectors.getCurrentWindow() ) {
- this.hide();
- }
-};
-
-/**
- * Handle selection start events on the view.
- *
- * @method
- */
-ve.ui.Context.prototype.onSelectionStart = function () {
- this.selecting = true;
- this.hide();
-};
-
-/**
- * Handle selection end events on the view.
- *
- * @method
- */
-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();
-};
-
-/**
- * Handle an inspector that's being opened.
- *
- * @method
- * @param {ve.ui.Inspector} inspector Inspector that's being opened
- * @param {Object} [config] Inspector opening information
- */
-ve.ui.Context.prototype.onInspectorOpening = function () {
- this.selection = this.surface.getModel().getSelection();
- this.inspectorOpening = true;
-};
-
-/**
- * Handle an inspector that's been opened.
- *
- * @method
- * @param {ve.ui.Inspector} inspector Inspector that's been opened
- * @param {Object} [config] Inspector opening information
- */
-ve.ui.Context.prototype.onInspectorOpen = function () {
- this.inspectorOpening = false;
- this.show( true );
-};
-
-/**
- * Handle an inspector that's being closed.
- *
- * @method
- * @param {ve.ui.Inspector} inspector Inspector that's being closed
- * @param {Object} [config] Inspector closing information
- */
-ve.ui.Context.prototype.onInspectorClosing = function () {
- this.inspectorClosing = true;
-};
-
-/**
- * Handle an inspector that's been closed.
- *
- * @method
- * @param {ve.ui.Inspector} inspector Inspector that's been closed
- * @param {Object} [config] Inspector closing information
- */
-ve.ui.Context.prototype.onInspectorClose = function () {
- this.inspectorClosing = false;
- this.update();
-};
-
-/**
- * Gets the surface the context is being used in.
+ * Get the surface the context is being used in.
*
* @method
* @returns {ve.ui.Surface} Surface of context
@@ -243,7 +42,7 @@
};
/**
- * Gets an inspector.
+ * Get an inspector.
*
* @method
* @param {string} Symbolic name of inspector
@@ -266,221 +65,13 @@
};
/**
- * Updates the context menu.
+ * Hide the context.
*
* @method
- * @param {boolean} [transition=false] Use a smooth transition
- * @param {boolean} [repositionOnly=false] The context is only being moved so
don't fade in
+ * @abstract
* @chainable
- */
-ve.ui.Context.prototype.update = function ( transition, repositionOnly ) {
- var i, nodes, tools, tool,
- fragment = this.surface.getModel().getFragment( null, false ),
- selection = fragment.getRange(),
- inspector = this.inspectors.getCurrentWindow();
-
- if ( inspector && selection.equals( this.selection ) ) {
- // There's an inspector, and the selection hasn't changed,
update the position
- this.show( transition, repositionOnly );
- } else {
- // No inspector is open, or the selection has changed, show a
menu of available inspectors
- tools = ve.ui.toolFactory.getToolsForAnnotations(
fragment.getAnnotations() );
- nodes = fragment.getCoveredNodes();
- for ( i = 0; i < nodes.length; i++ ) {
- if ( nodes[i].range && nodes[i].range.isCollapsed() ) {
- nodes.splice( i, 1 );
- i--;
- }
- }
- if ( nodes.length === 1 ) {
- tool = ve.ui.toolFactory.getToolForNode( nodes[0].node
);
- if ( tool ) {
- tools.push( tool );
- }
- }
- if ( tools.length ) {
- // There's at least one inspectable annotation, build a
menu and show it
- this.$menu.empty();
- if ( this.toolbar ) {
- this.toolbar.destroy();
- }
- this.toolbar = new ve.ui.Toolbar( this.surface );
- this.toolbar.setup( [ { 'include': tools } ] );
- this.$menu.append( this.toolbar.$element );
- this.show( transition, repositionOnly );
- this.toolbar.initialize();
- } else if ( this.visible ) {
- // Nothing to inspect
- this.hide();
- }
- }
-
- // Remember selection for next time
- this.selection = selection.clone();
-
- return this;
-};
-
-/**
- * Updates the position and size.
- *
- * @method
- * @param {boolean} [transition=false] Use a smooth transition
- * @chainable
- */
-ve.ui.Context.prototype.updateDimensions = function ( transition ) {
- var $node, $container, focusableOffset, focusableWidth, nodePosition,
cursorPosition, position,
- documentOffset, nodeOffset,
- surface = this.surface.getView(),
- inspector = this.inspectors.getCurrentWindow(),
- focusedNode = surface.getFocusedNode(),
- surfaceOffset = surface.$element.offset(),
- rtl = this.surface.view.getDir() === 'rtl';
-
- $container = inspector ? this.inspectors.$element : this.$menu;
- if ( focusedNode ) {
- // We're on top of a node
- $node = focusedNode.$focusable || focusedNode.$element;
- if ( this.embedded ) {
- // Get the position relative to the surface it is
embedded in
- focusableOffset = OO.ui.Element.getRelativePosition(
- $node, this.surface.$element
- );
- position = { 'y': focusableOffset.top };
- // When context is embedded in RTL, it requires
adjustments to the relative
- // positioning (pop up on the other side):
- if ( rtl ) {
- position.x = focusableOffset.left;
- this.popup.align = 'left';
- } else {
- focusableWidth = $node.outerWidth();
- position.x = focusableOffset.left +
focusableWidth;
- this.popup.align = 'right';
- }
- } else {
- // The focused node may be in a wrapper, so calculate
the offset relative to the document
- documentOffset =
surface.getDocument().getDocumentNode().$element.offset();
- nodeOffset = $node.offset();
- nodePosition = {
- top: nodeOffset.top - documentOffset.top,
- left: nodeOffset.left - documentOffset.left
- };
- // Get the position of the focusedNode:
- position = { 'x': nodePosition.left, 'y':
nodePosition.top + $node.outerHeight() };
- // When the context is displayed in LTR, it should be
on the right of the node
- if ( !rtl ) {
- position.x += $node.outerWidth();
- }
- this.popup.align = 'center';
- }
- } else {
- // We're on top of a selected text
- // Get the position of the cursor
- cursorPosition = surface.getSelectionRect();
- if ( cursorPosition ) {
- // Correct for surface offset:
- position = {
- 'x': cursorPosition.end.x - surfaceOffset.left,
- 'y': cursorPosition.end.y - surfaceOffset.top
- };
- }
- // If !cursorPosition, the surface apparently isn't selected,
so getSelectionRect()
- // returned null. This shouldn't happen because the context is
only supposed to be
- // displayed in response to a selection, but for some reason
this does happen when opening
- // an inspector without changing the selection.
- // Skip updating the cursor position, but still update the
width and height.
-
- this.popup.align = 'center';
- }
-
- if ( position ) {
- this.$element.css( { 'left': position.x, 'top': position.y } );
- }
-
- this.popup.display(
- $container.outerWidth( true ),
- $container.outerHeight( true ),
- transition
- );
-
- return this;
-};
-
-/**
- * Shows the context menu.
- *
- * @method
- * @param {boolean} [transition=false] Use a smooth transition
- * @param {boolean} [repositionOnly=false] The context is only being moved so
don't fade in
- * @chainable
- */
-ve.ui.Context.prototype.show = function ( transition, repositionOnly ) {
- var inspector = this.inspectors.getCurrentWindow(),
- focusedNode = this.surface.getView().getFocusedNode();
-
- if ( !this.showing && !this.hiding ) {
- this.showing = true;
-
- // HACK: make the context and popup visibility: hidden; instead
of display: none; because
- // they contain inspector iframes, and applying display: none;
to those causes them to
- // not load in Firefox
- this.$element.css( 'visibility', '' );
- this.popup.$element.css( 'visibility', '' );
- this.popup.show();
-
- // Show either inspector or menu
- if ( inspector ) {
- this.$menu.hide();
- if ( !repositionOnly ) {
- inspector.$element.css( 'opacity', 0 );
- }
- // Update size and fade the inspector in after
animation is complete
- setTimeout( ve.bind( function () {
- inspector.fitHeightToContents();
- this.updateDimensions( transition );
- inspector.$element.css( 'opacity', 1 );
- }, this ), 200 );
- } else {
- this.embedded = (
- focusedNode &&
- focusedNode.$focusable.outerHeight() >
this.$menu.outerHeight() * 2 &&
- focusedNode.$focusable.outerWidth() >
this.$menu.outerWidth() * 2
- );
- this.popup.useTail( !this.embedded );
- this.$menu.show();
- }
-
- this.updateDimensions( transition );
-
- this.visible = true;
- this.showing = false;
- }
-
- return this;
-};
-
-/**
- * Hides the context menu.
- *
- * @method
- * @chainable
+ * @throws {Error} If this method is not overridden in a concrete subclass
*/
ve.ui.Context.prototype.hide = function () {
- var inspector = this.inspectors.getCurrentWindow();
-
- if ( !this.hiding && !this.showing ) {
- this.hiding = true;
- if ( inspector ) {
- inspector.close( { 'action': 'hide' } );
- }
- // HACK: make the context and popup visibility: hidden; instead
of display: none; because
- // they contain inspector iframes, and applying display: none;
to those causes them to
- // not load in Firefox
- this.popup.hide();
- this.popup.$element.show().css( 'visibility', 'hidden' );
- this.$element.css( 'visibility', 'hidden' );
- this.visible = false;
- this.hiding = false;
- }
- return this;
+ throw new Error( 've.ui.Context.hide must be overridden in subclass' );
};
diff --git a/modules/ve/ui/ve.ui.DesktopContext.js
b/modules/ve/ui/ve.ui.DesktopContext.js
new file mode 100644
index 0000000..43831b9
--- /dev/null
+++ b/modules/ve/ui/ve.ui.DesktopContext.js
@@ -0,0 +1,450 @@
+/*!
+ * VisualEditor UserInterface DesktopContext class.
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+/**
+ * UserInterface context that positions inspectors near the text cursor on
+ * desktop.
+ *
+ * @class
+ * @extends ve.ui.Context
+ *
+ * @constructor
+ * @param {ve.ui.Surface} surface
+ * @param {Object} [config] Configuration options
+ */
+ve.ui.DesktopContext = function VeUiDesktopContext( surface, config ) {
+ // Parent constructor
+ ve.ui.Context.call( this, surface, config );
+
+ // Properties
+ this.visible = false;
+ this.showing = false;
+ this.hiding = false;
+ this.selecting = false;
+ this.inspectorOpening = false;
+ this.inspectorClosing = false;
+ this.relocating = false;
+ this.embedded = false;
+ this.selection = null;
+ this.toolbar = null;
+ this.afterModelSelectTimeout = null;
+
+ // Events
+ this.surface.getModel().connect( this, { 'select': 'onModelSelect' } );
+ this.surface.getView().connect( this, {
+ 'selectionStart': 'onSelectionStart',
+ 'selectionEnd': 'onSelectionEnd',
+ 'relocationStart': 'onRelocationStart',
+ 'relocationEnd': 'onRelocationEnd',
+ 'focus': 'onSurfaceFocus'
+ } );
+ this.inspectors.connect( this, {
+ 'opening': 'onInspectorOpening',
+ 'open': 'onInspectorOpen',
+ 'closing': 'onInspectorClosing',
+ 'close': 'onInspectorClose'
+ } );
+
+ this.$( this.getElementWindow() ).on( {
+ 'resize': ve.bind( this.update, this )
+ } );
+ this.$element.add( this.$menu )
+ .on( 'mousedown', false );
+
+ // Properties
+ this.$menu = this.$( '<div>' );
+ this.popup = new OO.ui.PopupWidget( {
+ '$': this.$,
+ '$container': this.surface.getView().$element
+ } );
+
+ // Initialization
+ this.$element.addClass( 've-ui-desktopContext' ).append(
this.popup.$element );
+ this.popup.$body.append(
+ this.$menu.addClass( 've-ui-desktopContext-menu' ),
+ this.inspectors.$element.addClass(
've-ui-desktopContext-inspectors' )
+ );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.DesktopContext, ve.ui.Context );
+
+/* Methods */
+
+/**
+ * Handle selection changes in the model.
+ *
+ * Changes are ignored while the user is selecting text or relocating content,
apart from closing
+ * the popup if it's open. While an inspector is opening or closing, all
changes are ignored so as
+ * to prevent inspectors that change the selection from within their
open/close handlers from
+ * causing issues.
+ *
+ * The response to selection changes is deferred to prevent close handlers
that process
+ * changes from causing this function to recurse. These responses are also
batched for efficiency,
+ * so that if there are three selection changes in the same tick,
afterModelSelect() only runs once.
+ *
+ * @method
+ * @see #afterModelSelect
+ */
+ve.ui.DesktopContext.prototype.onModelSelect = function () {
+ if ( this.showing || this.hiding || this.inspectorOpening ||
this.inspectorClosing ) {
+ clearTimeout( this.afterModelSelectTimeout );
+ } else {
+ if ( this.afterModelSelectTimeout === null ) {
+ this.afterModelSelectTimeout = setTimeout( ve.bind(
this.afterModelSelect, this ) );
+ }
+ }
+};
+
+/**
+ * Deferred response to one or more select events.
+ *
+ * Update the context menu for the new selection, except if the user is
selecting or relocating
+ * content. If the popup is open, close it, even while selecting or relocating.
+ */
+ve.ui.DesktopContext.prototype.afterModelSelect = function () {
+ this.afterModelSelectTimeout = null;
+ if ( this.popup.isVisible() ) {
+ this.hide();
+ }
+ // Bypass while dragging
+ if ( this.selecting || this.relocating ) {
+ return;
+ }
+ this.update();
+};
+
+/**
+ * Respond to focus events on the surfaceView by hiding the context.
+ *
+ * If there's an inspector open and the user manages to drop the cursor in the
surface such that
+ * the selection doesn't change (i.e. the resulting model selection is equal
to the previous model
+ * selection), then #onModelSelect won't cause the inspector to be closed, so
we do that here.
+ *
+ * Hiding the context immediately on focus also avoids flickering phenomena
where the inspector
+ * remains open or the context remains visible in the wrong place while the
selection is visually
+ * already moving somewhere else. We deliberately don't call #update to avoid
drawing the context
+ * in a place that the selection is about to move away from.
+ *
+ * However, we only do this when clicking out of an inspector. Hiding the
context when the document
+ * is focused through other means than closing an inspector is actually
harmful.
+ *
+ * We don't have to defer the response to this event because there is no
danger that inspectors'
+ * close handlers will end up invoking this handler again.
+ */
+ve.ui.DesktopContext.prototype.onSurfaceFocus = function () {
+ if ( this.inspectors.getCurrentWindow() ) {
+ this.hide();
+ }
+};
+
+/**
+ * Handle selection start events on the view.
+ *
+ * @method
+ */
+ve.ui.DesktopContext.prototype.onSelectionStart = function () {
+ this.selecting = true;
+ this.hide();
+};
+
+/**
+ * Handle selection end events on the view.
+ *
+ * @method
+ */
+ve.ui.DesktopContext.prototype.onSelectionEnd = function () {
+ this.selecting = false;
+ if ( !this.relocating ) {
+ this.update();
+ }
+};
+
+/**
+ * Handle selection start events on the view.
+ *
+ * @method
+ */
+ve.ui.DesktopContext.prototype.onRelocationStart = function () {
+ this.relocating = true;
+ this.hide();
+};
+
+/**
+ * Handle selection end events on the view.
+ *
+ * @method
+ */
+ve.ui.DesktopContext.prototype.onRelocationEnd = function () {
+ this.relocating = false;
+ this.update();
+};
+
+/**
+ * Handle an inspector that's being opened.
+ *
+ * @method
+ * @param {ve.ui.Inspector} inspector Inspector that's being opened
+ * @param {Object} [config] Inspector opening information
+ */
+ve.ui.DesktopContext.prototype.onInspectorOpening = function () {
+ this.selection = this.surface.getModel().getSelection();
+ this.inspectorOpening = true;
+};
+
+/**
+ * Handle an inspector that's been opened.
+ *
+ * @method
+ * @param {ve.ui.Inspector} inspector Inspector that's been opened
+ * @param {Object} [config] Inspector opening information
+ */
+ve.ui.DesktopContext.prototype.onInspectorOpen = function () {
+ this.inspectorOpening = false;
+ this.show( true );
+};
+
+/**
+ * Handle an inspector that's being closed.
+ *
+ * @method
+ * @param {ve.ui.Inspector} inspector Inspector that's being closed
+ * @param {Object} [config] Inspector closing information
+ */
+ve.ui.DesktopContext.prototype.onInspectorClosing = function () {
+ this.inspectorClosing = true;
+};
+
+/**
+ * Handle an inspector that's been closed.
+ *
+ * @method
+ * @param {ve.ui.Inspector} inspector Inspector that's been closed
+ * @param {Object} [config] Inspector closing information
+ */
+ve.ui.DesktopContext.prototype.onInspectorClose = function () {
+ this.inspectorClosing = false;
+ this.update();
+};
+
+/**
+ * Updates the context menu.
+ *
+ * @method
+ * @param {boolean} [transition=false] Use a smooth transition
+ * @param {boolean} [repositionOnly=false] The context is only being moved so
don't fade in
+ * @chainable
+ */
+ve.ui.DesktopContext.prototype.update = function ( transition, repositionOnly
) {
+ var i, nodes, tools, tool,
+ fragment = this.surface.getModel().getFragment( null, false ),
+ selection = fragment.getRange(),
+ inspector = this.inspectors.getCurrentWindow();
+
+ if ( inspector && selection.equals( this.selection ) ) {
+ // There's an inspector, and the selection hasn't changed,
update the position
+ this.show( transition, repositionOnly );
+ } else {
+ // No inspector is open, or the selection has changed, show a
menu of available inspectors
+ tools = ve.ui.toolFactory.getToolsForAnnotations(
fragment.getAnnotations() );
+ nodes = fragment.getCoveredNodes();
+ for ( i = 0; i < nodes.length; i++ ) {
+ if ( nodes[i].range && nodes[i].range.isCollapsed() ) {
+ nodes.splice( i, 1 );
+ i--;
+ }
+ }
+ if ( nodes.length === 1 ) {
+ tool = ve.ui.toolFactory.getToolForNode( nodes[0].node
);
+ if ( tool ) {
+ tools.push( tool );
+ }
+ }
+ if ( tools.length ) {
+ // There's at least one inspectable annotation, build a
menu and show it
+ this.$menu.empty();
+ if ( this.toolbar ) {
+ this.toolbar.destroy();
+ }
+ this.toolbar = new ve.ui.Toolbar( this.surface );
+ this.toolbar.setup( [ { 'include': tools } ] );
+ this.$menu.append( this.toolbar.$element );
+ this.show( transition, repositionOnly );
+ this.toolbar.initialize();
+ } else if ( this.visible ) {
+ // Nothing to inspect
+ this.hide();
+ }
+ }
+
+ // Remember selection for next time
+ this.selection = selection.clone();
+
+ return this;
+};
+
+/**
+ * Updates the position and size.
+ *
+ * @method
+ * @param {boolean} [transition=false] Use a smooth transition
+ * @chainable
+ */
+ve.ui.DesktopContext.prototype.updateDimensions = function ( transition ) {
+ var $node, $container, focusableOffset, focusableWidth, nodePosition,
cursorPosition, position,
+ documentOffset, nodeOffset,
+ surface = this.surface.getView(),
+ inspector = this.inspectors.getCurrentWindow(),
+ focusedNode = surface.getFocusedNode(),
+ surfaceOffset = surface.$element.offset(),
+ rtl = this.surface.view.getDir() === 'rtl';
+
+ $container = inspector ? this.inspectors.$element : this.$menu;
+ if ( focusedNode ) {
+ // We're on top of a node
+ $node = focusedNode.$focusable || focusedNode.$element;
+ if ( this.embedded ) {
+ // Get the position relative to the surface it is
embedded in
+ focusableOffset = OO.ui.Element.getRelativePosition(
+ $node, this.surface.$element
+ );
+ position = { 'y': focusableOffset.top };
+ // When context is embedded in RTL, it requires
adjustments to the relative
+ // positioning (pop up on the other side):
+ if ( rtl ) {
+ position.x = focusableOffset.left;
+ this.popup.align = 'left';
+ } else {
+ focusableWidth = $node.outerWidth();
+ position.x = focusableOffset.left +
focusableWidth;
+ this.popup.align = 'right';
+ }
+ } else {
+ // The focused node may be in a wrapper, so calculate
the offset relative to the document
+ documentOffset =
surface.getDocument().getDocumentNode().$element.offset();
+ nodeOffset = $node.offset();
+ nodePosition = {
+ top: nodeOffset.top - documentOffset.top,
+ left: nodeOffset.left - documentOffset.left
+ };
+ // Get the position of the focusedNode:
+ position = { 'x': nodePosition.left, 'y':
nodePosition.top + $node.outerHeight() };
+ // When the context is displayed in LTR, it should be
on the right of the node
+ if ( !rtl ) {
+ position.x += $node.outerWidth();
+ }
+ this.popup.align = 'center';
+ }
+ } else {
+ // We're on top of a selected text
+ // Get the position of the cursor
+ cursorPosition = surface.getSelectionRect();
+ if ( cursorPosition ) {
+ // Correct for surface offset:
+ position = {
+ 'x': cursorPosition.end.x - surfaceOffset.left,
+ 'y': cursorPosition.end.y - surfaceOffset.top
+ };
+ }
+ // If !cursorPosition, the surface apparently isn't selected,
so getSelectionRect()
+ // returned null. This shouldn't happen because the context is
only supposed to be
+ // displayed in response to a selection, but for some reason
this does happen when opening
+ // an inspector without changing the selection.
+ // Skip updating the cursor position, but still update the
width and height.
+
+ this.popup.align = 'center';
+ }
+
+ if ( position ) {
+ this.$element.css( { 'left': position.x, 'top': position.y } );
+ }
+
+ this.popup.display(
+ $container.outerWidth( true ),
+ $container.outerHeight( true ),
+ transition
+ );
+
+ return this;
+};
+
+/**
+ * Shows the context menu.
+ *
+ * @method
+ * @param {boolean} [transition=false] Use a smooth transition
+ * @param {boolean} [repositionOnly=false] The context is only being moved so
don't fade in
+ * @chainable
+ */
+ve.ui.DesktopContext.prototype.show = function ( transition, repositionOnly ) {
+ var inspector = this.inspectors.getCurrentWindow(),
+ focusedNode = this.surface.getView().getFocusedNode();
+
+ if ( !this.showing && !this.hiding ) {
+ this.showing = true;
+
+ // HACK: make the context and popup visibility: hidden; instead
of display: none; because
+ // they contain inspector iframes, and applying display: none;
to those causes them to
+ // not load in Firefox
+ this.$element.css( 'visibility', '' );
+ this.popup.$element.css( 'visibility', '' );
+ this.popup.show();
+
+ // Show either inspector or menu
+ if ( inspector ) {
+ this.$menu.hide();
+ if ( !repositionOnly ) {
+ inspector.$element.css( 'opacity', 0 );
+ }
+ // Update size and fade the inspector in after
animation is complete
+ setTimeout( ve.bind( function () {
+ inspector.fitHeightToContents();
+ this.updateDimensions( transition );
+ inspector.$element.css( 'opacity', 1 );
+ }, this ), 200 );
+ } else {
+ this.embedded = (
+ focusedNode &&
+ focusedNode.$focusable.outerHeight() >
this.$menu.outerHeight() * 2 &&
+ focusedNode.$focusable.outerWidth() >
this.$menu.outerWidth() * 2
+ );
+ this.popup.useTail( !this.embedded );
+ this.$menu.show();
+ }
+
+ this.updateDimensions( transition );
+
+ this.visible = true;
+ this.showing = false;
+ }
+
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+ve.ui.DesktopContext.prototype.hide = function () {
+ var inspector = this.inspectors.getCurrentWindow();
+
+ if ( !this.hiding && !this.showing ) {
+ this.hiding = true;
+ if ( inspector ) {
+ inspector.close( { 'action': 'hide' } );
+ }
+ // HACK: make the context and popup visibility: hidden; instead
of display: none; because
+ // they contain inspector iframes, and applying display: none;
to those causes them to
+ // not load in Firefox
+ this.popup.hide();
+ this.popup.$element.show().css( 'visibility', 'hidden' );
+ this.$element.css( 'visibility', 'hidden' );
+ this.visible = false;
+ this.hiding = false;
+ }
+ return this;
+};
diff --git a/modules/ve/ui/ve.ui.DesktopSurface.js
b/modules/ve/ui/ve.ui.DesktopSurface.js
index 3172ec8..aeaa2d1 100644
--- a/modules/ve/ui/ve.ui.DesktopSurface.js
+++ b/modules/ve/ui/ve.ui.DesktopSurface.js
@@ -33,6 +33,6 @@
* @method
*/
ve.ui.DesktopSurface.prototype.setupContext = function () {
- this.context = new ve.ui.Context( this, { '$': this.$ } );
+ this.context = new ve.ui.DesktopContext( this, { '$': this.$ } );
this.$localOverlay.append( this.context.$element );
};
diff --git a/modules/ve/ui/ve.ui.MobileContext.js
b/modules/ve/ui/ve.ui.MobileContext.js
new file mode 100644
index 0000000..af060c3
--- /dev/null
+++ b/modules/ve/ui/ve.ui.MobileContext.js
@@ -0,0 +1,40 @@
+/*!
+ * VisualEditor UserInterface MobileContext class.
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+/**
+ * UserInterface context that displays inspector full screen.
+ *
+ * @class
+ * @extends ve.ui.Context
+ *
+ * @constructor
+ * @param {ve.ui.Surface} surface
+ * @param {Object} [config] Configuration options
+ */
+ve.ui.MobileContext = function VeUiMobileContext( surface, config ) {
+ // Parent constructor
+ ve.ui.Context.call( this, surface, config );
+
+ // Initialization
+ // TODO: do something more fancy, PanelLayout or a new drawer OOUI
widget
+ this.$element
+ .addClass( 've-ui-mobileContext' )
+ .append( this.inspectors.$element );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.MobileContext, ve.ui.Context );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ui.DesktopContext.prototype.hide = function () {
+ return this;
+};
diff --git a/modules/ve/ui/ve.ui.MobileSurface.js
b/modules/ve/ui/ve.ui.MobileSurface.js
index a7b23d6..a98bdf7 100644
--- a/modules/ve/ui/ve.ui.MobileSurface.js
+++ b/modules/ve/ui/ve.ui.MobileSurface.js
@@ -31,9 +31,9 @@
* Set up a context.
*
* @method
- * @returns {ve.ui.Context} Context instance
+ * @returns {ve.ui.MobileContext} Context instance
*/
ve.ui.MobileSurface.prototype.setupContext = function () {
- this.context = new ve.ui.Context( this, { '$': this.$ } );
+ this.context = new ve.ui.MobileContext( this, { '$': this.$ } );
this.$globalOverlay.append( this.context.$element );
};
diff --git a/modules/ve/ui/ve.ui.Surface.js b/modules/ve/ui/ve.ui.Surface.js
index dbf09b9..05cdf7c 100644
--- a/modules/ve/ui/ve.ui.Surface.js
+++ b/modules/ve/ui/ve.ui.Surface.js
@@ -116,6 +116,7 @@
*
* @method
* @abstract
+ * @throws {Error} If this method is not overridden in a concrete subclass
*/
ve.ui.Surface.prototype.setupContext = function () {
throw new Error( 've.ui.Surface.setupContext must be overridden in
subclass' );
--
To view, visit https://gerrit.wikimedia.org/r/118034
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: Id3f5f3f39978836f084f7c7e15505e4b6aeb9b44
Gerrit-PatchSet: 13
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: JGonera <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Trevor Parscal <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits