Qiitaのコードにコピーボタンを追加するユーザースクリプト

# <i class="fa fa-cubes"></i> 前置き

**"<ruby>待<rt>ま</rt></ruby>"ってたぜェ!! この"<ruby>瞬間<rt>とき</rt></ruby>"をよォ!!**

と、いうことで、去る9/22に[Firefox 41 が正式リリースされました。]
注目すべきはコレ

>document.execCommand("cut") / document.execCommand("copy") を利用して、Web コンテンツのコピーとカットができるようになりました

今までよくクリップボードへのコピーボタンは見かけましたが、あれってFlash経由らしいですね。
でも、これからは`JavaScript`のみで出来るらしいです！

`Chrome`ではすでに対応していたみたいですが、`Firefox`も遅ればせながら…
なので、

* [qiita > 欲しい機能 > コードのコピー機能]
* [【要望】ボタンクリックでコードをコピーしたい]

やってみました。

# <i class="fa fa-cogs"></i> 動作確認環境

`Windows7` + `Firefox41` + `Greasemonkey`
今回も
`Firefox`のスクラッチパッド機能で開発→`Greasemonkey`へ貼り付け
なので、`Greasemonkey`依存は無いはずです。

`Chrome`のコンソールにも貼り付けてみたのですが、出来るはずの`selection`でコケました。
コンソールは独特なのかな？と思っています。
[Chrome 43からcutおよびcopyコマンドが使えます]
はJSFiddleで`Chrome`でも動いたので、自分のコードが悪いかコンソールのせいか…

# <i class="fa fa-desktop"></i> デモ

![Qiita_copy_demo.gif](https://qiita-image-store.s3.amazonaws.com/0/62354/e069c21d-6eac-f572-d0be-3a8a3f11d944.gif)


# <i class="fa fa-code"></i> コード

## 人様用

[Qiita_code_copy_button.user.js]

```js:Qiita_code_copy_button.user.js

// ==UserScript==
// @name        Qiita code copy button
// @namespace   khsk
// @description コードをコピーするボタンを追加する
// @include     http://qiita.com/*/items/*
// @include     https://qiita.com/*/items/*
// @version     1
// @grant       none
// ==/UserScript==

const OFF = 0
const ON = 1

// マウスオーバー時のみボタンを表示 ただし、ボタン分の領域は常に確保
var enable_mouseover = ON
      
// インターフェース作成
var i = document.createElement('i')
i.className = 'fa fa-clipboard'

var div = document.createElement('div')
div.appendChild(i)
// クラスによるfloatを解除
div.style.float = 'none'
// クラスによるフォントの拡大を解除
div.style.fontSize = 'initial'
// コード内かつコード名の右側へ
div.style.display = 'inline-block'
// 上に張り付いているので少し離す(本来よりn pxだけ長くなる)
div.style.marginTop = '3px'
// 「書き方」のマウスオーバーを拝借
div.className = 'editorMarkdown_help'

if (enable_mouseover) {
  div.style.visibility = 'hidden'
}
div.addEventListener('click', copy_code)


// 先に宣言しないと登録できないので、関数定義
// コピー関数
var copy_code = function() {
  if (!document.queryCommandSupported('copy')) {
    // Firefox40までは未実装でもtrueが来てしまう
    alert('copyに対応していません')
    return
  }
  
  // コピー対象を選択状態にする
  var range = document.createRange();  
  range.selectNode(get_last_html_node(this.parentNode));  
  window.getSelection().addRange(range);
  try {
    document.execCommand('copy')
  } catch (e) {
    // 非対応なら例外が出る Firefox40までなど
    alert('copyに失敗しました')  
  }
  // 選択を解除する
  window.getSelection().removeAllRanges(); 
}

// 空のテキストノードを含まないchildrenからラストノードを取得するヘルパー
var get_last_html_node = function (node) {
  return node.children[node.children.length - 1]
}

// マウスオーバー処理有効時のみ宣言
if (enable_mouseover) {
    var show_button = function() {
      get_last_html_node(this).previousSibling.style.visibility = ''
  }

  var hide_button = function() {
      get_last_html_node(this).previousSibling.style.visibility = 'hidden'
  }
}


// 全 ``` 記法にコピーボタンをつける(``は対象外)

var code_frames = document.getElementsByClassName('code-frame')

Array.prototype.forEach.call(code_frames, function(item) {
  // クローンしないと最後のitemにしか追加されない。trueでアイコンまでクローン
  var div_clone = div.cloneNode(true)
  // cloneしたものにイベントを登録する必要がある
  div_clone.addEventListener('click', copy_code)
  // childNodes first・lastChildはtextノードも含み、挿入場所が末尾になる場合があるので、childrenから取得
  item.insertBefore(div_clone, get_last_html_node(item))
  
  if(enable_mouseover) {
    item.addEventListener('mouseover', show_button)
    item.addEventListener('mouseout', hide_button)
  }
})


```

## 自分用(不要なコメント追加)

```js:my_Qiita_code_copy_button.user.js

// ==UserScript==
// @name        Qiita code copy button
// @namespace   khsk
// @description コードをコピーするボタンを追加する
// @include     http://qiita.com/*/items/*
// @version     1
// @grant       none
// ==/UserScript==

const OFF = 0
const ON = 1

// マウスオーバー時のみボタンを表示 ただし表示時にアイコン分下にずれる(OFFなら常にずれている)
var enable_mouseover = ON
      
// インターフェース作成
var i = document.createElement('i')
i.className = 'fa fa-clipboard'

var div = document.createElement('div')
div.appendChild(i)
// クラスによるfloatを解除
div.style.float = 'none'
// クラスによるフォントの拡大を解除
div.style.fontSize = 'initial'
// コード内かつコード名の右側へ
div.style.display = 'inline-block'
// 上に張り付いているので少し離す(本来よりn pxだけ長くなる)
div.style.marginTop = '3px'
// 「書き方」のマウスオーバーを拝借
div.className = 'editorMarkdown_help'
// notice 解除するぐらいなら必要なcssのみ手動でコピーした方が安心しそう
if (enable_mouseover) {
  // displayではボタン分のスペースを確保しないのでvisibilityで。
  // displayではマウスオーバー・アウトでガタガタになる。(marginを設定しないならば名前付きはずれない)
  //div.style.display = 'none'
  div.style.visibility = 'hidden'
}
div.addEventListener('click', copy_code)


// 先に宣言しないと登録できないので、関数定義
// コピー関数
var copy_code = function() {
  if (!document.queryCommandSupported('copy')) {
    // Firefox40までは未実装でもtrueが来てしまう
    alert('copyに対応していません')
    return
  }
  
  // コピー対象を選択状態にする
  var range = document.createRange();  
  range.selectNode(get_last_html_node(this.parentNode));  
  window.getSelection().addRange(range);
  try {
    document.execCommand('copy')
  } catch (e) {
    // 非対応なら例外が出る Firefox40までなど
    alert('copyに失敗しました')  
  }
  // 選択を解除する
  window.getSelection().removeAllRanges(); 
}

// 空のテキストノードを含まないchildrenからラストノードを取得するヘルパー
var get_last_html_node = function (node) {
  return node.children[node.children.length - 1]
}

// マウスオーバー処理有効時のみ宣言
if (enable_mouseover) {
    var show_button = function() {
      //get_last_html_node(this).previousSibling.style.display = 'inline-block'
      get_last_html_node(this).previousSibling.style.visibility = ''
  }

  var hide_button = function() {
    //get_last_html_node(this).previousSibling.style.display = 'none'
    get_last_html_node(this).previousSibling.style.visibility = 'hidden'
  }
}


// 全 ``` 記法にコピーボタンをつける(``は対象外)

var code_frames = document.getElementsByClassName('code-frame')
/* Chromeでは Array.forEachに対応していない http://ptech.g.hatena.ne.jp/noromanba/20120521/1337639496
Array.forEach(code_frames, function(item) {
  // クローンしないと最後のitemにしか追加されない。trueでアイコンまでクローン
  var div_clone = div.cloneNode(true)
  // cloneしたものにイベントを登録する必要がある
  div_clone.addEventListener('click', copy_code)
  // childNodes first・lastChildはtextノードも含み、挿入場所が末尾になる場合があるので、childrenから取得
  item.insertBefore(div_clone, get_last_html_node(item))
  
  if(enable_mouseover) {
    item.addEventListener('mouseover', show_button)
    item.addEventListener('mouseout', hide_button)
  }
})
*/

Array.prototype.forEach.call(code_frames, function(item) {
  // クローンしないと最後のitemにしか追加されない。trueでアイコンまでクローン
  var div_clone = div.cloneNode(true)
  // cloneしたものにイベントを登録する必要がある
  div_clone.addEventListener('click', copy_code)
  // childNodes first・lastChildはtextノードも含み、挿入場所が末尾になる場合があるので、childrenから取得
  item.insertBefore(div_clone, get_last_html_node(item))
  
  if(enable_mouseover) {
    item.addEventListener('mouseover', show_button)
    item.addEventListener('mouseout', hide_button)
  }
})

```

# その他

徘徊したら
[Qiitaの記事内のコード部分を楽にコピペするユーザースクリプト]
もありました。こちらは見た感じコードがすごく綺麗なので、こっちにコピー機能を付け足した方がいいんじゃないでしょうか。


[Firefox 41 が正式リリースされました。]:http://www.mozilla.jp/firefox/41.0/releasenotes/
[qiita > 欲しい機能 > コードのコピー機能]:http://qiita.com/7of9/items/c7ad8569e97fe3bbd29f
[【要望】ボタンクリックでコードをコピーしたい]:http://qiita.com/koryuohproject/items/431a0bb9c640f194c982

[Qiitaの記事内のコード部分を楽にコピペするユーザースクリプト]:http://qiita.com/vzvu3k6k/items/0a0abb0e848ed2c10651
[Chrome 43からcutおよびcopyコマンドが使えます]:http://qiita.com/yoichiro@github/items/43ee75196bf643cde093

***

ボタンを隠す場合は`visibility = 'hidden'`にしています。
`display = 'none'`の場合は、マウスオーバー時にアイコン分の幅が新たに確保され、ガタッとなります。
ただし、隠している際は余分な幅を確保しないので、適用前と同じレイアウトで閲覧できます。
そちらが良ければ、切り替えは個人用コードのコメントをいじって貰えれば…
個人的にはボタンは常に表示でいいと思うのですが、マウスオーバーまで隠している例が多い気がします。
隠すなら隠すで、レイアウトの邪魔にならない部分に表示するようできればよかったのですが、いかんせん技量やアイデア不足です…


[Qiita_code_copy_button.user.js]:https://gist.github.com/khsk/e4c10a98889a0a7e5ce5/raw/edeec3f0dbb89140a386bfdc80998840aa8c5ab0/Qiita_code_copy_button.user.js