958

When you hover over a card in Trello and press Ctrl+C, the URL of this card is copied to the clipboard. How do they do this?

As far as I can tell, there is no Flash movie involved. I've got Flashblock installed, and the Firefox network tab shows no Flash movie loaded. (That's the usual method, for example, by ZeroClipboard.)

How do they achieve this magic?

(Right at this moment I think I had an epiphany: You cannot select text on the page, so I assume they have an invisible element, where they create a text selection via JavaScript code, and Ctrl+C triggers the browser's default behaviour, copying that invisible node's text value.)

Michael
  • 10,124
  • 1
  • 34
  • 49
Boldewyn
  • 81,211
  • 44
  • 156
  • 212
  • 23
    If you look at the live DOM, there's a div with the class "clipboard-container". When you hold down the ctrl key, it gets filled with a textarea (and is removed when you lift off the ctrl key). I would assume your epiphany is correct. I'm just not exactly sure where they are storing the URL per card – Ian Jul 08 '13 at 13:42
  • @Ian, yes, I can confirm, that's exactly how it worked. Thanks for digging it up! (I don't bother with where the URL is stored. I was interested in the clipboard-without-flash technology.) – Boldewyn Jul 08 '13 at 14:01
  • Well the answer that's here seems a lot better than what I could explain, so I think you should just go with that :) And as it says, the element has an **id**, not **class** of "clipboard-container"...that was a typo on my part – Ian Jul 08 '13 at 14:04
  • 2
    I looked up Daniel's profile, and it seems, he's a Trello developer. (I wondered, where he got the Coffeescript source from.) So he has an unjust advantage ;-) Thanks anyway! – Boldewyn Jul 08 '13 at 14:05
  • Haha yeah seriously, I was like "he just wrote all that code to show what it might be doing?!?!" – Ian Jul 08 '13 at 14:07
  • 1
    I don't intend to detract from the resourcefulness of this technique, it's quite clever; but I can't help but think this is, at best, poorly publicized/documented, and at worst, a pretty jarring user experience. Granted, it's not invasively jarring (as I can't recall a time in which I accidentally copied the card URL), but as a long-time Trello user I had absolutely no idea this existed. – Michael Wales Jul 12 '13 at 18:35
  • 3
    @MichaelWales This feature was added 5 days ago; we're still testing it out, and if it seems to be working it'll be documented as a keyboard shortcut. – Daniel LeCheminant Jul 12 '13 at 19:37
  • @Daniel: Ah, makes sense then! Thanks for following up - it's definitely a cool little hack that has inspired me to implement similar functionality in some of our applications (primarily revolve around geospatial data and visualizations). – Michael Wales Jul 23 '13 at 07:03
  • If you want to do copy in chrome console. you can use [copy("")](https://developer.chrome.com/devtools/docs/commandline-api#copyobject) – wener Jul 07 '14 at 02:50
  • Found this very nice [npm package](https://www.npmjs.com/package/react-clipboard) built as a solution to this question. – PrashanD Apr 22 '18 at 10:11

5 Answers5

1563

Disclosure: I wrote the code that Trello uses; the code below is the actual source code Trello uses to accomplish the clipboard trick.


We don't actually "access the user's clipboard", instead we help the user out a bit by selecting something useful when they press Ctrl+C.

Sounds like you've figured it out; we take advantage of the fact that when you want to hit Ctrl+C, you have to hit the Ctrl key first. When the Ctrl key is pressed, we pop in a textarea that contains the text we want to end up on the clipboard, and select all the text in it, so the selection is all set when the C key is hit. (Then we hide the textarea when the Ctrl key comes up.)

Specifically, Trello does this:

TrelloClipboard = new class
  constructor: ->
    @value = ""

    $(document).keydown (e) =>
      # Only do this if there's something to be put on the clipboard, and it
      # looks like they're starting a copy shortcut
      if !@value || !(e.ctrlKey || e.metaKey)
        return

      if $(e.target).is("input:visible,textarea:visible")
        return

      # Abort if it looks like they've selected some text (maybe they're trying
      # to copy out a bit of the description or something)
      if window.getSelection?()?.toString()
        return

      if document.selection?.createRange().text
        return

      _.defer =>
        $clipboardContainer = $("#clipboard-container")
        $clipboardContainer.empty().show()
        $("<textarea id='clipboard'></textarea>")
        .val(@value)
        .appendTo($clipboardContainer)
        .focus()
        .select()

    $(document).keyup (e) ->
      if $(e.target).is("#clipboard")
        $("#clipboard-container").empty().hide()

  set: (@value) ->

In the DOM we've got:

<div id="clipboard-container"><textarea id="clipboard"></textarea></div>

CSS for the clipboard stuff:

#clipboard-container {
  position: fixed;
  left: 0px;
  top: 0px;
  width: 0px;
  height: 0px;
  z-index: 100;
  display: none;
  opacity: 0;
}
#clipboard {
  width: 1px;
  height: 1px;
  padding: 0px;
}

... and the CSS makes it so you can't actually see the textarea when it pops in ... but it's "visible" enough to copy from.

When you hover over a card, it calls

TrelloClipboard.set(cardUrl)

... so then the clipboard helper knows what to select when the Ctrl key is pressed.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Daniel LeCheminant
  • 50,583
  • 16
  • 120
  • 115
  • 3
    Awesome! But how do you have Mac OS - do you "listen" to the Command key there? – Suman Jul 08 '13 at 17:25
  • 29
    It's worth noting that a similar method works just as well for capturing pasted content – Michael Robinson Jul 09 '13 at 03:01
  • 19
    This sounds like it'd be bad for keyboard users - anytime you try to copy (or ctrl+click to open in another window, or Ctrl+F to search, and so on), your focus is moved somewhere unrelated. – Adam A Jul 14 '13 at 08:45
  • Thanks for your solution, it's awesome! I have created a simple demo, to illustrate it - http://codepen.io/malyw/pen/xAdgn – Serg Hospodarets Jul 20 '13 at 18:59
  • 2
    +1. Lots of neat stuff going on in this answer. I like that you actually shared the source code. But what I thought was clever was the actual explanation of the process used to provide the ctrl+c functionality. In my opinion it was very smart to take advantage of the fact that ctrl and c can not be pressed at the exact same time by starting to prepare for the c when ctrl is pushed. I really liked that approach. – Travis J Jul 20 '13 at 21:40
  • 8
    Feel free to use http://js2coffee.org/ to translate the original into js if so inclined. – Alexandr Kurilin Aug 03 '13 at 17:37
  • 1
    @SergeyGospodarets it doesn't seem to work in Safari on Mac OS X. – Åke Gregertsen Aug 03 '13 at 20:02
  • @Neal it doesn't work in Chrome only if you take away the focus from the window (e.g. when cursor is positioned in the html/css/js edit box). By default, demo window will be focused with the code: $focusInput = $('').appendTo(document.body).focus();# sets the keyboard focus to the demo window (by default, CodePen sets the focus to the editor) – Serg Hospodarets Oct 13 '13 at 20:00
  • 2
    I translated the code to js using [js2coffee.org](http://js2coffee.org), which was mentioned [above](http://stackoverflow.com/questions/17527870/how-does-trello-access-the-users-clipboard#comment26382952_17528590), and it doesn't work for me. @Daniel , can you please provide us with a working js version of the code? – Iryn Oct 22 '13 at 23:59
  • The code doesn't work because the body of the `set` method is missing. @Daniel-LeCheminant can you supply the missing code? – James M. Greene Nov 05 '13 at 19:15
  • 2
    @JamesM.Greene In coffeescript, `set: (@value) ->` becomes `set: function(value) { this.value = value; }` – Daniel LeCheminant Nov 05 '13 at 22:05
  • @SergeyGospodarets it just doesn't work, no matter where the cursor is. – Maksim Vi. Feb 27 '14 at 00:51
  • @DanielLeCheminant, What if the user had mapped their "copy keys" to something else besides `Ctrl C`? – Pacerier May 22 '14 at 21:59
  • 1
    @SergeyGospodarets http://codepen.io/anon/pen/vgdql fixed it for you, you were setting focus on the pastebox before the keyUp event, you needed to do that in the keyUp event – EaterOfCode Jun 03 '14 at 08:05
  • 1
    I implemented an Angular attribute Directive to do this on enter/leave of an element: http://jsfiddle.net/lapo/4qhn8gm1/ – lapo Nov 27 '14 at 12:23
  • I have an issue with pasting. If I click on a button on the page for example, and then do Ctrl+V by keyboard, the hidden input does not get the 'paste' event. Is it possible to catch paste operations in this case? – Don Box Jul 25 '16 at 16:16
  • @Pacerier Then they are outliers and nobody cares about their experience. – Hejazzman Aug 26 '16 at 20:56
  • @AdamA I agree. Instead of stealing keyboard focus for all Ctrl/Meta keydown events, it should only happen for very specific keydown combos (e.g. Ctrl + C). I guess a minor problem arises if someone remaps their cut/copy/paste shortcut keys. They'll be out of luck and they might have even mapped copy to Alt + Shift + C, which would never get caught anyway, so . The real problem is that browser vendors don't handle/allow cut/copy/paste events on all elements. This focus hackery wouldn't be required if sanity existed. – CubicleSoft May 24 '20 at 15:57
83

I actually built a Chrome extension that does exactly this, and for all web pages. The source code is on GitHub.

I find three bugs with Trello's approach, which I know because I've faced them myself :)

The copy doesn't work in these scenarios:

  1. If you already have Ctrl pressed and then hover a link and hit C, the copy doesn't work.
  2. If your cursor is in some other text field in the page, the copy doesn't work.
  3. If your cursor is in the address bar, the copy doesn't work.

I solved #1 by always having a hidden span, rather than creating one when user hits Ctrl/Cmd.

I solved #2 by temporarily clearing the zero-length selection, saving the caret position, doing the copy and restoring the caret position.

I haven't found a fix for #3 yet :) (For information, check the open issue in my GitHub project).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Dhruv Vemula
  • 952
  • 5
  • 7
23

With the help of raincoat's code on GitHub, I managed to get a running version accessing the clipboard with plain JavaScript.

function TrelloClipboard() {
    var me = this;

    var utils = {
        nodeName: function (node, name) {
            return !!(node.nodeName.toLowerCase() === name)
        }
    }
    var textareaId = 'simulate-trello-clipboard',
        containerId = textareaId + '-container',
        container, textarea

    var createTextarea = function () {
        container = document.querySelector('#' + containerId)
        if (!container) {
            container = document.createElement('div')
            container.id = containerId
            container.setAttribute('style', [, 'position: fixed;', 'left: 0px;', 'top: 0px;', 'width: 0px;', 'height: 0px;', 'z-index: 100;', 'opacity: 0;'].join(''))
            document.body.appendChild(container)
        }
        container.style.display = 'block'
        textarea = document.createElement('textarea')
        textarea.setAttribute('style', [, 'width: 1px;', 'height: 1px;', 'padding: 0px;'].join(''))
        textarea.id = textareaId
        container.innerHTML = ''
        container.appendChild(textarea)

        textarea.appendChild(document.createTextNode(me.value))
        textarea.focus()
        textarea.select()
    }

    var keyDownMonitor = function (e) {
        var code = e.keyCode || e.which;
        if (!(e.ctrlKey || e.metaKey)) {
            return
        }
        var target = e.target
        if (utils.nodeName(target, 'textarea') || utils.nodeName(target, 'input')) {
            return
        }
        if (window.getSelection && window.getSelection() && window.getSelection().toString()) {
            return
        }
        if (document.selection && document.selection.createRange().text) {
            return
        }
        setTimeout(createTextarea, 0)
    }

    var keyUpMonitor = function (e) {
        var code = e.keyCode || e.which;
        if (e.target.id !== textareaId || code !== 67) {
            return
        }
        container.style.display = 'none'
    }

    document.addEventListener('keydown', keyDownMonitor)
    document.addEventListener('keyup', keyUpMonitor)
}

TrelloClipboard.prototype.setValue = function (value) {
    this.value = value;
}

var clip = new TrelloClipboard();
clip.setValue("test");

See a working example: http://jsfiddle.net/AGEf7/

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Felix
  • 1,097
  • 1
  • 10
  • 16
  • @don41382 it doesn't properly work on Safari (at least Mac version). Under proper I mean it does copy, but you have to push cmd+C twice. – Vadim Ivanov Apr 16 '14 at 08:44
  • @VadimIvanov True! Does somebody knows why? – Felix Apr 17 '14 at 15:58
  • 1
    @don41382 I don't know exactly why, but I found a solution. You have a minor bug, onKeyDown the first statement should be if (!(e.ctrlKey || e.metaKey)) { return; } It means that we need to prepare textarea for copy on metaKey pressed (this is how guys from trello made a trick). This is a code from trello.com https://gist.github.com/fustic/10870311 – Vadim Ivanov Apr 17 '14 at 19:22
  • @VadimIvanov Thanks. I'll fix it above. – Felix May 02 '14 at 14:58
  • 1
    It wasn't working in FF 33.1 because `el.innerText` was undefined, so I changed the last line of the `clipboard()` function to `clip.setValue(el.innerText || el.textContent);` for more cross-browser compatibility. link: http://jsfiddle.net/AGEf7/31/ – RevanProdigalKnight Nov 14 '14 at 15:06
7

Daniel LeCheminant's code didn't work for me after converting it from CoffeeScript to JavaScript (js2coffee). It kept bombing out on the _.defer() line.

I assumed this was something to do with jQuery deferreds, so I changed it to $.Deferred() and it's working now. I tested it in Internet Explorer 11, Firefox 35, and Chrome 39 with jQuery 2.1.1. The usage is the same as described in Daniel's post.

var TrelloClipboard;

TrelloClipboard = new ((function () {
    function _Class() {
        this.value = "";
        $(document).keydown((function (_this) {
            return function (e) {
                var _ref, _ref1;
                if (!_this.value || !(e.ctrlKey || e.metaKey)) {
                    return;
                }
                if ($(e.target).is("input:visible,textarea:visible")) {
                    return;
                }
                if (typeof window.getSelection === "function" ? (_ref = window.getSelection()) != null ? _ref.toString() : void 0 : void 0) {
                    return;
                }
                if ((_ref1 = document.selection) != null ? _ref1.createRange().text : void 0) {
                    return;
                }
                return $.Deferred(function () {
                    var $clipboardContainer;
                    $clipboardContainer = $("#clipboard-container");
                    $clipboardContainer.empty().show();
                    return $("<textarea id='clipboard'></textarea>").val(_this.value).appendTo($clipboardContainer).focus().select();
                });
            };
        })(this));

        $(document).keyup(function (e) {
            if ($(e.target).is("#clipboard")) {
                return $("#clipboard-container").empty().hide();
            }
        });
    }

    _Class.prototype.set = function (value) {
        this.value = value;
    };

    return _Class;

})());
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
TugboatCaptain
  • 4,150
  • 3
  • 47
  • 79
5

Something very similar can be seen on http://goo.gl when you shorten the URL.

There is a read-only input element that gets programmatically focused, with tooltip press Ctrl+C to copy.

When you hit that shortcut, the input content effectively gets into the clipboard. Really nice :)

Josh Correia
  • 3,807
  • 3
  • 33
  • 50
Boris Brdarić
  • 4,674
  • 2
  • 24
  • 19