レベル: 中級 Greg Travis, Software Engineer, Google
2008年 09月 23日 ドラッグ・アンド・ドロップ機能を使って Web ページのセクションを移動させる手法を学んでください。対話性の異なる側面をそれぞれ個別に実装した後、まとめて 1 つに組み立てることによって、Web ユーザーを大いに満足させられる柔軟なカスタマイズを可能にします。
JavaScript は Web ベースのアプリケーションを作成するための強力な言語です。この言語は安定性と機能の豊富さという点で、従来のデスクトップ・アプリケーションに匹敵するプログラムを作成できるまでに成熟しました。しかし JavaScript は、元来静的な Web ページに対話性を加えるための言語として始まったもので、今でもこの目的で使用されています。
これから紹介する手法の難関の 1 つは、JavaScript コードと対話動作できるように適切にページを構成することです。一般に、ページを構成するときには JavaScript コードを頭に入れながら構成するものですが、多くの場合はページを構成した後に新しい対話機能を追加する必要が出てきます。後から追加するとなると、多少の巧妙さが必要になってきます。なぜなら JavaScript コードが文書構造をトラバースし、適切な場所にコードを追加し、そして通常は既存の構造だけでなく、ページ上にすでに存在する JavaScript コードに干渉しないようにしなければならないためです。要するに、他への影響ができるだけ少ないシステムにしなければなりません。
 |
よく使われる頭字語
- UI: User interface
- GUI: Graphical user interface
- HTML: Hypertext Markup Language
- XML: Extensible Markup Language
- DOM: Document Object Model
|
|
交換可能なシステム
この記事で紹介するのは、ページを構成するセクションを移動できるようにしてページをアクティブにするという手法です。具体的に言うと、交換可能なセクションを別の交換可能なセクションにドラッグ・アンド・ドロップして 2 つのセクションを入れ替えることができるようにします。
このようなセクションをアクティブにするために必要な作業は、セクションに class パラメーターを追加して JavaScript ファイルをロードするだけです。このコードをアクティブにするには、onload メソッドを <body> タグに追加します。こうすることで、ページのロードが完了した直後にこのメソッドによってコードが起動されます。後の作業はすべてコードが引き受けます。
注: この記事に記載するサンプルのソース・コードは、「ダウンロード」セクションから入手することができます。
さらに、このコードは、できる限り抽象化を使用して構成します。しかし、プログラムの異なる要素が不必要に結び付けられることは珍しくありません。それは、UI コードに至っては尚更のことです。交換可能システムは複数の部分で構成され、そのそれぞれが対話性の異なる部分を実装します。これらの部分を組み合わせて連動させることで、簡単に実験して微調整できる UI には欠かせない、単純でシームレスなインターフェースを実現することができます。
交換可能なインターフェース
この交換可能システムの使い方は簡単です。Web ページの設計者が特定のセクションに対して swappable という指定をすると、交換可能なあらゆる要素をクリックして別の交換可能な要素にドラッグできるようになります。そしてマウスのボタンを放すと、この 2 つの要素が入れ替わります。
何が行われているかを明らかにするために、その手がかりとして以下の 2 つの標準 GUI を使用します。
ドラッグした要素を強調表示する
最初に交換可能要素をクリックすると、カーソルの下に半透明の枠が現れます。coveringDiv() 関数によって作成されるこの枠は、要素を正確に囲みます。別の要素にドラッグするのは、実際にはこの枠です。ドラッグ操作中は半透明の枠だけが動き、元の要素はマウスのボタンが放されるまでその場に留まります。
ドラッグ・ターゲットを強調表示する
行われていることを明らかにするもう 1 つの重要な手がかりは、ドラッグ先の要素を明確に識別することです。半透明の枠を動かすと、それに伴ってカーソルがさまざまな交換可能要素に移動します。カーソルが交換可能要素に重なると、別の半透明の枠が現れてその要素が強調表示されます。この強調表示によって、ドラッグのターゲットとする要素が特定されるというわけです。マウスのボタンを放した時点で、ドラッグした要素とドラッグ・ターゲットの要素の場所が入れ替わり、表示されていた半透明の枠は次の交換が行われるまで消えてなくなります。
システムをアクティブにする
前述したように、コードはできる限り他に影響を与えないものにしなければなりません。つまり、HTML または XML で作業しているページの設計者が交換可能システムに対処しなくても済むようにするということです。これは、彼らの仕事ではありません。
ページに必要となるのは、以下の 3 つの要素だけです。
- JavaScript タグ
<body> タグ内の onload メソッド
- swappable と指定した交換可能領域
JavaScript タグ
ページ・ファイルの先頭に、以下のタグを配置してください。
<script src="rearrange-your-page.js"></script>
|
このタグはロード・プロセスの初期にロードされますが、body タグ内の onload 関数が呼び出されるまでは実行されません。
body タグ内の onload メソッド
この onload メソッドは、ページ全体がロードされた時点で交換可能システムを起動します。このタイミングは重要です。このコードが最初に行うのは、ページ全体を検索して交換可能な要素を見つけることなので、これらの要素が確実にロード済みになっていなければならないためです。そこで、body タグ内の onload メソッドはリスト 1 のようにする必要があります。
リスト 1. body タグ内の onload ハンドラー
<body onload="swappable_start();">
... rest of page
</body>
|
swappable と指定した交換可能領域
交換可能にする領域のそれぞれに、class パラメーターを使用して swappable と指定してください。ここでページの作成者や設計者が注意しなければならないのは、このパラメーターを交換可能なすべてのセクションに追加するという点だけです。リスト 2 を参照してください。
リスト 2. class に swappable を設定した div のアノテーション
<div class='swappable'>
lorem ipsum lorem ipsum
</div>
|
交換可能なセクションを検索する
コードが最初に実行しなければならないことは、アクティブにするページのセクションを見つけ出すことです。前にも説明しましたが、セクションをアクティブにするためには、そのセクションを囲むタグに class パラメーターを設定すればよいだけです。そのため、交換可能なセクションを検出するには、class が swappable に設定されたすべてのタグを探します。この関数は標準 DOM ライブラリーには含まれていませんが、実装するのは難しい作業ではありません。リスト 3 に実装例を示します。
リスト 3. getElementsByClass() の実装
// By Dustin Diaz
function getElementsByClass(searchClass,node,tag) {
var classElements = new Array();
if ( node == null )
node = document;
if ( tag == null )
tag = '*';
var els = node.getElementsByTagName(tag);
var elsLen = els.length;
var pattern = new RegExp("(^|\\\\s)"+searchClass+"(\\\\s|$)");
for (i = 0, j = 0; i < elsLen; i++) {
if ( pattern.test(els[i].className) ) {
classElements[j] = els[i];
j++;
}
}
return classElements;
}
|
対話性を持つ要素
プログラムをビルドするには、機能のさまざまな部分を組み合わせて 1 つに統合します。その方法はプログラマーによってそれぞれに異なりますが、原則として、プログラムは数少ない大きな部分からビルドするよりは、多数の小さな部分からビルドしたほうが得策です。それぞれの小さな部分が 1 つの機能を果たすため、セマンティクスが明確になります。
しかしこのようなビルド方法は、GUI をプログラミングする場合には簡単にはいきません。優れた GUI であれば、多数のインターフェース要素の調整を取り、それぞれの振る舞いを 1 つの全体的な振る舞いとして統合して直感的に機能させなければならないためです。イベント・ベースのシステムは、複雑な連係動作によって結合された融通の利かないコールバックのセットとなってしまうことがよくあります。モジュール式の対話型要素を作成するには、慎重さが求められます。
モジュール式の対話型要素
交換可能システムのコードでは、モジュール式の対話型要素を使用するようにします。前に、交換可能システムの対話性には 2 つの主要な要素があると説明しました。1 つはドラッグした要素の強調表示、そしてもう 1 つはドラッグ・ターゲットの強調表示です。コードには、この 2 つの要素を別々に実装します。
対話性をモジュール化する場合に何が厄介かを説明する好例は、交換可能インターフェースの状態記述として、対話性を持つこの 2 つの要素が極めて密接に結び付けられていることです。強調表示とその解除はすべて単一のマウスの動きのなかで行われますが、そのそれぞれはマウス入力の異なる側面に対応して行われます。2 つの要素を 1 つの一体化したコードとして実装したとすると、同時にいくつものことが進行することになるため、コードが読みにくくなってしまいます。
ドラッグ・ハンドラー
GUI 実装をモジュール化するためにここで使用するのは、ドラッグ・ハンドラーと呼ぶものです。これは GUI システムに組み込まれるイベント・ハンドラーと同様のものですが、イベント・ハンドラーはある種の単一のイベントを処理する一方、ドラッグ・ハンドラーはドラッグ・アンド・ドロップ・プロセス全体を処理します。ドラッグ・ハンドラーは、単一のイベントではなく、イベントのシーケンスに対処するハンドラーの一例です。リスト 4 に、ドラッグ・ハンドラーのスケルトンを記載します。
リスト 4. ドラッグ・ハンドラーのスケルトン
{
start:
function( x, y ) {
// ...
},
move:
function( x, y ) {
// ...
},
done:
function() {
// ...
},
}
|
このドラッグ・ハンドラーは、start、move、done という 3 つのメソッドを持つオブジェクトです。ドラッグ・アンド・ドロップ操作を開始すると、start メソッドが呼び出され、このメソッドによってクリックした地点の座標が渡されます。カーソルを動かすと、その動きに従って move メソッドが繰り返し呼び出され、同じくカーソルの現在位置の座標が渡されます。そして最後にマウスのボタンを放すと、done メソッドが呼び出されます。
交換可能システムは 2 つの異なるドラッグ・ハンドラーを同時に使用することによって、複雑に関係する連係動作の 2 つの側面に手際良く対処できるようにします。連係動作の一方がもう一方の動作の一部になってしまうのは適切でないため、連係動作を 2 つに分けるとともに、シームレスかつ同時に使用できるようにします。
rectangle_drag_handler
2 つのドラッグ・ハンドラーのうち、最初のドラッグ・ハンドラーは rectangle_drag_handler です。このハンドラーは、ドラッグ対象の要素を表す半透明の枠を移動する役割を果たします。リスト 5 に、start メソッドを記載します。
リスト 5. rectangle_drag_handler ハンドラー
function rectangle_drag_handler( target )
{
this.start = function( x, y ) {
this.cover = coveringDiv( target );
make_translucent( this.cover, .6 );
this.cover.style.backgroundColor = "#777";
dea( this.cover );
this.dragger = new dragger( this.cover, x, y );
};
// ...
}
|
上記の start メソッドが半透明の枠を作成し、作成した枠を dragger という別のオブジェクトに渡します。dragger は、カーソルの動きに応じて DOM 要素を移動させるオブジェクトです。現在のカーソル座標を dragger オブジェクトに渡すと、このオブジェクトがドラッグされたオブジェクトを更新し、カーソルに併せて移動させます。
リスト 6 を見るとわかるように、この dragger を更新するのは move メソッドです。
リスト 6. dragger の更新
this.move = function( x, y ) {
this.dragger.update( x, y );
};
|
最後にドラッグ・アンド・ドロップ・プロセスが終了した時点で、リスト 7 に記載する done メソッドが半透明の枠を除去します。
リスト 7. rectangle_drag_handler の done メソッド
this.move = function( x, y ) {
this.done = function() {
this.cover.parentNode.removeChild( this.cover );
};
}
|
ドラッグ・ハンドラーを組み合わせる
ここで、2 つのドラッグ・ハンドラーを同時に使用する方法を見つけなければなりません。その方法としては、compose_drag_handlers() 関数を使用すると簡単です。この関数は任意の 2 つのドラッグ・ハンドラーを引数に取り、1 つのドラッグ・ハンドラーとして結合します。この結合ドラッグ・ハンドラーを通常のドラッグ・ハンドラーを使用するような場所で使用すると、結合前の 2 つのドラッグ・ハンドラーの振る舞いがシームレスに統合される結果となります。
compose_drag_handlers() 関数は簡単に作成することができます。これはドラッグ・ハンドラーのように見えますが、すべてのメソッドが呼び出すのは、2 つの結合前のドラッグ・ハンドラーでの対応するメソッドです。リスト 8 に、この関数を記載します。
リスト 8. compose_drag_handlers()
function compose_drag_handlers( a, b )
{
return {
start:
function( x, y ) {
a.start( x, y );
b.start( x, y );
},
move:
function( x, y ) {
a.move( x, y );
b.move( x, y );
},
done:
function() {
a.done();
b.done();
},
}
}
|
ご覧のように、ドラッグ・ハンドラー a と b が組み合わせられて 1 つの結合ドラッグ・ハンドラーになります。例えばこの結合ハンドラーの start() メソッドを呼び出すと、a.start() が呼び出され、続いて b.start() が呼び出されます。
compose_drag_handlers は、prepare_swappable() というセットアップ関数のなかで呼び出します (リスト 9 を参照)。
リスト 9. prepare_swappable() 関数
function prepare_swappable( o )
{
swappables.push( o );
var sdp = new rectangle_drag_handler( o );
var hdp = new highlighting_drag_handler( o );
var both = compose_drag_handlers( sdp, hdp );
install_drag_handler( o, both );
}
|
この関数は何よりも、交換可能な要素の rectangle_drag_handler と highlighting_drag_handler を作成し、この 2 つを組み合わせて 1 つの結合ドラッグ・ハンドラーにします。そして最終的にこの結合ドラッグ・ハンドラーを要素に対してアクティブにするための手段として、次の 2 つのセクションで説明する install_drag_handler() が呼び出されます。
マウス・ハンドラーを安全にインストールする
通常のイベント・ハンドラーとは異なり、ドラッグ・ハンドラーは複数のイベントを処理します。それにも関わらず、ドラッグ・ハンドラーはやはり通常のイベント・ハンドラーと同じくオブジェクトに接続する必要があります。
どのような種類のイベント・ハンドラーであれ、インストールは厄介な作業になります。それは、変更対象の要素にはイベント・ハンドラーがすでにインストールされている可能性が常にあるからです。インストールされているイベント・ハンドラーを置き換えた場合、ページの振る舞いが変わってくることもあります。
このような事態を避けるために使用するのが、リスト 10 に記載する install_mouse_handlers() という関数です。
リスト 10. install_mouse_handlers() 関数
function install_mouse_handlers( target, onmouseup, onmousedown, onmousemove )
{
var original_handlers = {
onmouseup: target.onmouseup,
onmousedown: target.onmousedown,
onmousemove: target.onmousemove
};
target.onmouseup = onmouseup;
target.onmousedown = onmousedown;
target.onmousemove = onmousemove;
return {
restore: function() {
target.onmouseup = original_handlers.onmouseup;
target.onmousedown = original_handlers.onmousedown;
target.onmousemove = original_handlers.onmousemove;
}
};
}
|
install_mouse_handlers() 関数は指定されたマウス・ハンドラーを指定されたオブジェクトに追加します。また、元のハンドラーをリストアするために使用できるオブジェクトを返します。このようにすれば、ドラッグ・アンド・ドロップ・シーケンスの終了時に restore() 関数を呼び出して、ドラッグ・アンド・ドロップ・シーケンスが開始する前の状態に戻すことが可能になります。
ドラッグ・ハンドラーをアクティブにする
ドラッグ・アンド・ドロップ操作のドラッグ・ハンドラーは、onmousedown、onmouseup、onmousemove という 3 つのマウス・ハンドラーをすべて使用します。ただし、最初にインストールする必要があるのは mousedown ハンドラーだけです。最初の段階では、ドラッグ・アンド・ドロップ・シーケンスを開始するクリックをまだ待っている状態だからです。
クリックが行われたら、次に mousemove ハンドラーと mouseup ハンドラーをインストールします。また、クリックしたこの時点で mousedown ハンドラーは不要になります。このハンドラーは除去され、ドラッグ・アンド・ドロップ・シーケンスが完了したときにリストアされることになります。mousedown ハンドラーの初期状態は、リスト 11 のとおりです。
リスト 11. 初期の mousedown ハンドラー
var onmousedown = function( e ) {
var x = e.clientX;
var y = e.clientY;
p.start( x, y );
var target_handler_restorer = null;
var document_handler_restorer = null;
var onmousemove = function( e ) {
var x = e.clientX;
var y = e.clientY;
p.move( x, y );
};
var onmouseup = function( e ) {
p.done();
target_handler_restorer.restore();
document_handler_restorer.restore();
};
target_handler_restorer =
install_mouse_handlers( target, onmouseup, null, onmousemove );
document_handler_restorer =
install_mouse_handlers( document, onmouseup, null, onmousemove );
e.stopPropagation();
return false;
};
|
ドラッグ・アンド・ドロップ・シーケンスを開始して mousedown ハンドラーを呼び出すと、このハンドラーが onmousemove および onmouseup ハンドラーを作成してターゲット要素にインストールします。その際にはもちろん、install_mouse_handlers() を使用して、後で簡単にアンインストールできるようにします。
これらのハンドラーは文書オブジェクトにインストールしなければならないということにも注意してください。これは重要な点です。なぜならユーザーはドラッグ・アンド・ドロップ・シーケンスの間、カーソルをページ全体に動かす可能性があるためで、マウスが交換可能な要素の外側に移動した場合 (その可能性は大です) でも、これらのイベントを受け取らなければなりません。そのため、この場合も同じく install_mouse_handlers() を使用して、ハンドラーを後でリストアできるようにします。
すべてを組み立てる
この記事ではさまざまなクラスと関数を説明しました。その 1 つひとつは極めて単純なものですが、全体としてどのように連動するかを確認することも重要です。
以下のリストに、ドラッグ・アンド・ドロップ・プロセス全体を要約します。
- 交換可能な要素をクリックします。
- クリックされた要素の
onmousedown ハンドラーが呼び出され、それによって onmousemove ハンドラーと onmouseup ハンドラーがインストールされます。
- マウスを動かすと、この 2 つのハンドラーが呼び出されます。
- 2 つのハンドラーは、事前にインストールされているドラッグ・ハンドラーを呼び出します。
- ドラッグ・ハンドラーは実際には、2 つの異なるドラッグ・ハンドラーの作用を組み合わせた結合ドラッグ・ハンドラーです。
- 2 つのドラッグ・ハンドラーのうちの 1 つ、
rectangle_drag_handler は、ドラッグされている要素を表す半透明の枠をカーソルに付加します。
- もう一方のドラッグ・ハンドラー、
highlighting_drag_handlerはカーソルが重なった交換可能要素を強調表示し、どこに要素をドラッグできるかを示します。
- ドラッグ先の要素でマウスのボタンを放すと、
highlighting_drag_handler の done() メソッドが 2 つの要素を入れ替えます。2 つのドラッグ・ハンドラーはアンインストールされ、元の onmousedown ハンドラーが残されて次のドラッグ・アンド・ドロップ・シーケンスを開始できる状態にします。
まとめ
ドラッグ・アンド・ドロップは比較的簡単な操作ですが、この操作には、ユーザーの入力を追跡し、プロセス全体での迅速なフィードバックを可能にする、いくつかの対話プロセスが伴います。この記事では、モジュール式の対話型要素を組み合わせて 1 つに結合することにより、完全な GUI を作成する方法を実例で紹介しました。
この手法にはいくつかの利点があります。その 1 つは、コードはモジュール式なので、作成するのが簡単で、管理するにも容易だという点です。関数やクラスのコードはいずれも 40 行を超えていません。さらに、大抵の場合はそれよりも短いコードになります。
対話型要素のそれぞれが通常の GUI プロセスまたは効果を実装するため、別のコンテキストでも簡単に使用できることは想像に難くありません。このような対話型要素を十分に揃えたライブラリーを開発すれば、パーツを組み合わせるという方法で高度な UI を簡単に作成できるようになるはずです。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Source code, with example.1 | rearrange-your-page-src.zip | 181KB | HTTP |
|---|
注 - 交換可能ライブラリーのソース・コード、そしてこのソース・コードを使用したサンプル・ページが含まれています。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | |  | Greg Travis は Google のソフトウェア・エンジニアです。彼はこれまで、Web プログラマーとして、独立の契約エンジニアとして、そしてゲーム開発者として働いてきました。 |
記事の評価
|