cool#8648 clipboard: try to use navigator.clipboard.write()
For one, getting the selection HTML all the time when we create a text selection is a waste of resources, since only a subsequent copy needs that HTML. For another, the complex selection case required a confusing "two step copy" workflow, where first you press Ctrl-C, then you download the large selection, finally you press Ctrl-C again. The underlying problem is the same: the document.execCommand() API for copy (and cut) is synchronous, but network operations are async, which don't play well together. Fix the problem by trying to use navigator.clipboard.write() instead: the write() call still has to happen inside a magic security context (keyboard used, click happened), but it can take a callback as a parameter, and inside that callback it's fine to perform async network operations, which allows both using a one-step copy and getting rid of the HTML download on text selection change (when most of the time we don't need it). Tested: - Chrome and Safari; the behavior for Firefox is unchanged, unless about:config opts in to expose the new Clipboard API. - HTML, plain text and image copy. - Cut, not only copy. - Doing this with the notebookbar button & keyboard. - A single cypress test now uses a fake clipboard to assert copy. The rest of the tests are left unchanged for now, but likely we need to get rid of this implicit assumption that the copy container is updated on selection change: different behavior for automated vs manual testing is ugly. Signed-off-by: Miklos Vajna <vmiklos@collabora.com> Change-Id: Ifcf16474a339f3f1dae3dc99181836e645340048pull/8709/head
parent
6b37c158d1
commit
75251f9496
|
@ -3265,11 +3265,17 @@ L.CanvasTileLayer = L.Layer.extend({
|
|||
this._cellCSelections.setPointSet(pointSet);
|
||||
|
||||
this._map.removeLayer(this._map._textInput._cursorHandler); // User selected a text, we remove the carret marker.
|
||||
if (this._selectionContentRequest) {
|
||||
clearTimeout(this._selectionContentRequest);
|
||||
// Keep fetching the text selection during testing, for now: too many tests
|
||||
// depend on this behavior currently.
|
||||
if (navigator.clipboard.write && !L.Browser.cypressTest) {
|
||||
this._map._clip.setTextSelectionType('text');
|
||||
} else {
|
||||
if (this._selectionContentRequest) {
|
||||
clearTimeout(this._selectionContentRequest);
|
||||
}
|
||||
this._selectionContentRequest = setTimeout(L.bind(function () {
|
||||
app.socket.sendMessage('gettextselection mimetype=text/html,text/plain;charset=utf-8');}, this), 100);
|
||||
}
|
||||
this._selectionContentRequest = setTimeout(L.bind(function () {
|
||||
app.socket.sendMessage('gettextselection mimetype=text/html,text/plain;charset=utf-8');}, this), 100);
|
||||
}
|
||||
else {
|
||||
this._textCSelections.clear();
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
* local & remote clipboard data.
|
||||
*/
|
||||
|
||||
/* global app _ brandProductName $ */
|
||||
/* global app _ brandProductName $ ClipboardItem */
|
||||
|
||||
// Get all interesting clipboard related events here, and handle
|
||||
// download logic in one place ...
|
||||
|
@ -513,6 +513,12 @@ L.Clipboard = L.Class.extend({
|
|||
populateClipboard: function(ev) {
|
||||
this._checkSelection();
|
||||
|
||||
if (this._navigatorClipboardWrite()) {
|
||||
// This is the codepath where the browser initiates the clipboard operation,
|
||||
// e.g. the keyboard is used.
|
||||
return true;
|
||||
}
|
||||
|
||||
var text = this._getHtmlForClipboard();
|
||||
|
||||
var plainText = this.stripHTML(text);
|
||||
|
@ -640,6 +646,13 @@ L.Clipboard = L.Class.extend({
|
|||
var serial = this._clipboardSerial;
|
||||
|
||||
this._unoCommandForCopyCutPaste = cmd;
|
||||
|
||||
if (operation !== 'paste' && this._navigatorClipboardWrite()) {
|
||||
// This is the codepath where an UNO command initiates the clipboard
|
||||
// operation.
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.execCommand(operation) &&
|
||||
serial !== this._clipboardSerial) {
|
||||
window.app.console.log('copied successfully');
|
||||
|
@ -779,6 +792,49 @@ L.Clipboard = L.Class.extend({
|
|||
this.paste(ev);
|
||||
},
|
||||
|
||||
// Executes the navigator.clipboard.write() call, if it's available.
|
||||
_navigatorClipboardWrite: function() {
|
||||
if (navigator.clipboard.write === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._selectionType !== 'text') {
|
||||
return false;
|
||||
}
|
||||
|
||||
app.socket.sendMessage('uno ' + this._unoCommandForCopyCutPaste);
|
||||
const url = this.getMetaURL() + '&MimeType=text/html,text/plain;charset=utf-8';
|
||||
const that = this;
|
||||
const text = new ClipboardItem({
|
||||
'text/html': fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(function(text) {
|
||||
const type = "text/html";
|
||||
const content = that.parseClipboard(text)['html'];
|
||||
const blob = new Blob([content], { 'type': type });
|
||||
return blob;
|
||||
}),
|
||||
'text/plain': fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(function(text) {
|
||||
const type = 'text/plain';
|
||||
const content = that.parseClipboard(text)['plain'];
|
||||
const blob = new Blob([content], { 'type': type });
|
||||
return blob;
|
||||
}),
|
||||
});
|
||||
let clipboard = navigator.clipboard;
|
||||
if (L.Browser.cypressTest) {
|
||||
clipboard = this._dummyClipboard;
|
||||
}
|
||||
clipboard.write([text]).then(function() {
|
||||
}, function(error) {
|
||||
window.app.console.log('navigator.clipboard.write() failed: ' + error.message);
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
// Parses the result from the clipboard endpoint into HTML and plain text.
|
||||
parseClipboard: function(text) {
|
||||
let textHtml;
|
||||
|
@ -971,6 +1027,11 @@ L.Clipboard = L.Class.extend({
|
|||
this._scheduleHideDownload();
|
||||
},
|
||||
|
||||
// Sets the selection type without having the selection content (async clipboard).
|
||||
setTextSelectionType: function(selectionType) {
|
||||
this._selectionType = selectionType;
|
||||
},
|
||||
|
||||
// sets the selection to some (cell formula) text)
|
||||
setTextSelectionText: function(text) {
|
||||
// Usually 'text' is what we see in the formulabar
|
||||
|
|
|
@ -13,6 +13,27 @@ describe(['tagdesktop', 'tagnextcloud', 'tagproxy'], 'Clipboard operations.', fu
|
|||
helper.afterAll(testFileName, this.currentTest.state);
|
||||
});
|
||||
|
||||
function setDummyClipboard() {
|
||||
cy.window().then(win => {
|
||||
const app = win['0'].app;
|
||||
const clipboard = app.map._clip;
|
||||
clipboard._dummyClipboard = {
|
||||
write: function(clipboardItems) {
|
||||
const clipboardItem = clipboardItems[0];
|
||||
clipboardItem.getType('text/html').then(blob => blob.text())
|
||||
.then(function (text) {
|
||||
clipboard._dummyDiv.innerHTML = text;
|
||||
});
|
||||
return {
|
||||
then: function(resolve/*, reject*/) {
|
||||
resolve();
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
it('Copy and Paste text.', function() {
|
||||
before('copy_paste.odt');
|
||||
// Select some text
|
||||
|
@ -27,10 +48,12 @@ describe(['tagdesktop', 'tagnextcloud', 'tagproxy'], 'Clipboard operations.', fu
|
|||
cy.cGet('body').rightclick(XPos, YPos);
|
||||
});
|
||||
|
||||
setDummyClipboard();
|
||||
|
||||
cy.cGet('body').contains('.context-menu-link', 'Copy')
|
||||
.click();
|
||||
|
||||
cy.cGet('#copy_paste_warning-box').should('exist');
|
||||
cy.cGet('#copy-paste-container div p').should('have.text', 'text');
|
||||
});
|
||||
|
||||
it('Copy plain text.', function() {
|
||||
|
|
Loading…
Reference in New Issue