/*jshint strict:false, undef:false, unused:false */ 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 { self.setSelection( range ); 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' ); }; } 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' );