mirror of
https://github.com/fastmail/Squire.git
synced 2025-01-03 05:00:13 -05:00
Split cleaning fns and clipboard handlers into separate file.
This commit is contained in:
parent
e90f18dba9
commit
395a5825e9
7 changed files with 1167 additions and 1173 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/KeyHandlers.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/Clean.js source/Clipboard.js source/Editor.js source/outro.js
|
||||||
mkdir -p $(@D)
|
mkdir -p $(@D)
|
||||||
cat $^ | grep -v '^\/\*jshint' >$@
|
cat $^ | grep -v '^\/\*jshint' >$@
|
||||||
|
|
||||||
|
|
1166
build/squire-raw.js
1166
build/squire-raw.js
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
353
source/Clean.js
Normal file
353
source/Clean.js
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
/*jshint strict:false, undef:false, unused:false */
|
||||||
|
|
||||||
|
var linkRegExp = /\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))|([\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,}\b)/i;
|
||||||
|
|
||||||
|
var addLinks = function ( frag ) {
|
||||||
|
var doc = frag.ownerDocument,
|
||||||
|
walker = new TreeWalker( frag, SHOW_TEXT,
|
||||||
|
function ( node ) {
|
||||||
|
return !getNearest( node, 'A' );
|
||||||
|
}, false ),
|
||||||
|
node, data, parent, match, index, endIndex, child;
|
||||||
|
while ( node = walker.nextNode() ) {
|
||||||
|
data = node.data;
|
||||||
|
parent = node.parentNode;
|
||||||
|
while ( match = linkRegExp.exec( data ) ) {
|
||||||
|
index = match.index;
|
||||||
|
endIndex = index + match[0].length;
|
||||||
|
if ( index ) {
|
||||||
|
child = doc.createTextNode( data.slice( 0, index ) );
|
||||||
|
parent.insertBefore( child, node );
|
||||||
|
}
|
||||||
|
child = doc.createElement( 'A' );
|
||||||
|
child.textContent = data.slice( index, endIndex );
|
||||||
|
child.href = match[1] ?
|
||||||
|
/^(?:ht|f)tps?:/.test( match[1] ) ?
|
||||||
|
match[1] :
|
||||||
|
'http://' + match[1] :
|
||||||
|
'mailto:' + match[2];
|
||||||
|
parent.insertBefore( child, node );
|
||||||
|
node.data = data = data.slice( endIndex );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE|UDIO)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|UL)$/;
|
||||||
|
|
||||||
|
var fontSizes = {
|
||||||
|
1: 10,
|
||||||
|
2: 13,
|
||||||
|
3: 16,
|
||||||
|
4: 18,
|
||||||
|
5: 24,
|
||||||
|
6: 32,
|
||||||
|
7: 48
|
||||||
|
};
|
||||||
|
|
||||||
|
var spanToSemantic = {
|
||||||
|
backgroundColor: {
|
||||||
|
regexp: notWS,
|
||||||
|
replace: function ( doc, colour ) {
|
||||||
|
return createElement( doc, 'SPAN', {
|
||||||
|
'class': 'highlight',
|
||||||
|
style: 'background-color: ' + colour
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
regexp: notWS,
|
||||||
|
replace: function ( doc, colour ) {
|
||||||
|
return createElement( doc, 'SPAN', {
|
||||||
|
'class': 'colour',
|
||||||
|
style: 'color:' + colour
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fontWeight: {
|
||||||
|
regexp: /^bold/i,
|
||||||
|
replace: function ( doc ) {
|
||||||
|
return createElement( doc, 'B' );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fontStyle: {
|
||||||
|
regexp: /^italic/i,
|
||||||
|
replace: function ( doc ) {
|
||||||
|
return createElement( doc, 'I' );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
regexp: notWS,
|
||||||
|
replace: function ( doc, family ) {
|
||||||
|
return createElement( doc, 'SPAN', {
|
||||||
|
'class': 'font',
|
||||||
|
style: 'font-family:' + family
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
regexp: notWS,
|
||||||
|
replace: function ( doc, size ) {
|
||||||
|
return createElement( doc, 'SPAN', {
|
||||||
|
'class': 'size',
|
||||||
|
style: 'font-size:' + size
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var replaceWithTag = function ( tag ) {
|
||||||
|
return function ( node, parent ) {
|
||||||
|
var el = createElement( node.ownerDocument, tag );
|
||||||
|
parent.replaceChild( el, node );
|
||||||
|
el.appendChild( empty( node ) );
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var stylesRewriters = {
|
||||||
|
SPAN: function ( span, parent ) {
|
||||||
|
var style = span.style,
|
||||||
|
doc = span.ownerDocument,
|
||||||
|
attr, converter, css, newTreeBottom, newTreeTop, el;
|
||||||
|
|
||||||
|
for ( attr in spanToSemantic ) {
|
||||||
|
converter = spanToSemantic[ attr ];
|
||||||
|
css = style[ attr ];
|
||||||
|
if ( css && converter.regexp.test( css ) ) {
|
||||||
|
el = converter.replace( doc, css );
|
||||||
|
if ( newTreeBottom ) {
|
||||||
|
newTreeBottom.appendChild( el );
|
||||||
|
}
|
||||||
|
newTreeBottom = el;
|
||||||
|
if ( !newTreeTop ) {
|
||||||
|
newTreeTop = el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( newTreeTop ) {
|
||||||
|
newTreeBottom.appendChild( empty( span ) );
|
||||||
|
parent.replaceChild( newTreeTop, span );
|
||||||
|
}
|
||||||
|
|
||||||
|
return newTreeBottom || span;
|
||||||
|
},
|
||||||
|
STRONG: replaceWithTag( 'B' ),
|
||||||
|
EM: replaceWithTag( 'I' ),
|
||||||
|
STRIKE: replaceWithTag( 'S' ),
|
||||||
|
FONT: function ( node, parent ) {
|
||||||
|
var face = node.face,
|
||||||
|
size = node.size,
|
||||||
|
colour = node.color,
|
||||||
|
doc = node.ownerDocument,
|
||||||
|
fontSpan, sizeSpan, colourSpan,
|
||||||
|
newTreeBottom, newTreeTop;
|
||||||
|
if ( face ) {
|
||||||
|
fontSpan = createElement( doc, 'SPAN', {
|
||||||
|
'class': 'font',
|
||||||
|
style: 'font-family:' + face
|
||||||
|
});
|
||||||
|
newTreeTop = fontSpan;
|
||||||
|
newTreeBottom = fontSpan;
|
||||||
|
}
|
||||||
|
if ( size ) {
|
||||||
|
sizeSpan = createElement( doc, 'SPAN', {
|
||||||
|
'class': 'size',
|
||||||
|
style: 'font-size:' + fontSizes[ size ] + 'px'
|
||||||
|
});
|
||||||
|
if ( !newTreeTop ) {
|
||||||
|
newTreeTop = sizeSpan;
|
||||||
|
}
|
||||||
|
if ( newTreeBottom ) {
|
||||||
|
newTreeBottom.appendChild( sizeSpan );
|
||||||
|
}
|
||||||
|
newTreeBottom = sizeSpan;
|
||||||
|
}
|
||||||
|
if ( colour && /^#?([\dA-F]{3}){1,2}$/i.test( colour ) ) {
|
||||||
|
if ( colour.charAt( 0 ) !== '#' ) {
|
||||||
|
colour = '#' + colour;
|
||||||
|
}
|
||||||
|
colourSpan = createElement( doc, 'SPAN', {
|
||||||
|
'class': 'colour',
|
||||||
|
style: 'color:' + colour
|
||||||
|
});
|
||||||
|
if ( !newTreeTop ) {
|
||||||
|
newTreeTop = colourSpan;
|
||||||
|
}
|
||||||
|
if ( newTreeBottom ) {
|
||||||
|
newTreeBottom.appendChild( colourSpan );
|
||||||
|
}
|
||||||
|
newTreeBottom = colourSpan;
|
||||||
|
}
|
||||||
|
if ( !newTreeTop ) {
|
||||||
|
newTreeTop = newTreeBottom = createElement( doc, 'SPAN' );
|
||||||
|
}
|
||||||
|
parent.replaceChild( newTreeTop, node );
|
||||||
|
newTreeBottom.appendChild( empty( node ) );
|
||||||
|
return newTreeBottom;
|
||||||
|
},
|
||||||
|
TT: function ( node, parent ) {
|
||||||
|
var el = createElement( node.ownerDocument, 'SPAN', {
|
||||||
|
'class': 'font',
|
||||||
|
style: 'font-family:menlo,consolas,"courier new",monospace'
|
||||||
|
});
|
||||||
|
parent.replaceChild( el, node );
|
||||||
|
el.appendChild( empty( node ) );
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var removeEmptyInlines = function ( root ) {
|
||||||
|
var children = root.childNodes,
|
||||||
|
l = children.length,
|
||||||
|
child;
|
||||||
|
while ( l-- ) {
|
||||||
|
child = children[l];
|
||||||
|
if ( child.nodeType === ELEMENT_NODE && !isLeaf( child ) ) {
|
||||||
|
removeEmptyInlines( child );
|
||||||
|
if ( isInline( child ) && !child.firstChild ) {
|
||||||
|
root.removeChild( child );
|
||||||
|
}
|
||||||
|
} else if ( child.nodeType === TEXT_NODE && !child.data ) {
|
||||||
|
root.removeChild( child );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Two purposes:
|
||||||
|
|
||||||
|
1. Remove nodes we don't want, such as weird <o:p> tags, comment nodes
|
||||||
|
and whitespace nodes.
|
||||||
|
2. Convert inline tags into our preferred format.
|
||||||
|
*/
|
||||||
|
var cleanTree = function ( node, allowStyles ) {
|
||||||
|
var children = node.childNodes,
|
||||||
|
i, l, child, nodeName, nodeType, rewriter, childLength,
|
||||||
|
data, j, ll;
|
||||||
|
for ( i = 0, l = children.length; i < l; i += 1 ) {
|
||||||
|
child = children[i];
|
||||||
|
nodeName = child.nodeName;
|
||||||
|
nodeType = child.nodeType;
|
||||||
|
rewriter = stylesRewriters[ nodeName ];
|
||||||
|
if ( nodeType === ELEMENT_NODE ) {
|
||||||
|
childLength = child.childNodes.length;
|
||||||
|
if ( rewriter ) {
|
||||||
|
child = rewriter( child, node );
|
||||||
|
} else if ( !allowedBlock.test( nodeName ) &&
|
||||||
|
!isInline( child ) ) {
|
||||||
|
i -= 1;
|
||||||
|
l += childLength - 1;
|
||||||
|
node.replaceChild( empty( child ), child );
|
||||||
|
continue;
|
||||||
|
} else if ( !allowStyles && child.style.cssText ) {
|
||||||
|
child.removeAttribute( 'style' );
|
||||||
|
}
|
||||||
|
if ( childLength ) {
|
||||||
|
cleanTree( child, allowStyles );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ( nodeType === TEXT_NODE ) {
|
||||||
|
data = child.data;
|
||||||
|
// Use \S instead of notWS, because we want to remove nodes
|
||||||
|
// which are just nbsp, in order to cleanup <div>nbsp<br></div>
|
||||||
|
// construct.
|
||||||
|
if ( /\S/.test( data ) ) {
|
||||||
|
// If the parent node is inline, don't trim this node as
|
||||||
|
// it probably isn't at the end of the block.
|
||||||
|
if ( isInline( node ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
j = 0;
|
||||||
|
ll = data.length;
|
||||||
|
if ( !i || !isInline( children[ i - 1 ] ) ) {
|
||||||
|
while ( j < ll && !notWS.test( data.charAt( j ) ) ) {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
if ( j ) {
|
||||||
|
child.data = data = data.slice( j );
|
||||||
|
ll -= j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( i + 1 === l || !isInline( children[ i + 1 ] ) ) {
|
||||||
|
j = ll;
|
||||||
|
while ( j > 0 && !notWS.test( data.charAt( j - 1 ) ) ) {
|
||||||
|
j -= 1;
|
||||||
|
}
|
||||||
|
if ( j < ll ) {
|
||||||
|
child.data = data.slice( 0, j );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// If we have just white space, it may still be important if it
|
||||||
|
// separates two inline nodes, e.g. "<a>link</a> <a>link</a>".
|
||||||
|
else if ( i && i + 1 < l &&
|
||||||
|
isInline( children[ i - 1 ] ) &&
|
||||||
|
isInline( children[ i + 1 ] ) ) {
|
||||||
|
child.data = ' ';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.removeChild( child );
|
||||||
|
i -= 1;
|
||||||
|
l -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
|
||||||
|
var notWSTextNode = function ( node ) {
|
||||||
|
return node.nodeType === ELEMENT_NODE ?
|
||||||
|
node.nodeName === 'BR' :
|
||||||
|
notWS.test( node.data );
|
||||||
|
};
|
||||||
|
var isLineBreak = function ( br ) {
|
||||||
|
var block = br.parentNode,
|
||||||
|
walker;
|
||||||
|
while ( isInline( block ) ) {
|
||||||
|
block = block.parentNode;
|
||||||
|
}
|
||||||
|
walker = new TreeWalker(
|
||||||
|
block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode );
|
||||||
|
walker.currentNode = br;
|
||||||
|
return !!walker.nextNode();
|
||||||
|
};
|
||||||
|
|
||||||
|
// <br> elements are treated specially, and differently depending on the
|
||||||
|
// browser, when in rich text editor mode. When adding HTML from external
|
||||||
|
// sources, we must remove them, replacing the ones that actually affect
|
||||||
|
// line breaks with a split of the block element containing it (and wrapping
|
||||||
|
// any not inside a block). Browsers that want <br> elements at the end of
|
||||||
|
// each block will then have them added back in a later fixCursor method
|
||||||
|
// call.
|
||||||
|
var cleanupBRs = function ( root ) {
|
||||||
|
var brs = root.querySelectorAll( 'BR' ),
|
||||||
|
brBreaksLine = [],
|
||||||
|
l = brs.length,
|
||||||
|
i, br, parent;
|
||||||
|
|
||||||
|
// Must calculate whether the <br> breaks a line first, because if we
|
||||||
|
// have two <br>s next to each other, after the first one is converted
|
||||||
|
// to a block split, the second will be at the end of a block and
|
||||||
|
// therefore seem to not be a line break. But in its original context it
|
||||||
|
// was, so we should also convert it to a block split.
|
||||||
|
for ( i = 0; i < l; i += 1 ) {
|
||||||
|
brBreaksLine[i] = isLineBreak( brs[i] );
|
||||||
|
}
|
||||||
|
while ( l-- ) {
|
||||||
|
br = brs[l];
|
||||||
|
// Cleanup may have removed it
|
||||||
|
parent = br.parentNode;
|
||||||
|
if ( !parent ) { continue; }
|
||||||
|
// If it doesn't break a line, just remove it; it's not doing
|
||||||
|
// anything useful. We'll add it back later if required by the
|
||||||
|
// browser. If it breaks a line, wrap the content in div tags
|
||||||
|
// and replace the brs.
|
||||||
|
if ( !brBreaksLine[l] ) {
|
||||||
|
detach( br );
|
||||||
|
} else if ( !isInline( parent ) ) {
|
||||||
|
fixContainer( parent );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
162
source/Clipboard.js
Normal file
162
source/Clipboard.js
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
/*jshint strict:false, undef:false, unused:false */
|
||||||
|
|
||||||
|
var onCut = function () {
|
||||||
|
// Save undo checkpoint
|
||||||
|
var range = this.getSelection();
|
||||||
|
var self = this;
|
||||||
|
this._recordUndoState( range );
|
||||||
|
this._getRangeAndRemoveBookmark( range );
|
||||||
|
this.setSelection( range );
|
||||||
|
setTimeout( function () {
|
||||||
|
try {
|
||||||
|
// If all content removed, ensure div at start of body.
|
||||||
|
self._ensureBottomLine();
|
||||||
|
} catch ( error ) {
|
||||||
|
self.didError( error );
|
||||||
|
}
|
||||||
|
}, 0 );
|
||||||
|
};
|
||||||
|
|
||||||
|
var onPaste = function ( event ) {
|
||||||
|
if ( this._awaitingPaste ) { return; }
|
||||||
|
|
||||||
|
// Treat image paste as a drop of an image file.
|
||||||
|
var clipboardData = event.clipboardData,
|
||||||
|
items = clipboardData && clipboardData.items,
|
||||||
|
fireDrop = false,
|
||||||
|
hasImage = false,
|
||||||
|
l, type;
|
||||||
|
if ( items ) {
|
||||||
|
l = items.length;
|
||||||
|
while ( l-- ) {
|
||||||
|
type = items[l].type;
|
||||||
|
if ( type === 'text/html' ) {
|
||||||
|
hasImage = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ( /^image\/.*/.test( type ) ) {
|
||||||
|
hasImage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( hasImage ) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.fireEvent( 'dragover', {
|
||||||
|
dataTransfer: clipboardData,
|
||||||
|
/*jshint loopfunc: true */
|
||||||
|
preventDefault: function () {
|
||||||
|
fireDrop = true;
|
||||||
|
}
|
||||||
|
/*jshint loopfunc: false */
|
||||||
|
});
|
||||||
|
if ( fireDrop ) {
|
||||||
|
this.fireEvent( 'drop', {
|
||||||
|
dataTransfer: clipboardData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._awaitingPaste = true;
|
||||||
|
|
||||||
|
var self = this,
|
||||||
|
body = this._body,
|
||||||
|
range = this.getSelection(),
|
||||||
|
startContainer, startOffset, endContainer, endOffset, startBlock;
|
||||||
|
|
||||||
|
// Record undo checkpoint
|
||||||
|
self._recordUndoState( range );
|
||||||
|
self._getRangeAndRemoveBookmark( range );
|
||||||
|
|
||||||
|
// Note current selection. We must do this AFTER recording the undo
|
||||||
|
// checkpoint, as this modifies the DOM.
|
||||||
|
startContainer = range.startContainer;
|
||||||
|
startOffset = range.startOffset;
|
||||||
|
endContainer = range.endContainer;
|
||||||
|
endOffset = range.endOffset;
|
||||||
|
startBlock = getStartBlockOfRange( range );
|
||||||
|
|
||||||
|
// We need to position the pasteArea in the visible portion of the screen
|
||||||
|
// to stop the browser auto-scrolling.
|
||||||
|
var pasteArea = this.createElement( 'DIV', {
|
||||||
|
style: 'position: absolute; overflow: hidden; top:' +
|
||||||
|
( body.scrollTop +
|
||||||
|
( startBlock ? startBlock.getBoundingClientRect().top : 0 ) ) +
|
||||||
|
'px; right: 150%; width: 1px; height: 1px;'
|
||||||
|
});
|
||||||
|
body.appendChild( pasteArea );
|
||||||
|
range.selectNodeContents( pasteArea );
|
||||||
|
this.setSelection( range );
|
||||||
|
|
||||||
|
// A setTimeout of 0 means this is added to the back of the
|
||||||
|
// single javascript thread, so it will be executed after the
|
||||||
|
// paste event.
|
||||||
|
setTimeout( function () {
|
||||||
|
try {
|
||||||
|
// Get the pasted content and clean
|
||||||
|
var frag = self._doc.createDocumentFragment(),
|
||||||
|
next = pasteArea,
|
||||||
|
first, range;
|
||||||
|
|
||||||
|
// #88: Chrome can apparently split the paste area if certain
|
||||||
|
// content is inserted; gather them all up.
|
||||||
|
while ( pasteArea = next ) {
|
||||||
|
next = pasteArea.nextSibling;
|
||||||
|
frag.appendChild( empty( detach( pasteArea ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
first = frag.firstChild;
|
||||||
|
range = self._createRange(
|
||||||
|
startContainer, startOffset, endContainer, endOffset );
|
||||||
|
|
||||||
|
// Was anything actually pasted?
|
||||||
|
if ( first ) {
|
||||||
|
// Safari and IE like putting extra divs around things.
|
||||||
|
if ( first === frag.lastChild &&
|
||||||
|
first.nodeName === 'DIV' ) {
|
||||||
|
frag.replaceChild( empty( first ), first );
|
||||||
|
}
|
||||||
|
|
||||||
|
frag.normalize();
|
||||||
|
addLinks( frag );
|
||||||
|
cleanTree( frag, false );
|
||||||
|
cleanupBRs( frag );
|
||||||
|
removeEmptyInlines( frag );
|
||||||
|
|
||||||
|
var node = frag,
|
||||||
|
doPaste = true,
|
||||||
|
event = {
|
||||||
|
fragment: frag,
|
||||||
|
preventDefault: function () {
|
||||||
|
doPaste = false;
|
||||||
|
},
|
||||||
|
isDefaultPrevented: function () {
|
||||||
|
return !doPaste;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
while ( node = getNextBlock( node ) ) {
|
||||||
|
fixCursor( node );
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fireEvent( 'willPaste', event );
|
||||||
|
|
||||||
|
// Insert pasted data
|
||||||
|
if ( doPaste ) {
|
||||||
|
insertTreeFragmentIntoRange( range, event.fragment );
|
||||||
|
if ( !canObserveMutations ) {
|
||||||
|
self._docWasChanged();
|
||||||
|
}
|
||||||
|
range.collapse( false );
|
||||||
|
self._ensureBottomLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setSelection( range );
|
||||||
|
self._updatePath( range, true );
|
||||||
|
|
||||||
|
self._awaitingPaste = false;
|
||||||
|
} catch ( error ) {
|
||||||
|
self.didError( error );
|
||||||
|
}
|
||||||
|
}, 0 );
|
||||||
|
};
|
588
source/Editor.js
588
source/Editor.js
|
@ -82,11 +82,11 @@ function Squire ( doc, config ) {
|
||||||
// IE sometimes fires the beforepaste event twice; make sure it is not run
|
// IE sometimes fires the beforepaste event twice; make sure it is not run
|
||||||
// again before our after paste function is called.
|
// again before our after paste function is called.
|
||||||
this._awaitingPaste = false;
|
this._awaitingPaste = false;
|
||||||
this.addEventListener( isIElt11 ? 'beforecut' : 'cut', this._onCut );
|
this.addEventListener( isIElt11 ? 'beforecut' : 'cut', onCut );
|
||||||
this.addEventListener( isIElt11 ? 'beforepaste' : 'paste', this._onPaste );
|
this.addEventListener( isIElt11 ? 'beforepaste' : 'paste', onPaste );
|
||||||
|
|
||||||
// Opera does not fire keydown repeatedly.
|
// Opera does not fire keydown repeatedly.
|
||||||
this.addEventListener( isPresto ? 'keypress' : 'keydown', this._onKey );
|
this.addEventListener( isPresto ? 'keypress' : 'keydown', onKey );
|
||||||
|
|
||||||
// Add key handlers
|
// Add key handlers
|
||||||
this._keyHandlers = Object.create( keyHandlers );
|
this._keyHandlers = Object.create( keyHandlers );
|
||||||
|
@ -1225,360 +1225,6 @@ var decreaseListLevel = function ( frag ) {
|
||||||
return frag;
|
return frag;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Clean ---
|
|
||||||
|
|
||||||
var linkRegExp = /\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))|([\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,}\b)/i;
|
|
||||||
|
|
||||||
var addLinks = function ( frag ) {
|
|
||||||
var doc = frag.ownerDocument,
|
|
||||||
walker = new TreeWalker( frag, SHOW_TEXT,
|
|
||||||
function ( node ) {
|
|
||||||
return !getNearest( node, 'A' );
|
|
||||||
}, false ),
|
|
||||||
node, data, parent, match, index, endIndex, child;
|
|
||||||
while ( node = walker.nextNode() ) {
|
|
||||||
data = node.data;
|
|
||||||
parent = node.parentNode;
|
|
||||||
while ( match = linkRegExp.exec( data ) ) {
|
|
||||||
index = match.index;
|
|
||||||
endIndex = index + match[0].length;
|
|
||||||
if ( index ) {
|
|
||||||
child = doc.createTextNode( data.slice( 0, index ) );
|
|
||||||
parent.insertBefore( child, node );
|
|
||||||
}
|
|
||||||
child = doc.createElement( 'A' );
|
|
||||||
child.textContent = data.slice( index, endIndex );
|
|
||||||
child.href = match[1] ?
|
|
||||||
/^(?:ht|f)tps?:/.test( match[1] ) ?
|
|
||||||
match[1] :
|
|
||||||
'http://' + match[1] :
|
|
||||||
'mailto:' + match[2];
|
|
||||||
parent.insertBefore( child, node );
|
|
||||||
node.data = data = data.slice( endIndex );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE|UDIO)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|UL)$/;
|
|
||||||
|
|
||||||
var fontSizes = {
|
|
||||||
1: 10,
|
|
||||||
2: 13,
|
|
||||||
3: 16,
|
|
||||||
4: 18,
|
|
||||||
5: 24,
|
|
||||||
6: 32,
|
|
||||||
7: 48
|
|
||||||
};
|
|
||||||
|
|
||||||
var spanToSemantic = {
|
|
||||||
backgroundColor: {
|
|
||||||
regexp: notWS,
|
|
||||||
replace: function ( doc, colour ) {
|
|
||||||
return createElement( doc, 'SPAN', {
|
|
||||||
'class': 'highlight',
|
|
||||||
style: 'background-color: ' + colour
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
regexp: notWS,
|
|
||||||
replace: function ( doc, colour ) {
|
|
||||||
return createElement( doc, 'SPAN', {
|
|
||||||
'class': 'colour',
|
|
||||||
style: 'color:' + colour
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fontWeight: {
|
|
||||||
regexp: /^bold/i,
|
|
||||||
replace: function ( doc ) {
|
|
||||||
return createElement( doc, 'B' );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fontStyle: {
|
|
||||||
regexp: /^italic/i,
|
|
||||||
replace: function ( doc ) {
|
|
||||||
return createElement( doc, 'I' );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
regexp: notWS,
|
|
||||||
replace: function ( doc, family ) {
|
|
||||||
return createElement( doc, 'SPAN', {
|
|
||||||
'class': 'font',
|
|
||||||
style: 'font-family:' + family
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fontSize: {
|
|
||||||
regexp: notWS,
|
|
||||||
replace: function ( doc, size ) {
|
|
||||||
return createElement( doc, 'SPAN', {
|
|
||||||
'class': 'size',
|
|
||||||
style: 'font-size:' + size
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var replaceWithTag = function ( tag ) {
|
|
||||||
return function ( node, parent ) {
|
|
||||||
var el = createElement( node.ownerDocument, tag );
|
|
||||||
parent.replaceChild( el, node );
|
|
||||||
el.appendChild( empty( node ) );
|
|
||||||
return el;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var stylesRewriters = {
|
|
||||||
SPAN: function ( span, parent ) {
|
|
||||||
var style = span.style,
|
|
||||||
doc = span.ownerDocument,
|
|
||||||
attr, converter, css, newTreeBottom, newTreeTop, el;
|
|
||||||
|
|
||||||
for ( attr in spanToSemantic ) {
|
|
||||||
converter = spanToSemantic[ attr ];
|
|
||||||
css = style[ attr ];
|
|
||||||
if ( css && converter.regexp.test( css ) ) {
|
|
||||||
el = converter.replace( doc, css );
|
|
||||||
if ( newTreeBottom ) {
|
|
||||||
newTreeBottom.appendChild( el );
|
|
||||||
}
|
|
||||||
newTreeBottom = el;
|
|
||||||
if ( !newTreeTop ) {
|
|
||||||
newTreeTop = el;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( newTreeTop ) {
|
|
||||||
newTreeBottom.appendChild( empty( span ) );
|
|
||||||
parent.replaceChild( newTreeTop, span );
|
|
||||||
}
|
|
||||||
|
|
||||||
return newTreeBottom || span;
|
|
||||||
},
|
|
||||||
STRONG: replaceWithTag( 'B' ),
|
|
||||||
EM: replaceWithTag( 'I' ),
|
|
||||||
STRIKE: replaceWithTag( 'S' ),
|
|
||||||
FONT: function ( node, parent ) {
|
|
||||||
var face = node.face,
|
|
||||||
size = node.size,
|
|
||||||
colour = node.color,
|
|
||||||
doc = node.ownerDocument,
|
|
||||||
fontSpan, sizeSpan, colourSpan,
|
|
||||||
newTreeBottom, newTreeTop;
|
|
||||||
if ( face ) {
|
|
||||||
fontSpan = createElement( doc, 'SPAN', {
|
|
||||||
'class': 'font',
|
|
||||||
style: 'font-family:' + face
|
|
||||||
});
|
|
||||||
newTreeTop = fontSpan;
|
|
||||||
newTreeBottom = fontSpan;
|
|
||||||
}
|
|
||||||
if ( size ) {
|
|
||||||
sizeSpan = createElement( doc, 'SPAN', {
|
|
||||||
'class': 'size',
|
|
||||||
style: 'font-size:' + fontSizes[ size ] + 'px'
|
|
||||||
});
|
|
||||||
if ( !newTreeTop ) {
|
|
||||||
newTreeTop = sizeSpan;
|
|
||||||
}
|
|
||||||
if ( newTreeBottom ) {
|
|
||||||
newTreeBottom.appendChild( sizeSpan );
|
|
||||||
}
|
|
||||||
newTreeBottom = sizeSpan;
|
|
||||||
}
|
|
||||||
if ( colour && /^#?([\dA-F]{3}){1,2}$/i.test( colour ) ) {
|
|
||||||
if ( colour.charAt( 0 ) !== '#' ) {
|
|
||||||
colour = '#' + colour;
|
|
||||||
}
|
|
||||||
colourSpan = createElement( doc, 'SPAN', {
|
|
||||||
'class': 'colour',
|
|
||||||
style: 'color:' + colour
|
|
||||||
});
|
|
||||||
if ( !newTreeTop ) {
|
|
||||||
newTreeTop = colourSpan;
|
|
||||||
}
|
|
||||||
if ( newTreeBottom ) {
|
|
||||||
newTreeBottom.appendChild( colourSpan );
|
|
||||||
}
|
|
||||||
newTreeBottom = colourSpan;
|
|
||||||
}
|
|
||||||
if ( !newTreeTop ) {
|
|
||||||
newTreeTop = newTreeBottom = createElement( doc, 'SPAN' );
|
|
||||||
}
|
|
||||||
parent.replaceChild( newTreeTop, node );
|
|
||||||
newTreeBottom.appendChild( empty( node ) );
|
|
||||||
return newTreeBottom;
|
|
||||||
},
|
|
||||||
TT: function ( node, parent ) {
|
|
||||||
var el = createElement( node.ownerDocument, 'SPAN', {
|
|
||||||
'class': 'font',
|
|
||||||
style: 'font-family:menlo,consolas,"courier new",monospace'
|
|
||||||
});
|
|
||||||
parent.replaceChild( el, node );
|
|
||||||
el.appendChild( empty( node ) );
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var removeEmptyInlines = function ( root ) {
|
|
||||||
var children = root.childNodes,
|
|
||||||
l = children.length,
|
|
||||||
child;
|
|
||||||
while ( l-- ) {
|
|
||||||
child = children[l];
|
|
||||||
if ( child.nodeType === ELEMENT_NODE && !isLeaf( child ) ) {
|
|
||||||
removeEmptyInlines( child );
|
|
||||||
if ( isInline( child ) && !child.firstChild ) {
|
|
||||||
root.removeChild( child );
|
|
||||||
}
|
|
||||||
} else if ( child.nodeType === TEXT_NODE && !child.data ) {
|
|
||||||
root.removeChild( child );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
Two purposes:
|
|
||||||
|
|
||||||
1. Remove nodes we don't want, such as weird <o:p> tags, comment nodes
|
|
||||||
and whitespace nodes.
|
|
||||||
2. Convert inline tags into our preferred format.
|
|
||||||
*/
|
|
||||||
var cleanTree = function ( node, allowStyles ) {
|
|
||||||
var children = node.childNodes,
|
|
||||||
i, l, child, nodeName, nodeType, rewriter, childLength,
|
|
||||||
data, j, ll;
|
|
||||||
for ( i = 0, l = children.length; i < l; i += 1 ) {
|
|
||||||
child = children[i];
|
|
||||||
nodeName = child.nodeName;
|
|
||||||
nodeType = child.nodeType;
|
|
||||||
rewriter = stylesRewriters[ nodeName ];
|
|
||||||
if ( nodeType === ELEMENT_NODE ) {
|
|
||||||
childLength = child.childNodes.length;
|
|
||||||
if ( rewriter ) {
|
|
||||||
child = rewriter( child, node );
|
|
||||||
} else if ( !allowedBlock.test( nodeName ) &&
|
|
||||||
!isInline( child ) ) {
|
|
||||||
i -= 1;
|
|
||||||
l += childLength - 1;
|
|
||||||
node.replaceChild( empty( child ), child );
|
|
||||||
continue;
|
|
||||||
} else if ( !allowStyles && child.style.cssText ) {
|
|
||||||
child.removeAttribute( 'style' );
|
|
||||||
}
|
|
||||||
if ( childLength ) {
|
|
||||||
cleanTree( child, allowStyles );
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ( nodeType === TEXT_NODE ) {
|
|
||||||
data = child.data;
|
|
||||||
// Use \S instead of notWS, because we want to remove nodes
|
|
||||||
// which are just nbsp, in order to cleanup <div>nbsp<br></div>
|
|
||||||
// construct.
|
|
||||||
if ( /\S/.test( data ) ) {
|
|
||||||
// If the parent node is inline, don't trim this node as
|
|
||||||
// it probably isn't at the end of the block.
|
|
||||||
if ( isInline( node ) ) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
j = 0;
|
|
||||||
ll = data.length;
|
|
||||||
if ( !i || !isInline( children[ i - 1 ] ) ) {
|
|
||||||
while ( j < ll && !notWS.test( data.charAt( j ) ) ) {
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
if ( j ) {
|
|
||||||
child.data = data = data.slice( j );
|
|
||||||
ll -= j;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( i + 1 === l || !isInline( children[ i + 1 ] ) ) {
|
|
||||||
j = ll;
|
|
||||||
while ( j > 0 && !notWS.test( data.charAt( j - 1 ) ) ) {
|
|
||||||
j -= 1;
|
|
||||||
}
|
|
||||||
if ( j < ll ) {
|
|
||||||
child.data = data.slice( 0, j );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// If we have just white space, it may still be important if it
|
|
||||||
// separates two inline nodes, e.g. "<a>link</a> <a>link</a>".
|
|
||||||
else if ( i && i + 1 < l &&
|
|
||||||
isInline( children[ i - 1 ] ) &&
|
|
||||||
isInline( children[ i + 1 ] ) ) {
|
|
||||||
child.data = ' ';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node.removeChild( child );
|
|
||||||
i -= 1;
|
|
||||||
l -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
};
|
|
||||||
|
|
||||||
var notWSTextNode = function ( node ) {
|
|
||||||
return node.nodeType === ELEMENT_NODE ?
|
|
||||||
node.nodeName === 'BR' :
|
|
||||||
notWS.test( node.data );
|
|
||||||
};
|
|
||||||
var isLineBreak = function ( br ) {
|
|
||||||
var block = br.parentNode,
|
|
||||||
walker;
|
|
||||||
while ( isInline( block ) ) {
|
|
||||||
block = block.parentNode;
|
|
||||||
}
|
|
||||||
walker = new TreeWalker(
|
|
||||||
block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode );
|
|
||||||
walker.currentNode = br;
|
|
||||||
return !!walker.nextNode();
|
|
||||||
};
|
|
||||||
|
|
||||||
// <br> elements are treated specially, and differently depending on the
|
|
||||||
// browser, when in rich text editor mode. When adding HTML from external
|
|
||||||
// sources, we must remove them, replacing the ones that actually affect
|
|
||||||
// line breaks with a split of the block element containing it (and wrapping
|
|
||||||
// any not inside a block). Browsers that want <br> elements at the end of
|
|
||||||
// each block will then have them added back in a later fixCursor method
|
|
||||||
// call.
|
|
||||||
var cleanupBRs = function ( root ) {
|
|
||||||
var brs = root.querySelectorAll( 'BR' ),
|
|
||||||
brBreaksLine = [],
|
|
||||||
l = brs.length,
|
|
||||||
i, br, parent;
|
|
||||||
|
|
||||||
// Must calculate whether the <br> breaks a line first, because if we
|
|
||||||
// have two <br>s next to each other, after the first one is converted
|
|
||||||
// to a block split, the second will be at the end of a block and
|
|
||||||
// therefore seem to not be a line break. But in its original context it
|
|
||||||
// was, so we should also convert it to a block split.
|
|
||||||
for ( i = 0; i < l; i += 1 ) {
|
|
||||||
brBreaksLine[i] = isLineBreak( brs[i] );
|
|
||||||
}
|
|
||||||
while ( l-- ) {
|
|
||||||
br = brs[l];
|
|
||||||
// Cleanup may have removed it
|
|
||||||
parent = br.parentNode;
|
|
||||||
if ( !parent ) { continue; }
|
|
||||||
// If it doesn't break a line, just remove it; it's not doing
|
|
||||||
// anything useful. We'll add it back later if required by the
|
|
||||||
// browser. If it breaks a line, wrap the content in div tags
|
|
||||||
// and replace the brs.
|
|
||||||
if ( !brBreaksLine[l] ) {
|
|
||||||
detach( br );
|
|
||||||
} else if ( !isInline( parent ) ) {
|
|
||||||
fixContainer( parent );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
proto._ensureBottomLine = function () {
|
proto._ensureBottomLine = function () {
|
||||||
var body = this._body,
|
var body = this._body,
|
||||||
last = body.lastElementChild;
|
last = body.lastElementChild;
|
||||||
|
@ -1588,236 +1234,8 @@ proto._ensureBottomLine = function () {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Cut and Paste ---
|
|
||||||
|
|
||||||
proto._onCut = function () {
|
|
||||||
// Save undo checkpoint
|
|
||||||
var range = this.getSelection();
|
|
||||||
var self = this;
|
|
||||||
this._recordUndoState( range );
|
|
||||||
this._getRangeAndRemoveBookmark( range );
|
|
||||||
this.setSelection( range );
|
|
||||||
setTimeout( function () {
|
|
||||||
try {
|
|
||||||
// If all content removed, ensure div at start of body.
|
|
||||||
self._ensureBottomLine();
|
|
||||||
} catch ( error ) {
|
|
||||||
self.didError( error );
|
|
||||||
}
|
|
||||||
}, 0 );
|
|
||||||
};
|
|
||||||
|
|
||||||
proto._onPaste = function ( event ) {
|
|
||||||
if ( this._awaitingPaste ) { return; }
|
|
||||||
|
|
||||||
// Treat image paste as a drop of an image file.
|
|
||||||
var clipboardData = event.clipboardData,
|
|
||||||
items = clipboardData && clipboardData.items,
|
|
||||||
fireDrop = false,
|
|
||||||
hasImage = false,
|
|
||||||
l, type;
|
|
||||||
if ( items ) {
|
|
||||||
l = items.length;
|
|
||||||
while ( l-- ) {
|
|
||||||
type = items[l].type;
|
|
||||||
if ( type === 'text/html' ) {
|
|
||||||
hasImage = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if ( /^image\/.*/.test( type ) ) {
|
|
||||||
hasImage = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( hasImage ) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.fireEvent( 'dragover', {
|
|
||||||
dataTransfer: clipboardData,
|
|
||||||
/*jshint loopfunc: true */
|
|
||||||
preventDefault: function () {
|
|
||||||
fireDrop = true;
|
|
||||||
}
|
|
||||||
/*jshint loopfunc: false */
|
|
||||||
});
|
|
||||||
if ( fireDrop ) {
|
|
||||||
this.fireEvent( 'drop', {
|
|
||||||
dataTransfer: clipboardData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._awaitingPaste = true;
|
|
||||||
|
|
||||||
var self = this,
|
|
||||||
body = this._body,
|
|
||||||
range = this.getSelection(),
|
|
||||||
startContainer, startOffset, endContainer, endOffset, startBlock;
|
|
||||||
|
|
||||||
// Record undo checkpoint
|
|
||||||
self._recordUndoState( range );
|
|
||||||
self._getRangeAndRemoveBookmark( range );
|
|
||||||
|
|
||||||
// Note current selection. We must do this AFTER recording the undo
|
|
||||||
// checkpoint, as this modifies the DOM.
|
|
||||||
startContainer = range.startContainer;
|
|
||||||
startOffset = range.startOffset;
|
|
||||||
endContainer = range.endContainer;
|
|
||||||
endOffset = range.endOffset;
|
|
||||||
startBlock = getStartBlockOfRange( range );
|
|
||||||
|
|
||||||
// We need to position the pasteArea in the visible portion of the screen
|
|
||||||
// to stop the browser auto-scrolling.
|
|
||||||
var pasteArea = this.createElement( 'DIV', {
|
|
||||||
style: 'position: absolute; overflow: hidden; top:' +
|
|
||||||
( body.scrollTop +
|
|
||||||
( startBlock ? startBlock.getBoundingClientRect().top : 0 ) ) +
|
|
||||||
'px; right: 150%; width: 1px; height: 1px;'
|
|
||||||
});
|
|
||||||
body.appendChild( pasteArea );
|
|
||||||
range.selectNodeContents( pasteArea );
|
|
||||||
this.setSelection( range );
|
|
||||||
|
|
||||||
// A setTimeout of 0 means this is added to the back of the
|
|
||||||
// single javascript thread, so it will be executed after the
|
|
||||||
// paste event.
|
|
||||||
setTimeout( function () {
|
|
||||||
try {
|
|
||||||
// Get the pasted content and clean
|
|
||||||
var frag = self._doc.createDocumentFragment(),
|
|
||||||
next = pasteArea,
|
|
||||||
first, range;
|
|
||||||
|
|
||||||
// #88: Chrome can apparently split the paste area if certain
|
|
||||||
// content is inserted; gather them all up.
|
|
||||||
while ( pasteArea = next ) {
|
|
||||||
next = pasteArea.nextSibling;
|
|
||||||
frag.appendChild( empty( detach( pasteArea ) ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
first = frag.firstChild;
|
|
||||||
range = self._createRange(
|
|
||||||
startContainer, startOffset, endContainer, endOffset );
|
|
||||||
|
|
||||||
// Was anything actually pasted?
|
|
||||||
if ( first ) {
|
|
||||||
// Safari and IE like putting extra divs around things.
|
|
||||||
if ( first === frag.lastChild &&
|
|
||||||
first.nodeName === 'DIV' ) {
|
|
||||||
frag.replaceChild( empty( first ), first );
|
|
||||||
}
|
|
||||||
|
|
||||||
frag.normalize();
|
|
||||||
addLinks( frag );
|
|
||||||
cleanTree( frag, false );
|
|
||||||
cleanupBRs( frag );
|
|
||||||
removeEmptyInlines( frag );
|
|
||||||
|
|
||||||
var node = frag,
|
|
||||||
doPaste = true,
|
|
||||||
event = {
|
|
||||||
fragment: frag,
|
|
||||||
preventDefault: function () {
|
|
||||||
doPaste = false;
|
|
||||||
},
|
|
||||||
isDefaultPrevented: function () {
|
|
||||||
return !doPaste;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
while ( node = getNextBlock( node ) ) {
|
|
||||||
fixCursor( node );
|
|
||||||
}
|
|
||||||
|
|
||||||
self.fireEvent( 'willPaste', event );
|
|
||||||
|
|
||||||
// Insert pasted data
|
|
||||||
if ( doPaste ) {
|
|
||||||
insertTreeFragmentIntoRange( range, event.fragment );
|
|
||||||
if ( !canObserveMutations ) {
|
|
||||||
self._docWasChanged();
|
|
||||||
}
|
|
||||||
range.collapse( false );
|
|
||||||
self._ensureBottomLine();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.setSelection( range );
|
|
||||||
self._updatePath( range, true );
|
|
||||||
|
|
||||||
self._awaitingPaste = false;
|
|
||||||
} catch ( error ) {
|
|
||||||
self.didError( error );
|
|
||||||
}
|
|
||||||
}, 0 );
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Keyboard interaction ---
|
// --- Keyboard interaction ---
|
||||||
|
|
||||||
var keys = {
|
|
||||||
8: 'backspace',
|
|
||||||
9: 'tab',
|
|
||||||
13: 'enter',
|
|
||||||
32: 'space',
|
|
||||||
37: 'left',
|
|
||||||
39: 'right',
|
|
||||||
46: 'delete',
|
|
||||||
219: '[',
|
|
||||||
221: ']'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ref: http://unixpapa.com/js/key.html
|
|
||||||
proto._onKey = function ( event ) {
|
|
||||||
var code = event.keyCode,
|
|
||||||
key = keys[ code ],
|
|
||||||
modifiers = '',
|
|
||||||
range = this.getSelection();
|
|
||||||
|
|
||||||
if ( !key ) {
|
|
||||||
key = String.fromCharCode( code ).toLowerCase();
|
|
||||||
// Only reliable for letters and numbers
|
|
||||||
if ( !/^[A-Za-z0-9]$/.test( key ) ) {
|
|
||||||
key = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// On keypress, delete and '.' both have event.keyCode 46
|
|
||||||
// Must check event.which to differentiate.
|
|
||||||
if ( isPresto && event.which === 46 ) {
|
|
||||||
key = '.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function keys
|
|
||||||
if ( 111 < code && code < 124 ) {
|
|
||||||
key = 'f' + ( code - 111 );
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to apply the backspace/delete handlers regardless of
|
|
||||||
// control key modifiers.
|
|
||||||
if ( key !== 'backspace' && key !== 'delete' ) {
|
|
||||||
if ( event.altKey ) { modifiers += 'alt-'; }
|
|
||||||
if ( event.ctrlKey ) { modifiers += 'ctrl-'; }
|
|
||||||
if ( event.metaKey ) { modifiers += 'meta-'; }
|
|
||||||
}
|
|
||||||
// However, on Windows, shift-delete is apparently "cut" (WTF right?), so
|
|
||||||
// we want to let the browser handle shift-delete.
|
|
||||||
if ( event.shiftKey ) { modifiers += 'shift-'; }
|
|
||||||
|
|
||||||
key = modifiers + key;
|
|
||||||
|
|
||||||
if ( this._keyHandlers[ key ] ) {
|
|
||||||
this._keyHandlers[ key ]( this, event, range );
|
|
||||||
} else if ( key.length === 1 && !range.collapsed ) {
|
|
||||||
// Record undo checkpoint.
|
|
||||||
this._recordUndoState( range );
|
|
||||||
this._getRangeAndRemoveBookmark( range );
|
|
||||||
// Delete the selection
|
|
||||||
deleteContentsOfRange( range );
|
|
||||||
this._ensureBottomLine();
|
|
||||||
this.setSelection( range );
|
|
||||||
this._updatePath( range, true );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
proto.setKeyHandler = function ( key, fn ) {
|
proto.setKeyHandler = function ( key, fn ) {
|
||||||
this._keyHandlers[ key ] = fn;
|
this._keyHandlers[ key ] = fn;
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -1,5 +1,70 @@
|
||||||
/*jshint strict:false, undef:false, unused:false */
|
/*jshint strict:false, undef:false, unused:false */
|
||||||
|
|
||||||
|
var keys = {
|
||||||
|
8: 'backspace',
|
||||||
|
9: 'tab',
|
||||||
|
13: 'enter',
|
||||||
|
32: 'space',
|
||||||
|
37: 'left',
|
||||||
|
39: 'right',
|
||||||
|
46: 'delete',
|
||||||
|
219: '[',
|
||||||
|
221: ']'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ref: http://unixpapa.com/js/key.html
|
||||||
|
var onKey = function ( event ) {
|
||||||
|
var code = event.keyCode,
|
||||||
|
key = keys[ code ],
|
||||||
|
modifiers = '',
|
||||||
|
range = this.getSelection();
|
||||||
|
|
||||||
|
if ( !key ) {
|
||||||
|
key = String.fromCharCode( code ).toLowerCase();
|
||||||
|
// Only reliable for letters and numbers
|
||||||
|
if ( !/^[A-Za-z0-9]$/.test( key ) ) {
|
||||||
|
key = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On keypress, delete and '.' both have event.keyCode 46
|
||||||
|
// Must check event.which to differentiate.
|
||||||
|
if ( isPresto && event.which === 46 ) {
|
||||||
|
key = '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function keys
|
||||||
|
if ( 111 < code && code < 124 ) {
|
||||||
|
key = 'f' + ( code - 111 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to apply the backspace/delete handlers regardless of
|
||||||
|
// control key modifiers.
|
||||||
|
if ( key !== 'backspace' && key !== 'delete' ) {
|
||||||
|
if ( event.altKey ) { modifiers += 'alt-'; }
|
||||||
|
if ( event.ctrlKey ) { modifiers += 'ctrl-'; }
|
||||||
|
if ( event.metaKey ) { modifiers += 'meta-'; }
|
||||||
|
}
|
||||||
|
// However, on Windows, shift-delete is apparently "cut" (WTF right?), so
|
||||||
|
// we want to let the browser handle shift-delete.
|
||||||
|
if ( event.shiftKey ) { modifiers += 'shift-'; }
|
||||||
|
|
||||||
|
key = modifiers + key;
|
||||||
|
|
||||||
|
if ( this._keyHandlers[ key ] ) {
|
||||||
|
this._keyHandlers[ key ]( this, event, range );
|
||||||
|
} else if ( key.length === 1 && !range.collapsed ) {
|
||||||
|
// Record undo checkpoint.
|
||||||
|
this._recordUndoState( range );
|
||||||
|
this._getRangeAndRemoveBookmark( range );
|
||||||
|
// Delete the selection
|
||||||
|
deleteContentsOfRange( range );
|
||||||
|
this._ensureBottomLine();
|
||||||
|
this.setSelection( range );
|
||||||
|
this._updatePath( range, true );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var mapKeyTo = function ( method ) {
|
var mapKeyTo = function ( method ) {
|
||||||
return function ( self, event ) {
|
return function ( self, event ) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
Loading…
Reference in a new issue