第 12 回 ブラウザでのHTML, DOM, Javascript

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

12-1. ブラウザ内のJavaScript

ブラウザでJavaScriptのプログラムを動かすには、HTMLファイルに埋め込 むか、HTMLファイルよりプログラムファイルを読み込みます。 これには、HTMLの script 要素を使用します。

  1. head内でscript要素内にプログラムを埋め込んだり、ファイルを指定 した場合、body内の要素が表示される前に、プログラムが実行されます。 但し、body内のDOMにはアクセスできません。
  2. body内でscript 要素が指定されたら、その場でプログラムが実行さ れます。DOMについては、すでに定義されたものだけにアクセスできます。
  3. body 内でscript要素が defer 属性付きで指定された場合、bodyが すべて読み込まれた後に、プログラムが実行されます。
  4. body 内でscript要素が async 属性付きで指定された場合、bodyの 読み込みとは独立にプログラムが読み込まれ、実行されます。

そのため、DOMに対するプログラムを動作させるには、次のような方法が あります。

  1. HTMLのbody の最後にscript要素を記述する
  2. body 内のscript要素にdefer 属性を指定する
  3. イベントハンドラコンテンツ属性である onload をbodyに指定し、必 要なプログラムをコールバック関数で与える。

さらに、表示したHTML文書に対して、ユーザからのインタラクティブな操 作に対応するには、 ユーザ操作などで生じるイベントに対応した動作を記述する必要がありま す。 特定の要素に対するイベントを処理するプログラムを書くには、 その要素にイベントハンドラコンテンツ属性を指定し、 必要な処理をイベントハンドラとして渡す必要があります。

HTML内の JavaScriptの実行

HTML からJavaScript を動かすには次の方法があります。

  1. head要素内の script 要素
  2. body要素内の script 要素
  3. イベントによる駆動

HTML内のJavaScriptの埋め込み

ブラウザで HTML の読み込みにより、プログラムを動かしたい場合、 以下の方法がある。

  1. JavaScript のプログラムを script 要素内に記述
  2. script の src 属性にURLを書き、読み込みを指定
  3. イベントハンドラ属性に直接プログラムを書く

属性にプログラムを直接書く場合、ダブルクォーテーションマーク(")を " でエスケープする必要があります。

script 要素の属性 src で URL でファイルを指定する場合、読み込み方 を指定することができる。 デフォルトでは、指定された場合、そのファイルを読み込んでプログラム を実行します。 そのため、プログラムの内容が単なる関数定義で、ユーザ操作に連動する ような場合、ファイル読み込みの時間にHTMLの表示などが止まることがあ ります。

  1. script 要素に defer 属性が指定されると、文書をすべて読み込んでから、 プログラムが実行されるようになります。
  2. さらに、 async 属性が指定されると、指定したファイルのプログラムは 実行可能になったときに実行されます。

head要素内の script 要素

head 内に置かれた script は body 文書が読み込まれる前に実行されま す。 そのため、文書に関する DOM はすべて参照できません。 プログラムと文書を独立させるために、プログラムを置くのには有効です が、文書を直接操作するプログラムは書けません。

body要素内の script 要素

body 要素内の script 要素でプログラムが置かれた場合、デフォルトの 動作では、そこまで文書が解釈された状態で実行されます。 そのため、script 以降のDOM要素は参照できません。 DOM全体に作用させるには defer 属性を指定させるか、 body に onload で関数を呼び出すかする必要がある。

12-2. イベント

イベントによる駆動

ブラウザでは、クリックされるなどの様々なイベントが発生します。 イベントはオブジェクトで、 DOMのオブジェクトに登録されたイベントハンドラと言う、イベントを引 数とした関数オブジェクトが呼び出されます。

イベントに関しては、 HTML standard の Eventsの章 にまとめてあります。 但し、ここにあるほかにも、メディア要素イベントやドラッグアンドドロップ イベントが定義されています。 8.1.8.2 章のEvent handlers on elements, Document objects, and Window objectsにも載ってます。 主要なものを下記に示します。

load
ドキュメントを読み終わったとき
click
マウスクリックしたとき
submit
Submitボタンを押したとき
reset
resetボタンを押したとき
change
変更があったとき
select
選択されたとき
mouseenter
領域にポインタが入ったとき
mouseleave
領域からポインタが外れたとき

イベントハンドラコンテンツ属性

HTMLの要素には共通のイベントハンドラコンテンツ属性がたくさん定義さ れています。 "on"+イベント名がそれぞれのHTML要素で許されていて、イベントハンドラを 指定できます。

例12-1

古くは、input のボタンなどに onclick を指定して文字列の中にプログ ラムを埋め込んでいました。 新しい DOM では、イベントを定義していて、 イベントリスナにイベントと関数の対を追加するようになっています。 例12-1


<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>例12-1</title>
    <style>
      .white{background:white;}
      .red { background: red;}
      .blue {background: blue;}
      .green {background: green;}
    </style>
  </head>
  <body>
    <p id="target">
      Hello world
    </p>
    <p>
      <input type="button" value="red" id="inputelement">
      <input type="button" value="blue" onclick="change('blue')">
      <a id="aelement">green</a>
      <a onclick="change('blue');">blue</a>
      <select id="selector">
	<option>white</option>
	<option>red</option>
	<option>blue</option>
	<option>green</option>
      </select>
    </p>
    <p id="pelement">
      hover
    </p>
    <script>
      const target=document.getElementById("target");
      function change(x){
      target.setAttribute('class',x);
      }
      const inputElement = document.getElementById('inputelement');
      inputElement.addEventListener('click',(event)=>{
      change('red');});
      const aElement = document.getElementById('aelement');
      aElement.addEventListener('click',(event)=>{
      change('green');});
      const selectElement = document.getElementById('selector');
      selectElement.addEventListener('change', (event) => {
      change(event.target.value);});
      const pElement = document.getElementById('pelement');
      pElement.addEventListener('mouseenter',(event)=>{
      change('red');});
      pElement.addEventListener('click',(event)=>{
      change('blue');});
      pElement.addEventListener('mouseleave',(event)=>{
      change('white');});
    </script>
  </body>
</html>

なお、イベントは、受け取ったオブジェクトからDOMの木構造 の Document オブジェクトまでのオブジェクトのパスを辿るように、イベ ントが伝搬されます。これはバブリングと呼ばれます。

また、イベント自体は予め定義されていたものだけではなく、自由に文字 列で定義して、プログラムから dispatch できます。

例12-2

バブリングの例 ex1.html


<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>バブリングの例</title>
  <style>
    .red {background-color: red;}
    .blue {background-color: blue;}
    .white {background-color: white;}
  </style>
</head>
<body>
  <h1 class="white">バブリングの例</h1>
  <ol id="www">
    <li id="rrr">red</li>
    <li id="bbb">blue</li>
  </ol>
  <script>
    function dispatchred(e) {
      this.dispatchEvent(new Event("r", { bubbles: true }));
    }
    function dispatchblue(e) {
      this.dispatchEvent(new Event("b", { bubbles: true }));
    }
    function dispatchwhite(e) {
      this.dispatchEvent(new Event("w", { bubbles: true }));
    }
    function setblue(e) {
      this.setAttribute("class", "blue");
    }
    function setred(e) {
      this.setAttribute("class", "red");
    }
    function setwhite(e) {
      this.setAttribute("class", "white");
    }
    function setEventListeners(elem, disp) {
      elem.addEventListener("click", disp, { capture: true });
      elem.addEventListener("w", setwhite, { capture: true });
      elem.addEventListener("b", setblue, { capture: true });
      elem.addEventListener("r", setred, { capture: true });
    }
    setEventListeners(document.body, dispatchwhite);
    setEventListeners(document.getElementById("www"), dispatchwhite);
    setEventListeners(document.getElementById("rrr"), dispatchred);
    setEventListeners(document.getElementById("bbb"), dispatchblue);
  </script>
</body>
</html>
  

onload など、ブラウザの状態などにより、イベントが生成された場合、 それに対応させたプログラムを実行させることができます。

例12-3

例12-3はJavaScriptの読む位置やオプショ ンにより、参照できる範囲が異なることを示しています。


<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Defer テスト</title>
    <style>
        .redmark {background:red;}
    </style>
    <script>
    // × head では DOM は見えない
    document.getElementById("id1").setAttribute("class","redmark");
    </script>
</head>
    <body onload="document.getElementById(&quot;id2&quot;).setAttribute(&quot;class&quot;,&quot;redmark&quot;);">
        <!-- 〇 onload は全てを読んでから実行される -->
        <p id="id1">1</p>
        <p id="id2">2</p>
        <p id="id3">3</p>
        <script>
            // 〇 前のDOMは読める
            document.getElementById("id3").setAttribute("class","redmark");
        </script>
         <script>
             // × 後ろのDOMは読めない
            document.getElementById("id4").setAttribute("class","redmark");
        </script>
        <p id="id4">4</p>
        <script defer>
            // × defer は src を指定するときだけ
            document.getElementById("id5").setAttribute("class","redmark");
        </script>
        <p id="id5">5</p>
    </body>
</html>

12-3. shadow tree, template, slot, カスタム要素

Shadow tree

今まで HTML 文書において、マークアップされた様々な要素は、DOM とし て木構造のデータ構造を持ちながら、実際に画面にレンダリングされます。 このようなDOM の構造をlight tree と呼ぶことがあります。

というのも、それとは別に HTML 文書中に表示されない Shadow Treeというデータ構造も作成できます。 要素に Shadow Treeをつけるには JavaScriptで 要素.attachShadow({mode: "open"}) とします。

Shadow Treeを付加できる要素は カスタム要素の他、article, asde, blockquote, body, div , h1...h6, header, main, nav, p, section, span だけです。

要素に Shadow Tree と Light Tree の両方が付与されている場合、 Shadow Tree が表示されます。 但し、Shadow Treeに slot 要素がある場合、slot要素にlight tree の要 素が埋め込まれます。

template

template 要素を使うと、HTML 文書内に、レンダリングはされずに、プロ グラムから参照可能な文書片を埋め込めます。 すると、その文書片をコピーすることにより、文書片の構造をそのまま文 書を構成する構造として使用することができます。

例12-4

template 使用例


<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>template テスト</title>
<style>
    th,td {border: 1px solid black;}
</style>
</head>
<body>
<h1>テンプレートテスト</h1>
<table>
    <thead>
        <tr><th>商品番号</th><th>商品名</th><th>単価</th></tr>
    </thead>
    <tbody>
        <template id="shohin">
            <tr><td id="no"></td><td id="name"></td><td id="price"></td></tr>
        </template>
    </tbody>
</table>
<script>
    const data =[
        {"no": 999001, "name": "リンゴ", "price": 200},
        {"no": 999002, "name": "みかん", "price": 100},
        {"no": 999003, "name": "もも", "price": 500}
];
</script>
<script>
    const temp = document.getElementById("shohin");
    for(let i=0; i< data.length; i++){
        const d = data[i];
        const c = temp.content.cloneNode(true);
        for(let key in d){
            c.querySelector("#"+key).innerText=d[key];
        }
        temp.parentNode.appendChild(c);
    }
</script>
</body>
</html>

slot

slot要素を shadow tree内に使うと、 shadow host が含む light tree を slot 要素に埋め込みます。 light tree の要素の slot 属性に名前を指定すると、 同じ名前をname 属 性に指定した slot 要素に埋め込まれます。

例12-5

slot 使用例


<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>Slot element</title>
  </head>
  <body>
    <div class="type1" id="a1">
      <span slot="id">99ec999</span>
      <span slot="name">坂本一郎</span>
    </div>
    <div class="type2">
      <span slot="id">99ec998</span>
      <span slot="name">坂本次郎</span>
    </div>
    <div class="type1">
      <span slot="id">99ec997</span>
      <span slot="name">坂本三郎</span>
    </div>
    <div class="type2">
      <span slot="id">99ec996</span>
      <span slot="name">坂本四郎</span>
    </div>
    <template id="t1">
      <p>
	idは<slot name="id"></slot>で、名前は<slot name="name"></slot>
      </p>
    </template>
    <template id="t2">
      <style>
.border1 { border-collapse: collapse;
          caption-side: top;
	  border: 1px solid black;}
.border1 td{ border: 1px solid black;}
.border1 th{ border: 1px solid black;}
      </style>
      <table class="border1">
	<tr><th>id</th><th>名前</th></tr>
	<tr>
	  <td><slot name="id"></slot></td>
	  <td><slot name="name"></slot></td>
	</tr>
      </table>
    </template>
    <script>
      for(let j of [['type1','t1'],['type2','t2']]){
	  console.log(j);
	  let  a = document.querySelectorAll('.'+j[0]);
	  let t = document.getElementById(j[1]);
	  for(let i=0 ; i< a.length; i++){
	      let c =a[i].attachShadow({mode:"open"});
	      c.appendChild(t.content.cloneNode(true));
	  }
      }
    </script>
  </body>
</html>

カスタム要素

HTML Standard では要素を自作することができます。 但し、要素名に -(ハイフン)を含む必要があります

カスタム要素を作るに:

  1. 要素名を決めます(ハイフンを含める)
  2. JavaScript で HTMLElement を継承したクラスを作ります。 コンストラクタでは以下の内容を指定します
    1. super(); を呼び出します
    2. this.attachShadow({mode:'open'}); でShadow Tree を作ります。
    3. Shadow Tree を構築し、 slot 要素でカスタム要素内の light tree の要素を埋め込みます。
  3. 事後のアクションが必要な場合は、適宜メソッドを付与します。

例12-6

カスタム要素例


<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>Custom element</title>
  </head>
  <body>
    <my-element-1>
      First
    </my-element-1>
    <my-element-2>
      Second
    </my-element-2>
    <my-element-3>
      Third
    </my-element-3>
    <my-element-4>
      <div slot="pqr">
	Fourth
      </div>
      <div slot="stu">
	Fifth
      </div>
    </my-element-4>
    <script>
class MyElement1 extends HTMLElement {
    constructor() {
	super();
	const shadow = this.attachShadow({ mode: 'open' });
    }
}
class MyElement2 extends HTMLElement {
    constructor() {
	super();
	const shadow = this.attachShadow({ mode: 'open' });
	shadow.innerHTML= "<div>あいうえお</div>";
    }
}
class MyElement3 extends HTMLElement {
    constructor() {
	super();
	const shadow = this.attachShadow({ mode: 'open' });
	shadow.innerHTML= "<div>かきくけこ</div><slot>";
    }
}
class MyElement4 extends HTMLElement {
    constructor() {
	super();
	const shadow = this.attachShadow({ mode: 'open' });
	shadow.innerHTML= '<slot name="stu"></slot>'+
	    '<div>さしすせそ</div>'+
	    '<slot name="pqr"></slot>';
    }
}
customElements.define('my-element-1', MyElement1);
customElements.define('my-element-2', MyElement2);
customElements.define('my-element-3', MyElement3);
customElements.define('my-element-4', MyElement4);
    </script>
  </body>
</html>

selectable-div.js の簡略版

本教材で使用した、 Java と Python でプログラムリストを切り替えるた めの selectable-div 要素の簡略版のソースを示します。

input 要素でイベントをブロードキャストし、指定の slot だけ表示する ようにイベントリスナを設定します。

head 要素内で script 要素を使って、sdarray で選ぶ項目名の配列を定 義し、 body 要素内で、 script 要素に defer 属性を指定して、以下のプログラ ムを読み込んで使用します。

selectable-div 要素に、項目名を slot 属性で指定した要素を入れると、 指定した項目だけが表示されるようになります。


function broadcast(message){
    const ev =  new CustomEvent('sdChange', {
	detail: { value: message }  
    });
    const sds = document.querySelectorAll('selectable-div');
    for(x of sds){
	const c = x.shadowRoot.children;
	c[0].firstChild.dispatchEvent(ev);
	for(let i=1; i<c.length; i++){
	    c[i].dispatchEvent(ev);
	}
    }
}
function changed(event){
    broadcast(event.target.value);
}

class SelectableDiv extends HTMLElement {
    constructor() {
	super();
	const shadow = this.attachShadow({ mode: 'open' });
	const divframe = document.createElement('div');
	const selectelem = document.createElement('select');
	selectelem.addEventListener('change',event => changed(event));
	selectelem.addEventListener('sdChange',
				    function(event){
					selectelem.value=event.detail.value;}
				   );
	for(let t of sdarray){
	    const optionelem = document.createElement('option');
	    optionelem.setAttribute('value',t);
	    optionelem.innerHTML=t;
	    selectelem.appendChild(optionelem);
	}
	divframe.appendChild(selectelem);
	shadow.appendChild(divframe);
	for(let t of sdarray){
	    const slotelem = document.createElement('slot');
	    slotelem.setAttribute('name',t);
	    slotelem.addEventListener('sdChange',
		function(event){
			if(slotelem.name===event.detail.value){
			    slotelem.style.display='block';
			}else{
			    slotelem.style.display='none';
			}
		    });
	    shadow.appendChild(slotelem);
	}
    }
}
customElements.define('selectable-div', SelectableDiv);
broadcast(getCurrentTarget());

12-4. Camvas

Canvas

camvas 要素は width と height 属性を指定することで、画面上に描画領 域を作り、JavaScript の API によりグラフィックを描画することができ ます。

canvas 要素に対して、 getContext でコンテキストを取得した後、 そのコンテキストに対して描画に関する API を指定して描画します。

strokeStyle
線種を決める
fillStyle
塗りつぶしのパターンを書く
fillRect
塗りつぶしの長方形
strokeRect
長方形の枠を描く

一方、線を書くには beginPathを宣言した後、 moveToで始点を決めた後 lineTo で通過点を指定していき、最後に stroke で描画します。

一方、文字を書く場合は、font でフォントを指定した後、 fillText や strokeText で文字を書きます。

例12-7

Canvas使用例


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>cambus test</title>
</head>
<body>
    <p>aaa</p>
    <canvas id="imgcanvas" width="600" height="500">
        <p>ccc</p>
    </canvas>
    <p>bbb</p>
    <script>
        const imgcanvas=document.getElementById("imgcanvas");
        if(imgcanvas.getContext) {
            const ctx=imgcanvas.getContext('2d');
            ctx.fillStyle="rgba(255,0,0,1)";
            ctx.strokeStyle="#000000";
            ctx.fillRect(0, 0, 100, 100);
            ctx.strokeRect(0, 0, 100, 100);
            ctx.fillRect(50, 50, 100, 200);
            ctx.strokeRect(80, 130, 200, 100);

            ctx.beginPath();
            ctx.moveTo(0,100);
            ctx.lineTo(200,200);
            ctx.stroke();

            ctx.font = "16pt 'Arial'";
            ctx.fillText("abc",250,50);
            ctx.strokeText("xyz",250,150);
        }
    </script>
</body>
</html>

12-5. Cookie

12-6. データ、ストレージ

12-7. 演習

演習12-2

与えたデータの配列に関して、折れ線グラフを書くプログラムを作れ


<script>
    const data=[3,4,2,6,10,3];
</script>

演習12-3

アナログの針を表示する時計を作成せよ。但し、 setInterval 関数は、第一引数の関数を第二引数で指定したミリ秒ごとに実行するものとする。

12-8. 演習の解答

演習12-2

与えたデータの配列に関して、折れ線グラフを書くプログラムを作れ

解答例


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>折れ線グラフ</title>
</head>
<body>
    <canvas id="graph" width="600" height="400"></canvas>
    <script>
        const data = [3, 4, 2, 6, 10, 3];
    </script>
    <script>
        const graph = document.getElementById("graph");
        if (graph.getContext) {
            const ctx = graph.getContext('2d');
            ctx.fillStyle = "rgba(255,0,0,1)";
            ctx.strokeStyle = "#000000";
            ctx.beginPath();
            ctx.moveTo(0, 100);
            for (let i = 0; i < data.length; i++) {
                ctx.lineTo(i * 100+100, 200 - data[i] * 10);
            }
            ctx.stroke();
        }
    </script>
</body>
</html>

演習12-3

アナログの針を表示する時計を作成せよ。但し、 setInterval 関数は、第一引数の関数を第二引数で指定したミリ秒ごとに実行するものとする。

解答例


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>時計</title>
</head>
<body>
    <p id="debug"></p>
    <canvas id="tokei" width="600" height="400"></canvas>
    <script>
        function plothari(ctx, deg, radius) {
            deg -= Math.PI / 2;
            ctx.fillStyle = "rgba(255,0,0,1)";
            ctx.strokeStyle = "#000000";
            ctx.beginPath();
            ctx.moveTo(300, 200);
            ctx.lineTo(Math.cos(deg) * radius + 300,
                Math.sin(deg) * radius + 200);
            ctx.stroke();
        }
        var tokei = document.getElementById("tokei");
        var debug = document.getElementById("debug");
        debug.textContent = "aaa" + tokei.getContext;
        if (tokei.getContext) {
            var ctx = tokei.getContext('2d');
            function tick() {
                var date = new Date();
                debug.textContent = "" + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds();
                ctx.clearRect(0, 0, 600, 400);
                plothari(ctx, (date.getHours() % 12) * Math.PI / 6, 100);
                plothari(ctx, date.getMinutes() * Math.PI / 30, 150);
                plothari(ctx, date.getSeconds() * Math.PI / 30, 200);
            }
            setInterval(tick, 500);
        }
    </script>
</body>
</html>

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科