縦長すぎるレイアウトをなんとかしたい

お疲れさまです、みやもとです。

去年の技術書典で買った本に触発されて、このところちょこっとhtmlとかCSSとかを書いてみたりしています。

https://techbookfest.org/product/1FQZ7xU7BKXHKgX8QF895X?productVariantID=3iPG5dZTBM9KcbQN6df597

[Bluma](https://bulma.io/)とかこの本で知ったものも結構あって、楽しみながらあれこれいじっているところです。

https://bulma.io/

そのへんの成り行きは[note](https://note.com/mymt_aggw2208/n/n31b89bd31094)にあるのでそちらをご参照いただくとして。いざ作ってみると思いがけないところで問題が出てきたので、今回はその解決方法について復習がてら書いていきたいと思います。

https://note.com/mymt_aggw2208/n/n31b89bd31094

:::note warn
この記事はBlumaを使う前提なので、記事内では省略しますがhtmlのhead部分に以下の記述が入ります。
```<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">```
Blumaの詳細については以下をご参照ください。
https://bulma.io/documentation/
:::

## テーブルが縦長すぎて見にくい
まずは画像をご覧ください。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/6efd73c9-ed6f-4683-951d-0b7e53681521.png">

かろうじて年表、というかテーブルであることはわかっていただけるでしょうか。
でもどこでどう区切られているのか、各部分が何を表しているかがわかりにくい。

年表作ってみてわかったのですが、書くことが多くなればなるほど縦にテーブルがびょーんと伸びて何が何だかわからなくなってしまうわけです。

とりあえず解決策として

* テーブル全体の高さを固定する
* スクロールバーを付けてテーブル内だけでスクロール可能にする
* 見出し部分を固定する

ぐらいすればわかりやすくなりますかね。
さっそく試してみましょう。

## AIに聞いてみる～CSS編～
悲しいかな、勉強したての身には何をどうすればいい感じに表示されるかさっぱり見当もつきませんでした。
こういう時はAI先生。
聞いてみたところCSS指定でどうにかなるらしく、サンプルコードをくれました。

```css:style.css
.table-container {
    overflow-y: auto;
    max-height: 60vh; 
}
.table th {
    position: sticky;
    top: 0;
    background: rgb(135, 165, 235);
    z-index: 1;
    border-bottom: 2px solid #ccc; 
}
```
Blumaでスクロール可能なテーブルを作るには、tableタグを囲むようにdivを作ってclass="table-container"を指定する必要があります。
ただ、無指定だと横のスクロールはしてくれても縦のスクロールはしてくれないようで、CSSで個別指定を追加する必要がありました。
overflow-yはブロックレベル要素のコンテンツが上下の端からあふれた時にどのように表示するかを設定するプロパティで、ここにautoを指定するとあふれた分がある場合のみスクロールさせることができます。
ちなみにscrollを指定するとあふれるかどうかに関係なくスクロールバーを表示するので、今回のように表示があふれるのが確定している場合はscroll使っても良さそうです。

max-heightはわかりやすいですね。
テーブルを入れ込むdiv、table-containerの最大高を指定します。
今回はvh（Viewport Height）でブラウザウィンドウの高さに占める割合を指定しました。
これでテーブル全体の高さ固定とスクロールバーの表示は完了です。

最後に見出しの固定ですが、これはtableタグ内thタグにスタイル指定することで解決しています。
positionプロパティにstickyを指定すると、スクロール要素を持つ直近の上位部品に対して相対配置されます。今回の場合はtable-containerですね。
相対配置の位置設定はtop、数値が0になっているのでtable-containerの一番上に固定された状態になるわけですね。
z-indexは要素の重ね合わせの指定プロパティで、数字が大きくなるほど上に表示されます。
今回ほかにz-index指定もthの内側の要素もないので、一番上に表示されるようになります。
backgroundとborder-bottomは見出し行の色とか下側罫線の書式ですね。

ここまで指定することで、こんな感じで表示されるようになりました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/35a9563a-a540-4ae7-9b8f-c1d3e0d4a88b.png">

これでだいぶ見やすくなったのではないでしょうか。

ちなみに、thのbackgroundを指定しないとこんなことになるのでご注意。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/dda17957-d334-4968-a22d-e9075ff8bbb9.png">

見出しの裏にテーブルの内容が透けてしまって違和感すごいし見づらいです。

## カードも縦に並べすぎると見づらい
見出しと文章をセットで表示するのにCardというコンポーネントを使っているのですが、これもやっぱり並べると見づらい。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/2d6ba58c-750f-40f1-8bb4-ec6b825f6d15.png">
程よい量ならいいと思うのですが、数が多いとやっぱり見づらい。
また、上の画像では例に使うのにちょうどいい素材がなかったので省いていますが、実際には画面の左半分に縦長の画像を置き、右半分にカードで区切ったテキストを配置しています。
これがテキスト量によってはカードが並んだ高さが横に並ぶ画像の高さより大きくなってしまい、左半分に空白が続く形になります。
見た目的にあんまりよろしくない。

BlumaのドキュメントでCardを見ると、ヘッダ部分に折りたたみのためのボタンがついているものがありました。
これで
* ボタンを押すとカードが開く
* ボタンを押したカード以外はすべて閉じる

というふうにすれば、高さをある程度調整できそうです。

## AIに聞いてみる～JavaScript編～
またしてもAIに聞いたところ、今度はJavaScriptを使えばいいとのこと。
まず、折りたたみ部品を表示するためにhtmlのhead部分に1行追加します。

```html:card.html
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
```
次にカード部分。

```html:card.html
  <div class="columns">
      <div class="column m-3">
          <div class="card">
              <header class="card-header">
                <p class="card-header-title">見出し１</p>
                <button class="card-header-icon" aria-label="more options" onclick="toggleCard(this)">
                  <span class="icon">
                    <i class="fas fa-angle-down" aria-hidden="true"></i>
                  </span>
                </button>
              </header>
              <div class="card-content" style="display: none;">
                <div class="content">
                  本文
                </div>
              </div>
          </div>
          <div class="card">
              <header class="card-header">
                <p class="card-header-title">見出し２</p>
                <button class="card-header-icon" aria-label="more options" onclick="toggleCard(this)">
                  <span class="icon">
                    <i class="fas fa-angle-down" aria-hidden="true"></i>
                  </span>
                </button>
              </header>
              <div class="card-content" style="display: none;">
                <div class="content">
                  本文
                </div>
              </div>
          </div>
      </div>
  </div>
```
記述中のボタン部分で指定している「fa-angle-down」がカード展開用のボタンアイコンですね。
先に追加したlinkタグの指定はこの部品の表示のため必要になります。
また、カード内テキストを表示するcard-contentは初期表示時点で畳んだ状態になるようdisplayをnoneにしています。
そしてカードヘッダ部分にはonclick時の処理としてtoggleCardという関数を実行するよう指定しました。

toggleCardの中身は以下の通りです。
```javascript
function toggleCard(button) {
  // 全てのカードコンテンツを取得
  const allCardContents = document.querySelectorAll('.card-content');
  const allIcons = document.querySelectorAll('.card-header-icon i');
  
  // クリックされたカードのコンテンツとアイコンを取得
  const clickedCardContent = button.closest('.card').querySelector('.card-content');
  const clickedIcon = button.querySelector('i');
  // クリックされたカードの状態を退避
  var cardStatus = clickedCardContent.style.display;
  
  // まず全てのカードを閉じる
  allCardContents.forEach(content => {
    content.style.display = 'none';
  });
  allIcons.forEach(icon => {
    icon.classList.remove('fa-angle-up');
    icon.classList.add('fa-angle-down');
  });
  
  if (cardStatus === 'none') {
    // クリックされたカードが閉じていた場合は開く
    clickedCardContent.style.display = 'block';
    clickedIcon.classList.remove('fa-angle-down');
    clickedIcon.classList.add('fa-angle-up');
  } else {
    // クリックされたカードが開いていた場合は閉じる
    clickedCardContent.style.display = 'none';
    clickedIcon.classList.remove('fa-angle-up');
    clickedIcon.classList.add('fa-angle-down');
  }
}
```
まずページ内でcard-contentをクラスに指定したdivとcard-header-icon内のiタグを全部取得します。
iタグで指定されているのはfa-angle-down、つまり本文展開用のボタンアイコンですね。

次にクリックされたカードのコンテンツとアイコンを取得します。
これは関数のパラメータで渡されたbuttonから、一番近いカードクラスの中のcard-contentと内部に持っているiタグを取得しています。

この時、クリックされたボタンを持つカードのdisplay状態を退避しておきます。
一度すべてのカードを閉じた後にボタンがクリックされたカードを処理するので、ここで状態を保持しないとカードが開いた状態でボタンをクリックしても一度閉じたものをクリックされた時点での状態と誤認して開きっぱなしになります。

そしてまず、いったんすべてのカードを閉じます。
本文であるcard-contentすべてにdisplay:none;を指定し、ボタンからfa-angle-upの要素を取り除いてfa-angle-downを追加します。
classList.addは同じ要素がすでにある場合は重複して追加しないし、classList.removeは含まれていない要素を指定されても無視してエラーにしないんですね。賢い。

そして最後、退避していたクリック時点でのカード状態によってカードの本文を非表示にするか表示するか、ヘッダ部分のボタンを開く表示にするか畳む表示にするかを設定します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/1e577c25-32e6-4b09-aa90-5d50bc867588.png">

ちょっと見やすくなった気がしますが、いかがでしょう。