※スマホ対応はしてません。

タグ: Advent Calendar 2015

textContentでjQueryみたいに文字列で取れるけど、ちょっと注意が必要ね。(DOMおれおれAdvent Calendar 2015 – 09日目)

カテゴリー: JavaScript

DOMおれおれAdvent Calendar 2015 – 09日目

2015-12-09

テキストノートはあくまでノードなので、文字列ではありません。 toString() も特に用意されてないみたいです。

文字列を取得するにはテキストノードの nodeValue を見ます。

var elTitle = document.querySelector('title');
var nText = elTitle.firstChild;
var text = nText.nodeValue;
console.log(text);  // => "Ginpen.com"

任意の要素内の文字列を全部取得するなら

要素ノードの textContent を使います。ノードを再帰的に検索して、全ての文字列をかき集めてくれます。(正確には要素ノード以外でも使えるけど、まあ要素ノードで使うよね。)

取得だけじゃなくて設定もできるので、ユーザー入力値を画面に表示する場合は innerHTML じゃなくてこっち使った方が良いですね。

問題点

問題は文字列なら何でも持ってきてくれちゃうところです。 <script> だろうが <style> だろうがお構いなし。 display:none を指定してある要素以下の文字列も持ってきます。jQueryも textContent を使ってるので同じ。

自力で見えない要素を無視して文字列取得

こんな感じではいかがでしょうか。

/**
 * 要素内の文字列を返す。
 * @param {Element} el
 * @returns {String}
 */
function getText(el) {
  var text = '';

  // 子ノードを再帰的に全検索
  var children = el.childNodes;
  for (var i=0; i<children.length; i++) {
    var child = children[i];
    var childText;

    // テキストノードなら、文字列を取得
    if (child.nodeType === child.TEXT_NODE) {
      childText = String.trim(child.nodeValue);  // インデント等を除去
    }
    // 非表示でない要素ノードなら、子孫の文字列を取得
    else if (child.nodeType === child.ELEMENT_NODE) {
      var display = getComputedStyle(child).display;
      if (display !== 'none') {
        childText = getText(child);

        // インラインでなければ改行を追加
        if (childText && display !== 'inline' && display !== 'inline-block') {
          childText += '\n';
        }
      }
    }

    text += childText;
  }

  return text;
}

からの、こう。

var text = getText(document.querySelector('title'));

<iframe> を無視するとかが必要かも。まあいいか。

getComputedStyle() は要素に実際当たっているスタイルを得る仕組みです。最終的に有効なスタイルの位置が要素の style 属性でもCSSのセレクターでも大丈夫。ただこの関数、パフォーマンス的に問題になりやすいので、多用するとよくないかもです。 textContent が非表示要素以下を無視しないのもたぶんこのせい。

環境

textContent 、 getComputedStyle 共にIEは9+、Androidは問題なさそう。

nodeValue は一通りで使えます。

参考

要素以外にもノードはあるので、nodeType見て確認してね。(DOMおれおれAdvent Calendar 2014 – 08日目)

カテゴリー: JavaScript

DOMおれおれAdvent Calendar 2015 – 08日目

2015-12-08

HTMLの要素をたくさん作ってウェブページを作るわけだけども、要素以外にも「ノード」はあります。

ノード?

HTMLはタグを入れ子に書いて組み立てるというのはご存知の通り。そしてそのタグで記述された要素の階層構造が「ツリー」になります。

ツリーの一番上の要素が <html> 要素で、タグとしてはその 中 に <head> と <body> とがあって、これを階層構造で見た場合は 下 になる、と。さらに <head> の 下 に <title> があって <body> の下には <h1> とかそういうのがあって、またその下には……みたいな。

html
+- head
|  +- meta
|  +- title
|
+- body
   +- main
   |  +- h1
   |  +- div
   |
   +- footer
      +- address

要素のツリーだとこんな感じ。そしてツリーの各所の事をノード(node; 節)と呼びます。要素は全部ノードね。

で、HTMLに各のは要素だけじゃないですね。なじみ深いものが他に二種類。

ひとつは文字列。

ひとつはコメント。

これらも全部ノードです。というわけで、それらもさっきの図に加えてみるとこんな感じに。(文字列は “#” 開始、コメントは “!” 開始というおれおれルールとしまし。)

html
+- head
|  +- meta
|  +- title
|     +- # Ginpen.com
|
+- body
   +- ! コンテンツ主部
   +- main
   |  +- h1
   |  |  +- # Ginpen.com
   |  +- div
   |
   +- ! フッター
   +- footer
      +- address
         +- # Ginpen.com

どうだろう、ちゃんと伝わるかなあ。開発者ツールのインスペクターを見ると、階層ってのがわかりやすいかも。

document-tree
インスペクターで見てみたもの。

ノードの種類

ここまでに三種類挙げましたが、現在のDOM的には以下の七種類があるみたいです。

種類 定数 補足
要素 node.ELEMENT_NODE
文字列 node.TEXT_NODE
node.PROCESSING_INSTRUCTION_NODE XML。よくわかんない
コメント node.COMMENT_NODE
文書 node.DOCUMENT_NODE JSの document
文書タイプ node.DOCUMENT_TYPE_NODE <!DOCTYPE html>
文書断片 node.DOCUMENT_FRAGMENT_NODE JavaScriptで使う

ウェブページ(HTML文書)全体が document ですね。HTMLファイルをテキストエディターで開くと <html> 要素の他にも <!DOCTYPE html> がいきなりでてくると思いますが、この二つが document ノードの子ノードになります。

document
+- `<!DOCTYPE html>`
+- html
   +- head
   +- body

nodeType

nodeType にノードの種類が入ってます。比較に使える定数も各ノードのオブジェクトにぶらさがってます。

var node;

node = document;
console.log(node.nodeType === node.DOCUMENT_NODE);  // => true

node = document.body;
console.log(node.nodeType === node.ELEMENT_NODE);  // => true

var elTitle = document.querySelector('title');
node = elTitle.firstChild;
console.log(node.nodeType === node.TEXT_NODE);  // => true

参考

属性の操作はsetAttribute()とかremoveAttribute()とか、あとattributesで。(DOMおれおれAdvent Calendar 2015 – 07日目)

カテゴリー: JavaScript

DOMおれおれAdvent Calendar 2015 – 07日目

2015-12-07

jQueryだと属性の操作はほとんど attr() で済むんですが、DOMは色々setとgetと分かれてます。あと名前が長いです。

// el = <a href="#top">TOP</a>

console.log(el.getAttribute('href'));  // => "#top"

el.setAttribute('href', '#top-of-the-world');
console.log(el.getAttribute('href'));  // => "#top-of-the-world"

属性値がないもの

は属性名と同じになります。

// el = `<input type="checkbox" checked />`

console.log(el.getAttribute('checked'));  // => "checked"

削除

el.setAttribute('foo', undefined) とかしても foo="undefined" とそのまま設定されるだけで、空文字列を設定 el.setAttribute('foo', '') してもやっぱり foo="" とそのまま設定されます。いやこれはjQueryでも同じなんだけど。

削除したい場合は削除用のAPIを用います。jQueryの removeAttr() に相当します。

// el = <a href="#top" target="_blank">TOP</a>

el.removeAttribute('target');

有無の確認

これはjQueryにはない機能だ。

if (el.hasAttribute('target')) {
  el.setAttribute('target', '_blank');
}

まあ getAttribute() は属性がなければ null を返す(jQueryだと undefined )ので、それで判断するんでも十分だけど。ちなみに昔の仕様だと、 getAttribute() は属性がない場合に空文字列を返すと定められていたそうだ。

まとめて

attributes という配列風プロパティがありまして、 length で個数が取れたり [i] でアクセスできたりしつつ、直接名前 [key] でもアクセスできるという便利なやつです。NamedNodeMapのインスタンスです。

とはいえそんなに使う事もないと思うんだよなー。

attributes の子はまたオブジェクトになってます。 name と value を持っているので、具体的な属性の名前と値はこれでアクセスします。この子オブジェクトはAttrのインスタンスだそうです。(なんでこれだけAttributeじゃなくて省略形なんだ!)

// el = <textarea name="comment" cols="30" rows="10"></textarea>

var attrs = el.attributes;
for (var i=0; i<attrs.length; i++) {
  var attr = attrs[i];
  console.log(attr.name, ':', attr.value);
}
// =>
// rows : 10
// cols : 30
// name : comment

【重要】属性とプロパティは別物

今回扱ったのはHTMLの属性なんだけども、通常JavaScriptから操作する対象は属性ではなくプロパティになります。 例えばチェックボックスにチェックが付いているかどうかを判断するのに getAttribute() は使えません。

// el = <input type="checkbox" checked />

// checked属性の値を確認
console.log(el.getAttribute('checked'));  // => "checked"

// checkedプロパティの値を変更
// (チェックを外す。)
el.checked = false;

// checked属性の値を再確認
console.log(el.getAttribute('checked'));  // => "checked"

というわけで、プロパティを使ってください。詳しくは過去に書いた記事をどうぞ。

属性を操作する場面ってほとんどないんじゃないかなあ。

参考

removeChilde()で要素を削除できるけど、面倒だよね。(DOMおれおれAdvent Calendar 2015 – 06日目)

カテゴリー: JavaScript

DOMおれおれAdvent Calendar 2015 – 06日目

2015-12-06

jQueryだと要素を削除するとき $el.remove() と書けるんだけど、DOMだとこんな書き方になります。

var parent = el.parentNode;
parent.removeChild(el);

「目的の要素を親要素から削除する」じゃなくて「目的の要素から子要素を削除する」というAPIになってます。まあこれはこれでありかな。

既に削除済みでドキュメントツリーに所属していない場合、 el.parentNode は null になります。(だから一行で el.parentNode.removeChild(el) と書くとエラーになる。) 汎用的にやるならそこで判定して、親がなければ何もしないようにした方が良いでしょう。

環境

古いIEも含めて一通り使えます。

remove() もあるにはある

Living Standardの方には書いてあります。IEは未対応、MS Edgeだと対応してるみたい。iOS Safariは未対応だそうです。

おまけ: replaceChild()

あの要素を削除して代わりにこっちの要素を~てな場面では replaceChild(elNew, elTarget) というのもあります。

var elNew = document.createElement('div');
elNew.appendChild(document.createTextNode('NEW!'));  // => <div>NEW!</div>
var elTarget = document.body.firstChild;
var elParent = document.body;

elParent.replaceChild(elNew, elTarget);

地味に大昔のIEからでも使えるみたいです。(エミュレーターの5でも動いた。)

参考

クラス操作はclassListで楽々です。(DOMおれおれAdvent Calendar 2015 – 05日目)

カテゴリー: JavaScript

DOMおれおれAdvent Calendar 2015 – 05日目

2015-12-05

昔からウェブ関係でJSやってた人は、動的に要素のクラス名を操作するので苦労した覚えがあるかと思います。

// el = <div class="is-hidden"></div>

var rx = /(?:^|\s)is-hidden(?:$|\s)/;
var className = el.className;
var hidden = rx.test(className);
console.log(hidden);  // => true

そう、伝統の className プロパティです。 class 属性の中身が文字列で入ってます。これに足したり引いたりしてクラス名を付けたり外したりするわけです。付けるのは el.className += ' new-class-name' と += でやるだけでいいんですが、あくまで文字列操作になるので、以下の問題があります。

  • 空白 " " も考えないといけない
    • HTML側が class="foo" の状態で +='bar' すると、二つのクラスじゃなくて class="foobar" と一つの別のクラスになる。
  • 既に設定されている場合は同じのが二つになる
    • HTML側が class="foo bar" の状態で +=' foo' すると、 class="foo bar foo" となる。

削除の操作なんかもう目も当てられない。(言い過ぎ) とまあ色々面倒でして、jQueryのクラス関係は便利だったし、jQueryなしの場合は自前のスニペットを用意していたんじゃないかなーと思います。

さてさてそんな苦労も今は昔、 classList を使うと何の苦労もありません。

var el = document.createElement('div');  // => <div></div>
var classList = el.classList;

// 追加
classList.add('foo');
classList.add('bar');
classList.add('foo');
console.log(el.className);  // => "foo bar"

// 削除
classList.remove('bar');
console.log(el.className);  // => "foo"

// 切り替え
classList.toggle('bar');
console.log(el.className);  // => "foo bar"
classList.toggle('bar');
console.log(el.className);  // => "foo"

// 有無確認
console.log(classList.contains('foo'));  // => true
console.log(classList.contains('bar'));  // => false

はぁ~楽ちんじゃあ~~~……。

  • add()
  • remove()
  • toggle()
  • contains()

ちなみに有無確認は contains() であって has() ではありません。(おれがよく間違える。)

環境

IEは10から。9では使えません。まじかよ! Androidも3+だそうです。微妙なサポート具合だ。

代替コードがMDNに載っているので、jQueryを使えないが確実なクラス操作を行う必要がある場合、そちらを参考にどうぞ。

参考