0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-22 15:23:29 -05:00
Squire/source/Clipboard.js
2016-12-11 12:32:25 +11:00

287 lines
10 KiB
JavaScript

/*jshint strict:false, undef:false, unused:false */
// The (non-standard but supported enough) innerText property is based on the
// render tree in Firefox and possibly other browsers, so we must insert the
// DOM node into the document to ensure the text part is correct.
var setClipboardData = function ( clipboardData, node, root ) {
var body = node.ownerDocument.body;
var html, text;
// Firefox will add an extra new line for BRs at the end of block when
// calculating innerText, even though they don't actually affect display.
// So we need to remove them first.
cleanupBRs( node, root );
node.setAttribute( 'style',
'position:fixed;overflow:hidden;bottom:100%;right:100%;' );
body.appendChild( node );
html = node.innerHTML;
text = node.innerText || node.textContent;
// Firefox (and others?) returns unix line endings (\n) even on Windows.
// If on Windows, normalise to \r\n, since Notepad and some other crappy
// apps do not understand just \n.
if ( isWin ) {
text = text.replace( /\r?\n/g, '\r\n' );
}
clipboardData.setData( 'text/html', html );
clipboardData.setData( 'text/plain', text );
body.removeChild( node );
};
var onCut = function ( event ) {
var clipboardData = event.clipboardData;
var range = this.getSelection();
var node = this.createElement( 'div' );
var root = this._root;
var self = this;
// Save undo checkpoint
this.saveUndoState( range );
// Edge only seems to support setting plain text as of 2016-03-11.
// Mobile Safari flat out doesn't work:
// https://bugs.webkit.org/show_bug.cgi?id=143776
if ( !isEdge && !isIOS && clipboardData ) {
moveRangeBoundariesUpTree( range, root );
node.appendChild( deleteContentsOfRange( range, root ) );
setClipboardData( clipboardData, node, root );
event.preventDefault();
} else {
setTimeout( function () {
try {
// If all content removed, ensure div at start of root.
self._ensureBottomLine();
} catch ( error ) {
self.didError( error );
}
}, 0 );
}
this.setSelection( range );
};
var onCopy = function ( event ) {
var clipboardData = event.clipboardData;
var range = this.getSelection();
var node = this.createElement( 'div' );
var root = this._root;
var startBlock, endBlock, copyRoot, contents, parent, newContents;
// Edge only seems to support setting plain text as of 2016-03-11.
// Mobile Safari flat out doesn't work:
// https://bugs.webkit.org/show_bug.cgi?id=143776
if ( !isEdge && !isIOS && clipboardData ) {
range = range.cloneRange();
startBlock = getStartBlockOfRange( range, root );
endBlock = getEndBlockOfRange( range, root );
copyRoot = ( ( startBlock === endBlock ) && startBlock ) || root;
moveRangeBoundariesDownTree( range );
moveRangeBoundariesUpTree( range, copyRoot );
contents = range.cloneContents();
parent = range.commonAncestorContainer;
if ( parent.nodeType === TEXT_NODE ) {
parent = parent.parentNode;
}
while ( parent && parent !== copyRoot ) {
newContents = parent.cloneNode( false );
newContents.appendChild( contents );
contents = newContents;
parent = parent.parentNode;
}
node.appendChild( contents );
setClipboardData( clipboardData, node, root );
event.preventDefault();
}
};
// Need to monitor for shift key like this, as event.shiftKey is not available
// in paste event.
function monitorShiftKey ( event ) {
this.isShiftDown = event.shiftKey;
}
var onPaste = function ( event ) {
var clipboardData = event.clipboardData;
var items = clipboardData && clipboardData.items;
var choosePlain = this.isShiftDown;
var fireDrop = false;
var hasImage = false;
var plainItem = null;
var self = this;
var l, item, type, types, data;
// Current HTML5 Clipboard interface
// ---------------------------------
// https://html.spec.whatwg.org/multipage/interaction.html
// Edge only provides access to plain text as of 2016-03-11.
if ( !isEdge && items ) {
event.preventDefault();
l = items.length;
while ( l-- ) {
item = items[l];
type = item.type;
if ( !choosePlain && type === 'text/html' ) {
/*jshint loopfunc: true */
item.getAsString( function ( html ) {
self.insertHTML( html, true );
});
/*jshint loopfunc: false */
return;
}
if ( type === 'text/plain' ) {
plainItem = item;
}
if ( !choosePlain && /^image\/.*/.test( type ) ) {
hasImage = true;
}
}
// Treat image paste as a drop of an image file.
if ( hasImage ) {
this.fireEvent( 'dragover', {
dataTransfer: clipboardData,
/*jshint loopfunc: true */
preventDefault: function () {
fireDrop = true;
}
/*jshint loopfunc: false */
});
if ( fireDrop ) {
this.fireEvent( 'drop', {
dataTransfer: clipboardData
});
}
} else if ( plainItem ) {
plainItem.getAsString( function ( text ) {
self.insertPlainText( text, true );
});
}
return;
}
// Old interface
// -------------
// Safari (and indeed many other OS X apps) copies stuff as text/rtf
// rather than text/html; even from a webpage in Safari. The only way
// to get an HTML version is to fallback to letting the browser insert
// the content. Same for getting image data. *Sigh*.
//
// Firefox is even worse: it doesn't even let you know that there might be
// an RTF version on the clipboard, but it will also convert to HTML if you
// let the browser insert the content. I've filed
// https://bugzilla.mozilla.org/show_bug.cgi?id=1254028
types = clipboardData && clipboardData.types;
if ( !isEdge && types && (
indexOf.call( types, 'text/html' ) > -1 || (
!isGecko &&
indexOf.call( types, 'text/plain' ) > -1 &&
indexOf.call( types, 'text/rtf' ) < 0 )
)) {
event.preventDefault();
// Abiword on Linux copies a plain text and html version, but the HTML
// version is the empty string! So always try to get HTML, but if none,
// insert plain text instead. On iOS, Facebook (and possibly other
// apps?) copy links as type text/uri-list, but also insert a **blank**
// text/plain item onto the clipboard. Why? Who knows.
if ( !choosePlain && ( data = clipboardData.getData( 'text/html' ) ) ) {
this.insertHTML( data, true );
} else if (
( data = clipboardData.getData( 'text/plain' ) ) ||
( data = clipboardData.getData( 'text/uri-list' ) ) ) {
this.insertPlainText( data, true );
}
return;
}
// No interface. Includes all versions of IE :(
// --------------------------------------------
this._awaitingPaste = true;
var body = this._doc.body,
range = this.getSelection(),
startContainer = range.startContainer,
startOffset = range.startOffset,
endContainer = range.endContainer,
endOffset = range.endOffset;
// We need to position the pasteArea in the visible portion of the screen
// to stop the browser auto-scrolling.
var pasteArea = this.createElement( 'DIV', {
contenteditable: 'true',
style: 'position:fixed; overflow:hidden; top:0; right:100%; 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 {
// IE sometimes fires the beforepaste event twice; make sure it is
// not run again before our after paste function is called.
self._awaitingPaste = false;
// Get the pasted content and clean
var html = '',
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;
detach( pasteArea );
// Safari and IE like putting extra divs around things.
first = pasteArea.firstChild;
if ( first && first === pasteArea.lastChild &&
first.nodeName === 'DIV' ) {
pasteArea = first;
}
html += pasteArea.innerHTML;
}
range = self._createRange(
startContainer, startOffset, endContainer, endOffset );
self.setSelection( range );
if ( html ) {
self.insertHTML( html, true );
}
} catch ( error ) {
self.didError( error );
}
}, 0 );
};
// On Windows you can drag an drop text. We can't handle this ourselves, because
// as far as I can see, there's no way to get the drop insertion point. So just
// save an undo state and hope for the best.
var onDrop = function ( event ) {
var types = event.dataTransfer.types;
var l = types.length;
var hasPlain = false;
var hasHTML = false;
while ( l-- ) {
switch ( types[l] ) {
case 'text/plain':
hasPlain = true;
break;
case 'text/html':
hasHTML = true;
break;
default:
return;
}
}
if ( hasHTML || hasPlain ) {
this.saveUndoState();
}
};