jQuery-UI-秘籍-全-

jQuery UI 秘籍(全)

原文:zh.annas-archive.org/md5/6053054F727DA7F93DC0A95B33107695

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

创造令人兴奋的用户体验是一项有趣而有价值的工作。实质上,您正在改善许多人的生活。大多数 UI 开发人员的目光都在终点上,看着他们的产品被使用。我们越快到达终点线而不牺牲质量,就越好。我们用来帮助我们达到这个目标的工具可能会产生世界上的所有差异。

jQuery 框架在开发人员中如此受欢迎的部分原因在于“少写,做得更多”的口号,在 jQuery UI 中也有体现。现代版本的 HTML 和 CSS 标准具有组装健壮,响应迅速的用户界面所需的工具。当这个想法破灭时——浏览器的不一致性以及跨项目的开发惯例和模式的缺乏——jQuery UI 介入。jQuery UI 的目标不是重新发明我们编写 Web 应用程序的方式,而是填补空白,并逐步增强现有的浏览器组件。

与任何框架一样,jQuery UI 并不适用于所有人,也不完全适合使用它的人。框架接受了这一事实,并为您可能遇到的大多数情况提供了可扩展性机制。我写这本书的目标是与您分享我在使用 jQuery UI 小部件时的一些经验。我尽可能地进行了扩展,并在必要时进行了修改。我相信您会发现本书中的大多数技巧都很有用,无论您构建什么类型的应用程序。

本书涵盖内容

第一章, 创建手风琴,帮助您学习如何在手风琴小部件之间拖放。此外,您还将学习如何扩展手风琴主题。

第二章, 包含自动完成,解释了自动完成小部件,显示如何使用多个数据源。还涵盖了将选择选项转换为自动完成小部件以及远程数据源过滤的内容。

第三章, 制作按钮,解释了如何修改我们应用程序中的按钮。按钮可以简单,修改文本和图标选项。或者,按钮可以更复杂,比如处理按钮集时。我们将研究间距问题,以及如何应用效果。

第四章, 开发日期选择器,讨论了日期选择器,这是最广泛使用的小部件,但利用率最低的。我们将通过使用一些技巧来更好地将日期选择器集成到您的应用程序中,发掘小部件的一些潜力。

第五章, 添加对话框,讨论了对话框小部件,这些小部件通常依赖于 API 数据。我们将研究加载数据和对话框显示问题。我们还涵盖了更改对话框标题栏以及对小部件应用效果的内容。

第六章, 制作菜单,帮助您学习如何制作可排序的菜单项。我们还将解决主题问题以及突出显示活动菜单项的问题。

第七章,进度条,展示了如何向进度条添加标签。我们还将扩展进度条以创建加载小部件。

第八章,使用滑块,介绍了不显示步进增量的滑块小部件。在这里,您将扩展小部件以提供此功能。我们还将研究更改滑块手柄的视觉显示。

第九章,使用微调器,解释了微调器,通常用于表单中。因此,我们在本章中处理了本地货币和日期的微调器值的格式化。我们还将研究处理小部件的主题问题。

第十章,使用选项卡,介绍了在处理选项卡时使用一些新技术,即使用每个选项卡作为普通 URL 链接。我们还涵盖了一些更高级的选项卡导航用法——动态加载和读取浏览器哈希值。

第十一章,使用工具提示,解释了工具提示,可以应用于页面上的几乎任何内容。在本章中,我们将向您展示如何将效果应用于工具提示,更改工具提示状态,并将工具提示应用于所选文本。

第十二章,小部件和更多!,讨论了小部件,它们不是独立存在的。它们是更大应用程序的一部分。本章涵盖了更大的 jQuery UI 开发画面。这包括从头开始构建小部件、构建自己的开发工具以及使用 Backbone。

本书需要什么

您将需要以下内容:

  • 用于运行示例的现代 Web 浏览器。

  • 一个用于阅读和调整示例的文本编辑器。

  • 所有 JavaScript 依赖项都包含在示例下载中。

  • Python(可选);一些示例需要 Web 服务器,并在示例中使用内置的 Python Web 服务器。示例可以使用任何具有适当调整的 Web 服务器。

本书适用于谁

本书适用于希望改进其现有应用程序、为其新应用程序提取想法或更好地理解整体小部件架构的 jQuery UI 开发人员。读者至少应具有初步的了解什么是 jQuery UI,并编写了一些使用 jQuery UI 的代码。本书中的配方面向中级 jQuery UI 开发人员。根据您的需求,每个配方都足够独立以在自身上有用,但又足够连接以引导您到其他内容。

约定

在本书中,您将找到一些样式的文本,用于区分不同类型的信息。以下是这些样式的一些示例,以及它们的含义解释。

文本中的代码词如下所示:“在这种情况下,我们最好只是将默认的 dateFormat 值更改为我们的应用程序在整个过程中使用的某些内容。”

代码块设置如下:

$(function() {
    $( ".calendar" ).datepicker();
});

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以此类样式显示在文本中:“单击no icons链接将导致按钮图标被移除,并用它们的文本替换。”

注意

警告或重要说明会以此类样式显示在框中。

提示

贴士和技巧显示为此类样式。

第一章:创建手风琴

在本章中,我们将涵盖以下配方:

  • 使用 Tab 键进行部分导航

  • 动态更改高度样式

  • 可调整大小的内容部分

  • 使用主题控制间距

  • 排序手风琴部分

  • 在手风琴之间进行拖放

介绍

在本章中,我们将探讨多种方法,以扩展手风琴小部件,以适应多种情况。手风琴小部件提供了很多开箱即用的功能。例如,没有任何配置,我们就得到了一个主题化的容器小部件,将内容分组到部分中。

我们将专注于揭示手风琴小部件内部工作原理的用例。键盘事件是导航页面的一种方式,我们可以增强手风琴对这些事件的支持。在展开时,每个部分的高度会发生一些神奇的变化。我们将看到我们如何处理这些配置,特别是当部分高度在飞行中改变时。

此外,在高度方面,我们可以让用户控制各个部分的高度,或者从主题的角度来看,我们可以控制手风琴组件之间的空间。最后,我们将看一些更高级的手风琴用法,其中我们让用户自由地对手风琴部分进行排序,并将部分从一个手风琴拖到另一个手风琴中。

使用 Tab 键进行部分导航

在大多数桌面环境中,Tab 键是导航中的秘密武器——许多用户习惯使用的一个工具。同样,我们可以使用 tabindex 属性在 HTML5 应用程序中利用 Tab 键。这告诉浏览器每次按下该键时焦点元素的顺序。

不幸的是,使用手风琴小部件并不像看起来那么简单。我们不能在每个部分标题中指定 tabindex 值,并期望 Tab 键事件按预期工作。相反,默认小部件实现提供了一种不同类型的键导航—— 箭头键。理想情况下,给用户使用他们熟悉的 Tab 键通过手风琴部分导航的能力是有用的,同时保留小部件提供的默认键导航。

准备工作

要开始,我们需要一个基本的手风琴;理想情况下,是一些简单的内容,每个部分都有基本的内容,这样我们就可以在实现自定义事件之前和之后直观地看到 Tab 键的行为如何工作。

作为指南,这是我的基本手风琴标记:

<div id="accordion">
    <h3>Section 1</h3>
    <div>
        <p>Section 1 content</p>
    </div>
    <h3>Section 2</h3>
    <div>
        <p>Section 2 content</p>
    </div>
    <h3>Section 3</h3>
    <div>
        <p>Section 3 content</p>
    </div>
    <h3>Section 4</h3>
    <div>
        <p>Section 4 content</p>
    </div>
</div>

并且,这是用于实例化手风琴小部件的代码:

$(function() {

    $( "#accordion" ).accordion({
        collapsible: true
    });

});

提示

下载示例代码

您可以从您在www.packtpub.com的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册以直接将文件发送到您的电子邮件。

现在我们有一个基本的可折叠手风琴小部件,我们可以在浏览器中查看。我们在这里添加collapsible选项的原因是为了可以实验按键导航——当所有部分都折叠时,我们可以更好地看到哪个部分处于焦点状态。您可以看到updown箭头键允许用户遍历手风琴部分,而Tab键没有任何效果。让我们改变一下。

如何做...

我们将扩展手风琴小部件以包括一个keypress事件的事件处理程序。默认的手风琴实现有处理updownleftrightEnter键的keypress事件。我们不需要改变这一点。相反,我们添加了一个理解当按下Tab键和Shift + Tab键时该做什么的自定义处理程序。

看一下以下代码:

(function( $, undefined ) {

$.widget( "ab.accordion", $.ui.accordion, {

    _create: function () {

        this._super( "_create" );
        this._on( this.headers, { keydown: "_tabkeydown" } );

    },

    _tabkeydown: function ( event ) {

        if ( event.altKey || event.ctrlKey ) {
            return;
         }

        if ( event.keyCode !== $.ui.keyCode.TAB ) {
            return;
        }

        var headers = this.headers,
            headerLength = headers.length,
            headerIndex = headers.index( event.target ),
            toFocus = false;

        if ( event.shiftKey && headerIndex - 1 >= 0 ) {
            toFocus = headers[ headerIndex - 1 ];
        }

        if ( !event.shiftKey && headerIndex + 1 < headerLength ) {
            toFocus = headers[ headerIndex + 1 ];
        }

        if ( toFocus ) {

            $( event.target ).attr( "tabIndex", -1 );
            $( toFocus ).attr( "tabIndex", 0 );
            toFocus.focus();
            event.preventDefault();

        }

    }

});

})( jQuery );

$(function() {

    $( "#accordion" ).accordion({
        collapsible: true
    });

});

它是如何工作的...

我们在这里通过扩展默认的手风琴小部件来创建一个新的手风琴小部件。扩展手风琴小部件的优势在于我们不会去修改小部件的实例;所有手风琴实例都将获得这种新的行为。

_create()方法被我们的新实现所取代。在这个替代方法中,我们首先调用原始的_create()方法。我们不想阻止手风琴小部件的默认设置操作发生。因此,使用_super()我们能够做到这一点。接下来我们绑定了我们的新的tabkeydown()事件处理程序到keydown事件上。

tabkeydown()处理程序是原始手风琴实现中提供的keydown事件处理程序的简化版本。如果AltCtrl键与其他键组合按下,则我们忽略事件。如果按下的键不是Tab,我们也会忽略事件,因为我们只对当手风琴标题处于焦点时改变Tab键行为感兴趣。

处理程序的要点在于确定Tab键按下时应该发生什么。我们应该将手风琴标题焦点移动到哪个方向?何时忽略事件并让默认浏览器行为接管?诀窍在于确定我们当前的索引位置。如果我们在第一个标题上,并且用户按下Shift + Tab,意味着他们想向后遍历,则我们不做任何操作。同样,如果我们在最后一个标题上,并且用户按下Tab,我们将控制权交给浏览器,以便不干扰预期功能。

动态改变高度样式

手风琴是用于组织和显示其他 UI 元素的容器。将每个手风琴部分视为静态内容是一个错误。手风琴部分的内容确实会发生变化。例如,用户触发的事件可能会导致在部分内创建新元素。很可能,部分内的组件会动态改变大小,这是我们需要注意的部分。为什么关注手风琴内容变化大小很重要?因为这是一个手风琴,我们可能会有几个部分(或至少有一些)。让它们都具有统一的高度有意义吗?在某个部分的高度增加到非常大的程度时,它就不再具有统一的高度了。当发生这种情况时,我们需要查看手风琴部分高度的变化,并在必要时动态调整一些高度设置。

准备就绪

让我们使用以下标记创建手风琴小部件:

<div id="accordion">
    <h3>Section 1</h3>
    <div>
        <p>Section 1 content</p>
    </div>
    <h3>Section 2</h3>
    <div>
        <p>Section 2 content</p>
    </div>
    <h3>Section 3</h3>
    <div>
        <p>Section 3 content</p>
    </div>
    <h3>Section 4</h3>
     <div>
        <ul>
            <li>First item</li>
            <li>Second item</li>
            <li>Third item</li>
            <li>Fourth item</li>
        </ul>
     </div>
</div>

我们将使用所有默认选项值创建手风琴如下:

$(function() {
    $("#accordion").accordion();
});

现在,我们会注意到一个关于高度的轻微不一致性。以下是第一部分的样子。它内容很少,但却使用了比所需更多的空间。

准备就绪

这是由于heightStyle选项的默认值造成的,该选项规定手风琴中每个部分的高度将等于最高部分的高度。因此,我们在第一部分浪费了空间。让我们看看以下屏幕截图中的第四部分,以了解为什么会发生这种情况:

准备就绪

我们可以看到,第一部分与第四部分一样高。这是由于heightStyleauto值造成的。在这个特定的例子中,差异并不是那么大。也就是说,第一部分没有浪费太多的空白空间。因此,保持每个部分具有相同高度的手风琴配置可能是有意义的。

当我们处理动态向特定手风琴部分提供内容的应用程序时,挑战就出现了,在某个临界点达到时,保持自动heightStyle配置就不再有意义了。

如何做...

heightStyle设置为auto可以为我们解决问题,因为每个部分只会使用必要的高度来显示内容。但是,如果能够在内容自身的高度发生变化时更改手风琴的此属性,那就更好了。

(function( $, undefined ) {

$.widget( "ab.accordion", $.ui.accordion, {

    refresh: function() {

        this._super( "refresh" );

        if ( this.options.heightStyle !== "content" ) {
            return;
        }

        this.headers.next().each( function() {

            if ( $( this ).css( "height" ) ) {
                $( this ).css( "height", "" );
            }

        });

    }

});

})(jQuery);

$(function() {

    $( "#accordion" ).accordion();

    for ( var i=0; i<20; i++ ){
        $( "ul" ).append( "<li>nth item</li>" );
    }

    $( "#accordion" ).accordion( "option", "heightStyle", "content" )
                     .accordion( "refresh" );

});

它是如何工作的...

我们在这里所做的是扩展手风琴小部件的refresh()方法,以允许在运行时将heightStyle选项更改为内容。默认实现不允许此操作。为了说明这个想法,请考虑上面的代码,我们正在创建手风琴小部件,并向最后一个内容部分添加 20 个新项。我们在这里使用的是默认部分高度,即auto。因此,如果我们没有扩展refresh()方法来允许此行为在填充第四部分后,我们会看到一个滚动条。

可调整大小的内容部分

可调整大小的内容部分允许用户通过拖动部分底部来调整高度。这是一种很好的选择,而不是依赖于heightStyle属性。因此,如果手风琴的每个部分都可以由用户调整,则他们可以自由地定制手风琴布局。例如,如果手风琴有一个高的部分,在底部浪费了空间,用户可能会选择缩小该部分的高度,以更好地查看手风琴以及 UI 的其他组件。

如何操作...

我们将通过使用可调整大小的交互小部件使手风琴内的每个内容的div可调整大小来扩展默认手风琴的_create()方法。

( function( $, undefined ) {

$.widget( "ab.accordion", $.ui.accordion, {

    _create: function () {

        this._super( "_create" );

        this.headers.next()
                    .resizable( { handles: "s" } )
                    .css( "overflow", "hidden" );

    },

    _destroy: function () {

        this._super( "_destroy" );

        this.headers.next()
                    .resizable( "destroy" )
                    .css( "overflow", "" );

    }

});

})( jQuery );

$( function() {

    $( "#accordion" ).accordion();

});

您将看到类似以下的内容。请注意,第二节已被向下拖动,并带有调整大小的鼠标光标。

如何操作...

工作原理...

我们的_create()方法的新版本首先调用默认手风琴的_create()方法。完成后,我们找到手风琴的所有内容部分,并应用resizable()小部件。您还会注意到,我们告诉可调整大小的小部件仅显示一个south手柄。这意味着用户只能使用部分底部的光标将手风琴的任何给定内容部分向上或向下拖动。

这个手风琴的特殊化还提供了一个新的_delete()方法的实现。再次,我们在调用原始手风琴的_delete()之后,清理我们添加的新可调整大小组件。这包括删除overflowCSS 属性。

还有更多...

我们可以通过提供关闭它的手段来扩展手风琴中的可调整大小行为。我们将在手风琴中添加一个简单的resizable选项,用于检查是否使手风琴部分可调整大小。

(function( $, undefined ) {

$.widget( "ab.accordion", $.ui.accordion, {

    options: {
        resizable: true
    },

    _create: function () {

        this._super( "_create" );

        if ( !this.options.resizable ) {
            return;
        }

        this.headers.next()
                    .resizable( { handles: "s" } )
                    .css( "overflow", "hidden" );
    },

    _destroy: function () {

        this._super( "_destroy" );

        if ( !this.options.resizable ) {
            return;
        }

        this.headers.next()
                    .resizable( "destroy" )
                    .css( "overflow", "" );

    },

});

})( jQuery );

$(function() {

    $( "#accordion" ).accordion( { resizable: false } );

});

使用主题控制间距

手风琴部分之间的间距由 CSS 主题框架控制。特别是,手风琴的视觉结构由一组 CSS 规则定义,可以修改以控制手风琴部分之间的间距。我们可以覆盖手风琴主题 CSS 以调整部分之间的间距。

如何操作...

我们将为我们的 UI 提供一个额外的 CSS 模块,它将覆盖我们目前正在使用的主题中提供的手风琴结构。然而,无需担心,我们的更改很简单。我们将更新margin-top属性。在一个名为theme.accordion.css的新 CSS 文件中,让我们添加以下样式规则:

.ui-accordion .ui-accordion-header {
    margin-top: 4px;
}

现在我们有了 CSS,我们需要将其包含在我们的 HTML 头部。它应该类似于这样:

操作步骤...

工作原理...

我们复制了与任何 jQuery UI 主题中找到的相同的 CSS 选择器。我们刚刚更改的特定属性改变了手风琴部分之间的间距。由于我们覆盖了默认主题值,所以将我们的 CSS 文件包含在默认主题文件之后非常重要。这样我们就可以覆盖默认主题,而不是默认主题覆盖我们的修改。

对手风琴部分进行排序

使用可排序交互式小部件,我们能够将静态手风琴部分布局转换为用户指定的内容。也就是说,可排序交互式小部件接受一个容器元素,并允许对所有子元素进行就地排序。用户通过将元素拖动到所需顺序来执行此操作。

我们将看看如何扩展手风琴功能,以便在创建时可以通过配置选项打开可排序部分功能。

操作步骤...

当手风琴小部件创建时,以及销毁手风琴时,我们必须执行几个操作。以下是我们如何扩展小部件的方式:

( function( $, undefined ) {

$.widget( "ab.accordion", $.ui.accordion, {

    options: {
        sortable: false
    },

    _create: function () {

        this._super( "_create" );

        if ( !this.options.sortable ) {
            return;
        }

        this.headers.each( function() {
            $( this ).next()
                     .addBack()
                     .wrapAll( "<div/>" );
        });

        this.element.sortable({
            axis: "y",
            handle: "h3",
            stop: function( event, ui ) {
                ui.item.children( "h3" )
                       .triggerHandler( "focusout" );
            }
        });        

    },

    _destroy: function () {

        if ( !this.options.sortable ) {
            this._super( "_destroy" );
            return;
        }

        this.element.sortable( "destroy" );

        this.headers.each( function () {
            $( this ).unwrap( "<div/>" );
        });

        this._super( "_destroy" );

    }

});

})( jQuery );

$( function() {

    $( "#accordion" ).accordion( { sortable: true } );

});

有了我们新的标记为sortable的手风琴小部件,用户现在可以在手风琴内拖动头部部分。例如,如果第一个手风琴部分属于底部,用户只需将其拖到底部。

操作步骤...

工作原理...

借助sortable()交互式小部件的帮助,我们能够扩展默认手风琴小部件实现,以包括排序功能。与任何 jQuery UI 小部件增强一样,我们实际上不需要扩展所讨论的小部件;新功能始终可以在小部件实例化后附加。然而,正如您将在本书中看到的,最佳实践是封装自定义内容并将其作为一组选项呈现给小部件客户端。

我们在此扩展了可用的手风琴选项集,包括一个sortable选项。这是我们打开或关闭自定义的方式(它是一个布尔值)。我们实现的自定义_create()版本将调用手风琴的_create()方法的默认版本。之后,我们将查看可排序行为是否被关闭(在这种情况下我们无需做任何事情,所以返回)。同样,我们的自定义_delete()函数在调用原始删除功能后检查可排序行为是否已打开。

实现可排序手风琴部分的棘手部分在于我们必须在手风琴元素内进行轻微的 DOM 操作。这是为了使用可排序交互小部件所必需的。手风琴小部件的标记结构化,使得所有部分都相邻。也就是说,我们有一个 h3 元素,后面跟着一个 div 元素。这是一个部分,并且后面跟着另一个 h3 和另一个 div,依此类推。这是一个平面结构。有两种处理方式:修改创建小部件所需的标记,或者注入一些轻微的 DOM 修改,并且小部件客户端对此一无所知。我们选择后一种方式,不要求客户端更改其代码。这是另一个最佳实践,即在提供定制时保持现有小部件客户端代码的功能性。

在我们定制的 _create() 版本中,我们正在迭代每个手风琴标题,并将标题元素和相应的内容元素包装在一个 div 元素中,以便将它们捆绑在一起。这样,可排序小部件就知道如何移动这个捆绑包了。如果我们没有这样做,用户只能移动标题部分,从而将其与内容分开。最后,我们正在创建可排序小部件,将移动限制为y轴,并将可移动手柄设置为手风琴标题。

我们定制的 _destroy() 函数在调用原始的 _destroy() 方法之前撤消我们的修改。这意味着取消包装我们的新 div 元素并销毁可排序小部件。

在手风琴之间拖放

一些应用程序需要比其他更流畅的布局,不仅从屏幕分辨率的角度来看,而且从功能的角度来看也是如此。手风琴小部件是一个静态分组组件,用于将较小的组件组织成部分。我们只需展开感兴趣的部分,就可以隐藏所有不相关的材料。正如我们在排序手风琴部分的示例中看到的那样,我们可以提供一个手风琴,用户可以通过拖放来操作其结构。实际上,这已经成为用户大规模预期的事情——通过拖放进行 UI 配置。

可排序手风琴专注于单个手风琴。当然,在应用程序的范围内给予用户自由的精神下,我们为什么不试着看看我们是否能支持将手风琴部分移动到一个新的手风琴中呢?

准备就绪

对于这个实验,我们需要两个基本的手风琴。标记应该假设如下所示的形式:

<div id="target-accordion" style="width: 30%">
    <h3>Section 1</h3>
    <div>
        <p>Section 1 content</p>
    </div>
    <h3>Section 2</h3>
    <div>
        <p>Section 2 content</p>
    </div>
    <h3>Section 3</h3>
    <div>
        <p>Section 3 content</p>
    </div>
</div>
<p></p>
<div id="accept-accordion" style="width: 30%">
    <h3>Section 4</h3>
    <div>
        <p>Section 4 content</p>
    </div>
    <h3>Section 5</h3>
    <div>
        <p>Section 5 content</p>
    </div>
    <h3>Section 6</h3>
    <div>
        <p>Section 6 content</p>
    </div>
</div>

如何做...

有了这个,让我们将这个标记转换为两个手风琴。我们首先将手风琴小部件扩展为带有一些花哨的拖放行为。意图是允许用户将第一个小部件的手风琴部分拖到第二个小部件中。下面是具体操作:

(function( $, undefined ) {

$.widget( "ui.accordion", $.ui.accordion, {

    options: {
         target: false,
         accept: false,
         header: "> h3, > div > h3"
    },

    _teardownEvents: function( event ) {

        var self = this,
            events = {};

        if ( !event ) {
            return;
        }

        $.each( event.split(" "), function( index, eventName ) {
            self._off( self.headers, eventName );
        });

    },

    _createTarget: function() {

        var self = this,
            draggableOptions = {
                handle: "h3",
                helper: "clone",
                connectToSortable: this.options.target,
            };

        this.headers.each( function() {
            $( this ).next()
                     .addBack()
                     .wrapAll( "<div/>" )
                     .parent()
                     .draggable( draggableOptions );
        });
    },

    _createAccept: function() {

        var self = this,
            options = self.options,
            target = $( options.accept ).data( "uiAccordion" );

        var sortableOptions = {

            stop: function ( event, ui ) {

                var dropped       = $(ui.item),
                    droppedHeader = dropped.find("> h3"),
                    droppedClass  = "ui-draggable",
                    droppedId;

                if ( !dropped.hasClass( droppedClass ) ) {
                    return;
                }

                // Get the original section ID, reset the cloned ID.
                droppedId = droppedHeader.attr( "id" );
                droppedHeader.attr( "id", "" );

                // Include dropped item in headers
                self.headers = self.element.find( options.header )

                // Remove old event handlers
                self._off( self.headers, "keydown" );
                self._off( self.headers.next(), "keydown" );
                self._teardownEvents( options.event );

                // Setup new event handlers, including dropped item.
                self._hoverable( droppedHeader );
                self._focusable( droppedHeader );
                self._on( self.headers, { keydown: "_keydown" } );
                self._on( self.headers.next(), { keydown: "_panelKeyDown" } );
                self._setupEvents( options.event );
                // Perform cleanup
                $( "#" + droppedId ).parent().fadeOut( "slow", function() {
                    $( this ).remove();
                    target.refresh();
                });

                dropped.removeClass( droppedClass );

            }

        };

        this.headers.each( function() {
            $(this).next()
                   .addBack()
                   .wrapAll( "<div/>" );
        });

        this.element.sortable( sortableOptions );

    },

    _create: function() {

        this._super( "_create" );

        if ( this.options.target ) {
            this._createTarget();
        }

        if ( this.options.accept ) {
            this._createAccept();
        }

    },

    _destroy: function() {

        this._super( "_destroy" );

        if ( this.options.target || this.options.accept ) {

            this.headers.each( function() {
                $( this ).next()
                         .addBack()
                         .unwrap( "<div/>" );
            });
        }
    }

});

})( jQuery );

$(function() {

    $( "#target-accordion" ).accordion({
        target: "#accept-accordion"
    });

    $( "#accept-accordion" ).accordion({
        accept: "#target-accordion" 
    });

});

现在我们有了两个看起来基本的手风琴小部件。然而,如果用户愿意,他们可以将第一个手风琴的部分拖到第二个手风琴中。

如何做...

它是如何运作的......

乍一看,这可能看起来是很多的代码,但是只需要很少的工作(约 130 行左右),我们就能够将手风琴部分从一个手风琴拖放到另一个手风琴。让我们进一步解析一下。

我们通过这个小部件扩展添加了两个手风琴选项:targetaccept。目标允许我们指定手风琴的部分目的地。在这个例子中,我们将第二个手风琴作为第一个手风琴的目标,这意味着我们可以从target-accordion拖放到accept-accordion。但是,为了实现这一点,必须告诉第二个手风琴从哪里接受部分;在这种情况下,它是target-accordion。我们基本上使用这两个选项在两个小部件之间建立拖放合同。

这个例子使用了两个交互式小部件:draggable 和 sortable。target-accordion使用了 draggable。如果指定了target选项,将调用_createTarget()方法。_createTarget()方法将浏览手风琴部分,将它们包装在div元素中,并创建一个draggable()小部件。这就是我们能够从第一个手风琴拖动部分的方法。

如果指定了accept选项,将调用_createAccept()方法。这遵循将每个手风琴标题与其内容包装在div元素中的相同模式。但在这里,我们使整个手风琴小部件sortable()

这可能看起来反直觉。为什么我们要使希望接受新部分的第二个手风琴可排序?使用 droppable 不是更合理吗?我们可以选择这条路线,但这将涉及大量使用connectToSortable选项的工作。这是在_createTarget()中指定的draggable选项,我们在其中说我们想把这些可拖动的项放到一个可排序的小部件中。在这个例子中,可排序的是第二个手风琴。

这解决了关于相对于其他部分在哪里放置手风琴部分的问题(可排序小部件知道如何处理)。然而,在这种方法中的一个有趣的约束是,我们必须克隆拖动的项目。也就是说,最终被放置到新手风琴中的部分只是一个克隆,而不是原件。因此,我们必须在放置时处理这个问题。

_createAccept()中定义的排序选项的一部分,我们提供了一个stop回调。当我们将新的手风琴部分放入手风琴时,将触发这个回调函数。实际上,这对于任何排序活动都会触发,包括新的部分被放置。因此,我们必须小心检查我们实际上正在处理什么。我们通过检查项目是否附有draggable类来做到这一点,如果是,我们可以假设我们正在处理一个新的手风琴部分。

请记住,这个新添加的折叠菜单部分只是原始部分的克隆,因此在我们开始将其插入折叠菜单之前,需要发生一些有趣的事情。首先,这个新部分具有与原始部分相同的 ID。最终,我们将从第一个折叠菜单中删除原始部分,因此我们存储了该 ID 以供以后使用。一旦我们获得了它,我们就可以摆脱被删除部分的 ID,以避免重复。

确保完成这一步之后,我们已经在适当的位置放置了新的 DOM 元素,但是折叠菜单部件对此一无所知。这就是我们重新加载标题的地方,包括新添加的标题。新的折叠菜单部分仍然不可用,因为它没有正确处理事件,所以,例如,展开新部分将不起作用。为了避免奇怪的行为,我们关闭所有事件处理程序并重新绑定它们。这样就将新的折叠菜单放在了新的上下文中,而事件则保持开启状态。

现在,我们在 accept-accordion 中有了一个新的部分。但是我们不能忘记原来的部分。它仍然需要被移除。回想一下,我们存储了原始部分的 DOM ID,现在我们可以安全地移除该部分并刷新折叠菜单以调整高度。

第二章:包括自动完成

在本章中,我们将涵盖:

  • 用主题样式化默认输入

  • 使用选择选项构建数据源

  • 使用多个数据源

  • 远程自动完成过滤

  • 自定义数据和分类

  • 将效果应用于下拉菜单

介绍

自动完成小部件的主要目的是增强标准 HTML 表单input元素的功能。用户不必每次输入字段的完整值,自动完成小部件会提供可能的值作为建议。例如,假设您正在添加一个新产品。产品字段可以是文本输入、选择输入等等。在这种情况下,一个人会使用系统中现有的产品作为自动完成小部件的来源。很有可能,输入产品的用户,或者其他用户,之前已经输入过该产品。通过自动完成,用户可以确保他们提供的是有效的输入。

用主题样式化默认输入

默认的自动完成实现不会改变输入元素的任何视觉效果。从功能上讲,我们不希望更改输入元素。我们只需要在用户开始输入时出现下拉组件。但让我们看看是否可以使用小部件框架和主题框架中的组件改变自动完成输入元素的虚拟外观。

准备工作

我们将使用以下标记作为我们的示例,一个简单的label元素和一个简单的input元素:

<div>
    <label for="autocomplete">Items: </label>
    <input id="autocomplete"/>
</div>

如何做...

我们将使用以下代码使用主题框架中的 CSS 类来扩展自动完成小部件。我们正在引入一个关于焦点事件的微小行为调整。

( function( $, undefined ) {

$.widget( "ab.autocomplete", $.ui.autocomplete, {

    inputClasses: "ui-widget ui-widget-content ui-corner-all",

    _create: function() {

        this._super( "_create" );
        this._focusable( this.element );
        this.element.addClass( this.inputClasses );

    },

    _destroy: function() {

        this._super( "_destroy" );
        this.element.removeClass( this.inputClasses );

    }

});

})( jQuery );

$( function() {

    var source = [
        'First Item',
        'Second Item',
        'Third Item',
        'Fourth Item'
    ];

    $( "#autocomplete" ).autocomplete( { source: source } );

});

完成我们自动完成input元素的样式要求的最后一件事是使用一些规则的新 CSS 样式表。样式表应该在定义输入标记的主 HTML 中包含。

input.ui-autocomplete-input {
    padding: 2px;
}

input.ui-autocomplete-input:focus {
    outline: none;
}

这是我们新样式的自动完成小部件在没有焦点时的样子。

如何做...

这是自动完成在有焦点时的样子,并且下拉菜单已展开。

如何做...

它是如何工作的...

文档加载时,我们正在创建一个简单的自动完成使用#autocomplete 输入元素。

你会注意到的第一件事是inputClasses属性。这个字符串代表了我们想要应用到小部件的主题框架的三个类:ui-widgetui-widget-contentui-corner-allui-widget类除了处理字体外并没有太多作用,将这个类应用到主题化的元素是一个好的做法。 ui-widget-content 类为我们修复了输入的边框,而 ui-corner-all 类为我们应用了漂亮的圆角。我们将这个字符串定义为小部件的属性的原因是因为这些类在两个地方使用,这样易于维护。

我们在这里覆盖的_create()方法只是调用了自动完成的_create()方法的原始实现。一旦这完成,我们通过调用_focusable()使input元素可聚焦。这是小部件工厂定义的一个方便的实用方法,并且被所有小部件继承。它通过在元素聚焦时从主题框架中应用ui-state-focusCSS 类来处理使元素可聚焦。当元素失去焦点时,它也会移除类。也许,_focusable()最好的部分是小部件工厂机制将在小部件销毁时清理任何焦点事件处理程序。我们自定义的_create()实现的最后一个任务是将inputClasses的 CSS 类添加到输入元素中。

一如既往,当我们从自动完成小部件中借用完成后,我们需要确保清理干净。这意味着扩展_delete()以确保从输入元素中删除inputClasses属性。

我们使用的微小 CSS 规则有两个作用。第一个改变是给input元素添加一点填充——这纯粹是出于美观考虑,因为我们做的其他改变使得文本在输入框中显得有点紧凑。第二个改变是在焦点集中时删除围绕input元素的轮廓。这仅适用于某些浏览器,如 Chrome,在其中会自动应用轮廓。

注意

通常,不建议移除轮廓,因为这会影响可访问性。但是,我们的改动已经考虑到了焦点输入,所以这样做是可以的。

使用选择选项构建数据源

有时,将数组用作自动完成小部件的数据源并不是最佳选择。例如,如果我们的用户界面中已经有一个select元素,那么重用该元素中的选项来创建自动完成会是个明智的选择。否则,我们不仅需要设计一些新代码来构建数组数据源,还需要删除现有的select元素。

准备工作

让我们为这个例子编写一些基本的标记。通常,自动完成小部件期望一个input作为其元素。相反,我们将给它一个带有一些简单选项的select元素。

<div>
    <label for="autocomplete">Items: </label>
    <select id="autocomplete">
        <option>First Item</option>
        <option>Second Item</option>
        <option>Third Item</option>
        <option>Fourth Item</option>
    </select>
</div>

操作步骤...

我们将扩展自动完成小部件的功能,使其知道如何处理select元素。之后,我们就能够使用自动完成小部件来定位我们的select元素了。

( function( $, undefined ) {

$.widget( "ab.autocomplete", $.ui.autocomplete, {

    inputClasses: "ui-widget ui-widget-content ui-corner-all",

    _create: function() {

        if ( this.element.is( "select" ) ) {

            var self = this;
            this.original = this.element.hide();
            this.element = $( "<input/>" ).insertAfter( this.original );

            this.options.source = function( request, response ) {

                var filter = $.ui.autocomplete.filter,
                    options = self.original.find( "option" ),
                    result = options.map( function() {
                        return $( this ).val();
                    });

                response( filter( result, request.term ) );

            };

        }

        this._super( "_create" );

    },

    _destroy: function() {

        this._super( "_destroy" );
        this.element.remove();
        this.original.show();

    }

});

})( jQuery );

$( function() {
    $( "#autocomplete" ).autocomplete();
});

现在你应该看到的是一个看起来像是普通的自动完成——看不到select元素。此外,如果你尝试使用自动完成,你会发现呈现的选项与select元素的选项相同。

操作步骤...

工作原理...

在这里,我们需要为 select 元素添加对自动完成小部件的支持;我们在自定义的 _create() 实现的开始时执行此操作。如果我们处理的是 select 元素,则我们要做的第一件事是隐藏它并将其引用存储在 original 属性中。记住,我们只对 select 元素通过其 options 提供的数据源感兴趣 - 我们不希望实际显示 select。相反,我们将 select 替换为一个 input 元素,因为这是用户键入的方式,而小部件则完成。

自动完成小部件的 source 选项是我们能够指定返回要使用的源数据的自定义函数的方式。在我们的例子中,我们提供了一个函数,该函数从每个选择 option 获取值。回想一下,select 元素先前存储在 original 属性中。我们在这里使用 jQuery map() 实用程序函数将 select 选项转换为自动完成可以使用的数组。filter() 函数被应用,并且 response() 函数被发送到下拉菜单。

当小部件被销毁时,我们希望恢复原始的 select 元素,因为这是我们替换的元素。在我们自定义的 _delete() 实现中,原始元素再次显示 - 这是在调用原始的 _delete() 方法执行常规清理任务后发生的。我们创建的 input 元素也在这里销毁。

使用多个数据源

有时,自动完成小部件不直接映射到一个数据源。以视频为例。想象一下用户需要选择一个视频,但是两个数据源是 DVD 和蓝光。如果我们要使用自动完成选择视频,我们需要一种方法来分配多个数据源。此外,该机制需要足够灵活,以支持添加更多数据源,特别是因为每隔一年就会诞生一种新的视频格式。

怎么做...

自动完成小部件的默认实现期望一个单一的数据源 - 一个数组或一个 API 端点字符串。我们将给小部件添加一个新的 sources 选项来允许这种行为。这就是我们将扩展自动完成并创建一个具有两个视频数据源的小部件实例 - 一个用于 DVD,一个用于蓝光光盘。

( function( $, undefined ) {

$.widget( "ab.autocomplete", $.ui.autocomplete, {

    options: { 
        sources: []    
    },

    _create: function() {

        var sources = this.options.sources;

        if ( sources.length ) {

            this.options.source = function ( request, response ) {

                var merged = [],
                    filter = $.ui.autocomplete.filter;

                $.each( sources, function ( index, value ) {
                    $.merge( merged, value );
                });

                response( filter( merged, request.term ) );

            };

        }

        this._super( "_create" );

    },

    _destroy: function() {
        this._super( "_destroy" );
    }

});

})( jQuery );

$( function() {
    var s1 = [
            "DVD 1",
            "DVD 2",
            "DVD 3"
        ],
        s2 = [
            "Blu-ray 1",
            "Blu-ray 2",
            "Blu-ray 3"
        ];

    $( "#autocomplete" ).autocomplete({
        sources: [s1, s2]
    });
});

怎么做...

如您所见,如果您开始搜索视频 1,您将在下拉菜单中从每个数据源获得版本。

工作原理...

我们不是在将我们的两个数据源合并到传递给自动完成之前,而是扩展了小部件的功能来处理这项任务。特别是,我们添加了一个新的 sources 选项,该选项可以接受多个数组。在示例中,我们将 DVD 和蓝光源都传递给我们的小部件。

我们的定制版本的_create()通过检查sources选项的长度来看是否已经提供了多个数据源。如果有多个数据源,我们使用merge()jQuery 实用函数创建一个新数组,并对其应用filter()函数。这种方法的一个很好的特点是它不在乎有多少个数据源——我们以后可以传递更多数据源到我们的实现中。这些数据源的合并被封装在小部件后面。

远程自动完成过滤

自动完成过滤功能并不仅限于默认实现,它搜索数组数据源中的对象。我们可以指定一个自定义source()函数,该函数将仅检索用户正在寻找的数据。如果您希望在包含数千个项目的数据源上使用自动完成,这是理想的方法。否则,在浏览器上过滤要求会变得过于苛刻——下载大型数据集,然后对每次按键进行大型数组搜索。

如何做...

我们将使用 GitHub API 作为自动完成小部件的数据源。这是一个很好的例子,因为它太大了,无法在浏览器内存中使用。

$( function() {
  $( "#autocomplete" ).autocomplete({
        minLength: 3,
        source: function( request, response ) {
            $.ajax({
                url: "https://api.github.com/legacy/repos/search/:" + request.term,
                dataType: "jsonp",
                success: function( resp ) {
                    var repositories = resp.data.repositories.splice( 0, 10 );
                    var items = $.map( repositories, function ( item ) {
                        return { 
                            label: item.name + " (" + 
                                      item.language + ")",
                            value: item.name
                        };
                    });
                    response( items );
                }
            });
        }
    });
});

现在,如果您在浏览器中查看结果小部件并开始输入,您将在下拉菜单中看到 Github 仓库数据。

如何做...

它是如何工作的...

由于我们使用了一个大型数据源,我们告诉这个特定的自动完成小部件,只有在至少有三个字符时才应执行项目的搜索。这是使用minLength选项来实现的。否则,我们将要求服务器基于一个或两个字符进行查询,这不是我们想要的。

在我们的示例中,source选项指定了我们将要使用的数据源——Github API。我们传递给source的函数执行了一个对 Github API 的$.ajax()调用。我们使用jsonp作为格式,这意味着 API 的回调函数将被发送回来。我们还向 API 传递了一些查询数据。

一旦 API 响应了数据,我们的成功回调函数就会执行。然后,我们通过$.map()实用程序函数将这些数据传递,以便生成自动完成小部件可以理解的数组。我们的成功函数对数据进行简单的$.map(),将其转换为自动完成可以使用的对象数组。

还有更多内容...

我们可以通过在小部件中引入术语缓存来进一步减少网络通信开销。术语缓存,顾名思义,会在本地存储执行远程过滤操作的结果。这样,当用户不可避免地在他们的按键中执行完全相同的操作时,我们不会再次执行相同的任务,并发出远程 API 调用,因为我们已经在小部件中缓存了结果。

( function( $, undefined ) {

$.widget( "ab.autocomplete", $.ui.autocomplete, {

    _cache: {},

    _search: function( value ) {

        var response = this._response(),
            cache = this._cache;

    this.pending++;
    this.element.addClass( "ui-autocomplete-loading" );
    this.cancelSearch = false;

        if ( value in cache ) {
            response( cache[value] );
        }
        else {
            this.source( { term: value }, response );
        }

    }

});

})( jQuery );

$( function() {
  $( "#autocomplete" ).autocomplete({
        minLength: 3,
        source: function( request, response ) {
            var self = this;
            $.ajax({
                url: "https://api.github.com/legacy/repos/search/:" + request.term,
                dataType: "jsonp",
                success: function( resp ) {
                    var repositories = resp.data.repositories.splice( 0, 10 );
                    var items = $.map( repositories, function ( item ) {
                        return { 
                            label: item.name + " (" + 
                                      item.language + ")",
                            value: item.name
                        };
                    });
                    self._cache[request.term] = items;
                    response( items );
                }
            });
        }
    });
});

您可以在前面的代码中看到我们所做的更改以支持缓存从 HTTP 请求返回的项目。现在我们正在扩展小部件以添加新的 _cache 属性。我们还扩展了 _search() 函数,该函数负责检查缓存值。如果找到一个,就使用缓存版本的数据调用渲染响应。source() 函数负责存储缓存结果,但这只是一个简单的一行代码。

自定义数据和类别

分离两个自动完成数据类别的一种方法可能是拥有两个不同的字段,每个字段都有自己的自动完成小部件。另一种方法是在小部件本身引入类别的概念。当下拉菜单出现为用户建议项目时,他们还将看到项目所属的类别。要在自动完成小部件中执行此操作,我们需要更改小部件如何理解源数据以及如何呈现菜单项。

如何做...

我们将扩展自动完成小部件,以改变菜单项的渲染方式。我们还需要考虑传递给小部件的数据作为源。

( function( $, undefined ) {

$.widget( "ab.autocomplete", $.ui.autocomplete, {

    _renderMenu: function( ul, items ) {

        var that = this,
            currentCategory = "";

        items.sort(function( a, b ) {
            return a.cat > b.cat 
        });

        $.each( items, function( index, item ) {

            if ( item.cat != currentCategory ) {
                that._renderCategory( ul, item );
                currentCategory = item.cat;
            }

            that._renderItemData( ul, item );

        });

    },

    _renderCategory: function( ul, item ) {
        return $( "<li>" ).addClass( "ui-autocomplete-category" )
                          .html( item.cat )                          
                          .appendTo( ul );
    },

    _renderItem: function( ul, item ) {
        return $( "<li>" ).addClass( "ui-autocomplete-item" )
                          .append( $( "<a>" )
                          .append( $( "<span>" ).html( item.label ) )
                          .append( $( "<span>" ).html( item.desc ) ) )
                          .appendTo( ul );
    }

});

})( jQuery );

$( function() {

    var items = [
        {
            value: "First Item",
            label: "First Item",
            desc: "A description of the first item goes here",
            cat: "Completed"
        },
        {
            value: "Second Item",
            label: "Second Item",
            desc: "A description of the second item goes here",
            cat: "In Progress"
        },
        {
            value: "Third Item",
            label: "Third Item",
            desc: "A description of the third item goes here",
            cat: "Completed"
        }
    ];

    $( "#autocomplete" ).autocomplete( {source: items} );

});

我们差不多完成了。我们对菜单所做的更改不会神奇地起作用,我们需要应用一些样式。以下 CSS 代码应包含在页面中:

.ui-autocomplete-category {
    font-weight: bold;
    padding: .2em .4em;
    margin: .8em 0 .2em;
    line-height: 1.5;
}

.ui-autocomplete-item > a > span {
    display: block;
}

.ui-autocomplete-item > a > span + span {
    font-size: .9em;
}

现在,如果您开始在自动完成中输入,您会注意到下拉菜单与我们所习惯的大不相同,因为它包含类别和描述信息。

如何做...

如何工作...

此小部件扩展的目标是接受自定义源数据并在下拉菜单的显示中使用该数据。具体而言,我们正在处理的新数据是类别和描述。类别是一对多关系,因此我们传递给小部件的几个项目可能具有相同的类别字符串。我们的工作是弄清楚任何给定类别下的项目,并在下拉菜单中表示此结构。此外,项目的描述是一对一关系,因此此处需要的工作较少,但我们仍然希望在下拉菜单中包含描述。

我们覆盖的原始实现的第一种方法是 _renderMenu()_renderMenu() 的工作是每次向用户提出建议时更改底层 HTML 结构。我们使用 currentCategory 跟踪当前类别。然后我们使用 _renderItem() 渲染每个项目。

_renderCategory() 函数将类别文本呈现为 <li>。它还添加了 ui-autocomplete-category 类。同样,我们的 _renderItem() 函数呈现项目文本,并在这里我们还使用 desc 属性。项目还具有 ui-autocomplete-item 类。

我们在用户界面中包含的新 CSS 样式是我们创建的新版本自动完成功能的必要组成部分。没有它们,描述将与项目标签具有相同的字体大小并显示在同一行上。同样,类别需要新添加的样式以突出显示为其他项目分组的类别,而不仅仅是另一个项目。

还有更多...

每当我们扩展自动完成小部件使用的数据时,我们都必须告诉小部件如何使用它。在这里,我们告诉自动完成如何在下拉菜单中显示新数据。或者,我们可以告诉小部件在用户实际上从未在下拉菜单中看到的一些数据字段上执行过滤。或者我们可以将两者结合起来。

这是我们在用户开始输入时如何同时使用类别和描述两个非标准字段进行过滤的方法。

$.ui.autocomplete.filter = function( array, term ) {

    var matcher = new RegExp( $.ui.autocomplete.escapeRegex( term ), "i" );

    return $.grep( array, function( value ) {
        return matcher.test( value.cat ) || 
               matcher.test( value.desc ) ||
               matcher.test( value.label )
    });

};

在这里,我们正在用我们自己的实现替换自动完成使用的filter()函数。这两者很相似,我们只是将RegExp.test()调用适应于desccat字段。我们将这段代码放在自动完成的自定义小部件声明的下方。之所以在自定义规范之外执行这些操作,是因为autocomplete.filter()有点像一个静态方法。在其他方法中,我们是根据每个实例进行覆盖。

对下拉菜单应用效果

默认情况下,我们得到一个相当简单的下拉菜单的呈现,其中包含基于我们输入的内容的建议。菜单只是简单地显示,没有太多的麻烦。这样做是可以的,它可以可靠地完成工作。但另一方面,我们总是可以做一些事情来使界面看起来更加精致。它可能只是将您应用程序中的自动完成小部件更改为在转换为可见状态时使用一些微妙的效果。

准备工作

由于我们这里追求的实际上更多是小部件的视觉呈现方面,我们可能可以安全地使用小部件的任何现有实例。

如何操作...

让我们在自动完成小部件的默认实现基础上增加一些微妙的动画效果。

( function( $, undefined ) {

$.widget( "ab.autocomplete", $.ui.autocomplete, {

    _suggest: function( items ) {

        this._resetMenu();
        this._renderMenu( this.menu.element, items );
        this.menu.refresh();

        this._resizeMenu();
        this._positionMenu();

    },

    _resetMenu: function() {

        this.menu.element
                 .empty()
                 .zIndex( this.element.zIndex() + 1 );

    },

    _positionMenu: function() {

        var pos = $.extend( { of: this.element }, this.options.position );
        this.menu.element.position( pos );

    },

    _resizeMenu: function() {

        var menu = this.menu,
            exclude = 0;
            target = Math.max(
                menu.element.width( "" ).outerWidth() + 1,
                this.element.outerWidth()
            ),
            excludeCSS = [
                'borderLeftWidth',
                'borderRightWidth',
                'paddingLeft',
                'paddingRight'
            ];

        if( menu.element.is( ":hidden" ) ) {
            menu.element.css( { display: "block", opacity: 0 } );
        }

        $.each( excludeCSS , function( index, item ) {
            exclude += parseFloat( menu.element.css( item ) );
        });

        if ( menu.element.css( "opacity" ) == 0 ) {
            menu.element.animate({
                width: target - exclude,
                opacity: 1
            }, 300);
        }
        else{
            menu.element.width( target - exclude );
        }

    },

    _close: function( event ) {

        var menu = this.menu;

        if ( menu.element.is( ":visible" ) ) {

            menu.element.fadeOut();
            menu.blur();
            this.isNewMenu = true;
            this._trigger( "close", event );

        }

    }

});

})( jQuery );

$(function() {
    var source = [
        "First Item",
        "Second Item",
        "Third Item",
        "Fourth Item"
    ];
    $( "#autocomplete" ).autocomplete({
        source: source,
    });
});

如果您开始在输入元素中使用此自动完成小部件,您会注意到下拉菜单会平滑地滑入视图,而不是突然弹出。此外,当不再需要菜单时,它会渐渐消失。

工作原理...

在这里扩展自动完成功能,以便我们可以注入我们自定义的动画功能。但是这一次,变化要复杂一些,我们不仅仅是用几行代码扩展_create()。在自动完成代码中有一些深藏的方法需要我们扩展。我们还在自动完成小部件中引入了一些我们自己的新方法。

我们要覆盖的第一个方法是_suggest()。当用户键入了最小长度的字符以执行搜索时,自动完成小部件会调用_suggest()方法。原始方法负责渲染和显示下拉菜单的各个操作。在我们的方法版本中,我们只是调用小部件的其他方法。_suggest()方法的工作是协调搜索发生时发生的所有操作。这里有两个逻辑步骤。首先,使用新内容渲染菜单。接下来,显示、调整大小和定位菜单。后者是动画发生的地方。

我们不会深入讨论_resetMenu()_positionMenu()方法的细节,因为这些代码片段大部分是从原始实现中取出的。它们只是分别清空并定位菜单。

_resizeMenu()方法是菜单显示时实际动画发生的地方。这是一个较长的方法,因为我们必须执行一些计算以传递给animate()_resizeMenu()的原始实现使用outerWidth() jQuery 函数来设置菜单的宽度。这是为了与input元素正确对齐。然而,我们想要动画改变width。因此,我们必须手动计算内部宽度。外部宽度值放在排除变量中。内部宽度为目标 - 排除

在实际显示菜单之前,我们会检查菜单是否已经显示,并在动画显示之前进行检查。如果元素不可见,我们会更改display CSS 属性,但将opacity属性设置为0。我们这样做的原因是我们需要元素的框模型尺寸以便定位它。但是,我们仍未将动画效果应用于菜单。在这里,我们检查菜单的opacity属性是否为0。如果不是,则表示菜单已经显示,现在重新对其进行动画化是没有意义的。否则,我们执行宽度和不透明度动画。

最后,_close()方法替换了原始的自动完成_close()实现。代码几乎与原始代码相同,只是在关闭菜单时我们做了一个基本的fadeOut(),而不是简单地隐藏它。

注意

这个自动完成功能的扩展并没有实现关闭此行为的选项。这没关系,因为这个扩展只做一件事情——对下拉菜单应用效果。因此,要禁用这些效果,我们只需禁用扩展。小部件的扩展是在调用自身的函数内定义的。当脚本首次加载时,会调用该函数,并使用新的行为对小部件进行扩展。我们可以禁用调用自身的函数的行为部分。

(function( $, undefined ) {
    // Code that extends a jQuery UI widget...
}); //( jQuery );

第三章:制作按钮

在本章中,我们将涵盖:

  • 制作简单清单

  • 控制按钮集内的间距

  • 自动填充空间按钮

  • 对组内按钮进行排序

  • 使用按钮悬停状态的效果

  • 按钮图标和隐藏文本

介绍

按钮小部件是装饰用户界面中的 HTML 按钮和链接元素的简便方法。通过对按钮小部件进行简单调用,我们能够使用 jQuery UI 中的主题框架装饰标准元素。此外,有两种类型的按钮。一种是单一的按钮概念,是更受欢迎的用例。但还有一个按钮集的概念——用于装饰典型 HTML 表单中的复选框和单选按钮的情况。

在本章中,我们将更仔细地查看按钮所包含的内容,通过示例涵盖一些使用场景。我们将从简单的用法开始,比如创建一个清单和排序按钮,到更高级的用法,比如应用效果和自动填充空间。沿途,你将了解到小部件框架如何支持开发人员在小部件不能完全满足他们需求时扩展按钮。

制作简单清单

在纯 HTML 中做清单非常简单,你真正需要的只是一些复选框和旁边的一些标签。然而,如果你使用诸如 jQuery UI 之类的小部件框架,我们可以轻松地增强该列表。按钮小部件知道在应用于input类型的checkbox元素时如何行为。因此,让我们从一个基本列表开始,看看我们如何将按钮小部件应用于input元素。我们还将看到我们是否可以通过一些状态和图标增强来进一步提高用户交互性。

准备工作

让我们从创建一个简单的 HTML div 开始来容纳我们的清单。在内部,每个项目由一个input元素表示,类型为checkbox,以及一个用于元素的label

<div>
    <input type="checkbox" id="first" />
    <label for="first">Item 1</label>
    <input type="checkbox" id="second" />
    <label for="second">Item 2</label>
    <input type="checkbox" id="third" />
    <label for="third">Item 3</label>
    <input type="checkbox" id="fourth" />
    <label for="fourth">Item 4</label>
</div>

有了这个标记,实际上我们已经拥有了一个可用的清单 UI,尽管不够可用。我们可以使用 jQuery UI 按钮小部件的切换功能将labelcheckbox封装在一起作为清单项。

如何做...

我们将介绍以下 JavaScript 代码来收集我们的checkbox输入,并使用它们的labels来组装切换按钮小部件。

$(function() {

    $( "input" ).button( { icons: { primary: "ui-icon-bullet" } } );

    $( "input" ).change( function( e ) {

        var button = $( this );

        if ( button.is( ":checked" ) ) {

            button.button( "option", {
                icons: { primary: "ui-icon-check" } 
            });

        }
        else {

            button.button( "option", {
                icons: { primary: "ui-icon-bullet" } 
            });

        }

    });

});

有了这个,你就有了一个切换按钮清单,完整的图标可辅助传达状态。当用户点击切换按钮时,它进入“开”状态,这通过背景颜色的变化和其他主题属性来表示。我们还添加了与按钮状态一起切换的图标。

如何做...

工作原理...

我们的事件处理程序在 DOM 准备就绪时触发,只需要一行代码就可以将页面上的 input 元素转换为切换按钮。在按钮构造函数中,我们指定要使用的默认图标是主题框架中的 ui-icon-bullet 图标类。按钮小部件知道我们正在创建一个切换按钮,因为底层 HTML 元素。由于这些是复选框,所以当单击按钮时,小部件会更改其行为——在 复选框 的情况下,我们希望按钮看起来像切换打开和关闭一样。此外,按钮小部件根据 for 属性知道哪个 label 属于哪个按钮。例如,for="first" 的标签将分配给 id="first" 的按钮。

接下来,我们将 change 事件处理程序应用于所有按钮。此处理程序对于每个按钮都相同,因此我们可以一次绑定它们所有按钮。此处理程序的工作是更新按钮图标。我们不必更改按钮状态的任何其他内容,因为默认按钮实现将为我们完成。在我们的事件处理程序中,我们只需要检查 复选框 本身的状态。如果选中,则显示 ui-icon-check 图标。否则,我们显示 ui-icon-bullet 图标。

使用 buttonset 控制间距

jQuery UI 工具包为开发人员提供了一个用于处理按钮组的容器小部件,称为buttonset。您可以将 buttonset 用于诸如复选框组或单选按钮组之类的东西——形成一个协同集合的东西。

buttonset 的默认外观是统一整体的。也就是说,目标是将几个按钮形成一个看似单一的小部件。默认情况下,buttonset 小部件对于开发人员没有间距控制。默认情况下,集合中的按钮都紧靠在一起。这可能不是我们想要的,这取决于 buttonset 小部件在整个用户界面中的上下文。

准备就绪

为了更好地说明我们所面临的间距约束,让我们构建一个按钮集小部件,然后再尝试解决这个问题之前看一下结果。我们将使用以下一组单选按钮作为我们的标记:

<div>
    <input type="radio" id="first" name="items" />
    <label for="first">Item 1</label>
    <input type="radio" id="second" name="items" />
    <label for="second">Item 2</label>
    <input type="radio" id="third" name="items" />
    <label for="third">Item 3</label>
    <input type="radio" id="fourth" name="items"/>
    <label for="fourth">Item 4</label>
</div>

我们将按如下方式创建 buttonset 小部件:

$(function() {
    $( "div" ).buttonset();
});

这是我们的 buttonset 的外观。请注意,此小部件仍然具有单选按钮功能。这里选择了第三个项目,但如果我在小部件中点击其他位置,它将变为未选中状态。

准备就绪

如何做...

现在,buttonset 小部件的默认呈现方式没有任何问题。我们可能面临的唯一潜在挑战是,如果我们在应用程序的其他地方有一个间距主题——小部件的堆叠在一起的外观可能不适合从美学角度看。我们可以通过相对较少的努力通过使用选项来扩展小部件来解决此问题,该选项允许我们“爆破”按钮,使它们不再接触。

我们将通过扩展按钮集小部件并添加一个新选项来实现这种新的爆炸式buttonset功能,该选项将启用这种行为。HTML 与以前相同,但这是新的 JavaScript 代码。

(function( $, undefined ) {

$.widget( "ab.buttonset", $.ui.buttonset, {

    options: {
        exploded: false
    },

    refresh: function() {

        this._super("refresh");

        if ( !this.options.exploded ) {
            return;
        }

        var buttons = this.buttons.map(function() {
            return $( this ).button( "widget" )[ 0 ];
        });

        this.element.addClass( "ui-buttonset-exploded" );

        buttons.removeClass( "ui-corner-left ui-corner-right" )
               .addClass( "ui-corner-all" );

    }

});

})( jQuery );

$(function() {
    $( "div" ).buttonset( { exploded: true } );
});

我们希望在页面中包括以下 CSS——通过新样式表的方式包含它是推荐的做法:

.ui-buttonset-exploded .ui-button {
    margin: 1px;
}

如何做...

它是如何工作的...

我们对按钮集小部件的扩展添加了exploded选项,允许使用该小部件的程序员指定他们是否希望将各个按钮分开还是不分开。我们还在这里重写了refresh()方法,以便在exploded选项为true时修改显示。

为此,我们创建代表按钮集中所有单独按钮的 jQuery 对象。这里我们使用map()的原因是因为checkboxradio按钮需要一个解决方法。ui-buttonset-exploded类添加了我们在按钮之间寻找的margin,它将它们向外扩展。接下来,我们移除任何按钮的ui-corner-leftui-corner-right类,并将ui-corner-all类添加到每个按钮上,使它们各自具有独立的边框。

自动填充空间的按钮

任何给定按钮小部件的宽度由其中的内容控制。这相当于主要或次要图标,或二者都没有,再加上文本。按钮本身的实际呈现宽度没有具体规定,而是由浏览器确定。当然,这是任何小部件的令人满意的特性——依赖浏览器计算尺寸。这种方法在需要考虑界面中有很多小部件,以及需要考虑有很多浏览器分辨率配置的情况下,很好地实现了比例缩放。

然而,有一些情况下,浏览器自动设置的宽度并不理想。想象一下在同一上下文中的几个按钮,也许是一个div元素。很可能,这些按钮不会呈现为具有相同宽度,而这实际上是一种期望的属性。仅仅因为组中有一个按钮具有稍多或稍少的文本,并不意味着我们不希望它们共享一致的宽度。

做好准备

这里的目标是将按钮组中最宽的按钮视为目标宽度。当添加新按钮时,按钮组的同级按钮会收到通知,如果它是最宽的话,可能会创建一个新的目标宽度。让我们通过查看默认按钮功能以及它在宽度方面的含义来更详细地说明问题。

以下是我们将使用来创建按钮小部件的 HTML。

<div>
    <button style="display: block;">Button 1</button>
    <button style="display: block;">Button 2</button>
    <button style="display: block;">Button with longer text</button>
</div>

我们明确将每个按钮标记为块级元素,这样我们就可以轻松地对比宽度。同时请注意,按钮都是同级的。

以下 JavaScript 将每个按钮元素转换为按钮小部件。

$(function() {
    $( "button" ).button();
});

您可以看到,前两个按钮的长度相同,而最后一个按钮使用更多文本且最宽。

准备就绪

如何做...

现在让我们通过一些新行为扩展按钮小部件,允许开发人员在组内同步每个按钮的宽度。扩展按钮小部件的修改后的 JavaScript 代码如下:

(function( $, undefined ) {

$.widget( "ab.button", $.ui.button, {

    options: {
        matchWidth: false
    },

    _create: function() {

        this._super( "create" );

        if ( !this.options.matchWidth ) {
            return;
        }

        this.element.siblings( ":" + this.widgetFullName )
                    .addBack()
                    .button( "refresh" );

    },

    refresh: function() {

        this._super( "refresh" );

        if ( !this.options.matchWidth ) {
            return;
        }

        var widths = this.element
                         .siblings( ":" + this.widgetFullName )
                         .addBack()
                         .children( ".ui-button-text" )
                         .map(function() {
                            return $( this ).width();
                         }),
            maxWidth = Math.max.apply( Math, widths ),
            buttonText = this.element.children( ".ui-button-text" );

        if ( buttonText.width() < maxWidth ) {
            buttonText.width( maxWidth );
        }

    }

});

})( jQuery );

$(function() {
    $( "button" ).button( { matchWidth: true } );
});

在这里,您可以看到按钮彼此通信,以确定组内每个同级元素的正确宽度。换句话说,前两个按钮由于添加到组中的最后一个按钮而改变宽度。

如何做...

工作原理...

我们刚刚添加的按钮小部件的扩展创建了一个新的 matchWidth 选项,如果为 true,将会根据需要将此按钮的宽度更改为该组中最宽的按钮的宽度。

我们的 _create() 方法的扩展调用了默认的 _create() 按钮实现,然后我们告诉所有的同级元素去 refresh()。我们通过使用 addBack() 将此按钮包含在同级元素列表中——原因是,如果已经有人比我们更大,我们可能必须调整自己的宽度。或者,如果我们现在是最宽的同级元素,我们必须通知每个人,以便他们可以调整自己的宽度。

refresh() 方法调用基本的 refresh() 实现,然后确定是否应更新此按钮的宽度。第一步是为组中的所有同级元素(包括自己)生成一个宽度数组。有了宽度数组,我们可以将其传递给 Math.max() 来获取最大宽度。如果此按钮的当前宽度小于组中最宽的按钮的宽度,则调整为新宽度。

请注意,我们实际上并没有收集或更改按钮元素本身的宽度,而是 span 元素内部。这个 span 具有 ui-button-text 类,并且是我们感兴趣的可变宽度元素。如果我们采取了简单地测量按钮宽度的方法,我们可能会遇到一些混乱的边距问题,使我们处于比起始状态更糟糕的状态。

还有更多...

在前面的示例中,您会注意到调整大小的按钮文本保持居中。如果愿意的话,我们可以在更改按钮宽度时引入一些小的 CSS 调整,以保持按钮文本对齐。

(function( $, undefined ) {

$.widget( "ab.button", $.ui.button, {

    options: {
        matchWidth: false
    },

    _create: function() {

        this._super( "create" );

        if ( !this.options.matchWidth ) {
            return;
        }

        this.element.siblings( ":" + this.widgetFullName )
                    .addBack()
                    .button( "refresh" );

    },

    _destroy: function() {
        this._super();
        this.element.css( "text-align", "" );
    },

    refresh: function() {

        this._super( "refresh" );

        if ( !this.options.matchWidth ) {
            return;
        }

        var widths = this.element
                         .siblings( ":" + this.widgetFullName )
                         .addBack()
                         .children( ".ui-button-text" )
                         .map(function() {
                            return $( this ).width();
                         }),
            maxWidth = Math.max.apply( Math, widths ),
            buttonText = this.element.children( ".ui-button-text" );

        if ( buttonText.width() < maxWidth ) {
            buttonText.width( maxWidth );
            this.element.css( "text-align", "left" );
        }

    }

});

})( jQuery );

$(function() {
    $( "button" ).button( { matchWidth: true } );
});

_refresh() 方法中,请注意我们现在指定了 text-align CSS 属性为 left。此外,我们必须添加一个新的 _destroy() 方法,在销毁按钮时清除该属性。最终结果与我们之前的示例相同,只是现在按钮文本是对齐的。

还有更多...

对组内排序的按钮

我们可以使用 sortable() 交互小部件为用户提供一些灵活性。为什么不让用户移动按钮呢?尤其是考虑到它所需的代码量很少。

准备就绪

我们将使用列表来组织我们的按钮,如下所示:

<ul>
    <li><a href="#">Button 1</a></li>
    <li><a href="#">Button 2</a></li>
    <li><a href="#">Button 3</a></li>
</ul>

我们将使用以下 CSS 来修复列表布局,以更好地显示按钮小部件。

ul {
    list-style-type: none;
    padding: 0;
}

li {
    margin: 4px;
}

如何操作...

使此功能生效的 JavaScript 代码实际上非常小——我们创建按钮,然后应用可排序的交互小部件。

$(function() {
    $( "a" ).button();
    $( "ul" ).sortable({
        opacity: 0.6
    });
});

到目前为止,我们能够拖放按钮——但只能在指定的容器元素内,此处为ul

如何操作...

工作原理...

在这个示例中,一旦文档准备就绪,我们首先要做的事情是创建按钮小部件。我们使用锚点作为底层元素,它与button元素一样有效。您还会注意到,我们将按钮小部件结构化在无序列表中显示在页面上。页面添加的样式只是移除了列表的缩进和项目符号。但是我们的目标是ul元素,用于可排序的交互。默认情况下,可排序小部件查找所有子元素并将它们作为可排序项目,在我们的情况下,这些是li元素。示例中指定的opacity选项告诉sortable改变正在拖动的元素的视觉不透明度。

使用按钮悬停状态的效果

按钮小部件利用了 jQuery UI 主题框架中找到的各种状态。例如,当用户悬停在按钮小部件上时,此事件会触发按钮小部件内的处理程序,将ui-state-hover类应用于元素,从而更改其外观。同样,当鼠标离开小部件时,另一个处理程序会移除该类。

按钮小部件的默认功能很好用——它只是使用addClass()removeClass() jQuery 函数来应用悬停类。当用户四处移动并考虑下一步要做什么时,鼠标可能会在按钮小部件上移进移出;这就是我们通过提供一些微妙的效果来调整体验的地方。

准备工作

在这个示例中,我们将创建三个简单的按钮元素,它们将作为按钮小部件。这样,我们就可以尝试将鼠标指针移动到几个按钮上。

<div>
    <button>Button 1</button>
    <button>Button 2</button>
    <button>Button 3</button>
</div>

如何操作...

让我们扩展默认按钮小部件的功能,包括一个名为animateHover的新选项,当设置为true时,会对ui-state-hover类的添加和移除进行动画处理。

(function( $, undefined ) {

$.widget( "ab.button", $.ui.button, {

    options: {
        animateHover: false 
    },

    _create: function() {

        this._super( "create" );

        if ( !this.options.animateHover ) {
            return;
        }

        this._off( this.element, "mouseenter mouseleave" );

        this._on({
            mouseenter: "_mouseenter",
            mouseleave: "_mouseleave"
        });

    },

    _mouseenter: function( e ) { 
        this.element.stop( true, true )
                    .addClass( "ui-state-hover", 200 );
    },

    _mouseleave: function( e ) {
        this.element.stop( true, true )
                    .removeClass( "ui-state-hover", 100 );
    }

});

})( jQuery );

$(function() {
    $( "button" ).button( { animateHover: true } );
});

工作原理...

我们为按钮小部件添加了一个名为animateHover的新选项。当设置为true时,按钮将对ui-state-hover类的添加或移除进行动画处理。这是通过覆盖首次实例化按钮小部件时调用的_create()方法来完成的。在这里,我们检查animateHover选项是否为false,然后在调用执行常规按钮初始化任务的原始_create()方法之后执行。

如果设置了该选项,首先要做的工作是解绑按钮上原始的mouseentermouseleave事件处理程序。这些处理程序默认情况下只是添加或删除悬停类。这正是我们想要改变的,因此一旦删除了原始处理程序,我们就可以自由地使用_on()注册我们自己的处理程序。这是我们使用stop()addClass()removeClass()函数的地方。如果在类名后给出了持续时间,jQuery UI 效果扩展将应用于addClass()removeClass()函数,我们在这里已经这样做了。我们希望添加ui-state-hover类需要200毫秒,并且删除类需要100毫秒,因为用户更容易注意到初始悬停。最后,stop( true, true )调用是 jQuery 中确保动画不重叠并导致用户视角中出现抖动行为的常用技巧。

按钮图标和隐藏文本

开发人员可以选择仅呈现图标按钮。通过告诉按钮我们不希望显示文本,就可以实现这一点。这很容易做到,并且适用于许多用例——通常情况下,根据上下文,一个图标就足以解释它的操作。此外,我们随时可以通过简单的选项更改重新添加按钮标签。这是因为按钮文本是底层 HTML 组件的一部分。然而,对于图标,情况就变得有点棘手了,因为它们是按钮上的装饰。我们不能像处理文本那样打开和关闭图标——整个图标规范需要再次应用。

那么,一个值得考虑的方法是在按钮构造函数中指定图标,但在关闭后记住它们。这样,图标就会表现得好像它们是原始 DOM 元素的一部分。

准备工作

我们将从创建三个图标按钮所需的结构开始。我们还将介绍两个链接,用于改变每个按钮的状态。

<div>
    <button class="play">Play</button>
    <button class="pause">Pause</button>
    <button class="stop">Stop</button>
</div>

<div>
    <br/>
    <a href="#" class="no-icons">no icons</a>
    <br/>
    <a href="#" class="icons">icons</a>
</div>

如何做...

通过添加一个新的icon选项,我们将为按钮小部件提供图标切换功能。记住,我们的想法是提供与text选项相同的功能,只是用于图标。

(function( $, undefined ) {

$.widget( "ab.button", $.ui.button, {

    options: {
        icon: true
    },

    _hiddenIcons: {},

    _setOption: function( key, value ) {

        if ( key != "icon" ) {
            this._superApply( arguments );
            return;
        }

        if ( !value && !$.isEmptyObject( this.options.icons ) ) {
            this._hiddenIcons = this.options.icons;
            this._super( "text", true );
            this._super( "icons", {} );
        }
        else if ( value && $.isEmptyObject( this.options.icons ) ) {
            this._super( "icons", this._hiddenIcons );
        }

    },

    _create: function() {

        if ( !this.options.icon ) {
            this._hiddenIcons = this.options.icons;
            this.options.icons = {};
        }

        this._superApply( arguments );

    }

});

})( jQuery );

$(function() {

    $( "a.no-icons" ).click( function( e ) {
        e.preventDefault();
        $( "button" ).button( "option", "icon", false );
    });

    $( "a.icons" ).click( function( e ) {
        e.preventDefault();
        $( "button" ).button( "option", "icon", true );
    });

    $( "button" ).button( {text: false} );

    $( ".play" ).button( "option", {
        icons: { primary: "ui-icon-play" }
    });

    $( ".pause" ).button( "option", {
        icons: { primary: "ui-icon-pause" }
    });

    $( ".stop" ).button( "option", {
        icons: { primary: "ui-icon-stop" } 
    });

});

工作原理...

最初,尽管按钮文本仍然作为底层 DOM 元素的一部分存在,但三个按钮都已禁用text。接下来,我们为三个按钮设置icon选项。当页面首次加载时,你应该只看到图标按钮。

工作原理...

页面上的两个链接,no iconsicons分别删除和添加按钮小部件的图标。每个链接的功能回调是通过为我们添加到button小部件的自定义icon选项设置一个值来完成的。点击no icons链接将导致删除按钮图标,并用它们的文本替换。

工作原理...

通过点击图标链接,我们重新启用了先前为每个按钮设置的icons选项。这是通过更改我们的自定义icon按钮完成的,因此现在如果我们点击该链接,我们可以看到我们的图标已恢复,而无需指定使用了哪些图标。

它是如何工作的...

您会注意到,通过将icon值设置为true,我们没有隐藏文本,这在按钮的原始状态下是这样的。我们仍然可以通过手动将text设置为false来做到这一点,但这应该是一个手动过程,而不是我们的按钮扩展修改。

我们的扩展添加了一个新的_hiddenIcons属性,用于在icon选项设置为false时存储icons选项的值。我们的大部分工作都在_setOption()中进行,这是在开发人员想要在小部件上设置选项时调用的。我们只关心我们添加的新icon选项。首先,我们检查是否禁用了图标,如果是,则将icons选项的副本存储在_hiddenIcons属性中,以便以后可以恢复它。我们还将text选项设置为true,这样如果隐藏了文本,文本就会显示。同时隐藏按钮图标和文本是一个坏主意。最后,我们通过取消设置icons选项来实际隐藏图标。

另一方面,如果我们启用了图标,我们需要在_hiddenIcons属性中查找它们,并将它们设置为icons按钮选项。我们在这里覆盖的_create()实现只是将图标设置存储在_hiddenIcons中,并在首次创建小部件时隐藏它们(如果已指定)。

第四章:开发日期选择器

在本章中,我们将涵盖:

  • 处理不同的日期格式

  • 制作全尺寸的日历小部件

  • 显示月度效果

  • 预约提醒作为工具提示

  • 限制日期范围

  • 隐藏输入字段

  • 附加数据和控件

介绍

日期选择器小部件通过提供日期选择实用工具来增强典型的文本输入表单元素。我们现在在 Web 上到处都可以看到这些类型的输入。日期选择器的图形化性质对大多数用户来说是直观的,因为它与物理日历非常相似。日期选择器小部件还解决了处理一致日期格式的挑战,这是用户无需担心的。

处理不同的日期格式

日期选择器小部件支持各种日期字符串格式。当用户进行选择时,日期字符串是填充在文本输入中的值。通常情况下,应用程序会尝试在整个用户界面中遵循相同的日期格式以保持一致性。因此,如果您不满意小部件提供的默认格式,我们可以在创建小部件时使用dateFormat选项进行更改。

如何做...

我们将从创建两个input字段开始,其中我们需要用户输入日期:

<div>
    <label for="start">Start:</label>
    <input id="start" type="text" size="30"/>
</div>

<div>
    <label for="stop">Stop:</label>
    <input id="stop" type="text" size="30"/>
</div>

接下来,我们将使用前面的input字段并指定我们的自定义格式来创建两个日期选择器小部件。

$(function() {

    $( "input" ).datepicker({
        dateFormat: "DD, MM d, yy"
    });

});

它是如何工作的...

当我们在日期选择器小部件中做出选择时,您会注意到文本input值会更改为所选日期,使用我们选择的格式。日期格式字符串本身,"DD, MM d, yy",是根据大多数其他编程语言中找到的格式而建模的,也就是说,日期选择器没有本机 JavaScript 日期格式化工具可用。当用户在日期选择器的下拉日历中进行选择时,将创建一个Date对象。然后小部件使用dateFormat选项来格式化Date对象,并将文本输入填充为结果。

它是如何工作的...

还有更多...

如果我们正在构建一个相当大的用户界面,我们可能会在几个不同的地方使用几个日期选择器小部件。为了保持日期格式的一致性,我们将不得不每次创建日期选择器小部件时指定dateFormat选项。我们可能会有几个调用创建小部件的调用使用不同的选择器,因此总是指定相同的dateFormat选项有点烦人,而它应该只是默认值。

在这种情况下,最好只是将默认的dateFormat值更改为我们的应用程序遍布的内容。这比一遍又一遍地指定相同的格式要好,同时保留了根据情况更改日期格式的能力。

我们将使用与之前相同的 HTML 结构-两个input字段是我们的日期选择器占位符。但让我们修改 JavaScript 如下:

(function( $, undefined ) {

$.widget( "ui.datepicker", $.ui.datepicker, {
    options: $.extend(
        $.ui.datepicker.prototype.options,
        { dateFormat: "DD, MM d, yy" }
    ),
});

})( jQuery );

$(function() {

    $( "#start" ).datepicker();
    $( "#stop" ).datepicker();

});

现在,如果你运行这个修改过的 JavaScript,你会得到与之前相同的日期选择器行为。然而,你会注意到,我们现在要调用两次 datepicker() 构造函数。都没有指定 dateFormat 选项,因为我们通过定制 datepicker 小部件和扩展 options 来改变了默认值。我们仍然可以为每个单独的小部件提供自定义日期格式的选项,这样做可以为我们节省大量潜在的重复 dateFormat 选项。

制作一个全尺寸的日历小部件

datepicker 小部件的典型用法是增强标准表单输入字段。当字段获得焦点时,我们希望为用户显示实际的日期选择器。如果我们遵循小部件的标准使用模式——选择日期,那么这就是有道理的。毕竟,这就是为什么它被称为日期选择器的原因。

但是,我们可以利用主题框架提供的一些灵活性,并对日期选择器进行一些微小的调整,以显示一个更大的日历。不一定是为了作为输入选择日期的目的,而是作为一个大窗口来显示与日期/时间相关的信息。我们需要对小部件进行的更改仅仅是将内联显示的尺寸放大。

准备工作

日期选择器小部件已经知道如何在内联中显示自己。我们只需要在 div 元素上调用日期选择器构造函数,而不是在 input 元素上。所以我们将使用这个基本的标记:

<div class="calendar"></div>

还有一个普通的 datepicker() 调用:

$(function() {
    $( ".calendar" ).datepicker();
});

其余的工作是在主题调整中完成的。

如何操作...

调整日期选择器 CSS 的目标是使其尺寸放大。想法是使小部件看起来更像一个日历,而不像一个表单输入字段助手。日历已经以内联方式显示了,所以让我们在页面上包含这个新的 CSS。

.ui-datepicker {
    width: 500px;
}

.ui-datepicker .ui-datepicker-title {
    font-size: 1.3em;
}

.ui-datepicker table {
    font-size: 1em;
}

.ui-datepicker td {
    padding: 2px;
}

.ui-datepicker td span, .ui-datepicker td a {
    padding: 1.1em 1em;
}

有了这个,我们就有了一个缩放的日历小部件,作为一个日期选择器仍然可以完美地运行,因为我们没有改变小部件的任何功能。

操作步骤...

工作原理...

我们对这些新的样式声明所做的第一件事是增加日历显示的宽度为500px。这可以是我们选择的任何数字,最适合我们正在开发的用户界面。接下来,我们将增加标题部分——月份和年份的字体大小。我们还增加了所有星期几和月份数字的字体大小,并在月份日槽之间提供了更多的填充。我们现在有了空间,我们可能会用到它。最后,设置在 td spantd a 元素上的 padding 修复了整个日历的高度;否则,纵横比将会非常不协调。这是另一个我们希望根据每个应用程序进行微调的数字,以便正确设置。

显示月度效果

当日期选择器选择器显示时,我们通常为用户每次显示一个月。如果用户需要向后导航时间,则使用上个月按钮。同样,他们可以使用下个月按钮向前导航时间。当发生这种情况时,日期选择器小部件仅清空日期选择器div,然后重新生成一些 HTML 以用于日历,并插入其中。这一切都发生得非常快,对于用户来说,基本上是瞬间的。

让我们通过向日期选择器内部注入一些效果来使这个月份导航更加生动。

准备工作

这个实验可以使用任何日期选择器小部件,但直接使用内联日期选择器显示可能更简单,而不是使用文本input。这样,当页面加载时,日期选择器就在那里,我们不需要打开它。内联日期选择器是使用div元素创建的。

<div class="calendar"></div>

如何做...

我们将扩展日期选择器小部件,以允许在我们调整当前月份时应用 jQuery 的fadeIn()fadeOut()函数。

(function( $, undefined ) {

$.extend( $.datepicker, {

    _updateDatepicker: function( inst ) {

        var self = this,
            _super = $.datepicker.constructor.prototype;

        inst.dpDiv.fadeOut( 500, function() {
            inst.dpDiv.fadeIn( 300 );
            _super._updateDatepicker.call( self, inst );
        });

    }

});

})( jQuery );

$(function() {
    $( ".calendar" ).datepicker();
});

现在,当用户单击日历顶部的下一个或上一个箭头按钮时,我们会看到小部件淡出,并淡入具有新日历月份布局的界面。

它的工作原理...

你会注意到这段代码的第一件事是它没有使用典型的小部件工厂机制扩展日期选择器小部件。这是因为日期选择器的默认实现尚未转移到新的小部件工厂方式进行操作。但这并不妨碍我们根据需要扩展小部件。

提示

日期选择器小部件很复杂——比框架内的大多数其他小部件都要复杂得多。在引入如此重大变化之前,核心 jQuery UI 团队必须考虑许多因素。截至撰写本文时的计划是,日期选择器小部件将在将来的版本中像其他小部件一样成为小部件工厂的产品。

我们在$.datepicker对象上使用了 jQuery extend()函数。该对象是Datepicker类的单例实例,这是我们为了简洁而感兴趣的内容。_updateDatepicker()方法是我们在此自定义中的目标。默认的日期选择器实现使用此方法来更新日期选择器div的内容。因此,我们想要重写它。在我们版本的方法中,我们使用fadeOut()隐藏了inst.dpDiv。一旦完成,我们调用fadeIn()_super变量是对用于定义小部件的Datepicker类的引用。由于$.datepicker是一个实例,因此访问Datepicker原型的唯一方法是通过$.datepicker.constructor.prototype。我们需要Datepicker原型的原因是,这样我们才能在我们完成了效果后调用原始的_updateDatepicker()方法,因为它执行了与配置显示相关的其他几个任务。

将预约提醒显示为工具提示

日期选择器小部件帮助用户为input字段选择正确的日期,或者作为基本显示。在任何一种情况下,如果我们能为用户提供更多的上下文信息,那么这不是很有用吗?也就是说,如果我正在使用日期选择器来选择表单中的日期,那么当我将鼠标指针移到日历中的某一天时,知道那天有些事情要做会很有帮助。也许我应该选择其他日期。

在本节中,我们将研究扩展日期选择器小部件的能力,以允许指定作为工具提示出现的提醒。这些作为选项传递给日期选择器构造函数,并且可能在应用程序内部产生,可能是从数据库中的用户资料中获得的。

如何实现...

我们将使用一个简单的内联日期选择器作为示例,目标标记为<div class="calendar"></div>

让我们通过接受提醒对象数组并为它们创建工具提示来扩展日期选择器的功能。提醒对象只是一个带有datetext字段的普通 JavaScript 对象。日期告诉日期选择器工具提示应该放在日历中的何处。

(function( $, undefined ) {

$.extend( $.datepicker, {

    _updateDatepicker: function( inst ) {

        var settings = inst.settings,
            days = "td[data-handler='selectDay']",
            $target = inst.dpDiv,
            _super = $.datepicker.constructor.prototype;

        _super._updateDatepicker.call( this, inst )

        if ( !settings.hasOwnProperty( "reminders" ) ) {
            return;
        }

        $target.find( days ).each( function( i, v ) {

            var td = $( v ),
                currentDay = new Date(
                    td.data( "year" ),
                    td.data( "month" ),
                    td.find( "a" ).html()
                );

            $.each( settings.reminders, function( i, v ) {

                var reminderTime = v.date.getTime(),
                    reminderText = v.text,
                    currentTime = currentDay.getTime();

                if ( reminderTime == currentTime ) {
                    td.attr( "title", reminderText ).tooltip();
                }

            });

        });

    }

});

})( jQuery );

$(function() {
    $( ".calendar" ).datepicker({
        reminders: [
            {
                date: new Date(2013, 0, 1),
                text: "Happy new year!"
            },
            {
                date: new Date(2013, 0, 14),
                text: "Call in sick, case of the Mondays"
            },
            {
                date: new Date(2013, 1, 14),
                text: "Happy Valentine's Day!"
            }
        ]
    });
});

现在,当您将鼠标指针移到日期选择器小部件中提供的提醒日期上时,您应该会看到所提供的文本作为工具提示显示出来:

如何实现...

工作原理...

让我们退一步,思考传递给提醒参数的数据以及我们对其进行了什么处理。传递的值是一个对象数组,每个对象都有datetext属性。文本是我们想要显示在工具提示中的内容,而日期告诉日期选择器在何处放置工具提示。因此,我们取得这个值并将其与日期选择器日历中呈现的日期进行比较。

所有定制工作都在我们自己的_updateDatepicker()方法的实现中完成。每次渲染日历时都会调用此方法。这包括从一个月切换到另一个月。我们使用对原始日期选择器实现的引用_super来调用_updateDatepicker()方法。一旦完成了这一点,我们就可以执行我们的自定义。我们首先检查是否已提供提醒参数,否则我们的工作已经完成。

接下来,我们查找并迭代当前显示的月份中表示一天的每个td元素。对于每一天,我们构造一个代表表格单元格的 JavaScriptDate对象——我们将需要这个对象来与每个提醒条目进行比较。最后,我们在reminders参数中迭代每个提醒对象。如果我们在应该显示此提醒的日期上,我们在设置td元素的title属性之后构造工具提示小部件。

限制日期范围

您的应用程序可能需要限制可选日期,限制日期范围。也许这是基于某些其他条件为真或事件被触发的。谢天谢地,我们有足够的灵活性来处理小部件的最常见的受限选择配置。

准备工作...

我们将使用基本输入元素标记来创建我们的日期选择器小部件:

<div>
    <label for="start">Start:</label>
    <input id="start" type="text" size="30"/>
</div>

如何做...

我们将使用minDatemaxDate选项创建我们的日期选择器小部件。

$(function() {

    $( "input" ).datepicker({
        minDate: new Date(),
        maxDate: 14
    });

});

当我们通过单击input字段激活日期选择器小部件时,您会注意到仅有特定范围的日期可选择。

如何做...

工作原理...

minDatemaxDate选项都接受各种格式。 在我们的示例中,我们给minDate选项一个代表今天的Date对象。 这意味着用户不能选择今天之前的任何日期。 此外,我们不希望允许用户选择任何日期超过未来两周。 通过给maxDate选项提供14天的增量,这很容易指定。

还有更多...

给定日期选择器实例的受限日期范围不一定要静态定义。 实际范围可能取决于 UI 中的某些动态内容,例如另一个日期选择器的选择。

让我们看看我们如何限制日期范围,取决于选择另一个日期。 我们将创建两个日期选择器小部件。 当用户在第一个小部件中选择日期时,将使用更新的范围限制启用第二个小部件。 用户不能在第一个日期选择器之前选择日期。

这是我们将用于两个日期选择器的标记:

<div>
    <label for="start">Start:</label>
    <input id="start" type="text" size="30"/>
</div>

<div>
    <label for="start">Stop:</label>
    <input id="stop" type="text" size="30"/>
</div>

这是创建两个日期选择器小部件的代码:

$(function() {

    function change ( e ) {

        var minDate = $( this ).datepicker( "getDate" );

        $( "#stop" ).datepicker( "enable" );
        $( "#stop" ).datepicker( "option", "minDate", minDate );

    }

    $( "#start" ).datepicker()
                 .change( change );

    $( "#stop" ).datepicker( { disabled: true } );

});

默认情况下,#stop日期选择器被禁用,因为我们需要知道minDate值应该是什么。

更多内容...

但是一旦用户在#start日期选择器中做出选择,我们就可以在#stop日期选择器中做出选择-我们只是不能在#start日期选择器中的选择之前选择任何内容。

更多内容...

#start日期选择器在进行选择时启用并更新#stop日期选择器。 它启用小部件并将getDate的值作为minDate选项传递。 这基于先前选择进行单向强制用户。

隐藏输入字段

日期选择器小部件的目标是在用户进行选择后填充文本input元素。 因此,小部件对于input元素有两种用途。 首先,它监听input元素上的focus事件。 这是它知道何时显示日历选择器的方式。 其次,一旦做出选择,input元素的值会更新以反映所选格式的日期。

向用户呈现一个 input 元素在大多数情况下都可以正常工作。但也许由于某些原因,输入框并不适合您的 UI。也许我们需要一种不同的方法来显示日历并存储/显示选择。

在本节中,我们将探讨一种替代方法,而不仅仅使用日期选择器 input 元素。我们将使用一个 button 小部件来触发日历显示,并且我们将伪装 input 元素,使其看起来像是其他东西。

准备就绪

让我们使用以下 HTML 示例。我们将布置四个日期部分,用户需要按按钮才能与日期选择器小部件交互。

<div>

    <div class="date-section">
        <label>Day 1:</label>
        <button>Day 1 date</button>
        <input type="text" readonly />
    </div>

    <div class="date-section">
        <label>Day 2:</label>
        <button>Day 2 date</button>
        <input type="text" readonly />
    </div>

    <div class="date-section">
        <label>Day 3:</label>
        <button>Day 3 date</button>
        <input type="text" readonly />
    </div>

    <div class="date-section">
        <label>Day 4:</label>
        <button>Day 4 date</button>
        <input type="text" readonly />
    </div>

</div>

如何做...

我们使日期部分按预期工作的第一件事是一些 CSS。这不仅对于布置我们正在构建的 UI 非常重要,还对于伪装 input 元素很重要,以便用户不知道它在那里。

div.date-section {
    padding: 5px;
    border-bottom: 1px solid;
    width: 20%;
}

div.date-section:last-child {
    border-bottom: none;
}

div.date-section label {
    font-size: 1.2em;
    font-weight: bold;
    margin-right: 2px;
}

div.date-section input {
    border: none;
}

现在我们将编写 JavaScript 代码来实例化日期选择器和按钮小部件。

$(function() {

    var input = $( "div.date-section input" ),
        button = $( "div.date-section button" );

    input.datepicker({
        dateFormat: "DD, MM d, yy"
    });

    button.button({
        icons: { primary: "ui-icon-calendar" }, 
        text: false
    });

    button.click( function( e ) {
        $( this ).next().datepicker( "show" )
    });

});

有了这些,现在我们有了四个日期部分,用户可以单击标签右侧的日期按钮来显示日历。他们选择一个日期,然后日历就会隐藏起来。您会注意到我们的 CSS 样式已经隐藏了 input 元素。

如何做...

工作原理...

此示例中的大多数 CSS 样式规则用于布局 UI 组件、labelbuttonsinput。您会注意到,在选择日期之前,输入框是不可见的。这是因为它尚无文本值,而且因为我们已经在我们的 div.date-section 输入 CSS 选择器中删除了 border

我们的 JavaScript 代码在页面加载时首先为每个输入元素创建日期选择器小部件。我们还将自定义字符串传递给 dateFormat 选项。对于每个日期部分,我们都有一个按钮。我们在此处使用按钮小部件来创建一个日历图标按钮,当单击时显示日历。我们通过调用 datepicker( "show" ) 来实现这一点。

附加的日历数据和控件

日期选择器小部件有各种附加数据和控件选项,开发人员可以使用该小部件公开这些选项。这些都是简单的布尔配置选项,用于打开数据或控件。

入门指南

让我们准备两个 div 元素,用它们可以创建两个内联日期选择器实例。

<div>
    <strong>Regular:</strong>
    <div id="regular"></div>
</div>

<div>
    <strong>Expanded:</strong>
    <div id="expanded"></div>
</div>

如何做...

让我们创建两个日期选择器小部件。我们创建两个小部件,以便我们可以对比普通日期选择器和具有扩展数据和控件的日期选择器之间的差异。

$(function() {

    $( "#regular" ).datepicker();

    $( "#expanded" ).datepicker({
        changeYear: true,
        changeMonth: true,
        showButtonPanel: true,
        showOtherMonths: true,
        selectOtherMonths: true,
        showWeek: true
    });

});

现在您可以看到两个渲染的日期选择器之间的差异。后者已经通过附加的控件和数据进行了扩展。

如何做...

工作原理...

我们对扩展的 datepicker 实例所做的所有工作都是打开一些默认关闭的功能。具体如下:

  • changeYear: 这样可以启用年份下拉菜单。

  • changeMonth: 这样可以启用月份下拉菜单。

  • showButtonPanel:这会在日历底部启用今天完成按钮。

  • showOtherMonths:这会显示来自相邻月份的日期。

  • showWeek:这会在日历中启用一周中的列。

第五章:添加对话框

在本章中,我们将介绍以下示例:

  • 对对话框组件应用效果

  • 等待 API 数据加载

  • 在对话标题中使用图标

  • 向对话框标题添加操作

  • 对对话框调整交互应用效果

  • 用于消息的模态对话框

介绍

对话框小部件为 UI 开发人员提供了一个工具,他们可以在不中断当前页面内容的情况下向用户呈现表单或其他信息片段;对话框创建了一个新的上下文。开箱即用,开发人员可以使用对话框选项做很多事情,并且其中许多功能默认情况下是打开的。这包括调整对话框的大小并在页面上移动它的能力。

在本章中,我们将解决在任何 Web 应用程序中典型对话框使用中的一些常见问题。通常需要调整对话框的控件和整体外观;我们将涉及其中一些。我们还将看看如何与 API 数据交互使对话框使用变得复杂以及处理方法。最后,我们可以通过查看如何以各种方式将效果应用于它们来为对话框小部件添加一些亮点。

对对话框组件应用效果

在开箱即用的情况下,对话框小部件允许开发人员在打开对话框时显示动画,以及在关闭时隐藏动画。此动画应用于整个对话框。因此,例如,如果我们指定show选项是fade动画,则整个对话框将对用户淡入视图。同样,如果hide选项是fade,则对话框会淡出视图,而不是立即消失。为了活跃这种showhide行为,我们可以对各个对话框组件进行操作。也就是说,我们可以将显示和隐藏效果应用于小部件内部的各个部分,如标题栏和按钮窗格,而不是将它们应用于整个对话框。

怎么做……

我们要创建的对话框在内容上非常简单。也就是说,在 HTML 中我们只会为对话框指定一些基本的title和内容字符串。

<div title="Dialog Title">
    <p>Basic dialog content</p>
</div>

为了将对话框组件的逐个动画化的想法变为现实,我们需要在几个地方扩展对话框小部件。特别是,我们将动画化小部件顶部的标题栏以及底部的按钮窗格。下面是 JavaScript 代码的样子:

(function( $, undefined ) {

$.widget( "ab.dialog", $.ui.dialog, {

    _create: function() {

        this._super();

        var dialog = this.uiDialog;

        dialog.find( ".ui-dialog-titlebar" ).hide();
        dialog.find( ".ui-dialog-buttonpane" ).hide();

    },

    open: function() {

        this._super();

        var dialog = this.uiDialog;

        dialog.find( ".ui-dialog-titlebar" ).toggle( "fold", 500 );
        dialog.find( ".ui-dialog-buttonpane" ).toggle( "fold", 500 );

    },

    close: function( event, isCallback ) {

        var self = this,
            dialog = this.uiDialog;

        if ( isCallback ) {
            this._super( event );
            return;
        }

        dialog.find( ".ui-dialog-titlebar" ).toggle( "fold", 500 );
        dialog.find( ".ui-dialog-buttonpane" ).toggle( "fold", 500, function(){
            self.element.dialog( "close", event, true );
        });

    }

});

})( jQuery );

$(function() {

    $( "div" ).dialog({
        show: "fade", 
        hide: "scale",
        buttons: {
            Cancel: function() {
                $( this ).dialog( "close" );
            }
        }
    });

});

当你打开页面时,你会看到独立于我们为对话指定的整体fade动画的各个对话框组件淡入视图。一旦可见,对话框应该看起来像这样:

怎么做……

你还会注意到,直到标题栏和按钮窗格应用fade效果之后,scale效果才会被应用。

它是如何工作的……

这段代码是规则的例外之一,我们没有提供关闭新扩展功能的机制。也就是说,我们在某些对话框方法的自定义实现中硬编码了更改,无法通过提供选项值来关闭。然而,这个例外是为了在复杂性和所需功能之间进行权衡。很可能这种自定义动画工作会作为特定项目需求的一部分进行,而不是对话框小部件功能的广泛扩展。

我们更改默认对话框实现的第一件事是在_create()方法中,我们隐藏了.ui-dialog-titlebar.ui-dialog-buttonpane组件。这是在调用_super()方法之后完成的,该方法负责创建基本对话框组件。即使对话框设置为使用autoOpen选项自动打开,_create()方法也不会实际显示它。因此,我们可以在用户没有注意到的情况下隐藏标题栏和按钮面板。

我们隐藏这两个组件的原因是因为我们希望在对话框打开时应用显示效果。下一个我们重写的方法open()就是这样做的。它首先调用_super()方法,该方法启动显示对话框的效果(在我们的情况下,我们告诉它在显示时淡入)。然后我们在标题栏和按钮面板上使用fold效果。

您会注意到我们在开始下一个动画之前不等待任何动画完成。对话框显示动画开始,然后是标题栏和按钮面板。这三个动画可能同时执行。我们之所以这样做是为了保持对话框的正确布局。我们要重写的最后一个方法是close()方法。这引入了一个有趣的解决方法,我们必须使用它来使得 _super() 在回调中起作用。即使在封闭范围内有 self 变量,我们在回调中调用 _super() 方法时也会遇到问题。因此,我们使用小部件元素,并假装我们是从小部件外部调用.dialog("close")一样。isCallback参数告诉close()方法调用 _super(),然后返回。我们之所以需要回调是因为我们实际上不想在完成按钮面板动画之前执行对话框隐藏动画。

等待 API 数据加载

通常情况下,对话框小部件需要从 API 加载数据。也就是说,并非所有对话框都由静态 HTML 构成。它们需要从 API 获取数据以使用 API 数据构建某些元素,例如select元素选项。

从 API 加载数据并构建结果元素并不是问题;我们一直在做这件事。挑战出现在我们尝试在对话上下文中执行这些活动时。我们不一定希望在从 API 加载数据并且用于显示对话框组件内部的 UI 组件已构建之前显示对话框,并且理想情况下,我们应该阻止对话框显示,直到对话框显示的组件准备好。

这在远程 API 功能中尤其棘手,因为不可能预测延迟问题。此外,对话框可能依赖于多个 API 调用,每个调用在对话框中填充自己的 UI 组件。

准备...

要为 API 数据问题实现解决方案,我们将需要一些基本的 HTML 和 CSS 来定义对话框及其内容。我们将在对话框中有两个空的 select 元素。这是 HTML 的样子:

<div id="dialog" title="Genres and Titles">
    <div class="dialog-field">
        <label for="genres">Genres:</label>
        <select id="genres"></select>
        <div class="ui-helper-clearfix"></div>
    </div>

    <div class="dialog-field">
        <label for="titles">Titles:</label>
        <select id="titles"></select>
        <div class="ui-helper-clearfix"></div>
    </div>
</div>

而且,这是上述代码的支持 CSS:

.dialog-field {
    margin: 5px;
}

.dialog-field label {
    font-weight: bold;
    font-size: 1.1em;
    float: left;
}

.dialog-field select {
    float: right;
}

如何做...

我们将给对话框小部件增加一个新选项来阻止在等待 API 请求时阻塞。此选项将允许我们传递一个延迟承诺的数组。承诺是用于跟踪单个 Ajax 调用状态的对象。通过一组承诺,我们能够使用简单的代码实现复杂的阻塞行为,如下所示:

(function( $, undefined ) {

$.widget( "ab.dialog", $.ui.dialog, {

    options: { 
        promises: []
    },

    open: function( isPromise ) {

        var $element = this.element,
            promises = this.options.promises;

        if ( promises.length > 0 && !isPromise ) {

            $.when.apply( $, promises ).then( function() {
                $element.dialog( "open", true );
            });

        }
        else {

            this._super();

        }

    },

});

})( jQuery );

$(function() {

    var repos = $.ajax({
        url: "https://api.github.com/repositories",
        dataType: "jsonp",
        success: function( resp ) {
            $.each( resp.data, function( i, v ) {
                $( "<option/>" ).html( v.name )
                                .appendTo( "#repos" );
            });
        },
    });

    var users = $.ajax({
        url: "https://api.github.com/users",
        dataType: "jsonp",
        success: function( resp ) {
            $.each( resp.data, function( i, v ) {
                $( "<option/>" ).html( v.login )
                                .appendTo( "#users" );
            });
        }
    });

    $( "#dialog" ).dialog({
        width: 400,
        promises: [
            repos.promise(),
            users.promise()
        ]
    });

});

一旦 API 数据返回,对于这两个调用,对话框将被显示,并且应该看起来像这样:

如何做...

它是如何工作的...

让我们首先看一下文档准备好的处理程序,在这里我们实际上是在实例化对话框小部件。这里定义的前两个变量 reposusers$.Deferred 对象。这代表了我们正在向 GitHub API 发送的两个 API 调用。这些调用的目的是分别填充 #repos#users select 元素。这些 select 元素构成了我们的 #dialog 内容的一部分。在每个 Ajax 调用中指定的 success 选项是一个回调,它执行创建 option 元素并将它们放置在 select 元素中的工作。

如果不自定义对话框小部件,这两个 API 调用将正常工作。对话框将打开,最终,选项将出现在 select 元素中(在对话框已经打开之后)。但是,您会注意到,我们正在向对话框传递一个 deferred.promise() 对象数组。这是我们赋予对话框小部件的新功能。延迟对象简单来说允许开发人员推迟某些可能需要一段时间才能完成的操作的后果,例如 Ajax 调用。承诺是我们从延迟对象中得到的,它允许我们组合一些条件,说出一个复杂的序列,例如进行多个 Ajax 调用,何时完成。

我们已添加到对话框小部件的自定义promises选项是在我们的open()方法的实现中使用的。在这里,我们可以利用这些承诺。基本上,我们正在将一个或多个承诺对象传递给对话框,一旦它们全部完成或解析为使用 jQuery 术语,我们就可以打开对话框。我们通过将承诺对象数组传递给$.when()函数来实现这一点,该函数在对话框上调用open()方法。但是,这里出现了一个我们必须处理的复杂情况。我们无法在回调函数内部调用_super(),因为核心小部件机制不知道如何找到父小部件类。

所以,我们必须假装我们是从小部件外部调用open()。我们通过使用self.element和额外的isPromise参数来做到这一点,指示我们自定义的open()实现如何行为。

在对话框标题中使用图标

对于某些对话框,根据应用程序的性质和对话框本身的内容,可能有益于在对话框标题旁边放置一个图标。这可能有利于用户提供额外的上下文。例如,编辑对话框可能具有铅笔图标,而用户个人资料对话框可能包含人物图标。

准备好了...

为了说明在对话框小部件的标题栏中添加图标,我们将使用以下内容作为我们的基本 HTML:

<div id="dialog" title="Edit">
    <div>
        <label>Field 1:</label>
        <input type="text"/>
    </div>
    <div>
        <label>Field 2:</label>
        <input type="text"/>
    </div>
</div>

如何操作...

我们需要定义的第一件事是一个自定义的 CSS 类,用于在将其放置在对话框标题栏中时正确对齐图标。CSS 如下所示:

.ui-dialog-icon {
    float: left;
    margin-right: 5px;
}

接下来,我们有我们的 JavaScript 代码来通过添加新的icon选项来自定义对话框小部件,以及使用我们的 HTML 作为源代码创建小部件的实例:

(function( $, undefined ) {

$.widget( "ab.dialog", $.ui.dialog, {

    options: {
        icon: false
    },

    _create: function() {

        this._super();

        if ( this.options.icon ) {

            var iconClass = "ui-dialog-icon ui-icon " + 
                            this.options.icon;

            this.uiDialog.find( ".ui-dialog-titlebar" )
                         .prepend( $( "<span/>" ).addClass( iconClass ));

        }

    },

});

})( jQuery );

$(function() {

    $( "#dialog" ).dialog({
        icon: "ui-icon-pencil",
        buttons: {
            Save: function() { $( this ).dialog( "close" ) }
        }
    });

});

打开时产生的对话框应该看起来像下面这样:

如何操作...

它是如何工作的...

对于这个特定的对话框实例,我们想显示铅笔图标。我们已添加到对话框小部件的icon选项允许开发人员从主题框架中指定图标类。在这种情况下,它是ui-icon-pencil。新的icon选项具有默认值false

我们正在覆盖_create()方法的默认对话框实现,以便我们可以在对话框标题栏中插入一个新的span元素,如果提供了icon选项。这个新的span元素得到了作为新选项值传递的图标类,以及ui-dialog-icon类,该类用于定位我们之前定义的图标。

将操作添加到对话框标题

默认情况下,对话框小部件为用户提供了一个不需要开发者干预的操作——标题栏中的关闭按钮。这是一个几乎适用于任何对话框的通用操作,因为用户期望能够关闭它们。此外,关闭对话框操作按钮是一个位于对话框右上角的图标,这并不是偶然的。这是一个标准的位置和动作,在图形窗口环境中以及其他操作中也是如此。让我们看看如何扩展放置在对话框小部件标题栏中的操作。

如何操作...

对于这个演示,我们只需要以下基本的对话框 HTML:

<div id="dialog" title="Dialog Title">
    <p>Basic dialog content</p>
</div>

接下来,我们将实现我们的对话框特化,添加一个新选项和一些创建使用该选项的新对话框实例的代码:

(function( $, undefined ) {

$.widget( "ab.dialog", $.ui.dialog, {

    options: {
        iconButtons: false
    },

    _create: function() {

        this._super();

        var $titlebar = this.uiDialog.find( ".ui-dialog-titlebar" );

        $.each( this.options.iconButtons, function( i, v ) {

            var button = $( "<button/>" ).text( v.text ),
                right = $titlebar.find( "[role='button']:last" )
                                 .css( "right" );

            button.button( { icons: { primary: v.icon }, text: false } )
                  .addClass( "ui-dialog-titlebar-close" )
                  .css( "right", (parseInt(right) + 22) + "px" )
                  .click( v.click )
                  .appendTo( $titlebar );

        });

    }

});

})( jQuery );

$(function() {

    $( "#dialog" ).dialog({
        iconButtons: [
            {
                text: "Search",
                icon: "ui-icon-search",
                click: function( e ) {
                    $( "#dialog" ).html( "<p>Searching...</p>" );
                }
            },
            {
                text: "Add",
                icon: "ui-icon-plusthick",
                click: function( e ) {
                    $( "#dialog" ).html( "<p>Adding...</p>" );
                }
            }
        ]
    });

});

当打开此对话框时,我们将在右上角看到我们传递给对话框的新操作按钮,如下截图所示:

如何操作...

它是如何工作的...

我们为对话框创建了一个名为iconButtons的新选项。这个新选项期望一个对象数组,其中每个对象都有与操作按钮相关的属性。像文本、图标类以及在用户打开对话框并单击按钮时执行的点击事件等。

在这个定制中,大部分工作都是在我们版本的_create()方法中进行的。在这里,我们遍历iconButtons选项中提供的每个按钮。在将新按钮插入标题栏时,我们首先创建button元素。我们还使用.ui-dialog-titlebar [role='button']:last选择器获取最后一个添加的操作按钮的宽度(这是需要计算操作按钮的水平位置的)。

接下来,我们按照按钮配置绑定click事件。对于我们添加的数组中的每个按钮,我们希望它放置在前一个按钮的左侧。因此,当我们首次开始遍历iconButtons数组时,默认的关闭操作是标题栏中的最后一个按钮。由于 CSS 结构需要一个固定的右值,我们必须计算它。为了做到这一点,我们需要列表中最后一个按钮的值。

将效果应用到对话框调整大小交互

默认情况下,对话框小部件允许用户通过拖动调整大小。实际的调整大小功能是由对话框在resizable选项为true时内部设置的resizable()交互小部件提供的。让我们看看如何访问内部可调整大小组件,以便我们可以使用animate特性。当设置在可调整大小组件上时,此选项会延迟重新绘制调整大小的组件,直到用户停止拖动调整大小手柄。

准备工作...

对于这个演示,我们只需要简单的对话框 HTML,如下所示:

<div id="dialog" title="Dialog Title">
    <p>Basic dialog content</p>
</div>

如何操作...

让我们为对话框小部件添加一个名为animateResize的新选项。当此选项为true时,我们将打开内部可调整大小交互小部件的animate选项。

(function( $, undefined ) {

$.widget( "ab.dialog", $.ui.dialog, {

    options: { 
        animateResize: false 
    },

    _makeResizable: function( handles ) {
        handles = (handles === undefined ? this.options.resizable : handles);
        var that = this,
            options = this.options,
            position = this.uiDialog.css( "position" ),
            resizeHandles = typeof handles === 'string' ?
                handles:
                "n,e,s,w,se,sw,ne,nw";

        function filteredUi( ui ) {
            return {
                originalPosition: ui.originalPosition,
                originalSize: ui.originalSize,
                position: ui.position,
                size: ui.size
            };
        }

        this.uiDialog.resizable({
            animate: this.options.animateResize,
            cancel: ".ui-dialog-content",
            containment: "document",
            alsoResize: this.element,
            maxWidth: options.maxWidth,
            maxHeight: options.maxHeight,
            minWidth: options.minWidth,
            minHeight: this._minHeight(),
            handles: resizeHandles,
            start: function( event, ui ) {
                $( this ).addClass( "ui-dialog-resizing" );
                that._trigger( "resizeStart", event, filteredUi( ui ) );
            },
            resize: function( event, ui ) {
                that._trigger( "resize", event, filteredUi( ui ) );
            },
            stop: function( event, ui ) {
                $( this ).removeClass( "ui-dialog-resizing" );
                options.height = $( this ).height();
                options.width = $( this ).width();
                that._trigger( "resizeStop", event, filteredUi( ui ) );
                if ( that.options.modal ) {
                    that.overlay.resize();
                }
             }
        })
        .css( "position", position )
        .find( ".ui-resizable-se" )
        .addClass( "ui-icon ui-icon-grip-diagonal-se" );
    }

});

})( jQuery );

$(function() {

    $( "#dialog" ).dialog({
        animateResize: true
    });

});

当创建并显示此对话框时,您将能够调整对话框的大小,观察到实际的调整现在是动画的。

它是如何工作的...

我们已经向对话框添加了animateResize选项,并为其提供了默认值false。要实际执行此功能,我们已经完全重写了对话框小部件在对话框创建时内部使用的_makeResizable()方法。事实上,我们已经采取了_makeResizable()的内部代码,并仅更改了一件事情——animate: this.options.animateResize

这有点多余,复制所有这些代码来打开一个简单的功能,比如动画化对话框调整交互。事实上,这不是理想的解决方案。一个更好的方法是调用_makeResizable()_super()版本,然后只需通过调用this.uiDialog.resizable( "option", "animate", true )打开动画即可。但在撰写本文时,这不符合预期。尽管我们的替代路径涉及多余的代码,但它只是展示了小部件工厂的灵活性。如果这种动画质量是用户界面的真实要求,我们很快就找到了一个可以忽略的折衷方案。

使用模态对话框进行消息传递

对话框小部件有一个保留的modal选项,用于当我们需要将用户的注意力集中在一件事上时。此选项显示对话框,同时防止用户与其余用户界面进行交互。他们别无选择,只能注意到。不言而喻,模态对话框应该节俭使用,特别是如果您想要用它来向用户广播消息。

让我们看看如何简化对话框以构建一个通用的通知工具在我们的应用程序中。本质上是一个模态对话框,用于那些我们不能让用户继续正在做的事情而不确保他们已经看到我们的消息的情况。

准备就绪...

这是这个示例所需的 HTML 看起来像。请注意,#notify div,它将成为一个对话框小部件,没有内容,因为我们的新通知小部件将提供一些内容。

<div id="notify"></div>

<button id="show-info">Show Info</button>
<button id="show-error">Show Error</button>

如何做...

让我们继续定义一个新的通知小部件,能够向用户显示错误和信息消息,就像这样:

(function( $, undefined ) {

$.widget( "ab.notify", $.ui.dialog, {

    options: { 
        modal: true,
        resizable: false,
        draggable: false,
        minHeight: 100,
        autoOpen: false,
        error: false
    },

    open: function() {

        var error = this.options.error,
            newClass = error ? "ui-state-error" : 
                               "ui-state-highlight",
            oldClass = error ? "ui-state-highlight" :
                               "ui-state-error";

        this.element.html( this.options.text );

        this.uiDialog.addClass( newClass )
                     .removeClass( oldClass )
                     .find( ".ui-dialog-titlebar" )
                     .removeClass( "ui-widget-header ui-corner-all" );

        this._super();

    },

});

})( jQuery );

$(function() {

    $( "#notify" ).notify();

    $( "#show-info, #show-error" ).button();

    $( "#show-info" ).click( function( e ) {

        $( "#notify" ).notify( "option", {
            error: false,
            text: "Successfully completed task"
        });

        $( "#notify" ).notify( "open" );

    });

    $( "#show-error" ).click(function( e ) {

        $( "#notify" ).notify( "option", {
            error: true,
            text: "Failed to complete task"
        });

        $( "#notify" ).notify( "open" );

    })

我们在这里创建的两个按钮用于演示通知小部件的功能。如果您点击#show-info按钮,您将看到以下信息消息:

如何做...

如果您点击#show-error按钮,您将看到此错误消息:

如何做...

它是如何工作的...

我们刚刚创建的notify小部件继承了对话框小部件的所有功能。在我们的小部件中,我们首先定义的是可用选项。在这种情况下,我们扩展了对话框小部件的options对象,并添加了一些新选项。您还会注意到,我们提供了一些更新后的对话框选项的默认值,例如打开modal并关闭draggable。每个 notify 实例都将共享这些默认值,因此没必要每次都要定义它们。

open()方法属于对话框小部件,我们在这里进行了重写,以实现将通知消息的文本插入对话框内容的自定义功能。我们还根据error选项设置对话框的状态。如果这是一个错误消息,我们将整个对话框应用ui-state-error类。如果error选项为false,我们应用ui-state-highlight类。最后,对话框标题栏组件被简化,删除了一些类,因为我们在消息显示中没有使用它。

在应用程序代码中,我们首先创建的是 notify 小部件的实例。然后我们创建了演示按钮,并将click事件绑定到将显示错误消息或信息性消息的功能,具体取决于点击了哪个按钮。

第六章:制作菜单

在本章中,我们将涵盖:

  • 创建可排序的菜单项

  • 高亮显示活动菜单项

  • 使用效果与菜单导航

  • 动态构建菜单

  • 控制子菜单的位置

  • 对子菜单应用主题

介绍

jQuery UI 菜单小部件接受链接列表,并通过处理子菜单中的导航,以及应用主题框架中的类,将它们呈现为一个连贯的菜单。我们可以使用默认提供的选项来定制菜单到一定程度。在其他情况下,例如当我们希望菜单项可排序时,我们可以轻松地扩展小部件。

创建可排序的菜单项

默认情况下,菜单小部件保留用于创建菜单项的列出元素的顺序。这意味着如果在菜单小部件中使用的 HTML 的创建者更改了排序方式,这将反映在渲染的菜单中。这对开发人员来说很好,因为它让我们控制如何向用户呈现项目。但是,也许用户对菜单项的排序有更好的想法。

通过将菜单小部件与sortable 交互小部件相结合,我们可以为用户提供这种灵活性。然而,有了这种新的能力,我们将不得不解决另一个问题;保留用户选择的顺序。如果他们可以按自己的意愿安排菜单项,那就太好了,但是如果他们每次加载页面都必须重复相同的过程,那就不太好了。因此,我们还将看看如何在 cookie 中保存排序后的菜单顺序。

准备工作

让我们使用以下 HTML 代码为我们的菜单小部件。这将创建一个具有四个项目的菜单,所有项目都在同一级别:

<ul id="menu">
    <li id="first"><a href="#">First Item</a></li>
    <li id="second"><a href="#">Second Item</a></li>
    <li id="third"><a href="#">Third Item</a></li>
    <li id="fourth"><a href="#">Fourth Item</a></li>
</ul>

如何做到…

现在让我们看一下用于扩展菜单小部件以提供可排序行为的 JavaScript。

(function( $, undefined ) {

$.widget( "ab.menu", $.ui.menu, {

    options: {
        sortable: false
    },

    _create: function() {

        this._super();

        if ( !this.options.sortable ) {
            return;
        }

        var $element = this.element,
            storedOrder = $.cookie( $element.attr( "id" ) ),
            $items = $element.find( ".ui-menu-item" );
        if ( storedOrder ) {

            storedOrder = storedOrder.split( "," );

            $items = $items.sort( function( a, b ) {

                var a_id = $( a ).attr( "id" ),
                    b_id = $( b ).attr( "id" ),
                    a_index = storedOrder.indexOf( a_id ),
                    b_index = storedOrder.indexOf( b_id );

                return a_index > b_index;

            });

            $items.appendTo( $element );

        }

        $element.sortable({

            update: function( e, ui ) {

                var id = $( this ).attr( "id" ),
                    sortedOrder = $( this ).sortable( "toArray" )
                                           .toString();

                $.cookie( id, sortedOrder );

            }

        });

    },

});

})( jQuery );

$(function() {
    $( "#menu" ).menu( { sortable: true } );
});

如果您在浏览器中查看此菜单,您会注意到您可以将菜单项拖到任何您喜欢的顺序中。此外,如果您刷新页面,您会看到顺序已经被保留了。

如何做到…

工作原理...

在本示例中创建的菜单实例被赋予了一个sortable选项值为true。这是我们添加到菜单小部件的新选项。我们大部分的扩展工作是在我们自己的_create()方法的重新呈现中执行的。我们在这里要做的第一件事是调用方法的原始实现,因为我们希望菜单像往常一样创建;我们通过使用_super()方法来做到这一点。从这里开始,我们将保持菜单项的排序顺序。

如果sortable选项的评估结果不为true,我们将退出,没有任何事情可做。如果此选项为true,且我们需要对菜单项目进行排序,我们尝试加载一个 Cookie,使用此菜单的 ID。此 Cookie 的值存储在一个名为storedOrder的变量中,因为它恰好代表了存储的用户排序。如果用户已经对菜单进行了排序,我们将菜单项目的顺序存储在 Cookie 中。例如,Cookie 值可能类似于second,fourth,first,third。这些是菜单项目的 ID。在我们分割逗号分隔列表时,我们得到了一个以正确顺序排列的菜单项目数组。

最后,我们必须将可排序交互小部件应用于菜单。我们将可排序配置传递给在更新排序顺序时使用的函数。使用可排序小部件的toArray()方法序列化菜单项目的排序顺序,并在此处使用菜单 ID 更新 Cookie 值。

关于此示例中使用 Cookie 有两件事情需要注意。首先,我们使用了 Cookie jQuery 插件。此插件体积小,在互联网上广泛使用。然而,值得一提的是,该插件不随 jQuery 或 jQuery UI 一起发布,您的项目将需要管理此依赖项。

第二个需要注意的事情是关于本地主机域。在所有浏览器中,Cookie 存储功能在本地将无法正常工作。换句话说,通过网络服务器查看时会正常工作。如果您真的需要在 Google Chrome 浏览器中测试此代码,您可以像我一样使用 Python 绕过它。在操作系统控制台中,运行以下代码:

python -m SimpleHTTPServer

高亮显示活动菜单项目

对于菜单小部件,根据项目的配置方式,唯一能判断项目是否激活的方法是页面 URL 由于点击项目而改变。菜单项目不会明显地指示任何实际发生的事情。例如,菜单中的项目一旦被点击,可能会改变可视状态。如果开发人员在用户界面中使用菜单小部件作为导航工具,这将特别有帮助。让我们看看如何扩展菜单小部件的功能,以便使用主题框架的部分提供此功能。

准备就绪

我们将在这里使用以下 HTML 代码作为我们的菜单示例。请注意,此特定菜单具有嵌套子菜单:

<ul id="menu">
    <li><a href="#first">First Item</a></li>
    <li><a href="#second">Second Item</a></li>
    <li><a href="#third">Third Item</a></li>
    <li>
      <a href="#forth">Fourth Item</a>
      <ul>
        <li><a href="#fifth">Fifth</a></li>
        <li><a href="#sixth">Sixth</a></li>
      </ul>
    </li
</ul>

如何做...

为了突出显示活动菜单项目,我们将需要通过一些额外规则扩展主题框架。

.ui-menu .ui-menu-item {
    margin: 1px 0;
    border: 1px solid transparent;
}

.ui-menu .ui-menu-item a.ui-state-highlight { 
    font-weight: normal; 
    margin: -px; 
}

接下来,我们将通过新的highlight选项和必要的功能扩展菜单小部件本身。

(function( $, undefined ) {

$.widget( "ab.menu", $.ui.menu, {

    options: {
      highlight: false
    },

    _create: function() {

      this._super();

        if ( !this.options.highlight ) {
          return;
        }

        this._on({
          "click .ui-menu-item:has(a)": "_click"
        });

    },

    _click: function( e ) {

      this.element.find( ".ui-menu-item a" )
        .removeClass( "ui-state-highlight" );

        $( e.target ).closest( ".ui-menu-item a" )
          .addClass( "ui-state-highlight ui-corner-all" );

    }

});

})( jQuery );

$(function() {
    $( "#menu" ).menu( { highlight: true });
});

如果您查看此菜单,您会注意到一旦选择了一个菜单项目,它会保持高亮状态。

如何做...

工作原理...

我们在这里定义的 CSS 规则是为了使 ui-state-highlight 类在应用于菜单项时能够正常运行。首先,使用 .ui-menu .ui-menu-item 选择器,我们将 margin 设置为在应用 ui-state-highlight 类后适当对齐菜单项的内容。我们还给每个菜单项一个不可见的 border,以防止鼠标进入和鼠标离开事件将菜单项挤出位置。接下来的选择器,.ui-menu .ui-menu-item a.ui-state-highlight,适用于我们将 ui-state-highlight 类应用于菜单项后。这些规则还控制了定位,并防止菜单失去对齐。

切换到 JavaScript 代码,您可以看到我们为菜单部件提供了一个新的 highlight 选项。在我们自定义的 _create() 方法中,我们调用相同方法的原始实现,然后再添加我们的事件处理程序。由 jQuery UI 基础部件定义的 _on() 方法在这里用于将我们的事件处理程序绑定到 click .ui-menu-item:has(a) 事件;这个事件在 menu 部件内部也使用。在这个处理程序内部,我们从任何已经应用 ui-state-highlight 类的菜单项中删除它。最后,我们将 ui-state-highlight 类添加到刚刚点击的菜单项上,还添加了 ui-corner-all 类,该类通过主题属性定义了圆角元素。

使用菜单导航效果

在应用效果到菜单部件时,我们可以采取几种方法。我们在菜单部件中哪些地方可以应用效果?用户将鼠标指针悬停在菜单项上,这会导致状态更改。用户展开子菜单。这两个动作是我们可以通过一些动画来提升视觉效果的主要交互。让我们看看如何使用尽可能少的 JavaScript 来解决这些效果,而不是使用 CSS 过渡。过渡是一个新兴的 CSS 标准,迄今为止,并非所有浏览器都支持它们使用标准语法。然而,按照渐进增强的思路,以这种方式应用 CSS 意味着即使在不支持它的浏览器中,基本的菜单功能也会正常工作。我们可以避免编写大量 JavaScript 来对菜单导航进行动画处理。

准备工作

对于这个示例,我们可以使用任何标准的菜单 HTML 代码。理想情况下,它应该有一个子菜单,这样我们就可以观察到它们展开时应用的过渡效果。

如何做...

首先,让我们定义所需的 CSS 过渡,以便在菜单项和子菜单在状态更改时应用。

.ui-menu-animated > li > ul {
    left: 0;
    transition: left 0.7s ease-out;
    -moz-transition: left .7s ease-out;
    -webkit-transition: left 0.7s ease-out;
    -o-transition: left 0.7s east-out;
}

.ui-menu-animated .ui-menu-item a {
    border-color: transparent;
    transition: font-weight 0.3s,
      color 0.3s,
      background 0.3s,
      border-color 0.5s;
    -moz-transition: font-weight 0.3s,
       color 0.3s,
       background 0.3s,
       border-color 0.5s;
    -webkit-transition: font-weight 0.3s,
       color 0.3s,
       background 0.3s,
       border-color 0.5s;
    -o-transition: font-weight 0.3s,
       color 0.3s,
       background 0.3s,
       border-olor 0.5s;
}

接下来,我们将介绍对菜单部件本身的一些修改,以控制任何给定菜单实例的动画功能。

(function( $, undefined ) {

$.widget( "ab.menu", $.ui.menu, {

    options: {
        animate: false
    },

    _create: function() {

        this._super();

        if ( !this.options.animate ) {
            return;
        }

        this.element.find( ".ui-menu" )
                     .addBack()
                     .addClass( "ui-menu-animated" );

    },

  _close: function( startMenu ) {

        this._super( startMenu );

        if ( !this.options.animate ) {
            return;
        }

        if ( !startMenu ) {
            startMenu = this.active ? this.active.parent() : this.element;
        }

        startMenu.find( ".ui-menu" ).css( "left", "" );

          }

});

})( jQuery );

$(function() {
    $( "#menu" ).menu( { animate: true } );
});

现在,如果你在浏览器中查看这个菜单并开始与它交互,你会注意到应用悬停状态时的平滑过渡。你也会注意到,展开子菜单时,应用的过渡似乎将它们向右滑动。

它是如何工作的...

首先,让我们考虑一下定义了我们所看到应用到menu部件的过渡的 CSS 规则。.ui-menu-animated > li > ul选择器将过渡应用到子菜单上。声明的第一个属性left: 0只是一个初始化程序,允许某些浏览器更好地与过渡配合。接下来的四行定义了左属性的过渡。菜单部件在展开子菜单时,使用的是位置实用程序部件,它在子菜单上设置了leftCSS 属性。我们在这里定义的过渡将在0.7秒的时间跨度内对left属性进行更改,并且会在过渡结束时减缓。

我们有多个过渡定义的原因是一些浏览器支持它们自己的供应商前缀版本的规则。因此,我们从通用版本开始,然后是特定于浏览器的版本。这是一个常见的做法,当浏览器特定的规则变得多余时,我们可以将其删除。

接下来是.ui-menu-animated .ui-menu-item a选择器,适用于每个菜单项。你可以看到这里的过渡涉及几个属性。在这个过渡中,每个属性都是ui-state-hover的一部分,我们希望它们被动画化。由于我们的调整,border-color过渡的持续时间稍长。

现在让我们看看将这个 CSS 运用到 JavaScript 的方法。我们通过添加一个新的animate选项来扩展菜单部件,该选项将上述定义的过渡应用到部件上。在我们的_create()方法版本中,我们调用了原始的_create()实现,然后将ui-menu-animated类应用到主ul元素和任何子菜单上。

延伸_close()方法的原因只有一个。这是在关闭子菜单时调用的。然而,当首次显示子菜单时,left CSS 属性是由position实用程序计算的。下一次显示时,它不必计算left属性。这是一个问题,因为很明显,如果我们尝试对left属性值进行动画更改,这会成为显而易见的问题。因此,我们只需要在关闭菜单时将left属性设置回0

动态构建菜单

经常情况下,菜单在与用户交互时会发生变化。换句话说,我们可能需要在菜单实例化后扩展菜单的结构。或者在构建最终成为菜单部件的 HTML 时,可能并没有所有必要的信息可用;例如,菜单数据可能只以JavaScript 对象表示法JSON)格式可用。让我们看看如何动态构建菜单。

准备

我们将从以下基本菜单 HTML 结构开始。我们的 JavaScript 代码将扩展这个结构。

<ul id="menu">
    <li><a href="#">First Item</a></li>
    <li><a href="#">Second Item</a></li>
    <li><a href="#">Third Item</a></li>
</ul>

如何做...

让我们创建菜单小部件,然后我们将扩展菜单 DOM 结构。

$(function() {

    var $menu = $( "#menu" ).menu(),
        $submenu = $( "<li><ul></ul></li>" ).appendTo( $menu );

    $submenu.prepend( $( "<a/>" ).attr( "href", "#" )
                                 .text( "Fourth Item" ) );

    $submenu.find( "ul" ).append( 
$( "<li><a href='#'>Fifth Item</a>" ) )
                                      .append( $( "<li><a href='#'>Sixth Item</a>" ) );

    $menu.menu( "refresh" );

});

当您查看这个菜单时,不再只有我们最初的三个项目,而是现在呈现了我们刚刚添加的三个新项目。

如何做...

工作原理是什么...

如果我们在 JavaScript 代码中不断添加新的菜单项,我们只会看到最初的三个项目。但是,我们正在使用核心 jQuery DOM 操纵工具来构建和插入一个子菜单。之后,我们必须调用 refresh() 菜单方法,它会为新的菜单项添加适当的 CSS 类和事件处理程序。例如,如果我们将 DOM 插入代码移到 menu 小部件被实例化之前,则没有理由调用 refresh(),因为菜单构造函数会直接调用它。

还有更多...

上述方法在菜单中插入新项目确实有其缺点。一个明显的缺点是实际构建新菜单项和子菜单的 DOM 插入代码不易维护。我们的示例已经将结构硬编码了,而大多数应用程序通常不这样做。相反,我们通常至少有一个数据源,可能来自 API。如果我们可以传递给菜单小部件一个标准格式的数据源,那就太好了。菜单小部件将负责我们上面实现的底层细节。

让我们尝试修改代码,以便更多的责任移到菜单小部件本身。我们将以与上面的代码完全相同的结果为目标,但我们将通过扩展菜单小部件,并传入代表菜单结构的数据对象来实现。我们将使用完全相同的 HTML 结构。以下是新的 JavaScript 代码:

(function( $, undefined ) {

$.widget( "ab.menu", $.ui.menu, {

    options: {
        data: false
    },

    _create: function() {

        this._super();

        if ( !this.options.data ) {
            return;
        }

        var self = this;

        $.each( this.options.data, function( i, v ) {
            self._insertItem( v, self.element );
        });

        this.refresh();

    },

    _insertItem: function( item, parent ) {

        var $li = $( "<li/>" ).appendTo( parent );

        $( "<a/>" ).attr( "id", item.id )
                   .attr( "href", item.href )
                   .text( item.text )
                   .appendTo( $li );

        if ( item.data ) {

            var $ul = $( "<ul/>" ).appendTo( $li ),
                self = this;

            $.each( item.data, function( i, v ) {
                self._insertItem( v, $ul );
            });

        }

    }

});

})( jQuery );

$(function() {

    $( "#menu" ).menu({
        data: [
            {
                id: "fourth",
                href: "#",
                text: "Fourth Item"
            },
            {
                id: "fifth",
                href: "#",
                text: "Fifth Item",
                data: [
                    {
                        id: "sixth",
                        href: "#",
                        text: "Sixth Item"
                    },
                    {
                        id: "seventh",
                        href: "#",
                        text: "Seventh Item"
                    }
                ]
            }
        ]
    });

});

如果您运行这段修改后的代码,您会发现结果与我们上面编写的原始代码没有任何变化。这种改进纯粹是一种重构,将难以维护的代码变成了更长寿的东西。

我们在这里引入的新选项 data 期望一个菜单项数组。该项是一个带有以下属性的对象:

  • id:它是菜单项的 id

  • href:它是菜单项链接的 href

  • text:它是项目标签的项

  • data:它是一个嵌套的子菜单

最后一个选项只是表示子菜单的菜单项嵌套数组。我们对 _create() 方法的修改将遍历数据选项数组(如果提供),并在每个对象上调用 _insertItem()_insertItem() 方法是我们引入的新东西,并不会覆盖任何现有的菜单功能。在这里,我们正在为传入的菜单数据创建必要的 DOM 元素。如果这个对象有一个嵌套的数据数组,也就是子菜单,那么我们会创建一个 ul 元素,并递归调用 _inserItem(),将 ul 作为父元素传递进去。

我们传递给菜单的数据比以前的版本更易读和可维护。 例如,现在传递 API 数据所需的工作相对较少。

控制子菜单的位置

菜单小部件使用位置小部件来控制任何子菜单在可见时的目的地。 默认情况下,将子菜单的左上角放置在展开子菜单的菜单项的右侧。 但是,根据我们的菜单大小、子菜单的深度和 UI 中围绕大小的其他约束,我们可能希望使用不同的默认值来设置子菜单的位置。

准备工作

我们将使用以下 HTML 结构来进行子菜单定位演示:

<ul id="menu">
            <li><a href="#first">First Item</a></li>
            <li><a href="#second">Second Item</a></li>
            <li><a href="#third">Third Item</a></li>
            <li>
              <a href="#forth">Fourth Item</a>
              <ul>
                <li><a href="#fifth">Fifth</a></li>
                <li>
                  <a href="#sixth">Sixth</a>
                  <ul>
                    <li><a href="#">Seventh</a></li>
                    <li><a href="#">Eighth</a></li>
                    </ul>
                  </li>
                </ul>
            </li>
        </ul

如何做...

当我们实例化此菜单时,我们将传递一个position选项,如下所示:

<ul id="menu">
            <li><a href="#first">First Item</a></li>
            <li><a href="#second">Second Item</a></li>
            <li><a href="#third">Third Item</a></li>
            <li>
                <a href="#forth">Fourth Item</a>
                <ul>
                    <li><a href="#fifth">Fifth</a></li>
                    <li>
                        <a href="#sixth">Sixth</a>
                        <ul>
                            <li><a href="#">Seventh</a></li>
                            <li><a href="#">Eighth</a></li>
                        </ul>
                    </li>
                </ul>
            </li>
        </ul>

当所有子菜单展开时,我们的菜单将与下图所示类似:

如何做...

如何运作...

在前面的示例中,我们向菜单小部件传递的position选项与我们直接传递给位置小部件的选项相同。 位置小部件期望的of选项是活动菜单项或子菜单的父项。 所有这些选项都传递给_open()方法中的位置小部件,该方法负责展开子菜单。

将主题应用于子菜单

当菜单小部件显示子菜单时,外观上没有明显的区别。 也就是说,在视觉上,它们看起来就像是主菜单。 我们希望向用户展示主菜单和其子菜单之间的一点对比;我们可以通过扩展小部件以允许将自定义类应用于子菜单来实现这一点。

准备工作

让我们使用以下标记来创建带有几个子菜单的菜单小部件:

<ul id="menu">
            <li><a href="#">First Item</a></li>
            <li><a href="#">Second Item</a></li>
            <li><a href="#">Third Item</a></li>
            <li>
                <a href="#">Fourth Item</a>
                <ul>
                    <li><a href="#">Fifth</a></li>
                    <li>
                        <a href="#">Sixth</a>
                        <ul>
                            <li><a href="#">Seventh</a></li>
                            <li><a href="#">Eighth</a></li>
                        </ul>
                    </li>
                </ul>
            </li>
        </ul>

如何做...

我们将通过添加一个新的submenuClass选项并将该类应用于子菜单来扩展菜单小部件,如下所示:

(function( $, undefined ) {

$.widget( "ab.menu", $.ui.menu, {

    options: {
      submenuClass: false
    },

    refresh: function() {

      if ( this.options.submenuClass ) {

        this.element.find( this.options.menus + ":not(.ui-menu)" )
          .addClass( this.options.submenuClass );

        }

        this._super();

    }

});

})( jQuery );

$(function() {
    $( "#menu" ).menu( { submenuClass: "ui-state-highlight } );
});

下面是子菜单的外观:

如何做...

如何运作...

在这里,我们使用一个新的submenuClass选项扩展了菜单小部件。 我们的想法是,如果提供了这个类,我们只想将它应用于小部件的子菜单。 我们通过重写refresh()菜单方法来实现这一点。 我们查找所有子菜单并将submenuClass应用于它们。 您会注意到,在调用原始实现此方法的_super()方法之前,我们应用了这个类。 这是因为我们正在寻找尚未具有ui-menu类的菜单。 这些是我们的子菜单。

第七章:进度条

在本章中,我们将涵盖以下主题:

  • 显示文件上传进度

  • 动画化进度变化

  • 创建进度指示器小部件

  • 使用状态警告阈值

  • 给进度条添加标签

介绍

progressbar 小部件相当简单——因为它没有太多的移动部分。事实上,它只有一个移动部分,即值栏。但是简单并不意味着进度条比其他小部件功能更弱。我们将看看如何在本章中利用这种简单性。进度条可以表达从文件上传进度到服务器端进程再到容量利用率的任何内容。

显示文件上传进度

如果有一种简单直接的方法可以使用进度条小部件显示文件上传的进度就好了。不幸的是,我们没有这样的奢侈。文件的上传发生在页面转换之间。然而,使用进度条小部件显示上传进度所需的必要技巧,由于现代标准和浏览器的发展,已经变得更加简洁。让我们看看如何利用Ajax请求中 XML HTTP 请求对象的 onprogress 事件。

准备工作

为了演示,我们将创建一个带有简单文件字段的简单表单。在表单内部,我们将创建一些用于显示进度条小部件的 HTML。它将在用户启动文件上传之前被隐藏。

<form action="http://127.0.0.1:8000/" method="POST">
    <input type="file" name="fileupload"/>
    <br/>
    <input type="submit" value="Upload"/>
    <div id="upload-container" class="ui-helper-hidden">
        <strong id="upload-value">Uploading...</strong>
        <div id="upload-progress"></div>
    </div>
</form>

操作方法...

更新文件上传过程中更新进度条小部件所需的大部分工作实际上是在 Ajax 请求机制和 onprogress 事件处理程序中完成的。以下代码很好地说明了为什么小部件设计者应该以简单为目标。生成的小部件适用于各种情境。

$(function() {

    $( "#upload-progress" ).progressbar();

    $( "form" ).submit( function( e ) {

        e.preventDefault();

        $.ajax({
            url: $( this ).attr("action"),
            type: "POST",
            data: new FormData( this ), 
            cache: false,
            contentType: false,
            processData: false,
            xhr: function() {

                xhr = $.ajaxSettings.xhr();

                if ( xhr.upload ) {
                    xhr.upload.onprogress = onprogress;
                }

                return xhr;

            }

        });

        return false;

    });

    var onprogress = function( e ) {

        var uploadPercent = ( e.loaded / e.total * 100 ).toFixed();

        $( "#upload-container" ).show();
        $( "#upload-value" ).text( "Uploading..." + uploadPercent + "%" );
        $( "#upload-progress" ).progressbar( "option", "max", e.total )
                               .progressbar( "value", e.loaded );

    }; 

});

如果您运行此示例并在本地上传文件到 http://127.0.0.1: 8000/,您会希望使用一个较大的文件。较小的文件上传速度太快,时间太短。较大的文件上传将使您能够在上传过程中看到以下内容。

操作方法...

注意

本书中的代码附带了一个最小的 Python 服务器,用于提供此演示上传页面并处理文件上传请求。该示例可以很容易地重新排列以与任何上传服务器配合使用,但是提供的 Python 服务器只需要安装 Python 即可。再次强调,这不是一个要求,但如果您渴望看到客户端代码运行的话,这只是一个方便的服务器。

工作原理...

该示例的目标是实时更新进度条小部件,随着文件上传进度的改变而改变。有几个插件可以提供这种功能,但如果您正在编写 jQuery UI 应用程序,最好统一使用进度条小部件。一旦文档准备就绪,我们首先创建用于显示文件上传进度的进度条小部件。 #upload-container 最初使用ui-helper-hidden类隐藏,因为我们不需要在上传正在进行之前显示上传进度。

接下来,我们设置我们上传表单的submit事件的事件处理程序。在执行任何其他操作之前,此处理程序防止默认表单提交。本质上,我们用我们自己的行为替换了浏览器实现的默认表单提交。我们需要覆盖此行为的原因是为了留在页面上,并对我们的进度条小部件应用更新。

接下来,我们设置实际将我们选定的文件发送到服务器的$.ajax()调用。我们从表单本身获取url参数。接下来的几个参数是发送多部分表单数据的先决条件,包括作为 Ajax 请求的一部分的选定文件。 xhr 选项是我们提供返回xhr对象的函数,内部由$.ajax()函数使用。这是我们截取xhr对象并附加其他行为的机会。我们主要感兴趣的是向onprogress事件添加新行为。

确保上传对象XMLHttpRequestUpload实际存在后,我们可以定义我们的onprogress事件处理程序函数。

首先,我们使用事件的loadedtotal属性计算实际上传百分比。接下来,我们显示进度容器,并使用uploadPercent中的值更新百分比标签。最后,我们确保上传进度条小部件的max选项设置为total,并使用value()方法设置进度条的当前值。

动画化进度变化

进度条小部件在设置valuemax选项时会改变其视觉外观。例如, value 的默认值为0max 的默认值为100。因此,当以这些值显示进度条小部件时,我们实际上并不看到图形化的条,然而这表示了进度百分比。但是,设置value选项将更新此条。如果条已经可见,则value选项的更改会导致进度条的宽度改变。使用默认进度条实现,这些更改会立即改变小部件。让我们看看如何修改小部件以支持进度条值之间的平滑过渡。

如何做...

我们将使用以下简单的标记作为我们进度条小部件实例的基础:

<div id="progress"></div>

这里是用于定制进度条小部件以支持动画更改进度的 JavaScript 代码:

(function( $, undefined ) {

$.widget( "ab.progressbar", $.ui.progressbar, {

    options: {
        animate: false
    },

    _refreshValue: function() {

        if ( !this.options.animate ) {
            return this._super();
        }

        var value = this.value(),
            percentage = this._percentage();

        if ( this.oldValue !== value ) {
            this.oldValue = value;
            this._trigger( "change" );
        }

        this.valueDiv.toggle( value > this.min )               .toggleClass( "ui-corner-right",
value === this.options.max )
                             .stop( true, true )
                             .animate( { width: percentage.toFixed( 0 ) + "%" }, 200 );

              this.element.attr( "aria-valuenow", value );

    }

});

})( jQuery );

$(function() {

    $( "#progress" ).progressbar( { animate: true } );

    var timer;

    var updater = function() {

        var value = $( "#progress" ).progressbar( "value" ) + 10,
            maximum = $( "#progress" ).progressbar( "option", "max" );

        if ( value >= maximum ) {
            $( "#progress" ).progressbar( "value", maximum );
            return;
        }

        $( "#progress" ).progressbar( "value", value );
        timer = setTimeout( updater, 700 );

    };

    timer = setTimeout( updater, 700 );

});

此示例包括一个更新器,将每 0.7 秒的间隔递增进度条值。您会注意到随着值的变化应用的平滑宽度过渡。与默认行为相比较,将animate选项设置为false,您现在将真正注意到每次更新值时进度条所做的视觉跳跃。

它是如何工作的...

我们的示例代码通过添加一个新的animate选项来扩展进度条小部件。新的animate选项默认为false。我们向进度条小部件引入的另一个更改是_refreshValue()方法的新实现,该方法在value选项更改时由小部件内部调用。此方法负责使div元素progress上的可视宽度发生变化。这代表了valuemax之间的进度。

很多这段代码都是从_refreshValue()的原始实现中借鉴而来的,因为我们只做了些微的修改。首先,我们检查了我们添加到小部件中的animate选项是否为true值。如果不是,则我们继续使用原始实现。否则,我们使用相同的代码,但对应用宽度的方式进行了轻微调整。然后,我们调用stop(true, true)来完成当前动画并清除动画队列。接下来,我们不再像原始实现那样使用width()函数,而是通过调用animate()来设置宽度。

这还不是全部...

与往常一样,我们不局限于使用 jQuery 的animate()函数来对进度条值之间的视觉过渡应用效果。除了animate()函数之外,我们还可以将 CSS 过渡应用于进度条值。当然,缺点是并非所有浏览器都支持 CSS 过渡,并且我们涉及到特定于供应商的样式规则。尽管如此,让我们将先前的方法与使用 CSS 样式来动画进度条进行比较。

我们将使用相同的标记,但我们将向页面引入以下样式:

.ui-progressbar-animated > .ui-progressbar-value {
    transition: width 0.7s ease-out;
    -moz-transition: width .7s ease-out;
    -webkit-transition: width 0.7s ease-out;
    -o-transition: width 0.7s east-out;
}

这里是 JavaScript 代码的必要更改。它看起来与之前的代码类似。

(function( $, undefined ) {

$.widget( "ab.progressbar", $.ui.progressbar, {

    options: {
        animate: false
    },

    _create: function() {

        this._super();

        if ( !this.options.animate ) {
            return;
        }

        this.element.addClass( "ui-progressbar-animated" );

    }

});

})( jQuery );

$(function() {

    $( "#progress" ).progressbar( { animate: true } );

    var timer;

    var updater = function() {

        var value = $( "#progress" ).progressbar( "value" ) + 10,
            maximum = $( "#progress" ).progressbar( "option", "max" );

        if ( value >= maximum ) {
            $( "#progress" ).progressbar( "value", maximum );
            return;
        }

        $( "#progress" ).progressbar( "value", value );
        timer = setTimeout( updater, 700 );

    };

    timer = setTimeout( updater, 700 );

});

运行此示例将与先前的animate选项实现看起来并无太大不同。过渡行为将基本相同。这里的关键区别在于我们正在扩展主题框架。我们为进度条小部件引入了一个新的 CSS 类——ui-progressbar-animated。选择器.ui-progressbar-animated > .ui-progressbar-value,适用于进度条值div,即宽度发生变化的元素。而我们的新样式正是如此。它们在 0.7 秒的时间段内过渡宽度属性值的变化。

这种方法的主要受益者是 JavaScript 代码,因为进度条小部件的变化较少。例如,我们不再覆盖_refreshValue()方法。相反,我们正在覆盖_create()方法,并且如果animated选项为true,则在元素中添加ui-progressbar-animated类。这是我们新样式如何生效的方式。其余实例化小部件和值更新器的 JavaScript 与前一个示例没有任何不同。

创建进度指示器小部件

进度条小部件旨在显示某个过程的进度。最终目标是在创建小部件时指定的max选项,默认为100。如果我们事先知道正在处理的数据的大小,我们将使用max选项来反映此最终目标。但是,有时我们面临的情况是在客户端执行一些处理;或者,我们正在等待某个后端进程完成并将响应发送回客户端。例如,用户使用 API 启动了后端任务,现在他们正在等待响应。关键是,我们希望向用户说明正在进行进度,而不知道已经完成了多少进度。

为了显示进度正在进行,尽管不知道有多少进度,我们需要一个指示器小部件。我们可以编写自己的小部件来实现这一点,扩展进度条小部件,因为我们可以在那里重用许多组件。

如何做…

对于我们的进度指示器小部件,我们将使用与基本进度条小部件相同的 HTML。

<div id="indicator"></div>

接下来,我们需要对进度条的 CSS 样式进行一些轻微的调整。这些应用于进度条div内部的值栏。我们去掉了bordermargin,因为在来回滑动值栏时这样看起来更好。

.ui-progressbar > .ui-progressbar-value {
    border: none;
    margin: 0px;
}

现在,我们来实现进度指示器小部件。此代码还将创建我们的进度指示器小部件的实例。

(function( $, undefined ) {

$.widget( "ab.progressindicator", $.ui.progressbar, {

    _create: function() {

        this._super();
        this.value( 40 );
        this.element.removeClass( "ui-corner-all" );
        this.valueDiv.removeClass( "ui-corner-right ui-corner-left" );

        var self = this,
            margin = ( this.element.innerWidth() - this.valueDiv.width() ) + "px";

        var _right = function() {

            self.valueDiv.animate(
                { "margin-left": margin },
                { duration: 1000, complete: _left }
            );

        };

        var _left = function() {

            self.valueDiv.animate(
                { "margin-left": "0px" },
                { duration: 1000, complete: _right }
            );

        };

        _right();

    },

    _destroy: function() {

        this.valueDiv.stop( true, true );
        this._super();

    }

});

})( jQuery );

$(function() {

    $( "#indicator" ).progressindicator();

});

如果您在浏览器中查看此进度指示器小部件,您将看到它通过来回滑动进度条小部件的值栏来进行动画处理,表示正在发生某事。

如何做…

它的工作原理…

我们创建了一个新的进度指示器小部件,继承了进度条小部件的功能。进度指示器小部件的目标是获取进度值栏div,在其中设置宽度,并在进度条容器div内滑动。视觉上,这表示幕后正在发生某事。这种图形化描述活动是对用户普遍令人放心的,因为它给人一种正在发生某事的感觉,并且应用程序没有崩溃。

在新进度指示器小部件的定义中,我们要重写的第一个方法是进度条的_create()方法。在这里,我们调用进度条小部件的原始构造函数,因为我们在开始进行更改之前需要所有的 UI 组件就位。接下来,我们使用value()方法为值条div设置宽度。我们在progressindicator()构造函数中硬编码了此值,只是因为使用此小部件的开发人员没有必要更改它;我们只需要设置元素的宽度。为了进一步简化此小部件,我们从元素中删除了角类。我们可以留下它们,但是在动画条时我们将不得不处理几种角例,因为我们追求的是一个简单的小部件,一个不需要开发人员进行配置的小部件。

仍然在_create()方法内部,我们定义了两个用于执行动画的实用函数。正如你可能猜到的那样,_right()函数将进度值条向右滑动,而_left()函数将其向左滑动。我们在该小部件的valueDiv属性上调用了animate()jQuery 函数。_right()函数通过更新margin-left值将值div向右滑动。您会注意到,margin变量在_create()内部局部定义。这是通过计算我们在值div右侧有多少空间来完成的,这意味着我们将此值设置为margin-left以将其向右滑动。要再次将其向左滑动,我们只需在_left()函数中将margin-left CSS 属性设置回0px

通过在_create()方法的底部调用_right()来引导动画。通过将_left()作为初始动画的回调传递,进度指示器动画循环发生。同样,在_left()函数内部将_right()作为动画完成回调传递。此过程将继续直到小部件被销毁。我们的小部件重写了_destroy()方法,只是为了确保所有动画立即停止。这包括任何等待执行的排队动画。然后,我们通过调用原始的_destroy()实现来继续销毁小部件。

还有更多...

我们的进度指示器小部件的一个优点是它提供了一个非常简单的 API。您可以根据需要创建和销毁小部件,而无需处理任何中间步骤。理想情况下,这个小部件的寿命会非常短,可能只有一秒钟(刚好足够看到一个动画循环)。然而,有时候可能需要更长一点。如果这个小部件要长时间显示,它可能会对应用程序造成问题。jQuery 的animate()函数并不是设计成无限循环运行动画的。我们的小部件也不是设计成长时间显示的。问题在于animate()使用计时器,可能会大幅消耗客户端的 CPU 周期。这不仅可能对我们的应用程序造成破坏,还可能对在用户机器上运行的其他应用程序造成影响。

尽管这是一个相对较小的问题,让我们来看看我们的进度指示器小部件的另一种实现方式,即使用 CSS 动画。以下是我们如何在 CSS 中定义动画的方式:

.ui-progressindicator > .ui-progressbar-value {
    border: none;
    margin: 0px;
    animation: indicator 2s ease-in-out infinite;
    -moz-animation: indicator 2s ease-in-out infinite;
    -webkit-animation: indicator 2s ease-in-out infinite;
}

@keyframes indicator {
    0%   { margin-left: 0px; }
    50%  { margin-left: 108px; }
    100% { margin-left: 0px; }
}

@-moz-keyframes indicator {
    0%   { margin-left: 0px; }
    50%  { margin-left: 108px; }
    100% { margin-left: 0px; }
}

@-webkit-keyframes indicator {
    0%   { margin-left: 0px; }
    50%  { margin-left: 108px; }
    100% { margin-left: 0px; }
}

@-o-keyframes indicator {
    0%   { margin-left: 0px; }
    50%  { margin-left: 108px; }
    100% { margin-left: 0px; }
}

并且,这是我们的progressindicator小部件的修改后的 JavaScript 实现,它知道如何利用先前的 CSS:

(function( $, undefined ) {

$.widget( "ab.progressindicator", $.ui.progressbar, {

  _create: function() {

        this._super();
        this.value( 40 );
        this.element.addClass( "ui-progressindicator" )
                    .removeClass( "ui-corner-all" );
        this.valueDiv.removeClass( "ui-corner-right ui-corner-left" );

    },

    _destroy: function() {

        this.element.removeClass( "ui-progressindicator" );
        this._super();

    }

});

})( jQuery );

$(function() {

    $( "#indicator" ).progressindicator();

});

现在,如果你在浏览器中查看这个小部件的修改版本,你应该会发现与以前的实现相比几乎完全一样的结果。当然,关键的区别在于动画是在 CSS 中指定并直接由浏览器执行。与基于 JavaScript 的对应物相比,浏览器可以更有效地处理这些类型的 CSS 动画。浏览器只需要一次读取动画规范,然后在内部运行动画,使用本机代码而不是执行 JavaScript 并直接操作 DOM。我们可以让这个版本运行一整天,浏览器会愉快地继续运行。

但是这个版本的进度指示器并不是没有缺点的。首先,让我们仔细看看 CSS。事实上,我们依赖 CSS 动画本身并不是最好的选择,因为不同浏览器对其支持存在差异。在这里,通过我们的样式,我们将自己陷入了浏览器厂商前缀混乱的困境。总的来说,支持还不错,因为只有 IE 不支持 CSS 动画;但是动画的定义很直接。在.ui-progressindicator > .ui-progressbar-value选择器中,我们指定了指示器动画将运行2秒,并且会无限重复。@keyframes指示器动画指定了margin-left属性本身的变化方式。

在 JavaScript 中,你会注意到代码本身要简单得多。这是因为它现在负责的事情要少得多。主要是,在创建时需要将 ui-progressindicator 类添加到小部件的 DOM 元素上,并在销毁时删除该类。你还会注意到,在实现小部件的 JavaScript 代码中不再进行边距计算。相反,我们将这些数字移到了定义小部件动画的 CSS 中作为硬编码值。再次强调,这只是小部件设计者必须考虑的一个权衡。我们在 CSS 中交换了更高的维护成本以获得更高效的动画,并为我们的小部件提供了更简单的 JavaScript,以牺牲可疑的浏览器支持。

使用状态来警告阈值

进度条小部件不仅限于标记朝某个结束点的进展。它还可以用作某些资源利用的标记。例如,你的应用程序可能允许用户存储 100 MB 的图像数据。显示当前使用了多少容量可能是有意义的。进度条小部件是图形化显示此类资源利用情况的理想解决方案。更进一步,我们可能还希望警告用户关于使用阈值。也就是说,在某个百分比下,资源接近容量,但用户仍然有时间对此做出反应。

准备工作

为了演示,我们将为要显示的两个进度条小部件创建两个简单的 div 元素:

<span>CPU:</span>
<div id="cpu-utilization"></div>
<span>Memory:</span>
<div id="memory-utilization"></div>

如何做...

下面是扩展进度条小部件的 JavaScript 代码,提供了一个新的选项来指定阈值:

(function( $, undefined ) {

$.widget( "ab.progressbar", $.ui.progressbar, {

    options: {
        threshold: 0
    },

  _percentage: function() {

        var percentage = this._super(),
            threshold = this.options.threshold;

        if ( threshold <= 0 ) {
            return percentage;
        }

        if ( percentage > threshold ) {
            this.valueDiv.addClass( "ui-state-error" );
        }
        else {
            this.valueDiv.removeClass( "ui-state-error" );
        }

        return percentage;

  },

});

})( jQuery );

$(function() {

    $( "#cpu-utilization" ).progressbar( { threshold: 80 } );
    $( "#memory-utilization" ).progressbar( { threshold: 85 } );

    setInterval(function() {
        var cpu = Math.floor( ( Math.random() * 100 ) + 1 ),
            memory = Math.floor( ( Math.random() * 100 ) +1 );

        $( "#cpu-utilization" ).progressbar( "value", cpu );
        $( "#memory-utilization" ).progressbar( "value", memory );

    }, 1300);

});

我们在这里实例化了两个进度条小部件,并启动了一个基本的定时器间隔,每 1.30 秒更改一次两个进度条小部件的值。如果你在浏览器中查看此示例,你会注意到一个或两个进度条小部件将进入错误状态,因为值已超过提供的阈值。

如何做...

工作原理...

我们添加到进度条小部件的新 threshold 选项是一个以百分比表示的数字。这是进度条的阈值,在这个阈值上,状态会改变以向用户发出视觉警告。这是通过重写 _percentage() 方法来实现的。在这里,我们通过调用 _percentage() 的原始实现并将其存储在 percentage 中来获得实际的百分比值。然后,我们确保 threshold 值非零,并且计算出的百分比大于 threshold 值。每次更新值时,进度条小部件都会内部调用 _percentage() 方法,并且视觉显示会发生变化。因此,在我们的 _percentage() 实现中,如果超过阈值,我们将 ui-state-error 类添加到 valueDiv 元素中,该元素是进度条内部移动的图形条。否则,我们低于阈值,并且必须确保删除 ui-state-error 类。

一旦我们创建了两个小部件,我们就使用 setInterval() 不断为两个进度条分配一个随机值。您可以坐下来观看进度条小部件如何根据输入的数据是否跨越我们指定的阈值而改变状态。在这种情况下,#cpu-utilization 进度条的阈值为 80%,而 #memory-utilization 进度条的阈值为 85%

给进度条添加标签

反映进度百分比变化宽度的图形条表现得很好。进度条小部件的强大之处在于一眼就能看到已经完成了多少进度,或者正在利用多少资源。但有时候我们可能需要一些关于百分比的准确度,即显示底层百分比的标签。

进度条小部件具有在进度条容器内显示标签的功能,这比在小部件外部显示百分比标签更直观。让我们看看如何扩展主题 CSS,为小部件提供额外的标记,并扩展进度条以利用这些新的附加功能来显示标签。

如何操作...

我们首先为我们的两个进度条小部件创建 HTML。

<span>Network:</span>
<div id="network-utilization">
    <div class="ui-progressbar-label"></div>
</div>
<span>Storage:</span>
<div id="storage-utilization">
    <div class="ui-progressbar-label"></div>
</div>

接下来,我们将添加进度条标签所需的 CSS 类。

.ui-progressbar-label {
    float: left;
    width: 100%;
    text-align: center;
    margin-top: 5px;
    font-weight: bold;
}

最后,我们将扩展进度条小部件本身,将这个新的 HTML 和新的 CSS 绑定在一起。

(function( $, undefined ) {

$.widget( "ab.progressbar", $.ui.progressbar, {

    _create: function() {
        this.$label = this.element.find( ".ui-progressbar-label" );
        this._super();

    },

    _destroy: function() {

        this.$label.remove();

        this._super();

    },

  _refreshValue: function() {
        this.$label.text( this._percentage().toFixed( 0 ) + "%" );
        this._super();

  },

});

})( jQuery );

$(function() {

    $( "#network-utilization" ).progressbar({
        value: 746586112,
        max: 1073741824
    });

    $( "#storage-utilization" ).progressbar({
        value: 24696061952,
        max: 107374182400
    });

});

您现在可以在浏览器中查看这两个进度条,您会注意到两个标签显示百分比值的位置位于小部件的中心。

如何操作...

它是如何工作的...

默认情况下,进度条小部件不支持标签,因此我们必须将标签 div 放在进度条 div 中。我们还给这个新的标签 div 添加了 ui-progressbar-label 类,这与 jQuery UI 主题命名规范一致。这个类实际上有两个作用:在我们引入的小部件自定义中,我们使用这个类来搜索标签 div 并应用标签样式。

ui-progressbar-label 中指定的 CSS 规则有助于将标签文本定位在进度条元素的中间。我们给标签 div 一个宽度为 100%,并使用 text-align 属性水平对齐文本。最后,我们使标签的 font-weightbold 以使其突出显示;否则,在进度条的背景下很难看到它。

我们在这里介绍的进度条小部件的自定义 JavaScript 实现覆盖了 _create() 方法。我们创建了一个称为 labelDiv 的新实例变量,它存储对我们新元素的引用。然后我们调用原始的 _create() 实现,构造函数继续正常进行,创建我们的新标签元素旁边的值 div。我们还重写了 _refreshValue() 方法以更新 labelDiv 的内容。_refreshValue() 方法在任何时候内部被小部件调用,当值改变并且进度条小部件需要更新值显示时,会更新 labelDiv 的值。我们通过在恢复 _refreshValue() 的原始实现之前使用 _percentage() 数字来扩展此行为。

还有更多...

我们实施进度条标签的这种方法可能遇到的一个潜在问题是,我们必须改变 HTML 结构。这违反了 DRY 原则,因为我们为每个创建的进度条小部件添加的每个标签 div 都是完全相同的。此外,我们可能希望为已存在于应用程序中的进度条小部件应用标签。改变已经正常工作的小部件的 HTML 不是最好的方法。让我们想想如何改进之前的代码。

我们创建的用于定位和样式化标签元素的 CSS 是可以的。它遵循正确的命名约定,并适用于所有进度条小部件实例。我们想要更改的是用于实例化带有显示的标签的进度条小部件的必要标记。问题是如何。理想情况下,通过一个选项,让开发人员切换标签的显示和隐藏。然后小部件本身将负责在必要时插入标签 div,因为它对于小部件的所有实例都是相同的,这反过来意味着最小的 JavaScript 代码。

让我们看一下简化的标记,遵循与之前相同的例子:


<span>Network:</span>
<div id="network-utilization"></div>
<span>Storage:</span>
<div id="storage-utilization"></div>

我们现在回到了进度条小部件在我们引入修改之前期望的原始标记。 现在让我们更新小部件代码以利用这个标记,通过添加一个新选项。

(function( $, undefined ) {

$.widget( "ab.progressbar", $.ui.progressbar, {

    options: {
        label: false
    },

    _create: function() {

        if ( !this.options.label ) {
            return this._super();
        }

        this.$label = $( "<div/>" ).addClass( "ui-progressbar-label" )
                                   .appendTo( this.element );

        this._super();

    },

    _destroy: function() {

        if ( !this.options.label ) {
            return this._super();
        }

        this.$label.remove();

        this._super();

    },

    _refreshValue: function() {

        if ( !this.options.label ) {
            return this._super();
        }

        this.$label.text( this._percentage().toFixed( 0 ) + "%" );

        this._super();

    },

});

})( jQuery );

$(function() {

    $( "#network-utilization" ).progressbar({
        value: 746586112,
        max: 1073741824,
        label: true
    });

    $( "#storage-utilization" ).progressbar({
        value: 24696061952,
        max: 107374182400
    });

});

在这里,我们通过新的label选项扩展了进度条小部件,该选项默认为false。 思路是当这个值为true时,我们将label div插入到进度条容器中。 我们对_create()_refreshValue()方法的修改基本与先前的代码相同,只是现在我们在执行自定义行为之前检查label选项是否已打开。 正如您所看到的,我们将这个新的标签选项提供给了#network-utilization div,但没有提供给#storage-utilization div。

更多内容请参考...

第八章:使用滑块

在本章中,我们将涵盖:

  • 控制滑块手柄的大小

  • 移除焦点轮廓

  • 使用主滑块和子滑块

  • 标记步进增量

  • 获取范围值

  • 更改滑块方向

介绍

滑块部件几乎就像一个用户可以操纵的进度条。滑块给用户一个手柄,可以沿平面拖动以产生所需值。这在处理表单值时尤其有用。滑块部件默认具有有用的选项,如更改方向的能力和允许用户选择值范围。在本章中,我们将看看通过添加新选项或附加事件处理函数来调整滑块部件的各种方法。我们还将研究一些视觉调整以及滑块实例如何相互通信。

控制滑块手柄的大小

用于控制滑块位置的滑块手柄,由鼠标拖动,是一个正方形。也就是说,宽度与高度相同,而我们可能想要不同形状的滑块手柄。在水平滑块的情况下,即默认方向,让我们看看如何通过覆盖部件 CSS 样式来改变滑块手柄的形状,以满足我们应用程序的需求。

准备好...

我们将创建的 HTML 是两个滑块部件。我们还将为它们添加标签,并将它们各自包装在容器 div 元素中以控制布局。

<div class="slider-container">
    <span>Treble:</span>
    <div id="treble"></div>
</div>
<div class="slider-container">
    <span>Bass:</span>
    <div id="bass"></div>
</div>

如何做...

这是用于自定义滑块手柄的 CSS。这覆盖了部件 CSS 中定义的值,因此应包含在 jQuery UI 样式表之后的页面中:

.ui-slider-horizontal .ui-slider-handle {
    width: 0.8em;
    height: 1.6em;
    top: -0.48em;
}

以下是用于创建两个滑块部件实例的 JavaScript 代码:

$(function() {

    $( "#treble" ).slider();
    $( "#bass" ).slider();

});

作为参考,这是应用我们自定义 CSS 前两个滑块部件的外观:

如何做...

这是应用我们自定义 CSS 后的相同两个滑块部件:

如何做...

它的工作原理...

如您所见,手柄变得更高,延伸到滑块边界之外。这为用户提供了更大的点击和拖动滑块手柄的表面积。我们引入的确切尺寸变化是任意的,可以根据每个应用程序进行调整。

.ui-slider-horizontal .ui-slider-handle 选择器覆盖了部件 CSS 中定义的三个属性。宽度被改变为 0.8em,使其略微变细。height 属性的值被改为 1.6em,使其变得更高。当我们使用 height 属性使手柄变高时,我们将其向下推,以使其不再与滑块对齐。为了弥补高度变化,我们通过减少 top 值来将其拉回上来,直到 -0.48em

移除焦点轮廓

大多数浏览器在接收到焦点时在元素周围显示虚线或实线轮廓。这不是用户界面样式的一部分,而是浏览器内置的辅助功能特性。例如,滑块手柄周围的这种强制视觉显示并不总是理想的。让我们看看我们如何取消滑块手柄的默认浏览器行为。

如何做到...

我们可以使用任何基本的div元素来构建我们的示例滑块小部件。所以让我们直接跳转到我们的自定义滑块小部件 CSS。

.ui-slider-handle-no-outline {
    outline: 0;
}

现在,我们已经有了我们的滑块小部件的自定义实现和我们自定义滑块的一个实例。

(function( $, undefined ) {

$.widget( "ab.slider", $.ui.slider, {

    options: { 
        handleOutline: true
    },

    _create: function() {

        this._super();

        if ( this.options.handleOutline ) {
            return;
        }

        this.handles.addClass( "ui-slider-handle-no-outline" );

    }

});

})( jQuery );

$(function() {

    $( "#slider" ).slider({
        handleOutline: false,
    });

});

在对滑块小部件应用我们的更改之前,拖动手柄后轮廓看起来如下所示:

如何做到...

在对滑块小部件应用我们的更改后,拖动手柄后我们的滑块实例如下所示:

如何做到...

工作原理...

我们已经为滑块小部件添加了一个名为handleOutline的新选项。我们将此选项默认设置为true,因为始终支持原生浏览器行为是一个好主意。当此选项设置为false时,该选项会关闭此原生边框轮廓功能。它通过向滑块中的每个手柄元素添加ui-slider-handle-no-outline类来实现。一个滑块中可以有很多手柄,例如,一个范围滑块。因此,在_create()方法中,我们检查handleOutline选项是否为true,如果是,我们使用存储为该小部件属性的handles jQuery 对象来应用我们创建的新类。

类本身很简单,因为它只改变了一个属性。事实上,我们可以简单地将outline属性添加到ui-slider-handle类中,值为0,而不是创建一个新类。但是,我们选择的方法允许我们保持本地小部件样式不变,这样可以让轮廓浏览器功能为我们的小部件的每个实例切换打开或关闭。您还会注意到,即使没有本地浏览器轮廓,手柄也不会失去任何可访问性,因为 jQuery UI 状态类为我们处理了这个问题。

使用主滑块和子滑块

应用程序可能会使用一些可以进一步分解为较小值的数量。此外,用户可能需要控制这些较小值,而不仅仅是聚合值。如果我们决定使用滑块小部件来实现这个目的,我们可以想象子滑块观察主滑块的变化值。让我们看看如何实现这样一组滑块。我们将设计一个界面,允许我们分配该应用程序可以使用多少 CPU。这是主滑块。我们假设一个四核架构,因此我们将有四个依赖于主 CPU 滑块并观察主 CPU 滑块的子滑块。

如何做到...

这里是用于定义我们的五个滑块布局的 HTML。每个滑块都有自己的 div 容器,主要用于定义宽度和边距。在 div 容器内,我们有每个 CPU 的标签,它们的当前 MHz 分配和最大值。这也是放置每个滑块小部件的地方。

<div class="slider-container">
    <h2 class="slider-header">CPU Allocation:</h2>
    <h2 class="slider-value ui-state-highlight"></h2>
    <div class="ui-helper-clearfix"></div>
    <div id="master"></div>
</div>

<div class="slider-container">
    <h3 class="slider-header">CPU 1:</h3>
    <h3 class="slider-value ui-state-highlight"></h3>
    <div class="ui-helper-clearfix"></div>
    <div id="cpu1"></div>
</div>

<div class="slider-container">
    <h3 class="slider-header">CPU 2:</h3>
    <h3 class="slider-value ui-state-highlight"></h3>
    <div class="ui-helper-clearfix"></div>
    <div id="cpu2"></div>
</div>

<div class="slider-container">
    <h3 class="slider-header">CPU 3:</h3>
    <h3 class="slider-value ui-state-highlight"></h3>
    <div class="ui-helper-clearfix"></div>
    <div id="cpu3"></div>
</div>

<div class="slider-container">
    <h3 class="slider-header">CPU 4:</h3>
    <h3 class="slider-value ui-state-highlight"></h3>
    <div class="ui-helper-clearfix"></div>
    <div id="cpu4"></div>
</div>

接下来,我们有一些 CSS 样式来帮助对齐和定位这些组件。

.slider-container { 
    width: 200px;
    margin: 5px;
}

.slider-header {
    float: left;
}

.slider-value {
    float: right;
}

最后,我们有我们的 JavaScript 代码,该代码扩展了滑块小部件,为使用它的开发人员提供了两个新选项,parentpercentage。文档加载时,我们实例化了我们的 CPU 滑块小部件,并利用我们的新滑块功能来建立它们之间的适当关系。

(function( $, undefined ) {

$.widget( "ui.slider", $.ui.slider, {

    options: {
        parent: null,
        percentage: null
    },

    _create: function() {

        this._super();

        var parent = this.options.parent,
            percentage = this.options.percentage,
            $parent;

        if ( !( parent && percentage ) ) {
            return;
        }

        $parent = $( parent );

        this._reset( $parent.slider( "value" ) );

        this._on( $parent , { 
            slidechange: function( e, ui ) {
                this._reset( ui.value );
            }
        });

    },

    _reset: function( parentValue ) {

        var percentage = ( 0.01 * this.options.percentage ),
            newMax = percentage * parentValue,
            oldMax = this.option( "max" ),
            value = this.option( "value" );

        value = ( value / oldMax ) * newMax;

        this.option( "max", newMax );
        this.option( "value", value );

    }

});

})( jQuery );

$(function() {

    function updateLabel( e, ui ) {

        var maxValue = $( this ).slider( "option", "max" )
                                .toFixed( 0 ),
            value = $( this ).slider( "value" )
                             .toFixed( 0 ) + " MHz" +
                                             " / " + 
                                             maxValue + 
                                             "MHz";

        $( this ).siblings( ".slider-value" ).text( value );

    }

    $( "#master" ).slider({
        range: "min",
        value: 379,
        min: 1,
        max: 2400,
        create: updateLabel,
        change: updateLabel
    });

    $( "#cpu1" ).slider({
        parent: "#master",
        percentage: 25,
        range: "min",
        min: 0,
        create: updateLabel,
        change: updateLabel
    });

    $( "#cpu2" ).slider({
        parent: "#master",
        percentage: 35,
        range: "min",
        min: 0,
        create: updateLabel,
        change: updateLabel
    });

    $( "#cpu3" ).slider({
        parent: "#master",
        percentage: 15,
        range: "min",
        min: 0,
        create: updateLabel,
        change: updateLabel
    });

    $( "#cpu4" ).slider({
        parent: "#master",
        percentage: 25,
        range: "min",
        min: 0,
        create: updateLabel,
        change: updateLabel
    });

});

在浏览器中查看结果滑块小部件,并调整一些子 CPU 值。您会注意到标签更新已经改变,并且每个 CPU 都有其自己的 CPU 分配。

操作步骤...

现在,保持 CPU 值不变,尝试调整主 CPU 分配滑块。您会注意到每个子 CPU 滑块的当前值和最大值都会改变,但比例是保持不变的。这意味着如果我们设置 CPU 1 使用总体 CPU 分配的 10%,即使总体分配增加或减少,它仍将继续使用 10%。

操作步骤...

工作原理...

在我们为 CPU 滑块创建的每个容器 div 元素中,我们都有一个名为 slider-value 的头部,用于显示滑块的当前值以及最大值。这是一个需要在大多数情况下考虑的重要补充,而滑块小部件则非常适合让用户更改值,但他们需要特定的反馈来显示他们操作的结果。在这个例子中,更改主滑块会更新五个标签,进一步凸显了在用户能够看到的滑块外部标记特定滑块值的必要性。

我们在滑块小部件中新增了两个选项,parentpercentage。这两个选项彼此相关,基本上可以理解为"此滑块的最大值是其父级滑块值的百分比"。在 _create() 方法中,我们在继续之前会检查这两个选项是否有实际值,因为它们默认为null。如果没有值,我们已经使用 _super() 方法调用了原始滑块构造函数,因此我们可以安全地返回。

另一方面,如果我们已经得到了一个父级滑块小部件和一个百分比,我们将调用_reset()方法,并将当前值传递给我们的父级滑块。这将可能更新此小部件的最大值和当前值。完成这些操作后,我们设置了一个观察者,用于观察父级滑块的更改。这是使用_on()方法完成的,我们在其中传递parent作为我们正在监听事件的元素以及配置对象。该对象具有一个slidechange事件,这是我们感兴趣的事件,以及回调函数。在回调函数内部,我们只是使用来自父级的更新值简单地调用了我们的_reset()方法。值得注意的是,我们必须使用_on()来注册我们的事件处理程序。如果销毁了子滑块,事件处理程序将从父级中删除。

_reset()方法接受来自父级滑块的值,并重置此子滑块的最大选项。我们在首次创建子元素和父元素值更改时都使用此方法。目标是保持当前值/最大值比率。这就是percent选项发挥作用的地方。由于这作为整数传递给小部件,我们必须将其乘以0.01。这是我们计算出该子级的新最大值的方法。一旦我们有了新的最大值,我们就可以将当前值放大或缩小。

最后,在文档准备就绪的事件处理程序中,我们实例化了五个滑块小部件,在其中定义了一个用于更新每个 CPU div 中标签的通用回调函数。这个函数被传递给了每个滑块小部件的创建和更改选项。我们还在这里使用了我们新定义的选项的值。每个子滑块都有一个独特的总 CPU 分配的百分比值,并且每个子元素都使用#master作为其父级

标记步长增量

滑块小部件可以传递一个步长值,该值确定用户可以滑动手柄的增量。如果未指定,步长选项为1,手柄会平滑地来回滑动。另一方面,如果步长值更加明显,比如10,我们会注意到随着移动手柄而手柄会吸附到位置。让我们看看我们如何扩展滑块小部件以使用户更好地感受到这些增量的位置。我们将使用刻度来在视觉上标记增量。

如何做...

我们将直接进入用于此小部件增强的自定义 CSS。用于滑块元素的基础div元素可以简单地是<div></div>

.ui-slider-tick {
    position: absolute;
    width: 2px;
    height: 15px;
    z-index: -1;
}

这是我们的 JavaScript 代码,扩展了滑块并使用新的ticks选项创建了小部件的实例:

(function( $, undefined ) {

$.widget( "ab.slider", $.ui.slider, {

    options: {
        ticks: false
    },

    _create: function() {

        this._super();

        if ( !this.options.ticks || this.options.step < 5 ) {
            return;
        }

        var maxValue = this.options.max,
            cnt = this.options.min + this.options.step,
            background = this.element.css( "border-color" ),
            left;

        while ( cnt < maxValue ) {

            left = ( cnt / maxValue * 100 ).toFixed( 2 ) + "%";

            $( "<div/>" ).addClass( "ui-slider-tick" )
                         .appendTo( this.element )
                         .css( { left: left,
                                 background: background } );

            cnt += this.options.step;

        }

    }

});

})( jQuery );

$(function() {

    $( "#slider" ).slider({
        min: 0,
        max: 200,
        step: 20,
        ticks: true
    });

});

查看此滑块小部件,我们可以看到我们指定的步长20在滑块下方使用刻度标记来表示。

如何做...

工作原理...

让我们检查我们已经引入到滑块小部件中的附加功能。我们添加了ticks布尔选项,默认情况下关闭。当这个选项为真时,告诉小部件使用刻度标记显示步进增量。在_create()方法中,我们使用_super()调用了原始的_create()实现,因为我们希望滑块按照正常方式构造。然后,我们检查ticks选项是否已打开,以及step值是否大于5。如果已打开ticks选项并且我们有一个小于5step值,它们将看起来彼此靠近;所以我们简单地不显示它们。

计数器变量cnt控制着我们的刻度渲染循环,并初始化为min选项上方的第一个step。同样,循环在max选项值之前退出。这是因为我们不想在滑块的开头或结尾渲染刻度标记,而只想在中间部分显示。变量background用于从滑块小部件中提取border-color CSS 属性。我们实际上在这里所做的是将主题设置传递给我们要添加到小部件中的新元素。这允许主题被交换,刻度标记的颜色也会相应更改。

while循环内,我们正在创建代表刻度标记的div元素。left CSS 属性被计算为实际定位div,使其与用户移动手柄时的滑块手柄对齐。我们将ui-slider-tick CSS 类添加到div元素中,配置每个刻度标记的公共属性,包括z-index,将div的一部分推到主滑块栏的后面。

获取范围数值

滑块小部件可用于控制范围值。因此,用户不是在滑块轴上来回移动一个固定点,即手柄,而是在两个手柄之间来回移动。这两个点之间的空间表示范围值。但是我们如何计算这个数字呢?滑块小部件给我们提供了原始数据,即用户选择的上限和下限。我们可以在我们的事件处理程序中使用这些值来计算范围值。

准备工作...

我们将仅使用基本的滑块进行演示,但我们需要一些支持的 CSS 和 HTML 来包围滑块,以便在更改时显示范围值。以下是 CSS:

.slider-container { 
    width: 180px;
    margin: 20px;
}

.slider-container .slider-label {
    margin-bottom: 10px;
    font-size: 1.2em;
}

这是 HTML 代码:

<div class="slider-container">
    <div class="slider-label">
        <span>Range Value: </span>
        <strong id="range-value"></strong>
    </div>
    <div id="slider"></div>
</div>

操作方法...

我们将使用以下 JavaScript 代码创建slider实例。请注意,我们传递了支持范围选择的特定选项。

$(function() {

    $( "#slider" ).slider({
        min: 0,
        max: 600,
        values: [280, 475],
        range: true,
        create: function( e, ui ) {
            var values = $( this ).data( "uiSlider" ).values();
            $( "#range-value" ).text( values[1] - values[0] );
        },
        change: function( e, ui ) {
            $( "#range-value" ).text( ui.values[1] - ui.values[0] );
        }
    });

});

现在,当您在浏览器中查看此滑块时,您会注意到范围值显示为小部件外的标签。而且,如果您移动滑块手柄中的任何一个,标签将反映更改的范围值。

操作方法...

工作原理...

在这个例子中,我们正在创建一个简单的滑块小部件,它使用一系列值而不是单个值。我们通过将值数组传递给小部件构造函数,并将range值传递给构造函数,以此来实现。这就是小部件知道要使用两个手柄而不是一个,并填充它们之间的空间的方式。我们还将滑块构造函数与两个事件回调函数一起传递:一个用于create事件,另一个用于change事件。

这两个回调函数执行相同的操作:它们计算范围值并将其显示在我们的#range-value标签中。然而,这两个回调函数以稍微不同的方式实现相同的逻辑。create回调函数不包含ui对象的values数组,该数组用于保存小部件数据。因此,在这里我们的解决方法是使用uiSlider数据,该数据保存了 JavaScript 滑块小部件实例,以便访问values()方法。这将返回传递给 change 事件回调函数的ui对象中找到的相同数据。

我们在这里计算的数字只是第一个手柄的值减去第二个手柄的值。例如,如果我们在表单中使用这样的滑块,API 可能不关心由两个滑块手柄表示的两个值,而只关心由这两个数字导出的范围值。

更改滑块方向

默认情况下,滑块小部件将水平呈现。我们可以通过orientation选项轻松将滑块方向更改为垂直布局。

操作步骤...

我们将使用以下 HTML 来定义我们的两个小部件。第一个滑块将是垂直的,而第二个则使用默认的水平布局:

<div class="slider-container">
    <div id="vslider"></div>
</div>

<div class="slider-container">
    <div id="hslider"></div>
</div>

接下来,我们将使用以下 JavaScript 代码实例化这两个小部件:

$(function() {

    $( "#vslider" ).slider({
        orientation: "vertical",
        range: "min",
        min: 1,
        max: 200,
        value: 128
    });

    $( "#hslider" ).slider({
        range: "min",
        min: 0,
        max: 200,
        value: 128
    });

});

如果您在浏览器中查看这两个滑块,您可以看到垂直布局和默认水平布局之间的对比:

操作步骤...

工作原理...

我们在这里创建的两个滑块小部件,#vslider#hslider,在内容上是相同的。唯一的区别是#vslider实例是使用orientation选项设置为vertical创建的。#hslider实例没有指定orientation选项,因此使用默认的horizontal。它们之间的关键区别在于布局,正如我们的示例中明显的那样。布局本身由ui-slider-verticalui-slider-horizontalCSS 类控制,这两个类是互斥的。

控制滑块的方向是有价值的,这取决于你想把小部件放在 UI 上下文中的位置。例如,包含元素可能没有太多的水平空间,所以在这里使用垂直方向选项可能是个不错的选择。然而,要小心动态改变滑块的方向。手柄有时会从滑块条中脱离。因此,在设计时最好确定方向。

第九章:使用旋转器

在本章中,我们将涵盖:

  • 移除输入焦点轮廓

  • 为本地文化格式化货币

  • 为本地文化格式化时间

  • 控制值之间的步骤

  • 指定旋转溢出

  • 简化旋转器按钮

介绍

在本章中,我们将使用旋转器。 旋转器 只不过是文本input元素上的装饰品。但与此同时,它还有很多其他用途。例如,旋转器在本章中将有助于将数字格式化为本地文化。我们还将探讨旋转器小部件提供的一些选项,以及如何扩展和改进这些选项。最后,我们将看一些修改旋转器小部件外观和感觉的方法。

移除输入焦点轮廓

大多数浏览器在用户从中获得焦点时,将自动在input元素周围应用输入焦点轮廓。当用户单击input元素或通过标签到达时,元素会获得焦点。旋转器小部件本质上是一个带有装饰的input元素。这包括利用 CSS 主题框架中的内在 jQuery 状态类的能力。虽然浏览器的自动聚焦行为对于单独的input元素可能效果很好,但是这些焦点环可能会使旋转器看起来有点凌乱。让我们看看如何删除自动焦点轮廓,同时保持相同的可访问性水平。

如何做...

对于这个示例,我们将创建一个简单的input元素。以下是 HTML 结构的样子。

<div class="spinner-container">
    <input id="spinner"/>
</div>

这是与我们的小部件修改一起使用的自定义 CSS,以移除焦点轮廓。

.ui-spinner-input-no-outline {
    outline: 0;
}

最后,这是我们的 JavaScript 代码,它修改了旋转器小部件的定义,并创建了一个实例,浏览器不会自动应用任何轮廓。

(function( $, undefined ) {

$.widget( "ab.spinner", $.ui.spinner, {

    options: {        
inputOutline: true    
},

    _create: function() {

        this._super();

        if ( this.options.inputOutline ) {            
return;        
}

        this.element.addClass( "ui-spinner-input-no-outline" );
        this._focusable( this.uiSpinner );

    }
});

})( jQuery );

$(function() {

    $( "#spinner" ).spinner( { inputOutline: false } );

});

为了让您更好地了解我们引入的更改,这就是我们在对旋转器定义进行修改之前创建的旋转器小部件的外观。

如何做...

在这里,您可以清楚地看到input元素具有焦点,但是我们可以不使用双重边框,因为它与我们的主题不太匹配。以下是在引入我们的更改后处于焦点状态的相同小部件的修改版本。

如何做...

我们不再有焦点轮廓,当小部件获得焦点时,小部件仍然会在视觉上更改其状态。只是现在,我们正在使用 CSS 主题中的状态类更改外观,而不是依赖浏览器为我们完成。

它是如何工作的...

处理移除轮廓的 CSS 类,ui-spinner-input-no-outline类,非常容易理解。我们只需将outline设置为0,这将覆盖浏览器的默认操作方式。我们自定义的旋转器小部件知道如何利用这个类。

我们已经向旋转器小部件添加了一个新的inputOutline选项。如果设置为false,此选项将向input元素应用我们的新 CSS 类。但是,默认情况下,inputOutline默认为true,因为我们不希望默认情况下覆盖默认浏览器功能。此外,我们也不一定想要默认情况下覆盖默认的旋转器小部件功能。相反,最安全的方式是提供一个选项,当显式设置时,改变默认设置。在我们的_create()方法的实现中,我们调用旋转器构造函数的原始实现。然后,如果inputOutline选项为true,我们应用ui-spinner-input-no-outline类。

再次,请注意,我们最后要做的事情是将this.uiSpinner属性应用于_focusable()方法。原因是,我们需要弥补失去的可访问性;浏览器不再应用轮廓,因此当小部件获得焦点时,我们需要应用ui-state-focus类。_focusable()方法是在基本小部件类中定义的一个简单辅助方法,因此对所有小部件都可用,使传递的元素处理焦点事件。这比自己处理事件设置和撤消要简单得多。

格式化本地文化的货币

可以将旋转器小部件与Globalize jQuery 库一起使用。 Globalize 库是 jQuery 基金会的一项努力,旨在标准化 jQuery 项目根据不同文化格式化数据的方式。文化是根据文化规范格式化字符串、日期和货币的一组规则。例如,我们的应用程序应该将德语日期和货币与法语日期和货币区分对待。这就是我们能够向旋转器小部件传递culture值的方式。让我们看看如何使用 Globalize 库与旋转器小部件将货币格式化为本地文化。

操作步骤...

当我们的应用程序在多个区域设置中运行时,第一件需要做的事情就是包含globalize库。每种文化都包含在自己的 JavaScript 文件中。

<script src="img/globalize.js"
  type="text/javascript"></script>
<script src="img/globalize.culture.de-DE.js"
  type="text/javascript"></script>
<script src="img/globalize.culture.fr-CA.js"
  type="text/javascript"></script>
<script src="img/globalize.culture.ja-JP.js"
  type="text/javascript"></script>

接下来,我们将定义用于显示文化选择器的 HTML,由单选按钮组成,并且用于显示货币的旋转器小部件。

<div class="culture-container"></div>
<div class="spinner-container">
    <input id="spinner"/>
</div>

最后,我们有用于填充culture选择器、实例化旋转器小部件并将更改事件绑定到文化选择器的 JavaScript 代码。

$(function() {

    var defaultCulture = Globalize.cultures.default;

    $.each( Globalize.cultures, function( i, v ) {

      if ( i === "default" ) {
        return;
      }

       var culture = $( "<div/>" ).appendTo( ".culture-container" );

       $( "<input/>" ).attr( "type", "radio" )
          .attr( "name", "cultures" )
          .attr( "id", v.name )
          .attr( "checked", defaultCulture.name === v.name )
          .appendTo( culture );

       $( "<label/>" ).attr( "for", v.name )
           .text( v.englishName )
           .appendTo( culture );

    });

    $( "#spinner" ).spinner({
        numberFormat: "C",
        step: 5,
        min: 0,
        max: 100,
        culture: $( "input:radio[name='cultures']:checked" )
          .attr( "id" )
    });

    $( "input:radio[name='cultures']" ).on
      ( "change", function( e ) {
        $( "#spinner" ).spinner( "option", "culture",
          $( this ).attr( "id" ) );
    });

});

当您首次在浏览器中查看此用户界面时,您会注意到英语是选定的文化,并且旋转器将相应地格式化货币。

操作步骤...

但是,文化的更改会导致旋转器小部件中的货币格式发生变化,如前所述。

操作步骤...

工作原理...

在 JavaScript 代码中,一旦 DOM 准备就绪,我们首先使用 Globalize.cultures 对象填充 culture 选择器。 Globalize 库根据可用的文化构建此对象;你会注意到可用文化选项与页面中包含的文化脚本之间存在直接关联。 我们将文化的名称存储为 id 属性,因为这是我们稍后传递给微调器小部件的内容。 Globalize.cultures 对象还具有默认文化,我们使用此值来确定页面首次加载时选择了哪个选项。

我们创建的微调器实例使用了一个 numberFormat 选项值为 C。 这个字符串实际上在渲染微调器值时直接传递给 Globalize.format() 函数。 接下来的三个选项,stepminmax,与任何数字微调器实例一样。 我们将 culture 选项设置为所选的默认文化,告诉微调器小部件如何格式化货币。 最后,我们设置了一个事件处理程序,每当文化选择更改时触发。 此处理程序将更新微调器小部件以使用新选择的文化。

为本地文化格式化时间

微调器小部件利用了 Globalize jQuery 项目;这是一项根据本地文化标准化数据格式的工作。 微调器小部件利用此库来格式化其值。 例如,指定 numberFormatculture 选项允许我们使用微调器小部件根据本地文化显示货币值。 然而,货币只是我们喜欢本地格式化的一个值; 时间是另一个值。 我们可以在微调器小部件中使用内置的 Globalize 功能来显示时间值。 我们需要在我们自己的部分上做更多工作来扩展小部件以正确地允许时间值。 实际上,让我们基于微调器创建我们自己的时间小部件。

如何实现...

首先,让我们看一下创建两个时间小部件所需的标记,我们将在其中显示多伦多时间和伦敦时间。 我们在这里不展示时区计算能力,只是在同一个 UI 中展示两种不同的文化。

<div class="spinner-container">
    <h3>Toronto</h3>
    <input id="time-ca" value="2:30 PM"/>
</div>

<div class="spinner-container">
    <h3>London</h3>
    <input id="time-gb" value="7:30 PM"/>
</div>

接下来,让我们看一下用于定义新时间小部件并创建两个实例的 JavaScript 代码。

( function( $, undefined ) {

$.widget( "ab.time", $.ui.spinner, {

    options: {
        step: 60 * 1000,
        numberFormat: "t"
    },

    _parse: function( value ) {

        var parsed = value;

        if ( typeof value === "string" && value !== "" ) {

            var format = this.options.numberFormat,
                culture = this.options.culture;

            parsed = +Globalize.parseDate( value, format );

            if ( parsed === 0 ) {
                parsed = +Globalize.parseDate( value,
                  format, culture );
            }

        }

        return parsed === "" || isNaN( parsed ) ? null : 
          parsed;

    },

    _format: function( value ) {
        return this._super( new Date( value ) );
    }

});

})( jQuery );

$(function() {

    $( "#time-ca" ).time({
        culture: "en-CA"
    });

    $( "#time-gb" ).time({
        culture: "en-GB"
    });

});

在浏览器中查看两个时间小部件,我们可以看到它们已按其各自的本地文化格式化。

如何实现...

工作原理...

让我们首先看一下用于定义时间小部件实例的两个输入元素。 注意 value 属性,它们都具有默认时间,使用相同的格式表示。 现在,让我们跳转到新时间小部件的定义。

你在这里首先注意到的是,我们使用小部件工厂在 ab 命名空间下定义了时间小部件。您还会注意到,我们正在扩展微调器小部件。这是因为实质上我们正在构建的是一个微调器,在这里有一些小但重要的区别。这实际上是一个很好的例子,说明了当设计从标准小部件集派生的 jQuery UI 小部件自定义时,您必须考虑的一些事情。在这种情况下,您应该保留原始小部件名称,即微调器,还是应该叫它其他名称,比如时间?可以帮助您指导这个决定的唯一事情是思考此小部件的使用方式。例如,我们本可以保持微调器小部件不变以显示这些受文化影响的时间值,但这意味着引入新的选项,并可能让使用该小部件的开发人员感到困惑。我们已经决定这里的用例很简单,我们应该尽可能少地允许时间以尽可能少的选项显示。

我们在此定义的选项并不是新的;stepnumberFormat 选项已经由微调器小部件定义,我们只是将它们设置为适合我们时间小部件的默认值。step 值将针对一个 timestamp 值递增,因此我们给它一个默认值,以秒为步长。numberFormat 选项指定微调器在解析和格式化输出时所期望的格式。

我们对微调器的扩展,_parse() 方法,是我们直接使用 Globalize 库解析时间字符串的地方。请记住,我们的输入具有相同的字符串格式。如果我们尝试解析一个格式不可识别的值,这就成为了一个问题。因此,我们尝试在不指定值所属文化的情况下解析时间值。如果这样不起作用,我们就使用附加到此小部件的文化。通过这种方式,我们可以使用一个格式指定初始值,就像我们在这里做的一样,并且我们可以动态更改文化;一切仍将正常工作。我们的_format()方法的版本很简单,因为我们知道值始终是一个时间戳数字,我们只需将一个新的 Date 对象传递回原始的微调器_format()方法即可。

最后,我们有两个时间小部件实例,其中一个传递了 en-CA 的文化,另一个传递了 en-GB

控制值之间的步长

有几种方法可以控制微调器小部件中的步骤。步骤是微调器小部件用来向上或向下移动其数字的值。例如,您经常会看到循环代码,它会增加一个计数器 cnt ++。在这里,步骤是一,这是微调器步骤值的默认值。更改微调器中的此选项很简单;我们甚至可以在创建小部件后更改此值。

我们可以采取其他措施来控制旋转器的步进行为。让我们看看增量选项,并看看这如何影响旋转器。

如何做...

我们将创建三个旋转器部件来演示增量选项的潜力。以下是 HTML 结构:

<div class="spinner-container">
    <h3>Non-incremental</h3>
    <input id="spin1" />
</div>

<div class="spinner-container">
    <h3>Doubled</h3>
    <input id="spin2" />
</div>

<div class="spinner-container">
    <h3>Faster and Faster</h3>
    <input id="spin3" />
</div>

下面是用于创建三个旋转器实例的 JavaScript 代码:

$(function() {

    $( "#spin1" ).spinner({
        step: 5,
        incremental: false
    });

    $( "#spin2" ).spinner({
        step: 10,
        incremental: function( spins ) {
            if ( spins >= 10 ) {
                return 2;
            }
            return 1;
        }
    });

    $( "#spin3" ).spinner({
        step: 15,
        incremental: function( spins ) {
            var multiplier = Math.floor( spins / 100 ),
                limit = Math.pow( 10, 10 );
            if ( multiplier < limit && multiplier > 0 ) {
                return multiplier;
            }
            return 1;
        }
    });

});

在您的浏览器中,这三个旋转器部件应该看起来是这样的。

如何做...

工作原理...

我们创建了三个不同的旋转器实例,它们在用户按住其中一个旋转按钮时的行为不同。#spin1旋转器的步长值为5,并且将始终将旋转器值递增5。您可以通过按住旋转按钮来尝试这一点。您会注意到这将花费您很长时间才能达到一个较大的整数值。

incremental选项接受一个布尔值,就像我们在第一个旋转器中看到的那样,但它还接受一个callback函数。#spin2旋转器的步长值为10,但它将根据我们传递给增量选项的函数而改变。我们定义的这个incremental callback函数通过用户按住旋转按钮的旋转次数传递。我们从这里正常开始,前10次旋转,然后我们从那时起加速返回2而不是1。当我们返回2时,我们的步长值变为20,因为该函数的返回值是一个乘数。但它只在用户按住旋转按钮时使用;此函数不会永久改变step选项。

我们的最后一个旋转器实例,#spin3,也使用了一个incremental callback函数。然而,这个函数会随着用户持续旋转而使用一个逐渐变大的值。每旋转一百次,我们就增加乘数,也增加步长。后者的递增函数在旋转器值本身变大时非常有用,我们可以控制步长变化的速度。

更多内容...

我们刚刚看到了如何控制旋转器部件的值步进。step选项决定了在给定旋转时值在任一方向上移动的距离。当用户按住旋转按钮时,我们可以使用incremental选项来计算步长值。这有助于加快或减慢旋转到给定目标值所需的时间。

另一种方法是改变旋转之间的实际时延。如果您想要在用户按住旋转按钮时减慢旋转速度,这可能会很方便。让我们看一个如何改变旋转延迟的例子。以下是 HTML 结构:

<div class="spinner-container">
    <h3>Default delay</h3>
    <input id="spin1" />
</div>

<div class="spinner-container">
    <h3>Long delay</h3>
    <input id="spin2" />
</div>

<div class="spinner-container">
    <h3>Longer delay</h3>
    <input id="spin3" />
</div>

这是自定义旋转器部件定义,以及使用不同旋转值的三个实例。

( function( $, undefined ) {

$.widget( "ab.spinner", $.ui.spinner, {

    options: {
        spinDelay: 40
    },

    _repeat: function( i, steps, event ) {

        var spinDelay = this.options.spinDelay;

        i = i || 500;

        clearTimeout( this.timer );
        this.timer = this._delay(function() {
            this._repeat( spinDelay, steps, event );
        }, i );

        this._spin( steps * this.options.step, event );

     }

});

})( jQuery );

$(function() {

    $( "#spin1" ).spinner();

    $( "#spin2" ).spinner({
        spinDelay: 80
    });

    $( "#spin3" ).spinner({
        spinDelay: 120
    });

});

您可以在浏览器中尝试这些旋转器中的每一个,并观察旋转延迟的对比。

更多内容...

我们已将spinDelay选项添加到微调器小部件中,以便可以指定延迟的毫秒数。为了实际使用此选项,我们必须对其中一个核心微调器小部件方法进行一些更改。当用户按住微调器按钮时,内部使用_repeat()方法。它实际上使用很少的代码执行了大量工作。基本上,目标是重复给定的事件,直到用户松开按钮并且旋转应该停止。但是,我们不能仅仅重复调用_spin(),而不添加任何延迟,否则用户每次更新文本输入时都会看到模糊的内容。因此,微调器正好利用_delay()方法来实现此目的。_delay()方法为过去的函数设置延迟执行,并在基本小部件类中定义;所有小部件都可以访问_delay()

我们的_repeat()方法版本与原始版本几乎相同,除了我们现在不再硬编码旋转之间的延迟;我们现在从spinDelay选项中获取它。

指定旋转溢出

微调器小部件将愉快地让用户无限地旋转。当达到 JavaScript 整数限制时,它甚至会将显示更改为使用指数表示法,这没问题。几乎没有应用程序需要担心这些限制。事实上,最好为应用程序制定一些有意义的限制。也就是说,指定min边界和max边界。

这很有效,但是如果我们在处理溢出的微调器中插入一些逻辑,它甚至可以工作得更好,当用户想要超出边界时。与默认行为停止旋转不同,我们只是将它们发送到相反的边界,但是以相同的方向开始旋转。最好的方法是将这些约束想象成默认情况下,微调器的最小 - 最大边界就像一条直线。我们想让它看起来更像一个圆。

如何做...

我们将有两个微调器小部件,第一个使用默认边界约束逻辑,第二个使用我们自己定义的行为。以下是用于创建这两个小部件的 HTML 结构:

<div class="spinner-container">
    <h3>Default</h3>
    <input id="spin1" />
</div>

<div class="spinner-container">
    <h3>Overflow</h3>
    <input id="spin2" />
</div>

这里是文档加载后用于实例化两个微调器的 JavaScript 代码:

$(function() {

    $( "#spin1" ).spinner({
        min: 1,
        max: 100
    });

    $( "#spin2" ).spinner({
        minOverflow: 1,
        maxOverflow: 100,
        spin: function( e, ui ) {

            var value = ui.value,
              minOverflow = $( this ).spinner
                ( "option", "minOverflow" ),
                  maxOverflow = $( this ).spinner
                    ( "option", "maxOverflow" );

            if ( value > maxOverflow ) {
                $( this ).spinner( "value", minOverflow );
                return false;
            }
            else if ( value < minOverflow ) {
                $( this ).spinner( "value", maxOverflow );
                return false;
            }

        }
    });

});

以下是浏览器中的两个微调器。您将看到,后一个微调器处理边界溢出的方式与默认实现不同。

如何做...

工作原理...

#spin1微调器达到边界之一,即1100时,旋转将停止。另一方面,#spin2微调器将从另一端开始旋转。您会注意到我们在这里传递了两个非标准的微调器选项;minOverflowmaxOverflow。这些实际上不会像minmax一样约束微调器的边界。我们之所以故意添加这些新选项,是因为我们不希望常规约束逻辑触发。

我们为这个小部件提供的spin回调函数在每次旋转时都会被调用。如果我们使用传统的旋转minmax选项,我们就永远不会知道是否出现了溢出,因为min会小于1,而max永远不会大于100。因此,我们使用新的选项根据方向重定向值。如果值超过了100,那么我们将值设置回minOverflow。或者如果值低于1,那么我们将值设置为maxOverflow

还有更多...

你可能会决定,当我们将用户带到旋转器边界的另一侧时,溢出行为并不完全符合你的期望。你可能只想在达到边界时停止旋转。然而,我们仍然可以通过禁用旋转按钮来改进小部件。这只是对旋转器溢出的另一种方法,我们只是为用户提供更好的反馈,而不是像之前那样改变业务逻辑。让我们看看如何做出这个改变。以下是用于简单旋转器小部件的 HTML 结构:

<div class="spinner-container">
    <input id="spin" value=10 />
</div>

这是我们在页面加载时用到的 JavaScript,用于创建小部件。

$(function() {

    $( "#spin" ).spinner({
        min: 1,
        max: 100,
        spin: function( e, ui ) {
            var value = ui.value,
                buttons = $( this ).data( "uiSpinner" ).buttons,
                min = $( this ).spinner( "option", "min" ),
                max = $( this ).spinner( "option", "max" );

            if ( value === max ) {
                buttons.filter( ".ui-spinner-up:not
                  (.ui-state-disabled)" )
                       .button( "disable" );
            }
            else if ( value === min ) {
                buttons.filter( ".ui-spinner-down:not
                  (.ui-state-disabled)" )
                       .button( "disable" );
            }
            else {
                buttons.filter( ".ui-state-disabled" )
                .button( "enable" );
            }
        }
    });

});

当你在浏览器中开始与这个小部件交互时,你会注意到当你达到min选项值时,即1,下旋转按钮会被禁用。

还有更多...

同样,当你达到了max,这里是100,上旋转按钮会被禁用。

还有更多...

通过向构造函数传递一个spin回调函数,我们引入了这种新的旋转器行为,该函数在每次旋转时执行。在这个回调中,我们将两个旋转按钮的引用都保存在buttons变量中。然后我们检查是否达到了max值,或者达到了min值。然后我们禁用适当的按钮。如果我们处于minmax之间,那么我们就简单地启用这些按钮。你还会注意到我们在这里有一些额外的过滤;not(.ui-state-disabled).ui-state-disabled。这是必要的,因为旋转器小部件触发旋转事件的方式。禁用按钮可能会触发旋转,导致无限循环。因此,我们必须小心地只禁用那些尚未被禁用的按钮。

简化旋转器按钮

spinner 小部件中实现的默认旋转按钮可能有点过多,具体取决于上下文。例如,您可以清楚地看到这些是作为子组件添加到滑块中的按钮小部件。当我们开始使用较小的小部件构建较大的小部件时,这完全有效。这更多地是一种审美偏好。也许如果单独的向上和向下旋转按钮没有悬停状态,也没有背景或边框,那么 spinner 会看起来更好。让我们尝试从滑块按钮中去除这些样式属性,并使它们看起来更紧密集成。

如何做...

这是作为我们 spinner 小部件基础的基本 HTML 结构:

<div class="spinner-container">
    <input id="spin" />
</div>

这是我们将使用的 CSS,用于移除我们不再感兴趣的按钮样式:

.ui-spinner-basic > a.ui-button {
    border: none;
    background: none;
    cursor: pointer;
}

input 元素尚未成为一个小部件,而我们创建的新 CSS 类也尚未成为 spinner 小部件的一部分。以下是完成这两件事情的 JavaScript 代码的样子:

 (function( $, undefined ) {

$.widget( "ab.spinner", $.ui.spinner, {

    options: {
        basic: false
    },

    _create: function() {

        this._super();

        if ( this.options.basic ) {
            this.uiSpinner.addClass( "ui-spinner-basic" );
        }

    }

});

})( jQuery );

$(function() {

    $( "#spin" ).spinner({
        basic: true
    });

});

如果您在浏览器中查看我们创建的 spinner,您会注意到 spinner 按钮的边框和背景已经被去除。现在它看起来更像一个整体小部件。您还会注意到,当用户将鼠标悬停在任一按钮上时,鼠标指针使用指针图标,这有助于表明它们是可点击的。

如何做...

工作原理...

我们刚刚创建的新 CSS 类 ui-spinner-basic 通过在 spinner 上下文中覆盖按钮小部件样式来工作。具体来说,我们从按钮小部件中移除了 borderbackground。此外,我们将 cursor 属性设置为 pointer,以便给用户一种箭头是可点击的印象。我们还稍微定制了 spinner 小部件本身的定义。我们通过添加一个新的 basic 选项来实现这一点,当 true 时,将新的 ui-spinner-basic 类应用于小部件。当小部件被销毁时,我们不需要显式地移除此类,因为它被添加到 spinner 小部件创建的一个元素中。此元素会被基本 spinner 实现自动移除,因此我们的代码不必担心它。

第十章:使用标签

在本章中,我们将涵盖:

  • 处理远程标签内容

  • 为标签添加图标

  • 简化标签主题

  • 将标签用作 URL 导航链接

  • 在标签转换之间创建效果

  • 使用可排序交互来排序标签

  • 使用 href 设置活动标签

介绍

标签 小部件是用于组织页面内容的容器。它是整理页面内容的绝佳方式,因此只显示相关项目。用户具有简单的导航机制来激活内容。标签小部件可以应用于较大的导航上下文中,其中标签小部件是页面的主要顶级容器元素。它还可以作为特定页面元素的较小组件使用,用于简单地拆分两个内容部分。

最新的 jQuery UI 版本中的标签小部件为开发人员提供了一套一致的选项,以调整小部件的行为。我们将看看如何组合这些选项,以及如何充分利用标签小部件的导航功能。我们还将探讨如何对标签转换应用效果,并使标签对用户可排序。

处理远程标签内容

标签小部件知道如何将给定的标签面板填充为远程内容。关键在于我们如何指定标签链接。例如,指向 #tab-content-homehref 属性将使用该元素中找到的 HTML 加载内容。但是,如果我们指向另一个页面而不是指向已存在的元素,则标签小部件将按需将内容加载到适当的面板中。

在不传递选项给标签的情况下,这样可以按预期运行,但是如果我们想要以任何方式调整 Ajax 请求的行为,可以使用 beforeLoad 选项。让我们来看看我们可以如何使用标签小部件处理远程内容的一些方法。

如何操作...

首先,我们将创建标签小部件的 HTML,其中包括四个链接。前三个链接指向现有资源,而第四个链接不存在,因此 Ajax 请求将失败。

<div id="tabs">
    <ul>
        <li><a href="ajax/tab1.html">Tab 1</a></li>
        <li><a href="ajax/tab2.html">Tab 2</a></li>
        <li><a href="ajax/tab3.html">Tab 3</a></li>
        <li><a href="doesnotexist.html">Tab 4</a></li>
    </ul>
</div>

接下来,我们有用于创建标签小部件实例的 JavaScript,以及指定一些自定义行为以修改 Ajax 请求。

$(function() {

    function tabLoad( e, ui ) {

        if ( ui.panel.html() !== "" ) {

            ui.jqXHR.abort();

        }
        else {

            ui.jqXHR.error(function( data ) {

                $( "<p/>" ).addClass( "ui-corner-all ui-state-error" )
                           .css( "padding", "4px" )
                           .text( data.statusText )
                           .appendTo( ui.panel );
            });

        }

    }

    $( "#tabs" ).tabs({
        beforeLoad: tabLoad
    });

});

为了查看此演示中实现的 Ajax 行为,您需要将 web 服务器放在前面。最简单的方法是安装 Python 并从包含主 HTML 文件的目录以及 Ajax 内容文件 tab1.htmltab2.htmltab3.html 运行 python -m SimpleHTTPServer。以下是 tab1.html 文件的示例:

<!doctype html>
<html lang="en">
    <body>
        <h1>Tab 1</h1>
        <p>Tab 1 content</p>
    </body>
</html>

当您在浏览器中加载此标签小部件时,默认情况下选择第一个标签。因此,小部件将立即执行加载第一个标签内容的 Ajax 请求。您应该看到类似于以下内容:

如何操作...

切换到第二个和第三个选项卡将执行必要的 Ajax 请求以获取内容。另一方面,第四个选项卡将导致错误,因为链接的资源不存在。在该面板中不会显示内容,而是显示了我们为 Ajax 请求添加的自定义行为显示的错误消息。

如何实现...

关于这个示例的最后一点要注意的是我们对 Ajax 请求的另一个修改。如果你重新访问第一个选项卡,我们不会发送另一个 Ajax 请求,因为我们已经有了面板内容。

工作原理...

当文档加载完成时,我们将从 #tabs div 创建一个选项卡部件。我们传递 beforeLoad 一个回调函数 tabLoad(),之前定义的。tabLoad 函数在分派用于获取选项卡面板内容的 Ajax 请求之前被调用。这给了我们一个机会来更新 jqXHR 对象的状态。

提示

$.ajax() 返回的 jqXHR 对象是 JavaScript 中原生 XMLHTTPRequest 类型的扩展。开发者很少与这个对象交互,但偶尔也会有需要,正如我们在这里看到的。

在这个示例中,我们首先检查选项卡面板是否有任何内容。ui.panel 对象代表最终将动态 Ajax 内容放置的 div 元素。如果是空字符串,我们继续加载内容。另一方面,如果已经有内容,我们会中止请求。如果服务器没有生成动态内容,而我们只是使用选项卡部件的此功能作为结构组合的手段,那么这是有用的。当我们已经拥有内容时,重复请求相同的内容是没有意义的。

我们还将行为附加到 jqXHR 对象上,以处理 Ajax 请求失败的情况。我们使用 ui-state-errorui-corner-all 类对服务器返回的状态文本进行格式化,然后更新选项卡内容。

还有更多...

前面的例子将从远程资源检索的 HTML 放置到选项卡面板中。但现在我们决定选项卡内容中的 h1 标签是多余的,因为活动选项卡具有相同的作用。我们可以直接从我们用于构建选项卡内容的远程资源中删除这些标签,但如果我们在应用程序的其他地方使用该资源,可能会出现问题。相反,我们可以在用户实际看到它之前,仅仅通过加载事件修改选项卡内容。这是我们选项卡部件实例的修改版本:

$(function() {

    function beforeLoad( e, ui ) {

        ui.jqXHR.error(function( data ) {

            ui.panel.empty();

            $( "<p/>" ).addClass( "ui-corner-all ui-state-error" )
                       .css( "padding", "4px" )
                       .text( data.statusText )
                       .appendTo( ui.panel );
        });

    }

    function afterLoad( e, ui ) {
        $( "h1", ui.panel ).remove();
    }

    $( "#tabs" ).tabs({
        beforeLoad: beforeLoad,
        load: afterLoad
    });

});

现在看,你会发现选项卡面板内不再有标题了。我们在构造函数中传递给选项卡的 load 回调将查找并删除任何 h1 标签。load 事件在 Ajax 调用返回并将内容插入面板后触发。我们无需担心在我们的代码运行之后出现 h1 标签。

还有更多...

给选项卡添加图标

选项卡小部件使用锚元素,点击时激活各种选项卡面板以显示其内容。默认情况下,此锚元素仅显示文本,这在绝大多数情况下足够好。然而,在其他一些情况下,选项卡链接本身可能受益于图标。例如,一个房子图标有助于快速提示面板内容是什么,然后再激活它。让我们看看如何扩展选项卡的功能以支持将图标和文本作为选项卡按钮使用。

如何做...

我们将创建一个支持我们小部件的基本tabs div,如下所示:

<div id="tabs">
    <ul>
        <li data-icon="ui-icon-home">
            <a href="#home">Home</a>
        </li>
        <li data-icon="ui-icon-search">
            <a href="#search">Search</a>
        </li>
        <li data-icon="ui-icon-wrench">
            <a href="#settings">Settings</a>
        </li>
    </ul>
    <div id="home">
        <p>Home panel...</p>
    </div>
    <div id="search">
        <p>Search panel...</p>
    </div>
    <div id="settings">
        <p>Settings panel...</p>
    </div>
</div>

接下来,我们有我们的 JavaScript 代码,包括对了解如何利用我们在标记中包含的new data-icon属性的选项卡小部件的扩展。

(function( $, undefined ) {

$.widget( "ab.tabs", $.ui.tabs, {

    _processTabs: function() {

        this._super();

        var iconTabs = this.tablist.find( "> li[data-icon]" );

        iconTabs.each( function( i, v ) {

            var $tab = $( v ),
                iconClass = $tab.attr( "data-icon" ),
                iconClasses = "ui-icon " +
                              iconClass + 
                              " ui-tabs-icon",
                $icon = $( "<span/>" ).addClass( iconClasses ),
                $anchor = $tab.find( "> a.ui-tabs-anchor" ),
                $text = $( "<span/>" ).text( $anchor.text() );

            $anchor.empty()
                   .append( $icon )
                   .append( $text );

        });
    },

    _destroy: function() {

        var iconTabs = this.tablist.find( "> li[data-icon]" );

        iconTabs.each( function( i, v ) {

            var $anchor = $( v ).find( "> a.ui-tabs-anchor" ),
                text = $anchor.find( "> span:not(.ui-icon)" )
                              .text();

            $anchor.empty().text( text );

        });

        this._super();

    }

});

})( jQuery );

$(function() {

    $( "#tabs" ).tabs();

});

如果您在浏览器中查看此选项卡小部件,您会注意到每个选项卡按钮现在在按钮文本的左侧有一个图标。

如何做...

运作原理...

这个选项卡小部件的自定义之处在于,我们通过代表选项卡按钮的li元素传递数据。由于任何给定的选项卡小部件实例可能有任意数量的选项卡,通过options对象来指定哪个选项卡获取哪个图标是困难的。相反,我们简单地通过使用data-icon数据属性传递这些选项。该值是我们想要从主题框架中使用的图标类。

我们实现的更改实际上可以在标记本身手动完成,因为我们只是向小部件添加新元素和新类。但是,这种思维方式存在两个问题。首先,有大量手动注入的标记,可以根据一个数据属性的值生成,这违反了 DRY 原则,特别是如果您为多个选项卡小部件遵循这种模式。其次,我们将引入默认小部件实现不了解的新标记。这可能效果很好,但当事情停止按预期工作时,这可能很难诊断。因此,我们最好扩展选项卡小部件。

我们正在重写的_processTabs()方法将迭代具有data-icon属性的每个li元素,因为这些是我们需要操作的元素。data-icon属性存储要从主题框架中使用的图标类。我们构造一个使用ui-icon类与特定图标类一起使用的span元素。它还得到我们新的ui-tabs-icon类,正确定位元素在链接内。然后,我们获取选项卡按钮的原始文本并将其包装在一个div中。原因是,插入图标span,然后是文本span更容易。

简化选项卡主题

有时,我们的选项卡小部件的上下文对主题有重要的影响。当小部件位于文档顶部附近时,选项卡小部件的默认视觉组件效果最佳,也就是说,大部分页面内容都嵌套在选项卡面板中。相反,可能存在着一些既有的页面元素,可以通过选项卡小部件进行组织。这就是挑战所在——将诸如选项卡这样的顶级小部件塞入较小的块中可能会显得尴尬,除非我们能够找到一种方法来从选项卡中剥离一些不必要的主题组件。

如何做...

让我们首先创建一些标记以便基于选项卡小部件。它应该看起来像下面这样:

<div id="tabs-container">
    <div id="tabs">
        <ul>
            <li><a href="#tab1">Tab 1</a></li>
            <li><a href="#tab2">Tab 2</a></li>
            <li><a href="#tab3">Tab 3</a></li>
        </ul>
        <div id="tab1">
            <h3>Tab 1...</h3>
            <ul>
                <li>Item 1</li>
                <li>Item 2</li>
                <li>Item 3</li>
            </ul>
        </div>
        <div id="tab2">
            <h3>Tab 2...</h3>
            <ul>
                <li>Item 4</li>
                <li>Item 5</li>
                <li>Item 6</li>
            </ul>
        </div>
        <div id="tab3">
            <h3>Tab 3...</h3>
            <ul>
                <li>Item 7</li>
                <li>Item 8</li>
                <li>Item 9</li>
            </ul>
        </div>
    </div>
</div>

接下来,我们将定义一些由选项卡小部件和选项卡小部件容器使用的 CSS。

div.ui-tabs-basic {
    border: none;
    background: none;
}

div.ui-tabs-basic > ul.ui-tabs-nav {
    background: none;
    border-left: none;
    border-top: none;
    border-right: none;
}

#tabs-container {
    width: 22%;
    background: #f7f7f7;
    padding: 0.9em;
}

接下来是我们的 JavaScript 代码,它在文档准备就绪后创建选项卡小部件。

$(function() {

    $( "#tabs" ).tabs({
        create: function( e, ui ) {
            $( this ).addClass( "ui-tabs-basic" )
                     .find( "> ul.ui-tabs-nav" )
                     .removeClass( "ui-corner-all" );
        }
    });

});

它是如何工作的...

我们正在传递给选项卡构造函数的create函数在小部件创建后触发。这是我们添加自定义类ui-tabs-basic的地方,该类用于覆盖backgroundborder设置。这些是我们希望被移除的组件,因此我们只需将它们设置为none。我们还从选项卡导航部分中移除了ui-corner-all类,因为我们保留了底部边框,保留此类看起来不合适。

通常情况下创建此小部件,也就是不传递我们的create函数,则选项卡小部件将如下所示:

它的工作原理...

如您所见,选项卡小部件似乎是毫无考虑地塞入了#tabs-container元素中。在引入我们的简化之后,选项卡在其新上下文中呈现出更自然的外观。

它的工作原理...

还有更多...

如果您在整个 UI 中的多个位置使用此精简版本的选项卡小部件,则多次定义要传递给选项卡构造函数的函数回调可能会很麻烦。您可以一次定义回调函数并在构造函数中传递其引用,但是然后您仍然需要将回调函数暴露在外。从设计的角度来看,我们可能希望将此行为封装在选项卡小部件中,并通过小部件选项将其暴露给外部世界。以下是对此示例进行的修改:

(function( $, undefined ) {

$.widget( "ab.tabs", $.ui.tabs, {

    options: {
        basic: false
    },

    _create: function() {

        this._super();

        if ( !this.options.basic ) {
            return;
        }

        $( this.element ).addClass( "ui-tabs-basic" )
                         .find( "> ul.ui-tabs-nav" )
                         .removeClass( "ui-corner-all" );

    }

});

})( jQuery );

$(function() {

    $( "#tabs" ).tabs({
        basic: true
    });

});

在这里,我们将之前在回调中的功能移至选项卡构造函数中,但仅当basic选项设置为true时才执行,并且默认为false

将选项卡用作 URL 导航链接

选项卡小部件不仅限于使用预加载的 div 元素或通过进行 Ajax 调用来填充选项卡面板。一些应用程序已经构建了许多组件,并且有大量内容要显示。如果您正在更新一个网站或应用程序,特别是如果您已经在使用 jQuery UI 小部件,则选项卡小部件可能作为主要的导航形式是有用的。那么我们需要的是一些通用的东西,可以应用于每个页面,而开发人员使用小部件的努力不多。尽管选项卡小部件并不是为这样的目的而设计的,但我们不会让这阻止我们,因为稍加调整,我们就可以创建一个能够给我们带来所需功能的通用组件。

如何做...

我们将首先查看应用程序中一个页面上的内容。HTML 定义了选项卡小部件结构以及活动选项卡下显示的内容。

<div id="nav">
    <ul>
        <li>
            <a href="tab1.html">Tab 1</a>
        </li>
        <li>
            <a href="tab2.html">Tab 2</a>
        </li>
        <li>
            <a href="tab3.html">Tab 3</a>
        </li>
    </ul>
    <div>
        <p>Tab 1 content...</p>
    </div>
</div>

您会注意到此应用程序中有三个页面,并且它们都使用相同的小部件 HTML 结构;唯一的区别是选项卡内容段落。接下来,我们将定义我们的新导航小部件并在页面上创建它。相同的 JavaScript 代码包含在应用程序的每个页面中。

(function( $, undefined ) {

$.widget( "ab.nav", $.ui.tabs, {

    _initialActive: function() {

        var path = location.pathname,
            path = path.substring( path.search( /[^\/]+$/ ) ),
            tabs = this.tabs,
            $active = tabs.find( "> a[href$='" + path + "']" );

        return tabs.find( "a" )
                   .index( $active );

    },

    _eventHandler: function( event ) {

        window.open( $( event.target ).attr( "href" ), "_self" );

    },

    _createPanel: function( id ) {

        var panel = this.element.find( "> div:first" );

        if ( !panel.hasClass( "ui-tabs-panel" ) ) {
            panel.data( "ui-tabs-destroy", true )
                 .addClass( "ui-tabs-panel " +
                            "ui-widget-content " +
                            "ui-corner-bottom" );

        }

        return panel;

    },

    _getPanelForTab: function( tab ) {

        return this.element.find( "> div:first" );

    },

    load: $.noop

});

})( jQuery );

$(function() {

    $( "#nav" ).nav();

});

现在,当您与此导航小部件交互时,您会看到每次激活一个新的选项卡时,浏览器都会重新加载页面以指向选项卡的href;例如,tab3.html

如何做...

它是如何工作的...

让我们先看看我们创建的新nav小部件之前的 HTML 结构。首先要注意的是,我们在这里提供的 HTML 结构与选项卡小部件所期望的不同。我们有一个不带 ID 的div元素,用于保存页面的主要内容,因此没有任何选项卡链接可以引用它。但不用担心,这是有意为之的。nav小部件是为具有多个页面的站点或应用程序设计的——我们不会在此小部件中嵌入多个选项卡面板内容。由于我们对小部件使用的 HTML 进行了这种结构性变更,最好的做法是创建一个全新的小部件,而不仅仅是扩展选项卡小部件。这种方法将避免对选项卡小部件的 HTML 结构应该是什么样子产生混淆。

我们的nav小部件的目标,基于选项卡小部件,是激活适当的选项卡并将div元素呈现为所选的选项卡面板。当单击选项卡链接时,我们不执行任何常规的选项卡活动,只是跟随href

nav 小部件的定义中覆盖的所有方法都来自标签小部件,而且在大多数情况下,我们都替换了不需要的标签功能。第一个方法是 _initialActive(),它确定小部件首次创建时的活动选项卡。在这里,我们将此决定基于位置对象中的路径。我们将其与选项卡的 href 属性进行比较。接下来是 _eventHandler() 方法。当用户激活选项卡时,将调用此方法。在这里,我们只执行与默认浏览器链接相同的操作,并遵循选项卡链接的 href 属性。由于我们在 _eventHandler() 方法中执行此操作,因此用于切换选项卡的 keypress 事件仍将按预期工作。接下来,当标签小部件需要创建和插入选项卡面板时,将调用 _createPanel() 方法。标签小部件调用此方法的原因是在进行 Ajax 调用时需要面板。由于我们在 nav 小部件中不进行任何 Ajax 调用,因此此方法现在将使用具有页面内容的默认 div。我们对内容 div 做的唯一更改是添加了适当的选项卡面板 CSS 类。最后,我们有 _getPanelForTab() 方法,该方法返回我们的内容 div,对于此小部件,这是唯一重要的内容 div,并且 load() 方法是 $.noop。这样做可以防止小部件在首次创建时尝试加载 Ajax 内容。

在选项卡之间创建效果

标签小部件允许开发人员指定在选项卡之间进行转换时运行的效果。具体来说,我们能够告诉标签小部件在显示选项卡时运行特定效果,并在隐藏选项卡时运行另一个效果。当用户点击选项卡时,如果指定了这两个效果,则会运行它们。首先是隐藏效果,然后是显示效果。让我们看看如何结合这两个选项卡选项来增强小部件的互动性。

如何做到这一点...

首先,我们将创建我们构建选项卡小部件所需的 HTML 结构。它应该看起来类似于以下内容,生成三个选项卡:

<div id="tabs">
    <ul>
        <li><a href="#tab1">Tab 1</a></li>
        <li><a href="#tab2">Tab 2</a></li>
        <li><a href="#tab3">Tab 3</a></li>
    </ul>
    <div id="tab1">
        <p>Tab 1 content...</p>
        <button>Tab 1 Button</button>
    </div>
    <div id="tab2">
        <p>Tab 2 content...</p>
        <strong>Tab 2 bold text</strong>
    </div>
    <div id="tab3">
        <p>Tab 3 content...</p>
        <p>...and more content</p>
    </div>
</div>

下面的 JavaScript 代码实例化了标签小部件,其中 showhide 效果选项传递给小部件构造函数。

$(function() {

    $( "#tabs" ).tabs({
        show: {
            effect: "slide",
            direction: "left"
        },
        hide: {
            effect: "drop",
            direction: "right"
        }
    });

});

它是如何工作的...

当您在浏览器中查看此选项卡小部件并点击选项卡时,您会注意到当前选项卡的内容向右滑动,同时淡出。一旦此效果执行完毕,当前活动选项卡的 show 效果就会运行,在这种情况下,内容从左侧滑入。这两种效果相辅相成——结合在一起,它们产生了新内容将旧内容推出面板的幻觉。

我们在这里选择的两种效果实际上非常相似。drop效果实际上只是slide效果,额外加上了在滑动时的淡入淡出。它们协作的关键是我们传递给每个effect对象的direction属性。我们告诉hide效果在运行时向右移动。同样,我们告诉show效果从左侧进入。

使用可排序交互进行标签排序

当我们在用户界面中实现标签时,我们可能会简短地考虑标签的默认排序。显然,我们希望最相关的标签按照用户最能理解的顺序进行访问。但通常我们无法以让所有人满意的方式做到这一点。那么为什么不让用户自行安排标签的顺序呢?让我们看看能否通过在标签小部件中提供这种功能来利用可排序交互小部件来提供帮助。

如何实现...

我们将使用以下 HTML 作为驱动我们标签实例的示例:

<div id="tabs">
    <ul>
        <li><a href="#tab1">Tab 1</a></li>
        <li><a href="#tab2">Tab 2</a></li>
        <li><a href="#tab3">Tab 3</a></li>
    </ul>
    <div id="tab1">
        <p>Tab 1 content...</p>
    </div>
    <div id="tab2">
        <p>Tab 2 content...</p>
    </div>
    <div id="tab3">
        <p>Tab 3 content...</p>
    </div>
</div>

接下来,我们将在标签小部件中实现新的sortable选项。我们还需要扩展小部件的行为以利用这个新选项。

(function( $, undefined ) {

$.widget( "ab.tabs", $.ui.tabs, {

    options: {
        sortable: false
    },

    _create: function() {

        this._super();

        if ( !this.options.sortable ) {
            return;
        }

        this.tablist.sortable({
            axis: "x",
            stop: $.proxy( this, "_stopped" )
        });

    },

    _destroy: function() {

        if ( this.options.sortable ) {
            this.tablist.sortable( "destroy" );
        }

        this._super();

    },

    _stopped: function( e, ui ) {
        this.refresh();
    }

});

})( jQuery );

$(function() {

    $( "#tabs" ).tabs({
        sortable: true
    });

});

现在当您沿着 x 轴拖动标签按钮时,放下它们将重新排列它们的顺序。例如,拖动第一个标签会看起来像这样:

如何实现...

如果我们将第一个标签放在末尾并激活 Tab 2,现在第一个标签,你应该看到类似这样的东西:

如何实现...

它是如何工作的...

我们已经向标签小部件添加了一个新选项,sortable,当设置为 true 时,将使用可排序交互小部件来启用标签按钮的可排序行为。我们通过在options对象中将默认sortable值设置为false来添加了这个选项。该对象将与默认标签选项合并。在_create()方法中,标签构造函数中,我们调用原始的标签小部件构造函数,因为默认小部件构造不存在特殊情况。接下来,在_create()内部,我们检查sortable选项是否为true,如果是,就创建可排序小部件。我们使用tablist属性来创建可排序小部件,这是一个包含标签按钮的ul元素。这就是为什么我们在这里调用它,我们想让它的子元素在 x 轴上可以进行排序。

我们还将可排序小部件的stop选项传递给一个回调函数,这种情况下是_stopped()方法的代理。这里使用了$.proxy()实用程序,这样我们可以像实现标签的常规方法一样实现_stopped()。请注意在_stopped()的实现中,这是小部件实例,而没有代理,它会是ul元素。

最后,在这里重写了_destroy()方法以确保可排序小部件被销毁。如果不这样做,我们就无法可靠地销毁并重新创建标签小部件。

更多信息...

当将sortable选项设置为true时,我们可以进一步增强选项卡小部件的用户交互。首先,让我们在用户拖动选项卡时修改cursor,以便使用标准的移动图标。接下来,我们将激活放置的选项卡。这是我们为修改后的光标所需的 CSS;我们将保持先前的 HTML 结构不变:

.ui-tabs .ui-tabs-nav li.ui-tab-move > a {
    cursor: move;
}

这是修改后的 JavaScript 代码:

(function( $, undefined ) {

$.widget( "ab.tabs", $.ui.tabs, {

    options: {
        sortable: false
    },

    _create: function() {

        this._super();

        if ( !this.options.sortable ) {
            return;
        }

        this.tablist.sortable({
            axis: "x",
            start: $.proxy( this, "_started" ),
            stop: $.proxy( this, "_stopped" )
        });

    },

    _destroy: function() {

        if ( this.options.sortable ) {
            this.tablist.sortable( "destroy" );
        }

        this._super();

    },

    _started: function( e, ui ) {
        ui.item.addClass( "ui-tab-move" );
    },

    _stopped: function( e, ui ) {

        ui.item.removeClass( "ui-tab-move" );
        this.refresh();
        this._activate( ui.item.index() );

    }

});

})( jQuery );

$(function() {

    $( "#tabs" ).tabs({
        sortable: true
    });

});

现在,当您对这些选项卡进行排序时,您会注意到新的光标如以下截图所示。ui-tab-move类定义了cursor的 CSS 属性,此类被添加到可排序小部件的start事件处理程序中的li元素中。在stop处理程序中随后删除。您还会注意到当放置选项卡时会激活选项卡。这是通过获取li元素的索引并将其传递给activate()方法来完成的。

还有更多...

使用 href 设置活动选项卡

选项卡小部件允许开发人员通过将零基索引值传递给active选项来以编程方式设置活动选项卡。这可以通过在选项卡构造函数中设置此选项来完成,告诉小部件默认激活哪个选项卡,或者可以在之后设置,从而可能改变活动选项卡。使用此选项更改活动选项卡实质上与用户点击选项卡按钮激活面板是相同的。然而,我们可以改进此界面,让使用选项卡小部件的开发人员传递href值而不是索引值。这样,您就不必记住选项卡的顺序—哪个数字代表哪个链接,等等。

如何实现...

让我们首先设置此演示中使用的 HTML 作为选项卡小部件的基础。

<div id="tabs">
    <ul>
        <li><a href="#tab1">Tab 1</a></li>
        <li><a href="#tab2">Tab 2</a></li>
        <li><a href="#tab3">Tab 3</a></li>
    </ul>
    <div id="tab1">
        <p>Tab 1 content...<a class="tab-link" href="#tab2">tab 2</a></p>
    </div>
    <div id="tab2">
        <p>Tab 2 content...<a class="tab-link" href="#tab3">tab 3</a></p>
    </div>
    <div id="tab3">
        <p>Tab 3 content...<a class="tab-link" href="#tab1">tab 1</a></p>
    </div>
</div>

下面是修改后的选项卡小部件实现,使我们可以通过将字符串"#tab2"传递给active选项来激活第二个选项卡。

(function( $, undefined ) {

$.widget( "ab.tabs", $.ui.tabs, {

    _findActive: function( index ) {
        return this._super( this._getIndex( index ) );
    },

    _initialActive: function() {

        this.options.active = this._getIndex( this.options.active );
        return this._super();

    }

});

})( jQuery );

$(function() {

    $( "#tabs" ).tabs({
        active: "#tab2"
    });

    $( ".tab-link" ).on( "click", function( e ) {
        e.preventDefault();
        $( "#tabs" ).tabs( "option", "active", $( this ).attr( "href" ) );
    });

});

它是如何运作的...

当您在浏览器中查看此选项卡小部件时,您会注意到第二个选项卡默认处于激活状态,因为我们传递了字符串"#tab2"。还会注意到每个选项卡面板的内容都指向另一个选项卡的链接。

它是如何运作的...

我们正在扩展选项卡小部件,以便我们可以重写一些选项卡方法。第一个方法是_findActive(),在原始实现中期望一个整数。我们已经改变了这一点,使用了根据选项卡按钮的href属性返回索引的_getIndex()方法,也就是说,除非它得到传递给它的整数值,否则它只返回那个数字。简而言之,我们已经改变了_findActive(),以接受零基索引号,或href字符串。

下一个方法是 _initialActive(),当标签小部件首次实例化时调用。我们在这里要做的是在调用 _initialActive() 的原始实现之前,将活动选项设置为适当的索引值。这是为了支持构造函数中的 href 字符串作为 active 选项值。

最后,我们使用 href 字符串创建我们的标签小部件,并将事件处理程序绑定到标签面板中的每个标签链接上。在这里,我们仅基于链接的 href 属性激活标签,所以你可以看到我们引入的这种新 href 功能的价值。

还有更多...

在前面的示例中,我们利用了标签按钮链接的 href 属性。但是,我们没有利用浏览器的位置哈希。换句话说,当激活标签时,浏览器 URL 中的位置哈希不会更改。支持这种方法有几个优点。首先,我们可以使用返回按钮浏览我们的活动标签。另一个好处是,标签内容面板中的链接不再需要事件处理程序;它们可以直接指向标签 href

这里是修改后的 JavaScript,支持与上一个示例相同的功能。唯一的区别是,每次激活标签时,URL 哈希都会更改。

(function( $, undefined ) {

$.widget( "ab.tabs", $.ui.tabs, {

    _create: function() {

        this._super();

        this._on( window, { 
            hashchange: $.proxy( this, "_hashChange" )
        });

    },

    _hashChange: function( e ) {

        if ( this.active.attr( "href" ) === location.hash ) {
            return;
        }

        this._activate( this._getIndex( location.hash ) );

    },

    _eventHandler: function( e ) {

        this._super( e );  

        var href = $( e.target ).attr( "href" );

        if ( href === location.hash ) {
            return;
        }

        if ( href.indexOf( "#" ) === 0 ) {
            location.hash = href;
        }
        else {
            location.hash = "";
        }

    }

});

})( jQuery );

$(function() {
    $( "#tabs" ).tabs();
});

现在,当你在浏览器中与此标签小部件交互时,你会注意到在导航标签时 URL 中的哈希会更改。这是通过在调用 _create() 的原始实现后向该方法添加事件处理程序来完成的。我们使用 _on() 实用程序方法订阅窗口的 hashchange 事件。接下来,我们添加的 _hashChange() 方法是此事件的处理程序。首先,我们检查 URL 哈希,存储在 location.hash 变量中,是否已经指向活动标签。如果没有,我们根据当前 URL 哈希值激活标签。这是我们支持指向 URL 哈希的标签面板内容中的链接所需的全部内容。但是,当用户直接单击标签按钮时,哈希值不会更改。这对我们没有帮助,因为我们无法跟踪标签导航历史记录。

这就是为什么我们实现了 _eventHandler() 方法的自定义。我们首先调用方法的原始实现,然后再处理 URL 哈希的具体情况。如果 URL 哈希已经指向活动标签,我们在这里没有任何操作;否则,我们更新 URL 哈希。

第十一章:使用工具提示

在本章中,我们将涵盖:

  • 改变工具提示状态

  • 在工具提示中使用自定义标记

  • 显示鼠标移动

  • 对工具提示显示应用效果

  • 选定文本的工具提示

介绍

在本章中,我们将探讨用于向用户提供上下文信息的工具提示小部件的各个方面。工具提示小部件与现有代码配合得很好,因为默认情况下,它使用标准的 HTML 属性来设置工具提示的文本。此外,只需一行代码就可以为整个用户界面创建工具提示实例,非常容易。

超越简单用例,我们将研究我们可以传递到小部件中的不同类型的内容,以及如何动态生成内容。我们还将探讨工具提示如何作为工具来辅助开发过程,以及开发人员如何操纵可用的效果来显示和隐藏小部件。

改变工具提示状态

工具提示小部件的视觉显示有一个默认状态。也就是说,默认情况下,该小部件经过精心设计,使用了主题框架中的元素。然而,我们可能会根据应用程序中某些资源的状态而进行更改。例如,由于权限更改而对用户新的按钮可能希望工具提示状态在页面上与其他工具提示有视觉上的差异。同样,如果存在损坏的资源,并且用户将鼠标悬停在其组件上,则显示的工具提示应处于错误状态。当然,当更改工具提示的状态时,我们应该记住状态应该与实际工具提示的上下文和语气相匹配。例如,不要在读取“一切都准备就绪!”的工具提示上放置错误状态。让我们看看自定义工具提示的一个快速而简单的入口点。我们将使用一个标准的工具提示选项来传递状态 CSS 类。

如何做...

我们将使用以下 HTML 为我们的工具提示小部件。这里有三个按钮,每个按钮都有自己的状态和工具提示实例。

<div class="button-container">
    <button class="tt-default" title="I'm using the default tooltip state">Default</button>
</div>
<div class="button-container">
    <button class="tt-highlight" title="I'm using the highlight tooltip state">Highlight</button>
</div>
<div class="button-container">
    <button class="tt-error" title="I'm using the error tooltip state">Error</button>
</div>

接下来,我们将使用以下 JavaScript 为其各自的按钮创建工具提示小部件:

$(function() {

    $( "button" ).tooltip();

    $( "button.tt-highlight" ).tooltip( "option", { 
        tooltipClass: "ui-state-highlight" 
    });

    $( "button.tt-error" ).tooltip( "option", {
        tooltipClass: "ui-state-error"
    });

});

在浏览器中悬停在每个按钮上会显示默认状态、高亮状态和错误状态,如下图所示:

  • 默认状态:如何做...

  • 高亮状态:如何做...

  • 错误状态:如何做...

工作原理...

对于这个特定的例子,我们使用tooltipClass选项将主题框架中的状态 CSS 类传递给小部件。首先,我们简单地将页面上的每个按钮都设置为提示小部件。调用提示构造函数后,我们有三个使用默认状态的提示实例。接下来,我们找到带有tt-highlight类的按钮,并将tooltipClass选项的值设为ui-state-highlight。最后,我们找到带有tt-error类的按钮,并使用tooltipClass选项将该提示小部件分配给ui-state-error类。

还有更多...

我们之前使用的方法有一些缺点。首先,用户无法知道有什么问题,直到他们将鼠标移到元素上并看到提示处于错误状态。在更现实的情况下,如果按钮有什么问题,它可能会自身应用错误状态。因此,为了应用错误状态,我们不得不发明自己的类名,并在创建提示时确定使用哪个类。

一个更健壮的解决方案将围绕在元素上使用框架的实际状态而不是发明我们自己的状态。此外,提示小部件应该足够智能,以根据应用的元素的状态更改其类。换句话说,如果按钮应用了ui-state-error类,则应该将此类用作tooltipClass选项。让我们为提示小部件添加一个inheritState选项,以打开此行为。

这是修改后的 HTML 源代码:

<div class="button-container">
    <button title="I'm using the default tooltip state">Default</button>
</div>
<div class="button-container">
    <button class="ui-state-highlight" title="I'm using the highlight tooltip state">Highlight</button>
</div>
<div class="button-container">
    <button class="ui-state-error" title="I'm using the error tooltip state">Error</button>
</div>

下面是包含新选项的提示小部件扩展的定义:

(function( $, undefined ) {

$.widget( "ab.tooltip", $.ui.tooltip, {

    options: {
        inheritState: false
    },

    _create: function() {

        var self = this,
            options = this.options,
            states = [
                "ui-state-highlight",
                "ui-state-error"
            ];

        if ( !options.inheritState || options.tooltipClass ) {
            return this._super();
        }

        $.each( states, function( i, v ) {

            if ( self.element.hasClass( v ) ) {
                self.options.tooltipClass = v;
            }

        });

        this._super();

    }

});

})( jQuery );

$(function() {

    $( "button" ).tooltip({
        inheritState: true
    });

});

这个版本的代码应该与第一个版本的行为完全相同。当然,区别在于按钮本身具有可见状态,我们希望提示小部件能够捕捉到这一点。我们通过将inheritState选项设置为true来告诉它这样做。

还有更多...

我们的新选项inheritState被添加到提示小部件的默认options对象中,该对象由提示小部件的原始实现设置。在_create()方法中,小部件构造函数中,我们检查inheritState选项是否为true,或者tooltipClass选项是否已设置。在任何一种情况下,我们都返回,调用原始实现。否则,我们检查元素是否具有states数组中的任何状态,如果是,则将该类设置为tooltipClass

在提示中使用自定义标记

我们不限于使用title属性来提供基本文本字符串以供工具提示内容使用。有时,工具提示部件的内容需要格式化。例如,标题部分的字体样式将与主文本部分不同。工具提示部件允许开发人员通过content选项传递自定义内容。这可以是原始字符串,也可以是返回我们想要显示的内容的函数。让我们看看如何在您的应用程序中使用此选项。

操作步骤...

我们将创建两个button元素;每个都有一个title属性,其中的文本将用于工具提示。我们还将添加按钮的名称作为工具提示标题。

<div class="button-container">
    <button title="Logs the user in by establishing a new session.">Login</button>
</div>
<div class="button-container">
    <button title="Deactivates the session, and logs the user out.">Logout</button>
</div>

接下来,让我们创建格式化我们的工具提示的基本 CSS 样式。

.ui-tooltip-title {
    font-weight: bold;
    font-size: 1.1em;
    margin-bottom: 5px;
}

最后,我们将使用自定义内容函数创建工具提示部件来格式化工具提示内容。

$(function() {

    $( "button" ).tooltip({
        content: function() {

            var $content = $( "<div/>" );

            $( "<div/>" ).text( $( this ).text() )
                         .addClass( "ui-tooltip-title" )
                         .appendTo( $content );

            $( "<span/>" ).text( $( this ).attr( "title" ) )
                          .appendTo( $content );

            return $content;

        }

    });

});

当我们悬停在其中一个button元素上时,工具提示应该看起来像以下屏幕截图一样。注意格式化的标题部分。

操作步骤...

工作原理...

我们向每个工具提示部件传递的content函数将内容包装成一个div元素,存储在$content变量中。目的是将标题和主文本元素存储在此div中,这样我们就可以简单地从函数中返回$content变量。标题div使用按钮文本或其名称。这个div得到了我们之前定义的ui-tooltip-title类,它只是修改了字体,并在元素底部添加了一些空间。接下来,我们添加了主内容span元素,它只是使用元素的title属性。

还有更多...

我们刚刚检查过的修改工具提示的方法是自由形式的——函数可以返回几乎任何它想要的东西。让我们看看修改工具提示内容的更结构化的方法。我们将修改工具提示部件,使其接受特定的内容部分选项。为了演示这一点,我们将利用Rotten Tomatoes API。我们唯一需要的 HTML 是一个简单的div元素,看起来像<div class="titles"></div>。现在让我们定义标题的 CSS 样式,我们将要列出的标题,以及特定的工具提示内容部分。

.titles { 
    margin: 20px;
}

.titles img {
    padding: 10px;
}

.ui-tooltip-header {
    font-weight: bold;
    font-size: 1.4em;
}

.ui-tooltip-body {
    margin: 7px 0 7px 0;
    font-size: 1.2em;
}

.ui-tooltip-footer {
    font-weight: bold;
    border-top: solid 1px;
    padding-top: 7px;
}

这是自定义的工具提示部件声明,它添加了新的内容选项。当文档加载时,我们调用 Rotten Tomatoes API,并在我们的容器div中显示五张图片。每张图片也是一个工具提示,它使用了我们已添加到部件的新特定内容选项。

(function( $, undefined ) {

$.widget( "ab.tooltip", $.ui.tooltip, {

    options: {
        header: null,
        body: null,
        footer: null
    },

    _create: function() {

        this._super();

        var header = this.options.header,
            body = this.options.body,
            footer = this.options.footer;

        if ( !header && !body && !footer ) {
            return;
        }

        this.options.content = $.proxy( this, "_content" );

    },

    _content: function() {

        var header = this.options.header,
            body = this.options.body,
            footer = this.options.footer,
            $content = $( "<div/>" );

        if ( header ) {

            $( "<div/>" ).text( header )
                         .addClass( "ui-tooltip-header" )
                         .appendTo( $content );

        }

        if ( body ) {

            $( "<div/>" ).text( body )
                         .addClass( "ui-tooltip-body" )
                         .appendTo( $content );

        }

        if ( footer ) {

            $( "<div/>" ).text( footer )
                         .addClass( "ui-tooltip-footer" )
                         .appendTo( $content );

        }

        return $content;

    }

});

})( jQuery );

$(function() {

    var apikey = "2vnk...",  // Your Rotten Tomatoes API key goes here
        apibase = "http://api.rottentomatoes.com/api/public/v1.0";

    $.ajax({
        url: apibase + "/lists/movies/in_theaters.json",
        dataType: "jsonp",
        data: {
            apikey: apikey,
            page_limit: "5",
        },
        success: function( data ) {

            $.each( data.movies, function( i, v ) {

                var $logo = $( "<img/>" );

                $logo.attr( "src", v.posters.thumbnail )
                     .appendTo( ".titles" );

                $logo.tooltip({
                    header: v.title,
                    body: v.synopsis.substring( 0, 150 ) + "...",
                    footer: v.year + " (" + v.mpaa_rating + ")",
                    items: "img"
                });

            });

        }

    });

});

在浏览器中查看此页面应该会用五张图片填充标题div,当您将鼠标悬停在每个图片上时,您应该会看到我们的自定义工具提示内容。

还有更多...

让我们首先查看当文档加载完成时我们正在向 Rotten Tomatoes API 发出的 API 调用。我们要获取的仅是正在上映的目录中的前五部电影。然后,我们创建一个 img 元素并将 src 属性设置为相应电影的缩略图。这就是你在示例中看到的图片是如何呈现的。我们还对每个图像调用工具提示构造函数,并向其传递我们定义的新选项。具体来说,这些是工具提示内容的部分,headerbodyfooter。请注意,我们必须告诉工具提示这是一个 img 元素,它不会在通常的位置找到工具提示内容 - 这是使用 items 选项完成的。

现在看看我们在工具提示小部件中实现的自定义内容,我们可以看到选项是通过向 options 属性分配新选项来定义的 - 这些选项会合并到默认工具提示 options 对象中。接下来,我们有一个 _create() 方法的自定义实现,当工具提示被实例化时会调用该方法。这里的目标是检查是否已指定了三个内容部分之一,如果没有,则我们无事可做,简单地退出。 _create() 方法的原始版本是使用 _super() 调用的,因此在此时,小部件已经被创建。构造函数中的我们的最后一项工作是将 content 选项分配给生成工具提示内容的函数。在这种情况下,它是一个代理到 _content() 方法的函数。

_content() 方法将其返回的 HTML 包装在一个 div 元素中,这存储在 $content 变量中。然后,我们根据选项将指定的内容添加到 div 元素中。每个内容部分都是一个 div 元素,并且它们被赋予相应的 CSS 类来控制外观 - ui-tooltip-headerui-tooltip-bodyui-tooltip-footer

显示鼠标移动

在开发过程中,我们可以使用工具提示小部件作为辅助工具,但不一定要将其作为最终产品的一部分。例如,我们可以使用工具提示小部件来跟踪鼠标移动并显示 X 和 Y 坐标。这有助于我们在组装 UI 组件时诊断一些棘手的鼠标行为。我们将研究跟踪特定元素的鼠标坐标,但请记住,重要的是概念。我们可以使用此技术来显示任意数量的事件属性 - 当不再需要时,我们只需丢弃调用。

如何做到…

首先我们将创建所需的 CSS。这些简单地定位我们希望跟踪鼠标移动的 div 元素。

.mouse-tracker {
    margin: 20px;
    background-image: none;
    padding: 3px;
}

.mouse-tracker p {
    font-size: 1.2em;
}

.mouse-tracker-page {
    width: 180px;
    height: 170px;
}

.mouse-tracker-relative {
    width: 150px;
    height: 140px;
}

接下来是 HTML 本身,两个我们正在设计中的 div 元素。我们希望我们的鼠标跟踪实用程序在用户将鼠标移动到这些元素上时显示出发生了什么。

<div class="ui-widget-content mouse-tracker mouse-tracker-page">
    <p>Page mouse movement</p>
</div>
<div class="ui-widget-content ui-state-default mouse-tracker mouse-tracker-relative">
    <p>Element mouse movement</p>
</div>

最后但同样重要的是,我们将实现我们的跟踪器工具。这是一个名为跟踪器的小部件,它扩展了提示小部件。我们称其为其他内容,以免将其与我们可能在生产系统中使用的现有提示小部件混淆。

(function( $, undefined ) {

$.widget( "ab.tracker", $.ui.tooltip, {

    options: {
        track: true,
        items: ".ui-tracker",
        relative: false
    },

    _create: function() {

        this.element.addClass( "ui-tracker" );

        this._super();

        this.options.content = $.proxy( this, "_content" );

    },

    _content: function() {

        var $content = $( "<div/>" ),
            relative = this.options.relative,
            xlabel = relative ? "Element X: " : "Page X: ",
            ylabel = relative ? "Element Y: " : "Page Y: ";

        $( "<div/>" ).append( $( "<strong/>" ).text( xlabel ) )
                     .append( $( "<span/>" ).attr( "id", "ui-tracker-x" ) )
                     .appendTo( $content );

        $( "<div/>" ).append( $( "<strong/>" ).text( ylabel ) )
                     .append( $( "<span/>" ).attr( "id", "ui-tracker-y" ) )
                     .appendTo( $content );

        return $content;

    },

    _mousemove: function( e ) {

        var $target = $( e.target ).closest( this.options.items ),
            offset,
            offsetLeft = 0
            offsetTop = 0;

        if ( this.options.relative ) {
            offset = $target.offset();
            offsetLeft = offset.left;
            offsetTop = offset.top;
        }

        $( "#ui-tracker-x" ).text( e.pageX - offsetLeft );
        $( "#ui-tracker-y" ).text( e.pageY - offsetTop );

    },

    open: function( e ) {

        this._super( e );

        var $target = $( e.target ).closest( this.options.items );

        this._on( $target, {
            mousemove: $.proxy( this, "_mousemove" )
        });

    }

});

})( jQuery );

$(function() {

    $( ".mouse-tracker-page" ).tracker();
    $( ".mouse-tracker-relative" ).tracker({
        relative: true
    });

});

在浏览器中查看这两个div元素,您应该会看到类似以下的内容:

操作步骤...

工作原理...

我们刚刚定义的跟踪器小部件通过填充一些新的默认选项以及提供一个新选项来扩展提示小部件。track提示选项告诉小部件相对于鼠标移动定位自己。由于我们正在实现鼠标坐标跟踪器,将其默认打开是有道理的。我们希望更改的下一个提示选项值是items选项。这告诉提示哪些目标元素可以成为有效的提示,而在我们的情况下,我们希望它是赋予我们跟踪器小部件的类—ui-trackerrelative选项是我们要添加到小部件中的新内容。这告诉跟踪器,当为true时,将坐标显示为相对于问题元素,而不是相对于页面,默认情况下是相对于页面的。

接下来,我们要扩展提示小部件的_create()方法,这是构造函数。在调用构造函数的原始实现之前,我们要做的第一件事是将跟踪小部件类添加到元素中。这是必要的,以便元素被视为有效的跟踪器—参见items选项。一旦我们完成了_super()方法,我们就会将content选项分配给回调函数,这是对此小部件的_callback()方法的代理。_callback()方法只是返回我们想要显示在提示中的模板内容。这包括鼠标事件的 X 和 Y 坐标。根据relative选项,我们必须弄清楚标签是应该是一个页面,还是应该是一个元素。

我们重写open()方法来设置我们的mousemove事件处理。通常,这将在_create()方法中完成。但是当提示未打开时,没有必要跟踪鼠标移动,而且触发回调会浪费宝贵的 CPU 周期。我们使用_on()实用程序方法将代理处理程序绑定到此小部件的_mousemove()方法。_mousemove()方法负责更新提示的内容。具体来说,它设置由我们的_content()方法生成的#ui-tracker-x#ui-tracker-y标签的文本值。X 和 Y 坐标的值将基于事件的pageXpageX属性的值,或者与偏移值结合,具体取决于relative选项。

跟踪器小部件的实例化方式与提示小部件相同。当我们不再需要显示这些值时,例如,当我们准备好上线时,这些小部件调用将被删除。

对提示显示应用效果

工具提示小部件附带了控制元素显示和隐藏动作的选项。这些分别是showhide选项,每个选项都接受指定动画选项的对象。由于showhide选项控制小部件显示的不同方面,我们可以自由使用不同的设置,例如显示和隐藏操作的延迟。或者,我们可以彻底改变,对动画使用两种完全不同的效果。让我们探索工具提示小部件中可用的各种showhide选项。

如何操作...

首先,让我们创建一些按钮元素,我们将用它们来显示工具提示。

<div class="button-container">
    <button class="drop" title="I'm using the drop effect">Drop</button>
</div>
<div class="button-container">
    <button class="slide" title="I'm using the slide effect">Slide</button>
</div>
<div class="button-container">
    <button class="explode" title="I'm using the clip/explode effect">Explode</button>
</div>

接下来,我们将为每个按钮实例化一个工具提示小部件,传递我们自定义的showhide动画选项。

$(function() {

    $( "button" ).tooltip();

    $( "button.drop" ).tooltip( "option", {
        show: {
            effect: "drop",
            delay: 150,
            duration: 450,
            direction: "up",
        },
        hide: {
            effect: "drop",
            delay: 100,
            duration: 200,
            direction: "down"
        }
    });

    $( "button.slide" ).tooltip( "option", {
        show: {
            effect: "slide",
            delay: 250,
            duration: 350,
            direction: "left"
        },
        hide: {
            effect: "slide",
            delay: 150,
            duration: 350,
            direction: "right",
        }
    });

    $( "button.explode" ).tooltip( "option", {
        show: {
            effect: "clip",
            delay: 150,
            duration: 450
        },
        hide: {
            effect: "explode",
            delay: 200,
            duration: 1000
        }
    });

});

在您的网络浏览器中查看三个按钮,并将鼠标移到每个按钮上。您会注意到它们以独特的方式显示和隐藏工具提示。例如,这是最后一个工具提示,正在被隐藏时的中爆炸。

如何操作...

它是如何工作的...

有些效果接受其他效果不接受的选项,例如方向。button.drop工具提示小部件同时对显示和隐藏操作使用drop效果。然而,show指定了directionup,而hide操作指定了directiondown。这意味着工具提示将以向上的方式进入页面,并以向下的方式退出页面。相同的概念也适用于button.slide小部件,其中我们使用slide效果。工具提示将从左侧滑入,并从右侧滑出。

button.explode工具提示使用两种不同的效果类型——show使用clip效果,而hide使用explode效果。一般来说,像这样混合效果是可以的,但通常需要一些时间进行尝试和错误,找到两种互补而不是看起来不合适的效果类。最后,我们将delayduration选项应用于我们创建的工具提示的showhide选项。delay选项推迟工具提示的实际显示,而duration控制动画的运行时间。

选定文本的工具提示

大多数应用程序使用用户首次遇到的术语。因此,提供类似词汇表的东西是有帮助的,这样他们可以查找新术语的含义。但是,在用户界面中放置这个词汇表的位置是一件大事。例如,如果我正在执行某项任务,我不想中断去查找某些内容。这就是工具提示的帮助之处——用户会得到某些内容的上下文解释。

默认情况下,工具提示在应用于页面上特定元素(例如按钮或进度条)时效果很好。但是对于一段文字呢?让我们看看如何允许用户选择一些文本,并使用工具提示小部件显示所选内容的上下文定义。

如何操作...

我们将设计一个新的词典小部件,基于提示小部件,用于处理文本。这个小部件将通过显示提示(如果找到)来处理文本选择。首先,这里是我们将使用的段落,取自前一节。

<p>
    Most applications use terms that the user is encountering for the first 
    time.  And so, it's helpful to provide a glossary of sorts so they may 
    lookup the meaning of a new term.  However, deciding on where to put this 
    glossary inside the user interface is a big deal.  For example, if I'm 
    performing some task, I don't want to drop it to go look something up.  
    This is where tooltips help – the user gets a contextual explanation 
    of something.
</p>

<p>
    Out of the box, tooltips work great when applied to a specific element on 
    the page, such as a button or a progressbar. But what about paragraphs of 
    text?  Let's look at how we could allow the user to select some text, and 
    display some contextual definition for the selection using the tooltip 
    widget.
</p>

这里是词典小部件的实现以及如何将其应用于我们的两段文本。

( function( $, undefined ) {

$.widget( "ab.dictionary", {

    options: {
        terms: []
    },

    ttPos: $.ui.tooltip.prototype.options.position,

    _create: function() {

        this._super();

        this._on({
            mouseup: this._tip,
            mouseenter: this._tip
        });

    },

    _destroy: function() {
        this._super();
        this._destroyTooltip();
    },

    _tip: function( e ) {

        var text = this._selectedText(),
            term = this._selectedTerm( text );

        if ( text === undefined || term === undefined ) {
            this._destroyTooltip();
            return;
        }

        if ( this.element.attr( "title" ) !== term.tip ) {
            this._destroyTooltip();
        }

        this._createTooltip( e, term );

    },

    _selectedText: function() {

        var selection, range, fragment;

        selection = window.getSelection();

        if ( selection.type !== "Range" ) {
            return;
        }

        range = selection.getRangeAt( 0 ),
        fragment = $( range.cloneContents() );

        return $.trim( fragment.text().toLowerCase() );

    },

    _selectedTerm: function( text ) {

        function isTerm( v ) {
            if ( v.term === text || v.term + "s" === text ) {
                return v;
            }
        }

        return $.map( this.options.terms, isTerm )[ 0 ];

    },

    _createTooltip: function( e, term ) {

        if ( this.element.is( ":ui-tooltip" ) ) {
            return;
        }

        var pos = $.extend( this.ttPos, { of: e } );

        this.element.attr( "title", term.tip )
                    .tooltip( { position: pos } )
                    .tooltip( "open" );
    },

    _destroyTooltip: function() {

        if ( !this.element.is( ":ui-tooltip" ) ) {
           return;
        }

        this.element.tooltip( "destroy" )
                    .attr( "title", "");

    }

});

})( jQuery );

$(function() {

    var dict = [
        {
            term: "tooltip",
            tip: "A contextual widget providing information to the user"
        },
        {
            term: "progressbar",
            tip: "A widget illustrating the progress of some task"
        },
        {
            term: "element",
            tip: "An HTML element on the page"
        },
        {
            term: "user interface",
            tip: "Components on the screen the user interacts with"
        }
    ];

    $( "p" ).dictionary({
        terms: dict
    });

});

如果您在浏览器中打开此页面并使用鼠标指针选择“tooltips”,您应该会得到如下屏幕截图所示的提示。

如何做...

工作原理...

我们的新词典小部件增加了用户选择段落文本并获取其上下文定义的功能(如果存在)。该小部件接受一个terms选项,这只是一个术语和提示的数组。这是用于选择文本时执行查找的词典数据。ttPos属性是对默认提示position设置对象的引用。我们保持这个便于使用,因为每次用户选择文本并显示提示小部件时都需要使用它。在实例化小部件时调用的_create()方法设置了事件处理。特别是,我们对mouseupmouseenter事件感兴趣,这两个事件根据许多因素显示提示小部件。_destroy()方法确保我们使用的提示小部件也通过调用_destroyTooltip()销毁。

_tip()方法可谓是此小部件的主程序,因为它将具有特定责任的所有方法联系在一起。我们使用_selectedText()方法获取所选文本。我们使用字典中的选定文本获取所选术语。现在,这些值中的任何一个可能是未定义的—当调用_tip()时,用户可能未选择任何内容,或者用户选择的文本在字典中不存在。如果任何一种情况为真,我们必须确保销毁提示。另一方面,如果找到了术语,我们使用_createTooltip()方法创建和显示提示。

_createTooltip()方法接受一个事件对象以及一个术语对象。事件对象用于在打开提示时定位提示。回想一下,我们将提示的默认位置选项存储在ttPos属性中。我们通过扩展该属性与事件创建一个新的position对象。这意味着我们可以将提示相对于用户选择文本的位置进行定位。现在我们已经设置了提示的位置,我们只需将段落的title属性设置为我们希望在提示内显示的文本。这是传递给方法的所选术语的tip属性。_destroyTooltip()负责销毁提示小部件,但仅在该元素实际上是提示时,并还原title属性。

最后需要注意的是,您会注意到我们将简单的字符串传递给字典实例。但我们能够在给定的用户选择中找到几个变体的术语。例如,“tooltips”会找到术语“tooltip”,因为我们在原始字符串之外添加了“s”。我们还会对选择两侧的空白进行标准化,以及忽略大小写。

还有更多...

我们使用字典小部件的方法的缺点是,用户必须选择文本才能获得单词的上下文定义。例如,示例中的两个段落共定义了四个术语。要使此工作,用户必须猜测哪些文本实际上被定义。此外,选择段落文本是直观的,但仅当您经常在使用的应用程序中执行此操作时——大多数用户并不这样做。

让我们通过引入一个新的模式——hover来增强我们的字典小部件。当此模式为true时,我们将实际操作段落文本,以使字典中定义的术语突出显示。这些术语看起来像链接,包含定义的工具提示会像您典型的工具提示一样工作。首先,让我们添加这个简单的 CSS 规则,我们将应用于段落中的每个术语。

.ui-dictionary-term {
    text-decoration: underline;
    cursor: help;
}

我们将保留先前使用的相同两个段落,并使用新的mode选项实例化字典,我们还将修改小部件定义以使用此新选项。以下是新的 JavaScript 代码:

( function( $, undefined ) {

$.widget( "ab.dictionary", {

    options: {
        terms: [],
        mode: "select"
    },

    ttPos: $.ui.tooltip.prototype.options.position,

    _create: function() {

        this._super();

        if ( this.options.mode === "select" ) {

            this._on({
                mouseup: this._tip,
                mouseenter: this._tip
            });

        }
        else if ( this.options.mode === "hover" ) {

            this._formatTerms();
            this._createTooltip();

        }

    },

    _destroy: function() {

        this._super();
        this._destroyTooltip();

        if ( this.options.mode === "hover" ) {
            this._unformatTerms();
        }

    },

    _tip: function( e ) {

        var text = this._selectedText(),
            term = this._selectedTerm( text );

        if ( text === undefined || term === undefined ) {
            this._destroyTooltip();
            return;
        }

        if ( this.element.attr( "title" ) !== term.tip ) {
            this._destroyTooltip();
        }

        this._createTooltip( e, term );

    },

    _selectedText: function() {

        var selection, range, fragement;

        selection = window.getSelection();

        if ( selection.type !== "Range" ) {
            return;
        }

        range = selection.getRangeAt( 0 ),
        fragment = $( range.cloneContents() );

        return $.trim( fragment.text().toLowerCase() );

    },

    _selectedTerm: function( text ) {

        function isTerm( v ) {
            if ( v.term === text || v.term + "s" === text ) {
                return v;
            }
        }

        return $.map( this.options.terms, isTerm )[ 0 ];

    },

    _createTooltip: function( e, term ) {

        if ( this.options.mode === "hover" ) {
            this.element.find( ".ui-dictionary-term" ).tooltip();
            return;
        }

        if ( this.element.is( ":ui-tooltip" ) ) {
            return;
        }

        var pos = $.extend( this.ttPos, { of: e } );

        this.element.attr( "title", term.tip )
                    .tooltip( { position: pos } )
                    .tooltip( "open" );

    },

    _destroyTooltip: function() {

        if( this.options.mode === "hover" ) {
            this.element.find( ".ui-dictionary-term" )
                        .tooltip( "destroy" );
            return;
        }

        if ( !this.element.is( ":ui-tooltip" ) ) {
            return;
        }

        this.element.tooltip( "destroy" )
                    .attr( "title", "");

    },

    _formatTerms: function() {

        function getTerm( v ) {
            return v.term;
        }

        var text = this.element.html(),
            terms = $.map( this.options.terms, getTerm );

        $.each( this.options.terms, function( i, v ) {

            var t = v.term,
                ex = new RegExp( "(" + t + "s|" + t + ")", "gi" ),
                termClass = "ui-dictionary-term",
                formatted = "<span " +
                            "class='" + termClass + "'" +
                            "title='" + v.tip + "'" +
                            ">$1</span>";

            text = text.replace( ex, formatted );

        });

        this.element.html( text );

    },

    _unformatTerms: function() {

        var $terms = this.element.find( ".ui-dictionary-term" );

        $terms.each( function( i, v ) {
            $( v ).replaceWith( $( v ).text() );
        });

    }

});

})( jQuery );

$(function() {

    var dict = [
        {
            term: "tooltip",
            tip: "A contextual widget providing information to the user"
        },
        {
            term: "progressbar",
            tip: "A widget illustrating the progress of some task"
        },
        {
            term: "element",
            tip: "An HTML element on the page"
        },
        {
            term: "user interface",
            tip: "Components on the screen the user interacts with"
        }
    ]

    $( "p" ).dictionary({
        terms: dict,
        mode: "hover"
    });

});

现在,当您在浏览器中查看两个段落时,您会注意到我们在字典数据中定义的术语已被下划线标记。因此,当用户将鼠标指针悬停在术语上时,他们将获得带有工具提示的帮助光标图标。

还有更多...

我们向字典小部件引入的新mode选项接受字符串值,可以是selecthover,默认为select,这是我们在此示例中最初实现的行为。在小部件构造函数_create()方法中,我们检查mode值。如果我们处于hover模式,则调用_formatTerms()方法,该方法会更改段落内术语的视觉外观。接下来,我们调用_createTooltip(),与原始实现中使用的相同方法,只是现在也具有模式感知性。_formatTerms()存储给定元素的文本,然后遍历字典术语。对于每个术语,它构建一个正则表达式,并用用于创建工具提示的span元素替换找到的任何术语。

第十二章:小部件和更多!

在本章中,我们将介绍以下配方:

  • 从折叠到标签,再返回

  • 从头开始构建自定义小部件

  • 构建一个观察者小部件

  • 使用 Backbone 应用程序的小部件

介绍

到目前为止,本书中的每一章都专注于使用 jQuery UI 附带的特定小部件进行工作。在本章中,我们更感兴趣的是大局观。毕竟,您正在构建一个应用程序,而不是一个演示。因此,对于使用 jQuery UI 的开发人员来说,重要的是不仅要意识到每个单独小部件在其自身上的工作方式,还要意识到它们在其环境中的行为方式,以及它们如何与其他小部件和框架交互。

我们还将通过使用小部件工厂从头开始构建一个小部件来解决框架的基本知识。通过通用小部件机制,您可以编写一些与默认小部件无关的小部件。尽管这些自定义小部件没有继承太多功能,但它们的行为类似于 jQuery UI 小部件,仅这一点就值得付出努力——将一层一致性固化到您的应用程序中。

从折叠到标签,再返回

折叠和标签小部件都是容器。也就是说,它们在应用程序的上下文中的典型用途是组织子组件。这些子组件可能是其他小部件,或者任何其他 HTML 元素。因此,这两个小部件符合容器的通用描述,即具有不同部分的小部件。显然,这个描述有一些微妙之处;例如,折叠不支持远程 Ajax 内容。此外,用户遍历部分的方式也大不相同。但它们本质上是可以互换的。为什么不在两个小部件之间引入切换的能力,特别是在运行时,用户可以设置自己的偏好并在两个容器之间切换的情况下?事实证明,我们可以实现这样的东西。让我们看看我们将如何做到这一点。我们需要两个小部件之间的双向转换。这样,标签小部件可以转换为折叠小部件,反之亦然。

如何做...

要实现我们在这里讨论的两种不同小部件之间的转换,我们将不得不扩展折叠和标签小部件。我们将为每个小部件添加一个新方法,将小部件转换为其对应的小部件。这是我们需要使此示例发生的 HTML 结构:

<button class="toggle">Toggle</button>

<div id="accordion">
    <h3>Section 1</h3>
    <div>
        <p>Section 1 content...</p>
    </div>
    <h3>Section 2</h3>
    <div>
        <p>Section 2 content...</p>
    </div>
    <h3>Section 3</h3>
    <div>
        <p>Section 3 content...</p>
    </div>
</div>

<button class="toggle">Toggle</button>

<div id="tabs">
    <ul>
        <li><a href="#section1">Section 1</a></li>
        <li><a href="#section2">Section 2</a></li>
        <li><a href="#section3">Section 3</a></li>
    </ul>
    <div id="section1">
        <p>Section 1 content...</p>
    </div>
    <div id="section2">
        <p>Section 2 content...</p>
    </div>
    <div id="section3">
        <p>Section 3 content...</p>
    </div>
</div>

在这里,我们有两个切换按钮,一个折叠 div 和一个标签 div。切换按钮将使其对应的容器小部件变形为另一种小部件类型。以下是 JavaScript 代码:

( function( $, undefined ) {

$.widget( "ab.accordion", $.ui.accordion, {

    tabs: function() {

        this.destroy();

        var self = this,
            oldHeaders = this.headers,
            newHeaders = $( "<ul/>" );

        oldHeaders.each( function( i, v ) {

            var id = self.namespace + "-tabs-" + self.uuid + "-" + i,
                header = $( "<li/>" ).appendTo( newHeaders );

            $( "<a/>" ).text( $( v ).text() )
                       .attr( "href", "#" + id )
                       .appendTo( header );

            oldHeaders.next().eq( i ).attr( "id", id );

        });

        newHeaders.prependTo(this.element);

        this.headers.remove();
        return this.element.tabs();

    }

});

$.widget( "ab.tabs", $.ui.tabs, {

    accordion: function() {

        this.destroy();

        var self = this;

        this.tabs.each( function( i, v ) {

            var $link = $( v ).find( "a" ),
                id = $link.attr( "href" ),
                text = $link.text();

            $( "<h3/>" ).text( text )
                        .insertBefore( id );

        });

        this.tablist.remove();
        return this.element.accordion();

    },

});

})( jQuery );

$(function() {

    $( "button.toggle" ).button().on( "click", function( e ) {

        var $widget = $( this ).next();

        if ( $widget.is( ":ab-accordion" ) ) {
            $widget.accordion( "tabs" );
        }
        else if ( $widget.is( ":ab-tabs" ) ) {
            $widget.tabs( "accordion" );
        }

    });

    $( "#accordion" ).accordion();
    $( "#tabs" ).tabs();

});

它是如何工作的...

当页面首次加载并且所有 DOM 元素都准备就绪时,我们创建切换按钮小部件、折叠小部件和标签小部件。如下截图所示:

它的工作原理...

现在,点击顶部的切换按钮将把手风琴部件转换为标签部件。另外,第二个切换按钮将标签部件转换为手风琴。点击每个切换按钮一次的结果如下:

它的运行原理...

切换按钮的工作原理是使用 jQuery 的next()函数来获取下一个部件,无论是#accordion还是#tabs,具体取决于所点击的按钮。然后将其存储在$widget变量中,因为我们会多次访问它。首先,我们检查部件是否是手风琴,如果是,我们在手风琴上调用tabs()方法。同样地,如果$widget是标签,我们调用accordion()方法来转换它。请注意,我们正在使用内置的部件选择器,部件工厂为每个部件创建,以确定元素是什么类型的部件。另外,请注意,命名空间是ab,而不是ui,这是编写自己的部件或自定义现有部件时的推荐做法,就像这里一样。在这里,我选择了我的缩写作为命名空间。在实践中,这将是一个与应用程序相关的标准约定。

现在让我们把注意力转向我们已经添加到手风琴部件的tabs()方法。这个新方法的基本工作是销毁手风琴部件,操作 DOM 元素,使其呈现出标签部件将识别的形式,然后实例化标签部件。所以,我们首先调用destroy()方法。然而,请注意,我们仍然可以访问手风琴部件的一些属性,比如headers。销毁部件主要涉及删除任何由于首次创建部件而引入到 DOM 中的装饰,以及删除事件处理程序。在 JavaScript 级别上,销毁我们在这里使用的部件对象并不太关心。

此时,我们有一个oldHeaders变量,它指向原始手风琴的h3元素。接下来,我们有newHeaders,它是一个空的ul元素。newHeaders元素是标签部件期望找到的新元素的起点。接下来,我们必须构建指向标签的内容面板的li元素。对于每个标题,我们向newHeaders ul添加一个链接。但是,我们还必须使用将标题链接到的id更新面板 ID。我们首先使用选项卡的位置以及部件本身的uuid构建一个 ID 字符串。虽然 uuid 并不是必需的;然而,确保唯一的选项卡 ID 仍然是一个好主意。

最后,我们将新的标题添加到元素中,并删除旧的标题。此时,我们有足够的内容来实例化标签部件。而且我们确实这样做了。请注意,我们返回了新创建的对象,以便如果在代码的其他地方引用它,可以用此方法替换它,例如,myTabs = myAccordion.accordion( "tabs" )

我们添加到标签小部件的 accordion() 方法遵循了上述 tabs() 方法中应用的相同原则——我们想要销毁小部件,操作 DOM,并创建折叠小部件。为了实现这一点,我们需要在相应的内容面板之前插入 h3 标题元素。然后,我们删除 tablist 元素和标签 ul,然后调用实例化并返回折叠小部件。

从头开始构建自定义小部件

jQuery UI 最强大的部分并不是随附的预构建小部件,而是用于构建这些小部件的机制。每个小部件都共享一个称为小部件工厂的公共基础设施,并且该基础设施对开发人员使用该框架是可见的。小部件工厂提供了一种让开发人员定义自己的小部件的方式。我们在本书中已经多次看到小部件工厂的实际应用。我们一直在使用它来扩展任何给定小部件的功能。本节的重点是以不同的角度来看待小部件工厂。也就是说,我们如何利用它从零开始构建自己的小部件?

嗯,我们不想从零开始,因为那样会违背小部件工厂的整个目的。相反,构建任何小部件的目标是利用基础小部件类提供的通用功能。此外,开发人员在创建小部件时应该尽量遵循一些基本的设计原则。例如,您的小部件在销毁时应该进行清理,删除属性、事件处理程序,并基本上将元素恢复到原始状态。小部件还应该提供简单的 API,并且对于使用您的小部件的开发人员来说,它应该清楚该小部件做什么,更重要的是,它不做什么。在开始之前和设计小部件时,请记住一些原则:

  • 保持简单:随着 jQuery UI 的最新版本,一些标准小部件经历了重大的重构工作,以简化其界面。在设计您的小部件时,借鉴这个教训,并将其责任最小化。在实现小部件的过程中,可能会有添加另一个 API 方法的冲动,甚至可能有几个。在这样做之前,请认真考虑,因为扩展 API 通常会导致难以维护和保持稳定的小部件。而这正是小部件背后的整个理念,一个小而可靠的模块化组件,可以在各种上下文中使用而不会出现问题。话虽如此,一个不满足应用程序需求的小部件也毫无价值。

  • 可扩展性设计:在简洁保持原则的基础上构建的是可扩展性。同样,正如我们在本书中所见,可扩展性通常是赋予小部件额外功能以执行其工作所需的关键。这些可以是简单的自定义,也可以是方法的完全重写。无论如何,假设您的小部件将被修改,并且它将有观察者监听事件。换句话说,一个好的小部件将以合理的粒度提供功能在实现它的方法之间的分布。每个方法都是专门化的入口点,因此潜在的入口点应该是一个有意识的关注点。小部件触发的事件将小部件的状态传达给外界。因此,当您的小部件的状态发生变化时,请务必让其他人知道。

如何做...

足够的说了,现在,让我们来构建一个检查表小部件。它真的就像听起来的那么简单。我们将基于一个ul元素构建小部件,该元素将每个li元素转换为检查表项。但是,检查表不会孤立存在;我们将添加一些外部组件来与我们的小部件进行交互。我们将需要一个按钮来添加新的检查表项,一个按钮来删除一个项目,以及一个用于跟踪我们列表进度的进度条。用户与小部件本身的主要交互集中在检查和取消检查项目上。

这是我们在本示例中将使用的 HTML:

<div class="container">
    <button id="add">Add</button>
    <button id="remove">Remove</button>
</div>
<div class="container">
    <ul id="checklist">
        <li><a href="#">Write some code</a></li>
        <li><a href="#">Deploy some code</a></li>
        <li><a href="#">Fix some code</a></li>
        <li><a href="#">Write some new code</a></li>
    </ul>
</div>
<div class="container">
    <div id="progressbar"></div>
</div>

接下来,我们将添加我们的检查表小部件所需的 CSS。

.ui-checklist {
    list-style-type: none;
    padding: 0.2em;
}

.ui-checklist li {
    padding: 0.4em;
    border: 1px solid transparent;
    cursor: pointer;    
}

.ui-checklist li a {
    text-decoration: none;
    outline: none;
}

.ui-checklist-checked {
    text-decoration: line-through;
}

最后,我们将使用以下 JavaScript 代码添加我们的小部件定义。此代码还创建了本示例中使用的两个按钮小部件和进度条小部件。

( function( $, undefined ) {

$.widget( "ab.checklist", {

    options: {
        items: "> li",
        widgetClasses: [
            "ui-checklist",
            "ui-widget",
            "ui-widget-content",
            "ui-corner-all"
        ],
        itemClasses: [
            "ui-checklist-item",
            "ui-corner-all"
        ],
        checkedClass: "ui-checklist-checked"
    },

    _getCreateEventData: function() {

        var items = this.items,
            checkedClass = this.options.checkedClass;

        return {
            items: items.length,
            checked: items.filter( "." + checkedClass ).length
        }

    },

    _create: function() {

        this._super();

        var classes = this.options.widgetClasses.join( " " );

        this.element.addClass( classes );

        this._on({
            "click .ui-checklist-item": this._click,
        });

        this.refresh();

    },

    _destroy: function() {

        this._super();

        var widgetClasses = this.options.widgetClasses.join( " " ),
            itemClasses = this.options.itemClasses.join( " " ),
            checkedClass = this.options.checkedClass;

        this.element.removeClass( widgetClasses );

        this.items.removeClass( itemClasses )
                  .removeClass( checkedClass )
                  .removeAttr( "aria-checked" );

    },

    _click: function( e ) {

        e.preventDefault();
        this.check( this.items.index( $( e.currentTarget ) ) );

    },

    refresh: function() {

        var trigger = true,
            items,
            newItems;

        if ( this.items === undefined ) {
            trigger = false;
            this.items = $();
        }

        items = this.element.find( this.options.items )
        newItems = items.not( this.items );

        items.addClass( this.options.itemClasses.join( " " ) );

        this._hoverable( newItems );
        this._focusable( newItems );

        this.items = items;

        if ( trigger ) {
            this._trigger( "refreshed",
                           null,
                           this._getCreateEventData() );
        }

    },

    check: function( index ) {

        var $item = this.items.eq( index ),
            checked;

        if ( !$item.length ) {
            return;
        }

        checked = $item.attr( "aria-checked" ) === "true" ?
                  "false" : "true";

        $item.toggleClass( this.options.checkedClass )
             .attr( "aria-checked", checked );

        this._trigger( "checked", null, this._getCreateEventData());

    }

});

})( jQuery );

$(function() {

    $( "#add" ).button({
        icons: {
            primary: "ui-icon-plus"
        },
        text: false
    });

    $( "#add" ).on( "click", function( e ) {

        var $checklist = $( "#checklist" ),
            $item = $( "<li/>" ).appendTo( checklist );

        $( "<a/>" ).attr( "href", "#" )
                   .text( "Write some documentation" )
                   .appendTo( $item );

        $checklist.checklist( "refresh" );

    });

    $( "#remove" ).button({
        icons: {
            primary: "ui-icon-minus"
        },
        text: false
    });

    $( "#remove" ).on( "click", function( e ) {

        var $checklist = $( "#checklist" ),
            $item = $checklist.find( ".ui-checklist-item:last" );

        $item.remove();
        $checklist.checklist( "refresh" );

    });

    $( "#progressbar" ).progressbar();

    $( "#checklist" ).checklist({
        create: function( e, ui ) {
            $( "#progressbar" ).progressbar( "option", {
                max: ui.items,
                value: ui.checked
            });
        },
        refreshed: function( e, ui ) {
            $( "#progressbar" ).progressbar( "option", {
                max: ui.items,
                value: ui.checked
            });
        },
        checked: function( e, ui ) {
            $( "#progressbar" ).progressbar( "value", ui.checked );
        }
    });

});

当您首次加载页面时,检查表组件以及页面上的其他组件应该看起来像这样:

如何做...

您可以看到,这些是 HTML 结构中指定的默认检查表项。悬停状态按预期工作,但进度条为 0。这是因为检查表没有任何选定的项目。让我们勾选一些项目,并添加一些项目。

如何做...

您可以看到,每次添加或删除检查表项以及单独检查或取消检查一个项目时,进度条都会更新。

工作原理...

让我们首先讨论检查表小部件的 HTML 结构以及显示它所需的新 CSS。然后,我们将将小部件的定义和实例化分成几个部分,并解决这些部分。此示例中使用的 HTML 分为三个主要容器div元素。第一个元素保存我们的添加和删除项目按钮。第二个是检查表小部件,最后一个是进度条。这是一般布局。

HTML 结构的最重要方面是#container元素,它是我们清单小部件的基础。每个项目都存储在一个li元素内。请注意,项目的文本也包装在一个a元素中。这使得在用户通过页面元素时处理单个项目的焦点变得更加简单。清单的主要样式由ui-checklist类控制。这个类在小部件第一次创建时被应用于元素,并对列表执行一些标准样式操作,比如移除项目符号图片。我们需要处理的另一件事是边框间距,当用户悬停在项目上时,ui-state-hover被添加和移除。包装项目文本的a元素不需要任何文本装饰,因为我们不将它们用作标准链接。最后,ui-checklist-checked类与单个清单项目的状态相关,并在视觉上标记项目为已选中。它还在我们需要收集所有已选中项目时作为查询辅助工具。

现在让我们把注意力转向小部件的定义,以及我们是如何实例化和使用它的。

  • 选项: 我们的小部件首先定义的是它的选项,每个选项都有一个默认值。始终确保您向小部件添加的任何选项都有一个默认值,因为我们永远不能指望在创建时提供一个选项。我们在这里为我们的清单小部件定义的选项非常简单,很少会被开发人员更改。例如,我们查找的项目通常总是li元素。而且,我们在这里定义的类,应用于小部件本身,可能永远不会更改。然而,它们需要在某个地方声明,所以我们可以硬编码它,或者将它们放在开发人员可以访问的地方。把选项想象成小部件对象的属性或属性。

  • 私有方法: 按照惯例,私有方法或不构成对用户可见的 API 的方法以下划线作为前缀。我们的第一个私有方法是_getCreateEventData()方法。当小部件的创建事件被触发时,此方法会被基础小部件类在内部调用。这个方法是一个钩子,允许我们向创建事件处理程序提供自定义数据。我们在这里做的一切就是传递一个对象,该对象具有存储在项目属性中的项目数,以及存储在已检查属性中的已检查项目数。

  • create 方法_create()方法可能是任何小部件的最常见方法,因为它是由小部件工厂作为小部件构造函数调用的。我们使用_super()实用方法为我们调用基础小部件构造函数,它为我们执行一些样板初始化工作。接下来,我们使用widgetClasses选项将相关的小部件 CSS 类应用于元素。然后,我们使用_on()方法为点击事件设置事件处理程序。请注意,在事件名后面我们传递了一个委托选择器.ui-checklist-item。我们这样做的原因是因为可以向清单中添加项目,也可以从清单中删除项目,因此使用这种方法比手动管理每个项目的点击事件更合理。

  • destroy 方法_destroy()方法是必不可少的,如前所述,用于执行清理任务。我们在这里使用_super()调用基础小部件_destroy()方法,该方法将清理我们使用_on()创建的任何事件处理程序。然后,我们只需要删除我们在小部件的生命周期中添加的任何类和属性。最后一个私有方法是_click()方法,这是当小部件首次创建时绑定到点击事件的事件处理程序。此方法的工作是更改所点击项目的状态,我们通过调用check()方法来实现这一点,该方法是向开发人员公开的 API 的一部分。我们还希望在这里阻止链接点击的默认操作,因为它们有可能重新加载页面。

  • API:秉承保持小部件简单的精神,暴露的 API 仅包括两种方法。第一个是refresh()方法,它负责定位构成我们清单的项目。这些项目存储在小部件对象的items属性中,这是一个不通过 API 公开的示例。items属性仅在内部使用;然而,如果开发人员要扩展我们的小部件,他们的自定义方法将是可访问的,甚至可能很有用。refresh()方法在发现新项目时更改小部件的状态,这就是为什么它会触发刷新事件的原因。但是,在某些情况下,我们不希望触发此事件,即当第一次实例化小部件时。这在trigger变量中进行跟踪(如果我们尚未存储任何项目,则可以安全地假定我们正在创建而不是刷新)。我们不希望与创建事件冲突的原因是,这对使用小部件的开发人员非常具有误导性。我们还在每个新发现的项目上使用了_hoverable()_focusable()方法。这是小部件内用户与之交互的项目的标准模式。

  • check 方法check()方法是检查清单 API 的另一半,它也会更改小部件的状态。它触发一个 changed 事件,其中包含有关项目计数和已检查计数的数据,与创建事件数据相同。您会注意到,此方法确保处理适当的aria属性,就像标准的 jQuery UI 小部件一样。aria标准促进了可访问性,这就是为什么 jQuery UI 框架使用它的原因,而我们的小部件也不应该有所不同。最后,该方法的工作是使用存储在checkedClass选项中的值切换此项目的类。

  • 主要应用程序:页面加载时,我们首先做的是创建两个按钮小部件:#add#remove。点击#add按钮时,会将新项目的 DOM 元素添加到检查清单中。然后,它使用refresh()方法更新小部件的状态,并触发任何事件。同样,#remove按钮会移除一个 DOM 元素,并调用refresh()方法,触发任何状态更改行为。进度条小部件在不包含任何选项的情况下实例化,因为它对我们的检查清单小部件一无所知。

最后,我们的检查清单小部件是用三个选项创建的。这些都是事件处理程序,它们都承担着相同的责任——更新#progressbar小部件。例如,小部件首先被创建,然后进度条根据在 DOM 中找到的项目进行更新(尚未检查任何项目)。当从列表中添加或删除新项目时,将触发refreshed事件;我们也希望在这里更新进度条。每当用户选中或取消选中项目时,都会触发checked事件处理程序,在这里,我们只关心更新进度条的值,因为项目的总数是相同的。

构建观察者小部件

处理由 jQuery UI 小部件触发的事件的典型方法是将事件处理程序绑定到该事件名称,直接传递到构造函数中。这是典型的方法,因为它易于做到,并且通常解决了我们遇到的特定问题。例如,假设当我们的手风琴小部件的某个部分展开时,我们希望更新另一个 DOM 元素。为此,在构造手风琴时将事件处理程序函数分配给激活事件。

这种方法非常适用于小型、单一用途的作业,适用于给定小部件的单个实例。然而,大多数有意义的应用程序有许多小部件,都触发着自己的事件。小部件工厂用小部件的名称前缀每个事件,这通常意味着即使在小部件上下文之外,我们也知道我们在处理什么。当我们想要将事件处理程序绑定到小部件事件时,长时间之后,小部件已经被创建了,这一点尤其有帮助。

让我们构建一个观察者小部件,帮助我们可视化应用程序中发生的所有潜在小部件事件。观察者小部件能够绑定到单个小部件、一组小部件或整个文档。我们将看看后一种情况,在那里观察者甚至会捕获未来创建的小部件的事件。

如何做...

让我们首先看一下观察者小部件使用的 CSS 样式:

.ui-observer-event {
    padding: 1px;
}

.ui-observer-event-border {
    border-bottom: 1px solid;
}

.ui-observer-event-timestamp {
    float: right;
}

现在,让我们看一下用于创建一个基本页面和几个示例小部件的 HTML。这些小部件将触发我们试图用观察者捕获的事件。

<div class="container">
    <h1 class="ui-widget">Accordion</h1>
    <div id="accordion">
        <h3>Section 1</h3>
        <div>
            <p>Section 1 content</p>
        </div>
        <h3>Section 2</h3>
        <div>
            <p>Section 2 content</p>
        </div>
    </div>
</div>
<div class="container">
    <h1 class="ui-widget">Menu</h1>
    <ul id="menu">
        <li><a href="#">Item 1</a></li>
        <li><a href="#">Item 2</a></li>
        <li><a href="#">Item 3</a></li>
    </ul>
</div>
<div class="container">
    <h1 class="ui-widget">Tabs</h1>
    <div id="tabs">
        <ul>
            <li><a href="#tab1">Tab 1</a></li>
            <li><a href="#tab2">Tab 2</a></li>
            <li><a href="#tab3">Tab 3</a></li>
        </ul>
        <div id="tab1">
            <p>Tab 1 content</p>
        </div>
        <div id="tab2">
            <p>Tab 2 content</p>
        </div>
        <div id="tab3">
            <p>Tab 3 content</p>
        </div>
    </div>
</div>

最后,这是小部件的实现方式,以及在此页面上使用的四个小部件实例:

( function( $, undefined ) {

$.widget( "ab.observer", {

    options: {

        observables: [
            {
                widget: $.ui.accordion,
                events: [
                    "activate",
                    "beforeActivate",
                    "create"
                ]
            },
            {
                widget: $.ui.menu,
                events: [
                    "blur",
                    "create",
                    "focus",
                    "select"
                ]
            },
            {
                widget: $.ui.tabs,
                events: [
                    "activate",
                    "beforeActivate",
                    "create"
                ]
            }
        ]

    },

    _getEvents: function() {

        var events = {};

        $.each( this.options.observables, function ( i, v ) {

            var prefix = v.widget.prototype.widgetEventPrefix;

            $.each( v.events, function( i, v ) {
                events[ prefix + v.toLowerCase() ] = "_event";
            });

        });

        return events;

    },

    _create: function() {

        this._super();

        var dialogId = "ui-observer-dialog-" + this.uuid,
            dialogSettings = {
                minHeight: 300,
                maxHeight: 300,
                position: {
                    my: "right top",
                    at: "right top"
                },
                title: this.element.selector
            };

        this.dialog = $( "<div/>" ).attr( "id", dialogId )
                                   .attr( "title", "Observer" )
                                   .addClass( "ui-observer" )
                                   .appendTo( "body" )
                                   .dialog( dialogSettings );

        this._on( this.element, this._getEvents() );

    },

    _event: function( e, ui ) {

        var eventClasses = "ui-observer-event " +
                           "ui-observer-event-border",
            $event = $( "<div/>" ).prependTo( this.dialog )
                                  .addClass( eventClasses ),
            time = new Date( e.timeStamp ).toLocaleTimeString();

        $( "<span/>" ).html( e.type )
                      .appendTo( $event );

        $( "<span/>" ).html( time )
                      .addClass( "ui-observer-event-timestamp" )
                      .appendTo( $event );

        this.dialog.find( ".ui-observer-event:last" )
                   .removeClass( "ui-observer-event-border" );

    },

    _destroy: function() {

        this._super();
        this.dialog.dialog( "destroy" )
                   .remove();

    }

});

})( jQuery );

$(function() {

    $( document ).observer();

    $( "#accordion" ).accordion();
    $( "#menu" ).menu();
    $( "#tabs" ).tabs();

});

在浏览器中查看此页面时,基本小部件布局如下截图所示:

如何做...

甚至只是创建这些小部件也会触发事件。例如,当页面首次加载时,您会看到观察者小部件创建的对话框已经填充了事件。

如何做...

工作原理...

在这个例子中,可观察小部件应用于document元素。这意味着它将捕获冒泡到该级别的任何小部件事件。可观察小部件定义了一个observables选项,一个我们想要监听其事件的小部件数组。在这种情况下,为了简洁起见,我们只包括了三个小部件。这可以根据应用程序的需要随时扩展,因为它是一个选项。

_getEvents() 方法的目的是读取observables选项并构建一个我们可以使用它来将这些事件绑定到_event()方法的对象。请注意,我们在这里自动将小部件前缀值添加到事件名称——这在小部件原型的widgetEventPrefix属性中是可用的。_create()方法的工作是将div元素插入到body元素中,然后它成为一个对话框小部件。我们将其定位在页面的右上角,以便不妨碍用户。最后,我们使用由_getEvents()返回的对象使用_on()方法开始监听事件。

_event() 方法是我们监听的任何小部件事件触发时使用的单个回调函数。它简单地将事件记录到观察者对话框中。它还记录事件的时间;因此,这个工具对于尝试任何 jQuery UI 应用程序都是有用的,无论是大还是小,因为它可以突出显示实际发生的事件以及它们的顺序。该小部件还负责销毁它之前创建的对话框小部件。

在 Backbone 应用程序中使用小部件

由于 JavaScript 环境的变化多端,您可能会发现自己在不同的环境中工作,最好接受这一事实,不是所有事情都是按照 jQuery UI 的方式完成的。如果您发现自己在一个项目中渴望使用 jQuery UI 小部件,因为使用案例很多,那么您将不得不花费必要的时间来理解 jQuery UI 与另一个框架混合的后果。

对于任何开发人员来说,将完全不同的小部件框架混合在一起通常是不明智的,因此希望这是可以轻松避免的事情。当然,您必须处理其他自制的 HTML 和 CSS 组合,但这很正常。这并不是太糟糕,因为您可以控制它(其他开源框架很难做到)。那么,如果不是其他小部件框架,我们可能要考虑使用哪些其他框架?

Backbone 是一个通用框架,它基于较低级别的 underscore.js 实用库,用于为 Web 应用程序客户端添加结构。在 Backbone 应用程序中,您会找到模型、集合和视图等概念。对 Backbone 库的全面介绍远远超出了本书的范围。但是,将 Backbone 视为应用程序的脚手架很有帮助,这部分不会改变。无论是否使用 jQuery UI 小部件,它都会以相同的方式运行。但是,由于我们感兴趣的是使用 jQuery UI,让我们构建一个使用 jQuery UI 小部件的小型 Backbone 应用程序。

如何操作...

应用程序的目标是显示一个自动完成小部件,用户可以过滤编程语言名称。当进行选择时,会显示有关该语言的一些详细信息,包括一个删除按钮,该按钮从集合中删除语言。简单吧?让我们开始吧。

在页面页眉中,我们将做一些不同的事情——包括一个模板。模板只是一串文本,由 Backbone 视图渲染。我们将其类型设为 text/template,这样浏览器就不会将其解释为模板之外的东西(比如 JavaScript 代码)。它有一个 id,这样在渲染模板时我们可以稍后引用模板文本。

<script type="text/template" id="template-detail">
    <div>
        <strong>Title: </strong>
        <span><%= title %></span>
    </div>
    <div>
        <strong>Authors: </strong>
        <span><%= authors %></span>
    </div>
    <div>
        <strong>Year: </strong>
        <span><%= year %></span>
    </div>
    <div>
        <button class="delete">Delete</button>
    </div>
</script>

接下来,是此 UI 使用的最小 CSS——简单的字体和布局调整。

.search, .detail {
    margin: 20px;
}

.detail {
    font-size: 1.4em;
}

.detail button {
    font-size: 0.8em;
    margin-top: 5px;
}

接下来,我们有用户界面使用的实际标记。请注意 detaildiv 是多么简洁。这是因为它只是一个模板的容器,由视图渲染,我们马上就会看到。

<div class="search">
    <label for="search">Search:</label>
    <input id="search"/>
</div>
<div class="detail"></div>

最后,我们有实际使用自动完成和按钮 jQuery UI 小部件的 Backbone 应用程序。

注意

为了简洁起见,在此处我们将削减代码清单的大部分内容,试图只显示必需的内容。完全运作的 Backbone 代码可供下载,以及本书中的所有其他示例。

$(function() {

    // Model and collection classes

    var Language,
        LanguageCollection;

    // View classes

    var AutocompleteView,
        LanguageView;

    // Application router

    var AppRouter;

    // Collection instance

    var languages;

    // Application and view instances

    var app,
        searchView,
        detailView;

    /**
     *
     * Class definitions
     *
     **/

    Language = Backbone.Model.extend({        
       // ...
    });

    LanguageCollection = Backbone.Collection.extend({
       // ...
    });

    AutocompleteView = Backbone.View.extend({        
       // ...
    });

    LanguageView = Backbone.View.extend({        
       // ...
    });

    AppRouter = Backbone.Router.extend({

    });

    /**
     *
     * Collection, view, and application instances
     *
     **/

    languages = new LanguageCollection([        
        // …
    ]);

    searchView = new AutocompleteView({
        // ….
    });

    detailView = new LanguageView({
        // …
    });

    app = new AppRouter();

    Backbone.history.start();

});

运行此示例将向用户显示一个自动完成 input 元素。所选语言的详细信息如下截图所示:

如何操作...

工作原理...

我们整个 Backbone 应用程序都在文档就绪的回调函数中声明。一旦完成,一切都是基于事件的。让我们逐步了解应用程序组件。您将注意到的第一件事是,我们在顶部声明了变量,并为它们提供了简要的分类解释。当我们与超过一小撮的变量共享相同的命名空间时,这通常是有帮助的。类别如下:

  • 模型和集合类:我们应用程序中用于定义数据模型的类。

  • 视图类:我们应用程序中用于为用户提供数据模型不同视图的类。

  • 应用程序路由器:一个类似于控制器的单个类,用于操作浏览器地址,并在路径更改时执行相关功能。

  • 集合实例:集合实例代表应用程序数据 - 一组模型实例。

  • 应用程序和视图实例:单个应用程序以及该应用程序用于呈现数据的各种视图。

鉴于此,请让我们现在深入了解每个 Backbone 类的工作原理。应用程序只有一个模型类,即Language。我们在这里可以看到,Language声明在实例化时为属性定义了一些默认值。接下来,LanguageCollection类是 Backbone Collection 类的扩展。这是所有我们的Language实例的地方。请注意,我们正在指定模型属性指向Language类。由于我们没有 RESTful API,我们必须告诉集合,任何同步操作都应在本地执行。我们必须在 Backbone 中包含本地存储插件,以使此操作生效。这实际上是在真正的后端完全成形之前启动 UI 开发的理想方式。

接下来,我们有我们的第一个视图类,AutocompleteView,它专门针对自动完成 jQuery UI 小部件。我们将其命名为这样是因为我们在这里尽力使其足够通用,以便与另一个自动完成小部件一起使用。我们在视图类中有一些语言特定的硬编码内容,但这些内容如果有需要的话可以轻松改进。在这个类中定义的第一个属性是events对象。这些大多与自动完成小部件事件相关。每个回调事件处理程序在下面被定义为一个视图方法。initialize()方法是视图构造函数,在这里我们调用delegateEvents()来为当前元素以及未来元素激活我们的事件处理程序。然后构造函数创建自动完成小部件,并监听其连接以获取销毁事件。

autocompleteCreate()方法在创建自动完成小部件后触发,并将小部件的source选项分配给小部件。这是对此视图的autocompleteSource方法的代理。autocompleteSelect方法在用户选择项目并导航到适当路由时触发。autocompleteChange()方法在自动完成小部件失去焦点并且项目不同的情况下触发。我们这样做是为了在用户删除其先前选择但尚未模糊自动完成焦点时更新路径。最后,autocompleteSearch()方法是用户开始输入时自动完成小部件填充项目的方法。首先,我们使用集合上的 underscore filter()方法执行过滤,然后我们使用集合上的 underscore map()方法进行映射。映射是必要的以返回自动完成小部件期望的格式。

应用程序的下一个关键部分是LanguageView类,负责渲染编程语言的详细信息。和之前的视图一样,这个视图使用events属性设置事件处理程序。我们还在构造函数中列出了该视图的集合上的一些事件。需要注意的一个事件是change:selected事件。这只有在selected属性更改时才会触发,这很好,因为这是我们感兴趣的。

render()方法负责渲染模板,但仅在实际选择了相应的模型时才执行。一旦渲染完成,我们就可以实例化此视图使用的按钮小部件。但是,请注意,由于在首次创建视图时已经委托了单击事件处理程序,因此不会再次绑定事件处理程序。

AppRouter类是应用程序控制器,因为它负责对 URL 路径的更改做出反应。routeLang()方法响应特定语言并将其标记为选定。routeDefault()方法处理所有其他请求。它的唯一工作是确保没有语言被标记为选定,并且作为副作用,任何先前选定的语言都将从 UI 中移除,因为LanguageView正在监听selected属性的更改。

最后,我们在集合实例中创建我们模型的实例,然后创建我们的视图和应用程序路由器。

posted @ 2024-05-19 20:12  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报