各位老師、各位同學,我今天要示範的網頁介面開發技巧是--拖拉元素調整排序,(停頓兩秒),拖拉元素調整排序...

開始前,請大家先看示範網頁,網頁上會顯示五個<span>方塊,假設其排序有特定意義(例如: 出場順序,決定先用皮卡丘之後再派妙蛙種子之類的),使用者可用滑鼠或手指直接拖曳方塊改變其排列順序,當某個方塊被拖到另一個方塊上,就對調兩個方塊的位置,藉此自由調整所有方塊的排列順序。

另外,網頁還很騷包地貼心地加上兩個方塊交換位置的動畫效果,更直覺地呈現"位置對調"的操作感受。

相信大家都已經試玩體驗過效果了。

拍拍手覺得好酷好炫,但一點也不想知道程式該怎麼寫的同學,可由教室後方離開去福利社買冰棒吃囉! 有興趣了解程式開發細節的苦命同學們,則可以留下來看看程式裡用到的技巧:

  1. 產生五個<span>方塊的部分,採用的是陳研希knockout.js MVVM的做法。宣告了一個viewModel函數,透過ko.observableArray()保存資料物件陣列,<div id="dvList" data-bind="foreach: items">將資料物件陣列的每個元素對應成一個<span>,用<span data-bind="text: name">可將屬性當成<span>的內容。 viewModel函數中,var self = this;的做法可視為knockout.js中的Best Practice,請同學抄一下筆記。
  2. 為突顯knockout.js可即時反應資料變化的特性,我刻意每隔0.5秒塞入一筆資料,讓我們可觀察到Player0到Player4逐一出現的過程。而由於setTimeout塞入資料為非同步作業,為了確保在全部完成後才執行下一步驟,再次使用$.Deferred()的技巧處理同步化。(上回在做Google Maps API呼叫時已用過一次)
  3. 元素拖拉部分採用的是Kendo UI Web (補充簡介)的Drag & Drop功能。實測發現,用Kendo UI所開發的拖拉功能,不管在PC使用滑鼠操作或在行動裝置進行手指觸控操作都很順暢,不需針對不同裝置特意改寫,十分便捷。(但有些小眉角,例如: 有多個放置目標時如何取得當時的放置目標,需要點小技巧,細節可參見程式註解)
  4. 至於方塊對調位置的動畫效果,則是將<span>元素先轉成position: absolute,再靠.animate({ left: 新座標 })搞定,對林志玲jQuery來說不過小菜一碟。移動過程為了讓使用者有方塊是浮起來在空中飛移的錯覺,借用了CSS3的transform: scale(1.1,1.1)讓<span>放大10%,此時早先介紹的HTML5/CSS3瀏覽器支援速查工具派上用場,查詢結果顯示要IE10, FF16才支援不加-ms-, –moz-的寫法,Safari/Chrome則仍需要-webkit-,所以乖乖加上-ms-, –moz-, –webkit-寫成三筆。
  5. <span>對調順序的部分,用jQuery的.after()就可以解決,但為了定址方便,我偷偷加放了一個空白<span class='item-pos'>,程式碼瞬間簡化許多。

完整程式碼如下,已內含不少註解,但坦白說,因涉及不少進階技巧,程式碼並不好懂,希望註解夠清楚,若有疑問請提出,我再修正補充。

<!DOCTYPE html>
 
<html>
<head>
    <title>Drag to Swap Demo</title>
    <script src="../Scripts/jquery-1.7.2.min.js"></script>
    <script src="../Scripts/kendo/kendo.web.js"></script>
    <script src="../Scripts/knockout-2.1.0.js"></script>
    <link href="../Content/kendo/kendo.common.min.css" rel="stylesheet" type="text/css" />
    <link href="../Content/kendo/kendo.metro.min.css" rel="stylesheet" type="text/css" />
    <script>
       //定義ViewModel類別
        function viewModel() {
          //將this另指派給self變數,之後以其代表View Model本體,
            //避免與函數中的this所指的對象混淆
            var self = this;
          //定義一個集合存放資料
            self.items = ko.observableArray();
          //定義加入item的方法, 在items中加入具有name及score屬性的物件
            self.addItem = function(name, score) {
                self.items.push({ name: name, score: score });
          };
        }
 
        $(function () {
           //建立ViewModel
           var vm = new viewModel();
           //每隔0.5秒加一筆以觀察knockoutJs讓UI即時反應資料變化的效果
            //使用jQuery.Deferred處理實現完成時機的同步
            function job(i) {
                var df = $.Deferred();
                setTimeout(function () {
                    vm.addItem("Player" + i, i * 100);
                    df.resolve();
                }, i * 500);
                return df.promise();
            }
            //建立延遲0-4秒執行的加入item作業
            var jobs = [];
            for (var i = 0; i < 5; i++) {
                jobs.push(job(i));
            }
            //等待所有job執行完畢,掛上Kendo UI拖拉功能
            $.when.apply(null, jobs).then(function () {
                var $items = $("span.item");
                //加上拖曳特性,hint事件回傳拖曳過程顯示的元素
    $items.kendoDraggable({
                    hint: function (e) {
                        return e.clone().addClass("drag-item");
                    }
                })
                .each(function () {
                    //由於kendoDropTarget事件中,被放置對象this非亦無事件屬性可存取
                       //故使用Closure方式將元素存入$item供drop事件存取
                    var $item = $(this);
                    //加上放置目標特性才能接成為拖曳的目標
                    $item.kendoDropTarget({
                        //拖曳到目標元素上方及離開時改變CSS,提供使用者可以放置的提示
                        dragenter: function (e) { $item.addClass("drop-item"); },
                        dragleave: function (e) { $item.removeClass("drop-item"); },
                        //在目標元素上方鬆開滑鼠或手指離開觸控螢幕時觸發drop事件
                           drop: function (e) {
                            //透過以下方式取得拖曳元素及放置元素
                                 var $drag = e.draggable.element;
                            var $drop = $item;
                            $drop.removeClass("drop-item");
                            //拖曳對象與放置目標相同時不處理
                                 if ($drag.text() == $drop.text()) return;
                            //顯示位置交換動畫
                                //先將全部元素轉為絕對座標
                            $items.each(function () {
                                //先記下座標值,以.data()保存
                                var $elem = $(this);
                                $elem.data("pos", $elem.position());
                            })
                            .each(function () {
                                //維持座標位置,但換成絕對座標
                                      var $elem = $(this);
                                var pos = $elem.data("pos");
                                $(this).css({
                                    position: "absolute",
                                    top: pos.top + "px",
                                    left: pos.left + "px"
                                });
                            });
                            //$drag與$drop交換位置,使用animate
                            var dragLeft = $drag.data("pos").left + "px";
                            var dropLeft = $drop.data("pos").left + "px";
                            var $moving = $drag.add($drop);
                            $moving.addClass("move-item");
                            $drag.animate({ left: dropLeft }, 1000);
                            $drop.animate({ left: dragLeft }, 1000, function () {
                                $moving.removeClass("move-item");
                                //交換位置
                                var $dragPos = $drag.prev(".item-pos");
                                $drop.prev(".item-pos").after($drag);
                                $dragPos.after($drop);
                                //改回相對座標
                                $items.css({ position: "", left: "", top: "" });
                            });
                        }
                    });
                });
            });
            //將ViewModel與UI結合
            ko.applyBindings(vm);
        });
    </script>
    <style>
        html,body { font-size: 9pt; }
        .item 
        {
            font-family: Segoe UI; display: inline-block;
            border: 1px solid gray;
            margin-left: 5px; padding: 5px;
            width: 80px; height: 25px;
            background-color: #0099FF; color: white;
            text-align: center;
        }
        .drag-item 
        {
            opacity: 0.5;
            background-color: #FF3300;
        }
        .move-item  
        {
            z-index: 9; 
            -ms-transform: scale(1.1,1.1); /* IE 9 */
            -webkit-transform: scale(1.1,1.1); /* Safari and Chrome */
            -moz-transform: scale(1.1,1.1); /* Firefox */
        }
        #dvList { margin-top: 20px; }
        .drop-item 
        {
            background-color: Purple;
        }
    </style>
</head>
<body>
<div id="dvList" data-bind="foreach: items">
    <span class='item-pos'></span>
    <span class='item'>
        <span class='item-name' data-bind="text: name"></span>
        (<span class='item-score' data-bind="text: score"></span>)
    </span>
</div>
</body>
</html>

Comments

Be the first to post a comment

Post a comment