8.5 使用 jQuery UI 部件工厂创建插件

在第7章我们看到过,jQuery UI也提供了一套部件,这些部件本身是插件,只不过用于生成特定的UI元素,例如按钮或滑动条。这些部件对JavaScript开发人员而言,有一组非常统一的API,因而学习起来非常简单。如果我们自己要编写的插件会创建新的用户界面元素,通常最好以扩展 jQuery UI库的方式来实现。

每个部件都会包含一组复杂的功能,但所幸的是,这们不需要自己承担这些复杂性。jQuery UI库的核心包含了一个工厂方法,叫$.widget(),这个方法能帮我们做很多事情。使用这个方法可以确保我们的代码达到所有jQuery UI部件用户认可的API标准。

使用部件工厂创建的插件具有很多不错的特性。只要编写少量代码,就可以额外获得这些功能(甚至更多):

  • 插件具有了“状态”,可以检测、修改甚至在应用之后完全颠覆插件的原始效果;

  • 自动将用户提供的选项与定制的选项合并到一起;

  • 多个插件方法无缝组合为一个jQuery方法,这个方法接受一个表明要调用哪个子方法的字符串;

  • 插件触发的自定义事件处理程序可以访问部件实例的数据。

事实上,鉴于这些功能如此诱人,在构建任何适当的(无论与UI有关还是无关的)复杂插件时,谁都希望使用部件工厂方法。

8.5.1 创建部件

在下面的例子中,我们要编写一个插件为元素添加自定义的提示条。为了创建这个提示条,需要为页面中的每个元素创建一个<div>容器,然后在鼠标悬停在元素上时,把这个容器放在相应元素的旁边。先来看看这个插件的代码(代码清单8-16),然后我们再一点点地分析。

 在最近的版本中,jQuery UI库包含了自己内置的提示条部件,这个部件比我们例子中的要高级。我们这个部件会覆盖内置的.tooltip()方法,这在真实的项目中是应该避免的。但出于学习演示的目的,这样却可以验证一些重要的概念。

每次调用$.widget()都会通过部件工厂创建一个jQuery UI插件。这个函数接受部件的名称和一个包含部件属性的对象作为参数。部件名称必须带命名空间,在这里我们使用ljq作为命名空间,使用tooltip作为插件名称。这样,在jQuery项目中就可以通过.tooltip()调用我们这个插件了。

我们要定义的第一个部件属性是._create()

代码清单8-16

  1. (function($) {
  2. $.widget('ljq.tooltip', {
  3. _create: function() {
  4. this._tooltipDiv = $('<div></div>')
  5. .addClass('ljq-tooltip-text ' +
  6. 'ui-widget ui-state-highlight ui-corner-all')
  7. .hide().appendTo('body');
  8. this.element
  9. .addClass('ljq-tooltip-trigger')
  10. .on('mouseenter.ljq-tooltip',
  11. $.proxy(this._open, this))
  12. .on('mouseleave.ljq-tooltip',
  13. $.proxy(this._close, this));
  14. }
  15. });
  16. })(jQuery);

这个属性是一个函数,每当jQuery对象中每个匹配的元素调用.tooltip()时,部件工厂就会调用它。

 部件属性(如_create())以下划线开头,表示私有。稍后我们会讨论公用函数。

_create函数内部,需要设置将来要显示的提示条。为此,要创建一个<div>元素并将其添加到文档中。同时,将对这个元素的引用保存在this._tooltipDiv中以备将来使用。

在这个函数的上下文中,this引用的是当前部件实例,可以通过它为部件添加任何想要的属性。另外,部件实例本身也有一些预定义的属性可以为我们提供便利;特别地,this.element中保存着一个jQuery对象,这个对象指向最初选择的元素。

在此,我们使用this.element为提示条的触发元素绑定了mouseentermouseleave处理程序。这些处理程序可以在鼠标悬停在相应元素上面时显示提示条,而在鼠标离开时隐藏提示条。需要注意的是,这里的事件名也要加上与插件一样的命名空间前缀。我们在第3章曾经讨论过,使用命名空间就不会干扰其他也要为这些元素绑定处理程序的代码。

这些.on()调用中还涉及了另一个新语法:把处理程序传递给$.proxy()函数。这个函数会修改方法中this的指向,因此才能在._open函数中引用部件的实例。

接下来需要定义绑定到mouseentermouseleave._open()._close()函数:

代码清单8-17

  1. (function($) {
  2. $.widget('ljq.tooltip', {
  3. _create: function() {
  4. // ...
  5. },
  6. _open: function() {
  7. var elementOffset = this.element.offset();
  8. this._tooltipDiv.css({
  9. position: 'absolute',
  10. left: elementOffset.left,
  11. top: elementOffset.top + this.element.height()
  12. }).text(this.element.data('tooltip-text'));
  13. this._tooltipDiv.show();
  14. },
  15. _close: function() {
  16. this._tooltipDiv.hide();
  17. }
  18. });
  19. })(jQuery);

至于._open()._close()函数本身,其实也没有什么好解释的。这两个名字就足以表明它们的作用,不过它们倒是展示了怎么在部件中创建私有函数,那就是在函数名字前加上下划线。在打开(open)提示条时,使用CSS定位将它放到合适的位置然后显示它;而在关闭(close)提示条时,隐藏它即可。

在打开提示的过程中,需要用相关信息来填充提示条。为此,我们用到了.data()方法,这个方法可以用来取得和设置与任何元素相关的数据。不过,我们这里利用了这个方法读取HTML5数据属性的能力,取得了每个元素的data-tooltip-text属性的值。

有了这个插件之后,代码$('a').tooltip()就可以让鼠标悬停时显示提示条,如图8-8所示。

8.5 使用 jQuery UI 部件工厂创建插件 - 图1

图 8-8

这个插件并不算复杂,代码也不长,但它却浓缩了很多高级的概念。为了充分地利用这些高级的概念,首先需要把这个部件变成有状态的部件。部件的状态允许用户根据需要启动和禁用部件,甚至在创建之后完全销毁它。

8.5.2 销毁部件

我们知道,部件工厂可以创建新的jQuery方法。在我们的例子中这个方法就是.tooltip(),不传递任何参数调用它,可以为一组元素应用提示条部件。不过,除了单纯的应用提示条,这个方法还可以做其他很多事情。这时候,需要给这个方法传入一个字符串参数,以便调用适当的子方法

其中一个内置的子方法是destroy。调用.tooltip('destroy')就可以从页面中删除这个提示条部件。然后部件工厂会为我们完成大部分工作,但在通过_create修改了文档(比如这里创建了用于保存提示条文本的<div>)的情况下,还要负责将其清理掉,参见代码清单8-18。

代码清单8-18

  1. (function($) {
  2. $.widget('ljq.tooltip', {
  3. _create: function() {
  4. // ...
  5. },
  6. destroy: function() {
  7. this._tooltipDiv.remove();
  8. this.element
  9. .removeClass('ljq-tooltip-trigger')
  10. .off('.ljq-tooltip');
  11. $.Widget.prototype.destroy.apply(this, arguments);
  12. },
  13. _open: function() {
  14. // ...
  15. },
  16. _close: function() {
  17. // ...
  18. }
  19. });
  20. })(jQuery);

新写的代码为这个部件添加了一个新属性。这个函数撤销之前所做的修改,然后调用保存在原型对象中的destroy自动完成清理工作。

 注意,这一次的destroy前面并没有加下划线,这是因为它是一个可以通过.tooltip('destroy')调用的公有子方法。

8.5.3 启用和禁用部件

除了被完全销毁,也可以临时禁用然后再在将来重新启用部件。内置的enabledisable子方法可以帮我们实现部件的启用和禁用,方法是将this.options.disabled的值设置为truefalse。要支持这两个子方法,我们要做的就是在对部件进行任何操作前先检查这个值,参见代码清单8-19。

代码清单8-19

  1. _open: function() {
  2. if (!this.options.disabled) {
  3. var elementOffset = this.element.offset();
  4. this._tooltipDiv.css({
  5. position: 'absolute',
  6. left: elementOffset.left,
  7. top: elementOffset.top + this.element.height()
  8. }).text(this.element.data('tooltip-text'));
  9. this._tooltipDiv.show();
  10. }
  11. },

有了这个额外的检查之后,提示条就会在调用.tooltip('disable')之后暂停显示,而在调用.tooltip('enable')之后恢复显示。

8.5.4 接受部件选项

现在,我们要考虑让部件可以定制了。在前面编写.shadow()插件时,我们已经体验到为部件提供一组定制的默认设置,然后再让用户指定的选项覆盖默认设置是一种很友好的机制。在这个过程中,几乎所有工作都是由部件工厂执行的,而我们所要做的就是提供一个options属性,如代码清单8-20所示。

代码清单8-20

  1. options: {
  2. offsetX: 10,
  3. offsetY: 10,
  4. content: function() {
  5. return $(this).data('tooltip-text');
  6. }
  7. },

这个options属性就是一个对象,其中应该包含部件所需的所有选项,这样用户就不必非要提供它们了。在此,我们提供了提示条相对于其触发元素的水平和垂直坐标,以及为每个元素生成提示的函数。

在我们的代码中,唯一需要用到options属性的就是._open()方法:

代码清单8-21

  1. _open: function() {
  2. if (!this.options.disabled) {
  3. var elementOffset = this.element.offset();
  4. this._tooltipDiv.css({
  5. position: 'absolute',
  6. left: elementOffset.left + this.options.offsetX,
  7. top: elementOffset.top + this.element.height() + this.options.offsetY
  8. }).text(this.options.content.call(this.element[0]));
  9. this._tooltipDiv.show();
  10. }
  11. },

在包括._open()在内的子方法中,可以通过this.options访问这些选项。通过访问这些选项始终都可以保证取得正确的值,要么是默认值,要么是用户提供的覆盖默认值的值。

现在,不用传递参数也还是可以向页面中添加部件(比如,直接调用.tooltip()),但得到的都是默认行为。不过,提供选项则将覆盖默认行为,例如.tooltip({offsetX: -10, offsetX: 25})。部件工厂甚至可以让我们在部件实例化之后再修改选项,例如:.tooltip ('option', 'offsetX', 20)。下次再访问这些选项时,就会取得新设置的值。

 对选项变化作出响应

如果需要立即对选项变化作出响应,可以在部件中添加一个_setOption函数,这个函数负责处理变化,然后调用_setOption的默认实现。

8.5.5 添加子方法

内置的子方法确实很方便,但有时候我们可能想为自己插件的用户提供更多“挂钩”。前面已经介绍过如何在部件中创建私有函数,实际上创建公有函数(也就是子方法)也一样,唯一的区别在于部件的属性名不以下划线开头。知道了这一点,要创建手工打开和关闭提示条的子方法就非常简单了,参见代码清单8-22。

代码清单8-22

  1. open: function() {
  2. this._open();
  3. },
  4. close: function() {
  5. this._close();
  6. },

就这么简单。通过添加调用私有函数的子方法,现在就可以使用.tooltip('open')来打开提示条,使用.tooltip('close')来关闭提示条了。即使在子方法中什么也不返回,部件工厂也会替我们做很多工作,从而确保连缀语法可以正常工作。

8.5.6 触发部件事件

真正的好插件不仅自己扩展jQuery,而且还能为其他代码提供机制来扩展它。提供这种扩展能力的方法之一就是支持与插件相关的一组自定义事件。部件工厂同样可以让这个过程变得很简单,参见代码清单8-23。

代码清单8-23

  1. _open: function() {
  2. if (!this.options.disabled) {
  3. var elementOffset = this.element.offset();
  4. this._tooltipDiv.css({
  5. left: elementOffset.left + this.options.offsetX,
  6. top: elementOffset.top + this.element.height() + this.options.offsetY
  7. }).text(this.options.content.call(this.element[0]));
  8. this._tooltipDiv.show();
  9. this._trigger('open');
  10. }
  11. },
  12. _close: function() {
  13. this._tooltipDiv.hide();
  14. this._trigger('close');
  15. }

在一个函数中调用this._trigger()可以让代码监听新的自定义事件。事件名字会加上部件名作为前缀,因而不必担心它会与其他事件冲突。因为这里在提示条的_open函数中调用了this._trigger('open'),那么每次打开提示条的时候都会分派tooltipopen事件。而在这个元素上调用.on('tooltipopen')可以监听这个事件。

虽然这些内容其实也只涉及创建成熟完善的插件的皮毛,但已经足够让我们了解如何创建部件,创建部件时可以使用哪些工具,以及如何确保部件符合标准了。这样创建出来的部件,能够让熟悉jQuery UI的用户也感觉到很专业。