Is it possible to copy/paste with Electron?
Asked Answered
V

4

9

I'm using Electron + Nightmare.js to do unit tests.

I need to copy a string to the clibboard > focus a element > paste the content. Then the test is about if my JavaScript is handling behaving properly.

I read in the electron docs about the clipboard api, and copy/paste in webview, but not sure how that integrates with the Nightmare.js API, probably in a .action as suggested in this issue.

A example would be:

import nightmare from 'nightmare'
nightmare.action('copyPaste', function(name, options, parent, win, renderer, done) {
    // some magic here
});

// and then

let res = await page
    .wait('.my-element-to-render')
    .copyPaste(blob)
    .evaluate(() => {
        return document.querySelector('.my-element').value;
    }).end();
expect(res).to.equal('my pasted string');

Any pointers or experience with this?

From the arguments I get from nightmare.action what is the equivalent to <webview> so I can call its copy/paste method?

Vivica answered 24/4, 2017 at 9:48 Comment(0)
M
11

Copy / Paste isn't working in Electron. This is due to the lack of the application’s menu with keybindings to the native clipboard. You can fix that with this code of JS.

You should also checkout this GitHub repository - its a clean HowTo fix this problem.

CODESNIPPET

var app = require("app");
var BrowserWindow = require("browser-window");
var Menu = require("menu");
var mainWindow = null;

app.on("window-all-closed", function(){
    app.quit();
});

app.on("ready", function () {
    mainWindow = new BrowserWindow({
        width: 980,
        height: 650,
        "min-width": 980,
        "min-height": 650
    });
    mainWindow.openDevTools();
    mainWindow.loadUrl("file://" + __dirname + "/index.html");
    mainWindow.on("closed", function () {
        mainWindow =  null;
    });

    // Create the Application's main menu
    var template = [{
        label: "Application",
        submenu: [
            { label: "About Application", selector: "orderFrontStandardAboutPanel:" },
            { type: "separator" },
            { label: "Quit", accelerator: "Command+Q", click: function() { app.quit(); }}
        ]}, {
        label: "Edit",
        submenu: [
            { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" },
            { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" },
            { type: "separator" },
            { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
            { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
            { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
            { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }
        ]}
    ];

    Menu.setApplicationMenu(Menu.buildFromTemplate(template));
});
Mayemayeda answered 24/4, 2017 at 9:52 Comment(6)
Thanks for checking this! Could you point me how this could be integrated with nightmare.js? what is app looking at npm I find a old module that is gone from Github.Vivica
Managed to get it working actually. And posted an answer. Thanks again for checking this!Vivica
doesn't work for me Error: Cannot find module 'menu'Cashbook
look up the github repository linked in my post, its only a snippet and not a working codeMayemayeda
Solve Error: Cannot find module 'menu' replace var Menu = require("menu"); with const {Menu} = require('electron')Manvil
As mentioned, thats only a snippet and not a working code - look up your dependencies, looks like something is missing.Mayemayeda
V
5

Ok, after many hours got it working!

The trick is to use the click event that Nightmare.js can simulate and use that to trigger copy and paste in the browser.

The copy/paste logic is:

.evaluate((blob) => {
    var editor = document.querySelector('.the-element-to-paste-to');
    editor.innerHTML = ''; // so we have a clean paste
    window.addEventListener('copy', function (e){ // this will fire because of "document.execCommand('copy')"
        e.clipboardData.setData('text/html', blob);
        e.preventDefault();
        return false;
    });
    var mockClick = document.createElement('button');
    mockClick.id = 'mockClick';
    mockClick.addEventListener('click', function(e){ // this will fire first
        document.execCommand("copy");
        editor.focus();
        document.execCommand('paste');
    });
    document.body.appendChild(mockClick);
}, myTextThatIWantToPaste)

Nightmare.js will take care of triggering the click and then we get a copy/paste with our own text.

My whole test is now like:

it('should handle pasted test', async function () {
    let page = visit('/index.html');
    let res = await page
        .wait('.the-element-to-paste-to')
        .evaluate((blob) => {
            var editor = document.querySelector('.the-element-to-paste-to');
            editor.innerHTML = ''; // so we have a clean paste
            window.addEventListener('copy', function (e){
                e.clipboardData.setData('text/html', blob);
                e.preventDefault();
                return false;
            });
            var mockClick = document.createElement('button');
            mockClick.id = 'mockClick';
            mockClick.addEventListener('click', function(e){
                document.execCommand("copy");
                editor.focus();
                document.execCommand('paste');
            });
            document.body.appendChild(mockClick);
        }, tableBlob)
        .click('#mockClick') // <---- this is the trigger to the click
        .wait(100)
        .evaluate(() => {
            var editor = document.querySelector('.the-element-to-paste-to');
            return {
                someBold: editor.querySelector('strong').innerHTML,
                someItalic: editor.querySelector('em').innerHTML,
                someUnderlined: editor.querySelector('u').innerHTML,
                someRows: editor.querySelectorAll('table tr').length,
                someColumns: editor.querySelectorAll('table tr:first-child td').length,
            }
        }).end();
    expect(res.someBold).toEqual('Col1 Row 1');
    expect(res.someItalic).toEqual('Col2 Row 2');
    expect(res.someUnderlined).toEqual('Col3 Row 3');
    expect(res.someRows).toEqual(3);
    expect(res.someColumns).toEqual(3);
});
Vivica answered 25/4, 2017 at 5:35 Comment(0)
B
4

The other answers didn't work for me, because 'selector' was unknown and doesn't exist in type 'MenuItemConstructorOptions'. So I had to change 'selector' to 'role'. Setting the type of the template gave me also an easier to read error, that the selector was unknown.

function setMenu() {
  var template: Electron.MenuItemConstructorOptions[] = [{
    label: "Application",
    submenu: [
      { label: "About Application", role: "about" },//orderFrontStandardAboutPanel
        { type: "separator" },
        { label: "Quit", accelerator: "Command+Q", click: function() { app.quit(); }}
    ]}, {
    label: "Edit",
    submenu: [
        { label: "Undo", accelerator: "CmdOrCtrl+Z", role: "undo" },
        { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", role: "redo" },
        { type: "separator" },
        { label: "Cut", accelerator: "CmdOrCtrl+X", role: "cut" },
        { label: "Copy", accelerator: "CmdOrCtrl+C", role: "copy" },
        { label: "Paste", accelerator: "CmdOrCtrl+V", role: "paste" },
        { label: "Select All", accelerator: "CmdOrCtrl+A", role: "selectAll" }
    ]}
]

Menu.setApplicationMenu(Menu.buildFromTemplate(template));
}

This post also helped me: https://stackoverflow.com/a/46466437

Battles answered 12/4, 2022 at 10:48 Comment(0)
O
1

Use the plain old JavaScript to handle the key-down events.

Snippet:

if (
  process.env.NODE_ENV === 'development' ||
  process.env.DEBUG_PROD === 'true'
) {
  mainWindow.webContents.on('context-menu', (e, props) => {
    const { x, y } = props;

    Menu.buildFromTemplate([
      {
        label: 'Inspect element',
        click: () => {
          mainWindow.inspectElement(x, y);
        }
      }
    ]).popup(mainWindow);
  });

  mainWindow.webContents.on('devtools-opened', () => {
    mainWindow.webContents.devToolsWebContents.executeJavaScript(`
            window.addEventListener('keydown', function (e) {
              if (e.keyCode === 88 && e.metaKey) {
                document.execCommand('cut');
              }
              else if (e.keyCode === 67 && e.metaKey) {
                document.execCommand('copy');
              }
              else if (e.keyCode === 86 && e.metaKey) {
                document.execCommand('paste');
              }
              else if (e.keyCode === 65 && e.metaKey) {
                document.execCommand('selectAll');
              }
              else if (e.keyCode === 90 && e.metaKey) {
                document.execCommand('undo');
              }
              else if (e.keyCode === 89 && e.metaKey) {
                document.execCommand('redo');
              }
            });
        `);
  });

  mainWindow.openDevTools();
}
Overstrain answered 26/10, 2018 at 8:25 Comment(2)
Good answer, would be easier if you removed the boilerplate code that isn't necessary to answer the question.Circularize
It would be nice to add a note about portability of this code. These keyCode s are sufficiently portable, but metaKey seems to be relevant on Mac only.Parkinson

© 2022 - 2024 — McMap. All rights reserved.