エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

省略されたテキストの全体をポップアップ表示する、CSSで

動機

表示領域に対して、表示したいテキストが長い場合に、はみ出す分を省略表示するスタイルを指定する方法が CSS にはあります。

こんな感じ。

これを、省略表示されたばあいに、マウスオーバーで全体をポップアップ表示しよう、という試みです。

1. はみ出た部分を切り詰めて省略記号を表示する

text-overflow の仕様にもあるように、text-overflow, overflow, white-space を組み合わせることで、表示領域を超える場合に省略表示することができます。

developer.mozilla.org

/* 表示幅を指定(カラムごとに幅を変えています) */
.foo .truncateable { width: 5rem; }
.bar .truncateable { width: 7rem; }
.baz .truncateable { width: 10rem; }

.truncateable {
  overflow: hidden;        /* はみ出た部分を表示しない */
  white-space: nowrap;     /* 行を折り返さない */
  text-overflow: ellipsis; /* 省略記号を表示する */
}

ここがスタート地点。

ここからポップアップ表示を実装していきます。

2. マウスオーバーで全体を表示する

行の折り返し禁止を解除すれば文字列全体が表示されます。

:hover 擬似クラスを使って、マウスオーバー時に white-space のスタイルを変更するようにします。

.truncateable:hover {
  white-space: normal;
}

ただし、行を折り返して表示するということは、行方向に表示領域を広げることになるので、その方向にある要素を押しのけてしまうことになります。

3. 複数行をその場所に表示する

他の要素を押しのけないように position: absolute を指定します。 コンテナ要素との相対位置で配置したいので、コンテナ要素に position: relative を指定します。

表示位置の調整はあとで行うことにして、ここではコンテナ要素の上端に合わせて表示するため top: 0 を指定しておきます。

.foo, .bar, .baz { position: relative; }

.truncateable:hover {
  white-space: normal;

  position: absolute;
  top: 0;
}

4. 他の要素よりも“上”に表示する

他の要素よりも上に表示されるように z-index を指定します。 ここでは大雑把な数値を指定しています。 この値は表示内容に合わせて適切に設定することになると思います。

また、下の要素が透過して見えてしまわないように、background-color を設定しておきます。

.truncateable:hover {
  white-space: normal;

  position: absolute;
  top: 0;

  z-index: 10;
  background-color: #fff;
}

5. 枠線を表示する

ポップアップした範囲がわかるように、枠線を表示します。 ここで先に保留した表示位置の調整も行います。

枠線や余白の幅を考慮して表示位置を調整し、元の表示と同じ位置に表示されるようにします。

.truncateable:hover {
  white-space: normal;

  position: absolute;

  background-color: #fff;
  z-index: 10;

  border: solid 1px #ccc;
  border-radius: 5px;
  padding: 5px 10px;
  top: -3px;
  left: -8px;
}

これでポップアップ表示の目的は果たせたのですが、このままでだと省略されていないばあいでもポップアップ表示されてしまいます。 ポップアップとは言っても、表示されるテキストの内容は変化しないので、マウスオーバーで枠線が表示されるように見えてしまいます。

次に、この表示を抑制する方法を考えます。

6. 省略表示されていないテキストをポップアップ表示しない

CSS を使ったこの省略表示は、表示に必要なテキスト幅と表示領域の幅の関係で省略するか否かが決まるため、レンダリングされないと省略されるかされないか決まりません。 そのため事前に静的に省略の要否を決めておくことができません。

そのため、この部分だけ JavaScript を使って動的に解決することにします。

はみ出た領域があるか調べる

表示領域の幅を知るには offsetWidth が、表示に必要なテキストの幅を知るには scrollWidth が利用できます。

developer.mozilla.org

developer.mozilla.org

テキストの幅が表示領域の幅以下のばあい(= 省略表示されていないばあい)、つまり

el.scrollWidth <= el.offsetWidth

のばあいにポップアップ表示をやめればよさそうです。

はみ出た領域がないばあいにポップアップ表示を停止する

ここまで .truncateable にスタイルを適用することで省略表示やポップアップ表示を実現してきました。 逆に言うと、省略表示されていない要素には .truncateable に適用したスタイルは不要ということになります。

ということで。 .truncateable を持つ要素のうち、省略表示されていないものから .truncateable を削除することで、ポップアップ表示停止を実現します。

document.querySelectorAll('.truncateable').forEach((el) => {
  if (el.scrollWidth <= el.offsetWidth) {
    el.classList.remove('truncateable')
  }
})

これで不要なポップアップを停止することができました。

7. コード全容

キャプチャした内容を表示するために利用したコードです。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <style>
      table { border-collapse: collapse; }
      tr { border-top: solid thin #ccc; }
      tr:last-child { border-bottom: solid thin #ccc; }
      th, td { padding: .2rem; }

      .foo--header { width: 5rem; }
      .bar--header { width: 7rem; }
      .baz--header { width: 10rem; }

      .foo .truncateable { width: 5rem; }
      .bar .truncateable { width: 7rem; }
      .baz .truncateable { width: 10rem; }

      .foo, .bar, .baz { position: relative; }

      .truncateable {
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
      }

      .truncateable:hover {
        white-space: normal;

        position: absolute;

        background-color: #fff;
        z-index: 10;

        border: solid 1px #ccc;
        border-radius: 5px;
        padding: 5px 10px;
        top: -3px;
        left: -8px;
      }
    </style>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        document.querySelectorAll('.truncateable').forEach((el) => {
          if (el.scrollWidth <= el.offsetWidth) {
            el.classList.remove('truncateable')
          }
        })
      })
    </script>
  </head>
  <body>
    <table class="table">
      <thead>
        <tr>
          <th class="foo--header">FOO</th>
          <th class="bar--header">BAR</th>
          <th class="baz--header">BAZ</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td class="foo">
            <div class="truncateable">あああああ</div>
          </td>
          <td class="bar">
            <div class="truncateable">いいいいいいい</div>
          </td>
          <td class="baz">
            <div class="truncateable">うううううううううう</div>
          </td>
        </tr>
        <tr>
          <td class="foo">
            <div class="truncateable">ええええええええええ</div>
          </td>
          <td class="bar">
            <div class="truncateable">おおおおおおおおおお</div>
          </td>
          <td class="baz">
            <div class="truncateable">かかかかかかかかかか</div>
          </td>
        </tr>
        <tr>
          <td class="foo">
            <div class="truncateable">きききききききききき</div>
          </td>
          <td class="bar">
            <div class="truncateable">くくくくくくくくくく</div>
          </td>
          <td class="baz">
            <div class="truncateable">けけけけけけけけけけ</div>
          </td>
        </tr>
      </tbody>
    </table>
  </body>
</html>

8. 留意点

コンテナ要素がつぶれるばあいがある

次の画像の表の 2 段目のように、省略されている要素が一つだけのばあい、

ポップアップ表示になると、その要素を格納している要素がつぶれて表示されてしまいます。

今の状態では、コンテナ要素の高さは、内側の要素の高さによって決まっているのですが、 position: absolute が機能することでコンテナ要素の領域を超えて表示されるようになることで、内側の「支え」を失ってコンテナ要素の高さのみで表示されるようになるためです。

これを回避するためには、コンテナ要素のスタイルに height を指定して、常に一定の高さを維持するようにします。

省略表示されていないのに offsetWidth < scrollWidth になるばあいがある

わたしが確認したかぎりの話ですが、計算上小数点以下の端数が発生するばあい、Internet Explorer では省略表示されていないにもかかわらず offsetWidth よりも scrollWidth の方が大きくなることがあるようです。

明確な根拠となる情報を見つけることができなかったのですが、offsetWidth の代わりに getBoundingClientRect().width で小数部を含めた値を取得し、Math.ceil() で切り上げた整数値を利用することで回避することができるようです。

el.scrollWidth <= Math.ceil(el.getBoundingClientRect().width)

これで今のところ不都合は確認できていないのですが、ばあいによってはこのわずかの差が表示に影響を与えることがあるかもしれません。

動的に追加される要素があるばあい、そのつどポップアップ停止の処理を適用する必要がある

ポップアップ停止を実現するために、一旦レンダリングされた後にクラス属性を削除するという手段を取りました。 当然なのですが、動的に要素が追加されるようなばあいには、そのつどこのコードを実行しなければなりません。

動的に追加する要素を組み立てる時点で、省略表示されるかどうかの判定ができれば、クラス属性を付加しない要素を最初から組み立てればできそうな気がしますが、可能かどうかまだ確認できていません。

いつか読むはずっと読まない:私たちは何もしていないのに

傍観者でいることはできない。