JavaScript で幅に合わせて文字列を切りつめる

デスクトップアプリを開発してると、ListView のカラム幅が足りなくなったときに「My Docume...」のように自動的に末尾を「...」で埋めて切りつめてくれる機能がある。ウェブでも同じことをやりたかったので、作ってみた。
まず、文字列の幅を測定するには、

<span id="ruler" style="visibility:hidden;position:absolute;">
</span>

みたいな隠しエレメントを用意しておいて、

String.prototype.getExtent = function(ruler) {
  var e = $(ruler);
  var c;
  while (c = e.lastChild) e.removeChild(c);
  var text = e.appendChild(document.createTextNode(this));
  var width = e.offsetWidth;
  e.removeChild(text);
  return width;
}

文字列をテキストノードにして流し込んでやるといい。

'JavaScript is fun'.getExtent('ruler'); // => 148

次に、この関数を利用して、指定した幅に収まるように文字列を切りつめる方法を考えてみる。
一番簡単なのは、与えられた幅より小さくなるまで文字列をどんどん切りつめていくという方法。
イメージはこんな感じ。

maxWidth = 120
JavaScript is fun  // => 146
JavaScript is fu   // => 135
JavaScript is f    // => 124
JavaScript is      // => 110 ok

この処理をコードにすると、

String.prototype.truncateTailInWidth = function(maxWidth, ruler) {
  if (this.length == 0) return '';
  if (this.getExtent(ruler) <= maxWidth) return this;
  for (var i=this.length-1; i>=1; --i) {
    var s = this.slice(0, i);
    if (s.getExtent(ruler) <= maxWidth) return s;
  }
  return ''
}

こんな感じになる。
やりたいことは「...」をつけて切りつめることなので、このコードを少し変えて、

String.prototype.truncateTailInWidth = function(maxWidth, ruler) {
  if (this.length == 0) return '';
  if (this.getExtent(ruler) <= maxWidth) return this;
  for (var i=this.length-1; i>=1; --i) {
    var s = this.slice(0, i) + '...';
    if (s.getExtent(ruler) <= maxWidth) return s;
  }
  return '';
}

これで完成。
使ってみると、

'JavaScript is fun'.truncateTailInWidth(120, 'ruler'); // => JavaScript i...
'日本語も平気'.truncateTailInWidth(60, 'ruler');       // => 日本...

ちゃんと動作してることがわかる。
動作サンプル: http://limechat.net/sample/truncate/
実際に使うときには、ruler として使う span 要素に、切り詰めた文字列を使う場所と同じスタイルを指定してください。
完成したソースは以下の通り。

String.prototype.getExtent = function(ruler) {
  var e = $(ruler);
  var c;
  while (c = e.lastChild) e.removeChild(c);
  var text = e.appendChild(document.createTextNode(this));
  var width = e.offsetWidth;
  e.removeChild(text);
  return width;
}

String.prototype.truncateTailInWidth = function(maxWidth, ruler) {
  if (this.length == 0) return '';
  if (this.getExtent(ruler) <= maxWidth) return this;
  for (var i=this.length-1; i>=1; --i) {
    var s = this.slice(0, i) + '...';
    if (s.getExtent(ruler) <= maxWidth) return s;
  }
  return '';
}