デスクトップアプリケーションの操作性の再現
要素の選択と移動は、すべてのデスクトップマネージャの基本です。ここでは、この2つを組み合わせてWebページ内で同じエクスペリエンスを再現し、デスクトップ上でのアイコン操作と同様にユーザーがアイテムを選択して移動できるようにします。選択されたアイテムを追跡して、それらのアイテムが一緒に移動するようにdrag()ハンドラを修正する方法をリスト4に示します。
$('.box').draggable({
// ... omissis
drag: function(ev, ui) {
// if we are dragging a selected item, move
// all the others selected ones as well.
if ($sel.isSelected(ui.helper[0].id)) {
for (id in $sel.allSelected()) {
if (id != ui.helper[0].id) {
// update their initial position
// (stored in the top0 and left0 attributes)
// with the relative offset of the current drag
// (stored in ui.position)
$('#' + id).css(
'top',
$('#'+id).data("top0") + ui.position.top);
$('#' + id).css(
'left',
$('#'+id).data("left0") + ui.position.left);
}
}
}
},
// ... omissis
});
パンとエンドレスデスクトップ
ここまでは、ありふれた機能を再現しただけです。ここでスパイスを少し加えて、新たな動作を追加しましょう。表示するアイテムが多いと、1ページに収まらないことがあります。その場合、単純にページサイズを大きくしてスクロールバーを表示することもできますが、空間的なナビゲーションが必要になると厄介なことになります。それよりも良い代替策として、一種のエンドレスデスクトップで自由にパンする方法をユーザーに提供することが考えられます。Googleマップを使ったことがあれば、この考え方は既におなじみのはずです。つまり、マウスを使って地図をドラッグすることで、ブラウザのウィンドウに表示される部分を変更できるということです。
これと同じ効果を再現するには、Webページの構造を変更しなければなりません。これを達成するためには、次の3つのレイヤが必要になるからです。
- ''ユニバースレイヤ''―すべてのアイテムとオブジェクトが置かれているレイヤです。
- ''ビューポートレイヤ''―ユーザーが直接見るものであり、境界外の全コンテンツをクリッピングします。ユニバースレイヤの上に位置します。
- ''スクロールレイヤ''―マウスドラッグイベントをトラップして、相対動作をユニバースレイヤに伝達するためだけの透明なレイヤです。このレイヤはボタンによってアクティブになり、ビューポートをアクセス可能な状態に維持します。
図4にエンドレスデスクトップ設定の概念図を示します。
このレイアウトを実装するには、3種類のDIVを使用し、それらのCSS属性を慎重に選びます。最も重要なのがoverflow:hiddenです。これによりビューポートDIVをクリップ領域に変換します。詳しくはリスト5を見てください。
<style type="text/css" media="screen">
#scroll-overlay {
background: transparent url(img/scroll_big.png) no-repeat center center;
position: absolute;
z-index: -1;
cursor: move;
}
/* This is the viewport layer */
#container {
position: absolute;
top: 150;
left: 0;
overflow: hidden;
width: 300px;
height: 300px;
}
#universe {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
</style>
<!-- ... omissis -->
<div id="container">
<div id="universe">
<div id="box1" class="box">Blue</div>
<!-- All your contents here -->
</div>
<!-- When the scroll-trigger is clicked, it toggles
the scroll layer -->
<div id="scroll-trigger" title="Click to pan around" >
</div>
</div>
<!-- The scroll layer, initially hidden, with a button to
dismiss it, once the panning is done -->
<div id="scroll-overlay" style="display: none">
<div><button id="scroll-done">Done</button></div>
</div>
残っている仕事は、ドラッグ動作をスクロールレイヤからユニバースレイヤに伝達するコードを書くことです(リスト6を参照)。
// scroll-overlay is the id of the scroll layer.
$('#scroll-overlay').draggable({
helper: function() {
return $("<div />");
},
start: function(ev, ui) {
// detect the current universe offset
var offset = $('#universe').offset();
$('#universe')
.data("top0", offset.top)
.data("left0", offset.left);
},
drag: function(ev, ui) {
// apply to the universe the same relative movement
// that occurred to the scroll layer.
var universe = $('#universe');
var offset = universe.offset();
// dragDelta contains the initial offset between
// the universe and scroll layers, if any.
var dragTop = ui.position.top +
universe.data("top0") - dragDelta.top;
var dragLeft = ui.position.left +
universe.data("left0") - dragDelta.left;
$('#universe')
.css('top', dragTop).css('bottom', -dragTop)
.css('left', dragLeft).css('right', -dragLeft);
}
});
図5に最終結果を示します。

ズーム
実装すべき最後の機能はズームです。考え方は比較的単純です。マウスホイールの上下の動きを検知し、そうした情報を使ってユーザーインターフェース内でアイテムを拡大縮小します。マウスホイールプラグインを利用すると、jQueryでさらにmousewheel()というコールバックを使用できるようになります。このコールバックはマウスホイールの操作が行われるたびに呼び出されます。このコールバックのdeltaパラメータにはホイールの移動量が含まれます。
このコールバックがトリガーされたら、サイズ関連の適切なCSS属性を調整してDOM要素を拡大または縮小するようにします。ここで調整する属性としては、フォントサイズ、パディング、マージン、幅、高さがあります。ラスタイメージの拡大縮小には、もっと複雑な動作が必要になることがあります。つまり、ピクセル化(モザイク)を防ぐために、同じイメージを異なる解像度で格納しなければならないことがあります。
この例では、DOM要素で絶対位置指定を使用しているので、それらの要素の位置属性(CSSのtopおよびleft属性)を調整するときは特別な注意が必要になります。このような調整は、ズームがマウスの現在位置でセンタリングされるように、マウスの現在位置を基準にして行わなければなりません。イメージの拡大縮小がないズーム処理の完全なロジックについては、リスト7を参照してください。
// Scale a numeric attribute
function rescale(selector, property, percent) {
var oldValue = parseFloat($(selector).css(property));
$(selector).css(property, oldValue*percent);
}
// Scale according to a relative position
function rescaleRelative(selector, property, relative, percent) {
var oldValue = parseFloat($(selector).css(property)) -
relative;
$(selector).css(property, oldValue*percent +
relative);
}
$(document).ready(function() {
$('#container').mousewheel(function(event, delta) {
var containerTopOffset =
parseFloat($('#container').css('top'));
var containerLeftOffset =
parseFloat($('#container').css('left'));
// a scale factor for the delta received from the
// mousewheel event
var scale = 0.1;
var percent = 1 + delta*scale;
$('.box').each(function() {
// relative scale takes into account:
// - mouse position
// - container offset
// - scaled element centering (width and height)
rescaleRelative(this,
'top',
event.clientY -
containerTopOffset -
parseFloat($(this).css('height')) / 2,
percent);
rescaleRelative(this,
'left',
event.clientX -
containerLeftOffset -
parseFloat($(this).css('width')) / 2,
percent);
rescale(this, 'width', percent);
rescale(this, 'height', percent);
rescale(this, 'font-size', percent);
});
// prevent the mousewheel to trigger any
// scrollbars that may exist.
event.preventDefault();
});
});

