mirror of
https://github.com/fastmail/Squire.git
synced 2025-01-18 04:32:28 -05:00
parent
27267ff376
commit
223060ecf9
7 changed files with 774 additions and 725 deletions
2
Makefile
2
Makefile
|
@ -10,7 +10,7 @@ clean:
|
||||||
|
|
||||||
build: build/squire.js build/document.html
|
build: build/squire.js build/document.html
|
||||||
|
|
||||||
build/squire-raw.js: source/intro.js source/Constants.js source/TreeWalker.js source/Node.js source/Range.js source/Editor.js source/outro.js
|
build/squire-raw.js: source/intro.js source/Constants.js source/TreeWalker.js source/Node.js source/Range.js source/KeyHandlers.js source/Editor.js source/outro.js
|
||||||
mkdir -p $(@D)
|
mkdir -p $(@D)
|
||||||
cat $^ | grep -v '^\/\*jshint' >$@
|
cat $^ | grep -v '^\/\*jshint' >$@
|
||||||
|
|
||||||
|
|
14
README.md
14
README.md
|
@ -94,6 +94,20 @@ The method takes two arguments:
|
||||||
|
|
||||||
Returns self (the Squire instance).
|
Returns self (the Squire instance).
|
||||||
|
|
||||||
|
### setKeyHandler
|
||||||
|
|
||||||
|
Adds or removes a keyboard shortcut. You can use this to override the default keyboard shortcuts (e.g. Ctrl-B for bold – see the bottom of KeyHandlers.js for the list).
|
||||||
|
|
||||||
|
This method takes two arguments:
|
||||||
|
|
||||||
|
* **key**: The key to handle, including any modifiers in alphabetical order. e.g. `"alt-ctrl-meta-shift-enter"`
|
||||||
|
* **fn**: The function to be called when this key is pressed, or `null` if removing a key handler. The function will be passed three arguments when called:
|
||||||
|
* **self**: A reference to the Squire instance.
|
||||||
|
* **event**: The key event object.
|
||||||
|
* **range**: A Range object representing the current selection.
|
||||||
|
|
||||||
|
Returns self (the Squire instance).
|
||||||
|
|
||||||
### focus
|
### focus
|
||||||
|
|
||||||
Focuses the editor.
|
Focuses the editor.
|
||||||
|
|
|
@ -42,6 +42,15 @@ var notWS = /[^ \t\r\n]/;
|
||||||
|
|
||||||
var indexOf = Array.prototype.indexOf;
|
var indexOf = Array.prototype.indexOf;
|
||||||
|
|
||||||
|
// Polyfill for FF3.5
|
||||||
|
if ( !Object.create ) {
|
||||||
|
Object.create = function ( proto ) {
|
||||||
|
var F = function () {};
|
||||||
|
F.prototype = proto;
|
||||||
|
return new F();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Native TreeWalker is buggy in IE and Opera:
|
Native TreeWalker is buggy in IE and Opera:
|
||||||
* IE9/10 sometimes throw errors when calling TreeWalker#nextNode or
|
* IE9/10 sometimes throw errors when calling TreeWalker#nextNode or
|
||||||
|
@ -1056,6 +1065,365 @@ var expandRangeToBlockBoundaries = function ( range ) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 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 <br>.
|
||||||
|
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 <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 && !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 (<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;
|
||||||
|
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' );
|
||||||
|
|
||||||
var instances = [];
|
var instances = [];
|
||||||
|
|
||||||
function getSquireInstance ( doc ) {
|
function getSquireInstance ( doc ) {
|
||||||
|
@ -1133,6 +1501,9 @@ function Squire ( doc ) {
|
||||||
// Opera does not fire keydown repeatedly.
|
// Opera does not fire keydown repeatedly.
|
||||||
this.addEventListener( isPresto ? 'keypress' : 'keydown', this._onKey );
|
this.addEventListener( isPresto ? 'keypress' : 'keydown', this._onKey );
|
||||||
|
|
||||||
|
// Add key handlers
|
||||||
|
this._keyHandlers = Object.create( keyHandlers );
|
||||||
|
|
||||||
// Fix IE<10's buggy implementation of Text#splitText.
|
// Fix IE<10's buggy implementation of Text#splitText.
|
||||||
// If the split is at the end of the node, it doesn't insert the newly split
|
// If the split is at the end of the node, it doesn't insert the newly split
|
||||||
// node into the document, and sets its value to undefined rather than ''.
|
// node into the document, and sets its value to undefined rather than ''.
|
||||||
|
@ -2758,365 +3129,6 @@ var keys = {
|
||||||
221: ']'
|
221: ']'
|
||||||
};
|
};
|
||||||
|
|
||||||
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 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 <br>.
|
|
||||||
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 <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 && !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 (<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;
|
|
||||||
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' );
|
|
||||||
|
|
||||||
// Ref: http://unixpapa.com/js/key.html
|
// Ref: http://unixpapa.com/js/key.html
|
||||||
proto._onKey = function ( event ) {
|
proto._onKey = function ( event ) {
|
||||||
var code = event.keyCode,
|
var code = event.keyCode,
|
||||||
|
@ -3156,8 +3168,8 @@ proto._onKey = function ( event ) {
|
||||||
|
|
||||||
key = modifiers + key;
|
key = modifiers + key;
|
||||||
|
|
||||||
if ( keyHandlers[ key ] ) {
|
if ( this._keyHandlers[ key ] ) {
|
||||||
keyHandlers[ key ]( this, event, range );
|
this._keyHandlers[ key ]( this, event, range );
|
||||||
} else if ( key.length === 1 && !range.collapsed ) {
|
} else if ( key.length === 1 && !range.collapsed ) {
|
||||||
// Record undo checkpoint.
|
// Record undo checkpoint.
|
||||||
this._recordUndoState( range );
|
this._recordUndoState( range );
|
||||||
|
@ -3170,6 +3182,11 @@ proto._onKey = function ( event ) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
proto.setKeyHandler = function ( key, fn ) {
|
||||||
|
this._keyHandlers[ key ] = fn;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
// --- Get/Set data ---
|
// --- Get/Set data ---
|
||||||
|
|
||||||
proto._getHTML = function () {
|
proto._getHTML = function () {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -37,3 +37,12 @@ var canObserveMutations = typeof MutationObserver !== 'undefined';
|
||||||
var notWS = /[^ \t\r\n]/;
|
var notWS = /[^ \t\r\n]/;
|
||||||
|
|
||||||
var indexOf = Array.prototype.indexOf;
|
var indexOf = Array.prototype.indexOf;
|
||||||
|
|
||||||
|
// Polyfill for FF3.5
|
||||||
|
if ( !Object.create ) {
|
||||||
|
Object.create = function ( proto ) {
|
||||||
|
var F = function () {};
|
||||||
|
F.prototype = proto;
|
||||||
|
return new F();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
371
source/Editor.js
371
source/Editor.js
|
@ -77,6 +77,9 @@ function Squire ( doc ) {
|
||||||
// Opera does not fire keydown repeatedly.
|
// Opera does not fire keydown repeatedly.
|
||||||
this.addEventListener( isPresto ? 'keypress' : 'keydown', this._onKey );
|
this.addEventListener( isPresto ? 'keypress' : 'keydown', this._onKey );
|
||||||
|
|
||||||
|
// Add key handlers
|
||||||
|
this._keyHandlers = Object.create( keyHandlers );
|
||||||
|
|
||||||
// Fix IE<10's buggy implementation of Text#splitText.
|
// Fix IE<10's buggy implementation of Text#splitText.
|
||||||
// If the split is at the end of the node, it doesn't insert the newly split
|
// If the split is at the end of the node, it doesn't insert the newly split
|
||||||
// node into the document, and sets its value to undefined rather than ''.
|
// node into the document, and sets its value to undefined rather than ''.
|
||||||
|
@ -1702,365 +1705,6 @@ var keys = {
|
||||||
221: ']'
|
221: ']'
|
||||||
};
|
};
|
||||||
|
|
||||||
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 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 <br>.
|
|
||||||
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 <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 && !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 (<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;
|
|
||||||
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' );
|
|
||||||
|
|
||||||
// Ref: http://unixpapa.com/js/key.html
|
// Ref: http://unixpapa.com/js/key.html
|
||||||
proto._onKey = function ( event ) {
|
proto._onKey = function ( event ) {
|
||||||
var code = event.keyCode,
|
var code = event.keyCode,
|
||||||
|
@ -2100,8 +1744,8 @@ proto._onKey = function ( event ) {
|
||||||
|
|
||||||
key = modifiers + key;
|
key = modifiers + key;
|
||||||
|
|
||||||
if ( keyHandlers[ key ] ) {
|
if ( this._keyHandlers[ key ] ) {
|
||||||
keyHandlers[ key ]( this, event, range );
|
this._keyHandlers[ key ]( this, event, range );
|
||||||
} else if ( key.length === 1 && !range.collapsed ) {
|
} else if ( key.length === 1 && !range.collapsed ) {
|
||||||
// Record undo checkpoint.
|
// Record undo checkpoint.
|
||||||
this._recordUndoState( range );
|
this._recordUndoState( range );
|
||||||
|
@ -2114,6 +1758,11 @@ proto._onKey = function ( event ) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
proto.setKeyHandler = function ( key, fn ) {
|
||||||
|
this._keyHandlers[ key ] = fn;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
// --- Get/Set data ---
|
// --- Get/Set data ---
|
||||||
|
|
||||||
proto._getHTML = function () {
|
proto._getHTML = function () {
|
||||||
|
|
360
source/KeyHandlers.js
Normal file
360
source/KeyHandlers.js
Normal file
|
@ -0,0 +1,360 @@
|
||||||
|
/*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 <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 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 <br>.
|
||||||
|
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 <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 && !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 (<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;
|
||||||
|
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' );
|
Loading…
Add table
Reference in a new issue