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

Reply via email to