Skip to content

Commit

Permalink
cool#8648 clipboard: try to use navigator.clipboard.write()
Browse files Browse the repository at this point in the history
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: Ifcf16474a339f3f1dae3dc99181836e645340048
  • Loading branch information
vmiklos authored and caolanm committed Apr 4, 2024
1 parent 6b37c15 commit 75251f9
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 6 deletions.
14 changes: 10 additions & 4 deletions browser/src/layer/tile/CanvasTileLayer.js
Expand Up @@ -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();
Expand Down
63 changes: 62 additions & 1 deletion browser/src/map/Clipboard.js
Expand Up @@ -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 ...
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion cypress_test/integration_tests/desktop/writer/copy_paste_spec.js
Expand Up @@ -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
Expand All @@ -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() {
Expand Down

0 comments on commit 75251f9

Please sign in to comment.