mirror of
https://github.com/fastmail/Squire.git
synced 2025-01-04 22:00:09 -05:00
a3de6add71
Previously ctrl+shift+{7,8} would make an (un)ordered list but hitting the shortcut again would have no effect. This change causes the list to be removed when triggering the shortcut while inside the list.
623 lines
23 KiB
JavaScript
623 lines
23 KiB
JavaScript
/*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 <font> 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 <div> in Chrome,
|
|
// it removes the div and replaces it with just a <br> inside the
|
|
// root. Detach the <br>; 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 <br>.
|
|
if ( !block || shiftKey || /^T[HD]$/.test( block.nodeName ) ) {
|
|
// If inside an <a>, 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 <b>/<i> 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, parent;
|
|
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 (<a>foo|</a>) then move it
|
|
// outside of the link (<a>foo</a>|) 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' );
|