897 lines
27 KiB
JavaScript
897 lines
27 KiB
JavaScript
/* -*- js-indent-level: 8 -*- */
|
|
/*
|
|
* L.Clipboard is used to abstract our storage and management of
|
|
* local & remote clipboard data.
|
|
*/
|
|
/* global app _ vex brandProductName isAnyVexDialogActive $ */
|
|
|
|
// Get all interesting clipboard related events here, and handle
|
|
// download logic in one place ...
|
|
// We keep track of the current selection content if it is simple
|
|
// So we can do synchronous copy/paste in the callback if possible.
|
|
L.Clipboard = L.Class.extend({
|
|
initialize: function(map) {
|
|
this._map = map;
|
|
this._selectionContent = '';
|
|
this._selectionType = null;
|
|
this._accessKey = [ '', '' ];
|
|
this._clipboardSerial = 0; // incremented on each operation
|
|
this._failedTimer = null;
|
|
this._dummyDivName = 'copy-paste-container';
|
|
this._unoCommandForCopyCutPaste = null;
|
|
|
|
var div = document.createElement('div');
|
|
this._dummyDiv = div;
|
|
|
|
div.setAttribute('id', this._dummyDivName);
|
|
div.setAttribute('style', 'user-select: text !important');
|
|
div.style.opacity = '0';
|
|
div.setAttribute('contenteditable', 'true');
|
|
div.setAttribute('type', 'text');
|
|
div.setAttribute('style', 'position: fixed; left: 0px; top: -200px; width: 15000px; height: 200px; ' +
|
|
'overflow: hidden; z-index: -1000, -webkit-user-select: text !important; display: block; ' +
|
|
'font-size: 6pt">');
|
|
|
|
// so we get events to where we want them.
|
|
var parent = document.getElementById('map');
|
|
parent.appendChild(div);
|
|
|
|
// sensible default content.
|
|
this._resetDiv();
|
|
|
|
var that = this;
|
|
var beforeSelect = function(ev) { return that._beforeSelect(ev); };
|
|
|
|
document.oncut = function(ev) { return that.cut(ev); };
|
|
document.oncopy = function(ev) { return that.copy(ev); };
|
|
document.onpaste = function(ev) { return that.paste(ev); };
|
|
document.onbeforecut = beforeSelect;
|
|
document.onbeforecopy = beforeSelect;
|
|
document.onbeforepaste = beforeSelect;
|
|
},
|
|
|
|
// We can do a much better job when we fetch text/plain too.
|
|
stripHTML: function(html) {
|
|
var tmp = document.createElement('div');
|
|
tmp.innerHTML = html;
|
|
// attempt to cleanup unwanted elements
|
|
var styles = tmp.querySelectorAll('style');
|
|
for (var i = 0; i < styles.length; i++) {
|
|
styles[i].parentNode.removeChild(styles[i]);
|
|
}
|
|
return tmp.textContent.trim() || tmp.innerText.trim() || '';
|
|
},
|
|
|
|
setKey: function(key) {
|
|
if (this._accessKey[0] === key)
|
|
return;
|
|
this._accessKey[1] = this._accessKey[0];
|
|
this._accessKey[0] = key;
|
|
},
|
|
|
|
getMetaBase: function() {
|
|
return window.makeHttpUrl('');
|
|
},
|
|
|
|
getMetaPath: function(idx) {
|
|
if (!idx)
|
|
idx = 0;
|
|
return '/cool/clipboard?WOPISrc=' + encodeURIComponent(this._map.options.doc) +
|
|
'&ServerId=' + app.socket.WSDServer.Id +
|
|
'&ViewId=' + this._map._docLayer._viewId +
|
|
'&Tag=' + this._accessKey[idx];
|
|
},
|
|
|
|
getMetaURL: function(idx) {
|
|
return this.getMetaBase() + this.getMetaPath(idx);
|
|
},
|
|
|
|
// Returns the marker used to identify stub messages.
|
|
_getHtmlStubMarker: function() {
|
|
return '<title>Stub HTML Message</title>';
|
|
},
|
|
|
|
// Returns true if the argument is a stub html.
|
|
_isStubHtml: function(text) {
|
|
return text.indexOf(this._getHtmlStubMarker()) > 0;
|
|
},
|
|
|
|
// wrap some content with our stub magic
|
|
_originWrapBody: function(body, isStub) {
|
|
var encodedOrigin = encodeURIComponent(this.getMetaURL());
|
|
var text = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\n' +
|
|
'<html>\n' +
|
|
' <head>\n';
|
|
if (isStub)
|
|
text += ' ' + this._getHtmlStubMarker() + '\n';
|
|
text += ' <meta http-equiv="content-type" content="text/html; charset=utf-8"/>\n' +
|
|
' <meta name="origin" content="' + encodedOrigin + '"/>\n' +
|
|
' </head>\n'
|
|
+ body +
|
|
'</html>';
|
|
return text;
|
|
},
|
|
|
|
// what an empty clipboard has on it
|
|
_getStubHtml: function() {
|
|
var lang = 'en_US'; // FIXME: l10n
|
|
return this._substProductName(this._originWrapBody(
|
|
' <body lang="' + lang + '" dir="ltr">\n' +
|
|
' <p>' + _('To paste outside %productName, please first click the \'download\' button') + '</p>\n' +
|
|
' </body>\n', true
|
|
));
|
|
},
|
|
|
|
// used for DisableCopy mode to fill the clipboard
|
|
_getDisabledCopyStubHtml: function() {
|
|
var lang = 'en_US'; // FIXME: l10n
|
|
return this._substProductName(this._originWrapBody(
|
|
' <body lang="' + lang + '" dir="ltr">\n' +
|
|
' <p>' + _('Copying from the document disabled') + '</p>\n' +
|
|
' </body>\n', true
|
|
));
|
|
},
|
|
|
|
_getMetaOrigin: function (html) {
|
|
var match = '<meta name="origin" content="';
|
|
var start = html.indexOf(match);
|
|
if (start < 0) {
|
|
return '';
|
|
}
|
|
var end = html.indexOf('"', start + match.length);
|
|
if (end < 0) {
|
|
return '';
|
|
}
|
|
var meta = html.substring(start + match.length, end);
|
|
|
|
// quick sanity checks that it one of ours.
|
|
if (meta.indexOf('%2Fclipboard%3FWOPISrc%3D') >= 0 &&
|
|
meta.indexOf('%26ServerId%3D') > 0 &&
|
|
meta.indexOf('%26ViewId%3D') > 0 &&
|
|
meta.indexOf('%26Tag%3D') > 0)
|
|
return decodeURIComponent(meta);
|
|
else
|
|
console.log('Mis-understood foreign origin: "' + meta + '"');
|
|
return '';
|
|
},
|
|
|
|
_encodeHtmlToBlob: function(text) {
|
|
var content = [];
|
|
var data = new Blob([text]);
|
|
content.push('text/html\n');
|
|
content.push(data.size.toString(16) + '\n');
|
|
content.push(data);
|
|
content.push('\n');
|
|
return new Blob(content);
|
|
},
|
|
|
|
_readContentSyncToBlob: function(dataTransfer) {
|
|
var content = [];
|
|
var types = dataTransfer.types;
|
|
for (var t = 0; t < types.length; ++t) {
|
|
if (types[t] === 'Files')
|
|
continue; // images handled elsewhere.
|
|
var dataStr = dataTransfer.getData(types[t]);
|
|
// Avoid types that has no content.
|
|
if (!dataStr.length)
|
|
continue;
|
|
var data = new Blob([dataStr]);
|
|
console.log('type ' + types[t] + ' length ' + data.size +
|
|
' -> 0x' + data.size.toString(16) + '\n');
|
|
content.push((types[t] === 'text' ? 'text/plain' : types[t]) + '\n');
|
|
content.push(data.size.toString(16) + '\n');
|
|
content.push(data);
|
|
content.push('\n');
|
|
}
|
|
if (content.length > 0)
|
|
return new Blob(content, {type : 'application/octet-stream', endings: 'transparent'});
|
|
else
|
|
return null;
|
|
},
|
|
|
|
// Abstract async post & download for our progress wrappers
|
|
// type: GET or POST
|
|
// url: where to get / send the data
|
|
// optionalFormData: used for POST for form data
|
|
// forClipboard: a boolean telling if we need the "Confirm copy to clipboard" link in the end
|
|
// completeFn: called on completion - with response.
|
|
// progressFn: allows splitting the progress bar up.
|
|
_doAsyncDownload: function(type,url,optionalFormData,forClipboard,completeFn,progressFn,onErrorFn) {
|
|
try {
|
|
var that = this;
|
|
var request = new XMLHttpRequest();
|
|
|
|
// avoid to invoke the following code if the download widget depends on user interaction
|
|
if (!that._downloadProgress || !that._downloadProgress.isVisible()) {
|
|
that._startProgress();
|
|
that._downloadProgress.startProgressMode();
|
|
}
|
|
request.onload = function() {
|
|
that._downloadProgress._onComplete();
|
|
if (!forClipboard) {
|
|
that._downloadProgress._onClose();
|
|
}
|
|
|
|
// For some reason 400 error from the server doesn't
|
|
// invoke onerror callback, but we do get here with
|
|
// size==0, which signifies no response from the server.
|
|
// So we check the status code instead.
|
|
if (this.status == 200) {
|
|
completeFn(this.response);
|
|
} else if (onErrorFn) {
|
|
onErrorFn(this.response);
|
|
}
|
|
};
|
|
request.onerror = function() {
|
|
if (onErrorFn)
|
|
onErrorFn();
|
|
that._downloadProgress._onComplete();
|
|
that._downloadProgress._onClose();
|
|
};
|
|
|
|
request.upload.addEventListener('progress', function (e) {
|
|
if (e.lengthComputable) {
|
|
var percent = progressFn(e.loaded / e.total * 100);
|
|
var progress = { statusType: 'setvalue', value: percent };
|
|
that._downloadProgress._onUpdateProgress(progress);
|
|
}
|
|
}, false);
|
|
request.open(type, url, true /* isAsync */);
|
|
request.timeout = 20 * 1000; // 20 secs ...
|
|
request.responseType = 'blob';
|
|
if (optionalFormData !== null)
|
|
request.send(optionalFormData);
|
|
else
|
|
request.send();
|
|
} catch (error) {
|
|
if (onErrorFn)
|
|
onErrorFn();
|
|
}
|
|
},
|
|
|
|
// Suck the data from one server to another asynchronously ...
|
|
_dataTransferDownloadAndPasteAsync: function(src, dest, fallbackHtml) {
|
|
var that = this;
|
|
// FIXME: add a timestamp in the links (?) ignore old / un-responsive servers (?)
|
|
that._doAsyncDownload(
|
|
'GET', src, null, false,
|
|
function(response) {
|
|
console.log('download done - response ' + response);
|
|
var formData = new FormData();
|
|
formData.append('data', response, 'clipboard');
|
|
that._doAsyncDownload(
|
|
'POST', dest, formData, false,
|
|
function() {
|
|
if (this.pasteSpecialVex && this.pasteSpecialVex.isOpen) {
|
|
vex.close(this.pasteSpecialVex);
|
|
console.log('up-load done, now paste special');
|
|
app.socket.sendMessage('uno .uno:PasteSpecial');
|
|
} else {
|
|
console.log('up-load done, now paste');
|
|
app.socket.sendMessage('uno .uno:Paste');
|
|
}
|
|
|
|
},
|
|
function(progress) { return 50 + progress/2; }
|
|
);
|
|
},
|
|
function(progress) { return progress/2; },
|
|
function() {
|
|
console.log('failed to download clipboard using fallback html');
|
|
|
|
// If it's the stub, avoid pasting.
|
|
if (that._isStubHtml(fallbackHtml))
|
|
{
|
|
// Let the user know they haven't really copied document content.
|
|
vex.dialog.alert({
|
|
message: _('Failed to download clipboard, please re-copy'),
|
|
callback: function () {
|
|
that._map.focus();
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
var formData = new FormData();
|
|
formData.append('data', new Blob([fallbackHtml]), 'clipboard');
|
|
that._doAsyncDownload(
|
|
'POST', dest, formData, false,
|
|
function() {
|
|
if (this.pasteSpecialVex && this.pasteSpecialVex.isOpen) {
|
|
vex.close(this.pasteSpecialVex);
|
|
console.log('up-load of fallback done, now paste special');
|
|
app.socket.sendMessage('uno .uno:PasteSpecial');
|
|
} else {
|
|
console.log('up-load of fallback done, now paste');
|
|
app.socket.sendMessage('uno .uno:Paste');
|
|
}
|
|
|
|
},
|
|
function(progress) { return 50 + progress/2; },
|
|
function() {
|
|
that.dataTransferToDocumentFallback(null, fallbackHtml);
|
|
}
|
|
);
|
|
}
|
|
);
|
|
},
|
|
|
|
_onFileLoadFunc: function(file) {
|
|
var socket = app.socket;
|
|
return function(e) {
|
|
var blob = new Blob(['paste mimetype=' + file.type + '\n', e.target.result]);
|
|
socket.sendMessage(blob);
|
|
};
|
|
},
|
|
|
|
_asyncReadPasteImage: function(file) {
|
|
if (file.type.match(/image.*/)) {
|
|
var reader = new FileReader();
|
|
reader.onload = this._onFileLoadFunc(file);
|
|
reader.readAsArrayBuffer(file);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// Returns true if it finished synchronously, and false if it have started an async operation
|
|
// that will likely end at a later time (required to avoid closing progress bar in paste(ev))
|
|
dataTransferToDocument: function (dataTransfer, preferInternal, htmlText, usePasteKeyEvent) {
|
|
// Look for our HTML meta magic.
|
|
// cf. ClientSession.cpp /textselectioncontent:/
|
|
|
|
var meta = this._getMetaOrigin(htmlText);
|
|
var id = this.getMetaPath(0);
|
|
var idOld = this.getMetaPath(1);
|
|
|
|
// for the paste, we always prefer the internal LOK's copy/paste
|
|
if (preferInternal === true &&
|
|
(meta.indexOf(id) >= 0 || meta.indexOf(idOld) >= 0))
|
|
{
|
|
// Home from home: short-circuit internally.
|
|
console.log('short-circuit, internal paste');
|
|
this._doInternalPaste(this._map, usePasteKeyEvent);
|
|
return true;
|
|
}
|
|
|
|
// Do we have a remote Online we can suck rich data from ?
|
|
if (meta !== '')
|
|
{
|
|
console.log('Transfer between servers\n\t"' + meta + '" vs. \n\t"' + id + '"');
|
|
this._dataTransferDownloadAndPasteAsync(meta, this.getMetaURL(), htmlText);
|
|
return false; // just started async operation - did not finish yet
|
|
}
|
|
|
|
// Fallback.
|
|
this.dataTransferToDocumentFallback(dataTransfer, htmlText, usePasteKeyEvent);
|
|
return true;
|
|
},
|
|
|
|
dataTransferToDocumentFallback: function(dataTransfer, htmlText, usePasteKeyEvent) {
|
|
|
|
var content;
|
|
if (dataTransfer) {
|
|
// Suck HTML content out of dataTransfer now while it feels like working.
|
|
content = this._readContentSyncToBlob(dataTransfer);
|
|
}
|
|
|
|
// Fallback on the html.
|
|
if (!content && htmlText != '') {
|
|
content = this._encodeHtmlToBlob(htmlText);
|
|
}
|
|
|
|
// FIXME: do we want this section ?
|
|
|
|
// Images get a look in only if we have no content and are async
|
|
var htmlImage = htmlText.substring(0, 4) === '<img';
|
|
if (((content == null && htmlText === '') || htmlImage) && dataTransfer != null)
|
|
{
|
|
var types = dataTransfer.types;
|
|
|
|
console.log('Attempting to paste image(s)');
|
|
|
|
// first try to transfer images
|
|
// TODO if we have both Files and a normal mimetype, should we handle
|
|
// both, or prefer one or the other?
|
|
for (var t = 0; t < types.length; ++t) {
|
|
console.log('\ttype' + types[t]);
|
|
if (types[t] === 'Files') {
|
|
var files = dataTransfer.files;
|
|
if (files !== null)
|
|
{
|
|
for (var f = 0; f < files.length; ++f)
|
|
this._asyncReadPasteImage(files[f]);
|
|
}
|
|
else // IE / Edge
|
|
this._asyncReadPasteImage(dataTransfer.items[t].getAsFile());
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (content != null) {
|
|
console.log('Normal HTML, so smart paste not possible');
|
|
|
|
var formData = new FormData();
|
|
formData.append('file', content);
|
|
|
|
var that = this;
|
|
this._doAsyncDownload('POST', this.getMetaURL(), formData, false,
|
|
function() {
|
|
console.log('Posted ' + content.size + ' bytes successfully');
|
|
that._doInternalPaste(that._map, usePasteKeyEvent);
|
|
},
|
|
function(progress) { return progress; }
|
|
);
|
|
} else {
|
|
console.log('Nothing we can paste on the clipboard');
|
|
}
|
|
},
|
|
|
|
_checkSelection: function() {
|
|
var checkSelect = document.getSelection();
|
|
if (checkSelect && checkSelect.isCollapsed)
|
|
console.log('Error: collapsed selection - cannot copy/paste');
|
|
},
|
|
|
|
_getHtmlForClipboard: function() {
|
|
var text;
|
|
|
|
if ($('.ui-edit').is(':focus'))
|
|
return $('.ui-edit').value();
|
|
|
|
if ($('.w2ui-input').is(':focus'))
|
|
return $('.w2ui-input').value();
|
|
|
|
if (this._selectionType === 'complex' ||
|
|
this._map._docLayer.hasGraphicSelection()) {
|
|
console.log('Copy/Cut with complex/graphical selection');
|
|
if (this._selectionType === 'text' && this._selectionContent !== '')
|
|
{ // back here again having downloaded it ...
|
|
text = this._selectionContent;
|
|
console.log('Use downloaded selection.');
|
|
}
|
|
else
|
|
{
|
|
console.log('Downloaded that selection.');
|
|
text = this._getStubHtml();
|
|
this._onDownloadOnLargeCopyPaste();
|
|
this._downloadProgress.setURI( // richer, bigger HTML ...
|
|
this.getMetaURL() + '&MimeType=text/html');
|
|
}
|
|
} else if (this._selectionType === null) {
|
|
console.log('Copy/Cut with no selection!');
|
|
text = this._getStubHtml();
|
|
} else {
|
|
console.log('Copy/Cut with simple text selection');
|
|
text = this._selectionContent;
|
|
}
|
|
return text;
|
|
},
|
|
|
|
// returns whether we shold stop processing the event
|
|
populateClipboard: function(ev) {
|
|
this._checkSelection();
|
|
|
|
var text = this._getHtmlForClipboard();
|
|
//this._stopHideDownload(); - this confuses the browser ruins copy/cut on iOS
|
|
|
|
var plainText = this.stripHTML(text);
|
|
if (ev.clipboardData) { // Standard
|
|
if (this._unoCommandForCopyCutPaste === '.uno:CopyHyperlinkLocation') {
|
|
var ess = 's';
|
|
var re = new RegExp('^(.*)(<a href=")([^"]+)(">.*</a>)(</p>\n</body>\n</html>)$', ess);
|
|
var match = re.exec(text);
|
|
if (match !== null && match.length === 6) {
|
|
text = match[1] + match[3] + match[5];
|
|
plainText = this.stripHTML(text);
|
|
}
|
|
}
|
|
// if copied content is graphical then plainText is null and it does not work on mobile.
|
|
ev.clipboardData.setData('text/plain', plainText ? plainText: ' ');
|
|
ev.clipboardData.setData('text/html', text);
|
|
console.log('Put "' + text + '" on the clipboard');
|
|
this._clipboardSerial++;
|
|
}
|
|
|
|
return true; // prevent default
|
|
},
|
|
|
|
_isAnyInputFieldSelected: function() {
|
|
if ($('#search-input').is(':focus'))
|
|
return true;
|
|
|
|
if ($('.ui-edit').is(':focus'))
|
|
return true;
|
|
|
|
if ($('.w2ui-input').is(':focus'))
|
|
return true;
|
|
|
|
if (isAnyVexDialogActive() && !(this.pasteSpecialVex && this.pasteSpecialVex.isOpen))
|
|
return true;
|
|
|
|
if ($('.annotation-active').length)
|
|
return true;
|
|
|
|
return false;
|
|
},
|
|
|
|
// Does the selection of text before an event comes in
|
|
_beforeSelect: function(ev) {
|
|
console.log('Got event ' + ev.type + ' setting up selection');
|
|
|
|
if (this._isAnyInputFieldSelected())
|
|
return;
|
|
|
|
this._beforeSelectImpl();
|
|
},
|
|
|
|
_beforeSelectImpl: function() {
|
|
// We need some spaces in there ...
|
|
this._resetDiv();
|
|
|
|
var sel = document.getSelection();
|
|
if (!sel)
|
|
return;
|
|
|
|
var selected = false;
|
|
var selectRange;
|
|
|
|
if (!selected)
|
|
{
|
|
sel.removeAllRanges();
|
|
selectRange = document.createRange();
|
|
selectRange.selectNodeContents(this._dummyDiv);
|
|
sel.addRange(selectRange);
|
|
|
|
var checkSelect = document.getSelection();
|
|
if (checkSelect.isCollapsed)
|
|
console.log('Error: failed to select - cannot copy/paste');
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
_resetDiv: function() {
|
|
// cleanup the content:
|
|
this._dummyDiv.innerHTML =
|
|
'<b style="font-weight:normal; background-color: transparent; color: transparent;"><span> </span></b>';
|
|
},
|
|
|
|
// Try-harder fallbacks for emitting cut/copy/paste events.
|
|
_execOnElement: function(operation) {
|
|
var serial = this._clipboardSerial;
|
|
|
|
this._resetDiv();
|
|
|
|
var success = false;
|
|
var active = null;
|
|
|
|
// selection can change focus.
|
|
active = document.activeElement;
|
|
|
|
success = (document.execCommand(operation) &&
|
|
serial !== this._clipboardSerial);
|
|
|
|
// try to restore focus if we need to.
|
|
if (active !== null && active !== document.activeElement)
|
|
active.focus();
|
|
|
|
console.log('fallback ' + operation + ' ' + (success?'success':'fail'));
|
|
|
|
return success;
|
|
},
|
|
|
|
// Encourage browser(s) to actually execute the command
|
|
_execCopyCutPaste: function(operation, cmd) {
|
|
var serial = this._clipboardSerial;
|
|
|
|
this._unoCommandForCopyCutPaste = cmd;
|
|
if (document.execCommand(operation) &&
|
|
serial !== this._clipboardSerial) {
|
|
console.log('copied successfully');
|
|
this._unoCommandForCopyCutPaste = null;
|
|
return;
|
|
}
|
|
this._unoCommandForCopyCutPaste = null;
|
|
|
|
// try a hidden div
|
|
if (this._execOnElement(operation)) {
|
|
console.log('copied on element successfully');
|
|
return;
|
|
}
|
|
|
|
// see if we have help for paste
|
|
if (operation === 'paste')
|
|
{
|
|
try {
|
|
console.warn('Asked parent for a paste event');
|
|
this._map.fire('postMessage', {msgId: 'UI_Paste'});
|
|
} catch (error) {
|
|
console.warn('Failed to post-message: ' + error);
|
|
}
|
|
}
|
|
|
|
// wait and see if we get some help
|
|
var that = this;
|
|
clearTimeout(this._failedTimer);
|
|
setTimeout(function() {
|
|
if (that._clipboardSerial !== serial)
|
|
{
|
|
console.log('successful ' + operation);
|
|
if (operation === 'paste')
|
|
that._stopHideDownload();
|
|
}
|
|
else
|
|
{
|
|
console.log('help did not arrive for ' + operation);
|
|
that._warnCopyPaste();
|
|
}
|
|
}, 150 /* ms */);
|
|
},
|
|
|
|
// Pull UNO clipboard commands out from menus and normal user input.
|
|
// We try to massage and re-emit these, to get good security event / credentials.
|
|
filterExecCopyPaste: function(cmd) {
|
|
if (window.ThisIsAMobileApp) {
|
|
// We do native copy/paste in the iOS and Android cases
|
|
return false;
|
|
}
|
|
|
|
if (this._map['wopi'].DisableCopy && (cmd === '.uno:Copy' || cmd === '.uno:Cut')) {
|
|
// perform internal operations
|
|
app.socket.sendMessage('uno ' + cmd);
|
|
return true;
|
|
}
|
|
|
|
if (cmd === '.uno:Copy' || (L.Browser.mobile && L.Browser.safari && cmd === '.uno:CopyHyperlinkLocation')) {
|
|
this._execCopyCutPaste('copy', cmd);
|
|
} else if (cmd === '.uno:Cut') {
|
|
this._execCopyCutPaste('cut', cmd);
|
|
} else if (cmd === '.uno:Paste') {
|
|
this._execCopyCutPaste('paste', cmd);
|
|
} else {
|
|
return false;
|
|
}
|
|
console.log('filtered uno command ' + cmd);
|
|
return true;
|
|
},
|
|
|
|
_doCopyCut: function(ev, unoName) {
|
|
console.log(unoName);
|
|
|
|
if (this._isAnyInputFieldSelected())
|
|
return;
|
|
|
|
if (this._downloadProgressStatus() === 'downloadButton')
|
|
this._stopHideDownload(); // Terminate pending confirmation
|
|
|
|
var preventDefault = true;
|
|
|
|
if (this._map['wopi'].DisableCopy === true)
|
|
{
|
|
var text = this._getDisabledCopyStubHtml();
|
|
var plainText = this.stripHTML(text);
|
|
if (ev.clipboardData) {
|
|
console.log('Copying disabled: put stub message on the clipboard');
|
|
ev.clipboardData.setData('text/plain', plainText ? plainText: ' ');
|
|
ev.clipboardData.setData('text/html', text);
|
|
this._clipboardSerial++;
|
|
}
|
|
} else {
|
|
preventDefault = this.populateClipboard(ev);
|
|
}
|
|
|
|
app.socket.sendMessage('uno .uno:' + unoName);
|
|
if (preventDefault) {
|
|
ev.preventDefault();
|
|
return false;
|
|
}
|
|
},
|
|
|
|
_doInternalPaste: function(map, usePasteKeyEvent) {
|
|
if (usePasteKeyEvent) {
|
|
// paste into dialog
|
|
var KEY_PASTE = 1299;
|
|
map._textInput._sendKeyEvent(0, KEY_PASTE);
|
|
} else if (this.pasteSpecialVex && this.pasteSpecialVex.isOpen) {
|
|
this.pasteSpecialVex.close();
|
|
app.socket.sendMessage('uno .uno:PasteSpecial');
|
|
} else {
|
|
// paste into document
|
|
app.socket.sendMessage('uno .uno:Paste');
|
|
}
|
|
},
|
|
|
|
cut: function(ev) { return this._doCopyCut(ev, 'Cut'); },
|
|
|
|
copy: function(ev) { return this._doCopyCut(ev, 'Copy'); },
|
|
|
|
paste: function(ev) {
|
|
console.log('Paste');
|
|
|
|
if (this._isAnyInputFieldSelected())
|
|
return;
|
|
|
|
// If the focus is in the search box, paste there.
|
|
if (this._map.isSearching())
|
|
return;
|
|
|
|
if (this._downloadProgressStatus() === 'downloadButton')
|
|
this._stopHideDownload(); // Terminate pending confirmation
|
|
|
|
if (this._map._activeDialog)
|
|
ev.usePasteKeyEvent = true;
|
|
|
|
if (ev.clipboardData) {
|
|
ev.preventDefault();
|
|
var usePasteKeyEvent = ev.usePasteKeyEvent;
|
|
// Always capture the html content separate as we may lose it when we
|
|
// pass the clipboard data to a different context (async calls, f.e.).
|
|
var htmlText = ev.clipboardData.getData('text/html');
|
|
var hasFinished = this.dataTransferToDocument(ev.clipboardData, /* preferInternal = */ true, htmlText, usePasteKeyEvent);
|
|
this._map._textInput._abortComposition(ev);
|
|
this._clipboardSerial++;
|
|
if (hasFinished)
|
|
this._stopHideDownload();
|
|
}
|
|
return false;
|
|
},
|
|
|
|
clearSelection: function() {
|
|
this._selectionContent = '';
|
|
this._selectionType = null;
|
|
this._scheduleHideDownload(15);
|
|
},
|
|
|
|
// textselectioncontent: message
|
|
setTextSelectionHTML: function(html) {
|
|
this._selectionType = 'text';
|
|
this._selectionContent = html;
|
|
if (L.Browser.cypressTest) {
|
|
this._dummyDiv.innerHTML = html;
|
|
}
|
|
this._scheduleHideDownload(15);
|
|
},
|
|
|
|
// sets the selection to some (cell formula) text)
|
|
setTextSelectionText: function(text) {
|
|
this._selectionType = 'text';
|
|
this._selectionContent = this._originWrapBody(
|
|
'<body>' + text + '</body>');
|
|
this._scheduleHideDownload(15);
|
|
},
|
|
|
|
// complexselection: message
|
|
onComplexSelection: function (/*text*/) {
|
|
// Mark this selection as complex.
|
|
this._selectionType = 'complex';
|
|
this._scheduleHideDownload(15);
|
|
},
|
|
|
|
_startProgress: function() {
|
|
if (!this._downloadProgress) {
|
|
this._downloadProgress = L.control.downloadProgress();
|
|
}
|
|
if (!this._downloadProgress.isVisible()) {
|
|
this._downloadProgress.addTo(this._map);
|
|
}
|
|
this._downloadProgress.show();
|
|
},
|
|
|
|
_onDownloadOnLargeCopyPaste: function () {
|
|
if (!this._downloadProgress || this._downloadProgress.isClosed()) {
|
|
this._warnFirstLargeCopyPaste();
|
|
this._startProgress();
|
|
}
|
|
else if (this._downloadProgress.isStarted()) {
|
|
// Need to show this only when a download is really in progress and we block it.
|
|
// Otherwise, it's easier to flash the widget or something.
|
|
this._warnLargeCopyPasteAlreadyStarted();
|
|
}
|
|
},
|
|
|
|
_downloadProgressStatus: function() {
|
|
if (this._downloadProgress && this._downloadProgress.isVisible())
|
|
return this._downloadProgress.currentStatus();
|
|
},
|
|
|
|
// Download button is still shown after selection changed -> user has changed their mind...
|
|
_scheduleHideDownload: function(s) {
|
|
if (!this._downloadProgress || !this._downloadProgress.isVisible())
|
|
return;
|
|
|
|
// If no other copy/paste things occurred then ...
|
|
var that = this;
|
|
var serial = this._clipboardSerial;
|
|
if (!this._hideDownloadTimer)
|
|
this._hideDownloadTimer = setTimeout(function() {
|
|
that._hideDownloadTimer = null;
|
|
if (serial == that._clipboardSerial && that._downloadProgressStatus() === 'downloadButton')
|
|
that._stopHideDownload();
|
|
}, 1000 * s);
|
|
},
|
|
|
|
// useful if we did an internal paste already and don't want that.
|
|
_stopHideDownload: function() {
|
|
clearTimeout(this._hideDownloadTimer);
|
|
this._hideDownloadTimer = null;
|
|
|
|
if (!this._downloadProgress ||
|
|
!this._downloadProgress.isVisible() ||
|
|
this._downloadProgress.isClosed())
|
|
return;
|
|
this._downloadProgress._onClose();
|
|
},
|
|
|
|
_userAlreadyWarned: function (warning) {
|
|
var itemKey = warning;
|
|
var storage = localStorage;
|
|
if (storage && !storage.getItem(itemKey)) {
|
|
storage.setItem(itemKey, '1');
|
|
return false;
|
|
} else if (!storage)
|
|
return false;
|
|
|
|
return true;
|
|
},
|
|
|
|
_warnCopyPaste: function() {
|
|
var self = this;
|
|
var msg;
|
|
if (window.mode.isMobile() || window.mode.isTablet()) {
|
|
msg = _('<p>Please use the copy/paste buttons on your on-screen keyboard.</p>');
|
|
} else {
|
|
msg = _('<p>Your browser has very limited access to the clipboard, so use these keyboard shortcuts:</p><table class="warn-copy-paste"><tr><td><kbd>Ctrl</kbd><span class="kbd--plus">+</span><kbd>C</kbd></td><td><kbd>Ctrl</kbd><span class="kbd--plus">+</span><kbd>X</kbd></td><td><kbd>Ctrl</kbd><span class="kbd--plus">+</span><kbd>V</kbd></td></tr><tr><td>Copy</td><td>Cut</td><td>Paste</td></tr></table>');
|
|
msg = L.Util.replaceCtrlInMac(msg);
|
|
}
|
|
vex.dialog.alert({
|
|
unsafeMessage: msg,
|
|
callback: function () {
|
|
self._map.focus();
|
|
}
|
|
});
|
|
},
|
|
|
|
_substProductName: function (msg) {
|
|
var productName = (typeof brandProductName !== 'undefined') ? brandProductName : 'Collabora Online Development Edition';
|
|
return msg.replace('%productName', productName);
|
|
},
|
|
|
|
_warnFirstLargeCopyPaste: function () {
|
|
if (this._userAlreadyWarned('warnedAboutLargeCopy'))
|
|
return;
|
|
|
|
var self = this;
|
|
var msg = _('<p>If you would like to share larger elements of your document with other applications ' +
|
|
'it is necessary to first download them onto your device. To do that press the ' +
|
|
'"Start download" button below, and when complete click "Confirm copy to clipboard".</p>' +
|
|
'<p>If you are copy and pasting between documents inside %productName, ' +
|
|
'there is no need to download.</p>');
|
|
vex.dialog.alert({
|
|
unsafeMessage: this._substProductName(msg),
|
|
callback: function () {
|
|
self._map.focus();
|
|
}
|
|
});
|
|
},
|
|
|
|
_warnLargeCopyPasteAlreadyStarted: function () {
|
|
var self = this;
|
|
vex.dialog.alert({
|
|
unsafeMessage: _('<p>A download due to a large copy/paste operation has already started. ' +
|
|
'Please, wait for the current download or cancel it before starting a new one</p>'),
|
|
callback: function () {
|
|
self._map.focus();
|
|
}
|
|
});
|
|
},
|
|
|
|
});
|
|
|
|
L.clipboard = function(map) {
|
|
if (window.ThisIsAMobileApp)
|
|
console.log('======> Assertion failed!? No L.Clipboard object should be needed in a mobile app');
|
|
return new L.Clipboard(map);
|
|
};
|