【问题标题】:Angular tabs - sortable/moveable角度标签 - 可排序/可移动
【发布时间】:2014-04-03 23:20:27
【问题描述】:

是否有任何 Angular JS 选项卡指令允许对其重新排序(如浏览器的选项卡)

如果不是一个开始的实现会很棒

使用angular-ui-bootstap

<tabset> 
    <tab ng-repeat="tab in vm.tabs" active="tab.active" sortable-tab> </tab> 
    <tab disabled="true" ng-click"vm.addNewTab()" class="nonSortable-addTab-plusButton"></tab> 
</tabset>

如何使它们可重新排序?

编辑:添加赏金以使用上面的原始标签集语法。

【问题讨论】:

    标签: angularjs angularjs-directive


    【解决方案1】:

    使用 Angular UI Bootstrap tabset,只需一个 sortable-tab 指令:

    <tabset>
      <tab sortable-tab ng-repeat="tab in tabs" heading="{{tab.title}}" active="tab.active" disabled="tab.disabled">
        <p>{{tab.content}}</p>
      </tab>
      <tab disabled="true">
        <tab-heading>
          <i class="glyphicon glyphicon-plus"></i>
        </tab-heading>
      </tab>
    </tabset>
    

    首先,它需要一些技巧/hack 才能与ngRepeat 集成,因此它可以重新排序数组。它(重新)解析 ng-repeat 属性,并从范围中获取数组,就像 ngRepeat 所做的那样

    // Attempt to integrate with ngRepeat
    // https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js#L211
    var match = attrs.ngRepeat.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
    var tabs;
    scope.$watch(match[2], function(newTabs) {
      tabs = newTabs;
    });
    

    您还可以在作用域上查看$index 变量,以确保您始终拥有当前元素的最新索引:

    var index = scope.$index;
    scope.$watch('$index', function(newIndex) {
      index = newIndex;
    });
    

    然后使用HTML5 drag and drop,通过setDatagetData将元素的索引作为其数据传递

    attrs.$set('draggable', true);
    
    // Wrapped in $apply so Angular reacts to changes
    var wrappedListeners = {
      // On item being dragged
      dragstart: function(e) {
        e.dataTransfer.effectAllowed = 'move';
        e.dataTransfer.dropEffect = 'move';
        e.dataTransfer.setData('application/json', index);
        element.addClass('dragging');
      },
      dragend: function(e) {
        e.stopPropagation();
        element.removeClass('dragging');
      },
    
      dragleave: function(e) {
        element.removeClass('hover');
      },
      drop: function(e) {
        e.preventDefault();
        e.stopPropagation();
        var sourceIndex = e.dataTransfer.getData('application/json');
        move(sourceIndex, index);
        element.removeClass('hover');
      }
    };
    
    // For performance purposes, do not
    // call $apply for these
    var unwrappedListeners = {
      dragover: function(e) {
        e.preventDefault();
        element.addClass('hover');
      },
      /* Use .hover instead of :hover. :hover doesn't play well with 
         moving DOM from under mouse when hovered */
      mouseenter: function() {
        element.addClass('hover');
      },
      mouseleave: function() {
        element.removeClass('hover');
      }
    };
    
    angular.forEach(wrappedListeners, function(listener, event) {
      element.on(event, wrap(listener));
    });
    
    angular.forEach(unwrappedListeners, function(listener, event) {
      element.on(event, listener);
    });
    
    function wrap(fn) {
      return function(e) {
        scope.$apply(function() {
          fn(e);
        });
      };
    }
    

    注意:对于某些悬停效果,使用 hover 类而不是 :hover 有一些技巧。这部分是因为 CSS :hover 样式在元素从鼠标下方重新排列后没有被删除,至少在 Chrome 中是这样。

    实际移动选项卡的函数,获取ngRepeat 使用的数组,并对其重新排序:

    function move(fromIndex, toIndex) {
      // http://stackoverflow.com/a/7180095/1319998
      tabs.splice(toIndex, 0, tabs.splice(fromIndex, 1)[0]);
    };
    

    你可以看到这一切in a Plunker

    【讨论】:

    • 感谢您的解决方案我真的很高兴看到它在 plunkr 中工作。尽管 e.dataTransfer 在包装的 dragstart 侦听器中未定义,但我遇到了一个小问题。当我将您的 plunkr 中的调用堆栈与我自己的进行比较时,我发现在您的情况下 angular.js 会触发事件,而在我的调用堆栈中 jquery.js 会触发事件。知道为什么吗?
    • 如果您使用的是 jQuery,事件可能会被包装。您可能必须在侦听器中使用 e = e.originalEvent 才能访问本机事件对象。
    • 正则表达式无法正确匹配我的 ng-repeat:ng-repeat="tab in vm.tabsCollection" 我认为它试图确保变量以“s”结尾,而“collection”没有。此外,它不是直接在作用域上,作用域['vm.tabsCollection'] 无法访问嵌套的道具,所以我只是删除了黑客并使用:var tabs = scope['vm']['tabsCollection'] || scope['vm']['tabs']; 我的情况使用这个约定就足够了。
    • 我不认为是“s”,而是“.”。我已经编辑了帖子 + 链接的 Plunker 以使用任何 expression
    • Michal,标签的拖放工作正常。现在,我的应用程序中有 50 个选项卡,所以除了拖放之外,我还需要支持分页,一次显示 5 个选项卡,并且有上一个和下一个图标来来回导航选项卡。 In addition when the tab is dragged to the edge of the visible tab set, the remaining tabs should be scrolled automatically so that that the dragged tab can be dropped anywhere else within the entire tabset.是否可以使用此功能修改指令?
    【解决方案2】:

    如果您不想使用 Angular UI,比如出于尺寸原因,您可以推出自己的基本版本。演示地址为http://plnkr.co/edit/WnvZETQlxurhgcm1k6Hd?p=preview

    数据

    您说您不需要标签是动态的,但这可能会使它们更易于重用。所以在包装范围内,你可以有:

    $scope.tabs = [{
      header: 'Tab A',
      content: 'Content of Tab A'
    },{
      header: 'Tab B',
      content: 'Content of Tab B'
    }, {
      header: 'Tab C',
      content: 'Content of Tab C'
    }];
    

    标签 HTML

    设计 HTML 结构,您可以在上面的列表中重复按钮和内容

    <tabs>
      <tab-buttons>
        <tab-button ng-repeat="tab in tabs">{{tab.header}}</tab-button>
      </tab-buttons>
      <tab-contents>
        <tab-content ng-repeat="tab in tabs">{{tab.content}}</tab-body>
      </tab-contents>
    </tabs>
    

    标签指令

    有很多方法可以做到这一点,但一种方法是在单个按钮指令上注册点击处理程序,然后将它们传达给父 tabs 控制器。这可以使用require 属性来完成,在父控制器上公开一个方法,在本例中为show,并通过传递ngRepeat 添加到范围的变量$index 来传递按钮的当前索引.

    app.directive('tabs', function($timeout) {
      return {
        restrict: 'E',
        controller: function($element, $scope) {
          var self = this;
    
          this.show = function(index) {
            // Show only current tab
            var contents = $element.find('tab-content');
            contents.removeClass('current');
            angular.element(contents[index]).addClass('current');
    
            // Mark correct header as current
            var buttons = $element.find('tab-button');
            buttons.removeClass('current');
            angular.element(buttons[index]).addClass('current');
          };
    
          $timeout(function() {
            self.show('0');
          });
        }
      };
    });
    
    app.directive('tabButton', function() {
      return {
        restrict: 'E',
        require: '^tabs',
        link: function(scope, element, attr, tabs) {
          element.on('click', function() {
            tabs.show(scope.$index);   
          });
        }
      };
    });
    

    假设您在页面中有正确的 CSS,特别是 .current 类的样式,如 http://plnkr.co/edit/WnvZETQlxurhgcm1k6Hd?p=preview ,此时有一组工作标签。

    可排序

    使用 HTML5 拖放 API,您可以进行一些基本的拖放操作,而无需担心鼠标位置等问题。首先要做的是设计使其工作所需的属性。在这种情况下,父项上的 sortable 属性引用列表,sortable-item 属性包含对当前项索引的引用。

    <tabs sortable="tabs">
      <tab-buttons>
        <tab-button ng-repeat="tab in list" sortable-item="$index">{{tab.header}}</tab-button>
      </tab-buttons>
      <tab-contents>
        <tab-content ng-repeat="tab in list">{{tab.content}}</tab-body>
      </tab-contents>
    </tabs>
    

    sortablesortableItem 指令可以如下所示(更多详细信息可以在 http://www.html5rocks.com/en/tutorials/dnd/basics/ 找到)

    app.directive('sortable', function() {
      return {
        controller: function($scope, $attrs) {
          var listModel = null;
          $scope.$watch($attrs.sortable, function(sortable) {
            listModel = sortable;
          });
          this.move = function(fromIndex, toIndex) {
            // http://stackoverflow.com/a/7180095/1319998
            listModel.splice(toIndex, 0, listModel.splice(fromIndex, 1)[0]);
          };
        }
      };
    });
    
    app.directive('sortableItem', function($window) {
      return {
        require: '^sortable',
        link: function(scope, element, attrs, sortableController) {
          var index = null;
          scope.$watch(attrs.sortableItem, function(newIndex) {
            index = newIndex;
          });
    
          attrs.$set('draggable', true);
    
          // Wrapped in $apply so Angular reacts to changes
          var wrappedListeners = {
            // On item being dragged
            dragstart: function(e) {
              e.dataTransfer.effectAllowed = 'move';
              e.dataTransfer.dropEffect = 'move';
              e.dataTransfer.setData('application/json', index);
              element.addClass('dragging');
            },
            dragend: function(e) {
              e.stopPropagation();
              element.removeClass('dragging');
            },
    
            // On item being dragged over / dropped onto
            dragenter: function(e) {
              element.addClass('hover');
            },
            dragleave: function(e) {
              element.removeClass('hover');
            },
            drop: function(e) {
              e.preventDefault();
              e.stopPropagation();
              element.removeClass('hover');
              var sourceIndex = e.dataTransfer.getData('application/json');
              sortableController.move(sourceIndex, index);
            }
          };
    
          // For performance purposes, do not
          // call $apply for these
          var unwrappedListeners = {
            dragover: function(e) {
              e.preventDefault();
            }
          };
    
          angular.forEach(wrappedListeners, function(listener, event) {
            element.on(event, wrap(listener));
          });
    
          angular.forEach(unwrappedListeners, function(listener, event) {
            element.on(event, listener);
          });
    
          function wrap(fn) {
            return function(e) {
              scope.$apply(function() {
                fn(e);
              });
            };
          }
        }
      };
    });
    

    主要需要注意的是每个sortableItem只需要知道它当前的索引即可。如果它检测到另一个项目已被丢弃,它会调用sortable 控制器上的函数,然后在外部范围内重新排序数组。 ngRepeat 然后做它平常的事情并移动标签。

    虽然我怀疑有更简单的解决方案,但这个解决方案的可排序行为和选项卡行为完全解耦。您可以在不是tabs 的元素上使用sortable,也可以在没有可排序行为的情况下使用tabs

    【讨论】:

    • 不是一个糟糕的实现,但有点矫枉过正。我尝试切换我现有的 angularui 选项卡集,但它只是对 UI 造成了严重破坏。在我的情况下,这必须从一开始就使用。如果您能想到一个指令覆盖在 angular-ui 选项卡集之上,我添加了 100 赏金
    【解决方案3】:

    至少有两种方法可以实现。

    第一。转到 http://angular-ui.github.io/bootstrap/ 并下载引导选项卡。 Bootstrap UI 是用 Angularjs 编写的,包含很多有用的模块。尽管您必须自己实现一些代码来动态添加新选项卡,但这应该是微不足道的。只需使用 ng-click 创建一个按钮/div,它会调用一个动态添加新选项卡的函数。

    第二。使用 ng-repeat 自己实现它。下面只是一些伪代码。

    HTML:
    
    <div class="tabs" ng-controller="TabController">
       <div class="add-tab" ng-click="add_tab()"></div>
    
       <div ng-repeat="tab in tabs" class="tab"></div>
    </div>
    
    Controller(JS):
    
    app.controller('TabController',['$scope', function($scope){
    $scope.tabs = [1, 1]
    $scope.add_tab = function(){
    $scope.tabs.push(1);
    }
    }]);
    

    关于可排序部分。您可以创建自己的可排序(基本上给选项卡一个可拖动的组件,如果这样做,您应该将其编写为指令),使用 jQuery,或使用一些 Angularjs 可排序/可拖动,这很容易通过搜索找到。

    【讨论】:

    • 很抱歉没有澄清,但可排序是我没有把头放在头上的功能,因此是标题的焦点。添加标签很容易,是的,它当然是引导程序和指令。我从问题中删除了标签的动态添加,因此不再有混淆。谢谢
    • 有道理。我假设您并不热衷于使用 jQuery 来完成它。我曾经写过一个原生的 Angularjs 可拖动,但因为它更容易、更好看、更广泛的浏览器支持以及更好的性能明智地使用 jQuery,所以我把它刮掉了。否则,如果您想编写自己的,一个很好的起点来做 angularjs 原生,docs.angularjs.org/guide/directive,他们在那里有一个可拖动的演示。您可以使用它,然后对移动对象实施一些约束。
    【解决方案4】:

    我为此做了一个Plunker。 为此,我使用了AngularJS 和来自Angular-UIui-sortable Angular 指令。我还使用了Bootstrap tabs 来简化它。

    剩下要做的就是连接所有这些。

    我希望这个例子可以帮助到你。

    【讨论】:

    • 你可以使用原始的 angular-ui tabset 指令而不剖析生成的标记吗?请查看额外的赏金
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-21
    • 2010-11-17
    • 1970-01-01
    相关资源
    最近更新 更多