6.2.2 Info弹窗扩展

首先,我们先来明确一下UI的设计。Info弹窗和普通弹窗的唯一不同点是:Info弹窗里有“确认”和“取消”按钮,分别对应确认和取消操作。相应的HTML部分如下:

  1. <div class="modal-footer">
  2. <button type="button" class="btn btn-success">确认</button>
  3. <button type="button" class="btn btn-danger">取消</button>
  4. </div>

在普通的Modal弹窗里,如果我们要实现Info弹窗效果,就需要在“确认”和“取消”按钮上分别绑定事件,而且如果Modal内容是从远程(remote)获取的,那只能在shown.bs.modal的回调里绑定上述两个按钮的click事件,然后再处理相应的逻辑。所以,这也就意味着,要为这两个按钮分别添加id属性。

通过对data-dismiss="modal"的了解可以看出,只需要以类似的方式,给按钮声明一个自定义属性,然后在新的Info弹窗里处理上述两个按钮的单击行为就可以了,然后再暴露两个事件回调出来。自定义属性的修改如下:

  1. <div class="modal-footer">
  2. <button type="button" class="btn btn-success" data-info="confirm">确认</button>
  3. <button type="button" class="btn btn-danger" data-info="cancel">取消</button>
  4. </div>

添加一个data-info按钮,在弹窗显示的时候,绑定事件,然后执行操作(处理回调),整个思路就是这样的。也就是说,声明式用法里只多了两个自定义属性,其他的和Modal插件一模一样,我们就一步一步地在Modal插件基础上扩展吧。

首先,我们将自己新扩展的插件取名为InfoModal,所以要先定义整个插件类函数:

  1. // InfoModal类定义
  2. var InfoModal = function (element, options) {
  3. this.$element = $(element);
  4. this.super = this.$element.data('bs.modal'); // 获取自定义属性bs.modal的值
  5. this.options = options;
  6.  
  7. this.$element.on('click.confirm.infomodal', '[data-info="confirm"]',
  8. $.proxy(this.confirm, this));
  9. this.$element.on('click.cancel.infomodal', '[data-info="cancel"]',
  10. $.proxy(this.cancel, this));
  11.  
  12. var that = this; // 防止污染作用域,用临时变量that
  13. }
  14.  
  15. // 默认设置
  16. InfoModal.DEFAULTS = {
  17. closeAfterConfirm: false,
  18. closeAfterCancel: true
  19. }

上述代码和Modal类似,但有以下几个地方不一样:

1)多了一个this.super对象,它的值指向前面所说的Modal弹窗的示例,以便可以使用该实例上的一些原型方法。

2)在modal元素内部的子元素(带有data-info属性)上分别绑定了confirm和cancel方法,以便在单击的时候调用它们。

3)还定义了两个默认值,分别设置在单击完“确认”和“取消”按钮后,使用的过程中可以随意设置要不要自动关闭,也可以不关闭(利用回调函数手动进行关闭)。

基本的类定义完了,开始定义原型方法和自定义事件。要思考到底要定义多少原型方法:首先confirm和cancel肯定得要,其次show、hide和toggle这3个也应该要。而对于自定义事件,首先要有和Modal弹窗一样的show、shown、hide、hidden事件,另外,又添加了confirm.bs.infomodal和cancel.bs.infomodal事件。

先来定义原型方法:

  1. // 反转弹窗状态
  2. InfoModal.prototype.toggle = function (_relatedTarget) {
  3. // 如果是关闭状态,则打开弹窗,否则关闭
  4. return this[!this.super.isShown ? 'show' : 'hide'](_relatedTarget)
  5. }
  6. // 打开弹窗
  7. InfoModal.prototype.show = function (_relatedTarget) {
  8. this.super.show(_relatedTarget);
  9. }
  10. // 关闭弹窗
  11. InfoModal.prototype.hide = function (e) {
  12. if (e) e.preventDefault(); // 先阻止冒泡行为
  13. this.super.hide();
  14. }
  15. // 单击“确认”按钮的行为
  16. InfoModal.prototype.confirm = function (e) {
  17. if (e) e.preventDefault(); // 先阻止冒泡行为
  18.  
  19. var e = $.Event('confirm.bs.infomodal');
  20. this.$element.trigger(e); // 确认前触发事件,主要用于处理相关代码
  21.  
  22. if (e.isDefaultPrevented()) return;
  23. if (this.options.closeAfterConfirm) {
  24. this.hide(); // 如果设置了data-close-after-confirm=true参数,
  25. // 则关闭弹窗
  26. }
  27. }
  28. // 单击“取消”按钮的行为
  29. InfoModal.prototype.cancel = function (e) {
  30. if (e) e.preventDefault(); // 先阻止冒泡行为
  31.  
  32. var e = $.Event('cancel.bs.infomodal');
  33. this.$element.trigger(e); // 取消前先触发事件
  34.  
  35. if (e.isDefaultPrevented()) return;
  36. if (this.options.closeAfterCancel) {
  37. this.hide(e); // 如果设置了data-close-after-cancel=true参数,
  38. // 则关闭弹窗
  39. }
  40. }

上述几个原型方法,首先是利用modal实例(this.super)里已有的方法,然后在confirm和cancel里分别触发了所对应的自定义事件(其实两个方法里也没处理什么逻辑,主要就是触发事件)。

笔者在这里是通过绑定modal现有的事件来触发这些新的事件。代码的位置放置在InfoModal类里的最底部。代码如下:

  1. var that = this; // 防止污染作用域,用临时变量that
  2.  
  3. this.$element.on("show.bs.modal", function (e) {
  4. that.$element.trigger(e = $.Event('show.bs.infomodal'));
  5. if (e.isDefaultPrevented()) return;
  6. });
  7.  
  8. this.$element.on("shown.bs.modal", function (e) {
  9. that.$element.trigger(e = $.Event('shown.bs.infomodal'));
  10. if (e.isDefaultPrevented()) return;
  11. });
  12.  
  13. this.$element.on("hide.bs.modal", function (e) {
  14. that.$element.trigger(e = $.Event('hide.bs.infomodal'));
  15. if (e.isDefaultPrevented()) return;
  16. });
  17.  
  18. this.$element.on("hidden.bs.modal", function (e) {
  19. that.$element.trigger(e = $.Event('hidden.bs.infomodal'));
  20. if (e.isDefaultPrevented()) return;
  21. });

也就是,modal触发自己的事件时,就会利用这些回调触发InfoModal类的事件,而且这样做有个好处:那就是我如果阻塞了事件(调用e.preventDefault()),modal后续就不能再触发了。所以如果分别对这8个事件进行定义,它们的执行顺序应该如下:

❑show.bs.modal

❑show.bs.infomodal

❑shown.bs.modal

❑shown.bs.infomodal

❑hide.bs.modal

❑hide.bs.infomodal

❑hidden.bs.modal

❑hidden.bs.infomodal

读者可能会问:不在show和hide方法里,是否也可以触发新的show、shown、hide、hidden自定义事件呢?这是可以的,大家可以自行试验。

另外,由于这种机制是因为对新的事件进行绑定,肯定得先触发Modal的事件,才能触发Infomodal的事件。如果想要改变show.bs.modal和show.bs.infomodal的执行顺序,也必须在新的show和hide原型方法里改变触发顺序的设置,大家可以自行试验一下。

在jQuery.fn.infomodal插件定义的时候,和modal几乎一样,除了要把对应的名字全换成InfoModal以外,唯一需要注意的就是,InfoModal默认应该是关闭的,每次触发时都应该打开,也就是说原来定义的toggle方法基本上用不上。具体代码如下:

  1. $.fn.infomodal = function (option, _relatedTarget) {
  2. return this.each(function () {
  3. // 根据选择器,遍历所有符合规则的元素
  4.  
  5. var $this = $(this)
  6. var data = $this.data('bs.infomodal') // 获取自定义属性bs.infomodal的值
  7. var options = $.extend({}, InfoModal.DEFAULTS, $this.data(),
  8. typeof option == 'object' && option)
  9. // 将默认参数、选择器所在元素的自定义属性(data-开头)和option参数,
  10. // 这3种的值合并在一起,作为options参数
  11. // 优先级:后面的参数优先级高于前面的参数
  12. options.show = false; // 默认先关闭,然后在后面手动打开
  13. var modal = $this.modal(options, _relatedTarget);
  14.  
  15. if (!data) $this.data('bs.infomodal', (data = new InfoModal(this, options)))
  16. // 如果值不存在,则将InfoModal实例设置为bs.infomodal的值
  17. if (typeof option == 'string') {
  18. data[option](_relatedTarget)
  19. }
  20. else {
  21. data.show(_relatedTarget);
  22. }
  23. // 如果option传递了string,则表示要执行某个方法
  24. // 比如传入了show,则要执行InfoModal实例的show方法,data["show"]相当于data.show()
  25. })
  26. }

其他关于防冲突的和DATA-API的,和modal除了名字不一样,其他都一样(InfoModal是通过[data-toggle="infomodal"]触发的)。运行效果如图6-3所示。

6.2.2 Info弹窗扩展 - 图1 图6-3 Info弹窗运行效果

使用时的示例代码如下:

  1. <div class="modal fade" id="DisabledPopup" data-close-after-confirm="true">
  2. <div class="modal-dialog">
  3. <div class="modal-content">
  4. <div class="modal-header">... </div>
  5. <div class="modal-body">... </div>
  6. <div class="modal-footer">
  7. <button type="button" class="btn btn-success" data-info="confirm">
  8. 确认</button>
  9. <button type="button" class="btn btn-danger" data-info="cancel">
  10. 取消</button>
  11. </div>
  12. </div>
  13. </div>
  14. </div>
  15. <button class="btn btn-warning" data-toggle="infomodal" data-target="#DisabledPopup"
  16. data-backdrop="static">Disable</button>

由示例代码也可以看到,我们没有改动原有modal的任何内容,只是添加了一些自定义属性。同样的道理,InfoModal既支持声明式用法,也支持JavaScript用法,具体的实例读者可以自行试验。完整的源代码如下:

  1. +function ($) {
  2. "use strict";
  3.  
  4. // InfoModal类定义
  5. var InfoModal = function (element, options) {
  6. this.$element = $(element);
  7. this.super = this.$element.data('bs.modal');
  8. // 获取自定义属性bs.modal的值
  9. this.options = options;
  10.  
  11. this.$element.on('click.confirm.infomodal', '[data-info="confirm"]',
  12. $.proxy(this.confirm, this));
  13. this.$element.on('click.cancel.infomodal', '[data-info="cancel"]',
  14. $.proxy(this.cancel, this));
  15.  
  16. var that = this; // 防止污染作用域,用临时变量that
  17.  
  18. this.$element.on("show.bs.modal", function (e) {
  19. that.$element.trigger(e = $.Event('show.bs.infomodal'));
  20. if (e.isDefaultPrevented()) return;
  21. });
  22.  
  23. this.$element.on("shown.bs.modal", function (e) {
  24. that.$element.trigger(e = $.Event('shown.bs.infomodal'));
  25. if (e.isDefaultPrevented()) return;
  26. });
  27.  
  28. this.$element.on("hide.bs.modal", function (e) {
  29. that.$element.trigger(e = $.Event('hide.bs.infomodal'));
  30. if (e.isDefaultPrevented()) return;
  31. });
  32.  
  33. this.$element.on("hidden.bs.modal", function (e) {
  34. that.$element.trigger(e = $.Event('hidden.bs.infomodal'));
  35. if (e.isDefaultPrevented()) return;
  36. });
  37. }
  38.  
  39. // 默认设置
  40. InfoModal.DEFAULTS = {
  41. closeAfterConfirm: false,
  42. closeAfterCancel: true
  43. }
  44. // 反转弹窗状态
  45. InfoModal.prototype.toggle = function (_relatedTarget) {
  46. return this[!this.super.isShown ? 'show' : 'hide'](_relatedTarget)
  47. // 如果是关闭状态,则打开弹窗,否则关闭
  48. }
  49. // 打开弹窗
  50. InfoModal.prototype.show = function (_relatedTarget) {
  51. this.super.show(_relatedTarget);
  52. }
  53. // 关闭弹窗
  54. InfoModal.prototype.hide = function (e) {
  55. if (e) e.preventDefault(); // 先阻止冒泡行为
  56. this.super.hide();
  57. }
  58. // 单击“确认”按钮的行为
  59. InfoModal.prototype.confirm = function (e) {
  60. if (e) e.preventDefault(); // 先阻止冒泡行为
  61.  
  62. var e = $.Event('confirm.bs.infomodal');
  63. this.$element.trigger(e); // 确认前触发事件,主要用于处理相关代码
  64.  
  65. if (e.isDefaultPrevented()) return;
  66. if (this.options.closeAfterConfirm) {
  67. this.hide(e); // 如果设置了data-close-after-confirm=true
  68. // 参数,则关闭弹窗
  69. }
  70. }
  71. // 单击“取消”按钮的行为
  72. InfoModal.prototype.cancel = function (e) {
  73. if (e) e.preventDefault(); // 先阻止冒泡行为
  74.  
  75. var e = $.Event('cancel.bs.infomodal');
  76. this.$element.trigger(e); // 取消前先触发事件
  77.  
  78. if (e.isDefaultPrevented()) return;
  79. if (this.options.closeAfterCancel) {
  80. this.hide(e); // 如果设置了data-close-after-cancel=true
  81. // 参数,则关闭弹窗
  82. }
  83. }
  84. // InfoModal 插件定义
  85. var old = $.fn.infomodal
  86. // 如果定义了其他infomodal,则保留它(以便在noConflict(解决防冲突)之后,
  87. // 可以继续使用该旧插件代码)
  88.  
  89. $.fn.infomodal = function (option, _relatedTarget) {
  90. return this.each(function () {
  91. // 根据选择器,遍历所有符合规则的元素
  92.  
  93. var $this = $(this)
  94. var data = $this.data('bs.infomodal')
  95. // 获取自定义属性bs.infomodal的值
  96. var options = $.extend({}, InfoModal.DEFAULTS, $this.data(),
  97. typeof option == 'object' && option)
  98. // 将默认参数、选择器所在元素的自定义属性(data-开头)、option参数这3种的值合
  99. // 并在一起,作为options参数
  100. // 优先级:后面的参数优先级高于前面的参数
  101. options.show = false; // 默认先关闭,然后在后面手动打开
  102. var modal = $this.modal(options, _relatedTarget);
  103.  
  104. if (!data) $this.data('bs.infomodal', (data = new InfoModal(this, options)))
  105. // 如果值不存在,则将InfoModal实例设置为bs.infomodal的值
  106. // 如果option传递了string,则表示要执行某个方法
  107. // 比如传入了show,则要执行InfoModal实例的show方法,data["show"]相当于data.show();
  108. if (typeof option == 'string') {
  109. data[option](_relatedTarget)
  110. }
  111. else {
  112. data.show(_relatedTarget);
  113. }
  114. })
  115. }
  116. $.fn.infomodal.Constructor = InfoModal;
  117. // 重设插件构造器,可以通过该属性获取插件的真实类函数
  118.  
  119. // InfoModal防冲突
  120. $.fn.infomodal.noConflict = function () {
  121. $.fn.infomodal = old
  122. return this
  123. }
  124. // InfoModal DATA-API
  125. $(document).on('click.bs.infomodal.data-api', '[data-toggle="infomodal"]',
  126. function (e) {
  127. // 监测所有拥有自定义属性data-toggle="modal"的元素上的单击事件
  128. var $this = $(this)
  129. var href = $this.attr('href') // 获取href属性值
  130. var $target = $($this.attr('data-target') || (href &&
  131. href.replace(/.*(?=#[^\s]+$)/, '')))
  132. // 获取data-target属性值,如果没有,则获取href值,该值是所弹出元素的id
  133.  
  134. var option = $target.data('infomodal') ? 'toggle' : $.extend
  135. ({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
  136. // 如果弹窗元素上设置了data-infomodal属性值,则option值是字符串toggle
  137. // 否则将remote值(如果有)、弹窗元素上的自定义属性值集合、触发元素上的自定义属性
  138. // 值集合进行合并,作为option选项对象
  139.  
  140. e.preventDefault() // 阻止默认行为
  141. $target
  142. .infomodal(option, this)
  143. // 给弹窗元素绑定infomodal插件(也就是实例化InfoModal),并传入option参数
  144. .one('hide', function () {
  145. $this.is(':visible') && $this.focus()
  146. // 定义一次hide事件,给所单击元素加上焦点
  147. })
  148. })
  149. }(jQuery);