Qiitaの長いコードをdetailsでざっくり折りたたみ先頭n行だけ見せるユーザースクリプト

これでコードは読めないけど流行っているからなんとなく文章だけ流し読みしているアレやコレの記事もサクサクだ！

# 前置き(廃案)

[teratailのように]CSSでheightを調整したクラスを用意して切り替えるボタン用意してCSSで下向き三角上向き三角マーク用意してCSSでブラー効果かけてボタンクリックでクラスとイベントを切り替えればQiitaでもコードの畳み込みができる。

そんなふうに考えていた時期が
俺にもありました

でも書き始めるとCSSが得意ではないので諸要素の微調整保守が労力の割りに合わないなと見送っていました。

たとえばheightの値で開閉管理するのがイマイチでしたし、
開閉ボタンの場所をコード内のように見せかけるためにコード下部にマージンを与えたり本質的でない場所の作業が必要で、かといって省けば違和感がすごかったりでした。

# 今回のアプローチ

そんな感じで数年放置していたのですが、久しぶりに考えてみると雑でいいなら`<details>`でいけるのでは？と思いました。

[Qiitaの`<details>`についてはしばらくの間勘違いしていたり]もありましたが、最近それを更新したせいで想起しやすかったのかも。

そもそも`<summary>`内に普通のHTMLが反映されるかも不明でしたのでどっちにしろ調査ネタになるかなと。

# スクリーンショット

というか動画。

![gif.gif](https://qiita-image-store.s3.amazonaws.com/0/62354/6d34522d-9449-51a0-c52a-51c69622bf19.gif)

5行以上のコードを5行に折りたたんだ例です。
&#x25B6;の有無でそれとなく察してください。
折りたたんだ状態ではコードクリックでも開きます。

# コード

いつもの流用まみれです。

```js:Qiita_summarize_code.user.js
// ==UserScript==
// @name         Qiita summarize code
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  一定行数以上のコードをdetail summaryに変換して折りたたむ
// @author       khsk
// @match        http://qiita.com/*/items/*
// @match        https://qiita.com/*/items/*
// @grant        none
// ==/UserScript==

// TODO: 閉じていることの可視化を考えていたが、ひとまずdetailsの矢印の有無で。気になるならsummaryに分かりやすい文言追加やcssで背景色変更などなど
// TODO: 特定タグの有or無で開or閉を切り替えられるとそのタグ技術が得意だから読む・読まない 不得意だから読む・読まないを切り替えるといいかも

// innerHTMLを使っている箇所が不安。\n周りとか

(function() {
  'use strict';
  // 人によっては長過ぎ判定の行数とサマリーに表示したい行数は異なりそうだから別々にしてみる
  const ROWS = 5 // 置き換える閾値
  const SUMMARY_ROWS = 5 // 閉じた状態の行数

  // 行数カウントスクリプトではinnerTextを使っていたが、SUMMARY_ROWSで切り詰めた子孫タグを含めたnodeを取得するためにHTMLで計算するようあわせる
  const countRows = pre => {
    // 最後に改行が入っていることにより、空要素が生まれているため-1する
    return pre.innerHTML.split('\n').length - 1
  }

  // サマリー用の先頭SUMMARY_ROWS行分のコードをハイライト保持したまま取得
  const getSummaryCode = code => {
    const clone = code.cloneNode(true)
    const codePre = clone.querySelector('pre')
    codePre.innerHTML = codePre.innerHTML.split('\n').slice(0, SUMMARY_ROWS).join('\n')
    return clone
  }

  // 開いたとき、サマリーのコードは不要なので非表示に
  // cloneNodeはイベントをコピーしないのでcloneしたdetailsにつど設定
  // summaryのテキストが見た目カラでもデフォルトなら三角のマーカーが残るのでその行クリックで閉じられる
  const addToggleSummaryDisplay = details => {
    details.addEventListener("toggle", e => {
      const summaryCode = e.target.querySelector('div')
      if (e.target.open) {
        summaryCode.style.display = 'none'
      } else {
        summaryCode.style.display = ''
      }
    }, false);
  }

  // 入れ子が一発綺麗に作れないのがなー
  const details = document.createElement('details')
  const summary = document.createElement('summary')
  // summaryを改行させる方法が開発ツールでイジってこれ。原理は不明。MDNのサンプルでは↓で改行表示だったが、preかdivのせいかQiitaでは不要だった
  //summary.style.display = 'table-caption'
  details.appendChild(summary)

  // 準備ここまで。以下main

  // getElementsByClassNameではHTMLCollectionが返りDOMの変更が反映されるので、静的なNodeListのためquerySelectorAllのほうを使う(array変換でもいいけど)
  // replaceChildする関係かHTMLCollectionでは全ループが最初のコードを参照していた
  const codeFrames = document.querySelectorAll('.code-frame')
  codeFrames.forEach(code => {
    // コード部分のみはpre要素内。名前付けなどがあるのでその外側をコピー対象にしているが、行数はpre要素で
    const codePre = code.querySelector('pre')
    // returnできないのでネストで書く。for ofならletになるのでトレードオフで？
    if(countRows(codePre) >= ROWS) {
      const d = details.cloneNode(true)
      addToggleSummaryDisplay(d)

      d.querySelector('summary').appendChild(getSummaryCode(code))
      d.appendChild(code.cloneNode(true))
      code.parentNode.replaceChild(d, code)
    }
  })
})();
```

閾値に引っかかったコードブロックを、全体は`details`へコピー。
先頭n行を切り出して`summary`へ入れて元のコードブロックと置換しただけですね。

通常は開いた状態で`summary`のテキストは消えませんが、`details`の`toggle`イベントで`display`を操作して非表示へ。
動かしてみた限りではちらつきも感じなく伸び縮みしているように見えますが、別々のDOMです。

# 参考

* [details: 詳細折りたたみ要素 - HTML: HyperText Markup Language | MDN](https://developer.mozilla.org/ja/docs/Web/HTML/Element/details)
* [Qiitaのコードの左側に選択不可能な行番号を追加するユーザースクリプト - Qiita](https://qiita.com/khsk/items/89fc40321f1387829135)  
  行数カウント流用
* [Qiitaのコードにコピーボタンを追加するユーザースクリプト - Qiita](https://qiita.com/khsk/items/27a80755d883521ecd73)  
  コードブロックの検出流用。するも`HTMLCollection`のliveに引っかかる
* [Qiitaの通知一覧から、いいねしてくれたユーザーのページに直リンするUserscriptを作った 非jQuery版 - Qiita](https://qiita.com/khsk/items/15bc9dc57a3a4cf7ba85)  
  `replaceChild`(と`cloneNode`)はユーザースクリプトでは結構使うのでよくコピペします
* [javascript - How to copy a DOM node with event listeners? - Stack Overflow](https://stackoverflow.com/questions/15408394/how-to-copy-a-dom-node-with-event-listeners)  
  だいたい毎回忘れます。
* [getElementsByClassNameでforEachがうまくいかなかった - Qiita](https://qiita.com/o-sushi/items/929f9bbf46d4f61819ac#comment-a67e787ad5c53d718337)  
  惰性で`Array.prototype.forEach.call()`のままにしてましたがコメントで言われ最後に書き直しました

# 注意

読み込み中にスクロールすると畳んだときに位置がずれてストレスフルかもしれません。

`// @run-at document-end`にすると少しは早くなるかも？

[teratailのように]: https://teratail.com/questions/177056
[Qiitaの`<details>`についてはしばらくの間勘違いしていたり]: https://qiita.com/khsk/items/a002630c034c98edc9d5