16 KiB
テラリウムプロジェクト パート3: DOM操作とクロージャ
スケッチノート: Tomomi Imura
講義前クイズ
はじめに
DOM(Document Object Model)の操作は、ウェブ開発の重要な側面です。MDNによると、「Document Object Model (DOM) は、ウェブ上のドキュメントの構造と内容を構成するオブジェクトのデータ表現です。」DOM操作の課題は、しばしばJavaScriptフレームワークを使用してDOMを管理する動機となりますが、ここではバニラJavaScriptを使って自力で管理します!
さらに、このレッスンではJavaScriptのクロージャの概念を紹介します。クロージャとは、ある関数の中に別の関数が閉じ込められており、内側の関数が外側の関数のスコープにアクセスできる仕組みのことです。
JavaScriptのクロージャは広範で複雑なトピックです。このレッスンでは、基本的なアイデアに触れるだけです。このテラリウムのコード内で、クロージャ(内側の関数と外側の関数が構築され、内側の関数が外側のスコープにアクセスできる仕組み)を見つけることができます。詳細については、豊富なドキュメントをご覧ください。
このレッスンでは、クロージャを使ってDOMを操作します。
DOMを木構造として考えてみてください。これは、ウェブページのドキュメントを操作するためのすべての方法を表しています。さまざまなAPI(アプリケーションプログラムインターフェース)が作成されており、プログラマーは選んだプログラミング言語を使ってDOMにアクセスし、編集、変更、再配置、その他の管理を行うことができます。
DOMとそれを参照するHTMLマークアップの表現。Olfa Nasraouiより
このレッスンでは、JavaScriptを作成して、ユーザーがページ上の植物を操作できるようにすることで、インタラクティブなテラリウムプロジェクトを完成させます。
前提条件
テラリウムのHTMLとCSSを作成済みである必要があります。このレッスンの終わりまでに、植物をドラッグしてテラリウムに出し入れできるようになります。
タスク
テラリウムフォルダ内に、新しいファイルscript.js
を作成します。そのファイルを<head>
セクションにインポートします:
<script src="./script.js" defer></script>
注: 外部JavaScriptファイルをHTMLファイルにインポートする際には、
defer
を使用して、HTMLファイルが完全に読み込まれた後にJavaScriptが実行されるようにします。また、async
属性を使用して、HTMLファイルの解析中にスクリプトを実行することもできますが、今回の場合、ドラッグスクリプトを実行する前にHTML要素が完全に利用可能である必要があります。
DOM要素
最初に行うべきことは、DOM内で操作したい要素への参照を作成することです。今回の場合、サイドバーにある14個の植物が対象です。
タスク
dragElement(document.getElementById('plant1'));
dragElement(document.getElementById('plant2'));
dragElement(document.getElementById('plant3'));
dragElement(document.getElementById('plant4'));
dragElement(document.getElementById('plant5'));
dragElement(document.getElementById('plant6'));
dragElement(document.getElementById('plant7'));
dragElement(document.getElementById('plant8'));
dragElement(document.getElementById('plant9'));
dragElement(document.getElementById('plant10'));
dragElement(document.getElementById('plant11'));
dragElement(document.getElementById('plant12'));
dragElement(document.getElementById('plant13'));
dragElement(document.getElementById('plant14'));
ここで何が起こっているのでしょうか?ドキュメントを参照し、そのDOMを調べて特定のIdを持つ要素を見つけています。HTMLの最初のレッスンで、各植物画像に個別のId(id="plant1"
)を付けたことを思い出してください。その作業がここで役立ちます。各要素を特定した後、それをdragElement
という関数に渡します。この関数は後で作成します。これにより、HTML内の要素がドラッグ可能になります。
✅ なぜ要素をIdで参照するのでしょうか?CSSクラスで参照するのではなく?CSSに関する前回のレッスンを振り返ってみてください。
クロージャ
次に、dragElement
クロージャを作成します。これは、内側の関数を囲む外側の関数です(今回の場合、内側の関数は3つあります)。
クロージャは、1つ以上の関数が外側の関数のスコープにアクセスする必要がある場合に便利です。以下はその例です:
function displayCandy(){
let candy = ['jellybeans'];
function addCandy(candyType) {
candy.push(candyType)
}
addCandy('gumdrops');
}
displayCandy();
console.log(candy)
この例では、displayCandy
関数が、既存の配列に新しいキャンディタイプを追加する関数を囲んでいます。このコードを実行すると、candy
配列は未定義になります。これは、candy
がローカル変数(クロージャ内のローカル)であるためです。
✅ candy
配列をアクセス可能にするにはどうすればよいでしょうか?配列をクロージャの外側に移動してみてください。これにより、配列はグローバルになり、クロージャのローカルスコープに限定されなくなります。
タスク
script.js
内の要素宣言の下に、次の関数を作成します:
function dragElement(terrariumElement) {
//set 4 positions for positioning on the screen
let pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
terrariumElement.onpointerdown = pointerDrag;
}
dragElement
は、スクリプトの冒頭で宣言されたterrariumElement
オブジェクトを取得します。そして、関数に渡されたオブジェクトのローカル位置を0
に設定します。これらは、ドラッグ&ドロップ機能を各要素に追加する際に操作されるローカル変数です。テラリウムはこれらのドラッグされた要素によって埋められるため、配置場所を追跡する必要があります。
さらに、この関数に渡されたterrariumElement
は、pointerdown
イベントを割り当てられます。これは、ウェブAPIの一部であり、DOM管理を支援するために設計されています。onpointerdown
は、ボタンが押されたとき、または今回の場合はドラッグ可能な要素がタッチされたときに発火します。このイベントハンドラは、いくつかの例外を除いて、ウェブおよびモバイルブラウザの両方で動作します。
✅ イベントハンドラonclick
は、クロスブラウザでのサポートがはるかに多いですが、ここではなぜ使用しないのでしょうか?作成しようとしている画面操作の正確なタイプについて考えてみてください。
Pointerdrag関数
terrariumElement
はドラッグ可能な状態になりました。onpointerdown
イベントが発火すると、pointerDrag
関数が呼び出されます。この関数を次の行の下に追加します:terrariumElement.onpointerdown = pointerDrag;
:
タスク
function pointerDrag(e) {
e.preventDefault();
console.log(e);
pos3 = e.clientX;
pos4 = e.clientY;
}
いくつかのことが起こります。まず、e.preventDefault();
を使用して、通常pointerdown
で発生するデフォルトイベントが発生しないようにします。これにより、インターフェースの動作をより細かく制御できます。
スクリプトファイルを完全に構築した後、この行を削除して試してみてください。何が起こるでしょうか?
次に、ブラウザウィンドウでindex.html
を開き、インターフェースを検査します。植物をクリックすると、e
イベントがどのようにキャプチャされるかを見ることができます。このイベントを掘り下げて、1回のpointerdown
イベントでどれだけの情報が収集されるかを確認してください。
次に、ローカル変数pos3
とpos4
がe.clientX
に設定される方法に注目してください。これらの値は、クリックまたはタッチした瞬間の植物のx座標とy座標をキャプチャします。植物をクリックしてドラッグする際の動作を細かく制御する必要があるため、その座標を追跡します。
✅ なぜこのアプリ全体が1つの大きなクロージャで構築されているのか、より明確になってきましたか?もしそうでなければ、14個のドラッグ可能な植物それぞれのスコープをどのように維持するか考えてみてください。
初期関数を完成させるために、pos4 = e.clientY
の下に2つのポインターイベント操作を追加します:
document.onpointermove = elementDrag;
document.onpointerup = stopElementDrag;
これで、植物をポインターと一緒にドラッグできるようになり、植物の選択を解除するとドラッグジェスチャーが停止します。onpointermove
とonpointerup
は、onpointerdown
と同じAPIの一部です。インターフェースは現在エラーをスローしますが、これはelementDrag
とstopElementDrag
関数がまだ定義されていないためです。次にこれらを構築します。
elementDragとstopElementDrag関数
クロージャを完成させるために、植物をドラッグしたときとドラッグを停止したときに何が起こるかを処理する2つの内部関数を追加します。目指す動作は、いつでもどの植物でもドラッグでき、画面上のどこにでも配置できることです。このインターフェースは非常に自由度が高く(ドロップゾーンなどはありません)、植物を追加、削除、再配置することで、テラリウムを自由にデザインできます。
タスク
pointerDrag
の閉じ中括弧の直後にelementDrag
関数を追加します:
function elementDrag(e) {
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
console.log(pos1, pos2, pos3, pos4);
terrariumElement.style.top = terrariumElement.offsetTop - pos2 + 'px';
terrariumElement.style.left = terrariumElement.offsetLeft - pos1 + 'px';
}
この関数では、冒頭で設定したローカル変数1〜4を大幅に編集します。ここで何が起こっているのでしょうか?
ドラッグ中に、pos1
をpos3
(以前にe.clientX
として設定)から現在のe.clientX
値を引いた値に再割り当てします。同様の操作をpos2
にも行います。その後、pos3
とpos4
を要素の新しいX座標とY座標にリセットします。これらの変更をドラッグ中にコンソールで確認できます。その後、植物のCSSスタイルを操作して、新しい位置に基づいて植物の新しい位置を設定します。これには、オフセットとこれらの新しい位置を比較して植物の上部と左側のX座標とY座標を計算します。
offsetTop
とoffsetLeft
は、親要素に基づいて要素の位置を設定するCSSプロパティです。親要素はstatic
以外の位置に設定されている必要があります。
これらの位置の再計算により、テラリウムとその植物の動作を微調整できます。
タスク
インターフェースを完成させる最後のタスクは、elementDrag
の閉じ中括弧の後にstopElementDrag
関数を追加することです:
function stopElementDrag() {
document.onpointerup = null;
document.onpointermove = null;
}
この小さな関数は、onpointerup
とonpointermove
イベントをリセットします。これにより、植物の進行を再開するか、新しい植物をドラッグし始めることができます。
✅ これらのイベントをnullに設定しないとどうなるでしょうか?
これでプロジェクトが完成しました!
🚀チャレンジ
クロージャに新しいイベントハンドラを追加して、植物にさらに何かをさせてみましょう。たとえば、植物をダブルクリックして最前面に持ってくるなどです。創造力を発揮してください!
講義後クイズ
復習と自己学習
画面上の要素をドラッグするのは一見簡単そうに見えますが、これを実現する方法は多岐にわたり、求める効果によっては多くの落とし穴があります。実際、ドラッグ&ドロップAPIというものがあり、試してみることができます。このモジュールでは、求める効果が少し異なるため使用しませんでしたが、自分のプロジェクトでこのAPIを試してみてください。
ポインターイベントに関する詳細は、W3CドキュメントやMDNウェブドキュメントで確認できます。
常にCanIUse.comを使用してブラウザの互換性を確認してください。
課題
免責事項:
この文書はAI翻訳サービスCo-op Translatorを使用して翻訳されています。正確性を追求しておりますが、自動翻訳には誤りや不正確な部分が含まれる可能性があります。元の言語で記載された文書が正式な情報源とみなされるべきです。重要な情報については、専門の人間による翻訳を推奨します。この翻訳の使用に起因する誤解や誤解釈について、当社は責任を負いません。