/*jshint strict:false, undef:false, unused:false */ var keys = { 8: 'backspace', 9: 'tab', 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 37: 'left', 39: 'right', 46: 'delete', 219: '[', 221: ']' }; // Ref: http://unixpapa.com/js/key.html var onKey = function ( event ) { var code = event.keyCode, key = keys[ code ], modifiers = '', range = this.getSelection(); if ( !key ) { key = String.fromCharCode( code ).toLowerCase(); // Only reliable for letters and numbers if ( !/^[A-Za-z0-9]$/.test( key ) ) { key = ''; } } // On keypress, delete and '.' both have event.keyCode 46 // Must check event.which to differentiate. if ( isPresto && event.which === 46 ) { key = '.'; } // Function keys if ( 111 < code && code < 124 ) { key = 'f' + ( code - 111 ); } // We need to apply the backspace/delete handlers regardless of // control key modifiers. if ( key !== 'backspace' && key !== 'delete' ) { if ( event.altKey ) { modifiers += 'alt-'; } if ( event.ctrlKey ) { modifiers += 'ctrl-'; } if ( event.metaKey ) { modifiers += 'meta-'; } } // However, on Windows, shift-delete is apparently "cut" (WTF right?), so // we want to let the browser handle shift-delete. if ( event.shiftKey ) { modifiers += 'shift-'; } key = modifiers + key; if ( this._keyHandlers[ key ] ) { this._keyHandlers[ key ]( this, event, range ); } else if ( key.length === 1 && !range.collapsed ) { // Record undo checkpoint. this._recordUndoState( range ); this._getRangeAndRemoveBookmark( range ); // Delete the selection deleteContentsOfRange( range ); this._ensureBottomLine(); this.setSelection( range ); this._updatePath( range, true ); } }; var mapKeyTo = function ( method ) { return function ( self, event ) { event.preventDefault(); self[ method ](); }; }; var mapKeyToFormat = function ( tag, remove ) { remove = remove || null; return function ( self, event ) { event.preventDefault(); var range = self.getSelection(); if ( self.hasFormat( tag, null, range ) ) { self.changeFormat( null, { tag: tag }, range ); } else { self.changeFormat( { tag: tag }, remove, range ); } }; }; // If you delete the content inside a span with a font styling, Webkit will // replace it with a tag (!). If you delete all the text inside a // link in Opera, it won't delete the link. Let's make things consistent. If // you delete all text inside an inline tag, remove the inline tag. var afterDelete = function ( self, range ) { try { if ( !range ) { range = self.getSelection(); } var node = range.startContainer, parent; // Climb the tree from the focus point while we are inside an empty // inline element if ( node.nodeType === TEXT_NODE ) { node = node.parentNode; } parent = node; while ( isInline( parent ) && ( !parent.textContent || parent.textContent === ZWS ) ) { node = parent; parent = node.parentNode; } // If focussed in empty inline element if ( node !== parent ) { // Move focus to just before empty inline(s) range.setStart( parent, indexOf.call( parent.childNodes, node ) ); range.collapse( true ); // Remove empty inline(s) parent.removeChild( node ); // Fix cursor in block if ( !isBlock( parent ) ) { parent = getPreviousBlock( parent ); } fixCursor( parent ); // Move cursor into text node moveRangeBoundariesDownTree( range ); } self._ensureBottomLine(); self.setSelection( range ); self._updatePath( range, true ); } catch ( error ) { self.didError( error ); } }; var keyHandlers = { enter: function ( self, event, range ) { var block, parent, nodeAfterSplit; // We handle this ourselves event.preventDefault(); // Save undo checkpoint and add any links in the preceding section. // Remove any zws so we don't think there's content in an empty // block. self._recordUndoState( range ); addLinks( range.startContainer ); self._removeZWS(); self._getRangeAndRemoveBookmark( range ); // Selected text is overwritten, therefore delete the contents // to collapse selection. if ( !range.collapsed ) { deleteContentsOfRange( range ); } block = getStartBlockOfRange( range ); // If this is a malformed bit of document or in a table; // just play it safe and insert a
. if ( !block || /^T[HD]$/.test( block.nodeName ) ) { insertNodeInRange( range, self.createElement( 'BR' ) ); range.collapse( false ); self.setSelection( range ); self._updatePath( range, true ); return; } // If in a list, we'll split the LI instead. if ( parent = getNearest( block, 'LI' ) ) { block = parent; } if ( !block.textContent ) { // Break list if ( getNearest( block, 'UL' ) || getNearest( block, 'OL' ) ) { return self.modifyBlocks( decreaseListLevel, range ); } // Break blockquote else if ( getNearest( block, 'BLOCKQUOTE' ) ) { return self.modifyBlocks( removeBlockQuote, range ); } } // Otherwise, split at cursor point. nodeAfterSplit = splitBlock( self, block, range.startContainer, range.startOffset ); // Clean up any empty inlines if we hit enter at the beginning of the // block removeZWS( block ); removeEmptyInlines( block ); fixCursor( block ); // Focus cursor // If there's a / etc. at the beginning of the split // make sure we focus inside it. while ( nodeAfterSplit.nodeType === ELEMENT_NODE ) { var child = nodeAfterSplit.firstChild, next; // Don't continue links over a block break; unlikely to be the // desired outcome. if ( nodeAfterSplit.nodeName === 'A' && ( !nodeAfterSplit.textContent || nodeAfterSplit.textContent === ZWS ) ) { child = self._doc.createTextNode( '' ); replaceWith( nodeAfterSplit, child ); nodeAfterSplit = child; break; } while ( child && child.nodeType === TEXT_NODE && !child.data ) { next = child.nextSibling; if ( !next || next.nodeName === 'BR' ) { break; } detach( child ); child = next; } // 'BR's essentially don't count; they're a browser hack. // If you try to select the contents of a 'BR', FF will not let // you type anything! if ( !child || child.nodeName === 'BR' || ( child.nodeType === TEXT_NODE && !isPresto ) ) { break; } nodeAfterSplit = child; } range = self._createRange( nodeAfterSplit, 0 ); self.setSelection( range ); self._updatePath( range, true ); // Scroll into view if ( nodeAfterSplit.nodeType === TEXT_NODE ) { nodeAfterSplit = nodeAfterSplit.parentNode; } var doc = self._doc, body = self._body; if ( nodeAfterSplit.offsetTop + nodeAfterSplit.offsetHeight > ( doc.documentElement.scrollTop || body.scrollTop ) + body.offsetHeight ) { nodeAfterSplit.scrollIntoView( false ); } }, backspace: function ( self, event, range ) { self._removeZWS(); // Record undo checkpoint. self._recordUndoState( range ); self._getRangeAndRemoveBookmark( range ); // If not collapsed, delete contents if ( !range.collapsed ) { event.preventDefault(); deleteContentsOfRange( range ); afterDelete( self, range ); } // If at beginning of block, merge with previous else if ( rangeDoesStartAtBlockBoundary( range ) ) { event.preventDefault(); var current = getStartBlockOfRange( range ), previous = current && getPreviousBlock( current ); // Must not be at the very beginning of the text area. if ( previous ) { // If not editable, just delete whole block. if ( !previous.isContentEditable ) { detach( previous ); return; } // Otherwise merge. mergeWithBlock( previous, current, range ); // If deleted line between containers, merge newly adjacent // containers. current = previous.parentNode; while ( current && !current.nextSibling ) { current = current.parentNode; } if ( current && ( current = current.nextSibling ) ) { mergeContainers( current ); } self.setSelection( range ); } // If at very beginning of text area, allow backspace // to break lists/blockquote. else if ( current ) { // Break list if ( getNearest( current, 'UL' ) || getNearest( current, 'OL' ) ) { return self.modifyBlocks( decreaseListLevel, range ); } // Break blockquote else if ( getNearest( current, 'BLOCKQUOTE' ) ) { return self.modifyBlocks( decreaseBlockQuoteLevel, range ); } self.setSelection( range ); self._updatePath( range, true ); } } // Otherwise, leave to browser but check afterwards whether it has // left behind an empty inline tag. else { self.setSelection( range ); setTimeout( function () { afterDelete( self ); }, 0 ); } }, 'delete': function ( self, event, range ) { self._removeZWS(); // Record undo checkpoint. self._recordUndoState( range ); self._getRangeAndRemoveBookmark( range ); // If not collapsed, delete contents if ( !range.collapsed ) { event.preventDefault(); deleteContentsOfRange( range ); afterDelete( self, range ); } // If at end of block, merge next into this block else if ( rangeDoesEndAtBlockBoundary( range ) ) { event.preventDefault(); var current = getStartBlockOfRange( range ), next = current && getNextBlock( current ); // Must not be at the very end of the text area. if ( next ) { // If not editable, just delete whole block. if ( !next.isContentEditable ) { detach( next ); return; } // Otherwise merge. mergeWithBlock( current, next, range ); // If deleted line between containers, merge newly adjacent // containers. next = current.parentNode; while ( next && !next.nextSibling ) { next = next.parentNode; } if ( next && ( next = next.nextSibling ) ) { mergeContainers( next ); } self.setSelection( range ); self._updatePath( range, true ); } } // Otherwise, leave to browser but check afterwards whether it has // left behind an empty inline tag. else { // But first check if the cursor is just before an IMG tag. If so, // delete it ourselves, because the browser won't if it is not // inline. var originalRange = range.cloneRange(), cursorContainer, cursorOffset, nodeAfterCursor; moveRangeBoundariesUpTree( range, self._body ); cursorContainer = range.endContainer; cursorOffset = range.endOffset; if ( cursorContainer.nodeType === ELEMENT_NODE ) { nodeAfterCursor = cursorContainer.childNodes[ cursorOffset ]; if ( nodeAfterCursor && nodeAfterCursor.nodeName === 'IMG' ) { event.preventDefault(); detach( nodeAfterCursor ); moveRangeBoundariesDownTree( range ); afterDelete( self, range ); return; } } self.setSelection( originalRange ); setTimeout( function () { afterDelete( self ); }, 0 ); } }, tab: function ( self, event, range ) { var node, parent; self._removeZWS(); // If no selection and in an empty block if ( range.collapsed && rangeDoesStartAtBlockBoundary( range ) && rangeDoesEndAtBlockBoundary( range ) ) { node = getStartBlockOfRange( range ); // Iterate through the block's parents while ( parent = node.parentNode ) { // If we find a UL or OL (so are in a list, node must be an LI) if ( parent.nodeName === 'UL' || parent.nodeName === 'OL' ) { // AND the LI is not the first in the list if ( node.previousSibling ) { // Then increase the list level event.preventDefault(); self.modifyBlocks( increaseListLevel, range ); } break; } node = parent; } event.preventDefault(); } }, space: function ( self, _, range ) { var node, parent; self._recordUndoState( range ); addLinks( range.startContainer ); self._getRangeAndRemoveBookmark( range ); // If the cursor is at the end of a link (foo|) then move it // outside of the link (foo|) so that the space is not part of // the link text. node = range.endContainer; parent = node.parentNode; if ( range.collapsed && parent.nodeName === 'A' && !node.nextSibling && range.endOffset === getLength( node ) ) { range.setStartAfter( parent ); } self.setSelection( range ); }, left: function ( self ) { self._removeZWS(); }, right: function ( self ) { self._removeZWS(); } }; // Firefox incorrectly handles Cmd-left/Cmd-right on Mac: // it goes back/forward in history! Override to do the right // thing. // https://bugzilla.mozilla.org/show_bug.cgi?id=289384 if ( isMac && isGecko && win.getSelection().modify ) { keyHandlers[ 'meta-left' ] = function ( self, event ) { event.preventDefault(); self._sel.modify( 'move', 'backward', 'lineboundary' ); }; keyHandlers[ 'meta-right' ] = function ( self, event ) { event.preventDefault(); self._sel.modify( 'move', 'forward', 'lineboundary' ); }; } // System standard for page up/down on Mac is to just scroll, not move the // cursor. On Linux/Windows, it should move the cursor, but some browsers don't // implement this natively. Override to support it. if ( !isMac ) { keyHandlers.pageup = function ( self ) { self.moveCursorToStart(); }; keyHandlers.pagedown = function ( self ) { self.moveCursorToEnd(); }; } keyHandlers[ ctrlKey + 'b' ] = mapKeyToFormat( 'B' ); keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat( 'I' ); keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' ); keyHandlers[ ctrlKey + 'shift-7' ] = mapKeyToFormat( 'S' ); keyHandlers[ ctrlKey + 'shift-5' ] = mapKeyToFormat( 'SUB', { tag: 'SUP' } ); keyHandlers[ ctrlKey + 'shift-6' ] = mapKeyToFormat( 'SUP', { tag: 'SUB' } ); keyHandlers[ ctrlKey + 'shift-8' ] = mapKeyTo( 'makeUnorderedList' ); keyHandlers[ ctrlKey + 'shift-9' ] = mapKeyTo( 'makeOrderedList' ); keyHandlers[ ctrlKey + '[' ] = mapKeyTo( 'decreaseQuoteLevel' ); keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' ); keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' ); keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' ); keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' );