/*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 ( event.defaultPrevented ) { return; } if ( !key ) { key = String.fromCharCode( code ).toLowerCase(); // Only reliable for letters and numbers if ( !/^[A-Za-z0-9]$/.test( key ) ) { 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-'; } if ( event.shiftKey ) { modifiers += 'shift-'; } } // However, on Windows, shift-delete is apparently "cut" (WTF right?), so // we want to let the browser handle shift-delete in this situation. if ( isWin && event.shiftKey && key === 'delete' ) { modifiers += 'shift-'; } key = modifiers + key; if ( this._keyHandlers[ key ] ) { this._keyHandlers[ key ]( this, event, range ); // !event.isComposing stops us from blatting Kana-Kanji conversion in Safari } else if ( !range.collapsed && !event.isComposing && !event.ctrlKey && !event.metaKey && ( event.key || key ).length === 1 ) { // Record undo checkpoint. this.saveUndoState( range ); // Delete the selection deleteContentsOfRange( range, this._root ); 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 focused 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, self._root ); } fixCursor( parent, self._root ); // Move cursor into text node moveRangeBoundariesDownTree( range ); } // If you delete the last character in the sole
in Chrome, // it removes the div and replaces it with just a
inside the // root. Detach the
; the _ensureBottomLine call will insert a new // block. if ( node === self._root && ( node = node.firstChild ) && node.nodeName === 'BR' ) { detach( node ); } self._ensureBottomLine(); self.setSelection( range ); self._updatePath( range, true ); } catch ( error ) { self.didError( error ); } }; var detachUneditableNode = function ( node, root ) { var parent; while (( parent = node.parentNode )) { if ( parent === root || parent.isContentEditable ) { break; } node = parent; } detach( node ); }; var handleEnter = function ( self, shiftKey, range ) { var root = self._root; var block, parent, node, offset, nodeAfterSplit; // 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 ); if ( self._config.addLinks ) { addLinks( range.startContainer, root, self ); } self._removeZWS(); self._getRangeAndRemoveBookmark( range ); // Selected text is overwritten, therefore delete the contents // to collapse selection. if ( !range.collapsed ) { deleteContentsOfRange( range, root ); } block = getStartBlockOfRange( range, root ); // Inside a PRE, insert literal newline, unless on blank line. if ( block && ( parent = getNearest( block, root, 'PRE' ) ) ) { moveRangeBoundariesDownTree( range ); node = range.startContainer; offset = range.startOffset; if ( node.nodeType !== TEXT_NODE ) { node = self._doc.createTextNode( '' ); parent.insertBefore( node, parent.firstChild ); } // If blank line: split and insert default block if ( !shiftKey && ( node.data.charAt( offset - 1 ) === '\n' || rangeDoesStartAtBlockBoundary( range, root ) ) && ( node.data.charAt( offset ) === '\n' || rangeDoesEndAtBlockBoundary( range, root ) ) ) { node.deleteData( offset && offset - 1, offset ? 2 : 1 ); nodeAfterSplit = split( node, offset && offset - 1, root, root ); node = nodeAfterSplit.previousSibling; if ( !node.textContent ) { detach( node ); } node = self.createDefaultBlock(); nodeAfterSplit.parentNode.insertBefore( node, nodeAfterSplit ); if ( !nodeAfterSplit.textContent ) { detach( nodeAfterSplit ); } range.setStart( node, 0 ); } else { node.insertData( offset, '\n' ); fixCursor( parent, root ); // Firefox bug: if you set the selection in the text node after // the new line, it draws the cursor before the line break still // but if you set the selection to the equivalent position // in the parent, it works. if ( node.length === offset + 1 ) { range.setStartAfter( node ); } else { range.setStart( node, offset + 1 ); } } range.collapse( true ); self.setSelection( range ); self._updatePath( range, true ); self._docWasChanged(); return; } // If this is a malformed bit of document or in a table; // just play it safe and insert a
. if ( !block || shiftKey || /^T[HD]$/.test( block.nodeName ) ) { // If inside an , move focus out moveRangeBoundaryOutOf( range, 'A', root ); 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, root, 'LI' ) ) { block = parent; } if ( isEmptyBlock( block ) ) { // Break list if ( getNearest( block, root, 'UL' ) || getNearest( block, root, 'OL' ) ) { return self.decreaseListLevel( range ); } // Break blockquote else if ( getNearest( block, root, '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, root ); // 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 ) { break; } nodeAfterSplit = child; } range = self.createRange( nodeAfterSplit, 0 ); self.setSelection( range ); self._updatePath( range, true ); }; var keyHandlers = { // This song and dance is to force iOS to do enable the shift key // automatically on enter. When you do the DOM split manipulation yourself, // WebKit doesn't reset the IME state and so presents auto-complete options // as though you were continuing to type on the previous line, and doesn't // auto-enable the shift key. The old trick of blurring and focussing // again no longer works in iOS 13, and I tried various execCommand options // but they didn't seem to do anything. The only solution I've found is to // let iOS handle the enter key, then after it's done that reset the HTML // to what it was before and handle it properly in Squire; the IME state of // course doesn't reset so you end up in the correct state! enter: isIOS ? function ( self, event, range ) { self._saveRangeToBookmark( range ); var html = self._getHTML(); var restoreAndDoEnter = function () { self.removeEventListener( 'keyup', restoreAndDoEnter ); self._setHTML( html ); range = self._getRangeAndRemoveBookmark(); // Ignore the shift key on iOS, as this is for auto-capitalisation. handleEnter( self, false, range ); }; self.addEventListener( 'keyup', restoreAndDoEnter ); } : function ( self, event, range ) { event.preventDefault(); handleEnter( self, event.shiftKey, range ); }, 'shift-enter': function ( self, event, range ) { return self._keyHandlers.enter( self, event, range ); }, backspace: function ( self, event, range ) { var root = self._root; self._removeZWS(); // Record undo checkpoint. self.saveUndoState( range ); // If not collapsed, delete contents if ( !range.collapsed ) { event.preventDefault(); deleteContentsOfRange( range, root ); afterDelete( self, range ); } // If at beginning of block, merge with previous else if ( rangeDoesStartAtBlockBoundary( range, root ) ) { event.preventDefault(); var current = getStartBlockOfRange( range, root ); var previous; if ( !current ) { return; } // In case inline data has somehow got between blocks. fixContainer( current.parentNode, root ); // Now get previous block previous = getPreviousBlock( current, root ); // Must not be at the very beginning of the text area. if ( previous ) { // If not editable, just delete whole block. if ( !previous.isContentEditable ) { detachUneditableNode( previous, root ); return; } // Otherwise merge. mergeWithBlock( previous, current, range, root ); // If deleted line between containers, merge newly adjacent // containers. current = previous.parentNode; while ( current !== root && !current.nextSibling ) { current = current.parentNode; } if ( current !== root && ( current = current.nextSibling ) ) { mergeContainers( current, root ); } self.setSelection( range ); } // If at very beginning of text area, allow backspace // to break lists/blockquote. else if ( current ) { // Break list if ( getNearest( current, root, 'UL' ) || getNearest( current, root, 'OL' ) ) { return self.decreaseListLevel( range ); } // Break blockquote else if ( getNearest( current, root, '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 ) { var root = self._root; var current, next, originalRange, cursorContainer, cursorOffset, nodeAfterCursor; self._removeZWS(); // Record undo checkpoint. self.saveUndoState( range ); // If not collapsed, delete contents if ( !range.collapsed ) { event.preventDefault(); deleteContentsOfRange( range, root ); afterDelete( self, range ); } // If at end of block, merge next into this block else if ( rangeDoesEndAtBlockBoundary( range, root ) ) { event.preventDefault(); current = getStartBlockOfRange( range, root ); if ( !current ) { return; } // In case inline data has somehow got between blocks. fixContainer( current.parentNode, root ); // Now get next block next = getNextBlock( current, root ); // Must not be at the very end of the text area. if ( next ) { // If not editable, just delete whole block. if ( !next.isContentEditable ) { detachUneditableNode( next, root ); return; } // Otherwise merge. mergeWithBlock( current, next, range, root ); // If deleted line between containers, merge newly adjacent // containers. next = current.parentNode; while ( next !== root && !next.nextSibling ) { next = next.parentNode; } if ( next !== root && ( next = next.nextSibling ) ) { mergeContainers( next, root ); } 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. originalRange = range.cloneRange(); moveRangeBoundariesUpTree( range, root, root, root ); 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 root = self._root; var node, parent; self._removeZWS(); // If no selection and at start of block if ( range.collapsed && rangeDoesStartAtBlockBoundary( range, root ) ) { node = getStartBlockOfRange( range, root ); // 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' ) { // Then increase the list level event.preventDefault(); self.increaseListLevel( range ); break; } node = parent; } } }, 'shift-tab': function ( self, event, range ) { var root = self._root; var node; self._removeZWS(); // If no selection and at start of block if ( range.collapsed && rangeDoesStartAtBlockBoundary( range, root ) ) { // Break list node = range.startContainer; if ( getNearest( node, root, 'UL' ) || getNearest( node, root, 'OL' ) ) { event.preventDefault(); self.decreaseListLevel( range ); } } }, space: function ( self, _, range ) { var node; var root = self._root; self._recordUndoState( range ); if ( self._config.addLinks ) { addLinks( range.startContainer, root, self ); } 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; if ( range.collapsed && range.endOffset === getLength( node ) ) { do { if ( node.nodeName === 'A' ) { range.setStartAfter( node ); break; } } while ( !node.nextSibling && ( node = node.parentNode ) && node !== root ); } // Delete the selection if not collapsed if ( !range.collapsed ) { deleteContentsOfRange( range, root ); self._ensureBottomLine(); self.setSelection( range ); self._updatePath( range, true ); } self.setSelection( range ); }, left: function ( self ) { self._removeZWS(); }, right: function ( self ) { self._removeZWS(); } }; // Firefox pre v29 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 ) { keyHandlers[ 'meta-left' ] = function ( self, event ) { event.preventDefault(); var sel = getWindowSelection( self ); if ( sel && sel.modify ) { sel.modify( 'move', 'backward', 'lineboundary' ); } }; keyHandlers[ 'meta-right' ] = function ( self, event ) { event.preventDefault(); var sel = getWindowSelection( self ); if ( sel && sel.modify ) { 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(); }; } const changeIndentationLevel = function ( methodIfInQuote, methodIfInList ) { return function ( self, event ) { event.preventDefault(); var path = self.getPath(); if ( /(?:^|>)BLOCKQUOTE/.test( path ) || !/(?:^|>)[OU]L/.test( path ) ) { self[ methodIfInQuote ](); } else { self[ methodIfInList ](); } }; }; const toggleList = function ( listRegex, methodIfNotInList ) { return function ( self, event ) { event.preventDefault(); var path = self.getPath(); if ( !listRegex.test( path ) ) { self[ methodIfNotInList ](); } else { self.removeList(); } }; }; 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' ] = toggleList( /(?:^|>)UL/, 'makeUnorderedList' ); keyHandlers[ ctrlKey + 'shift-9' ] = toggleList( /(?:^|>)OL/, 'makeOrderedList' ); keyHandlers[ ctrlKey + '[' ] = changeIndentationLevel( 'decreaseQuoteLevel', 'decreaseListLevel' ); keyHandlers[ ctrlKey + ']' ] = changeIndentationLevel( 'increaseQuoteLevel', 'increaseListLevel' ); keyHandlers[ ctrlKey + 'd' ] = mapKeyTo( 'toggleCode' ); keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' ); keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' ); keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' );