Problem
When you use Ctrl+C while hovering over a card in Trello, the URL of that card is copied to the clipboard. What are their methods for accomplishing this?
There is no Flash movie involved, as far as I can see. 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 pull off this feat of magic?
(At this point, I believe I had an epiphany: Because you can’t choose text on the page, I presume they have an invisible element where they generate a text selection through JavaScript code, and Ctrl+C activates the browser’s default behavior, copying the text value of that unseen node.)
Asked by Boldewyn
Solution #1
Disclosure: I built the code that Trello uses to accomplish the clipboard trick; the code below is the actual source code that Trello uses.
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.
It appears that you’ve worked it out; we take advantage of the fact that you must first press the Ctrl key before pressing Ctrl+C. When we press the Ctrl key, we open a textarea that contains the text we want to copy to the clipboard and select all of the text in it, so when we press the C key, the selection is complete. (When the Ctrl key is pressed, the textarea is hidden.)
Trello, in particular, accomplishes the following:
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) ->
We have the following in the DOM:
<div id="clipboard-container"><textarea id="clipboard"></textarea></div>
For the clipboard things, use the following CSS:
#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 hides the textarea when it appears, but it’s “visible” enough to copy from.
When you place your cursor over a card, it makes a sound.
TrelloClipboard.set(cardUrl)
… so then the clipboard helper knows what to select when the Ctrl key is pressed.
Answered by Daniel LeCheminant
Solution #2
I created a Chrome extension that does just this, and it works on all web pages. The source code can be found on GitHub.
I’ve discovered three flaws in Trello’s approach, which I’m aware of because I’ve experienced them myself:)
In the following conditions, the copy is ineffective:
Instead of producing a concealed span when the user presses Ctrl/Cmd, I solved #1 by always having one.
I solved #2 by clearing the zero-length selection temporarily, saving the caret position, copying, then restoring the caret position.
I haven’t figured out how to solve #3 yet:) (For further information, see my GitHub project’s open issue.)
Answered by Dhruv Vemula
Solution #3
I was able to download a working version of raincoat’s code on GitHub that used plain JavaScript to access the clipboard.
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");
http://jsfiddle.net/AGEf7/ is a functional example.
Answered by Felix
Solution #4
After translating Daniel LeCheminant’s code from CoffeeScript to JavaScript, it didn’t work for me (js2coffee). On the _.defer() line, it kept failing.
This seemed to be related to jQuery deferreds, so I changed it to $.Deferred() and it now works. With jQuery 2.1.1, I tried it in Internet Explorer 11, Firefox 35, and Chrome 39. The procedure is the same as 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;
})());
Answered by TugboatCaptain
Solution #5
When you shorten the URL to http://goo.gl, you’ll see something extremely similar.
There is a readonly input element that gets programmatically focused, with tooltip press CTRL-C to copy.
The input content is effectively copied to the clipboard when you use that shortcut. Exceptionally nice:)
Answered by Boris Brdarić
Post is based on https://stackoverflow.com/questions/17527870/how-does-trello-access-the-users-clipboard