jenkins-bot has submitted this change and it was merged.
Change subject: Introduce key down handlers to separate logic from ce surface
......................................................................
Introduce key down handlers to separate logic from ce surface
Bug: T78696
Bug: T116748
Change-Id: If0b1a855ac4454ad6087ec8c3f1e82e8fac618e4
---
M .jsduck/categories.json
M build/modules.json
M demos/ve/desktop.html
M demos/ve/mobile.html
A src/ce/keydownhandlers/ve.ce.LinearArrowKeyDownHandler.js
A src/ce/keydownhandlers/ve.ce.LinearDeleteKeyDownHandler.js
A src/ce/keydownhandlers/ve.ce.LinearEnterKeyDownHandler.js
A src/ce/keydownhandlers/ve.ce.LinearEscapeKeyDownHandler.js
A src/ce/keydownhandlers/ve.ce.TableArrowKeyDownHandler.js
A src/ce/keydownhandlers/ve.ce.TableDeleteKeyDownHandler.js
A src/ce/keydownhandlers/ve.ce.TableEnterKeyDownHandler.js
A src/ce/ve.ce.KeyDownHandler.js
A src/ce/ve.ce.KeyDownHandlerFactory.js
M src/ce/ve.ce.Surface.js
M tests/ce/ve.ce.Surface.test.js
M tests/index.html
16 files changed, 1,166 insertions(+), 740 deletions(-)
Approvals:
Divec: Looks good to me, approved
Jforrester: Looks good to me, but someone else must approve
jenkins-bot: Verified
diff --git a/.jsduck/categories.json b/.jsduck/categories.json
index fd21fd8..b34b8d6 100644
--- a/.jsduck/categories.json
+++ b/.jsduck/categories.json
@@ -114,6 +114,14 @@
"classes": ["ve.ce.*Annotation"]
},
{
+ "name": "Factories",
+ "classes": ["ve.ce.*Factory"]
+ },
+ {
+ "name": "Key down handlers",
+ "classes": ["ve.ce.*KeyDownHandler"]
+ },
+ {
"name": "Nodes",
"classes": ["ve.ce.Document", "ve.ce.*Node"]
}
@@ -132,9 +140,7 @@
"ve.ui.Toolbar",
"ve.ui.TargetToolbar",
"ve.ui.DebugBar",
- "ve.ui.Trigger",
- "ve.ui.*TransferHandler",
- "ve.ui.DataTransferItem"
+ "ve.ui.Trigger"
]
},
{
@@ -150,6 +156,10 @@
"classes": ["ve.ui.*ContextItem"]
},
{
+ "name": "Data transfer handlers",
+ "classes": ["ve.ui.*TransferHandler",
"ve.ui.DataTransferItem"]
+ },
+ {
"name": "Elements",
"classes": ["ve.ui.*Element"]
},
diff --git a/build/modules.json b/build/modules.json
index b4a4672..7779105 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -323,6 +323,15 @@
"src/ce/ve.ce.TableCellableNode.js",
"src/ce/ve.ce.Surface.js",
"src/ce/ve.ce.SurfaceObserver.js",
+ "src/ce/ve.ce.KeyDownHandlerFactory.js",
+ "src/ce/ve.ce.KeyDownHandler.js",
+
"src/ce/keydownhandlers/ve.ce.LinearArrowKeyDownHandler.js",
+
"src/ce/keydownhandlers/ve.ce.LinearDeleteKeyDownHandler.js",
+
"src/ce/keydownhandlers/ve.ce.LinearEnterKeyDownHandler.js",
+
"src/ce/keydownhandlers/ve.ce.LinearEscapeKeyDownHandler.js",
+
"src/ce/keydownhandlers/ve.ce.TableArrowKeyDownHandler.js",
+
"src/ce/keydownhandlers/ve.ce.TableDeleteKeyDownHandler.js",
+
"src/ce/keydownhandlers/ve.ce.TableEnterKeyDownHandler.js",
"src/ce/nodes/ve.ce.GeneratedContentNode.js",
"src/ce/nodes/ve.ce.AlienNode.js",
"src/ce/nodes/ve.ce.BlockquoteNode.js",
diff --git a/demos/ve/desktop.html b/demos/ve/desktop.html
index a29eeef..39f43a7 100644
--- a/demos/ve/desktop.html
+++ b/demos/ve/desktop.html
@@ -284,6 +284,15 @@
<script src="../../src/ce/ve.ce.TableCellableNode.js"></script>
<script src="../../src/ce/ve.ce.Surface.js"></script>
<script src="../../src/ce/ve.ce.SurfaceObserver.js"></script>
+ <script
src="../../src/ce/ve.ce.KeyDownHandlerFactory.js"></script>
+ <script src="../../src/ce/ve.ce.KeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.LinearArrowKeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.LinearDeleteKeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.LinearEnterKeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.LinearEscapeKeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.TableArrowKeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.TableDeleteKeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.TableEnterKeyDownHandler.js"></script>
<script
src="../../src/ce/nodes/ve.ce.GeneratedContentNode.js"></script>
<script src="../../src/ce/nodes/ve.ce.AlienNode.js"></script>
<script
src="../../src/ce/nodes/ve.ce.BlockquoteNode.js"></script>
diff --git a/demos/ve/mobile.html b/demos/ve/mobile.html
index b3a50d9..8f40031 100644
--- a/demos/ve/mobile.html
+++ b/demos/ve/mobile.html
@@ -286,6 +286,15 @@
<script src="../../src/ce/ve.ce.TableCellableNode.js"></script>
<script src="../../src/ce/ve.ce.Surface.js"></script>
<script src="../../src/ce/ve.ce.SurfaceObserver.js"></script>
+ <script
src="../../src/ce/ve.ce.KeyDownHandlerFactory.js"></script>
+ <script src="../../src/ce/ve.ce.KeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.LinearArrowKeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.LinearDeleteKeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.LinearEnterKeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.LinearEscapeKeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.TableArrowKeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.TableDeleteKeyDownHandler.js"></script>
+ <script
src="../../src/ce/keydownhandlers/ve.ce.TableEnterKeyDownHandler.js"></script>
<script
src="../../src/ce/nodes/ve.ce.GeneratedContentNode.js"></script>
<script src="../../src/ce/nodes/ve.ce.AlienNode.js"></script>
<script
src="../../src/ce/nodes/ve.ce.BlockquoteNode.js"></script>
diff --git a/src/ce/keydownhandlers/ve.ce.LinearArrowKeyDownHandler.js
b/src/ce/keydownhandlers/ve.ce.LinearArrowKeyDownHandler.js
new file mode 100644
index 0000000..13021e3
--- /dev/null
+++ b/src/ce/keydownhandlers/ve.ce.LinearArrowKeyDownHandler.js
@@ -0,0 +1,203 @@
+/*!
+ * VisualEditor ContentEditable linear arrow key down handler
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see
http://ve.mit-license.org
+ */
+
+/**
+ * Arrow key down handler for linear selections.
+ *
+ * @class
+ * @extends ve.ce.KeyDownHandler
+ *
+ * @constructor
+ */
+ve.ce.LinearArrowKeyDownHandler = function VeCeLinearArrowKeyDownHandler() {
+ // Parent constructor
+ ve.ui.LinearArrowKeyDownHandler.super.apply( this, arguments );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ce.LinearArrowKeyDownHandler, ve.ce.KeyDownHandler );
+
+/* Static properties */
+
+ve.ce.LinearArrowKeyDownHandler.static.name = 'linearArrow';
+
+ve.ce.LinearArrowKeyDownHandler.static.keys = [ OO.ui.Keys.UP,
OO.ui.Keys.DOWN, OO.ui.Keys.LEFT, OO.ui.Keys.RIGHT ];
+
+ve.ce.LinearArrowKeyDownHandler.static.supportedSelections = [ 'linear' ];
+
+/* Static methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ce.LinearArrowKeyDownHandler.static.execute = function ( surface, e ) {
+ var nativeRange, collapseNode, collapseOffset, direction,
directionality, upOrDown,
+ startFocusNode, startFocusOffset,
+ range = surface.model.getSelection().getRange();
+
+ // TODO: onDocumentKeyDown did this already
+ surface.surfaceObserver.stopTimerLoop();
+ // TODO: onDocumentKeyDown did this already
+ surface.surfaceObserver.pollOnce();
+
+ upOrDown = e.keyCode === OO.ui.Keys.UP || e.keyCode === OO.ui.Keys.DOWN;
+
+ if ( surface.focusedBlockSlug ) {
+ // Block level selection, so directionality is just css
directionality
+ if ( upOrDown ) {
+ direction = e.keyCode === OO.ui.Keys.DOWN ? 1 : -1;
+ } else {
+ directionality = $( surface.focusedBlockSlug ).css(
'direction' );
+ /*jshint bitwise:false */
+ if ( e.keyCode === OO.ui.Keys.LEFT ^ directionality ===
'rtl' ) {
+ // leftarrow in ltr, or rightarrow in rtl
+ direction = -1;
+ } else {
+ // leftarrow in rtl, or rightarrow in ltr
+ direction = 1;
+ }
+ }
+ range = surface.model.getDocument().getRelativeRange(
+ range,
+ direction,
+ 'character',
+ e.shiftKey,
+ surface.getActiveTableNode() ?
surface.getActiveTableNode().getEditingRange() : null
+ );
+ surface.model.setLinearSelection( range );
+ e.preventDefault();
+ return;
+ }
+
+ if ( surface.focusedNode ) {
+ if ( upOrDown ) {
+ direction = e.keyCode === OO.ui.Keys.DOWN ? 1 : -1;
+ } else {
+ directionality = surface.getFocusedNodeDirectionality();
+ /*jshint bitwise:false */
+ if ( e.keyCode === OO.ui.Keys.LEFT ^ directionality ===
'rtl' ) {
+ // leftarrow in ltr, or rightarrow in rtl
+ direction = -1;
+ } else {
+ // leftarrow in rtl, or rightarrow in ltr
+ direction = 1;
+ }
+ }
+
+ if ( !surface.focusedNode.isContent() ) {
+ // Block focusable node: move back/forward in DM (and
DOM) and preventDefault
+ range = surface.model.getDocument().getRelativeRange(
+ range,
+ direction,
+ 'character',
+ e.shiftKey,
+ surface.getActiveTableNode() ?
surface.getActiveTableNode().getEditingRange() : null
+ );
+ surface.model.setLinearSelection( range );
+ e.preventDefault();
+ return;
+ }
+ // Else inline focusable node
+
+ if ( e.shiftKey ) {
+ // There is no DOM range to expand (because the
selection is faked), so
+ // use "collapse to focus - observe - expand". Define
"focus" to be the
+ // edge of the focusedNode in the direction of motion
(so the selection
+ // always grows). This means that clicking on the
focusableNode then
+ // modifying the selection will always include the node.
+ if ( direction === -1 ^ range.isBackwards() ) {
+ range = range.flip();
+ }
+ surface.model.setLinearSelection( new ve.Range(
range.to ) );
+ } else {
+ // Move to start/end of node in the model in DM (and
DOM)
+ range = new ve.Range( direction === 1 ? range.end :
range.start );
+ surface.model.setLinearSelection( range );
+ if ( !upOrDown ) {
+ // un-shifted left/right: we've already moved
so preventDefault
+ e.preventDefault();
+ return;
+ }
+ // Else keep going with the cursor in the new place
+ }
+ }
+ // Else keep DM range and DOM selection as-is
+
+ if ( e.shiftKey && !surface.nativeSelection.extend &&
range.isBackwards() ) {
+ // If the browser doesn't support backwards selections, but the
dm range
+ // is backwards, then use "collapse to anchor - observe -
expand".
+ collapseNode = surface.nativeSelection.anchorNode;
+ collapseOffset = surface.nativeSelection.anchorOffset;
+ } else if ( e.shiftKey && !range.isCollapsed() && upOrDown ) {
+ // If selection is expanded and cursoring is up/down, use
+ // "collapse to focus - observe - expand" to work round quirks.
+ collapseNode = surface.nativeSelection.focusNode;
+ collapseOffset = surface.nativeSelection.focusOffset;
+ }
+ // Else don't collapse the selection
+
+ if ( collapseNode ) {
+ nativeRange = surface.getElementDocument().createRange();
+ nativeRange.setStart( collapseNode, collapseOffset );
+ nativeRange.setEnd( collapseNode, collapseOffset );
+ surface.nativeSelection.removeAllRanges();
+ surface.nativeSelection.addRange( nativeRange );
+ }
+
+ startFocusNode = surface.nativeSelection.focusNode;
+ startFocusOffset = surface.nativeSelection.focusOffset;
+
+ // Re-expand (or fixup) the selection after the native action, if
necessary
+ surface.eventSequencer.afterOne( { keydown: function () {
+ var viewNode, newRange, afterDirection;
+
+ // Chrome bug lets you cursor into a multi-line
contentEditable=false with up/down...
+ viewNode = $( surface.nativeSelection.focusNode ).closest(
'.ve-ce-leafNode,.ve-ce-branchNode' ).data( 'view' );
+ if ( !viewNode ) {
+ // Irrelevant selection (or none)
+ return;
+ }
+
+ if ( viewNode.isFocusable() ) {
+ // We've landed in a focusable node; fixup the range
+ if ( upOrDown ) {
+ // The intended direction is clear, even if the
cursor did not move
+ // or did something completely preposterous
+ afterDirection = e.keyCode === OO.ui.Keys.DOWN
? 1 : -1;
+ } else {
+ // Observe which way the cursor moved
+ afterDirection = ve.compareDocumentOrder(
+ surface.nativeSelection.focusNode,
+ surface.nativeSelection.focusOffset,
+ startFocusNode,
+ startFocusOffset
+ );
+ }
+ newRange = (
+ afterDirection > 0 ?
+ viewNode.getOuterRange() :
+ viewNode.getOuterRange().flip()
+ );
+ } else {
+ // Check where the range has moved to
+ surface.surfaceObserver.pollOnceNoCallback();
+ newRange = new ve.Range(
surface.surfaceObserver.getRange().to );
+ }
+
+ // Adjust range to use old anchor, if necessary
+ if ( e.shiftKey ) {
+ newRange = new ve.Range( range.from, newRange.to );
+ surface.getModel().setLinearSelection( newRange );
+ }
+ surface.updateActiveLink();
+ surface.surfaceObserver.pollOnce();
+ } } );
+};
+
+/* Registration */
+
+ve.ce.keyDownHandlerFactory.register( ve.ce.LinearArrowKeyDownHandler );
diff --git a/src/ce/keydownhandlers/ve.ce.LinearDeleteKeyDownHandler.js
b/src/ce/keydownhandlers/ve.ce.LinearDeleteKeyDownHandler.js
new file mode 100644
index 0000000..e3d2b2a
--- /dev/null
+++ b/src/ce/keydownhandlers/ve.ce.LinearDeleteKeyDownHandler.js
@@ -0,0 +1,235 @@
+/*!
+ * VisualEditor ContentEditable linear delete key down handler
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see
http://ve.mit-license.org
+ */
+
+/**
+ * Delete key down handler for linear selections.
+ *
+ * @class
+ * @extends ve.ce.KeyDownHandler
+ *
+ * @constructor
+ */
+ve.ce.LinearDeleteKeyDownHandler = function VeCeLinearDeleteKeyDownHandler() {
+ // Parent constructor
+ ve.ui.LinearDeleteKeyDownHandler.super.apply( this, arguments );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ce.LinearDeleteKeyDownHandler, ve.ce.KeyDownHandler );
+
+/* Static properties */
+
+ve.ce.LinearDeleteKeyDownHandler.static.name = 'linearDelete';
+
+ve.ce.LinearDeleteKeyDownHandler.static.keys = [ OO.ui.Keys.BACKSPACE,
OO.ui.Keys.DELETE ];
+
+ve.ce.LinearDeleteKeyDownHandler.static.supportedSelections = [ 'linear' ];
+
+/* Static methods */
+
+/**
+ * @inheritdoc
+ *
+ * The handler just schedules a poll to observe the native content removal,
unless
+ * one of the following is true:
+ * - The ctrlKey is down; or
+ * - The selection is expanded; or
+ * - We are directly adjacent to an element node in the deletion direction.
+ * In these cases, it will perform the content removal itself.
+ */
+ve.ce.LinearDeleteKeyDownHandler.static.execute = function ( surface, e ) {
+ var docLength, startNode, tableEditingRange, position, skipNode,
pairNode, linkNode, range,
+ documentModelSelectedNodes, i, node, nodeOuterRange, matrix,
+ direction = e.keyCode === OO.ui.Keys.DELETE ? 1 : -1,
+ unit = ( e.altKey === true || e.ctrlKey === true ) ? 'word' :
'character',
+ offset = 0,
+ rangeToRemove = surface.getModel().getSelection().getRange(),
+ documentModel = surface.getModel().getDocument(),
+ data = documentModel.data;
+
+ if ( direction === 1 && e.shiftKey && ve.getSystemPlatform() !== 'mac'
) {
+ // Shift+Del on non-Mac platforms performs 'cut', so
+ // don't handle it here.
+ return;
+ }
+
+ // Use native behaviour then poll if collapsed, unless we are adjacent
to some hard tag
+ // (or CTRL is down, in which case we can't reliably predict whether
the native behaviour
+ // would delete far enough to remove some element)
+ if ( rangeToRemove.isCollapsed() && !e.ctrlKey ) {
+ position = ve.adjacentDomPosition(
+ {
+ node: surface.nativeSelection.focusNode,
+ offset: surface.nativeSelection.focusOffset
+ },
+ direction
+ );
+ skipNode = position.steps[ position.steps.length - 1 ].node;
+ if ( skipNode.nodeType === Node.TEXT_NODE ) {
+ surface.eventSequencer.afterOne( {
+ keydown: surface.surfaceObserver.pollOnce.bind(
surface.surfaceObserver )
+ } );
+ return;
+ }
+
+ // If the native action would delete an outside nail, move
*two* cursor positions
+ // in the deletion direction, to get inside the link just past
the inside nail,
+ // then preventDefault
+ if (
+ direction > 0 ?
+ skipNode.classList.contains( 've-ce-nail-pre-open' ) :
+ skipNode.classList.contains( 've-ce-nail-post-close' )
+ ) {
+ position = ve.adjacentDomPosition( position, direction
);
+ range = document.createRange();
+ range.setStart( position.node, position.offset );
+ surface.nativeSelection.removeAllRanges();
+ surface.nativeSelection.addRange( range );
+ surface.updateActiveLink();
+ e.preventDefault();
+ return;
+ }
+
+ // If inside an empty link, delete it and preventDefault
+ if (
+ skipNode.classList &&
+ skipNode.classList.contains(
+ direction > 0 ?
+ 've-ce-nail-pre-close' :
+ 've-ce-nail-post-open'
+ ) &&
+ ( pairNode = (
+ direction > 0 ?
+ skipNode.previousSibling :
+ skipNode.nextSibling
+ ) ) &&
+ pairNode.classList &&
+ pairNode.classList.contains(
+ direction > 0 ?
+ 've-ce-nail-post-open' :
+ 've-ce-nail-pre-close'
+ )
+ ) {
+ linkNode = skipNode.parentNode;
+ range = document.createRange();
+ range.setStart(
+ linkNode.parentNode,
+ // Link's offset, minus 1 to allow for outer
nail deletion
+ // (browsers actually tend to adjust range
offsets automatically
+ // for previous sibling deletion, but just in
case ...)
+ Array.prototype.indexOf.call(
+ linkNode.parentNode.childNodes,
+ linkNode
+ ) - 1
+ );
+ // Remove the outer nails, then the link itself
+ linkNode.parentNode.removeChild(
linkNode.previousSibling );
+ linkNode.parentNode.removeChild( linkNode.nextSibling );
+ linkNode.parentNode.removeChild( linkNode );
+
+ surface.nativeSelection.removeAllRanges();
+ surface.nativeSelection.addRange( range );
+ surface.updateActiveLink();
+ e.preventDefault();
+ return;
+ }
+
+ // If the native action would delete an inside nail, move *two*
cursor positions
+ // in the deletion direction, to get outside the link just past
the outside nail,
+ // then preventDefault
+ if (
+ direction > 0 ?
+ skipNode.classList.contains( 've-ce-nail-pre-close' ) :
+ skipNode.classList.contains( 've-ce-nail-post-open' )
+ ) {
+ position = ve.adjacentDomPosition( position, direction
);
+ range = document.createRange();
+ range.setStart( position.node, position.offset );
+ surface.nativeSelection.removeAllRanges();
+ surface.nativeSelection.addRange( range );
+ surface.updateActiveLink();
+ e.preventDefault();
+ return;
+ }
+
+ offset = rangeToRemove.start;
+ if ( !e.ctrlKey && (
+ ( direction < 0 && !data.isElementData( offset - 1 ) )
||
+ ( direction > 0 && !data.isElementData( offset ) )
+ ) ) {
+ surface.eventSequencer.afterOne( {
+ keydown: surface.surfaceObserver.pollOnce.bind(
surface.surfaceObserver )
+ } );
+ return;
+ }
+ }
+
+ // Else range is uncollapsed or is adjacent to a non-nail element.
+ if ( rangeToRemove.isCollapsed() ) {
+ // Expand rangeToRemove
+ rangeToRemove = documentModel.getRelativeRange( rangeToRemove,
direction, unit, true );
+ tableEditingRange = surface.getActiveTableNode() ?
surface.getActiveTableNode().getEditingRange() : null;
+ if ( tableEditingRange && !tableEditingRange.containsRange(
rangeToRemove ) ) {
+ e.preventDefault();
+ return;
+ }
+
+ // Prevent backspacing/deleting over table cells, select the
cell instead
+ documentModelSelectedNodes = documentModel.selectNodes(
rangeToRemove, 'siblings' );
+ for ( i = 0; i < documentModelSelectedNodes.length; i++ ) {
+ node = documentModelSelectedNodes[ i ].node;
+ nodeOuterRange = documentModelSelectedNodes[ i
].nodeOuterRange;
+ if ( node instanceof ve.dm.TableNode ) {
+ if ( rangeToRemove.containsOffset(
nodeOuterRange.start ) ) {
+ surface.getModel().setSelection( new
ve.dm.TableSelection(
+ documentModel, nodeOuterRange,
0, 0
+ ) );
+ } else {
+ matrix = node.getMatrix();
+ surface.getModel().setSelection( new
ve.dm.TableSelection(
+ documentModel, nodeOuterRange,
matrix.getColCount() - 1, matrix.getRowCount() - 1
+ ) );
+ }
+ e.preventDefault();
+ return;
+ }
+ }
+
+ offset = rangeToRemove.start;
+ docLength = data.getLength();
+ if ( offset < docLength ) {
+ while ( offset < docLength && data.isCloseElementData(
offset ) ) {
+ offset++;
+ }
+ // If the user tries to delete a focusable node from a
collapsed selection,
+ // just select the node and cancel the deletion.
+ startNode =
documentModel.getDocumentNode().getNodeFromOffset( offset + 1 );
+ if ( startNode.isFocusable() ) {
+ surface.getModel().setLinearSelection(
startNode.getOuterRange() );
+ e.preventDefault();
+ return;
+ }
+ }
+ if ( rangeToRemove.isCollapsed() ) {
+ // For instance beginning or end of the document.
+ e.preventDefault();
+ return;
+ }
+ }
+
+ surface.getModel().getLinearFragment( rangeToRemove, true ).delete(
direction ).select();
+ // Rerender selection even if it didn't change
+ // TODO: is any of this necessary?
+ surface.focus();
+ surface.surfaceObserver.clear();
+ e.preventDefault();
+ return;
+};
+
+/* Registration */
+
+ve.ce.keyDownHandlerFactory.register( ve.ce.LinearDeleteKeyDownHandler );
diff --git a/src/ce/keydownhandlers/ve.ce.LinearEnterKeyDownHandler.js
b/src/ce/keydownhandlers/ve.ce.LinearEnterKeyDownHandler.js
new file mode 100644
index 0000000..e4bf9a6
--- /dev/null
+++ b/src/ce/keydownhandlers/ve.ce.LinearEnterKeyDownHandler.js
@@ -0,0 +1,225 @@
+/*!
+ * VisualEditor ContentEditable linear enter key down handler
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see
http://ve.mit-license.org
+ */
+
+/**
+ * Enter key down handler for linear selections.
+ *
+ * @class
+ * @extends ve.ce.KeyDownHandler
+ *
+ * @constructor
+ */
+ve.ce.LinearEnterKeyDownHandler = function VeCeLinearEnterKeyDownHandler() {
+ // Parent constructor
+ ve.ui.LinearEnterKeyDownHandler.super.apply( this, arguments );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ce.LinearEnterKeyDownHandler, ve.ce.KeyDownHandler );
+
+/* Static properties */
+
+ve.ce.LinearEnterKeyDownHandler.static.name = 'linearEnter';
+
+ve.ce.LinearEnterKeyDownHandler.static.keys = [ OO.ui.Keys.ENTER ];
+
+ve.ce.LinearEnterKeyDownHandler.static.supportedSelections = [ 'linear' ];
+
+/* Static methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ce.LinearEnterKeyDownHandler.static.execute = function ( surface, e ) {
+ var txRemove, txInsert, outerParent, outerChildrenCount, list,
prevContentOffset,
+ insertEmptyParagraph, node, focusedNode,
+ range = surface.model.getSelection().getRange(),
+ cursor = range.from,
+ documentModel = surface.model.getDocument(),
+ emptyParagraph = [ { type: 'paragraph' }, { type: '/paragraph'
} ],
+ advanceCursor = true,
+ stack = [],
+ outermostNode = null,
+ nodeModel = null,
+ nodeModelRange = null;
+
+ e.preventDefault();
+
+ focusedNode = surface.getFocusedNode();
+ if ( focusedNode ) {
+ focusedNode.executeCommand();
+ return;
+ }
+
+ // Handle removal first
+ if ( !range.isCollapsed() ) {
+ txRemove = ve.dm.Transaction.newFromRemoval( documentModel,
range );
+ range = txRemove.translateRange( range );
+ // We do want this to propagate to the surface
+ surface.model.change( txRemove, new ve.dm.LinearSelection(
documentModel, range ) );
+ }
+
+ node = surface.documentView.getBranchNodeFromOffset( range.from );
+ if ( node !== null ) {
+ // assertion: node is certainly a contentBranchNode
+ nodeModel = node.getModel();
+ nodeModelRange = nodeModel.getRange();
+ }
+
+ // Handle insertion
+ if ( node === null ) {
+ throw new Error( 'node === null' );
+ } else if (
+ nodeModel.getType() !== 'paragraph' &&
+ (
+ cursor === nodeModelRange.from ||
+ cursor === nodeModelRange.to
+ )
+ ) {
+ // If we're at the start/end of something that's not a
paragraph, insert a paragraph
+ // before/after. Insert after for empty nodes (from === to).
+ if ( cursor === nodeModelRange.to ) {
+ txInsert = ve.dm.Transaction.newFromInsertion(
+ documentModel, nodeModel.getOuterRange().to,
emptyParagraph
+ );
+ } else if ( cursor === nodeModelRange.from ) {
+ txInsert = ve.dm.Transaction.newFromInsertion(
+ documentModel, nodeModel.getOuterRange().from,
emptyParagraph
+ );
+ advanceCursor = false;
+ }
+ } else if ( e.shiftKey && nodeModel.hasSignificantWhitespace() ) {
+ // Insert newline
+ txInsert = ve.dm.Transaction.newFromInsertion( documentModel,
range.from, '\n' );
+ } else if ( !node.splitOnEnter() ) {
+ // Cannot split, so insert some appropriate node
+
+ insertEmptyParagraph = false;
+ if ( documentModel.hasSlugAtOffset( range.from ) ) {
+ insertEmptyParagraph = true;
+ } else {
+ prevContentOffset =
documentModel.data.getNearestContentOffset(
+ cursor,
+ -1
+ );
+ if ( prevContentOffset === -1 ) {
+ insertEmptyParagraph = true;
+ }
+ }
+
+ if ( insertEmptyParagraph ) {
+ txInsert = ve.dm.Transaction.newFromInsertion(
+ documentModel, cursor, emptyParagraph
+ );
+ } else {
+ // Act as if cursor were at previous content offset
+ cursor = prevContentOffset;
+ node = surface.documentView.getBranchNodeFromOffset(
cursor );
+ txInsert = undefined;
+ // Continue to traverseUpstream below. That will
succeed because all
+ // ContentBranchNodes have splitOnEnter === true.
+ }
+ insertEmptyParagraph = undefined;
+ }
+
+ // Assertion: if txInsert === undefined then node.splitOnEnter() ===
true
+
+ if ( txInsert === undefined ) {
+ // This node has splitOnEnter = true. Traverse upstream until
the first node
+ // that has splitOnEnter = false, splitting each node as it is
reached. Set
+ // outermostNode to the last splittable node.
+
+ node.traverseUpstream( function ( node ) {
+ if ( !node.splitOnEnter() ) {
+ return false;
+ }
+ stack.splice(
+ stack.length / 2,
+ 0,
+ { type: '/' + node.type },
+ node.getModel().getClonedElement()
+ );
+ outermostNode = node;
+ if ( e.shiftKey ) {
+ return false;
+ } else {
+ return true;
+ }
+ } );
+
+ outerParent = outermostNode.getModel().getParent();
+ outerChildrenCount = outerParent.getChildren().length;
+
+ if (
+ // This is a list item
+ outermostNode.type === 'listItem' &&
+ // This is the last list item
+ outerParent.getChildren()[ outerChildrenCount - 1 ] ===
outermostNode.getModel() &&
+ // There is one child
+ outermostNode.children.length === 1 &&
+ // The child is empty
+ node.getModel().length === 0
+ ) {
+ // Enter was pressed in an empty list item.
+ list = outermostNode.getModel().getParent();
+ if ( list.getChildren().length === 1 ) {
+ // The list item we're about to remove is the
only child of the list
+ // Remove the list
+ txInsert = ve.dm.Transaction.newFromRemoval(
+ documentModel, list.getOuterRange()
+ );
+ } else {
+ // Remove the list item
+ txInsert = ve.dm.Transaction.newFromRemoval(
+ documentModel,
outermostNode.getModel().getOuterRange()
+ );
+ surface.model.change( txInsert );
+ range = txInsert.translateRange( range );
+ // Insert a paragraph
+ txInsert = ve.dm.Transaction.newFromInsertion(
+ documentModel, list.getOuterRange().to,
emptyParagraph
+ );
+ }
+ advanceCursor = false;
+ } else {
+ // We must process the transaction first because
getRelativeContentOffset can't help us yet
+ txInsert = ve.dm.Transaction.newFromInsertion(
documentModel, range.from, stack );
+ }
+ }
+
+ // Commit the transaction
+ surface.model.change( txInsert );
+ range = txInsert.translateRange( range );
+
+ // Now we can move the cursor forward
+ if ( advanceCursor ) {
+ cursor = documentModel.data.getRelativeContentOffset(
range.from, 1 );
+ } else {
+ cursor = documentModel.data.getNearestContentOffset( range.from
);
+ }
+ if ( cursor === -1 ) {
+ // Cursor couldn't be placed in a nearby content node, so
create an empty paragraph
+ surface.model.change(
+ ve.dm.Transaction.newFromInsertion(
+ documentModel, range.from, emptyParagraph
+ )
+ );
+ surface.model.setLinearSelection( new ve.Range( range.from + 1
) );
+ } else {
+ surface.model.setLinearSelection( new ve.Range( cursor ) );
+ }
+ // Reset and resume polling
+ surface.surfaceObserver.clear();
+ // TODO: This setTimeout appears to be unnecessary (we're not
render-locked)
+ setTimeout( function () {
+ surface.checkSequences();
+ } );
+};
+
+/* Registration */
+
+ve.ce.keyDownHandlerFactory.register( ve.ce.LinearEnterKeyDownHandler );
diff --git a/src/ce/keydownhandlers/ve.ce.LinearEscapeKeyDownHandler.js
b/src/ce/keydownhandlers/ve.ce.LinearEscapeKeyDownHandler.js
new file mode 100644
index 0000000..32d2a67
--- /dev/null
+++ b/src/ce/keydownhandlers/ve.ce.LinearEscapeKeyDownHandler.js
@@ -0,0 +1,50 @@
+/*!
+ * VisualEditor ContentEditable linear escape key down handler
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see
http://ve.mit-license.org
+ */
+
+/**
+ * Escape key down handler for linear selections.
+ *
+ * @class
+ * @extends ve.ce.KeyDownHandler
+ *
+ * @constructor
+ */
+ve.ce.LinearEscapeKeyDownHandler = function VeCeLinearEscapeKeyDownHandler() {
+ // Parent constructor
+ ve.ui.LinearEscapeKeyDownHandler.super.apply( this, arguments );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ce.LinearEscapeKeyDownHandler, ve.ce.KeyDownHandler );
+
+/* Static properties */
+
+ve.ce.LinearEscapeKeyDownHandler.static.name = 'linearEscape';
+
+ve.ce.LinearEscapeKeyDownHandler.static.keys = [ OO.ui.Keys.ESCAPE ];
+
+ve.ce.LinearEscapeKeyDownHandler.static.supportedSelections = [ 'linear' ];
+
+/* Static methods */
+
+/**
+ * @inheritdoc
+ *
+ * Handle escape key down events with a linear selection while table editing.
+ */
+ve.ce.LinearEscapeKeyDownHandler.static.execute = function ( surface, e ) {
+ var tableNode = surface.getActiveTableNode();
+ if ( tableNode ) {
+ e.preventDefault();
+ e.stopPropagation();
+ tableNode.setEditing( false );
+ }
+};
+
+/* Registration */
+
+ve.ce.keyDownHandlerFactory.register( ve.ce.LinearEscapeKeyDownHandler );
diff --git a/src/ce/keydownhandlers/ve.ce.TableArrowKeyDownHandler.js
b/src/ce/keydownhandlers/ve.ce.TableArrowKeyDownHandler.js
new file mode 100644
index 0000000..f4dd1ff
--- /dev/null
+++ b/src/ce/keydownhandlers/ve.ce.TableArrowKeyDownHandler.js
@@ -0,0 +1,98 @@
+/*!
+ * VisualEditor ContentEditable table arrow key down handler
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see
http://ve.mit-license.org
+ */
+
+/**
+ * Arrow key down handler for table selections.
+ *
+ * @class
+ * @extends ve.ce.KeyDownHandler
+ *
+ * @constructor
+ */
+ve.ce.TableArrowKeyDownHandler = function VeCeTableArrowKeyDownHandler() {
+ // Parent constructor
+ ve.ui.TableArrowKeyDownHandler.super.apply( this, arguments );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ce.TableArrowKeyDownHandler, ve.ce.KeyDownHandler );
+
+/* Static properties */
+
+ve.ce.TableArrowKeyDownHandler.static.name = 'tableArrow';
+
+ve.ce.TableArrowKeyDownHandler.static.keys = [
+ OO.ui.Keys.UP, OO.ui.Keys.DOWN, OO.ui.Keys.LEFT, OO.ui.Keys.RIGHT,
+ OO.ui.Keys.END, OO.ui.Keys.HOME, OO.ui.Keys.PAGEUP, OO.ui.Keys.PAGEDOWN
+];
+
+ve.ce.TableArrowKeyDownHandler.static.supportedSelections = [ 'table' ];
+
+/* Static methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ce.TableArrowKeyDownHandler.static.execute = function ( surface, e ) {
+ var tableNode, newSelection,
+ checkDir = false,
+ selection = surface.getModel().getSelection(),
+ colOffset = 0,
+ rowOffset = 0;
+
+ switch ( e.keyCode ) {
+ case OO.ui.Keys.LEFT:
+ colOffset = -1;
+ checkDir = true;
+ break;
+ case OO.ui.Keys.RIGHT:
+ colOffset = 1;
+ checkDir = true;
+ break;
+ case OO.ui.Keys.UP:
+ rowOffset = -1;
+ break;
+ case OO.ui.Keys.DOWN:
+ rowOffset = 1;
+ break;
+ case OO.ui.Keys.HOME:
+ colOffset = -Infinity;
+ break;
+ case OO.ui.Keys.END:
+ colOffset = Infinity;
+ break;
+ case OO.ui.Keys.PAGEUP:
+ rowOffset = -Infinity;
+ break;
+ case OO.ui.Keys.PAGEDOWN:
+ rowOffset = Infinity;
+ break;
+ }
+
+ e.preventDefault();
+
+ if ( colOffset && checkDir ) {
+ tableNode = surface.documentView.getBranchNodeFromOffset(
selection.tableRange.start + 1 );
+ if ( tableNode.$element.css( 'direction' ) !== 'ltr' ) {
+ colOffset *= -1;
+ }
+ }
+ if ( !e.shiftKey && !selection.isSingleCell() ) {
+ selection = selection.collapseToFrom();
+ }
+ newSelection = selection.newFromAdjustment(
+ e.shiftKey ? 0 : colOffset,
+ e.shiftKey ? 0 : rowOffset,
+ colOffset,
+ rowOffset
+ );
+ surface.getModel().setSelection( newSelection );
+};
+
+/* Registration */
+
+ve.ce.keyDownHandlerFactory.register( ve.ce.TableArrowKeyDownHandler );
diff --git a/src/ce/keydownhandlers/ve.ce.TableDeleteKeyDownHandler.js
b/src/ce/keydownhandlers/ve.ce.TableDeleteKeyDownHandler.js
new file mode 100644
index 0000000..d6a0fce
--- /dev/null
+++ b/src/ce/keydownhandlers/ve.ce.TableDeleteKeyDownHandler.js
@@ -0,0 +1,69 @@
+/*!
+ * VisualEditor ContentEditable table delete key down handler
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see
http://ve.mit-license.org
+ */
+
+/**
+ * Delete key down handler for table selections.
+ *
+ * @class
+ * @extends ve.ce.KeyDownHandler
+ *
+ * @constructor
+ */
+ve.ce.TableDeleteKeyDownHandler = function VeCeTableDeleteKeyDownHandler() {
+ // Parent constructor
+ ve.ui.TableDeleteKeyDownHandler.super.apply( this, arguments );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ce.TableDeleteKeyDownHandler, ve.ce.KeyDownHandler );
+
+/* Static properties */
+
+ve.ce.TableDeleteKeyDownHandler.static.name = 'tableDelete';
+
+ve.ce.TableDeleteKeyDownHandler.static.keys = [ OO.ui.Keys.BACKSPACE,
OO.ui.Keys.DELETE ];
+
+ve.ce.TableDeleteKeyDownHandler.static.supportedSelections = [ 'table' ];
+
+/* Static methods */
+
+/**
+ * @inheritdoc
+ *
+ * Handle delete and backspace key down events with a table selection.
+ *
+ * Performs a strip-delete removing all the cell contents but not altering the
structure.
+ */
+ve.ce.TableDeleteKeyDownHandler.static.execute = function ( surface, e ) {
+ var i, l,
+ surfaceModel = surface.getModel(),
+ fragments = [],
+ cells = surfaceModel.getSelection().getMatrixCells();
+
+ if ( e ) {
+ e.preventDefault();
+ }
+
+ for ( i = 0, l = cells.length; i < l; i++ ) {
+ if ( cells[ i ].node.isCellEditable() ) {
+ // Create auto-updating fragments from ranges
+ fragments.push( surfaceModel.getLinearFragment( cells[
i ].node.getRange(), true ) );
+ }
+ }
+
+ for ( i = 0, l = fragments.length; i < l; i++ ) {
+ // Replace contents with empty wrapper paragraphs
+ fragments[ i ].insertContent( [
+ { type: 'paragraph', internal: { generated: 'wrapper' }
},
+ { type: '/paragraph' }
+ ] );
+ }
+};
+
+/* Registration */
+
+ve.ce.keyDownHandlerFactory.register( ve.ce.TableDeleteKeyDownHandler );
diff --git a/src/ce/keydownhandlers/ve.ce.TableEnterKeyDownHandler.js
b/src/ce/keydownhandlers/ve.ce.TableEnterKeyDownHandler.js
new file mode 100644
index 0000000..27da07a
--- /dev/null
+++ b/src/ce/keydownhandlers/ve.ce.TableEnterKeyDownHandler.js
@@ -0,0 +1,47 @@
+/*!
+ * VisualEditor ContentEditable table enter key down handler
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see
http://ve.mit-license.org
+ */
+
+/**
+ * Enter key down handler for table selections.
+ *
+ * @class
+ * @extends ve.ce.KeyDownHandler
+ *
+ * @constructor
+ */
+ve.ce.TableEnterKeyDownHandler = function VeCeTableEnterKeyDownHandler() {
+ // Parent constructor
+ ve.ui.TableEnterKeyDownHandler.super.apply( this, arguments );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ce.TableEnterKeyDownHandler, ve.ce.KeyDownHandler );
+
+/* Static properties */
+
+ve.ce.TableEnterKeyDownHandler.static.name = 'tableEnter';
+
+ve.ce.TableEnterKeyDownHandler.static.keys = [ OO.ui.Keys.ENTER ];
+
+ve.ce.TableEnterKeyDownHandler.static.supportedSelections = [ 'table' ];
+
+/* Static methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ce.TableEnterKeyDownHandler.static.execute = function ( surface, e ) {
+ var selection = surface.getModel().getSelection(),
+ tableNode = surface.documentView.getBranchNodeFromOffset(
selection.tableRange.start + 1 );
+
+ e.preventDefault();
+ tableNode.setEditing( true );
+};
+
+/* Registration */
+
+ve.ce.keyDownHandlerFactory.register( ve.ce.TableEnterKeyDownHandler );
diff --git a/src/ce/ve.ce.KeyDownHandler.js b/src/ce/ve.ce.KeyDownHandler.js
new file mode 100644
index 0000000..d6b60a9
--- /dev/null
+++ b/src/ce/ve.ce.KeyDownHandler.js
@@ -0,0 +1,63 @@
+/*!
+ * VisualEditor ContentEditable key down handler
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see
http://ve.mit-license.org
+ */
+
+/**
+ * Key down handler.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ */
+ve.ce.KeyDownHandler = function VeCeKeyDownHandler() {
+};
+
+/* Inheritance */
+
+OO.initClass( ve.ce.KeyDownHandler );
+
+/* Static properties */
+
+/**
+ * Symbolic name for this handler. Must be unique.
+ *
+ * @static
+ * @property {string}
+ * @inheritable
+ */
+ve.ce.KeyDownHandler.static.name = null;
+
+/**
+ * List of keys this handler matches
+ *
+ * @static
+ * @property {string[]|null}
+ * @inheritable
+ */
+ve.ce.KeyDownHandler.static.keys = [];
+
+/**
+ * List of selections this handler matches
+ *
+ * Null means all selections are matched.
+ *
+ * @static
+ * @property {string[]|null}
+ * @inheritable
+ */
+ve.ce.KeyDownHandler.static.supportedSelections = null;
+
+/* Static methods */
+
+/**
+ * Execute the handler
+ *
+ * @abstract
+ * @method
+ * @param {ve.ce.Surface} surface Surface
+ * @param {jQuery.Event} e Key down event
+ */
+ve.ce.KeyDownHandler.static.execute = null;
diff --git a/src/ce/ve.ce.KeyDownHandlerFactory.js
b/src/ce/ve.ce.KeyDownHandlerFactory.js
new file mode 100644
index 0000000..b69ed40
--- /dev/null
+++ b/src/ce/ve.ce.KeyDownHandlerFactory.js
@@ -0,0 +1,93 @@
+/*!
+ * VisualEditor KeyDownHandlerFactory class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see
http://ve.mit-license.org
+ */
+
+/**
+ * Key down handler factory.
+ *
+ * @class
+ * @extends OO.Factory
+ * @constructor
+ */
+ve.ce.KeyDownHandlerFactory = function VeUiKeyDownHandlerFactory() {
+ // Parent constructor
+ ve.ce.KeyDownHandlerFactory.super.apply( this, arguments );
+
+ // Handlers which match all kinds and a specific type
+ this.handlerNamesByKeys = {};
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ce.KeyDownHandlerFactory, OO.Factory );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ce.KeyDownHandlerFactory.prototype.register = function ( constructor ) {
+ var i, ilen, keys, name;
+
+ // Parent method
+ ve.ce.KeyDownHandlerFactory.super.prototype.register.call( this,
constructor );
+
+ keys = constructor.static.keys;
+ name = constructor.static.name;
+
+ for ( i = 0, ilen = keys.length; i < ilen; i++ ) {
+ this.handlerNamesByKeys[ keys[ i ] ] = this.handlerNamesByKeys[
keys[ i ] ] || [];
+ this.handlerNamesByKeys[ keys[ i ] ].push( name );
+ }
+};
+
+/**
+ * Get the handler for a specific key
+ *
+ * @param {number} key Key code
+ * @param {string} selectionName Selection type nane
+ * @return {Function[]} Matched handlers
+ */
+ve.ce.KeyDownHandlerFactory.prototype.lookupHandlersForKey = function ( key,
selectionName ) {
+ var i, constructor, supportedSelections,
+ constructors = [],
+ names = this.handlerNamesByKeys[ key ] || [];
+
+ // Length is likely to be 1 or 0 so don't cache
+ for ( i = 0; i < names.length; i++ ) {
+ constructor = this.registry[ names[ i ] ];
+ supportedSelections = constructor.static.supportedSelections;
+ if ( !supportedSelections || supportedSelections.indexOf(
selectionName ) !== -1 ) {
+ constructors.push( constructor );
+ }
+ }
+
+ return constructors;
+};
+
+/**
+ * Execute the handlers for a specific key
+ *
+ * @param {number} key Key code
+ * @param {string} selectionName Selection type nane
+ * @param {ve.ce.Surface} surface Surface
+ * @param {jQuery} e Key down event
+ * @return {boolean} Some handlers were executed
+ */
+ve.ce.KeyDownHandlerFactory.prototype.executeHandlersForKey = function ( key,
selectionName, surface, e ) {
+ var i,
+ handlers = this.lookupHandlersForKey( key, selectionName );
+
+ // Length is likely to be 1 or 0 so don't cache
+ for ( i = 0; i < handlers.length; i++ ) {
+ handlers[ i ].static.execute( surface, e );
+ }
+
+ return !!handlers.length;
+};
+
+/* Initialization */
+
+ve.ce.keyDownHandlerFactory = new ve.ce.KeyDownHandlerFactory();
diff --git a/src/ce/ve.ce.Surface.js b/src/ce/ve.ce.Surface.js
index 783ae6e..360b792 100644
--- a/src/ce/ve.ce.Surface.js
+++ b/src/ce/ve.ce.Surface.js
@@ -1133,7 +1133,7 @@
* @param {jQuery.Event} e Key down event
*/
ve.ce.Surface.prototype.onDocumentKeyDown = function ( e ) {
- var trigger, focusedNode, executed,
+ var trigger, executed,
selection = this.getModel().getSelection(),
updateFromModel = false;
@@ -1157,73 +1157,20 @@
this.storeKeyDownState( e );
- switch ( e.keyCode ) {
- case OO.ui.Keys.LEFT:
- case OO.ui.Keys.RIGHT:
- case OO.ui.Keys.UP:
- case OO.ui.Keys.DOWN:
- if ( selection instanceof ve.dm.LinearSelection ) {
- this.handleLinearArrowKey( e );
- updateFromModel = true;
- } else if ( selection instanceof ve.dm.TableSelection )
{
- this.handleTableArrowKey( e );
- }
- break;
- case OO.ui.Keys.END:
- case OO.ui.Keys.HOME:
- case OO.ui.Keys.PAGEUP:
- case OO.ui.Keys.PAGEDOWN:
- if ( selection instanceof ve.dm.TableSelection ) {
- this.handleTableArrowKey( e );
- }
- break;
- case OO.ui.Keys.ENTER:
- e.preventDefault();
- focusedNode = this.getFocusedNode();
- if ( focusedNode ) {
- focusedNode.executeCommand();
- } else if ( selection instanceof ve.dm.LinearSelection
) {
- this.handleLinearEnter( e );
- updateFromModel = true;
- } else if ( selection instanceof ve.dm.TableSelection )
{
- this.handleTableEnter( e );
- }
- break;
- case OO.ui.Keys.DELETE:
- if ( e.shiftKey && ve.getSystemPlatform() !== 'mac' ) {
- // Shift+Del on non-Mac platforms performs
'cut', so
- // don't handle it here.
- break;
- }
- /* falls through */
- case OO.ui.Keys.BACKSPACE:
- if ( selection instanceof ve.dm.LinearSelection ) {
- if ( this.handleLinearDelete( e ) ) {
- e.preventDefault();
- }
- updateFromModel = true;
- } else if ( selection instanceof ve.dm.TableSelection )
{
+ if ( ve.ce.keyDownHandlerFactory.executeHandlersForKey( e.keyCode,
selection.getName(), this, e ) ) {
+ updateFromModel = true;
+ } else {
+ trigger = new ve.ui.Trigger( e );
+ if ( trigger.isComplete() ) {
+ executed = this.surface.execute( trigger );
+ if ( executed || this.isBlockedTrigger( trigger ) ) {
e.preventDefault();
- this.handleTableDelete( e );
+ e.stopPropagation();
+ updateFromModel = true;
}
- break;
- case OO.ui.Keys.ESCAPE:
- if ( this.getActiveTableNode() ) {
- this.handleTableEditingEscape( e );
- }
- break;
- default:
- trigger = new ve.ui.Trigger( e );
- if ( trigger.isComplete() ) {
- executed = this.surface.execute( trigger );
- if ( executed || this.isBlockedTrigger( trigger
) ) {
- e.preventDefault();
- e.stopPropagation();
- updateFromModel = true;
- }
- }
- break;
+ }
}
+
if ( !updateFromModel ) {
this.incRenderLock();
}
@@ -2948,176 +2895,6 @@
};
/**
- * Handle up or down arrow key events with a linear selection.
- *
- * @param {jQuery.Event} e Up or down key down event
- */
-ve.ce.Surface.prototype.handleLinearArrowKey = function ( e ) {
- var nativeRange, collapseNode, collapseOffset, direction,
directionality, upOrDown,
- startFocusNode, startFocusOffset,
- range = this.model.getSelection().getRange(),
- surface = this;
-
- // TODO: onDocumentKeyDown did this already
- this.surfaceObserver.stopTimerLoop();
- // TODO: onDocumentKeyDown did this already
- this.surfaceObserver.pollOnce();
-
- upOrDown = e.keyCode === OO.ui.Keys.UP || e.keyCode === OO.ui.Keys.DOWN;
-
- if ( this.focusedBlockSlug ) {
- // Block level selection, so directionality is just css
directionality
- if ( upOrDown ) {
- direction = e.keyCode === OO.ui.Keys.DOWN ? 1 : -1;
- } else {
- directionality = $( this.focusedBlockSlug ).css(
'direction' );
- /*jshint bitwise:false */
- if ( e.keyCode === OO.ui.Keys.LEFT ^ directionality ===
'rtl' ) {
- // leftarrow in ltr, or rightarrow in rtl
- direction = -1;
- } else {
- // leftarrow in rtl, or rightarrow in ltr
- direction = 1;
- }
- }
- range = this.model.getDocument().getRelativeRange(
- range,
- direction,
- 'character',
- e.shiftKey,
- this.getActiveTableNode() ?
this.getActiveTableNode().getEditingRange() : null
- );
- this.model.setLinearSelection( range );
- e.preventDefault();
- return;
- }
-
- if ( this.focusedNode ) {
- if ( upOrDown ) {
- direction = e.keyCode === OO.ui.Keys.DOWN ? 1 : -1;
- } else {
- directionality = this.getFocusedNodeDirectionality();
- /*jshint bitwise:false */
- if ( e.keyCode === OO.ui.Keys.LEFT ^ directionality ===
'rtl' ) {
- // leftarrow in ltr, or rightarrow in rtl
- direction = -1;
- } else {
- // leftarrow in rtl, or rightarrow in ltr
- direction = 1;
- }
- }
-
- if ( !this.focusedNode.isContent() ) {
- // Block focusable node: move back/forward in DM (and
DOM) and preventDefault
- range = this.model.getDocument().getRelativeRange(
- range,
- direction,
- 'character',
- e.shiftKey,
- this.getActiveTableNode() ?
this.getActiveTableNode().getEditingRange() : null
- );
- this.model.setLinearSelection( range );
- e.preventDefault();
- return;
- }
- // Else inline focusable node
-
- if ( e.shiftKey ) {
- // There is no DOM range to expand (because the
selection is faked), so
- // use "collapse to focus - observe - expand". Define
"focus" to be the
- // edge of the focusedNode in the direction of motion
(so the selection
- // always grows). This means that clicking on the
focusableNode then
- // modifying the selection will always include the node.
- if ( direction === -1 ^ range.isBackwards() ) {
- range = range.flip();
- }
- this.model.setLinearSelection( new ve.Range( range.to )
);
- } else {
- // Move to start/end of node in the model in DM (and
DOM)
- range = new ve.Range( direction === 1 ? range.end :
range.start );
- this.model.setLinearSelection( range );
- if ( !upOrDown ) {
- // un-shifted left/right: we've already moved
so preventDefault
- e.preventDefault();
- return;
- }
- // Else keep going with the cursor in the new place
- }
- }
- // Else keep DM range and DOM selection as-is
-
- if ( e.shiftKey && !this.nativeSelection.extend && range.isBackwards()
) {
- // If the browser doesn't support backwards selections, but the
dm range
- // is backwards, then use "collapse to anchor - observe -
expand".
- collapseNode = this.nativeSelection.anchorNode;
- collapseOffset = this.nativeSelection.anchorOffset;
- } else if ( e.shiftKey && !range.isCollapsed() && upOrDown ) {
- // If selection is expanded and cursoring is up/down, use
- // "collapse to focus - observe - expand" to work round quirks.
- collapseNode = this.nativeSelection.focusNode;
- collapseOffset = this.nativeSelection.focusOffset;
- }
- // Else don't collapse the selection
-
- if ( collapseNode ) {
- nativeRange = this.getElementDocument().createRange();
- nativeRange.setStart( collapseNode, collapseOffset );
- nativeRange.setEnd( collapseNode, collapseOffset );
- this.nativeSelection.removeAllRanges();
- this.nativeSelection.addRange( nativeRange );
- }
-
- startFocusNode = this.nativeSelection.focusNode;
- startFocusOffset = this.nativeSelection.focusOffset;
-
- // Re-expand (or fixup) the selection after the native action, if
necessary
- this.eventSequencer.afterOne( { keydown: function () {
- var viewNode, newRange, afterDirection;
-
- // Chrome bug lets you cursor into a multi-line
contentEditable=false with up/down...
- viewNode = $( surface.nativeSelection.focusNode ).closest(
'.ve-ce-leafNode,.ve-ce-branchNode' ).data( 'view' );
- if ( !viewNode ) {
- // Irrelevant selection (or none)
- return;
- }
-
- if ( viewNode.isFocusable() ) {
- // We've landed in a focusable node; fixup the range
- if ( upOrDown ) {
- // The intended direction is clear, even if the
cursor did not move
- // or did something completely preposterous
- afterDirection = e.keyCode === OO.ui.Keys.DOWN
? 1 : -1;
- } else {
- // Observe which way the cursor moved
- afterDirection = ve.compareDocumentOrder(
- surface.nativeSelection.focusNode,
- surface.nativeSelection.focusOffset,
- startFocusNode,
- startFocusOffset
- );
- }
- newRange = (
- afterDirection > 0 ?
- viewNode.getOuterRange() :
- viewNode.getOuterRange().flip()
- );
- } else {
- // Check where the range has moved to
- surface.surfaceObserver.pollOnceNoCallback();
- newRange = new ve.Range(
surface.surfaceObserver.getRange().to );
- }
-
- // Adjust range to use old anchor, if necessary
- if ( e.shiftKey ) {
- newRange = new ve.Range( range.from, newRange.to );
- surface.getModel().setLinearSelection( newRange );
- }
- surface.updateActiveLink();
- surface.surfaceObserver.pollOnce();
- } } );
-};
-
-/**
* Insert cursor holders, if they might be required as a cursor target
*/
ve.ce.Surface.prototype.updateCursorHolders = function () {
@@ -3173,67 +2950,6 @@
};
/**
- * Handle arrow key events with a table selection.
- *
- * @param {jQuery.Event} e Arrow key down event
- */
-ve.ce.Surface.prototype.handleTableArrowKey = function ( e ) {
- var tableNode, newSelection,
- checkDir = false,
- selection = this.getModel().getSelection(),
- colOffset = 0,
- rowOffset = 0;
-
- switch ( e.keyCode ) {
- case OO.ui.Keys.LEFT:
- colOffset = -1;
- checkDir = true;
- break;
- case OO.ui.Keys.RIGHT:
- colOffset = 1;
- checkDir = true;
- break;
- case OO.ui.Keys.UP:
- rowOffset = -1;
- break;
- case OO.ui.Keys.DOWN:
- rowOffset = 1;
- break;
- case OO.ui.Keys.HOME:
- colOffset = -Infinity;
- break;
- case OO.ui.Keys.END:
- colOffset = Infinity;
- break;
- case OO.ui.Keys.PAGEUP:
- rowOffset = -Infinity;
- break;
- case OO.ui.Keys.PAGEDOWN:
- rowOffset = Infinity;
- break;
- }
-
- e.preventDefault();
-
- if ( colOffset && checkDir ) {
- tableNode = this.documentView.getBranchNodeFromOffset(
selection.tableRange.start + 1 );
- if ( tableNode.$element.css( 'direction' ) !== 'ltr' ) {
- colOffset *= -1;
- }
- }
- if ( !e.shiftKey && !selection.isSingleCell() ) {
- selection = selection.collapseToFrom();
- }
- newSelection = selection.newFromAdjustment(
- e.shiftKey ? 0 : colOffset,
- e.shiftKey ? 0 : rowOffset,
- colOffset,
- rowOffset
- );
- this.getModel().setSelection( newSelection );
-};
-
-/**
* Handle insertion of content.
*/
ve.ce.Surface.prototype.handleInsertion = function () {
@@ -3251,7 +2967,7 @@
if ( selection instanceof ve.dm.TableSelection ) {
cellSelection = selection.collapseToFrom();
this.model.setSelection( cellSelection );
- this.handleTableDelete();
+ ve.ce.keyDownHandlerFactory.lookup( 'tableDelete'
).static.execute( this );
this.documentView.getBranchNodeFromOffset(
selection.tableRange.start + 1 ).setEditing( true );
selection = this.model.getSelection();
}
@@ -3283,431 +2999,6 @@
this.surfaceObserver.stopTimerLoop();
this.surfaceObserver.pollOnce();
}
-};
-
-/**
- * Handle enter key down events with a linear selection.
- *
- * @param {jQuery.Event} e Enter key down event
- */
-ve.ce.Surface.prototype.handleLinearEnter = function ( e ) {
- var txRemove, txInsert, outerParent, outerChildrenCount, list,
prevContentOffset,
- insertEmptyParagraph, node,
- range = this.model.getSelection().getRange(),
- cursor = range.from,
- documentModel = this.model.getDocument(),
- emptyParagraph = [ { type: 'paragraph' }, { type: '/paragraph'
} ],
- advanceCursor = true,
- stack = [],
- outermostNode = null,
- nodeModel = null,
- nodeModelRange = null,
- surface = this;
-
- // Handle removal first
- if ( !range.isCollapsed() ) {
- txRemove = ve.dm.Transaction.newFromRemoval( documentModel,
range );
- range = txRemove.translateRange( range );
- // We do want this to propagate to the surface
- this.model.change( txRemove, new ve.dm.LinearSelection(
documentModel, range ) );
- }
-
- node = this.documentView.getBranchNodeFromOffset( range.from );
- if ( node !== null ) {
- // assertion: node is certainly a contentBranchNode
- nodeModel = node.getModel();
- nodeModelRange = nodeModel.getRange();
- }
-
- // Handle insertion
- if ( node === null ) {
- throw new Error( 'node === null' );
- } else if (
- nodeModel.getType() !== 'paragraph' &&
- (
- cursor === nodeModelRange.from ||
- cursor === nodeModelRange.to
- )
- ) {
- // If we're at the start/end of something that's not a
paragraph, insert a paragraph
- // before/after. Insert after for empty nodes (from === to).
- if ( cursor === nodeModelRange.to ) {
- txInsert = ve.dm.Transaction.newFromInsertion(
- documentModel, nodeModel.getOuterRange().to,
emptyParagraph
- );
- } else if ( cursor === nodeModelRange.from ) {
- txInsert = ve.dm.Transaction.newFromInsertion(
- documentModel, nodeModel.getOuterRange().from,
emptyParagraph
- );
- advanceCursor = false;
- }
- } else if ( e.shiftKey && nodeModel.hasSignificantWhitespace() ) {
- // Insert newline
- txInsert = ve.dm.Transaction.newFromInsertion( documentModel,
range.from, '\n' );
- } else if ( !node.splitOnEnter() ) {
- // Cannot split, so insert some appropriate node
-
- insertEmptyParagraph = false;
- if ( documentModel.hasSlugAtOffset( range.from ) ) {
- insertEmptyParagraph = true;
- } else {
- prevContentOffset =
documentModel.data.getNearestContentOffset(
- cursor,
- -1
- );
- if ( prevContentOffset === -1 ) {
- insertEmptyParagraph = true;
- }
- }
-
- if ( insertEmptyParagraph ) {
- txInsert = ve.dm.Transaction.newFromInsertion(
- documentModel, cursor, emptyParagraph
- );
- } else {
- // Act as if cursor were at previous content offset
- cursor = prevContentOffset;
- node = this.documentView.getBranchNodeFromOffset(
cursor );
- txInsert = undefined;
- // Continue to traverseUpstream below. That will
succeed because all
- // ContentBranchNodes have splitOnEnter === true.
- }
- insertEmptyParagraph = undefined;
- }
-
- // Assertion: if txInsert === undefined then node.splitOnEnter() ===
true
-
- if ( txInsert === undefined ) {
- // This node has splitOnEnter = true. Traverse upstream until
the first node
- // that has splitOnEnter = false, splitting each node as it is
reached. Set
- // outermostNode to the last splittable node.
-
- node.traverseUpstream( function ( node ) {
- if ( !node.splitOnEnter() ) {
- return false;
- }
- stack.splice(
- stack.length / 2,
- 0,
- { type: '/' + node.type },
- node.getModel().getClonedElement()
- );
- outermostNode = node;
- if ( e.shiftKey ) {
- return false;
- } else {
- return true;
- }
- } );
-
- outerParent = outermostNode.getModel().getParent();
- outerChildrenCount = outerParent.getChildren().length;
-
- if (
- // This is a list item
- outermostNode.type === 'listItem' &&
- // This is the last list item
- outerParent.getChildren()[ outerChildrenCount - 1 ] ===
outermostNode.getModel() &&
- // There is one child
- outermostNode.children.length === 1 &&
- // The child is empty
- node.getModel().length === 0
- ) {
- // Enter was pressed in an empty list item.
- list = outermostNode.getModel().getParent();
- if ( list.getChildren().length === 1 ) {
- // The list item we're about to remove is the
only child of the list
- // Remove the list
- txInsert = ve.dm.Transaction.newFromRemoval(
- documentModel, list.getOuterRange()
- );
- } else {
- // Remove the list item
- txInsert = ve.dm.Transaction.newFromRemoval(
- documentModel,
outermostNode.getModel().getOuterRange()
- );
- this.model.change( txInsert );
- range = txInsert.translateRange( range );
- // Insert a paragraph
- txInsert = ve.dm.Transaction.newFromInsertion(
- documentModel, list.getOuterRange().to,
emptyParagraph
- );
- }
- advanceCursor = false;
- } else {
- // We must process the transaction first because
getRelativeContentOffset can't help us yet
- txInsert = ve.dm.Transaction.newFromInsertion(
documentModel, range.from, stack );
- }
- }
-
- // Commit the transaction
- this.model.change( txInsert );
- range = txInsert.translateRange( range );
-
- // Now we can move the cursor forward
- if ( advanceCursor ) {
- cursor = documentModel.data.getRelativeContentOffset(
range.from, 1 );
- } else {
- cursor = documentModel.data.getNearestContentOffset( range.from
);
- }
- if ( cursor === -1 ) {
- // Cursor couldn't be placed in a nearby content node, so
create an empty paragraph
- this.model.change(
- ve.dm.Transaction.newFromInsertion(
- documentModel, range.from, emptyParagraph
- )
- );
- this.model.setLinearSelection( new ve.Range( range.from + 1 ) );
- } else {
- this.model.setLinearSelection( new ve.Range( cursor ) );
- }
- // Reset and resume polling
- this.surfaceObserver.clear();
- // TODO: This setTimeout appears to be unnecessary (we're not
render-locked)
- setTimeout( function () {
- surface.checkSequences();
- } );
-};
-
-/**
- * Handle enter key down events with a table selection.
- *
- * @param {jQuery.Event} e Enter key down event
- */
-ve.ce.Surface.prototype.handleTableEnter = function ( e ) {
- var selection = this.getModel().getSelection(),
- tableNode = this.documentView.getBranchNodeFromOffset(
selection.tableRange.start + 1 );
-
- e.preventDefault();
- tableNode.setEditing( true );
-};
-
-/**
- * Handle delete and backspace key down events with a linear selection.
- *
- * The handler just schedules a poll to observe the native content removal,
unless
- * one of the following is true:
- * - The ctrlKey is down; or
- * - The selection is expanded; or
- * - We are directly adjacent to an element node in the deletion direction.
- * In these cases, it will perform the content removal itself.
- *
- * @param {jQuery.Event} e Delete key down event
- * @return {boolean} Whether the content was removed by this method
- */
-ve.ce.Surface.prototype.handleLinearDelete = function ( e ) {
- var docLength, startNode, tableEditingRange, position, skipNode,
pairNode, linkNode, range,
- documentModelSelectedNodes, i, node, nodeOuterRange, matrix,
- direction = e.keyCode === OO.ui.Keys.DELETE ? 1 : -1,
- unit = ( e.altKey === true || e.ctrlKey === true ) ? 'word' :
'character',
- offset = 0,
- rangeToRemove = this.getModel().getSelection().getRange(),
- documentModel = this.getModel().getDocument(),
- data = documentModel.data;
-
- // Use native behaviour then poll if collapsed, unless we are adjacent
to some hard tag
- // (or CTRL is down, in which case we can't reliably predict whether
the native behaviour
- // would delete far enough to remove some element)
- if ( rangeToRemove.isCollapsed() && !e.ctrlKey ) {
- position = ve.adjacentDomPosition(
- {
- node: this.nativeSelection.focusNode,
- offset: this.nativeSelection.focusOffset
- },
- direction
- );
- skipNode = position.steps[ position.steps.length - 1 ].node;
- if ( skipNode.nodeType === Node.TEXT_NODE ) {
- this.eventSequencer.afterOne( {
- keydown: this.surfaceObserver.pollOnce.bind(
this.surfaceObserver )
- } );
- return false;
- }
-
- // If the native action would delete an outside nail, move
*two* cursor positions
- // in the deletion direction, to get inside the link just past
the inside nail,
- // then preventDefault
- if (
- direction > 0 ?
- skipNode.classList.contains( 've-ce-nail-pre-open' ) :
- skipNode.classList.contains( 've-ce-nail-post-close' )
- ) {
- position = ve.adjacentDomPosition( position, direction
);
- range = document.createRange();
- range.setStart( position.node, position.offset );
- this.nativeSelection.removeAllRanges();
- this.nativeSelection.addRange( range );
- this.updateActiveLink();
- return true;
- }
-
- // If inside an empty link, delete it and preventDefault
- if (
- skipNode.classList &&
- skipNode.classList.contains(
- direction > 0 ?
- 've-ce-nail-pre-close' :
- 've-ce-nail-post-open'
- ) &&
- ( pairNode = (
- direction > 0 ?
- skipNode.previousSibling :
- skipNode.nextSibling
- ) ) &&
- pairNode.classList &&
- pairNode.classList.contains(
- direction > 0 ?
- 've-ce-nail-post-open' :
- 've-ce-nail-pre-close'
- )
- ) {
- linkNode = skipNode.parentNode;
- range = document.createRange();
- range.setStart(
- linkNode.parentNode,
- // Link's offset, minus 1 to allow for outer
nail deletion
- // (browsers actually tend to adjust range
offsets automatically
- // for previous sibling deletion, but just in
case ...)
- Array.prototype.indexOf.call(
- linkNode.parentNode.childNodes,
- linkNode
- ) - 1
- );
- // Remove the outer nails, then the link itself
- linkNode.parentNode.removeChild(
linkNode.previousSibling );
- linkNode.parentNode.removeChild( linkNode.nextSibling );
- linkNode.parentNode.removeChild( linkNode );
-
- this.nativeSelection.removeAllRanges();
- this.nativeSelection.addRange( range );
- this.updateActiveLink();
- return true;
- }
-
- // If the native action would delete an inside nail, move *two*
cursor positions
- // in the deletion direction, to get outside the link just past
the outside nail,
- // then preventDefault
- if (
- direction > 0 ?
- skipNode.classList.contains( 've-ce-nail-pre-close' ) :
- skipNode.classList.contains( 've-ce-nail-post-open' )
- ) {
- position = ve.adjacentDomPosition( position, direction
);
- range = document.createRange();
- range.setStart( position.node, position.offset );
- this.nativeSelection.removeAllRanges();
- this.nativeSelection.addRange( range );
- this.updateActiveLink();
- return true;
- }
-
- offset = rangeToRemove.start;
- if ( !e.ctrlKey && (
- ( direction < 0 && !data.isElementData( offset - 1 ) )
||
- ( direction > 0 && !data.isElementData( offset ) )
- ) ) {
- this.eventSequencer.afterOne( {
- keydown: this.surfaceObserver.pollOnce.bind(
this.surfaceObserver )
- } );
- return false;
- }
- }
-
- // Else range is uncollapsed or is adjacent to a non-nail element.
- if ( rangeToRemove.isCollapsed() ) {
- // Expand rangeToRemove
- rangeToRemove = documentModel.getRelativeRange( rangeToRemove,
direction, unit, true );
- tableEditingRange = this.getActiveTableNode() ?
this.getActiveTableNode().getEditingRange() : null;
- if ( tableEditingRange && !tableEditingRange.containsRange(
rangeToRemove ) ) {
- return true;
- }
-
- // Prevent backspacing/deleting over table cells, select the
cell instead
- documentModelSelectedNodes = documentModel.selectNodes(
rangeToRemove, 'siblings' );
- for ( i = 0; i < documentModelSelectedNodes.length; i++ ) {
- node = documentModelSelectedNodes[ i ].node;
- nodeOuterRange = documentModelSelectedNodes[ i
].nodeOuterRange;
- if ( node instanceof ve.dm.TableNode ) {
- if ( rangeToRemove.containsOffset(
nodeOuterRange.start ) ) {
- this.getModel().setSelection( new
ve.dm.TableSelection(
- documentModel, nodeOuterRange,
0, 0
- ) );
- } else {
- matrix = node.getMatrix();
- this.getModel().setSelection( new
ve.dm.TableSelection(
- documentModel, nodeOuterRange,
matrix.getColCount() - 1, matrix.getRowCount() - 1
- ) );
- }
- return true;
- }
- }
-
- offset = rangeToRemove.start;
- docLength = data.getLength();
- if ( offset < docLength ) {
- while ( offset < docLength && data.isCloseElementData(
offset ) ) {
- offset++;
- }
- // If the user tries to delete a focusable node from a
collapsed selection,
- // just select the node and cancel the deletion.
- startNode =
documentModel.getDocumentNode().getNodeFromOffset( offset + 1 );
- if ( startNode.isFocusable() ) {
- this.getModel().setLinearSelection(
startNode.getOuterRange() );
- return true;
- }
- }
- if ( rangeToRemove.isCollapsed() ) {
- // For instance beginning or end of the document.
- return true;
- }
- }
-
- this.getModel().getLinearFragment( rangeToRemove, true ).delete(
direction ).select();
- // Rerender selection even if it didn't change
- // TODO: is any of this necessary?
- this.focus();
- this.surfaceObserver.clear();
- return true;
-};
-
-/**
- * Handle delete and backspace key down events with a table selection.
- *
- * Performs a strip-delete removing all the cell contents but not altering the
structure.
- *
- * @param {jQuery.Event} e Delete key down event
- */
-ve.ce.Surface.prototype.handleTableDelete = function () {
- var i, l,
- surfaceModel = this.getModel(),
- fragments = [],
- cells = surfaceModel.getSelection().getMatrixCells();
-
- for ( i = 0, l = cells.length; i < l; i++ ) {
- if ( cells[ i ].node.isCellEditable() ) {
- // Create auto-updating fragments from ranges
- fragments.push( surfaceModel.getLinearFragment( cells[
i ].node.getRange(), true ) );
- }
- }
-
- for ( i = 0, l = fragments.length; i < l; i++ ) {
- // Replace contents with empty wrapper paragraphs
- fragments[ i ].insertContent( [
- { type: 'paragraph', internal: { generated: 'wrapper' }
},
- { type: '/paragraph' }
- ] );
- }
-};
-
-/**
- * Handle escape key down events with a linear selection while table editing.
- *
- * @param {jQuery.Event} e Delete key down event
- */
-ve.ce.Surface.prototype.handleTableEditingEscape = function ( e ) {
- e.preventDefault();
- e.stopPropagation();
- this.getActiveTableNode().setEditing( false );
};
/**
diff --git a/tests/ce/ve.ce.Surface.test.js b/tests/ce/ve.ce.Surface.test.js
index 9147bd4..602cfb5 100644
--- a/tests/ce/ve.ce.Surface.test.js
+++ b/tests/ce/ve.ce.Surface.test.js
@@ -9,14 +9,14 @@
/* Tests */
ve.test.utils.runSurfaceHandleSpecialKeyTest = function ( assert, html, range,
operations, expectedData, expectedSelection, msg ) {
- var i, method, args, selection,
+ var i, e, selection,
actions = {
- backspace: [ 'handleLinearDelete', { keyCode:
OO.ui.Keys.BACKSPACE } ],
- delete: [ 'handleLinearDelete', { keyCode:
OO.ui.Keys.DELETE } ],
- modifiedBackspace: [ 'handleLinearDelete', { keyCode:
OO.ui.Keys.BACKSPACE, ctrlKey: true } ],
- modifiedDelete: [ 'handleLinearDelete', { keyCode:
OO.ui.Keys.DELETE, ctrlKey: true } ],
- enter: [ 'handleLinearEnter', {} ],
- modifiedEnter: [ 'handleLinearEnter', { shiftKey: true
} ]
+ backspace: { keyCode: OO.ui.Keys.BACKSPACE },
+ delete: { keyCode: OO.ui.Keys.DELETE },
+ modifiedBackspace: { keyCode: OO.ui.Keys.BACKSPACE,
ctrlKey: true },
+ modifiedDelete: { keyCode: OO.ui.Keys.DELETE, ctrlKey:
true },
+ enter: { keyCode: OO.ui.Keys.ENTER },
+ modifiedEnter: { keyCode: OO.ui.Keys.ENTER, shiftKey:
true }
},
view = html ?
ve.test.utils.createSurfaceViewFromHtml( html ) :
@@ -34,9 +34,12 @@
model.setSelection( selection );
for ( i = 0; i < operations.length; i++ ) {
- method = actions[ operations[ i ] ][ 0 ];
- args = actions[ operations[ i ] ].slice( 1 );
- view[ method ].apply( view, args );
+ e = ve.extendObject( {}, {
+ preventDefault: function () {}
+ }, actions[ operations[ i ] ] );
+ ve.ce.keyDownHandlerFactory.executeHandlersForKey(
+ e.keyCode, selection.getName(), view, e
+ );
}
expectedData( data );
@@ -1507,11 +1510,14 @@
model.setSelection( new ve.dm.TableSelection(
model.getDocument(), table.tableRange, offsets[ 0 ],
offsets[ 1 ], offsets[ 2 ], offsets[ 3 ] )
);
- view.handleTableArrowKey( {
- keyCode: OO.ui.Keys[ cases[ i ].key ],
- shiftKey: !!cases[ i ].shiftKey,
- preventDefault: fn
- } );
+ ve.ce.keyDownHandlerFactory.executeHandlersForKey(
+ OO.ui.Keys[ cases[ i ].key ],
model.getSelection().getName(), view,
+ {
+ keyCode: OO.ui.Keys[ cases[ i ].key ],
+ shiftKey: !!cases[ i ].shiftKey,
+ preventDefault: fn
+ }
+ );
selection = model.getSelection();
assert.deepEqual(
[ selection.fromCol, selection.fromRow,
selection.toCol, selection.toRow ],
diff --git a/tests/index.html b/tests/index.html
index e41a8b9..3310e91 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -211,6 +211,15 @@
<script src="../src/ce/ve.ce.TableCellableNode.js"></script>
<script src="../src/ce/ve.ce.Surface.js"></script>
<script src="../src/ce/ve.ce.SurfaceObserver.js"></script>
+ <script src="../src/ce/ve.ce.KeyDownHandlerFactory.js"></script>
+ <script src="../src/ce/ve.ce.KeyDownHandler.js"></script>
+ <script
src="../src/ce/keydownhandlers/ve.ce.LinearArrowKeyDownHandler.js"></script>
+ <script
src="../src/ce/keydownhandlers/ve.ce.LinearDeleteKeyDownHandler.js"></script>
+ <script
src="../src/ce/keydownhandlers/ve.ce.LinearEnterKeyDownHandler.js"></script>
+ <script
src="../src/ce/keydownhandlers/ve.ce.LinearEscapeKeyDownHandler.js"></script>
+ <script
src="../src/ce/keydownhandlers/ve.ce.TableArrowKeyDownHandler.js"></script>
+ <script
src="../src/ce/keydownhandlers/ve.ce.TableDeleteKeyDownHandler.js"></script>
+ <script
src="../src/ce/keydownhandlers/ve.ce.TableEnterKeyDownHandler.js"></script>
<script
src="../src/ce/nodes/ve.ce.GeneratedContentNode.js"></script>
<script src="../src/ce/nodes/ve.ce.AlienNode.js"></script>
<script src="../src/ce/nodes/ve.ce.BlockquoteNode.js"></script>
--
To view, visit https://gerrit.wikimedia.org/r/248911
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: If0b1a855ac4454ad6087ec8c3f1e82e8fac618e4
Gerrit-PatchSet: 5
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Esanders <[email protected]>
Gerrit-Reviewer: Divec <[email protected]>
Gerrit-Reviewer: Esanders <[email protected]>
Gerrit-Reviewer: Jforrester <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits