Angular-UI手風琴效果源碼分析

/*
 * angular-ui-bootstrap
 * http://angular-ui.github.io/bootstrap/

 * Version: 0.14.3 - 2015-10-23
 * License: MIT
 */
angular.module("ui.bootstrap", ["ui.bootstrap.tpls","ui.bootstrap.accordion","ui.bootstrap.collapse"]);
angular.module("ui.bootstrap.tpls", ["template/accordion/accordion-group.html","template/accordion/accordion.html"]);
angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])

.constant('uibAccordionConfig', {//constant 可以將一個已經存在的變量值注冊為服務
  closeOthers: true
})

.controller('UibAccordionController', ['$scope', '$attrs', 'uibAccordionConfig', function($scope, $attrs, accordionConfig) {
  // This array keeps track of the accordion groups
  this.groups = [];//用數組來保存所有的手風琴組

  // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to //確保所有的手風琴組是關閉的,除非其它關閉的并不是
  this.closeOthers = function(openGroup) { //關閉函數 如果oneAtATime存在且為true
    var closeOthers = angular.isDefined($attrs.closeOthers) ?  $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;//判斷$attrs.closeOthers是否定義過
    if (closeOthers) {
      angular.forEach(this.groups, function(group) { //遍歷所有的手風琴組
        if (group !== openGroup) { //如果沒有規定打開的手風琴組,就全部關閉
          group.isOpen = false;
        }
      });
    }
  };

  // This is called from the accordion-group directive to add itself to the accordion //要向手風琴組添加對象時調用的函數
  this.addGroup = function(groupScope) { //添加的是作用域
    var that = this;
    this.groups.push(groupScope);

    groupScope.$on('$destroy', function(event) {//當監聽到銷毀,就移除手風琴組
      that.removeGroup(groupScope);
    });
  };

  // This is called from the accordion-group directive when to remove itself //要移除手風琴組時候調用,需要移除數組中對應的項
  this.removeGroup = function(group) { 
    var index = this.groups.indexOf(group);
    if (index !== -1) {
      this.groups.splice(index, 1);
    }
  };

}])

    // The accordion directive simply sets up the directive controller 這個指令只是建立了這個指令的控制器和添加了一個css類型
    // and adds an accordion CSS class to itself element.
    //<uib-accordion close-others="oneAtATime">  </uib-accordion> 對應指令 模板<div class=\"panel-group\" ng-transclude></div>
.directive('uibAccordion', function() {
  return {
    controller: 'UibAccordionController',
    controllerAs: 'accordion',//控制器別名
    transclude: true,//可以替換
    templateUrl: function(element, attrs) { //
      return attrs.templateUrl || 'template/accordion/accordion.html';
    }
  };
})

    // The accordion-group directive indicates a block of html that will expand and collapse in an   這個指令表示出一段html展開或者折疊在一個手風琴效果中
    // <uib-accordion-group heading="Static Header, initially expanded" is-open="status.isFirstOpen" is-disabled="status.isFirstDisabled">This content is straight in the template. </uib-accordion-group>  的指令
.directive('uibAccordionGroup', function() {
  return {
    require: '^uibAccordion',         // We need this directive to be inside an accordion //我們需要這個指令來在其中放入一個手風琴效果
    transclude: true,              // It transcludes the contents of the directive into the template 允許這個指令的內容到模板中進行替換
    replace: true,                // The element containing the directive will be replaced with the template
    templateUrl: function(element, attrs) { //調用模板
      return attrs.templateUrl || 'template/accordion/accordion-group.html';
    },
    scope: {
      heading: '@',               // Interpolate the heading attribute onto this scope //傳遞的是字符串
      isOpen: '=?', //=雙向綁定
      isDisabled: '=?' //?告訴指令如果沒有找到依賴的指令,不要拋出異常。
    },
    controller: function() { //設置當前對象的heading 
      this.setHeading = function(element) {
        this.heading = element;
      };
    },
    link: function(scope, element, attrs, accordionCtrl) {
      accordionCtrl.addGroup(scope);//把當前scope添加進 手風琴數組

      scope.openClass = attrs.openClass || 'panel-open';//展開的樣式
      scope.panelClass = attrs.panelClass;//獲取具體定義的類名
      scope.$watch('isOpen', function(value) {//監聽isOpen,判斷是否發生變化 
        element.toggleClass(scope.openClass, !!value);//如果isOpen為ture時,就添加展開的樣式
        if (value) {
          accordionCtrl.closeOthers(scope);
        }
      });

      scope.toggleOpen = function($event) { //手風琴開關函數
        if (!scope.isDisabled) { //scope.isDisabled為false時執行
          if (!$event || $event.which === 32) {
            scope.isOpen = !scope.isOpen;
          }
        }
      };
    }
  };
})

// Use accordion-heading below an accordion-group to provide a heading containing HTML
    //使用手風琴標題下面的手風琴組,提供一個內容標簽
    //對應指令 <uib-accordion-heading> I can have markup, too! <i class="pull-right glyphicon" ng-class="{'glyphicon-chevron-down': status.open, 'glyphicon-chevron-right': !status.open}"></i> </uib-accordion-heading>
.directive('uibAccordionHeading', function() {
  return {
    transclude: true,   // Grab the contents to be used as the heading
    template: '',       // In effect remove this element!
    replace: true,
    require: '^uibAccordionGroup',
    link: function(scope, element, attrs, accordionGroupCtrl, transclude) {
      // Pass the heading to the accordion-group controller
      // so that it can be transcluded into the right place in the template
      // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
      accordionGroupCtrl.setHeading(transclude(scope, angular.noop));
    }
  };
})

    // Use in the accordion-group template to indicate where you want the heading to be transcluded 
    // You must provide the property on the accordion-group controller that will hold the transcluded element
    //<a href tabindex="0" class="accordion-toggle" ng-click="toggleOpen()" uib-accordion-transclude="heading"><span ng-class="{'text-muted': isDisabled}">{{heading}}</span></a>
.directive('uibAccordionTransclude', function() {
  return {
    require: ['?^uibAccordionGroup', '?^accordionGroup'],
    link: function(scope, element, attrs, controller) { 
      controller = controller[0] ? controller[0] : controller[1]; // Delete after we remove deprecation
      scope.$watch(function() { return controller[attrs.uibAccordionTransclude]; }, function(heading) {//{{heading}}  
        if (heading) {
          element.find('span').html('');
          element.find('span').append(heading);
        }
      });
    }
  };
});

/* Deprecated accordion below 
//棄用的手風琴 dead code
angular.module('ui.bootstrap.accordion')

  .value('$accordionSuppressWarning', false)
    //
  .controller('AccordionController', ['$scope', '$attrs', '$controller', '$log', '$accordionSuppressWarning', function($scope, $attrs, $controller, $log, $accordionSuppressWarning) {
    if (!$accordionSuppressWarning) {
      $log.warn('AccordionController is now deprecated. Use UibAccordionController instead.');
    }

    angular.extend(this, $controller('UibAccordionController', { //擴展當前控制器
      $scope: $scope,
      $attrs: $attrs
    })); //加載控制器并傳入一個作用域,同AngularJS在運行時做的一樣
  }])

  .directive('accordion', ['$log', '$accordionSuppressWarning', function($log, $accordionSuppressWarning) {
    return {
      restrict: 'EA',
      controller: 'AccordionController',
      controllerAs: 'accordion', //控制器別名
      transclude: true,
      replace: false,
      templateUrl: function(element, attrs) {
        return attrs.templateUrl || 'template/accordion/accordion.html';//替換成 <div class=\"panel-group\" ng-transclude></div>
      },
      link: function() {
        if (!$accordionSuppressWarning) {
          $log.warn('accordion is now deprecated. Use uib-accordion instead.');
        }
      }
    };
  }])

  .directive('accordionGroup', ['$log', '$accordionSuppressWarning', function($log, $accordionSuppressWarning) {
    return {
      require: '^accordion',         // We need this directive to be inside an accordion
      restrict: 'EA',
      transclude: true,              // It transcludes the contents of the directive into the template
      replace: true,                 // The element containing the directive will be replaced with the template
      templateUrl: function(element, attrs) {
        return attrs.templateUrl || 'template/accordion/accordion-group.html';
      },
      scope: {
        heading: '@',   //傳遞字符串值            // Interpolate the heading attribute onto this scope
        isOpen: '=?',  //=雙向綁定
        isDisabled: '=?'  //=雙向綁定
      },
      controller: function() { //讓這個指令的heading等于傳入的參數
        this.setHeading = function(element) {
          this.heading = element;
        };
      },
      link: function(scope, element, attrs, accordionCtrl) {
        if (!$accordionSuppressWarning) {
          $log.warn('accordion-group is now deprecated. Use uib-accordion-group instead.');
        }

        accordionCtrl.addGroup(scope);

        scope.openClass = attrs.openClass || 'panel-open';
        scope.panelClass = attrs.panelClass;
        scope.$watch('isOpen', function(value) {
          element.toggleClass(scope.openClass, !!value);
          if (value) {
            accordionCtrl.closeOthers(scope);
          }
        });

        scope.toggleOpen = function($event) {
          if (!scope.isDisabled) {
            if (!$event || $event.which === 32) {
              scope.isOpen = !scope.isOpen;
            }
          }
        };
      }
    };
  }])

  .directive('accordionHeading', ['$log', '$accordionSuppressWarning', function($log, $accordionSuppressWarning) {
    return {
      restrict: 'EA',
      transclude: true,   // Grab the contents to be used as the heading
      template: '',       // In effect remove this element!
      replace: true,
      require: '^accordionGroup',
      link: function(scope, element, attr, accordionGroupCtrl, transclude) {
        if (!$accordionSuppressWarning) {
          $log.warn('accordion-heading is now deprecated. Use uib-accordion-heading instead.');
        }
        // Pass the heading to the accordion-group controller
        // so that it can be transcluded into the right place in the template
        // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
        accordionGroupCtrl.setHeading(transclude(scope, angular.noop));
      }
    };
  }])

  .directive('accordionTransclude', ['$log', '$accordionSuppressWarning', function($log, $accordionSuppressWarning) {
    return {
      require: '^accordionGroup',
      link: function(scope, element, attr, controller) {
        if (!$accordionSuppressWarning) {
          $log.warn('accordion-transclude is now deprecated. Use uib-accordion-transclude instead.');
        }

        scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
          if (heading) {
            element.find('span').html('');
            element.find('span').append(heading);
          }
        });
      }
    };
  }]);
*/
//具體內容展示模塊,沒有獨立scope,與父級使用同一個scope
angular.module('ui.bootstrap.collapse', [])
//<div class="panel-collapse collapse" uib-collapse="!isOpen">的指令
  .directive('uibCollapse', ['$animate', '$injector', function($animate, $injector) {
    var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null;//判斷是否有$animateCss服務注入,如果有就取得這個服務
    return {
      link: function(scope, element, attrs) {//link函數
          //展開函數
        function expand() {
          element.removeClass('collapse') 
            .addClass('collapsing')
            .attr('aria-expanded', true)//aria-expanded表示展開狀態。默認為undefined, 表示當前展開狀態未知。這里表示展開
            .attr('aria-hidden', false);//這里表示關閉

          if ($animateCss) {//如果有$animateCss服務,就用來添加動畫
            $animateCss(element, {
              addClass: 'in',//in樣式就是block
              easing: 'ease',
              to: { height: element[0].scrollHeight + 'px' }
            }).start().finally(expandDone);
          } else {//如果沒有就用ng自帶的動畫模塊
            $animate.addClass(element, 'in', {
              to: { height: element[0].scrollHeight + 'px' }
            }).then(expandDone);//返回一個promise對象,用then來處理成功回調
          }
        }
        //展開后回調
        function expandDone() {//具體的成功回調函數,用來操作class
          element.removeClass('collapsing')
            .addClass('collapse')//display:none
            .css({height: 'auto'});
        }
        //收縮函數
        function collapse() {
          if (!element.hasClass('collapse') && !element.hasClass('in')) {//如果element沒有collapse樣式,并且也沒有in樣式
            return collapseDone();
          }

          element
            // IMPORTANT: The height must be set before adding "collapsing" class.
            // Otherwise, the browser attempts to animate from height 0 (in
            // collapsing class) to the given height here.
            .css({height: element[0].scrollHeight + 'px'}) //設置高度
            // initially all panel collapse have the collapse class, this removal
            // prevents the animation from jumping to collapsed state
            .removeClass('collapse') //移除collapse,也就是移除display:none
            .addClass('collapsing')  //添加collapsing,高度0
            .attr('aria-expanded', false)
            .attr('aria-hidden', true);

          if ($animateCss) {
            $animateCss(element, {
              removeClass: 'in',
              to: {height: '0'}
            }).start().finally(collapseDone);
          } else {
            $animate.removeClass(element, 'in', {//設置動畫,高度為0
              to: {height: '0'}
            }).then(collapseDone);
          }
        }
        //收縮后回調函數
        function collapseDone() {
          element.css({height: '0'}); // Required so that collapse works when animation is disabled 動畫不執行時運行
          element.removeClass('collapsing')//collapsing設置高度為0
            .addClass('collapse');//display:none
        }
        //監聽attrs.uibCollapse也就是!isOpen的值,來判斷展開和關閉
        scope.$watch(attrs.uibCollapse, function(shouldCollapse) { 
          if (shouldCollapse) {
            collapse();
          } else {
            expand();
          }
        });
      }
    };
  }]);

/* Deprecated collapse below 
 dead code
angular.module('ui.bootstrap.collapse')

  .value('$collapseSuppressWarning', false)

  .directive('collapse', ['$animate', '$injector', '$log', '$collapseSuppressWarning', function($animate, $injector, $log, $collapseSuppressWarning) {
    var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null;
    return {
      link: function(scope, element, attrs) {
        if (!$collapseSuppressWarning) {
          $log.warn('collapse is now deprecated. Use uib-collapse instead.');
        }

        function expand() {
          element.removeClass('collapse')
            .addClass('collapsing')
            .attr('aria-expanded', true)
            .attr('aria-hidden', false);

          if ($animateCss) {
            $animateCss(element, {
              easing: 'ease',
              to: { height: element[0].scrollHeight + 'px' }
            }).start().done(expandDone);
          } else {
            $animate.animate(element, {}, {
              height: element[0].scrollHeight + 'px'
            }).then(expandDone);
          }
        }

        function expandDone() {
          element.removeClass('collapsing')
            .addClass('collapse in')
            .css({height: 'auto'});
        }

        function collapse() {
          if (!element.hasClass('collapse') && !element.hasClass('in')) {
            return collapseDone();
          }

          element
            // IMPORTANT: The height must be set before adding "collapsing" class.
            // Otherwise, the browser attempts to animate from height 0 (in
            // collapsing class) to the given height here.
            .css({height: element[0].scrollHeight + 'px'})
            // initially all panel collapse have the collapse class, this removal
            // prevents the animation from jumping to collapsed state
            .removeClass('collapse in')
            .addClass('collapsing')
            .attr('aria-expanded', false)
            .attr('aria-hidden', true);

          if ($animateCss) {
            $animateCss(element, {
              to: {height: '0'}
            }).start().done(collapseDone);
          } else {
            $animate.animate(element, {}, {
              height: '0'
            }).then(collapseDone);
          }
        }

        function collapseDone() {
          element.css({height: '0'}); // Required so that collapse works when animation is disabled
          element.removeClass('collapsing')
            .addClass('collapse');
        }

        scope.$watch(attrs.collapse, function(shouldCollapse) {
          if (shouldCollapse) {
            collapse();
          } else {
            expand();
          }
        });
      }
    };
  }]);
*/
    
angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) {
  $templateCache.put("template/accordion/accordion-group.html",
    "<div class=\"panel {{panelClass || 'panel-default'}}\">\n" +
    "  <div class=\"panel-heading\" ng-keypress=\"toggleOpen($event)\">\n" +
    "    <h4 class=\"panel-title\">\n" +
    "      <a href tabindex=\"0\" class=\"accordion-toggle\" ng-click=\"toggleOpen()\" uib-accordion-transclude=\"heading\"><span ng-class=\"{'text-muted': isDisabled}\">{{heading}}</span></a>\n" +
    "    </h4>\n" +
    "  </div>\n" +
    "  <div class=\"panel-collapse collapse\" uib-collapse=\"!isOpen\">\n" +
    "     <div class=\"panel-body\" ng-transclude></div>\n" +
    "  </div>\n" +
    "</div>\n" +
    "");
}]);

angular.module("template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) {
  $templateCache.put("template/accordion/accordion.html",
    "<div class=\"panel-group\" ng-transclude></div>");
}]);

<div ng-controller="AccordionDemoCtrl">
  <script type="text/ng-template" id="group-template.html">
    <div class="panel {{panelClass || 'panel-default'}}">
      <div class="panel-heading">
        <h4 class="panel-title" style="color:#fa39c3">
          <a href tabindex="0" class="accordion-toggle" ng-click="toggleOpen()" uib-accordion-transclude="heading"><span
            ng-class="{'text-muted': isDisabled}">{{heading}}</span></a>
        </h4>
      </div>
      <div class="panel-collapse collapse" uib-collapse="!isOpen">
        <div class="panel-body" style="text-align: right" ng-transclude></div>
      </div>
    </div>
  </script>

  <p>
    <button type="button" class="btn btn-default btn-sm" ng-click="status.open = !status.open">Toggle last panel</button>
    <button type="button" class="btn btn-default btn-sm" ng-click="status.isFirstDisabled = ! status.isFirstDisabled">Enable / Disable first panel</button>
  </p>

  <div class="checkbox">
    <label>
      <input type="checkbox" ng-model="oneAtATime">
      Open only one at a time
    </label>
  </div>
  <uib-accordion close-others="oneAtATime">
    <uib-accordion-group heading="Static Header, initially expanded" is-open="status.isFirstOpen" is-disabled="status.isFirstDisabled">
      This content is straight in the template.
    </uib-accordion-group>
    <uib-accordion-group heading="{{group.title}}" ng-repeat="group in groups">
      {{group.content}}
    </uib-accordion-group>
    <uib-accordion-group heading="Dynamic Body Content">
      <p>The body of the uib-accordion group grows to fit the contents</p>
      <button type="button" class="btn btn-default btn-sm" ng-click="addItem()">Add Item</button>
      <div ng-repeat="item in items">{{item}}</div>
    </uib-accordion-group>
    <uib-accordion-group heading="Custom template" template-url="group-template.html">
      Hello
    </uib-accordion-group>
    <uib-accordion-group heading="Delete account" panel-class="panel-danger">
      <p>Please, to delete your account, click the button below</p>
      <button class="btn btn-danger">Delete</button>
    </uib-accordion-group>
    <uib-accordion-group is-open="status.open">
      <uib-accordion-heading>
        I can have markup, too! <i class="pull-right glyphicon" ng-class="{'glyphicon-chevron-down': status.open, 'glyphicon-chevron-right': !status.open}"></i>
      </uib-accordion-heading>
      This is just some content to illustrate fancy headings.
    </uib-accordion-group>
  </uib-accordion>
</div>
angular.module('ui.bootstrap.demo').controller('AccordionDemoCtrl', function ($scope) {
  $scope.oneAtATime = true;
  $scope.groups = [
    {
      title: 'Dynamic Group Header - 1',
      content: 'Dynamic Group Body - 1'
    },
    {
      title: 'Dynamic Group Header - 2',
      content: 'Dynamic Group Body - 2'
    }
  ];
  $scope.items = ['Item 1', 'Item 2', 'Item 3'];
  $scope.addItem = function() {
    var newItemNo = $scope.items.length + 1;
    $scope.items.push('Item ' + newItemNo);
  };
  $scope.status = {
    isFirstOpen: true,
    isFirstDisabled: false
  };
});

第一部分是angular插件的源碼,后面是官網的示例(https://angular-ui.github.io/bootstrap/)。
插件最核心的就是實現不同指令間的通信,這個插件實現的方式是讓兩個作用域,UibAccordionController和uibCollapse這兩個控制器下的作用域下的實現通信。通信的方式是利用父子指令間,通過scope繼承實現的。分別繼承(雙向綁定)heading\isOpen\isDisabled三個參數。然后在分別的指令中對isOpen進行監聽,就可以得到不同的效果。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容