Drag & Drop - HTML4 版

 

記事のタイトルを、「Drag & Drop - フルスクラッチ版」から「Drag & Drop - HTML4 版」に変更しました。 (2008-09-19)

Ajax JavaScript Framework を使用せずに、フルスクラッチで作成したドラッグ&ドロップのサンプルです。

サンプルは、W3C DOM 対応ブラウザ(Firefox, Safari, Opera など) を前提として作成していますが、 DXBL(Diaspar Cross Browser Layer) を読み込むことにより、 IE (Internet Explorer) でも動作するようにしてあります。

サンプルについて、ブラウザごとの動作確認結果は Drag & Drop (Ajax JavaScript) にまとめてあります。

Demo

問題.次の表をドラッグ&ドロップにより完成させてください。

オリンピック 開催国
2004年(夏季)
アテネ
2006年(冬季)
トリノ
2008年(夏季)
ペキン
選択肢

  

ソースコード

ここでは JavaScript のみ掲載します。 CSS と HTML についてはDrag & Drop - 共通ソース(CSS, HTML) 全ソースコード を参照してください。

ソースコードにはブラウザへの依存コードを含まない方針で作成していますが、 不本意ながら、要素のクライアント座標を取得する関数 dom.getClientPosition については関数内で分岐処理を行っています。

DXBL の読み込み
<!--[if IE]>
<script type="text/javascript" src="/usr/lib/dxbl/0.4.1/dxbl.js"></script>
<![endif]-->

JavaScript
<script type="text/javascript">
//<![CDATA[
(function() {

//-----------------------------------------------------------------------------
// ドラッグ&ドロップの制御

var dnd = {}

dnd.dropzone = [];      // ドロップゾーン(ユーザアプリケーションで指定:1/2)
dnd.elmDrag;            // ドラッグ中の要素
dnd.offsetX;            // ドラッグ要素とマウスカーソルの位置関係 X
dnd.offsetY;            // ドラッグ要素とマウスカーソルの位置関係 Y

// ドラッグ開始
dnd.onDragBegin = function(event) {
    dnd.elmDrag = event.currentTarget;
    var scrpos = dom.getScrollPosition();
    var clipos = dom.getClientPosition(dnd.elmDrag);
    dnd.elmDrag.style.position = 'absolute';            // 絶対座標へ移行
    dnd.elmDrag.style.left     = (scrpos.left + clipos.left) + 'px';
    dnd.elmDrag.style.top      = (scrpos.top  + clipos.top)  + 'px';
    dnd.elmDrag.style.opacity  = 0.5;
    dnd.elmDrag.style.zIndex   = 1;
    dnd.offsetX = event.clientX - clipos.left;
    dnd.offsetY = event.clientY - clipos.top;
    document.addEventListener('mousemove', dnd.onDragMove, false);
    document.addEventListener('mouseup',   dnd.onDragEnd,  false);
    event.preventDefault();
}

// ドラッグ要素の表示位置を移動
dnd.onDragMove = function(event) {
    var scrpos = dom.getScrollPosition();
    dnd.elmDrag.style.left = (scrpos.left + event.clientX - dnd.offsetX) + 'px';
    dnd.elmDrag.style.top  = (scrpos.top  + event.clientY - dnd.offsetY) + 'px';
    dnd.getAcceptable(event);
    event.preventDefault();
}

// ドラッグ終了
dnd.onDragEnd = function(event) {
    dnd.elmDrag.style.position = 'static';              // 通常座標へ戻る
    dnd.elmDrag.style.left     = '0px';
    dnd.elmDrag.style.top      = '0px';
    dnd.elmDrag.style.opacity  = 1.0;
    dnd.elmDrag.style.zIndex   = 0;
    var elmDrop = dnd.getAcceptable(event);
    if (elmDrop) {
        dnd.onDragDrop(elmDrop, dnd.elmDrag);
    }
    document.removeEventListener('mousemove', dnd.onDragMove, false);
    document.removeEventListener('mouseup',   dnd.onDragEnd,  false);
    event.preventDefault();
}

// ドロップされたときの処理
dnd.onDragDrop = function(dropzone, draggable) {
    // (ユーザアプリケーションで指定:2/2)
}

// ドロップ受け入れ可能状態の要素を取得
dnd.getAcceptable = function(event) {
    for (idx in dnd.dropzone) {
        var elm = dnd.dropzone[idx];
        if (dom.inBounds(elm, event)) {
            elm.style.backgroundColor = 'orange';
            return elm;
        }
        else {
            elm.style.backgroundColor = 'white';
        }
    }
    return null;
}

//-----------------------------------------------------------------------------
// DOM ユーティリティ

var dom = {}

// ブラウザのスクロール量を取得
dom.getScrollPosition = function() {
    return {
        left: document.body.scrollLeft || document.documentElement.scrollLeft,
        top:  document.body.scrollTop  || document.documentElement.scrollTop
    }
}

// 要素のクライアント座標を取得
dom.getClientPosition = function(elm) {
    if (/*@cc_on!@*/false) {                    // IE
        return {
            left: elm.getBoundingClientRect().left - 2,
            top:  elm.getBoundingClientRect().top  - 2
        }
    }
    var result = { left: 0, top: 0 }
    if (!window.netscape && !window.opera && window.devicePixelRatio) { // Safari
        result.left -= elm.clientLeft;
        result.top  -= elm.clientTop;
        while (elm) {
            result.left += elm.offsetLeft + elm.clientLeft;
            result.top  += elm.offsetTop  + elm.clientTop;
            elm = elm.offsetParent;
        }
    }
    else {                                      // Firefox, Opera, Others...
        while (elm) {
            result.left += elm.offsetLeft;
            result.top  += elm.offsetTop;
            elm = elm.offsetParent;
        }
    }
    var scrpos = dom.getScrollPosition();
    result.left -= scrpos.left;
    result.top  -= scrpos.top;
    return result;
}

// 要素がクライアント座標上に占める領域を取得
dom.getClientBounds = function(elm) {
    var clipos = dom.getClientPosition(elm);
    return {
        left:   clipos.left,
        top:    clipos.top,
        right:  clipos.left + elm.offsetWidth  - 1,
        bottom: clipos.top  + elm.offsetHeight - 1
    }
}

// 要素の上にマウスカーソルが進入しているか?
dom.inBounds = function(elm, event) {
    var bounds = dom.getClientBounds(elm);
    if (event.clientX > bounds.left  &&
        event.clientX < bounds.right &&
        event.clientY > bounds.top   &&
        event.clientY < bounds.bottom) {
            return true;    // 進入している!
    }
    return false;
}

// クラス名で指定された要素を取得
dom.getElementsByClassName = function(name, node) {
    var result = [];
    var children = (node || document).getElementsByTagName('*');
    for (var idx = 0; idx < children.length; ++idx) {
        if (children[idx].className.match(new RegExp(name))) {
            result.push(children[idx]);
        }
    }
    return result;
}

//-----------------------------------------------------------------------------
// ユーザアプリケーション

var correct = { opt0: 'ans1', opt1: 'ans2', opt2: 'ans0' }  // 正解

// 採点
var mark = function(event) {
    var points = 0;
    var max = 0;
    for (key in correct) {
        var answer = document.getElementById(key).parentNode.id;
        points += (correct[key] == answer) ? 1: 0;
        ++max;
    }
    var score = Math.floor(points / max * 100);
    var judge = (score >= 70) ? '合格': '不合格';
    document.getElementById('result').innerHTML = judge + ':' + score + '%';
}

// ドロップされたときの処理
dnd.onDragDrop = function(dropzone, draggable) {
    if (dom.getElementsByClassName('draggable', dropzone).length == 0) {
        dropzone.appendChild(draggable);
    }
}

// 初期化
window.addEventListener('load', function() {
    dnd.dropzone  = dom.getElementsByClassName('dropzone');
    var draggable = dom.getElementsByClassName('draggable');
    for (idx in draggable) {
        draggable[idx].addEventListener('mousedown', dnd.onDragBegin, false);
    }
    document.getElementById('submit').addEventListener('click', mark, false);
    document.getElementById('wait').style.display = 'none';
}, false);

}());
//]]>
</script>

解説

座標計算の解説図

ソースコードの中で、座標計算に関連する変数については、次の図が理解の助けになると思います。

更新履歴

日付 内容
2008-09-19 情報 記事のタイトルを、「Drag & Drop - フルスクラッチ版」から「Drag & Drop - HTML4 版」に変更しました。
2007-12-31 追加 2007-06-26版の、 全ソースコード ページを追加。
2007-06-29 追加 座標計算の解説図を追加した。
2007-06-26 追加 Safari でクライアント座標を取得するための対策を追加した。
変更 全体的に座標の計算方法を分かりやすく書き直した。
2007-02-25 追加 IE で絶対座標を取得するための対策を追加した。
2007-02-17 変更 ソースコードを全面的に書き直した。
2006-09-18 追加 ドラッグ中のオブジェクトを最前面へ移動。 ドロップゾーンに先客が居る場合は、ドロップをキャンセル。
2006-05-25 初版 ドラッグ&ドロップによる解答機能、および採点機能を実装。