YUI2.7.0でショッピングカートを作ってみた。

先のログに書いたが、これまでのサンプルなどを「一まとめのコード」にしようとしている。
そのせいで、ログの更新も滞りがち。

以前にサンプリングした、YUIOfficial Examplesの中にDrag Dropの面白いサンプル(これ)があったので、それをモディファイして、ショッピングカートを作ってみた。ログを見返して驚いてしまったが、スクリプト中に記載したコメントが出鱈目だった。ついでに修正をしたが、以前にご覧になった方がいたのなら、大変失礼してしまった。ログを見返していて、「これじゃ動かない」という「物件」が2件も見つかり、これも修正。テストをしているつもりでも、コピペの間違いとかあるんだろうなぁ。とにかく、いい加減なことをたくさん書いてしまったので、見つけたら直して行こうと思う。(それに、自分はつくづく、コードが下手だなぁ、と実感。これも直していきたい)

話がわき道にそれてしまったが、今回のサンプルのトップ画面は、以下。
ショッピングカートと、(品物らしき)アイテムがあって、「アイテムをカートにドラッグ&ドロップするとお買い物」という「ありがち」なサンプル。
ショッピングカートのアイコンは、Siena Iconから使わせていただいたので、この場で感謝しておく。
インタラクションモードは、ドロップする側とドラッグされる側のインタラクションを規定するが、(ドロップゾーンが一つしかない)このサンプルでは本質的ではない。その定義上、Intercectと定義すると「緩い重なり」で「ドロップしたこと」になる。


item1からitem4のいずれかを選んでドラッグし、ショッピングカートの上に落とすと、(落としたという行為を確認するために)以下のようにalertを上げるようにした。
alertには、カートに何のアイテムが落とされたかと、そのアイテムがいくつ入っているか(落としたものを含む)を表示している。


ドラッグ・アンド・ドロップのサンプルだと、ここまでで「めでたし・めでたし」となる(YUIOfficial Exampleもそうなっている)わけだが、実際には、これをなんらかの方法で「永続化」することが必要だろう。
このサンプルでは、これを行うことをサボって、「カートの確認」ボタンで「カートに入っているアイテムと、その数量」をパネルに表示するようにした。永続化したければ、「カートの確認」ボタンを「次」とか、「一時保存」とかに変更して、パネルの内容をサーバーに飛ばしてやればよい。


ついでに、アイテムごとにいくつカートに入れているかを調べるボタンも配置してみた。上のカートの内容から取得することができるが、上がカートオブジェクトからの取得であるのに対して、この表示の(データの)ソースはアイテムオブジェクトとなっている。


サーバーにデータを飛ばさないので、これらの動作は、全てページスコープ内で行われる(ブラウザーリフレッシュでデータは失われる)。
以下に、HTMLの全文を示す。これは、上で述べたように、YUIOfficial Exampleをモディファイしたもの。

  • EzItem
  • EzCart

の2つのオブジェクトを定義しているが、前者はExampleのYAHOO.example.DDPlayerを変形したものである。後者は、ショッピングカートに関する操作一式を行う。

<!-- 
/**
 * test_shopping.html
 * 
 * (C) 2009, tetsuya.odaka(EzoGP).
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 -->

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html> 
<head> 
	<meta http-equiv="Pragma" content="no-cache">
	<meta http-equiv="Cache-Control" content="no-cache">
	<meta http-equiv="Expires" content="Thu, 01 Dec 1994 16:00:00 GMT">
	<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
	<meta http-equiv="Content-Style-Type" content="text/css">
	<meta http-equiv="Content-Script-Type" content="text/javascript">
<title>Shopping Cart</title> 
 
<link rel="stylesheet" type="text/css" href="../scripts/lib/yui/build/fonts/fonts-min.css" /> 
<link rel="stylesheet" type="text/css" href="../scripts/lib/yui/build/container/assets/skins/sam/container.css" /> 
<style type="text/css" id="defaultstyle">
th {
	color: #FFFFFF;
	background-color: #33CC33;
	border-color: #666666 #666666 #666666 #666666;
	border-style: solid solid solid solid;
	border-width: 0px 1px 1px 0px;
	font-size: 100%;
}

.egp-my-dd-cart { border:0px; text-align:center; position: absolute; width:60px; height:60px; }

.egp-my-dd-item { border:2px solid #bbbbbb; color:#eeeeee; 
text-align:center; position: absolute; width:60px; height:60px; }

.target { border:2px solid #574188;  
text-align:center; position: absolute; width:60px; height:60px; }

#t1 { left: 10px; top: 0px; }

#i1 { background-color:#7E695E; left: 84px; top: 50px; }
#i2 { background-color:#7E695E; left: 84px; top: 130px; }
#i3 { background-color:#416153; left: 195px; top: 50px; }
#i4 { background-color:#416153; left: 195px; top: 130px; }

#usercontrols {
    top: -36px;
}

#workarea {
    position: relative;
    height: 220px;
}

</style>

<script type="text/javascript" src="../scripts/lib/yui/build/yahoo-dom-event/yahoo-dom-event.js"></script>
<script type="text/javascript" src="../scripts/lib/yui/build/container/container.js"></script> 
<script type="text/javascript" src="../scripts/lib/yui/build/dragdrop/dragdrop-min.js"></script>
<script type="text/javascript" src="../scripts/lib/yui/build/connection/connection-min.js" ></script>
<script type="text/javascript">

/************************************************
 * EzItemのコンストラクタの定義
 ************************************************/
EzItem = function(id, sGroup, config) {
	// superclassのコンストラクタの呼び出し
	EzItem.superclass.constructor.apply(this, arguments);
	// initItemメソッドの呼び出し
        this.initItem(id, sGroup, config);
};

/************************************************
 * YAHOO.util.DDProxyを継承してEzItemを作成する
 ************************************************/
YAHOO.extend(EzItem, YAHOO.util.DDProxy, {

    TYPE: "EzItem",

    /**
     * Itemを初期化するときの要素定義
     *  EzItemにするそれぞれの要素に定義される。(this.xxxで定義される)
     */
    initItem: function(id, sGroup, config) {
    	
	// idがnullならイニシャライズできない。
	if (!id) { 
            return; 
        }

	// このインスタンスのDOM要素の取得(getDragEl()) 
        var el = this.getDragEl()
        YAHOO.util.Dom.setStyle(el, "borderColor", "transparent");
        YAHOO.util.Dom.setStyle(el, "opacity", 0.80);

        // Target(=Cart)ではない。
        this.isTarget = false;

        this.originalStyles = [];

        this.type = EzItem.TYPE;

        // DropしたTargetオブジェクトを入れる
        this.slot = null;

	// Dropしたitemをいれる。
        this.item = null;

	// 移動する前のitem要素(getEl())の位置を保持しておく。 
        this.startPos = YAHOO.util.Dom.getXY( this.getEl() );
    },

	/**
	 * startDragのoverride
	 *  Dragを開始したときに発砲する。
	 */ 
    startDrag: function(x, y) {
        var Dom = YAHOO.util.Dom;

        // proxyを取得
        var dragEl = this.getDragEl();
        // クリックされたitem要素そのもの
        var clickEl = this.getEl();

        // proxy要素に、クリックされたitem要素innerHTMLとclass名をコピーする
        dragEl.innerHTML = clickEl.innerHTML;
        dragEl.className = clickEl.className;

        // proxy要素のcolor属性、backgroundColor属性を、クリックされたitem要素からコピー
        Dom.setStyle(dragEl, "color",  Dom.getStyle(clickEl, "color"));
        Dom.setStyle(dragEl, "backgroundColor", 
                Dom.getStyle(clickEl, "backgroundColor"));

        // 移動する間のitem要素の透過度を0.1まで下げる。
        Dom.setStyle(clickEl, "opacity", 0.1);

		// このEzItemオブジェクト(this)と、同一のDDグループのtarget(=cart)インスタンスをDDMより取得する。
		// 第2引数=trueでターゲットだけ取得できる。
        var targets = YAHOO.util.DDM.getRelated(this, true);

        for (var i=0; i<targets.length; i++) {
            
            var targetEl = targets[i].getEl();

            // このEzItemインスタンス(this)にターゲットのスタイル(クラス名)を保存していなければ、保存する。
            if (!this.originalStyles[targetEl.id]) {
                this.originalStyles[targetEl.id] = targetEl.className;
            }

            // class名をtargetに変更(cartのstyleが変更される)
            targetEl.className = "target";
        }
    },

    /**
     * Drag終了時の処理。
     *  Targetの位置にPlayer要素が入る
     */
    endDrag: function(e) {
        // dropした要素の透過度を1に戻す。
    	YAHOO.util.Dom.setStyle(this.getEl(), "opacity", 1);
	// dropした要素のスタイルを元に戻す。
        this.resetTargets();
    },

	/** 
	 * ターゲットのスタイルを、Drop時に置き換えたitemのスタイルにリセットする。
	 */
    resetTargets: function() {
        // 同一のDDグループのTarget(=cart)オブジェクトの取得
        var targets = YAHOO.util.DDM.getRelated(this, true);
        for (var i=0; i<targets.length; i++) {
            // 要素の取得(itemが入っていればitem、入っていなければ、targetそのものがどもってくる)
            var targetEl = targets[i].getEl();
	    // class名をもとに戻す。=> original なスタイルに戻す。	
            var oldStyle = this.originalStyles[targetEl.id];
            if (oldStyle) {
                targetEl.className = oldStyle;
            }
        }
    },

    /**
     * DragされたオブジェクトがDropした際に発火する。
     * idは、mode=POINTの際はDropされたTargetのidか、mode=INTERSECTの場合は、配列
     * (TargetにまたがってDropされた場合の考慮)のいずれか。
     */ 
    onDragDrop: function(e, id) {
    	
    	// thisはプロキシオブジェクト
    	
        // DropされるTarget(=cart)のインスタンスを入れる。
        var oDD;

        if ("string" == typeof id) {
		// mode=pointの場合は、oDDにTargetを入れる。
        	oDD = YAHOO.util.DDM.getDDById(id);
        } else {
		// mode=intersectの場合は、BestMatchするTargetをoDD入れる。
		// TargetにまたがってDropされない場合には、pointと同じこと。
        	oDD = YAHOO.util.DDM.getBestMatch(id);
        }

        // プロキシのエレメントの取得
        var el = this.getEl();

    	// Targetにitemプロパティーが存在する場合;itemが入っている場合
        if (oDD.item) {
		// Proxyオブジェクトの追加
		oDD.item.push(this);
    	// Targetにitemプロパティーが存在しない場合;itemが入っていない場合
        } else {
		oDD.item = new Array();
		// Proxyオブジェクトの追加
		oDD.item.push(this);
		// スロットの初期化
		if (this.slot) {
			this.slot = 0;
                }
        }

        // このオブジェクトのslotに、DropしたTargetオブジェクトを入れる。
		this.slot++;
		alert(this.getDragEl().innerHTML+'がCartに追加されました。 '
					+this.getDragEl().innerHTML+'計'+this.slot+'個');

    },

    // 何もしないようにoverride;
    onDragOver: function(e, id) {
    },

    // 何もしないようにoverride;
    onDrag: function(e, id) {
    }

});

/********************************************************
* Cartを操作するオブジェクト
* 
*********************************************************/
var EzCart =function(){
	var Dom = YAHOO.util.Dom;

	/**
	* Cartからデータを引っ張り出す
	*/
	var getItems = function(){
		var itemArr = carts[0].item;

		var quant = Array();
		if(itemArr == undefined){
			quant = null;
		}else{
			for(i=0; i < itemArr.length; i++){
				var key = itemArr[i].getEl().innerHTML;
				if(quant[key] == undefined){
					quant[key]=1;
				}else{
					quant[key]++;
				}
			}
		}
		return quant;
	}

	/**
	* itemの購入数を表示する。
	*/
	this.showParchasedItem = function(item){

		ret = '<div><table style="width:150px;background:#ffffff;">'
		if(item.slot == undefined){
			ret += item.getEl().innerHTML + 'は未購入です';
		}else{
			ret += '<tr><th>数量</th></tr>';
			ret += '<tr><td align="center">'+item.slot+'</td></tr>';
		}
		ret += '</table></div>';
		createPanel(ret,item.getEl().innerHTML+'の購入数');
		
		return;
	}

	/**
	* carの中身を表示する。
	*/
	this.showCart = function(){
		dArr = getItems();
			
		ret = '<div id="egp-my-cart-contents"><table style="width:300px;background:#ffffff;">'
		if(dArr!=null){
			ret += '<tr><th>item</th><th>quantity</th></tr>';
			for(var key in dArr){
				ret += '<tr><td>'+key+'</td><td align="center">'+dArr[key]+'</td></tr>';
			}
		}else{
			ret += 'カートは空です。';
		}
		ret += '</table></div>';
		createPanel(ret,'カートに入っているもの');
		
		return;
	}

	/**
	* パネルの生成
	*/
	var createPanel = function(ret,header){
	    // パネルを生成する。
        if (!subWin) {
		var subWin = new YAHOO.widget.Panel('egp-my-modal-subwindow-show',  
		    { 
               	    fixedcenter: true, 
               	    close: true, 
               	    draggable: true, 
               	    zindex:10,
               	    modal: false,
		    // showされるまでは見えない。
       		    visible: false
		    }
		);

			// パネルを定義する
		subWin.setHeader(header);
	    	subWin.setBody(ret);
	        subWin.render(document.body);
		}
		subWin.show();

		return;
	}
}

/**
 * page scopeで変数を定義&Alias
 */
var carts = [], 
    items = [];

var	Event 	= YAHOO.util.Event,
	DDM 	= YAHOO.util.DDM,
	DDTarget = YAHOO.util.DDTarget;
	
var cartObj = new EzCart();

Event.onDOMReady(
	function() { 
    	// cart
    	carts[0] = new DDTarget("t1", "egp-my-dd-group");
    
    	// items
 	   	items[0] = new EzItem("i1", "egp-my-dd-group");
    	items[1] = new EzItem("i2", "egp-my-dd-group");
    	items[2] = new EzItem("i3", "egp-my-dd-group");
    	items[3] = new EzItem("i4", "egp-my-dd-group");

    	DDM.mode = document.getElementById("ddmode").selectedIndex;

    	Event.on("ddmode", 
    	   	"change", 
    	   	function(e) {
           		YAHOO.util.DDM.mode = this.selectedIndex;
        	}
        );
	}
);

</script>
</HEAD>

<BODY class="yui-skin-sam">
test_shopping.html
<br>
<h4>[sample] ショッピング・カートのサンプル</h4>
<br>
<div id="egp-my-dd-container">
itemをドラッグして、ショッピングカートにドロップしてください。
<br>
<br>

<div id="usercontrols">
	インタラクション・モード:
	<select id="ddmode" >
  		<option value="0" selected>Point</option>
  		<option value="1">Intersect</option>
	</select>
</div>

<br>
<div id="workarea">
	<div class="egp-my-dd-cart" id="t1" ><img src="../images/shopping_cart blue.png"></div>

	<div class="egp-my-dd-item" id="i1" >item1</div>
	<div class="egp-my-dd-item" id="i2" >item2</div>

	<div class="egp-my-dd-item" id="i3" >item3</div>
	<div class="egp-my-dd-item" id="i4" >item4</div>

</div>

<input type="button" value="カートの確認" onclick="cartObj.showCart();">
<br>
<input type="button" value="item1の購入数の確認" onclick='cartObj.showParchasedItem(items[0]);'>

</div>

</BODY>
</HTML>