【问题标题】:jQuery UI Tabs plugin can't handle 2 tiers of tabsjQuery UI 选项卡插件无法处理 2 层选项卡
【发布时间】:2020-03-03 01:13:26
【问题描述】:

我正在处理一个遗留项目,该项目使用 jQuery UI 选项卡以结构化方式显示用户生成的数据。系统不限制用户可以创建的标签数量(但通常第二层有30个左右),jQuery UI Tabs只是将标签包装起来,看起来很不专业。

我从 2014 年偶然发现了 Paul Blundell 的漂亮插件 OverflowTabs(这是他的 JSFiddle:http://jsfiddle.net/o1wLbtj4/,),但不幸的是,我无法让它与我的 2 层选项卡一起正常工作。我正在使用 jQuery 1.10.2 和 jQuery UI 1.10.4。

我制作了一个简化的 JSFiddle,它显示了我正在尝试做的事情:打开一个对话窗口,其中包含 2 层选项卡中的数据。当标签太多时,两个层都应该将溢出的选项卡推入一个单击时打开下拉目的菜单中,但此刻(在附层上的名称上)仅显示最后一个选项卡,下拉按钮不起作用并读取 0:http://jsfiddle.net/megahra/6s2xwgec/18/

据我所知,插件正确地从第一层删除选项卡并将它们添加到溢出 div(_hideTab 函数),但由于某种原因,后来这被覆盖了。我怀疑该插件无法正确处理第二层,但我不知道在哪里解决这个问题。

如果有人能指出我正确的方向,我将不胜感激!

编辑:
感谢 Twisty 的帮助,该插件现在适用于多层选项卡,并且我修复了一些错误。
这是一个有效的 JSFiddle:https://jsfiddle.net/megahra/uexr4qfw/289/
- 已知错误:在初始化时,此插件不适用于带有style="display: none" 的 div 中的选项卡。

$.widget("ui.tabs", $.ui.tabs, {
  options: {
    overflowTabs: false,
    tabPadding: 25,
    containerPadding: 0,
    dropdownSize: 50
  },

  _create: function() {
    this._super("_create");
    this.tabsWidth = 0;
    this.containerWidth = 0;
    this.overflowTabsId = "id" + this._createUniqueId();

    $(this.element).addClass(this.overflowTabsId);

    if (!this.options.overflowTabs)
      return;

    // update the tabs
    this.updateOverflowTabs();

    // Detect a window resize and check the tabs again
    var that = this;
    var el = this.element;

    $(window).resize(function() {
      // Add a slight delay after resize, to fix Maximise issue.
      setTimeout(function() {
        that.updateOverflowTabs();
      }, 150);
    });

    // Detect dropdown click
    $(el).on("click", '> .overflow-selector', function(e) {
      $('> .ui-tabs-overflow', el).toggleClass('hide');

      $overflowTabs = $('.ui-tabs-overflow').not($('> .ui-tabs-overflow', el));

      //if there is more than the currently clicked one, close all others
      if($overflowTabs) {
        $overflowTabs.toggleClass('hide', true);
        }
    });

    //Detect tab click
    $('li a').on("click", function(e) {
        //close dropdown if open
        $('> .ui-tabs-overflow', el).toggleClass('hide', true);
        //ToDo: apply overflowTabs plugin to content of new tab (if it contains tabs)

    });
  },

  refresh: function() {
    this._super("refresh");
    this.updateOverflowTabs();
  },

  updateOverflowTabs: function() {
    var failsafe = 0;
    this._calculateWidths();
    var el = this.element;

    // Loop until tabsWidth is less than the containerWidth
    while (this.tabsWidth > this.containerWidth -10 && failsafe < 30) {
      this._hideTab();
      this._calculateWidths();
      failsafe++;
    }

    // Finish now if there are no tabs in the overflow list
    if ($('> .ui-tabs-overflow li', el).length === 0)
      return;

    // Reset
    failsafe = 0;

    // Get the first tab in the overflow list
    var next = this._nextTab();

    // Loop until we cannot fit any more tabs
    while (next.totalSize < this.containerWidth && $('> .ui-tabs-overflow li', el).length > 0 && failsafe < 30) {
      this._showTab(next.tab);
      this._calculateWidths();
      next = this._nextTab();
      failsafe++;
    }

    $('> .overflow-selector .total', el).html($('> .ui-tabs-overflow li', el).length);
  },

  _calculateWidths: function() {
    var width = 0;
    $(this.element).find('> .ui-tabs-nav > li').each(function() {
      width += $(this).outerWidth(true);
    });
    this.tabsWidth = width;
    this.containerWidth = $(this.element).parent().width() - this.options.containerPadding - this.options.dropdownSize;
  },
  _hideTab: function() {
    if (!$(this.element).find('> .ui-tabs-overflow').length) {
      $(this.element).find('> .ui-tabs-nav').after('<ul class="ui-tabs-overflow hide"></ul>');
      $(this.element).find('> .ui-tabs-overflow').after('<div class="overflow-selector">&#8595 <span class="total">0</span></div>');
      //calculate position of overflow-selector relativ to tab row (overflow-selector is 15px high)
      let topOffset = ($(this.element).find('> .ui-tabs-nav').innerHeight() * 0.48) - 12;
      $(this.element).find('> .overflow-selector').css('top', topOffset);
    }
    var lastTab = $('> .ui-tabs-nav li', this.element).last();
    lastTab.prependTo($('> .ui-tabs-overflow', this.element));
  },
  _showTab: function(tab) {
    tab.appendTo($('> .ui-tabs-nav', this.element));

    // Check to see if overflow list is now empty
    if ($(this.element).find('> .ui-tabs-overflow li').size() == 0) {
      $(this.element).find('> .ui-tabs-overflow').remove();
      $(this.element).find('> .overflow-selector').remove();
    }
  },
  _nextTab: function() {
    var result = {};
    var firstTab = $(this.element).find('> .ui-tabs-overflow li').first();
    result.tab = firstTab;
    result.totalSize = this.tabsWidth + this._textWidth(firstTab) + this.options.tabPadding;
    return result;
  },
  _textWidth: function(element) {
    var self = $(element),
      children = self.children(),
      calculator = $('<span style="display: inline-block;" />'),
      width;

    children.wrap(calculator);
    width = children.parent().width();
    children.unwrap();

    return width;
  },

  _createUniqueId: function() {
    // Math.random should be unique because of its seeding algorithm.
    // Convert it to base 36 (numbers + letters), and grab the first 9 characters
    // after the decimal.
    return '_' + Math.random().toString(36).substr(2, 9);
    }
});

【问题讨论】:

    标签: jquery css jquery-ui jquery-plugins


    【解决方案1】:

    发现了一些遗留代码的用法,只是做了一些小的清理工作。我发现了一些问题。

    工作示例:https://jsfiddle.net/Twisty/tbvasj1g/

    1. 点击事件是贪婪的,并且正在隐藏不需要隐藏的列表项
    2. 添加/删除类是预切换方法。更新为使用.toggleClass()
    3. .size() 折旧为length

      注意:此方法已在 jQuery 3.0 中删除。请改用 .length 属性。 .size() 方法在功能上等同于 .length 属性;但是,.length 属性是首选,因为它没有函数调用的开销。

    4. $(this.element).find(selector) 替换为简写$(selector, this.element)

    5. .total 更新从计算函数移到更新函数中。在循环结束时,有些东西将其重置回0。不知道是什么。在那里更新它是没有意义的,所以我把它移走了,它现在可以工作了。

    小部件工厂

    $.widget("ui.tabs", $.ui.tabs, {
      options: {
        overflowTabs: false,
        tabPadding: 25,
        containerPadding: 0,
        dropdownSize: 50
      },
      _create: function() {
        this._super("_create");
        this.tabsWidth = 0;
        this.containerWidth = 0;
        if (!this.options.overflowTabs)
          return;
    
        // update the tabs
        this.updateOverflowTabs();
    
        // Detect a window resize and check the tabs again
        var that = this;
        var el = this.element;
    
        $(window).resize(function() {
          // Add a slight delay after resize, to fix Maximise issue.
          setTimeout(function() {
            that.updateOverflowTabs();
          }, 150);
        });
    
        // Detect dropdown click
        $(".tabStructure").on("click", ".overflow-selector", function(e) {
          $('.ui-tabs-overflow', el).toggleClass('hide');
        });
      },
    
      refresh: function() {
        this._super("refresh");
        this.updateOverflowTabs();
      },
    
      updateOverflowTabs: function() {
        var failsafe = 0;
        this._calculateWidths();
        var el = this.element;
        console.log("cWidth", this.containerWidth);
    
        // Loop until tabsWidth is less than the containerWidth
        while (this.tabsWidth > this.containerWidth && failsafe < 30) {
          //debugger;
          this._hideTab();
          this._calculateWidths();
          failsafe++;
        }
    
        // Finish now if there are no tabs in the overflow list
        if ($('.ui-tabs-overflow li', el).length === 0)
          return;
    
        // Reset
        failsafe = 0;
    
        // Get the first tab in the overflow list
        var next = this._nextTab();
    
        // Loop until we cannot fit any more tabs
        while (next.totalSize < this.containerWidth && $('.ui-tabs-overflow li', el).length > 0 && failsafe < 30) {
          this._showTab(next.tab);
          this._calculateWidths();
          next = this._nextTab();
          failsafe++;
        }
    
        $('.overflow-selector .total', el).html($('.ui-tabs-overflow li', el).length);
      },
    
      _calculateWidths: function() {
        var width = 0;
        $(this.element).find('.ui-tabs-nav > li').each(function() {
          width += $(this).outerWidth(true);
          //debugger;
        });
        this.tabsWidth = width;
        this.containerWidth = $(this.element).parent().width() - this.options.containerPadding - this.options.dropdownSize;
      },
      _hideTab: function() {
        if (!$('.ui-tabs-overflow').length) {
          $(this.element).find('.ui-tabs-nav').after('<ul class="ui-tabs-overflow hide"></ul>');
          $(this.element).find('.ui-tabs-overflow').after('<div class="overflow-selector">&#8595 <span class="total">0</span></div>');
        }
        var lastTab = $(this.element).find('.ui-tabs-nav li').last();
        lastTab.appendTo($(this.element).find('.ui-tabs-overflow'));
      },
      _showTab: function(tab) {
        tab.appendTo($(this.element).find('.ui-tabs-nav'));
    
        // Check to see if overflow list is now empty
        if ($(this.element).find('.ui-tabs-overflow li').size() == 0) {
          $(this.element).find('.ui-tabs-overflow').remove();
          $(this.element).find('.overflow-selector').remove();
        }
      },
      _nextTab: function() {
        var result = {};
        var firstTab = $(this.element).find('.ui-tabs-overflow li').first();
        result.tab = firstTab;
        result.totalSize = this.tabsWidth + this._textWidth(firstTab) + this.options.tabPadding;
        return result;
      },
      _textWidth: function(element) {
        var self = $(element),
          children = self.children(),
          calculator = $('<span style="display: inline-block;" />'),
          width;
    
        children.wrap(calculator);
        width = children.parent().width();
        children.unwrap();
    
        return width;
      }
    });
    
    $(function() {
      $("#rowDialog").dialog({
        title: 'Test modal',
        closeOnEscape: false,
        minHeight: 'auto',
        width: '600px',
        modal: true,
        resizable: true,
        buttons: [{
          // Close button
          text: 'Close',
          click: function() {
            $(this).dialog('close')
          },
        }]
      });
    
      $(".tabStructure").tabs({
        overflowTabs: true
      });
    });
    

    总体上不喜欢这种解决方案。我会建议标签的分页。如果其他选项卡中没有内容或内容不多,则下拉菜单将被剪裁。此外,如果您从下拉列表中选择一个选项卡,它会被隐藏,并且您无法知道您正在查看哪个选项卡,它们不是视觉指示器。

    【讨论】:

    • 谢谢! :) 我从你的回答中学到了很多。我选择这个解决方案而不是标签分页的原因是我的项目中的标签标签包含一个状态符号,并且用户希望能够一次看到所有标签。至于下拉剪辑:我的每个编号的选项卡都包含一个表格,因此我可以保证足够的内容以防止剪辑。您对下拉列表中所选项目的视觉指示器提出了很好的意见。我肯定会补充一点(就像 Paul Blundell 在他的 jsfiddle 中所说的那样)。但是你能告诉我为什么插件没有应用到第二层标签吗?
    • 哪个版本的 jQuery 引入了 $(selector, this.element) 简写? (因为我们的项目还是用jQuery 1.10.2...)
    • 现在适用于两层标签。我最终将直接子 (>) 添加到插件中的所有选择器语句中,例如$('&gt; .ui-tabs-overflow li', this.element),以确保目标层是正确的。该插件仍然存在严重问题(我正在发布一个新问题),但如果您将此添加到您的答案中,我会接受它。 :) 谢谢!
    • @megahra 选择器上下文是用.find()方法实现的;因此,$( "li.item-ii" ).find( "li" ) 等价于 $( "li", "li.item-ii" ) api.jquery.com/jquery/#selector-context
    • 是的,谢谢,我从您的回答中了解到这一点。但是所有选择器都需要以直接子选择器(>)作为前缀的原因是可能有多个级别的选项卡。这导致了 DOM 中不同级别的多个溢出选择器按钮和多个下拉菜单元素。如果您不告诉插件您想要作为其初始化节点的直接子节点的元素集,它会将选项卡放入错误的容器中,一次打开所有下拉菜单并执行各种不需要的行为。