summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/behaviors/copy_to_clipboard.js
blob: c3c28aeafc0699f875d74fd2b646e2181bc41df0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import ClipboardJS from 'clipboard';
import $ from 'jquery';

import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { fixTitle, add, show, hide, once } from '~/tooltips';

const CLIPBOARD_SUCCESS_EVENT = 'clipboard-success';
const CLIPBOARD_ERROR_EVENT = 'clipboard-error';
const I18N_ERROR_MESSAGE = __('Copy failed. Please manually copy the value.');

function showTooltip(target, title) {
  const { title: originalTitle } = target.dataset;

  once('hidden', (tooltip) => {
    if (tooltip.target === target) {
      target.setAttribute('title', originalTitle);
      target.setAttribute('aria-label', originalTitle);
      fixTitle(target);
    }
  });

  target.setAttribute('title', title);
  target.setAttribute('aria-label', title);
  fixTitle(target);
  show(target);
  setTimeout(() => {
    hide(target);
  }, 1000);
}

function genericSuccess(e) {
  // Clear the selection
  e.clearSelection();
  e.trigger.focus();
  e.trigger.dispatchEvent(new Event(CLIPBOARD_SUCCESS_EVENT));

  const { clipboardHandleTooltip = true } = e.trigger.dataset;
  if (parseBoolean(clipboardHandleTooltip)) {
    // Update tooltip
    showTooltip(e.trigger, __('Copied'));
  }
}

/**
 * Safari > 10 doesn't support `execCommand`, so instead we inform the user to copy manually.
 * See http://clipboardjs.com/#browser-support
 */
function genericError(e) {
  e.trigger.dispatchEvent(new Event(CLIPBOARD_ERROR_EVENT));

  const { clipboardHandleTooltip = true } = e.trigger.dataset;
  if (parseBoolean(clipboardHandleTooltip)) {
    showTooltip(e.trigger, I18N_ERROR_MESSAGE);
  }
}

export default function initCopyToClipboard() {
  const clipboard = new ClipboardJS('[data-clipboard-target], [data-clipboard-text]');
  clipboard.on('success', genericSuccess);
  clipboard.on('error', genericError);

  /**
   * This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting
   * of plain text or GFM. The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and
   * `gfm` keys into the `data-clipboard-text` attribute that ClipboardJS reads from.
   * When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly`
   * attribute`), sets its value to the value of this data attribute, focusses on it, and finally
   * programmatically issues the 'Copy' command, this code intercepts the copy command/event at
   * the last minute to deconstruct this JSON hash and set the `text/plain` and `text/x-gfm` copy
   * data types to the intended values.
   */
  $(document).on('copy', 'body > textarea[readonly]', (e) => {
    const { clipboardData } = e.originalEvent;
    if (!clipboardData) return;

    const text = e.target.value;

    let json;
    try {
      json = JSON.parse(text);
    } catch (ex) {
      return;
    }

    if (!json.text || !json.gfm) return;

    e.preventDefault();

    clipboardData.setData('text/plain', json.text);
    clipboardData.setData('text/x-gfm', json.gfm);
  });

  return clipboard;
}

/**
 * Programmatically triggers a click event on a
 * "copy to clipboard" button, causing its
 * contents to be copied. Handles some of the messiniess
 * around managing the button's tooltip.
 * @param {HTMLElement} btnElement
 */
export function clickCopyToClipboardButton(btnElement) {
  // Ensure the button has already been tooltip'd.
  add([btnElement], { show: true });

  btnElement.click();
}

export { CLIPBOARD_SUCCESS_EVENT, CLIPBOARD_ERROR_EVENT, I18N_ERROR_MESSAGE };