jQuery2-高级教程-十-

93 阅读49分钟

jQuery2 高级教程(十)

原文:Pro jQuery 2.0

协议:CC BY-NC-SA 4.0

二十五、使用其他交互

在这一章中,我将描述剩下的三个 jQuery UI 交互:可排序的可选择的可调整大小的。这些交互比draggabledroppable用得更少(也更没用),我在第二十四章中描述过。本章中的交互可能是有用的,但是它们使用的模型很难向用户突出显示。正因为如此,它们作为其他更传统的方法的补充表现得最好。表 25-1 提供了本章的总结。

表 25-1 。章节总结

问题解决办法列表
应用可排序交互选择容器元素,并调用sortable方法one
获取用户通过可排序交互创建的订单调用toArrayserialize方法2, 3
允许将元素从一个可排序项目拖动到另一个项目使用connectWith设置four
将可拖动元素与可排序项目连接使用可拖动元素上的connectToSortable设置five
指定哪些元素是可排序的使用items设置six
拖动可排序项目时创建的空白区域使用placeholder设置seven
忽略顺序的变化使用cancel方法eight
刷新可排序项目中的元素集使用refresh方法nine
获取有关正在进行的排序操作的信息使用提供给事件处理函数的ui对象Ten
应用可选交互选择容器元素,并调用selectable方法11, 12
防止元素被选中使用cancel方法Thirteen
应用可调整大小的交互使用resizable方法Fourteen
调整多个元素的大小使用alsoResize设置15, 16
限制可调整大小的元素的大小使用maxHeightmaxWidthminHeightminWidth设置Seventeen
为可调整大小的元素选择可拖动的边和角使用handles设置Eighteen

使用可排序交互

sortable交互允许用户通过拖动改变一组元素的顺序。通过选择包含想要排序的单个项目的元素,然后调用sortable方法,应用可排序交互,如清单 25-1 所示。

清单 25-1 。使用可排序交互

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        div.sortable { width: 100px; background-color: lightgrey; font-size: large;
            float: left; margin: 4px; text-align: center; border: medium solid black;
            padding: 4px;}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
            $("#sortContainer").sortable();
        });
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <div id="sortContainer">
        <div id="item1" class="sortable">Item 1</div>
        <div id="item2" class="sortable">Item 2</div>
        <div id="item3" class="sortable">Item 3</div>
    </div>
</body>
</html>

在清单 25-1 中,我创建了许多div元素,并将它们分配给sortable类。这个类对交互没有影响:我只是用它来设计元素的样式。为了创建交互,我选择父元素div(其idsortContainer)并调用sortable方法。结果是,我可以简单地通过将三个div元素拖动到一个新位置来改变它们的顺序。你可以在图 25-1 中看到效果(尽管,像本章中的所有例子一样,你将通过在浏览器中运行这个例子来更好地理解发生了什么)。

9781430263883_Fig25-01.jpg

图 25-1 。通过拖移来排序项目

为了演示sortable交互,我将标签为Item 2的元素拖到浏览器窗口的右侧。一旦我将元素拖过标签为Item 3的元素,jQuery UI 就会重新排列这些项目,使它们处于新的顺序。我只将一个元素拖动到一个位置,但是您可以一次将它们移动到几个位置。

获取可排序订单

在某些时候,您需要知道用户通过移动元素创建的顺序。要获得这些信息,您可以调用toArray方法,该方法返回排序元素的id属性值的 JavaScript 数组。清单 25-2 显示了在将当前订单写入控制台的例子中添加了一个按钮。

清单 25-2 。获取排序后的元素顺序

...
<script type="text/javascript">
    $(document).ready(function() {
        $("#sortContainer").sortable();

        $("<div id=buttonDiv><button>Get Order</button></div>").appendTo("body");
        $("button").button().click(function() {
            var order = $("#sortContainer").sortable("toArray");
            for (var i = 0; i < order.length; i++) {
                console.log("Position: " + i + " ID: " + order[i]);
            }
        });
    });
</script>
...

你可以在图 25-2 中看到效果。当按钮被按下时,我调用toArray方法并将结果数组的内容枚举到控制台。

9781430263883_Fig25-02.jpg

图 25-2 。添加一个按钮来写出排序顺序

对于图中的订单,按下按钮会产生以下输出:

Position: 0 ID: item2
Position: 1 ID: item3
Position: 2 ID: item1

您还可以使用serialize方法来生成一个字符串,该字符串很容易与表单一起使用。清单 25-3 提供了一个例子。

清单 25-3 。使用 serialize 方法

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        div.sortable { width: 100px; background-color: lightgrey; font-size: large;
            float: left; margin: 4px; text-align: center; border: medium solid black;
            padding: 4px;}
            #buttonDiv {clear: both}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
            $("#sortContainer").sortable();

            $("<div id=buttonDiv><button>Get Order</button></div>").appendTo("body");
            $("button").button().click(function() {
                var formstring = $("#sortContainer").sortable("serialize");
                console.log(formstring);
            })
        });
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <div id="sortContainer">
        <div id="item_1" class="sortable">Item 1</div>
        <div id="item_2" class="sortable">Item 2</div>
        <div id="item_3" class="sortable">Item 3</div>
    </div>
</body>
</html>

注意,我必须更改可排序元素的id值。serialize方法在生成其字符串时寻找<key>_<index>的模式。图 25-2 中所示命令的输出如下:

item[]=2&item[]=3&item[]=1

配置 可排序的交互

可排序交互依赖于我在第二十四章中描述的可拖动交互。这意味着我为该交互描述的选项(比如axistolerance)可以以同样的效果应用于配置可排序的交互。那些设置我就不再描述了。相反,表 25-2 显示了可分类交互特有且最有用的设置。我将在接下来的章节中描述这些设置。

表 25-2 。可排序设置

环境描述
connectWith指定要连接的另一个可排序元素,以便可以在它们之间拖动项目。默认为false,表示没有连接。
dropOnEmptyfalse时,物品不能放在不包含物品的已连接的可排序交互上。默认为true
items指定可通过选择器排序的项目。缺省值是> *,它选择调用了sortable方法的元素的任何后代。
placeholder指定一个类,该类将被分配给在可排序项目被拖动到新位置时为保留空间而创建的元素。

连接可排序的交互

我最喜欢的可排序特性是连接两个可排序交互的能力,允许在它们之间拖动项目。您可以使用connectWith设置来实现这一点,指定一个选择器来匹配您想要连接的元素。您可以通过在两个可排序元素上使用connectWith设置来创建一个双向连接,如清单 25-4 中的所示。

清单 25-4 。连接可排序的交互

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        div.sortable { width: 100px; background-color: lightgrey; font-size: large;
            margin: 4px; text-align: center; border: medium solid black; padding: 4px;}
        #fruitContainer {position: absolute; right:50px}
        #flowerContainer {position: absolute; left:50px}
        div.flower {background-color: lightgreen}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
            $("#fruitContainer").sortable({
                connectWith: "#flowerContainer"
            });
            $("#flowerContainer").sortable({
                connectWith: "#fruitContainer"
            });
        });
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <div id="fruitContainer" class="sortContainer">
        <div id="fruit_1" class="sortable fruit">Apple</div>
        <div id="fruit_2" class="sortable fruit">Orange</div>
        <div id="fruit_3" class="sortable fruit">Banana</div>
        <div id="fruit_4" class="sortable fruit">Pear</div>
    </div>
    <div id="flowerContainer" class="sortContainer">
        <div id="flower_1" class="sortable flower">Aster</div>
        <div id="flower_2" class="sortable flower">Peony</div>
        <div id="flower_3" class="sortable flower">Lily</div>
        <div id="flower_4" class="sortable flower">Orchid</div>
    </div>
</body>
</html>

在清单 25-4 中,我创建了两组项目,并在它们的容器元素上调用了sortable方法。我使用了connectwith设置来关联每个可排序的条目,并且图 25-3 显示了结果。

9781430263883_Fig25-03.jpg

图 25-3 。在连接的可排序交互之间拖动元素

将可拖动元素与可排序元素连接起来

你也可以将一个可拖动的元素和一个可排序的元素连接起来。通过在可拖动元素上应用connectToSortable设置,指定一个与您想要连接的可拖动元素相匹配的选择器,可以做到这一点。清单 25-5 展示了这是如何做到的。

清单 25-5 。连接可拖动元素和可排序元素

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        div.sortable { width: 100px; background-color: lightgrey; font-size: large;
            margin: 4px; text-align: center; border: medium solid black; padding: 4px;}
        #fruitContainer {position: absolute; right:50px}
        #flowerContainer {position: absolute; left:50px}
        div.flower {background-color: lightgreen}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
            $("#fruit_1").draggable({
                connectToSortable: "#flowerContainer",
                helper: "clone"
            });
            $("#flowerContainer").sortable();
        });
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <div id="fruitContainer" class="sortContainer">
        <div id="fruit_1" class="sortable fruit">Apple</div>
    </div>
    <div id="flowerContainer" class="sortContainer">
        <div id="flower_1" class="sortable flower">Aster</div>
        <div id="flower_2" class="sortable flower">Peony</div>
        <div id="flower_3" class="sortable flower">Lily</div>
        <div id="flower_4" class="sortable flower">Orchid</div>
    </div>
</body>
</html>

在清单 25-5 中,我将水果项目的数量减少到一个,并使其可拖动,连接到可排序的鲜花列表。结果是可拖动的项目可以被添加到可排序列表中,如图图 25-4 所示。当可拖动项目的helper设置为clone时,此设置效果最佳。它对其他值也有效,但是会报告一个错误。

9781430263883_Fig25-04.jpg

图 25-4 。连接可排序项目和可拖动项目

选择可排序的项目

您可以选择容器中的哪些物品是可分类的。通过items设置可以做到这一点,它的值是一个选择器,匹配您想要启用排序的元素。无法重新排列与选择器不匹配的元素。清单 25-6 演示了。

清单 25-6 。选择可排序的特定元素

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        div.sortable { width: 100px; background-color: lightgrey; font-size: large;
            margin: 4px; text-align: center; border: medium solid black; padding: 4px;}
        #fruitContainer {position: absolute; right:50px}
        #flowerContainer {position: absolute; left:50px}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
            $("div.flower:even").css("background-color", "lightgreen")
            $("#flowerContainer").sortable({
                items: ".flower:even"
            });
        });
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <div id="flowerContainer" class="sortContainer">
        <div id="flower_1" class="sortable flower">Aster</div>
        <div id="flower_2" class="sortable flower">Peony</div>
        <div id="flower_3" class="sortable flower">Lily</div>
        <div id="flower_4" class="sortable flower">Orchid</div>
    </div>
</body>
</html>

在清单 25-6 中,我使用了items设置来指定容器中只有偶数编号的元素是可排序的。在图 25-5 中,可以对AsterLily元素进行排序,但是PeonyOrchid元素不会对被拖动作出反应并保持在原位。

9781430263883_Fig25-05.jpg

图 25-5 。选择可以排序的项目

在使用items设置时,有一个奇怪的地方你应该注意,我已经在图的最后一帧中展示了。与选择器不匹配的元素不能被拖动到新位置,除非它已被另一个元素移走。所以在图中,我将Aster元素拖到一个新的位置,这迫使Peony元素移动。一旦被移动,Peony元素将响应被拖动和排序,就好像它匹配items选择器一样。

设计空白的空间

当您将项目拖到新位置时,它留下的空间保持空白。你可以通过placeholder设置将一个 CSS 类应用到这个空间。这是强调空白空间是拖放目标的一种有用方式。清单 25-7 显示了placeholder设置的使用。

清单 25-7 。使用占位符设置

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        div.sortable {width: 100px; background-color: lightgrey; font-size: large;
            margin: 4px; text-align: center; border: medium solid black; padding: 4px;}
        #flowerContainer {position: absolute; left:25%}
        .emptySpace {border: medium dotted red; height: 25px; margin: 4px}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
            $("#flowerContainer").sortable({
                placeholder: "emptySpace"
            });
        });
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <div id="flowerContainer" class="sortContainer">
        <div id="flower_1" class="sortable ">Aster</div>
        <div id="flower_2" class="sortable ">Peony</div>
        <div id="flower_3" class="sortable">Lily</div>
        <div id="flower_4" class="sortable">Orchid</div>
    </div>
</body>
</html>

在清单 25-7 的中,我定义了一个名为emptySpace的 CSS 类,它定义了heightmargin属性的大小,并定义了一个红色圆点border。我使用placeholder设置来指定这个类,如图 25-6 所示,当我拖动一个元素来排序它时,它留下的空间被分配给emptySpace类。

9781430263883_Fig25-06.jpg

图 25-6 。使用占位符设置

使用可排序的方法

可排序交互定义了所有标准的 jQuery UI 方法,以及一些特定于使用可排序元素的方法。表 25-3 描述了这些方法。

表 25-3 。可排序的方法

方法描述
sortable("destroy")从元素中移除交互
sortable("disable")禁用了可排序交互
sortable("enable")启用可排序交互
sortable("option")更改一个或多个设置
sortable("toArray")返回一个数组,其中包含一组已排序的id属性值(参见“获取可排序顺序”一节中的示例)
sortable("refresh")刷新了可排序的交互
sortable("cancel")取消排序操作

取消排序

你可以使用cancel方法来防止元素被排序。这是应该谨慎进行的事情,因为它实际上忽略了用户已经采取的行动。如果你取消了一个排序,你应该确保用户知道为什么会这样。清单 25-8 提供了一个结合使用cancel方法和update事件的例子。当用户拖动一个元素创建一个新的排序顺序后释放鼠标按钮时,触发update事件。我在“使用可排序事件”一节中描述了可排序事件

清单 25-8 。使用取消方法

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        div.sortable {width: 100px; background-color: lightgrey; font-size: large;
            margin: 4px; text-align: center; border: medium solid black; padding: 4px;}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
            $("#error").dialog({autoOpen: false, modal: true})

            $("#flowerContainer").sortable({
                update: function() {
                    var sortedItems = $("#flowerContainer").sortable("toArray");
                    if (sortedItems[0] != "item_1") {
                        $("#error").dialog("open")
                        $("#flowerContainer").sortable("cancel")
                    }
                }
            });
        });
    </script>
</head>
<body>
    <div id="error">The King must be first</div>
    <h1>Jacqui's Flower Shop</h1>
    <div id="flowerContainer" class="sortContainer">
        <div id="item_1" class="sortable ">King</div>
        <div id="item_2" class="sortable ">Queen</div>
        <div id="item_3" class="sortable ">Jack</div>
        <div id="item_4" class="sortable">10</div>
    </div>
</body>
</html>

在清单 25-8 中,如果用户创建的新排序意味着King元素不在列表的第一个位置,我就调用cancel方法。我使用第二十二章中描述的对话框部件来提醒用户这个问题。允许影响其他可排序元素的更改继续进行。

刷新可排序元素

refresh方法使 sortable 交互刷新 sortable 容器中元素的缓存。清单 25-9 展示了如何使用这个特性来添加新的可排序元素。

清单 25-9 。添加新的可排序元素

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        div.sortable {width: 100px; background-color: lightgrey; font-size: large;
            margin: 4px; text-align: center; border: medium solid black; padding: 4px;}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
            $("#flowerContainer").sortable();

            var itemCount = 2;

            $("button").click(function() {
                $("<div id=flower_" + (itemCount++) + " class=sortable>Item " +
                  itemCount + "</div>").appendTo("#flowerContainer");
                $("#flowerContainer").sortable("refresh");
            })
        });
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <button>Add Sortable Item</button>
    <div id="flowerContainer" class="sortContainer">
        <div id="flower_1" class="sortable">Aster</div>
        <div id="flower_2" class="sortable">Peony</div>
    </div>
</body>
</html>

在清单 25-9 中,我在文档中添加了一个button,向可排序的容器中添加新的条目,并调用refresh方法来确保这些条目可以正确排序。

使用可排序事件

可排序交互支持可拖动交互定义的所有事件,我在第二十四章中描述过。表 25-4 描述了可排序交互特有的事件。

表 25-4 。可排序事件

事件描述
change当用户对元素进行排序时位置发生变化时触发
receive当一个项目从一个连接的项目被拖动到这个可排序的项目时触发
remove当一个项目从这个可排序项目拖到一个连接的项目时触发
sort排序过程中每次鼠标移动时触发
update当用户停止拖动某项并且该项的顺序已经改变时触发

当触发这些事件时,jQuery UI 通过一个ui对象参数提供附加信息,该参数的属性如表 25-5 所示。

表 25-5 。可排序的 ui 对象属性

财产描述
helper返回辅助元素
position以具有topleft属性的对象的形式返回辅助对象的当前位置
item返回包含当前拖动项的 jQuery 对象
placeholder返回包含占位符元素的 jQuery 对象
sender返回一个 jQuery 对象,该对象包含元素所源自的连接的可排序表(当没有连接的可排序表时,该属性为 null)

清单 25-10 展示了ui对象与sortchange事件的使用。

清单 25-10 。使用更改和排序事件

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        div.sortable {width: 100px; background-color: lightgrey; font-size: large;
            margin: 4px; text-align: center; border: medium solid black; padding: 4px;}
        #flowerContainer {position: absolute; left:10px}
        #info {position: absolute; right: 10px; border: medium solid black; padding: 4px}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
            $("#flowerContainer").sortable({
                sort: function(event, ui) {
                    $("#itemId").text(ui.item.attr("id"))
                },
                change: function(event, ui) {
                    $("#pos").text($("#flowerContainer *").index(ui.placeholder))
                }
            });
        });
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <div id="flowerContainer" class="sortContainer">
        <div id="flower_1" class="sortable ">Aster</div>
        <div id="flower_2" class="sortable ">Peony</div>
        <div id="flower_3" class="sortable">Lily</div>
        <div id="flower_4" class="sortable">Orchid</div>
    </div>
    <div id="info" class="ui-widget">
        <div>Item ID: <span id="itemId">None</span></div>
        <div>Pos: <span id="pos">None</span></div>
    </div>
</body>
</html>

我使用事件来显示关于排序操作的信息。对于sort事件,我读取了ui.item属性并获得了被拖动元素的id属性。对于 change 事件,我使用了ui.placeholder属性,并使用了index方法来确定它在可排序元素中的位置。

使用可选交互

可选择的交互允许用户通过拖动鼠标或点击单个元素来选择一个或多个元素。您通过selectable方法应用交互,如清单 25-11 所示。

清单 25-11 。应用可选交互

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        div.flower {width: 200px; background-color: lightgrey; font-size: large;
            margin: 4px; text-align: center; border: medium solid black; padding: 4px;}
        #flowerContainer {position: absolute; left:10px}
        div.ui-selected {border: medium solid green; background-color: lightgreen}
        div.ui-selecting {border: medium solid green}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
            $("#flowerContainer").selectable();
        });
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <div id="flowerContainer">
        <div id="flower_1" class="flower">Aster</div>
        <div id="flower_2" class="flower">Peony</div>
        <div id="flower_3" class="flower">Lily</div>
        <div id="flower_4" class="flower">Orchid</div>
    </div>
</body>
</html>

您将可选交互应用于包含您希望用户能够选择的元素的元素。在这种情况下,我使用了本章前面用于可排序交互的相同的div元素。我选择容器并调用selectable方法,如下所示:

...
$("#flowerContainer").selectable();
...

尽管我现在已经将可选交互应用到了我的容器中,但是我还需要为特定的类定义一对 CSS 样式来给用户提供视觉反馈。以下是我与这些类关联的样式:

...
div.ui-selected {border: medium solid green; background-color: lightgreen}
div.ui-selecting {border: medium solid green}
...

可选交互将这些类应用到我的元素,以反映它们的选择状态。当用户拖动鼠标选择特定区域中的元素时,应用ui.selecting类,当元素被选中时,应用ui-selected类(因为用户点击了该元素,或者因为它位于鼠标拖动覆盖的区域中)。我使用了简单的样式,只使用绿色的边框和背景。在图 25-7 中可以看到拖动鼠标选择元素的效果。

9781430263883_Fig25-07.jpg

图 25-7 。用鼠标选择元素

用户必须开始在容器元素内拖动鼠标来开始选择过程。您可以在图的中间框架中看到被选中区域的轮廓(称为选框)——此时,jQuery UI 已经应用了ui-selecting类。当释放鼠标时,选取框重叠的元素被选中,并应用ui-selected类,如图的最后一帧所示。

用户也可以通过点击来选择元素。可以选择多个元素,按住 Control/Meta 键可以进行不连续的选择。如果您发现单击会导致单个选定元素切换,您需要添加清单 25-12 中所示的内容。

清单 25-12 。为可选择的交互启用多个元素选择

...
<script type="text/javascript">
    $(document).ready(function() {
        $("#flowerContainer")
            .bind("mousedown", function(e) {e.metaKey = true;})
            .selectable();
    });
</script>
...

当用户按下鼠标按钮时,应用ui-selecting类。当释放鼠标按钮时,应用ui-selected类。

配置可选交互

您可以使用表 25-6 中描述的设置来配置可选交互。

表 25-6 。可选设置

环境描述
disabledtrue时,交互最初被禁用。默认为false
autoRefreshtrue时,交互在每个选择操作开始时刷新每个可选元素的大小和位置。默认为true
cancel阻止匹配元素被选中的选择器字符串。
delay参见第二十四章中可拖动交互的delay设置。
distance参见第二十四章中关于可拖动交互的distance设置。
filter用于匹配容器中可选元素的选择器。默认为*,匹配所有元素。

这些设置中的大多数是不言而喻的,或者与其他交互相同。不过,特别有趣的是cancel设置,您可以用它来使元素不被用户选择。清单 25-13 演示了。

清单 25-13 。使用取消设置

...
<script type="text/javascript">
    $(document).ready(function() {
        $("#flowerContainer")
            .bind("mousedown", function(e) {e.metaKey = true;})
            .selectable({
                cancel: "#flower_3"
            });
    });
</script>
...

在这个脚本中,我使用了一个选择器来防止 ID 为flower_3的元素被选中。当用户通过点击元素来选择元素时,这种方法很有效,但是不能阻止通过拖动来选择。因此,小心使用cancel设置。

使用可选的交互方法

可选交互仅定义一个唯一的方法,如表 25-7 所述。其他方法是所有小部件和交互通用的方法。

表 25-7 。可选方法

方法描述
selectable("destroy")从元素中移除交互
selectable("disable")禁用可选交互
selectable("enable")启用可选交互
selectable("option")更改一个或多个设置
selectable("refresh")刷新可选交互;这是使用false作为autoRefresh设置值的手动替代方法

使用可选的交互事件

可选交互定义了表 25-8 中显示的事件。

表 25-8 。可选方法

事件描述
create当交互应用于元素时触发。
selected当一个项目被选中时触发。如果选择了多个项目,将为每个项目触发一次该事件。
selecting当用户开始选择过程(通过按下鼠标按钮或拖动鼠标)时触发。
unselected当取消选择项目时触发。如果未选择多个项目,则每个项目都会触发一次该事件。
unselecting当用户通过按下鼠标按钮开始取消选择过程时触发。

jQuery UI 通过一个ui对象为大多数事件提供了附加信息。对于selectedselecting事件,ui对象有一个名为selected的属性,该属性包含与已经(或即将)被选中的元素相对应的HTMLElement。对于unselectedunselecting事件,ui对象有一个执行相同目的的unselected属性。

使用可调整大小的交互

可调整大小的交互将拖动手柄添加到允许用户调整大小的元素。有些浏览器对文本区域自动执行此操作,但是可调整大小的交互让我们可以将此功能应用于文档中的任何元素。清单 25-14 展示了使用resizable方法执行的可调整大小的交互的应用。

清单 25-14 。应用可调整大小的交互

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        #aster, #lily {text-align: center; width: 150px; border: thin solid black;
            padding: 5px; float: left; margin: 20px}
        #aster img, #lily img {display: block; margin: auto}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
            $("#aster").resizable({
                alsoResize: "#aster img"
            });
        });
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <div id="aster" class="ui-widget">
        <img src="aster.png" />
        Aster
    </div>
    <div id="lily" class="ui-widget">
        <img src="lily.png" />
        Lilly
    </div>
</body>
</html>

在清单 25-14 中,我创建了两个div元素,它们的内容是一个img和一些文本。我在script 中选择其中一个并应用resizable方法(使用alsoResize设置,我将在本章稍后描述)。jQuery UI 为选中的元素添加了一个拖动手柄,允许我在垂直和水平方向调整它的大小,如图 25-8 所示。在图中,我增加了元素的高度,减少了宽度。

9781430263883_Fig25-08.jpg

图 25-8 。使用拖动手柄更改可调整大小的元素的尺寸

配置可调整大小的交互

您可以使用表 25-9 中描述的设置来配置可调整大小的交互。可调整大小的交互依赖于我在第二十四章中描述的可拖动交互。这意味着,除了表中描述的设置,您还可以使用可拖动设置来配置可调整大小的交互,包括delaydistancegridcontainment

表 25-9 。可调整大小的设置

环境描述
alsoResize用于匹配应与可调整大小的元素同时调整大小的元素的选择器。缺省值是false,意味着不调整其他元素的大小。
aspectRatiotrue时,元素的纵横比在调整大小时保持不变。默认为false
autoHidetrue时,只有当鼠标悬停在可调整大小的元素上时,拖动手柄才可见。默认为false
ghosttrue时,会绘制一个半透明的帮助元素,向用户显示该元素的新大小。默认为true
handles指定拖动手柄在可调整大小的元素上的放置位置。有关支持值的列表,请参阅本章后面的内容。
maxHeight指定元素可以调整到的最大高度。默认为null,表示无限制。
maxWidth指定元素可以调整到的最大宽度。默认为null,表示无限制。
minHeight指定元素可以调整到的最小高度。默认值为 10 像素。
minWidth指定元素可以调整到的最小宽度。默认值为 10 像素。

调整相关元素的大小

在我看来,alsoResize是配置可调整大小的交互最有用的设置。它允许您指定额外的元素,这些元素将随您应用了resizable方法的元素一起调整大小。我使用这个主要是为了确保内容元素的大小与其父元素同步,正如我在本章前面所演示的,当时我选择了用div调整大小的img元素。首先,它有助于理解当你有内容元素而不使用alsoResize设置时会发生什么。清单 25-15 设置场景。

清单 25-15 。在没有 alsoResize 设置的情况下使用内容调整元素的大小

...
<script type="text/javascript">
    $(document).ready(function() {
        $("#aster").resizable();
    });
</script>
...

如果没有alsoResize设置,只有div元素会改变大小。内容元素将保持原样。你可以在图 25-9 中看到发生了什么。

9781430263883_Fig25-09.jpg

图 25-9 。调整元素的大小,但不调整其内容

有时候这很有用,但是我发现自己几乎每次使用可调整大小的交互时都在使用alsoResize设置。对我来说,alsoResize设置的好处是匹配的元素不局限于你要调整大小的元素的内容。您可以指定任何元素,如清单 25-16 所示。

清单 25-16 。使用 alsoResize 设置调整附加元素的大小

...
<script type="text/javascript">
    $(document).ready(function() {
        $("#aster").resizable({
            alsoResize: "#aster img, #lily, #lily img"
        });
    });
</script>
...

在这个脚本中,我扩大了选择范围,以包括文档中的其他divimg元素。这意味着当我调整可调整大小的div元素时,jQuery UI 会同时调整四个元素的大小。你可以在图 25-10 中看到效果。

9781430263883_Fig25-10.jpg

图 25-10 。调整多个元素的大小

约束可调整大小的元素大小

您可以通过应用maxHeightmaxWidthminHeightminWidth设置来限制可调整大小的元素的大小。所有四个设置的值都是像素数或null,这意味着没有限制。清单 25-17 显示了如何使用这些设置。

image 提示minWidthminHeight设置的默认值为 10 像素。如果该值更小,jQuery UI 将无法显示拖动手柄,这意味着用户将无法再次增加尺寸。小心使用较小的值。

清单 25-17 。限制可调整大小的元素的大小

...
<script type="text/javascript">
    $(document).ready(function() {
        $("#aster").resizable({
            alsoResize: "#aster img",
            maxWidth: 200,
            maxHeight: 150
        });
    });
</script>
...

image 提示你也可以使用由可拖动交互定义的包容设置,我在第二十四章中描述过。这允许您将可调整大小的元素的最大大小限制为另一个元素的大小。

定位拖动控制柄

您可以通过handles设置来指定哪些棱角可以被拖动。该设置的值可以是all(意味着所有的边和角都是可拖动的)或罗盘点的组合(neswnesenwsw),以指定各个角和边。

您可以指定多个值,用逗号分隔。该设置的默认值是e, s, se,这意味着右下角(se)和右边(e)和下边(s)将是可拖动的。jQuery UI 仅在右下角绘制一个对角拖动手柄,并且只有当您将se指定为handles值的一部分时。对于所有其他的边和角,当鼠标悬停在边或角上时,光标将改变以指示可以拖动。清单 25-18 显示了handles设置的使用。

清单 25-18 。使用手柄设置

...
<script type="text/javascript">
    $(document).ready(function() {
        $("#aster").resizable({
            alsoResize: "#aster img"
        });

        $("#lily").resizable({
            alsoResize: "#lilyimg",
            handles: "n, s, e, w"

        });
    });
</script>
...

在这个脚本中,我调整了两个div元素的大小,并对其中一个应用了一组自定义的拖动手柄。你可以在图 25-11 中看到 jQuery UI 是如何处理可见的拖动手柄和光标变化的。

9781430263883_Fig25-11.jpg

图 25-11 。使用手柄设置

摘要

在本章中,我解释并演示了三种 jQuery UI 交互:可排序、可选择和可调整大小。与我在第二十四章中描述的可拖动和可放下交互相比,这些不太常用,但如果小心应用,它们仍然很有用。与所有的交互一样,主要的挑战是让用户意识到,当 web 应用中没有标准化的视觉提示时,他或她可以拖动、选择、排序或调整元素的大小。因此,交互应该作为与应用或文档交互的其他机制的补充。这允许高级用户发现交互的优点,而其他用户依赖更明显和常规的技术。

二十六、重构示例:第三部分

在本书的这一部分,我向您介绍了 jQuery UI 小部件和交互。这些允许您创建丰富的 web 应用,这些应用具有一致的主题,并且可以根据您的需要不断地配置和调整。在这一章中,我将把其中的一些特性添加到示例中,以演示它们是如何结合在一起的。

查看重构的示例

当您最后一次重构示例时,您正处于重新创建一些使用核心 jQuery 库的 jQuery UI 功能的边缘。你可以在图 26-1 中看到我到了哪里。

9781430263883_Fig26-01.jpg

图 26-1 。之前重构的示例文档

本书前一部分中添加的内容包括数据模板、表单验证和 Ajax,但是我还添加了一个简单的产品轮播,在一行中显示可用的产品。在这一章中,我将使用其中的一些特性,但是我的重点将是应用 jQuery UI 。清单 26-1 显示了本章的起点。

清单 26-1 。本章的起始文档

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <script src="handlebars.js"></script>
    <script src="handlebars-jquery.js"></script>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <script id="flowerTmpl" type="text/x-handlebars-template">
        {{#flowers}}
        <div class="dcell">
            <img src="{{product}}.png"/>
            <label for="{{product}}">{{name}}:</label>
            <input name="{{product}}" value="0" />
        </div>
        {{/flowers}}
    </script>
    <script type="text/javascript">
        $(document).ready(function () {
            $.getJSON("mydata.json", function (data) {
                $("#flowerTmpl").template({ flowers: data })
                    .filter("*").appendTo("#products");
            });
        });
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <form method="post" action="http://node.jacquisflowershop.com/order">
        <div id="products"></div>
        <div id="buttonDiv"><button type="submit">Place Order</button></div>
    </form>
</body>
</html>

我使用getJSON方法从 JSON 文件中获取产品的细节,并使用数据模板生成元素。我将产品元素添加到一个单独的div元素中,该元素的idproducts。你可以在图 26-2 中看到结果。

9781430263883_Fig26-02.jpg

图 26-2 。本章的起始文档

展示产品

我将使用一个手风琴向用户展示产品。我只需要处理六个产品,但是我将把它们分成两个一组,并使用 jQuery 来创建 accordion 所需的元素结构。清单 26-2 显示了对文档的修改。

清单 26-2 。分类和构建花卉元素

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <script src="handlebars.js"></script>
    <script src="handlebars-jquery.js"></script>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <style type="text/css">
        .dcell img {height: 60px}
    </style>
    <script id="flowerTmpl" type="text/x-handlebars-template">
        {{#flowers}}
            <div class="dcell">
                <img src="{{product}}.png"/>
                <label for="{{product}}">{{name}}:</label>
                <input name="{{product}}" value="0" />
            </div>
        {{/flowers}}
    </script>
    <script type="text/javascript">
        $(document).ready(function () {
            $.getJSON("mydata.json", function (data) {
                var flowers = $("#flowerTmpl").template({ flowers: data }).filter("*");

                var rowCount = 1;
                for (var i = 0; i < flowers.length; i += 2) {

                    $("<a>").text(data[i].name + "&" + data[i + 1].name)
                        .appendTo("<h2>").parent().appendTo("#products");

                    $("<div>").attr("id", "row" + (rowCount++))
                        .appendTo("#products")
                        .append(flowers.slice(i, i + 2))
                }
                $("#products").accordion();
            });
        });
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <form method="post" action="http://node.jacquisflowershop.com/order">
        <div id="products"></div>
        <div id="buttonDiv"><button type="submit">Place Order</button></div>
    </form>
</body>
</html>

我已经重写了传递给getJSON方法的函数来创建 accordion,包括构造元素结构和调用accordion方法。新的实现使用 JSON 数据对象来提取部分标题的花的名称,但仍然使用数据模板来生成 HTML 元素,这些元素被分割并放入包装器div元素中以适应 accordion 小部件。你可以在图 26-3 中看到添加对accordion方法的调用前后文档是如何出现的。

9781430263883_Fig26-03.jpg

图 26-3 。创建元素结构并调用 accordion 方法

添加购物篮

下一步是添加一个简单的购物篮,向用户显示她所做的选择。清单 26-3 显示了对示例文档的补充。

清单 26-3 。添加购物篮

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>

    <script src="handlebars.js"></script>
    <script src="handlebars-jquery.js"></script>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <style type="text/css">
        .dcell img {height: 60px}
        #basketTable {border: thin solid black; border-collapse: collapse}
        th, td {padding: 4px; width: 50px}
        td:first-child, th:first-child {width: 150px}
        #placeholder {text-align: center}
        #productWrapper {float: left; width: 65%}
        #basket {width: 30%; text-align: left; float: left; margin-left: 10px}
        #buttonDiv {clear: both}
    </style>
    <script id="flowerTmpl" type="text/x-handlebars-template">
        {{#flowers}}
            <div class="dcell">
                <img src="{{product}}.png"/>
                <label for="{{product}}">{{name}}:</label>
                <input name="{{product}}" value="0" />
            </div>
        {{/flowers}}
    </script>
    <script id="rowTmpl" type="text/x-handlebars-template">
        <tr id="{{name}}"><td>{{product}}</td><td>{{val}}</td>
            <td><a href="#">Remove</a></td>
        </tr>
    </script>
    <script type="text/javascript">
        $(document).ready(function () {

            $.getJSON("mydata.json", function (data) {

                var flowers = $("#flowerTmpl").template({ flowers: data }).filter("*");

                var rowCount = 1;
                for (var i = 0; i < flowers.length; i += 2) {
                    $("<a>").text(data[i].name + " & " + data[i + 1].name)
                        .appendTo("<h2>").parent().appendTo("#products");
                    $("<div>").attr("id", "row" + (rowCount++))
                        .appendTo("#products")
                        .append(flowers.slice(i, i + 2));
                }
                $("#products").accordion();

                $("input").change(function (event) {
                    $("#placeholder").hide();
                    var fname = $(this).attr("name");
                    var row = $("tr[id=" + fname + "]");

                    if (row.length == 0) {
                        $("#rowTmpl").template({
                            name: fname,
                            val: $(this).val(),
                            product: $(this).siblings("label").text()
                        }).appendTo("#basketTable").find("a").click(function () {
                            removeTableRow($(this).closest("tr"));
                            var iElem = $("#products").find("input[name=" + fname + "]");
                            $("#products").accordion("option", "active",
                                iElem.closest("div[id^=row]").index("div[id^=row]"));
                            iElem.val(0).select();
                        });
                    } else if ($(this).val() != "0") {
                        row.children().eq(1).text($(this).val());
                    } else {
                        removeTableRow(row);
                    }
                });
            });

            function removeTableRow(row) {
                row.remove();
                if ($("#basketTable tbody").children(":visible").length == 1) {
                    $("#placeholder").show();
                }
            }
        });
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <form method="post" action="http://node.jacquisflowershop.com/order">
        <div id="productWrapper">
            <div id="products"></div>
        </div>
        <div id="basket" class="ui-widget">
            <table border=1 id="basketTable">
                <tr><th>Product</th><th>Quantity</th><th>Remove</th></tr>
                <tr id="placeholder"><td colspan=3>No Products</td></tr>
            </table>
        </div>
        <div id="buttonDiv"><button type="submit">Place Order</button></div>
    </form>
</body>
</html>

折叠手风琴

我想把篮子放在手风琴旁边展示。为此,我将为accordion方法选择的元素包装在另一个div元素中,如下所示:

...
<div id="productWrapper">
    <div id="products"></div>
</div>
...

如果 accordion 小部件没有被设置为占据父元素宽度的 100 %,它就会变得混乱,所以我添加了 wrapper 元素,然后使用 CSS width属性来固定它的大小,如下所示:

...
#productWrapper {float: left;width: 65%}
...

accordion 小部件愉快地占据了包装器div元素的 100 %,而后者只占据了其父元素的 65%。

添加表格

我决定使用一个table元素来显示购物篮,我已经将它添加到了文档的静态元素中,如下所示:

...
<div id="basket" class="ui-widget">
    <table border=1 id="basketTable">
        <tr><th>Product</th><th>Quantity</th><th>Remove</th></tr>
        <tr id="placeholder"><td colspan=3>No Products</td></tr>
    </table>
</div>
...

就像手风琴一样,我将table元素放在一个包装器中,包装器的宽度是用 CSS 设置的:

...
#basket {width: 30%; text-align: left; float: left; margin-left: 10px}
...

table元素包含一个标题行和一个跨越整个表格的占位符。你可以在图 26-4 中看到创建的效果。

9781430263883_Fig26-04.jpg

图 26-4 。将表格添加到文档中

处理输入值更改

为了将表格链接到手风琴,我监听在getJSON函数中创建的input元素上的change事件,如下所示:

...
$("input").change(function (event) {
    $("#placeholder").hide();
    var fname = $(this).attr("name");
    var row = $("tr[id=" + fname + "]");

    if (row.length == 0) {
        $("#rowTmpl").template({
            name: fname,
            val: $(this).val(),
            product: $(this).siblings("label").text()
        }).appendTo("#basketTable").find("a").click(function () {
            removeTableRow($(this).closest("tr"));
            var iElem = $("#products").find("input[name=" + fname + "]");
            $("#products").accordion("option", "active",
                iElem.closest("div[id^=row]").index("div[id^=row]"));
            iElem.val(0).select();
        });
    } else if ($(this).val() != "0") {
        row.children().eq(1).text($(this).val());
    } else {
        removeTableRow(row);
    }
});
...

这个函数中发生了很多事情。当用户更改一个值时,我会检查表中是否已经有相应产品的一行。如果没有,那么我使用下面的模板创建一个新行:

...
<script id="rowTmpl" type="text/x-handlebars-template">
    <tr id="{{name}}"><td>{{product}}</td><td>{{val}}</td>
        <td><a href=#>Remove</a></td>
    </tr>
</script>
...

为了获取这个模板的值,我使用核心 jQuery 方法从触发事件的input元素中获取信息。我还想要产品的显示名称,这是通过导航 DOM 找到附近的label元素并读取其内容获得的,如下所示:

...
$(this).siblings("label").text()
...

我将新行追加到表中。占位符行已经隐藏,回到函数的开头:

...
$("#placeholder").hide();
...

您可以在图 26-5 中看到新增的行是如何出现的。用户在一个input元素中输入一个值,当焦点改变时,一个新的行出现在 basket 表中。

9781430263883_Fig26-05.jpg

图 26-5 。向购物篮表添加行

删除行

您可以看到,我已经将一个a元素添加到表格行中,作为数据模板的一部分。当我从数据模板创建行时,我为这个元素注册了一个处理程序,如下所示:

...
}).appendTo("#basketTable").find("a").click(function () {
    removeTableRow($(this).closest("tr"));
    var iElem = $("#products").find("input[name=" + fname + "]");
    $("#products").accordion("option", "active",
        iElem.closest("div[id^=row]").index("div[id^=row]"));
    iElem.val(0).select();
});
...

我做的第一件事是调用removeTableRow函数,将最近的祖先tr元素传递给a元素。removeTableRow函数使用remove方法从文档中删除指定的元素。如果没有与产品相关的行,它还会恢复表中的占位符行,如下所示:

...
function removeTableRow(row) {
    row.remove();
    if ($("#basketTable tbody").children(":visible").length == 1) {
        $("#placeholder").show();
    }
}
...

一旦删除了行,我就找到与产品中的行相关联的input元素。然后,我在 DOM 中导航,找到作为input元素的父元素的 accordion panel 元素,获取其对等元素中的index,并将其设置为 accordion 小部件的active选项。这具有打开包含用户刚刚从购物篮中删除的产品的折叠部分的效果。最后,我将input元素的值设置为零,并调用select方法,这样它就被聚焦,并且该值被选中。你可以在图 26-6 中看到效果(虽然这是你真的需要在浏览器中看到才能欣赏的东西)。

9781430263883_Fig26-06.jpg

图 26-6 。当表格行被删除时,聚焦于 accordion 中的输入元素

image 提示当用户在表格中有一行的input元素中输入零值时,我也会删除行。我使用removeTableRow函数来完成这个操作,以便在需要时显示占位符。

更新现有行

如果产品已经有一行,那么用户实际上是在改变她想要订购的数量。我没有删除和替换该行,而是在table中找到它并更新单元格的内容:

...
row.children().eq(1).text($(this).val())
...

row变量是一个 jQuery 对象,包含表中产品的tr元素。我通过位置(使用index方法)访问td元素,然后使用text方法设置其内容。

应用主题样式

篮子的功能性还好,但是外观很糟糕。幸运的是,jQuery UI 提供了一个 CSS 样式的框架,您可以将它应用于元素,使它们具有与主题应用于小部件相同的视觉外观。清单 26-4 显示了对文档中 HTML 元素的一些简单添加。

清单 26-4 。将 jQuery UI CSS 框架样式应用于表格元素

...
<body>
    <h1>Jacqui's Flower Shop</h1>
    <form method="post" action="http://node.jacquisflowershop.com/order">
        <div id="productWrapper">
            <div id="products"></div>
        </div>
        <div id="basket"class="ui-widget ui-widget-content">
            <table border=0id="basketTable">
                <trclass="ui-widget-header">
                    <th>Product</th><th>Quantity</th><th>Remove</th></tr>
                <tr id="placeholder"><td colspan=3>No Products</td></tr>
            </table>
        </div>
        <div id="buttonDiv"><button type="submit">Place Order</button></div>
    </form>
</body>
...

你可能已经注意到我在前面章节的一些例子中使用了ui-widget类。这是基本的 jQuery UI 样式,它应用于元素集的外部容器,这些元素集需要与 jQuery UI 小部件一致的外观。ui-widget-content类用于包含内容的元素,而ui-widget-header正如其名称所示,用于标题元素。

image 提示我在第三十五章中描述了 jQuery UI CSS 框架类。

除了应用这些类,我还禁用了table元素的边框,如下所示:

...
#basketTable {border: none; border-collapse: collapse}
...

你可以在图 26-7 中看到效果。

9781430263883_Fig26-07.jpg

图 26-7 。将 jQuery UI CSS 框架类应用到表中

更广泛地应用 CSS 框架

您可以更进一步,更广泛地应用框架风格。清单 26-5 显示了对文档的一些有用的补充。

清单 26-5 。更广泛地应用框架风格

...
<body>
    <div id="logoWrapper" class="ui-widget ui-widget-content ui-corner-all">
        <h1 id="logo">Jacqui's Flower Shop</h1>
    </div>
    <form method="post" action="http://node.jacquisflowershop.com/order">
        <div id="productWrapper">
            <div id="products"></div>
        </div>
        <div id="basket" class="ui-widget ui-widget-content">
            <table border=0 id="basketTable">
                <tr class="ui-widget-header">
                    <th>Product</th><th>Quantity</th><th>Remove</th></tr>
                <tr id="placeholder"><td colspan=3>No Products</td></tr>
            </table>
        </div>
        <div id="buttonDiv"><button type="submit">Place Order</button></div>
    </form>
</body>
...

我已经将h1元素放在了一个div中,并应用了几个框架样式,包括ui-corner-all,这创建了你在图 26-8 中可以看到的圆角。我还在这个文档中应用了一些新的样式来创建我想要的效果,覆盖了从第三章开始使用的styles.css文件中的样式:

...
<style type="text/css">
    .dcell img {height: 60px}
    #basketTable {border: none; border-collapse: collapse}
    th, td {padding: 4px; width: 50px}
    td:first-child, th:first-child {width: 150px}
    #placeholder {text-align: center}
    #productWrapper {float: left; width: 65%}
    #basket {width: 33%; text-align: left; float: left; margin-left: 10px;
        position: absolute; right: 10px}
    #buttonDiv {clear: both}
    #logo {font-size: 1.5em; background-size: contain; margin: 1px;
        border: none; color: inherit}
    #logoWrapper {margin-bottom: 5px}
</style>
...

9781430263883_Fig26-08.jpg

图 26-8 。将 CSS 框架样式应用到文档标题

将圆角应用于桌子

ui-corner-all类应用到table元素会导致一些问题,如图图 26-9 所示。你会注意到table元素没有圆角。这是由 jQuery UI CSS 框架类和大多数浏览器中处理表格的方式之间的交互引起的。

9781430263883_Fig26-09.jpg

图 26-9 。桌子上圆角的效果

为了解决这个问题,您需要更改table元素,稍微不同地应用 jQuery UI CSS 框架类,并定义一个新的定制样式。首先,您需要修改table,如清单 26-6 所示。

清单 26-6 。修改表格元素以支持圆角

...
<form method="post" action="http://node.jacquisflowershop.com/order">
    <div id="productWrapper">
        <div id="products"></div>
    </div>
    <div id="basket" class="ui-widget ui-widget-contentui-corner-all">
        <table border=0 id="basketTable">
            <thead id="theader" class="ui-widget-header">
                <tr>
                    <thclass="ui-corner-tl">Product</th>                    <th>Quantity</th>
                    <thclass="ui-corner-tr">Remove</th></tr>
            </thead>
            <tr id="placeholder"><td colspan=3>No Products</td></tr>
        </table>
    </div>
    <div id="buttonDiv"><button type="submit">Place Order</button></div>
</form>
...

我在table中添加了一个thead元素,将标题和正文行分开。给thead元素分配一个id并应用ui-widget-header类是很重要的。因为 header 是ui-widget-header类的一部分,所以可以将其从tr元素中移除。

接下来,将ui-corner-tlui-corner-tr类应用于标题行的外部单元格。这些类为它们被分配到的元素的左上角和右上角创建圆角。(我在第三十五章中描述了所有的 jQuery UI CSS 框架类。)

接下来,您需要使用赋予thead元素的id来禁用style元素中的 CSS border属性,并对table元素执行相同的操作,如下所示:

...
<style type="text/css">
    .dcell img {height: 60px}
    #basketTable {border: none; border-collapse: collapse}
    th, td {padding: 4px; width: 50px}
    td:first-child, th:first-child {width: 150px}
    #placeholder {text-align: center}
    #productWrapper {float: left; width: 65%}
    #basket {width: 33%; text-align: left; float: left; margin-left: 10px;
        position: absolute; right: 10px}
    #buttonDiv {clear: both}
    #logo {font-size: 1.5em; background-size: contain; margin: 1px;
        border: none; color: inherit}
    #logoWrapper {margin-bottom: 5px}
    #theader {border: none}
</style>
...

最后,您需要对removeTableRow函数做一点小小的调整。既然您已经分离了标题行并将其放入了一个thead元素中,那么在tbody中就少了一行。这是零钱:

...
function removeTableRow(row) {
    row.remove();
    if ($("#basketTable tbody").children(":visible").length == 0) {
        $("#placeholder").show();
    }
}
...

image 提示tbody元素是浏览器在解析表格元素时自动创建的。HTML 的一个奇怪之处是,您不必指定这个元素(尽管如果愿意,您可以指定)。

有了这些改变,你就有了一个与文档中其他元素相匹配的圆角表格,如图 26-10 所示。

9781430263883_Fig26-10.jpg

图 26-10 。圆角桌子

创建 jQuery UI 按钮

下一步是重新定位按钮,并将其转换为 jQuery UI 小部件。清单 26-7 显示了对文档的修改。

清单 26-7 。按钮的重新定位和变形

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <script src="handlebars.js"></script>
    <script src="handlebars-jquery.js"></script>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <style type="text/css">
        .dcell img {height: 60px}
        #basketTable {border: none; border-collapse: collapse}
        th, td {padding: 4px; width: 50px}
        td:first-child, th:first-child {width: 150px}
        #placeholder {text-align: center}
        #productWrapper {float: left; width: 65%}
        #basket {text-align: left;}
        #buttonDiv {clear: both; margin: 5px}
        #logo {font-size: 1.5em; background-size: contain; margin: 1px;
            border: none; color: inherit}
        #logoWrapper {margin-bottom: 5px}
        #theader {border: none}
    </style>
    <script id="flowerTmpl" type="text/x-handlebars-template">
        {{#flowers}}
            <div class="dcell">
                <img src="{{product}}.png"/>
                <label for="{{product}}">{{name}}:</label>
                <input name="{{product}}" value="0" />
            </div>
        {{/flowers}}
    </script>
    <script id="rowTmpl" type="text/x-handlebars-template">
        <tr id="{{name}}"><td>{{product}}</td><td>{{val}}</td>
            <td><a href="#">Remove</a></td>
        </tr>
    </script>
    <script type="text/javascript">
        $(document).ready(function () {

            $.getJSON("mydata.json", function (data) {

                var flowers = $("#flowerTmpl").template({ flowers: data }).filter("*");

                var rowCount = 1;
                for (var i = 0; i < flowers.length; i += 2) {
                    $("<a>").text(data[i].name + " & " + data[i + 1].name)
                        .appendTo("<h2>").parent().appendTo("#products");
                    $("<div>").attr("id", "row" + (rowCount++))
                        .appendTo("#products")
                        .append(flowers.slice(i, i + 2));
                }
                $("#products").accordion();

                $("input").change(function (event) {
                    $("#placeholder").hide();
                    var fname = $(this).attr("name");
                    var row = $("tr[id=" + fname + "]");

                    if (row.length == 0) {
                        $("#rowTmpl").template({
                            name: fname,
                            val: $(this).val(),
                            product: $(this).siblings("label").text()
                        }).appendTo("#basketTable").find("a").click(function () {
                            removeTableRow($(this).closest("tr"));
                            var iElem = $("#products").find("input[name=" + fname + "]");
                            $("#products").accordion("option", "active",
                                iElem.closest("div[id^=row]").index("div[id^=row]"));
                            iElem.val(0).select();
                        });
                    } else if ($(this).val() != "0") {
                        row.children().eq(1).text($(this).val());
                    } else {
                        removeTableRow(row);
                    }
                });
            });

            $("#buttonDiv, #basket").wrapAll("<div>").parent().css({
                float: "left",
                marginLeft: "2px"
            });

            $("button").button();

            function removeTableRow(row) {
                row.remove();
                if ($("#basketTable tbody").children(":visible").length == 0) {
                    $("#placeholder").show();
                }
            }
        });
    </script>
</head>
<body>
    <div id="logoWrapper" class="ui-widget ui-widget-content ui-corner-all">
        <h1 id="logo">Jacqui's Flower Shop</h1>
    </div>
    <form method="post" action="http://node.jacquisflowershop.com/order">
        <div id="productWrapper">
            <div id="products"></div>
        </div>
        <div id="basket" class="ui-widget ui-widget-content ui-corner-all">
            <table border=0 id="basketTable">
                <thead id="theader" class="ui-widget-header">
                    <tr>
                        <th class="ui-corner-tl">Product</th>
                        <th>Quantity</th>
                        <th class="ui-corner-tr">Remove</th></tr>
                </thead>
                <tr id="placeholder"><td colspan=3>No Products</td></tr>
            </table>
        </div>
        <div id="buttonDiv"><button type="submit">Place Order</button></div>
    </form>
</body>
</html>

我将buttonDivbasket元素包装在一个新的div元素中,并调整了一些 CSS 样式来调整这些元素的位置。并且,如图 26-11 所示,我调用button方法来创建一个 jQuery UI 按钮。

9781430263883_Fig26-11.jpg

图 26-11 。重新定位和变换按钮元素

添加完成对话框

当用户单击 Place Order 按钮时,我想从他们那里收集一些额外的信息。在第二十章中,我向你展示了如何使用标签显示多部分表单,所以为了更多样化,这次我将使用一个对话框小部件。清单 26-8 显示了对话框文档的变化。

清单 26-8 。添加对话框

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <script src="handlebars.js"></script>
    <script src="handlebars-jquery.js"></script>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <link rel="stylesheet" type="text/css" href="styles.css"/>

    <style type="text/css">
        .dcell img {height: 60px}
        #basketTable {border: none; border-collapse: collapse}
        th, td {padding: 4px; width: 50px}
        td:first-child, th:first-child {width: 150px}
        #placeholder {text-align: center}
        #productWrapper {float: left; width: 65%}
        #basket {text-align: left;}
        #buttonDiv {clear: both; margin: 5px}
        #logo {font-size: 1.5em; background-size: contain; margin: 1px;
            border: none; color: inherit}
        #logoWrapper {margin-bottom: 5px}
        #theader {border: none}
        #completeDialog input {width: 150px; margin-left: 5px; text-align: left}
        #completeDialog label {width: 60px; text-align: right}
    </style>
    <script id="flowerTmpl" type="text/x-handlebars-template">
        {{#flowers}}
            <div class="dcell">
                <img src="{{product}}.png"/>
                <label for="{{product}}">{{name}}:</label>
                <input name="{{product}}" value="0" />
            </div>
        {{/flowers}}
    </script>
    <script id="rowTmpl" type="text/x-handlebars-template">
        <tr id="{{name}}"><td>{{product}}</td><td>{{val}}</td>
            <td><a href="#">Remove</a></td>
        </tr>
    </script>
    <script type="text/javascript">
        $(document).ready(function () {

            $.getJSON("mydata.json", function (data) {

                var flowers = $("#flowerTmpl").template({ flowers: data }).filter("*");

                var rowCount = 1;
                for (var i = 0; i < flowers.length; i += 2) {
                    $("<a>").text(data[i].name + " & " + data[i + 1].name)
                        .appendTo("<h2>").parent().appendTo("#products");
                    $("<div>").attr("id", "row" + (rowCount++))
                        .appendTo("#products")
                        .append(flowers.slice(i, i + 2));
                }
                $("#products").accordion();

                $("#products input").change(function (event) {
                    $("#placeholder").hide();
                    var fname = $(this).attr("name");
                    var row = $("tr[id=" + fname + "]");

                    if (row.length == 0) {
                        $("#rowTmpl").template({
                            name: fname,
                            val: $(this).val(),
                            product: $(this).siblings("label").text()
                        }).appendTo("#basketTable").find("a").click(function () {
                            removeTableRow($(this).closest("tr"));
                            var iElem = $("#products").find("input[name=" + fname + "]");
                            $("#products").accordion("option", "active",
                                iElem.closest("div[id^=row]").index("div[id^=row]"));
                            iElem.val(0).select();
                        });
                    } else if ($(this).val() != "0") {
                        row.children().eq(1).text($(this).val());
                    } else {
                        removeTableRow(row);
                    }
                });
            });

            $("#buttonDiv, #basket").wrapAll("<div>").parent().css({
                float: "left",
                marginLeft: "2px"
            });

            $("button").button();

            $("#completeDialog").dialog({
                modal: true,
                buttons: [{ text: "OK", click: sendOrder },
                          {
                              text: "Cancel", click: function () {
                                  $("#completeDialog").dialog("close");
                              }
                          }]
            });

            function sendOrder() {

            }

            function removeTableRow(row) {
                row.remove();
                if ($("#basketTable tbody").children(":visible").length == 0) {
                    $("#placeholder").show();
                }
            }
        });
    </script>
</head>
<body>
    <div id="logoWrapper" class="ui-widget ui-widget-content ui-corner-all">
        <h1 id="logo">Jacqui's Flower Shop</h1>
    </div>
    <form method="post" action="http://node.jacquisflowershop.com/order">
        <div id="productWrapper">
            <div id="products"></div>
        </div>
        <div id="basket" class="ui-widget ui-widget-content ui-corner-all">
            <table border=0 id="basketTable">
                <thead id="theader" class="ui-widget-header">
                    <tr>
                        <th class="ui-corner-tl">Product</th>
                        <th>Quantity</th>
                        <th class="ui-corner-tr">Remove</th></tr>
                </thead>
                <tr id="placeholder"><td colspan=3>No Products</td></tr>
            </table>
        </div>
        <div id="buttonDiv"><button type="submit">Place Order</button></div>
    </form>
    <div id="completeDialog" title="Complete Purchase">
        <div><label for="name">Name: </label><input name="first" /></div>
        <div><label for="email">Email: </label><input name="email" /></div>
        <div><label for="city">City: </label><input name="city" /></div>
    </div>
</body>
</html>

我添加了一个div元素,它的内容将在body元素中显示给用户,同时添加了一些 CSS 样式来覆盖使用link元素导入到文档中的styles.css文件中的样式。下面是对创建对话框小部件的dialog方法的调用:

...
$("#completeDialog").dialog({
    modal: true,
    buttons: [{ text: "OK", click: sendOrder },
                {
                    text: "Cancel", click: function () {
                        $("#completeDialog").dialog("close");
                    }
                }]
});
...

我创建了一个有两个按钮的模态对话框。单击取消按钮将关闭对话框。点击确定按钮将调用sendOrder功能。这个函数目前不做任何事情。

正如你在第二十二章中所记得的,对话框部件默认是打开的,这意味着它一创建就显示给用户。你可以在图 26-12 中看到它是如何出现的。

9781430263883_Fig26-12.jpg

图 26-12 。用于完成购买的对话框

image 提示注意,当我在input元素上设置change事件时,我缩小了选择范围。我将选择限制为排除对话框中的那些输入元素。如果我没有这样做,在完成购买对话框中输入一个值就会在购物篮中添加一个新的项目。

点击处理下单按钮

我不想让用户看到对话框,直到他们点击下订单按钮。我使用autoOpen设置来隐藏对话框,直到需要的时候,并使用click方法来处理按钮点击,如清单 26-9 所示。

清单 26-9 。隐藏对话框和处理按钮单击

...
<script type="text/javascript">
    $(document).ready(function () {

        $.getJSON("mydata.json", function (data) {

            var flowers = $("#flowerTmpl").template({ flowers: data }).filter("*");

            var rowCount = 1;
            for (var i = 0; i < flowers.length; i += 2) {
                $("<a>").text(data[i].name + " & " + data[i + 1].name)
                    .appendTo("<h2>").parent().appendTo("#products");
                $("<div>").attr("id", "row" + (rowCount++))
                    .appendTo("#products")
                    .append(flowers.slice(i, i + 2));
            }
            $("#products").accordion();

            $("#products input").change(function (event) {
                $("#placeholder").hide();
                var fname = $(this).attr("name");
                var row = $("tr[id=" + fname + "]");

                if (row.length == 0) {
                    $("#rowTmpl").template({
                        name: fname,
                        val: $(this).val(),
                        product: $(this).siblings("label").text()
                    }).appendTo("#basketTable").find("a").click(function () {
                        removeTableRow($(this).closest("tr"));
                        var iElem = $("#products").find("input[name=" + fname + "]");
                        $("#products").accordion("option", "active",
                            iElem.closest("div[id^=row]").index("div[id^=row]"));
                        iElem.val(0).select();
                    });
                } else if ($(this).val() != "0") {
                    row.children().eq(1).text($(this).val());
                } else {
                    removeTableRow(row);
                }
            });
        });

        $("#buttonDiv, #basket").wrapAll("<div>").parent().css({
            float: "left",
            marginLeft: "2px"
        });

        $("button").button().click(function (e) {
            e.preventDefault();
            if ($("#placeholder:visible").length) {

                $("<div>Please select some products</div>").dialog({
                    modal: true,
                    buttons: [{
                        text: "OK",
                        click: function () { $(this).dialog("close") }
                    }]
                })
            } else {
                $("#completeDialog").dialog("open");
            }
        });

        $("#completeDialog").dialog({
            modal: true,
            autoOpen: false,
            buttons: [{ text: "OK", click: sendOrder },
                        {
                            text: "Cancel", click: function () {
                                $("#completeDialog").dialog("close");
                            }
                        }]
        });

        function sendOrder() {

        }

        function removeTableRow(row) {
            row.remove();
            if ($("#basketTable tbody").children(":visible").length == 0) {
                $("#placeholder").show();
            }
        }
    });
</script>
...

当用户点击按钮时,我会检查placeholder元素是否可见。我使用 jQuery 来实现这一点,使用一个选择器来生成一个jQuery对象,该对象仅在占位符可见时包含元素。

我使用占位符的可见性作为用户选择一些产品的代理。如果购物篮中有任何选择,占位符是隐藏的,因此一个可见的占位符告诉我没有选择。

image 提示这是一个很好的例子,展示了如何在文档中对功能进行分层,但这确实意味着我对产品选择的简单测试依赖于购物篮的实现,如果我修改了购物篮的工作方式,就需要进行修改。

如果用户没有选择任何产品就点击按钮,我会动态地创建并显示一个对话框小部件。你可以在图 26-13 中看到这是如何出现的。如果已经进行了选择,那么将显示完成对话框,以获取我希望从用户那里获得的最终信息。

9781430263883_Fig26-13.jpg

图 26-13 。如果没有产品选择,则显示一个对话框

完成订单

剩下的工作就是实现sendOrder函数。我已经向您展示了通过 Ajax 向服务器发送数据的不同方式,所以为了简化这一章,我将简单地从各种输入元素中收集值,并创建一个可以发送到服务器进行处理的 JSON 对象。清单 26-10 显示了对文档的补充。

清单 26-10 。完成订单流程

...
function sendOrder() {
    var data = new Object();
    $("input").each(function(index, elem) {
        var jqElem = $(elem);
        data[jqElem.attr("name")] = jqElem.val();
    })
    console.log(JSON.stringify(data));
    $("#completeDialog").dialog("close");
    $("#products input").val("0");
    $("#products").accordion("option", "active", 0)
    $("#basketTable tbody").children(":visible").remove();
    $("#placeholder").show();
}
...

在这个函数中,我从每个input元素中获取值,并将它们作为属性添加到一个对象中,然后我将其转换为 JSON 并写入控制台。

更有用的是,我然后重置文档,关闭对话框,重置input元素的值,切换到手风琴的第一个面板,并重置篮子。图 26-14 显示了一些产品选择的文件。我将使用这些来生成 JSON 字符串。

9781430263883_Fig26-14.jpg

图 26-14 。使用示例文档选择产品

当我点击“下订单”按钮时,会出现一个对话框,要求提供更多信息,如图图 26-15 所示。

9781430263883_Fig26-15.jpg

图 26-15 。提供附加信息完成订单

最后,单击 OK 按钮生成 JSON 并重置文档。这个例子的 JSON 如下:

{"aster":"12","daffodil":"7","rose":"5","peony":"2","primula":"0","snowdrop":"0",
 "first":"Adam Freeman","email":"adam@my.com","city":"London"}

如图 26-16 所示,你又回到了起点,准备再次经历这个过程。

9781430263883_Fig26-16.jpg

图 26-16 。重置文档

摘要

在这一章中,我重构了示例文档,加入了 jQuery UI 的特性。我添加了一些小部件,比如 accordion、dialog 和 button,并初步介绍了如何应用 jQuery UI CSS 框架类来管理其他元素的外观。我在第三十五章中给出了关于这些 CSS 类的更多细节。在第五部分中,我将转向 jQuery Mobile,您可以使用它来创建面向移动设备的 web 应用。

二十七、jQuery Mobile 入门

在这一章中,我将向您展示如何获得 jQuery Mobile 并将其添加到 HTML 文档中。我还解释了 jQuery Mobile 采用不同的方法来创建小部件,以及您必须如何适应这种方法。支持触摸的设备为 web 应用开发人员带来了一些独特的挑战,我将解释 jQuery Mobile 提供的一些核心特性,以帮助简化开发过程,并为移动 web 应用的开发和测试提供一些通用指南。表 27-1 提供了本章的总结。

表 27-1 。章节总结

问题解决办法列表
将 jQuery Mobile 添加到 HTML 文档。添加一个script元素来导入 jQuery 和 jQuery Mobile 库,添加一个link元素来导入 CSS。one
创建一个 jQuery Mobile 页面。使用值为pagedata-role属性。Two
禁用浏览器虚拟页面。配置视口。three
将定制 JavaScript 代码的执行推迟到 jQuery Mobile 增强文档之后。使用pageinit事件。four
简化触摸事件处理。使用 jQuery Mobile 手势和虚拟鼠标事件。5–7
响应设备方向的变化。处理orientationchange事件或使用 CSS 媒体查询。8, 9

image 注意正如我在第一章中所解释的,jQuery Mobile API 随着 1.3 版本的发布而发生了变化,我将在接下来的章节中重点介绍这些变化。

设置 jQuery Mobile

首先,我将向您展示如何获取和安装 jQuery Mobile。jQuery Mobile 是在 jQuery 和 jQuery UI 的基础上构建的,因此您将看到一些与这些库一致的常见模式。

获取 jQuery Mobile

你首先需要的是 jQuery Mobile,从http://jquerymobile.com开始就有了。在我写这篇文章时,jQuery Mobile 的当前版本是 1.3.1,您可以从下载页面获得一个 zip 文件。对于版本 1.3.1,这被称为jquery.mobile-1.3.1.zip

image 提示就像 jQuery 和 jQuery UI 一样,jQuery Mobile 可以通过内容分发网络(CDN)加载。我在第五章中描述了 cdn,它们对于部署在互联网上的 web 应用来说是一个很好的想法(但是很少用于内部网应用)。jQuery Mobile 下载页面包含了通过 CDN 使用 jQuery Mobile 所需的详细链接。

创建主题

jQuery Mobile 支持一个主题框架,它类似于 jQuery UI 使用的主题框架的简化版本。jQuery Mobile 包中包含一个默认主题,但是如果您想要创建一个自定义主题,您可以在http://jquerymobile.com/themeroller创建。使用 ThemeRoller 应用会生成一个 zip 文件,其中包含要包含在 web 文档中的 CSS 文件。我在第二十八章中描述了如何使用主题框架,但是我将在本书的这一部分使用默认主题,而不是创建一个自定义主题——部分原因是 jQuery Mobile ThemeRoller 没有一个方便的主题库。

获取 jQuery

还需要 jQuery。jQuery Mobile 1 . 3 . 1 版只适用于 jQuery 1 . 7 . 0 到 1.9.1 版。jQuery Mobile 往往落后于 jQuery 版本,在我写这篇文章时,对 jQuery 2.0 的支持还不可用。但是,您可以毫无问题地使用最新的 1.x 版本,因此我将使用 jQuery 1.10.1 作为本书这一部分的示例。

image 提示即使 jQuery Mobile 是基于 jQuery UI 构建的,你也不需要安装 jQuery UI 库。您需要的一切都包含在 jQuery Mobile 下载中。

正在安装 jQuery Mobile

您需要将 jQuery Mobile 下载中的三个项目复制到您提供 web 内容的目录中:

  • jquery.mobile-1.3.1.js文件(jQuery Mobile JavaScript 库)
  • jquery.mobile-1.3.1.css文件(jQuery Mobile 使用的 CSS 样式)
  • images目录(jQuery Mobile 图标)

当然,您还需要 jQuery 库,一旦一切就绪,您就可以创建一个使用 jQuery Mobile 的 HTML 文档。和前面的章节一样,我将我的文件命名为example.html,并将它保存在与前面列表中的项目相同的目录中。清单 27-1 显示了这个文件的内容。

清单 27-1 。example.html 的内容

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <script type="text/javascript" src="jquery.mobile-1.3.1.js"></script>
    <link rel="stylesheet" href="jquery.mobile-1.3.1.css" type="text/css" />
</head>
<body>
    <div data-role="page">
        <div data-role="header">
                <h1>Jacqui's Shop</h1>
        </div>
        <div data-role="content">
            This is Jacqui's Flower Shop
            <p><button>Press Me</button></p>
        </div>
   </div>
</body>
</html>

我突出显示的元素是 jQuery Mobile 所必需的。两个script元素导入 jQuery 和 jQuery Mobile JavaScript 库,link元素导入 jQuery Mobile 依赖的 CSS。因为我的 HTML 文件与 JavaScript 和 CSS 文件在同一个目录中,所以我可以简单地通过名称来引用这些文件。

image 提示暂时忽略文档的其余部分。我将简单解释一下meta元素的作用以及body元素的内容。

了解 jQuery Mobile 方法

虽然 jQuery Mobile 是基于 jQuery UI 的,但是您需要了解一些重要的区别。在您开始深入研究 jQuery Mobile 的功能之前,我需要解释一下这些差异,以便为后面几章中的信息提供一个背景。

分层支持

jQuery Mobile 为不同的移动浏览器提供了不同级别的支持。有三种级别的支持,每一种都有一个长长的支持设备和浏览器列表。在高端, A 级支持提供了最丰富的体验,并实现了我在本书这一部分描述的所有功能。

B 级支持提供了除 Ajax 导航之外的一切,我在第二十八章中描述了 Ajax 导航。这仍然是一个很好的功能水平,但是应用中页面之间的移动不会像 A 级设备那样流畅。

C 级品类基本。只有旧设备才属于这一类,jQuery Mobile 无法为这些设备提供太多的功能。

令人高兴的是,大多数现代移动设备都属于 A 级支持。您可以在http://jquerymobile.com/gbs看到支持设备的详细列表。

了解自动增强功能

使用 jQuery Mobile 时最显著的区别是,小部件不必显式创建。当使用 jQuery UI 时,您使用 jQuery 来选择一个或多个元素,然后应用诸如buttontabs之类的方法在文档中创建特定的 jQuery UI 小部件。如果你看一下清单 27-1 ,你会注意到我没有在文档中添加一个script元素来创建任何小部件。仅有的script元素用于导入 jQuery 和 jQuery Mobile 库。然而,如图 27-1 所示,我得到了格式化的内容。(该图显示了我在本书的这一部分广泛使用的 Opera 移动仿真器,我将在本章的后面适当地介绍它。)

9781430263883_Fig27-01.jpg

图 27-1 。该示例文档

image 注意对于本书这一部分中的大多数图形,我将使用横向分辨率的 Opera 移动浏览器模拟器,这让我可以在每页中装入更多的示例。

当您将 jQuery Mobile 库放入带有script元素的网页时,页面会自动增强。首先,jQuery Mobile 寻找具有data-role属性的元素。这些属性的值告诉 jQuery Mobile 应该对元素进行哪些增强。清单 27-2 突出显示了示例文档中的data-role属性。

image 提示名称以数据- 开头的属性称为数据属性。一段时间以来,数据属性一直是定义自定义属性的非正式约定,并且已经包含在 HTML5 的官方标准中。

清单 27-2 。示例文档中的数据角色属性

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <script type="text/javascript" src="jquery.mobile-1.3.1.js"></script>
    <link rel="stylesheet" href="jquery.mobile-1.3.1.css" type="text/css" />
</head>
<body>
    <divdata-role="page">
        <divdata-role="header">
                <h1>Jacqui's Shop</h1>
        </div>
        <divdata-role="content">
            This is Jacqui's Flower Shop
            <p><button>Press Me</button></p>
        </div>
   </div>
</body>
</html>

jQuery Mobile 的一个不寻常的特性是一个 HTML 文档可以包含多个页面(我在第二十八章中演示了这个特性)。页面是 jQuery Mobile 应用的构建块。在这个例子中只有一个页面,它由元素div表示,元素data-role的值是page。因为页面嵌套在 HTML 文档中,所以您还需要向 jQuery Mobile 提供关于页面中包含的元素的用途的附加信息。还有另外两个data-role属性,告诉 jQuery Mobile 哪个元素包含页面的标题信息,哪个元素包含内容。表 27-2 总结了本例中的三个data-role值及其意义。您可以很容易地将div元素及其data-role值与图 27-1 中所示的页面结构相关联。

表 27-2 。示例文档中的数据角色属性值

价值描述
page告诉 jQuery Mobile 将元素的内容视为页面。
header告诉 jQuery Mobile 该元素表示页面标题。
content告诉 jQuery Mobile 元素包含页面的内容。

image 提示 jQuery Mobile 会自动为页面的内容部分插入包装器。这意味着不属于另一部分的任何元素都被视为内容,从而允许您显式跳过为该部分定义元素。

您不必采取任何显式的操作来让 jQuery Mobile 找到具有data-role属性的元素并生成页面。这一切都是在加载 HTML 文档时自动发生的。有些元素,比如button,是自动样式化的(尽管,正如我在后面的章节中演示的,您可以使用其他数据属性配置大多数小部件)。

image 提示 jQuery Mobile 不遗余力地减少创建移动 web 应用所需的定制 JavaScript 的数量。事实上,根本不需要任何定制的 JavaScript 就可以创建简单的应用。然而,这并不意味着您可以为禁用了 JavaScript 的浏览器构建 jQuery Mobile 应用。jQuery Mobile 是一个 JavaScript 库,需要 JavaScript 支持来执行页面的自动增强。

了解视口

虽然不是 jQuery Mobile 的一部分,但添加到 HTML 文档中的一个重要元素是清单 27-3 中突出显示的元素。

清单 27-3 。配置视口的元元素

...
<head>
    <title>Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <script type="text/javascript" src="jquery.mobile-1.3.1.js"></script>
    <link rel="stylesheet" href="jquery.mobile-1.3.1.css" type="text/css" />
</head>
...

我突出显示了名称属性为viewportmeta元素。许多移动浏览器使用虚拟页面来显示网页内容,以提高与桌面浏览器设计的网站的兼容性。这通常是一个明智的想法,因为它为用户提供了页面结构的整体感觉,即使细节太小而无法阅读。图 27-2 显示了 jQuery Mobile 主页的初始显示和缩放,以便文本可读。

9781430263883_Fig27-02.jpg

图 27-2 。移动浏览器虚拟页面

第一个框架以纵向显示了 jQuery Mobile web 站点(这突出了效果)。文本太小,无法阅读,但移动浏览器支持放大页面区域,如第二帧所示。诚然,虚拟页面是一种妥协,但考虑到为移动设备定制的网站相对较少,这是可以理解的。

问题是虚拟页面的应用没有太多的区别,这给 jQuery Mobile 应用带来了问题。图 27-3 显示了当使用虚拟页面时,示例文档是如何显示的。

9781430263883_Fig27-03.jpg

图 27-3 。在宽虚拟页面中显示的示例文档

如图所示,jQuery Mobile 元素显示得非常小,以至于无法使用。示例文档中的meta元素告诉浏览器页面的宽度应该是屏幕的宽度。这导致浏览器以合理的大小显示您的 jQuery Mobile 元素。

了解 jQuery Mobile 事件

关于与 jQuery Mobile 相关的事件,有两条重要的信息。在接下来的小节中,我将对它们进行描述。

了解页面事件

jQuery Mobile 定义了一系列描述页面生命周期的事件。其中最重要的是pageinit事件。jQuery Mobile 通过注册其函数来处理 jQuery ready事件,自动增强页面,您在本书的前面部分已经依赖了该事件。如果您想在文档中包含定制的 JavaScript,您必须注意在 jQuery Mobile 处理完文档之前不要执行您的代码。这意味着您必须等待pageinit事件,然后在 jQuery Mobile 完成文档初始化时触发该事件。没有像ready事件那样方便的方法,所以你必须使用bind方法将你的函数与事件关联起来,如清单 27-4 所示。

清单 27-4 。使用 pageinit 事件

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <script type="text/javascript">
        $(document).bind("pageinit", function () {
            $("button").click(function () {
                console.log("Button pressed")
            })
        });
    </script>
    <script type="text/javascript" src="jquery.mobile-1.3.1.js"></script>
    <link rel="stylesheet" href="jquery.mobile-1.3.1.css" type="text/css" />
</head>
<body>
    <div data-role="page">
        <div data-role="header">
                <h1>Jacqui's Shop</h1>
        </div>
        <div data-role="content">
            This is Jacqui's Flower Shop
            <p><button>Press Me</button></p>
        </div>
   </div>
</body>
</html>

bind方法的参数是您感兴趣的事件的名称以及当事件被触发时应该执行的函数。只有当您选择并应用了bind方法的元素的事件被触发时,您的函数才会被执行。

在这个例子中,我使用了bind方法来注册一个函数,这个函数将在pageinit事件被触发时执行。在这个函数中,我放置了希望在文档加载和处理后执行的语句。在本例中,我使用 jQuery 选择了文档中的button元素,并使用click方法注册了另一个在单击按钮时将执行的函数,正如我在本书中一直做的那样。

image 提示请注意,在将 jQuery Mobile JavaScript 库导入到文档之前,我已经插入了新的script元素*。这对于pageinit事件来说并不重要,但是对于用于更改一些 jQuery Mobile 设置的mobileinit事件来说是必需的(我将在第二十八章中演示如何做到这一点)。我发现在导入 jQuery Mobile 库之前总是放置定制代码是一个好主意,即使我只是响应pageinit事件。*

理解触摸事件

浏览器中的触摸事件有一个规范,但它是非常低级的,因为在触摸交互模型中有很多变化。例如,一些设备支持多点触摸,并且触摸手势的解释方式多种多样。表 27-3 描述了这些低级触摸事件。

表 27-3 。标准触摸事件

事件描述
touchstart当用户触摸屏幕时触发。对于多点触摸设备,每次手指触摸屏幕时都会触发此事件。
touchend当用户将手指从屏幕上移开时触发。
touchmove当用户在触摸屏幕时握住或移动手指时触发。
touchcancel当触摸序列中断时触发。这是特定于设备的含义,但一个常见的例子是当用户将手指滑离屏幕边缘时。

解释这些事件并找出其意义的责任落在了开发人员身上。这是一项充满错误的痛苦任务,我建议你尽可能避免。这是 jQuery Mobile 可以帮助解决的问题,我将简单解释一下。

image 提示如果你确实想了解触摸事件的细节,那么你可以在www.w3.org/TR/touch-events找到规范。这包括事件的完整描述和可用于获得每个触摸交互细节的属性。

大多数网站在设计时都没有考虑到触摸事件。为了支持尽可能广泛的网站脚本,移动浏览器从触摸事件合成鼠标事件。这意味着浏览器触发触摸事件,然后生成相应的(假的)鼠标事件,这些事件代表相同的动作,但就好像它们是用传统鼠标执行的一样。清单 27-5 包含了一个有用的脚本,演示了这是如何实现的。

清单 27-5 。监控触摸和合成鼠标事件

<!DOCTYPE html>
<html>
<head>
    <title>Event Test</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="jquery.mobile-1.3.1.css" type="text/css" />
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <style type="text/css">
        table {border-collapse: collapse; border: medium solid black; padding: 4px}
        #placeholder {text-align: center}
        #countContainer * {display: inline; width:50px}
        th {width: 100px}
    </style>
    <script type="text/javascript">
        $(document).bind("pageinit", function() {
            var eventList = [
                "mousedown", "mouseup", "click", "mousecancel",
                "touchstart", "touchend", "touchmove", "touchcancel"]

            for (var i = 0; i < eventList.length; i++) {
                $("#pressme").bind(eventList[i], handleEvent)
            }

            $("#reset").bind("tap", function() {
                $("tbody").children().remove();
                $("#placeholder").show();
                startTime = 0;
            })
        });

        startTime = 0;
        function handleEvent(ev) {
            var timeDiff = startTime == 0 ? 0 : (ev.timeStamp - startTime);
            if (startTime == 0) {
                startTime = ev.timeStamp
            }
            $("#placeholder").hide();
            $("<tr><td>" + ev.type + "</td><td>" + timeDiff + "</td></tr>")
                .appendTo("tbody");
        }
    </script>
    <script type="text/javascript" src="jquery.mobile-1.3.1.js"></script>
</head>
<body>
    <div data-role="page">
        <div data-role="content">
            <div id="tcontainer" class="ui-grid-a">
                <div class="ui-block-a">
                    <button id="pressme">Press Me</button>
                    <button id="reset">Reset</button>
                </div>
                <div class="ui-block-b">
                    <table border=1>
                        <thead>
                            <tr><th>Event</th><th>Time</th></tr>
                            <tr id="placeholder"><td colspan=2>No Events</td><tr>
                        </thead>
                        <tbody></tbody>
                    </table>
                </div>
            </div>
        </div>
   </div>
</body>
</html>

本例中有两个按钮和一个表格。“按我”按钮是有线连接的,因此当单击按钮时,选择的鼠标和触摸事件会显示在表格中。对于每个事件,显示事件类型和自上次事件以来的毫秒数。重置按钮清除表格并重置计时器。你可以在图 27-4 中看到效果。

9781430263883_Fig27-04.jpg

图 27-4 。观察触摸和鼠标事件的顺序

表 27-4 显示了在 Opera 移动浏览器中点击按钮时出现的事件序列和计时。

表 27-4 。来自 Opera Mobile 的事件序列

事件相对时间
touchstart0
touchend96
mousedown315
mouseup315
click321

你可以看到touchstarttouchend事件首先被触发,响应我触摸然后放开屏幕的瞬间。浏览器然后生成mousedownmouseup事件,然后生成一个click事件。注意,在触发touchendmousedown事件之间有相当大的延迟,大约 300 毫秒。这种延迟足以使依赖合成事件成为问题,因为您的 web 应用将落后于用户的触摸交互。不是所有的浏览器都有这个问题,但这是一个很常见的问题,我建议您在您打算使用的浏览器上测量延迟。

使用 jQuery Mobile 手势方法

jQuery Mobile 做了两件事来简化事件处理。第一个是一组手势事件,这些事件是响应特定的低级触摸事件序列而触发的,这意味着您不必自己分析触摸序列来理解用户正在做出的手势。这些事件在表 27-5 中描述。

表 27-5 。标准触摸事件

事件描述
tap当用户触摸屏幕,然后快速连续移开手指时触发。
taphold当用户触摸屏幕,保持手指不动大约一秒钟,然后松开时触发。
swipe当用户在一秒钟内执行至少 30 个像素的水平拖动,并且垂直变化小于 20 个像素时触发。
swipeleft当用户向左滑动时触发。
swiperight当用户向右滑动时触发。

这些事件使得处理基本手势变得简单。清单 27-6 将这些事件添加到计时示例中。

清单 27-6 。将 jQuery Mobile 手势事件添加到计时示例中

...
<script type="text/javascript">
    $(document).bind("pageinit", function() {
        var eventList = [
            "mousedown", "mouseup", "click", "mousecancel",
            "touchstart", "touchend", "touchmove", "touchcancel",
            "tap", "taphold", "swipe", "swipeleft", "swiperight"]

        for (var i = 0; i < eventList.length; i++) {
            $("#pressme").bind(eventList[i], handleEvent)
        }

        $("#reset").bind("tap", function() {
            $("tbody").children().remove();
            $("#placeholder").show();
            startTime = 0;
        })
    });

    startTime = 0;
    function handleEvent(ev) {
        var timeDiff = startTime == 0 ? 0 : (ev.timeStamp - startTime);
        if (startTime == 0) {
            startTime = ev.timeStamp
        }
        $("#placeholder").hide();
        $("<tr><td>" + ev.type + "</td><td>" + timeDiff + "</td></tr>")
            .appendTo("tbody");
    }
</script>
...

图 27-5 显示了当我点击浏览器中的按钮时会发生什么。

9781430263883_Fig27-05.jpg

图 27-5 。将 jQuery Mobile 手势事件添加到计时示例中

表 27-6 以易读的表格显示了事件顺序。因为我正在点击一个按钮,所以唯一出现的手势事件是tap。需要注意的重要一点是,点击事件被快速触发,通常在我从屏幕上松开的几毫秒内。

表 27-6 。来自 Opera Mobile 的事件序列

事件相对时间
touchstart0
touchend63
轻点63
mousedown317
mouseup321
click328

手势事件的好处在于,jQuery Mobile 即使在不支持触摸事件的浏览器中,或者在没有触摸界面的设备上运行的浏览器中,也会触发手势事件。图 27-6 显示了运行在谷歌 Chrome 桌面浏览器中的例子。

9781430263883_Fig27-06.jpg

图 27-6 。桌面浏览器中的事件序列

表 27-7 更清楚地显示了事件的顺序及其相对时序。

表 27-7 。来自谷歌浏览器的事件序列

事件相对时间
mousedown0
mouseup79
click80
轻点80

如你所料,这个序列中没有touchstarttouchend事件,事件的顺序也不同(因为鼠标事件是真实的,而不是合成的)。即便如此,tap事件仍然会在click事件之后立即触发。

image 提示我在移动 web 应用中使用tap事件而不是click,因为它避免了来自合成事件的计时问题,并且因为它也是在非触摸平台上生成的。

使用 jQuery Mobile 虚拟鼠标事件

浏览器不需要合成鼠标事件,这意味着在触摸和非触摸设备上工作的 web 应用应该监听鼠标事件和触摸事件。对于合成事件的移动浏览器,每次交互都有触摸和鼠标事件。为了帮助简化这个过程,jQuery Mobile 定义了一组虚拟鼠标事件。当您注册这些事件时,jQuery Mobile 会负责删除重复的事件,并确保触发适当的事件,而不管您是否支持触摸。表 27-8 描述了虚拟事件。

表 27-8 。标准触摸事件

事件描述
vmouseover响应mouseover事件时触发(因为用户的手指并不总是接触屏幕,所以没有等效的触摸事件)。
vmousedown响应touchstartmousedown事件而触发。
vmousemove响应touchmovemousemove事件而触发。
vmouseup响应touchendmouseup事件而触发。
vclick响应click事件而触发。
vmousecancel响应touchcancelmousecancel事件而触发。

这些事件的生成方式创造了一个类似鼠标的序列,即使在触摸设备上也是如此。为了解释我的意思,我在计时示例中添加了一些虚拟事件,如清单 27-7 所示。

清单 27-7 。将 jQuery Mobile 虚拟事件添加到计时示例中

...
<script type="text/javascript">
    $(document).bind("pageinit", function() {
        var eventList = [
            "mousedown", "mouseup", "click", "mousecancel",
            "touchstart", "touchend", "touchmove", "touchcancel",
            "tap", "taphold", "swipe", "swipeleft", "swiperight",
            "vmouseover", "vmousedown", "vmouseup", "vclick", "vmousecancel"]

        for (var i = 0; i < eventList.length; i++) {
            $("#pressme").bind(eventList[i], handleEvent)
        }

        $("#reset").bind("tap", function() {
            $("tbody").children().remove();
            $("#placeholder").show();
            startTime = 0;
        })
    });

    startTime = 0;
    function handleEvent(ev) {
        var timeDiff = startTime == 0 ? 0 : (ev.timeStamp - startTime);
        if (startTime == 0) {
            startTime = ev.timeStamp
        }
        $("#placeholder").hide();
        $("<tr><td>" + ev.type + "</td><td>" + timeDiff + "</td></tr>")
            .appendTo("tbody");
    }
</script>
...

当我触摸屏幕时,jQuery Mobile 生成了vmouseovervmousedown事件。这些在纯触控环境下没有任何意义。如果您正在编写一个跨平台的应用,那么您可能希望在用户将桌面鼠标移动到某个元素上时执行一些操作。通过触发合成的vmouseover事件来响应真实的touchstart事件,您可以无缝地对触摸设备执行相同的操作。你可以在图 27-7 中看到结果。

9781430263883_Fig27-07.jpg

图 27-7 。将虚拟事件添加到计时示例中

表 27-9 以更易于阅读的形式显示了事件和时序。尽管vclick事件在合成click事件之前很久就被触发了,但情况并不总是如此,我不推荐使用vclick来代替click来解决事件延迟问题。

表 27-9 。来自谷歌浏览器的事件序列

事件相对时间
touchstart0
vmouseover0
vmouedown0
touchend64
vmouseup64
v 点击64
tap64
mousedown320
mouseup327
click331

image 警告不要对现实和虚拟事件交错的方式做出假设,这一点很重要。这是因为非触摸设备上的事件序列会有所不同。虚拟事件相对于彼此的顺序是相同的;中间的真实事件可能会改变。

响应器件方向变化

大多数移动浏览器支持一个名为orientationchange的事件,每当设备旋转 90 度时就会触发该事件。为了让生活更简单,jQuery Mobile 会在浏览器不支持的时候合成orientationevent 。这是通过监听窗口大小的变化并查看新的高度和宽度值的比率来完成的。清单 27-8 展示了如何对这个事件做出响应。

清单 27-8 。响应方向的变化

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="jquery.mobile-1.3.1.css" type="text/css" />
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <script type="text/javascript">
        $(document).bind("pageinit", function() {
            $(window).bind("orientationchange", function(e) {
                $("#status").text(e.orientation)
            })
            $("#status").text(jQuery.event.special.orientationchange.orientation())
        });
    </script>
    <script type="text/javascript" src="jquery.mobile-1.3.1.js"></script>
</head>
<body>
    <div data-role="page">
        <div data-role="header">
                <h1>Jacqui's Shop</h1>
        </div>
        <div data-role="content">
            <p>Device orientation is: <b><span id=status></span></b></p>
        </div>
   </div>
</body>
</html>

为了绑定到orientationchange事件,您必须选择window对象。在这个例子中,我修改了一个表示新方向的span元素的文本。这个信息可以通过传递给处理函数的Event对象的orientation属性获得。

jQuery Mobile 还提供了一种确定当前方向的方法,如下所示:

...
jQuery.event.special.orientationchange.orientation()
...

我在示例中使用这个方法来设置span元素的内容,因为在处理页面时不会触发orientationchange事件,只有在设备随后被重定向时才会触发。

如果你没有一个真实的移动设备来测试这个例子,那么你可以使用我在本章后面描述的模拟器。它们中的大多数都有模拟旋转的能力,由特定的击键或按钮触发。对于我正在使用的 Opera 移动模拟器 ,按 Ctrl+Alt+R 触发效果,如图图 27-8 所示。

9781430263883_Fig27-08.jpg

图 27-8 。响应方向的变化

jQuery Mobile 产生的合成事件意味着,当您调整不支持方向更改的浏览器(如桌面浏览器)的窗口大小时,可以获得相同的效果。在这种情况下,方向由窗口的宽度和高度决定。

使用媒体查询来管理方向

orientationchange事件允许您使用 JavaScript 响应方向的变化。另一种方法是使用 CSS,对每个方向的元素应用不同的样式,这可以使用 CSS 媒体查询来实现。清单 27-9 展示了如何做到这一点。

清单 27-9 。使用 CSS 媒体查询响应方向

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="jquery.mobile-1.3.1.css" type="text/css" />
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <script type="text/javascript" src="jquery.mobile-1.3.1.js"></script>
    <style type="text/css">
        @media screen and (orientation:portrait) {
            #pstatus {display: none}
            #lstatus {display: inline}
        }

        @media screen and (orientation:landscape) {
            #pstatus {display: inline}
            #lstatus {display: none}
        }
    </style>
</head>
<body>
    <div data-role="page">
        <div data-role="header">
                <h1>Jacqui's Shop</h1>
        </div>
        <div data-role="content">
            <p>Device orientation is:
                <span id=pstatus><b>Portrait</b></span>
                <span id=lstatus><b>Landscape</b></span>
            </p>
        </div>
   </div>
</body>
</html>

CSS 媒体查询允许您定义在特定情况下应用的样式集,在这种情况下,方向是横向或纵向。我使用 CSS display属性来显示或隐藏元素,这允许我创建与前一示例中使用 JavaScript 相同的效果。在这种情况下,不需要任何形式的合成。针对方向的媒体查询同样适用于桌面浏览器和移动浏览器。

使用移动设备

为移动设备开发应用与常规的桌面开发有一些明显的不同。在接下来的部分中,我将提供一些指导和信息来帮助您开始,并强调您将面临的一些关键问题。

识别移动设备

如果你为桌面和移动用户提供一个应用,你可能想要定制你所展示的界面。一种常见的方法是为桌面浏览器提供 jQuery UI 接口,为移动设备提供 jQuery Mobile 接口。

困难在于识别哪些浏览器运行在移动设备上。有多种技术可以做到这一点,所有这些都在服务器端执行,将浏览器重定向到适当的 HTML 文档。我不打算深入讨论这些细节,因为它们超出了本书的范围。如果您是这个问题的新手,那么我建议您看看http://wurfl.sourceforge.net,它包含了一个有用的服务器端组件,可以识别大多数移动设备。您还应该考虑https://github.com/kaimallea/isMobile,它提供了一个客户端解决方案。

我建议不要根据用户的浏览器自动强迫用户使用移动版本的应用。一些用户更喜欢使用桌面版本的应用,甚至在移动设备上,特别是因为移动版本通常具有受限的功能。我的建议是,当用户到达你的站点时,如果你检测到一个移动设备,就给用户一个简单的选择,并且即使已经做出了最初的决定,也要使在你的应用版本之间切换变得容易。

避免移动开发的两大罪

在为移动设备构建 web 应用时,有两个陷阱需要避免:糟糕的假设不切实际的模拟。我解释了这两者,并提供了一些背景来帮助您避免犯一些常见的错误。

避免不良假设

移动设备市场非常活跃,相对不成熟,缺乏标准化。在为桌面构建 web 应用时,通常会做出一些假设(尽管通常不会明说)。人们普遍期望最低屏幕分辨率、JavaScript 支持、某些插件的可用性,以及用户能够使用鼠标点击和键盘输入文本。

这并不是说这些都是合理的假设。例如,如果您假设 JavaScript 可用,那么您就排除了那些没有(或不能)在浏览器中启用 JavaScript 的潜在客户。您可能会认为这是一个很好的权衡,大多数用户如果愿意都可以启用 JavaScript,而您将放弃不符合您所要求的规范的用户。

移动设备市场的情况更加复杂,因为这个市场非常分散。桌面空间可能看起来多种多样,但是 Mac、Windows PC 和 Linux box 都有很多共同点。对于移动设备来说就不一样了,关于屏幕大小、网络连接和输入法的假设将会消除一些大的市场份额。

世界不是一部 iPhone

我看到的最糟糕的假设之一是目标市场是 iPhone。iPhone 取得了巨大的成功,但它并不是移动设备市场的全部,甚至不同型号的 iPhone 之间也存在差异。常见的目标屏幕分辨率是 320×480 像素,这来自于较旧的 iPhone 型号。很多设备都有这种分辨率,但越来越多的设备没有。在你的移动应用中使用固定的屏幕分辨率只会淘汰那些屏幕太小的用户,并让那些支付了额外费用来获得更高分辨率设备的用户感到烦恼。

这个世界根本不是一部电话

另一个常见的假设是,目标市场是手机,忽略了平板电脑市场的成功。不仅平板电脑通常具有更高的屏幕分辨率,而且人们持有和使用它们的方式也不同。要明白我的意思,去任何一家咖啡店看看顾客。我的观察(完全不科学,但始终如一)是,更大尺寸的平板电脑让它们拿起来有点别扭,所以它们通常靠在别的东西上。这意味着它们有些不稳定,在屏幕上拖动手指会使平板电脑轻微抖动(使准确性成为问题),并模糊了屏幕的许多部分(因为用户的手和手臂在平板电脑本身上方)。

我的观点是,移动设备的本质在很大程度上决定了它们的使用方式,以及什么样的交互是明智和可取的。弄清楚这一点的最好方法是观察人们与一系列设备的交互。如果你有时间和金钱,可用性实验室是一个很好的资源。但是,即使你很匆忙,预算有限,在星巴克度过一个下午也能提供一些有价值的见解。

这个世界不支持触摸

并非所有的移动设备都有触摸屏。有些依靠微型鼠标结合键盘,有些有多种输入方法。我的测试机器之一是一台可以转换成平板电脑的小型笔记本电脑。它有一个触摸屏以及一个完整的键盘和鼠标。用户希望能够使用他们可用的最佳输入方法,对可用输入进行假设只会让用户感到沮丧(这也是我除了测试之外很少使用笔记本电脑/平板电脑组合设备的原因)。

移动带宽不是免费的,也不是无限的

网络连接的价格经历了几个周期,由网络用户执行的各种活动决定。目前,网络提供商正在努力筹集资金和建设足够的网络容量来满足需求,特别是在人口密集的城市地区。最终,容量成本将会下降,可用带宽将会增加,但目前,网络提供商对数据访问收取额外费用,并对用户每月可以下载的数据量设置较低的上限。

假设用户愿意将他们的大量数据贡献给你的 web 应用是很危险的。一般来说,客户并不像你希望的那样关心你的应用。听起来可能很伤人,但几乎总是真的。你的应用充满了你的世界,这是应该的,但对用户来说,它只是众多应用中的一个。

在第二十八章中,我将向您展示 jQuery Mobile 如何在用户需要之前预取 web 应用的内容。这是一个很棒的功能,但应该谨慎使用,因为它假设用户愿意在他们可能永远不需要的内容上花费带宽。自动和频繁的数据更新也是如此。请谨慎使用,并且只有当用户明确表示您的应用应该是他的网络配额的大量用户时才使用。

同样,不要对移动设备可用的数据速率做出假设。考虑一下您对图像和视频等大型资源的使用。一些用户将有能力快速下载这些内容,但许多人不会,以我的经验,低带宽的选择总是受欢迎的。

您还应该准备好应对网络不可用的情况。以前坐火车上下班,一进隧道网络就会掉线。一个编写良好的 web 应用会预料到连接问题,向用户报告这些问题,并在网络重新可用时优雅地恢复。可悲的是,大多数应用都写得不好。

image 提示我曾经为一家全球集装箱运输公司工作过,他们面临的约束促使他们开发了一些我见过的最健壮、适应性最强的应用。他们在世界上几乎每一个港口都有运输代理人,这些地方非常偏远,代理人的办公室只是码头尽头的一间小屋。他们可以将现代电脑运送到这些地方(这不成问题,因为他们是一家运输公司),但联网通常仅限于慢速拨号连接,在停电期间每天工作几个小时,这意味着连接建立之间可能需要几天时间。即使没有网络链接,每个应用都必须允许本地运输办公室继续工作,并且只要可以建立连接,就将本地数据与全球网络同步。这需要大量的思考和测试,但结果是 It 基础设施帮助他们主宰了全球集装箱运输。我在设计移动应用时经常会想到这些限制——现代设备通常可以期望更好的操作条件,但最好的应用总是希望最好的,假设最坏的,并代表用户处理问题。

避免不切实际的模拟和测试

移动设备的多样性意味着你必须进行彻底的测试。在开发的早期阶段使用实际的移动设备可能会令人沮丧。网络请求通过单元网络路由,这要求开发机器是公开可用的。一些移动设备有开发者模式,但是它们有自己的缺点。

简而言之,您需要一个模拟的环境来开始您的开发,这种环境能够让您快速方便地构建和测试,而不必将您的开发环境暴露给外界。幸运的是,有仿真器提供您需要的设施。我将在本章后面描述一些可用的选项,但它们分为两类。

第一类模拟器是将实际的移动浏览器移植到另一个平台上。浏览器的一切都尽可能的接近真实。第二类依赖于这样一个事实,即大多数浏览器为移动和桌面机器使用一个通用的渲染引擎。因此,举例来说,如果你想大致了解 iPhone 浏览器将如何处理文档,你可以使用苹果 Safari 浏览器,因为它有共同的根。模拟器只不过是一个可视化包装器和桌面渲染引擎周围的屏幕大小限制。

这两种方法都很有用,值得探索。我经常在移动产品开发的早期阶段使用它们。但是一旦我有了基本的功能,我就开始在真实设备上进行测试,随着项目接近完成,我切换到只使用真实设备,并完全停止使用模拟器。

这是因为模拟器有两个主要缺点。首先,他们的模拟并不是 100%准确。即使是最好的模拟器也不总是像使用相同浏览器的真实设备那样呈现内容。第二个——在我看来也是最重要的——失败在于触摸输入是模拟的。

鼠标用于使支持触摸的浏览器在非触摸桌面 PC 上工作,鼠标只是不能产生与手指相同的效果。桌面仿真中缺少三个触摸因素:触感阻碍不准确

触感的缺乏

缺乏触感意味着你不知道使用网络应用会有什么样的感觉*。在玻璃显示屏上轻敲和滑动是一项奇怪的活动。当应用正确响应时,效果是优雅和令人愉快的。当应用滞后于输入或误解了触摸交互时,结果是令人沮丧的。鼠标不能给你反馈,告诉你如何处理触摸。*

*没有阻碍

我已经提到了阻碍的问题。当你使用触摸设备时,即使是很小的设备,你的手指和手也会遮住部分屏幕。在为触摸设备设计 web 应用时,您需要考虑到这一点。你需要小心地放置控件,以便用户在触摸屏幕时仍然可以看到正在发生的事情,并且你需要记住,大约 10%的人是左撇子,因此屏幕的不同部分对这些用户来说是模糊的。只有亲手触摸按钮和链接,您才能真正理解您的 web 应用是多么容易使用。

image 提示如果你去咖啡店做一些用户观察,留意那些遵循独特模式的用户。他们触摸屏幕,然后将手完全移开一秒钟,然后收回手,做出另一个触摸手势。这通常表示应用已经定位了它的窗口小部件,使得从一个动作产生的视觉反馈在用户的手中。用户必须把她的手移开才能看到发生了什么,然后再移回来做另一个手势,这是一个令人疲惫和沮丧的经历。

缺乏不准确性

有了鼠标,用户可以异常准确地击中屏幕上的目标。每像素精度可以通过现代鼠标和一点点练习来实现。人的手指却不是这样,你所能期望的最大精度是“大致在目标的区域内”。这意味着你必须选择简单的小工具,并创建考虑到不准确性的布局。您无法感受到在模拟器中点击小部件是多么容易。鼠标不是一个足够好的代理。你需要在一系列不同的屏幕尺寸和分辨率上进行测试,以了解你的用户将会面对什么。这些信息提供了关于页面上小部件的大小和密度的重要线索。

一个不准确的个人故事

我个人的挫折感可以追溯到我过去乘火车上下班的时候。我住在英国,在那里,火车准时到达被视为一个无法实现的目标。在夏天,当火车晚点时,我真的不介意。我可以在阳光下逗留。我从来不想在冬天逗留,几分钟后我想查看火车会晚点多长时间,这可以通过在线应用实现。

想象一下这个场景。太阳还没出来,风很刺骨,地面冰冷。我被裹得暖暖的,但是我从车里带出来的热量正在迅速消退。我想加载应用,导航到本地电台的信息,并了解我将等待多长时间(如果要等一段时间,回到我的车上并考虑开车去办公室)。

我一脱下手套,手指就开始发冷。几分钟后,我不能正常弯曲手指,我的手开始颤抖,这很不幸,因为我需要点击的小部件很小。它们只是用小字体显示的普通网络链接。我从来没有设法轻松导航到我想要的信息。我会点击错误的链接,被迫等待加载错误的信息,然后导航回来再试一次。与此同时,随着我的手越来越冷,我准确点击小工具的能力越来越差。我变得讨厌假定像素精度的移动网络应用,特别是那个应用。

使用移动浏览器模拟器

尽管移动浏览器模拟器有其局限性,但它仍然有着重要的作用。在本书的前一版本中,我描述了一系列不同的模拟器以及每种模拟器的优缺点。从那以后,我的开发风格改变了,我只用两个工具:Opera 移动模拟器和 BrowserStack。

使用桌面浏览器测试移动应用

桌面浏览器显然与移动版本不同,但有共同的基础,在部署到真实设备之前,可以用于快速和肮脏的测试。当我已经有了应用的主要构件,并且正在充实功能区域时,我经常使用这些浏览器。使用桌面浏览器的主要好处是它们拥有优秀的开发工具,包括 JavaScript 调试器,并且在大多数情况下,桌面浏览器是移动项目早期阶段或当您试图跟踪代码或标记中的问题时的优秀开发工具。

使用 Opera 手机模拟器

我在项目的初始阶段使用 Opera Mobile 模拟器,这是我唯一做的测试。这个浏览器允许我模拟不同屏幕尺寸的设备,包括平板电脑和横向设备。

正在被仿真的 Opera 移动浏览器被广泛使用,仿真器在精确布局内容方面做得很合理(如果不是完美的话)。一些 jQuery Mobile 特性,比如导航转换(我在第二十八章的中描述过)不被支持。这是我在本章前面用来获取图形屏幕截图的模拟器。

这种测试的主要好处是速度快,允许我喜欢的快速编写和测试的开发风格。一个很好的特性是,您可以使用 Opera 桌面版内置的调试器来调试移动模拟器。设置这个的过程有点笨拙,但是它是一个有用的特性。Opera 移动模拟器在http://www.opera.com/developer/mobile-emulator免费提供。

使用浏览器堆栈

BrowserStack 是一项商业服务,提供在通用操作系统上运行多种浏览器的虚拟机。我已经开始使用这项服务,因为它比维护我自己的测试环境更简单。这并不是一个完美的解决方案——例如,移动浏览器是模拟器,而不是实际的硬件,但这项服务快速、易于使用,而且相当全面。您可以在http://browserstack.com 获得一个试用账户,也有提供类似功能的竞争服务。

`image 注意除了作为普通用户,我与 BrowserStack 没有任何关系——我像其他人一样为我的账户付费,我没有得到任何特殊待遇或折扣。

摘要

在这一章中,我解释了如何获得 jQuery Mobile 并将其添加到 HTML 文档中,并阐述了 jQuery Mobile 自动增强 HTML 文档并从这些文档中分离页面的基本方法。我描述了 jQuery Mobile 提供的定制事件,它使创建触摸应用变得更加容易,我还列出了一些关于如何进行移动开发和测试的基本指南。`*