angularjs中UI插件ui-bootstrap中modal模态弹出源码分析

最近看了一下UI-Bootstrap的源码,感觉写的非常好,对自己写angular程序有很大的启发,源码指令也很高,这里先简单分析一下,然后把思路整理一下,如果有错误欢迎指出来。
我用思维导图做了一个模块依赖关系,可以结合看一下,以下是连接https://www.processon.com/view/link/565d9d78e4b00e1bc065a30b

/*
 * 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.modal","ui.bootstrap.stackedMap"]);
angular.module("ui.bootstrap.tpls", ["template/modal/backdrop.html","template/modal/window.html"]);
angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
/**
 * A helper, internal data structure that stores all references attached to key
 */
  .factory('$$multiMap', function() {//生成一个map数据结构用来保存
    return {
      createNew: function() {
        var map = {};//内部数据,声明周期是整个app的声明周期,用来保存数据

        return {
          entries: function() {//生成一个map数据结构
            return Object.keys(map).map(function(key) {//Object.keys(keys).map(function(key){})  Object.keys(keys)返回keys上的所有属性和方法
              return {
                key: key,
                value: map[key]
              };
            });
          },
          get: function(key) {//get 获取map上对应key的value
            return map[key];
          },
          hasKey: function(key) {//判断map数据结构上是否有key
            return !!map[key]; //把!!强制转换为布尔类型
          },
          keys: function() { //返回map数据结构上的所有属性和方法
            return Object.keys(map);
          },
          put: function(key, value) {//把key-value放入map
            if (!map[key]) {
              map[key] = []; //一个key可以对应多个value
            }

            map[key].push(value);
          },
          remove: function(key, value) {// 移除key-vaue值
            var values = map[key];

            if (!values) {
              return;
            }

            var idx = values.indexOf(value);

            if (idx !== -1) {
              values.splice(idx, 1);//splice(index,1)
            }

            if (!values.length) {
              delete map[key];
            }
          }
        };
      }
    };
  })

/**
 * A helper directive for the $modal service. It creates a backdrop element.
 */
  .directive('uibModalBackdrop', [ //背景指令
           '$animate', '$injector', '$uibModalStack',
  function($animate ,  $injector,   $modalStack) {
    var $animateCss = null;

    if ($injector.has('$animateCss')) { //判断$animateCss是否注入到服务中
      $animateCss = $injector.get('$animateCss');//如果有获取
    }

    return {
      replace: true,
      templateUrl: 'template/modal/backdrop.html',
      compile: function(tElement, tAttrs) {
        tElement.addClass(tAttrs.backdropClass); //为element添加backdrop-class属性对应的类名
        return linkFn;
      }
    };

    function linkFn(scope, element, attrs) {
      // Temporary fix for prefixing
      element.addClass('modal-backdrop');//为element添加modal-backdrop类

      if (attrs.modalInClass) {//如果属性集合中有modal-in-class
        if ($animateCss) {//并且有$animateCss
          $animateCss(element, {//调用函数
            addClass: attrs.modalInClass
          }).start();
        } else {
          $animate.addClass(element, attrs.modalInClass);// 使用angular自定义动画,添加属性集合中modal-in-class对应的类
        }

        scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {//监听$modalStack.NOW_CLOSING_EVENT事件
          var done = setIsAsync();
          if ($animateCss) {
            $animateCss(element, {
              removeClass: attrs.modalInClass
            }).start().then(done);
          } else {
            $animate.removeClass(element, attrs.modalInClass).then(done);// 使用angular自定义动画,添加属性集合中modal-in-class对应的类
          }
        });
      }
    }
  }])

  .directive('uibModalWindow', [//模态窗口指令
           '$uibModalStack', '$q', '$animate', '$injector',
  function($modalStack ,  $q ,  $animate,   $injector) {
    var $animateCss = null;

    if ($injector.has('$animateCss')) {//判断是否注入了$animateCss这个服务
      $animateCss = $injector.get('$animateCss');//如果有就获取
    }

    return {
      scope: {
        index: '@'//同父scope的index属性进行绑定
      },
      replace: true,
      transclude: true,
      templateUrl: function(tElement, tAttrs) { //模板
        return tAttrs.templateUrl || 'template/modal/window.html';
      },
      link: function(scope, element, attrs) {
        element.addClass(attrs.windowClass || '');//为element添加属性集合attrs中window-class属性对应的类,就是in
        element.addClass(attrs.windowTopClass || '');//为element添加属性集合attrs中window-top-class属性对应的类
        scope.size = attrs.size;

        scope.close = function(evt) { //关闭方法
          var modal = $modalStack.getTop(); //获取$uibModalStack顶部栈
          if (modal && modal.value.backdrop && modal.value.backdrop !== 'static' && (evt.target === evt.currentTarget)) {
            evt.preventDefault(); //阻止默认方法
            evt.stopPropagation();//阻止事件冒泡
            $modalStack.dismiss(modal.key, 'backdrop click');//调用$uibModalStack中返回的dismiss方法,解除key-value
          }
        };

        // moved from template to fix issue #2280
        element.on('click', scope.close);

        // This property is only added to the scope for the purpose of detecting when this directive is rendered.
        // We can detect that by using this property in the template associated with this directive and then use
        // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}.
        scope.$isRendered = true;

        // Deferred object that will be resolved when this modal is render.
        var modalRenderDeferObj = $q.defer();//获取一个异步对象
        // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready.
        // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template.
        attrs.$observe('modalRender', function(value) { 
          if (value == 'true') {
            modalRenderDeferObj.resolve();
          }
        });

        modalRenderDeferObj.promise.then(function() {
          var animationPromise = null;

          if (attrs.modalInClass) {//同上都是来判断用什么动画方法
            if ($animateCss) {
              animationPromise = $animateCss(element, {
                addClass: attrs.modalInClass
              }).start();
            } else {
              animationPromise = $animate.addClass(element, attrs.modalInClass);
            }

            scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
              var done = setIsAsync();
              if ($animateCss) {
                $animateCss(element, {
                  removeClass: attrs.modalInClass
                }).start().then(done);
              } else {
                $animate.removeClass(element, attrs.modalInClass).then(done);
              }
            });
          }


          $q.when(animationPromise).then(function() {
            var inputWithAutofocus = element[0].querySelector('[autofocus]');//获取element中有[autofocus]属性的元素
            /**
             * Auto-focusing of a freshly-opened modal element causes any child elements
             * with the autofocus attribute to lose focus. This is an issue on touch
             * based devices which will show and then hide the onscreen keyboard.
             * Attempts to refocus the autofocus element via JavaScript will not reopen
             * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
             * the modal element if the modal does not contain an autofocus element.
             */
            if (inputWithAutofocus) {
              inputWithAutofocus.focus(); //获取焦点
            } else {
              element[0].focus();
            }
          });

          // Notify {@link $modalStack} that modal is rendered.
          var modal = $modalStack.getTop();//获取顶部的栈
          if (modal) {
            $modalStack.modalRendered(modal.key); //渲染顶部栈
          }
        });
      }
    };
  }])

  .directive('uibModalAnimationClass', function() { //uib-modal-animation-class的指令
    return {
      compile: function(tElement, tAttrs) {
        if (tAttrs.modalAnimation) {
          tElement.addClass(tAttrs.uibModalAnimationClass);
        }
      }
    };
  })

  .directive('uibModalTransclude', function() { //ui-modal-transclude指令
    return {
      link: function($scope, $element, $attrs, controller, $transclude) { //transclude链接函数是实际被执行用来克隆元素和操作DOM的函数
        $transclude($scope.$parent, function(clone) {
          $element.empty(); //清空element
          $element.append(clone);//添加参数元素,这里用来向element添加元素
        });
      }
    };
  })

  .factory('$uibModalStack', [ //$uibModalStack服务
             '$animate', '$timeout', '$document', '$compile', '$rootScope',
             '$q',
             '$injector',
             '$$multiMap',
             '$$stackedMap',
    function($animate ,  $timeout ,  $document ,  $compile ,  $rootScope ,
              $q,
              $injector,
              $$multiMap,
              $$stackedMap) {
      var $animateCss = null;

      if ($injector.has('$animateCss')) { //判断是否有注入的$animateCss动画
        $animateCss = $injector.get('$animateCss');//获取
      }

      var OPENED_MODAL_CLASS = 'modal-open';

      var backdropDomEl, backdropScope;
      var openedWindows = $$stackedMap.createNew();//$$stackedMap实例
      var openedClasses = $$multiMap.createNew();//$$multiMap实例
      var $modalStack = { 
        NOW_CLOSING_EVENT: 'modal.stack.now-closing'
      };

      //Modal focus behavior
      var focusableElementList;
      var focusIndex = 0;
      var tababbleSelector = 'a[href], area[href], input:not([disabled]), ' + //多去焦点的元素
        'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' +
        'iframe, object, embed, *[tabindex], *[contenteditable=true]';

      function backdropIndex() { //设置背景z-index值
        var topBackdropIndex = -1;
        var opened = openedWindows.keys(); //获取openedWindows中map对象所有的属性和方法
        for (var i = 0; i < opened.length; i++) {
          if (openedWindows.get(opened[i]).value.backdrop) { //遍历openedWindows中value属性中backdrop
            topBackdropIndex = i;
          }
        }
        return topBackdropIndex;
      }

      $rootScope.$watch(backdropIndex, function(newBackdropIndex) { //监听backdropIndex,当有新的值的时候backdropScope.index等于新值
        if (backdropScope) {
          backdropScope.index = newBackdropIndex;
        }
      });

      function removeModalWindow(modalInstance, elementToReceiveFocus) { //移除模态窗口
        var body = $document.find('body').eq(0);//获取body对象
        var modalWindow = openedWindows.get(modalInstance).value; //获取openedWindows中modalInstance对应的实例的值

        //clean up the stack
        openedWindows.remove(modalInstance);//从openedWindows这个数据结构中移除modalInstance这个对象

        removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() { //移除后动画函数
          var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS; //value值中openedClass的属性值
          openedClasses.remove(modalBodyClass, modalInstance);//另一个map结构也移除
          body.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass));//body toggleClass动画
          toggleTopWindowClass(true);
        });
        checkRemoveBackdrop();

        //move focus to specified element if available, or else to body
        if (elementToReceiveFocus && elementToReceiveFocus.focus) { //获取焦点
          elementToReceiveFocus.focus();
        } else {
          body.focus();
        }
      }

      // Add or remove "windowTopClass" from the top window in the stack 添加或删除windowTopClass的类名在栈顶上
      function toggleTopWindowClass(toggleSwitch) {
        var modalWindow;

        if (openedWindows.length() > 0) {
          modalWindow = openedWindows.top().value;//openedWindows栈顶的value值
          modalWindow.modalDomEl.toggleClass(modalWindow.windowTopClass || '', toggleSwitch);//modalWindow上的元素动画
        }
      }

      function checkRemoveBackdrop() {
        //remove backdrop if no longer needed
        if (backdropDomEl && backdropIndex() == -1) {
          var backdropScopeRef = backdropScope;
          removeAfterAnimate(backdropDomEl, backdropScope, function() {
            backdropScopeRef = null;
          });
          backdropDomEl = undefined;
          backdropScope = undefined;
        }
      }

      function removeAfterAnimate(domEl, scope, done) { //动画后移除dom
        var asyncDeferred;
        var asyncPromise = null;
        var setIsAsync = function() { //生成一个promise对象
          if (!asyncDeferred) {
            asyncDeferred = $q.defer();
            asyncPromise = asyncDeferred.promise;
          }

          return function asyncDone() {
            asyncDeferred.resolve();
          };
        };
        scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync);

        // Note that it's intentional that asyncPromise might be null.
        // That's when setIsAsync has not been called during the
        // NOW_CLOSING_EVENT broadcast.
        return $q.when(asyncPromise).then(afterAnimating);//执行

        function afterAnimating() { //对应动画
          if (afterAnimating.done) {
            return;
          }
          afterAnimating.done = true;

          if ($animateCss) {
            $animateCss(domEl, {
              event: 'leave'
            }).start().then(function() {
              domEl.remove();
            });
          } else {
            $animate.leave(domEl);//angular自定义动画
          }
          scope.$destroy();
          if (done) {
            done();
          }
        }
      }

      $document.bind('keydown', function(evt) {//绑定keydow事件
        if (evt.isDefaultPrevented()) { //判断是否调用e.preventDefault函数
          return evt;
        }

        var modal = openedWindows.top(); //获取栈顶值
        if (modal && modal.value.keyboard) {
          switch (evt.which) {
            case 27: {
              evt.preventDefault();
              $rootScope.$apply(function() {
                $modalStack.dismiss(modal.key, 'escape key press');
              });
              break;
            }
            case 9: {
              $modalStack.loadFocusElementList(modal);
              var focusChanged = false;
              if (evt.shiftKey) {
                if ($modalStack.isFocusInFirstItem(evt)) {
                  focusChanged = $modalStack.focusLastFocusableElement();
                }
              } else {
                if ($modalStack.isFocusInLastItem(evt)) {
                  focusChanged = $modalStack.focusFirstFocusableElement();
                }
              }

              if (focusChanged) {
                evt.preventDefault();
                evt.stopPropagation();
              }
              break;
            }
          }
        }
      });

      $modalStack.open = function(modalInstance, modal) { //创建一个模态框
        var modalOpener = $document[0].activeElement, //获取有焦点的元素
          modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS; //获取类名

        toggleTopWindowClass(false);//栈顶不触发

        openedWindows.add(modalInstance, {//openedWindows注入keyvalue形式默认参数
          deferred: modal.deferred,
          renderDeferred: modal.renderDeferred,
          modalScope: modal.scope,
          backdrop: modal.backdrop,
          keyboard: modal.keyboard,
          openedClass: modal.openedClass,
          windowTopClass: modal.windowTopClass
        });

        openedClasses.put(modalBodyClass, modalInstance); //openedClasses注入模态的类,和刚才注入的模态实例

        var body = $document.find('body').eq(0), //获取body对象
            currBackdropIndex = backdropIndex(); //获取有backdrop的属性值的索引

        if (currBackdropIndex >= 0 && !backdropDomEl) {//如果存在
          backdropScope = $rootScope.$new(true);//生成新的scope
          backdropScope.index = currBackdropIndex;
          var angularBackgroundDomEl = angular.element('
');//生成背景angular元素 angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass);//添加backdrop-class属性值,为modal.backdropClass if (modal.animation) {//如果modal传入了animation angularBackgroundDomEl.attr('modal-animation', 'true');//设置属性modal-animation为true } backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope);//$compile编译angulardom元素,并且传入当前scope,生成dom元素 body.append(backdropDomEl); } var angularDomEl = angular.element('
');//模态窗 angularDomEl.attr({//设置属性 'template-url': modal.windowTemplateUrl, 'window-class': modal.windowClass, 'window-top-class': modal.windowTopClass, 'size': modal.size, 'index': openedWindows.length() - 1, 'animate': 'animate' }).html(modal.content); if (modal.animation) { angularDomEl.attr('modal-animation', 'true'); } var modalDomEl = $compile(angularDomEl)(modal.scope); openedWindows.top().value.modalDomEl = modalDomEl;//获取openedWindows栈顶值的dom元素 openedWindows.top().value.modalOpener = modalOpener; body.append(modalDomEl);//添加模态框 body.addClass(modalBodyClass);//添加类名 $modalStack.clearFocusListCache(); }; function broadcastClosing(modalWindow, resultOrReason, closing) {//向下广播事件modal.closing return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;//preventDefault把defaultPrevented标志设置为true。尽管不能停止事件的传播,可以告诉子作用域无需处理这个事件 } //消除模态框 $modalStack.close = function(modalInstance, result) { var modalWindow = openedWindows.get(modalInstance); if (modalWindow && broadcastClosing(modalWindow, result, true)) { modalWindow.value.modalScope.$$uibDestructionScheduled = true; modalWindow.value.deferred.resolve(result);//成功执行promise removeModalWindow(modalInstance, modalWindow.value.modalOpener); return true; } return !modalWindow; }; //解除模态框 $modalStack.dismiss = function(modalInstance, reason) { var modalWindow = openedWindows.get(modalInstance); if (modalWindow && broadcastClosing(modalWindow, reason, false)) { modalWindow.value.modalScope.$$uibDestructionScheduled = true; modalWindow.value.deferred.reject(reason);//失败 removeModalWindow(modalInstance, modalWindow.value.modalOpener); return true; } return !modalWindow; }; $modalStack.dismissAll = function(reason) {//解除所有模态框 var topModal = this.getTop(); while (topModal && this.dismiss(topModal.key, reason)) { topModal = this.getTop(); } }; $modalStack.getTop = function() {//返回openedWindows栈顶 return openedWindows.top(); }; $modalStack.modalRendered = function(modalInstance) {//重新渲染 var modalWindow = openedWindows.get(modalInstance); if (modalWindow) { modalWindow.value.renderDeferred.resolve(); } }; $modalStack.focusFirstFocusableElement = function() {//获取第一个焦点值 if (focusableElementList.length > 0) { focusableElementList[0].focus(); return true; } return false; }; $modalStack.focusLastFocusableElement = function() {//获取最后一个焦点值 if (focusableElementList.length > 0) { focusableElementList[focusableElementList.length - 1].focus(); return true; } return false; }; $modalStack.isFocusInFirstItem = function(evt) {//判断第一个是否获取焦点 if (focusableElementList.length > 0) { return (evt.target || evt.srcElement) == focusableElementList[0]; } return false; }; $modalStack.isFocusInLastItem = function(evt) {//判断最后一个是否获取焦点 if (focusableElementList.length > 0) { return (evt.target || evt.srcElement) == focusableElementList[focusableElementList.length - 1]; } return false; }; $modalStack.clearFocusListCache = function() {//清空焦点 focusableElementList = []; focusIndex = 0; }; $modalStack.loadFocusElementList = function(modalWindow) {//获取所有焦点的对象 if (focusableElementList === undefined || !focusableElementList.length) { if (modalWindow) { var modalDomE1 = modalWindow.value.modalDomEl; if (modalDomE1 && modalDomE1.length) { focusableElementList = modalDomE1[0].querySelectorAll(tababbleSelector); } } } }; return $modalStack; }]) .provider('$uibModal', function() { var $modalProvider = { options: { animation: true, backdrop: true, //can also be false or 'static' keyboard: true }, $get: ['$injector', '$rootScope', '$q', '$templateRequest', '$controller', '$uibModalStack', '$modalSuppressWarning', '$log', function ($injector, $rootScope, $q, $templateRequest, $controller, $modalStack, $modalSuppressWarning, $log) { var $modal = {}; function getTemplatePromise(options) {// return options.template ? $q.when(options.template) : $templateRequest(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl);//$templateRequest通过http下载模板,并存储在templateCache缓存中 } function getResolvePromises(resolves) {//获取resolvespromise对象 var promisesArr = []; angular.forEach(resolves, function(value) { if (angular.isFunction(value) || angular.isArray(value)) { promisesArr.push($q.when($injector.invoke(value)));//$q.when()把对象封装成promise对象 } else if (angular.isString(value)) { promisesArr.push($q.when($injector.get(value))); } else { promisesArr.push($q.when(value)); } }); return promisesArr; } var promiseChain = null;//返回promise链 $modal.getPromiseChain = function() { return promiseChain; }; $modal.open = function(modalOptions) { var modalResultDeferred = $q.defer();//获取延迟对象 var modalOpenedDeferred = $q.defer();//获取延迟对象 var modalRenderDeferred = $q.defer();//获取延迟对象 //prepare an instance of a modal to be injected into controllers and returned to a caller var modalInstance = {//模态实例 result: modalResultDeferred.promise,//获取promise对象 opened: modalOpenedDeferred.promise,//获取promise对象 rendered: modalRenderDeferred.promise,//获取promise对象 close: function (result) { //关闭实例 return $modalStack.close(modalInstance, result); }, dismiss: function (reason) {//解除实例(即不可能完成promise) return $modalStack.dismiss(modalInstance, reason); } }; //merge and clean up options modalOptions = angular.extend({}, $modalProvider.options, modalOptions);//把传入的modalOptions同$modalProvider.options进行扩展 modalOptions.resolve = modalOptions.resolve || {}; //verify options if (!modalOptions.template && !modalOptions.templateUrl) {//当templateUrl并且template不存在抛出错误 throw new Error('One of template or templateUrl options is required.'); } var templateAndResolvePromise = $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));//参数接收为一个promise数组,返回一个新的单一promise对象,当这些promise对象对应defer对象全部解决这个单一promise对象才会解决 function resolveWithTemplate() { return templateAndResolvePromise; } // Wait for the resolution of the existing promise chain. // Then switch to our own combined promise dependency (regardless of how the previous modal fared). // Then add to $modalStack and resolve opened. // Finally clean up the chain variable if no subsequent modal has overwritten it. var samePromise; samePromise = promiseChain = $q.all([promiseChain]) .then(resolveWithTemplate, resolveWithTemplate) .then(function resolveSuccess(tplAndVars) {//成功resolve var modalScope = (modalOptions.scope || $rootScope).$new();//生成一个新的scope modalScope.$close = modalInstance.close; modalScope.$dismiss = modalInstance.dismiss; modalScope.$on('$destroy', function() {//modalScope监听$destroy if (!modalScope.$$uibDestructionScheduled) { modalScope.$dismiss('$uibUnscheduledDestruction'); } }); var ctrlInstance, ctrlLocals = {}; var resolveIter = 1; //controllers if (modalOptions.controller) { ctrlLocals.$scope = modalScope; ctrlLocals.$uibModalInstance = modalInstance; Object.defineProperty(ctrlLocals, '$modalInstance', {//设置属性Object.defineProperty(对象名,属性名,属性) get: function() { if (!$modalSuppressWarning) { $log.warn('$modalInstance is now deprecated. Use $uibModalInstance instead.'); } return modalInstance; } }); angular.forEach(modalOptions.resolve, function(value, key) { ctrlLocals[key] = tplAndVars[resolveIter++];//把resolve植入ctrlLocals }); ctrlInstance = $controller(modalOptions.controller, ctrlLocals);//加载控制器并传入一个作用域$controller if (modalOptions.controllerAs) { if (modalOptions.bindToController) { angular.extend(ctrlInstance, modalScope);//把modalScope扩展到ctrlInstance实例上 } modalScope[modalOptions.controllerAs] = ctrlInstance; } } $modalStack.open(modalInstance, {//生成模态框 scope: modalScope, deferred: modalResultDeferred, renderDeferred: modalRenderDeferred, content: tplAndVars[0], animation: modalOptions.animation, backdrop: modalOptions.backdrop, keyboard: modalOptions.keyboard, backdropClass: modalOptions.backdropClass, windowTopClass: modalOptions.windowTopClass, windowClass: modalOptions.windowClass, windowTemplateUrl: modalOptions.windowTemplateUrl, size: modalOptions.size, openedClass: modalOptions.openedClass }); modalOpenedDeferred.resolve(true); }, function resolveError(reason) { modalOpenedDeferred.reject(reason); modalResultDeferred.reject(reason); }) .finally(function() { if (promiseChain === samePromise) { promiseChain = null; } }); return modalInstance; }; return $modal; } ] }; return $modalProvider; }); /* deprecated modal below */ angular.module('ui.bootstrap.modal') .value('$modalSuppressWarning', false) /** * A helper directive for the $modal service. It creates a backdrop element. */ .directive('modalBackdrop', [ '$animate', '$injector', '$modalStack', '$log', '$modalSuppressWarning', function($animate , $injector, $modalStack, $log, $modalSuppressWarning) { var $animateCss = null; if ($injector.has('$animateCss')) { $animateCss = $injector.get('$animateCss'); } return { replace: true, templateUrl: 'template/modal/backdrop.html', compile: function(tElement, tAttrs) { tElement.addClass(tAttrs.backdropClass); return linkFn; } }; function linkFn(scope, element, attrs) { if (!$modalSuppressWarning) { $log.warn('modal-backdrop is now deprecated. Use uib-modal-backdrop instead.'); } element.addClass('modal-backdrop'); if (attrs.modalInClass) { if ($animateCss) { $animateCss(element, { addClass: attrs.modalInClass }).start(); } else { $animate.addClass(element, attrs.modalInClass); } scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { var done = setIsAsync(); if ($animateCss) { $animateCss(element, { removeClass: attrs.modalInClass }).start().then(done); } else { $animate.removeClass(element, attrs.modalInClass).then(done); } }); } } }]) .directive('modalWindow', [ '$modalStack', '$q', '$animate', '$injector', '$log', '$modalSuppressWarning', function($modalStack , $q , $animate, $injector, $log, $modalSuppressWarning) { var $animateCss = null; if ($injector.has('$animateCss')) { $animateCss = $injector.get('$animateCss'); } return { scope: { index: '@' }, replace: true, transclude: true, templateUrl: function(tElement, tAttrs) { return tAttrs.templateUrl || 'template/modal/window.html'; }, link: function(scope, element, attrs) { if (!$modalSuppressWarning) { $log.warn('modal-window is now deprecated. Use uib-modal-window instead.'); } element.addClass(attrs.windowClass || ''); element.addClass(attrs.windowTopClass || ''); scope.size = attrs.size; scope.close = function(evt) { var modal = $modalStack.getTop(); if (modal && modal.value.backdrop && modal.value.backdrop !== 'static' && (evt.target === evt.currentTarget)) { evt.preventDefault(); evt.stopPropagation(); $modalStack.dismiss(modal.key, 'backdrop click'); } }; // moved from template to fix issue #2280 element.on('click', scope.close); // This property is only added to the scope for the purpose of detecting when this directive is rendered. // We can detect that by using this property in the template associated with this directive and then use // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}. scope.$isRendered = true; // Deferred object that will be resolved when this modal is render. var modalRenderDeferObj = $q.defer(); // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready. // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template. attrs.$observe('modalRender', function(value) { if (value == 'true') { modalRenderDeferObj.resolve(); } }); modalRenderDeferObj.promise.then(function() { var animationPromise = null; if (attrs.modalInClass) { if ($animateCss) { animationPromise = $animateCss(element, { addClass: attrs.modalInClass }).start(); } else { animationPromise = $animate.addClass(element, attrs.modalInClass); } scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { var done = setIsAsync(); if ($animateCss) { $animateCss(element, { removeClass: attrs.modalInClass }).start().then(done); } else { $animate.removeClass(element, attrs.modalInClass).then(done); } }); } $q.when(animationPromise).then(function() { var inputWithAutofocus = element[0].querySelector('[autofocus]'); /** * Auto-focusing of a freshly-opened modal element causes any child elements * with the autofocus attribute to lose focus. This is an issue on touch * based devices which will show and then hide the onscreen keyboard. * Attempts to refocus the autofocus element via JavaScript will not reopen * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus * the modal element if the modal does not contain an autofocus element. */ if (inputWithAutofocus) { inputWithAutofocus.focus(); } else { element[0].focus(); } }); // Notify {@link $modalStack} that modal is rendered. var modal = $modalStack.getTop(); if (modal) { $modalStack.modalRendered(modal.key); } }); } }; }]) .directive('modalAnimationClass', [ '$log', '$modalSuppressWarning', function ($log, $modalSuppressWarning) { return { compile: function(tElement, tAttrs) { if (!$modalSuppressWarning) { $log.warn('modal-animation-class is now deprecated. Use uib-modal-animation-class instead.'); } if (tAttrs.modalAnimation) { tElement.addClass(tAttrs.modalAnimationClass); } } }; }]) .directive('modalTransclude', [ '$log', '$modalSuppressWarning', function ($log, $modalSuppressWarning) { return { link: function($scope, $element, $attrs, controller, $transclude) { if (!$modalSuppressWarning) { $log.warn('modal-transclude is now deprecated. Use uib-modal-transclude instead.'); } $transclude($scope.$parent, function(clone) { $element.empty(); $element.append(clone); }); } }; }]) .service('$modalStack', [ '$animate', '$timeout', '$document', '$compile', '$rootScope', '$q', '$injector', '$$multiMap', '$$stackedMap', '$uibModalStack', '$log', '$modalSuppressWarning', function($animate , $timeout , $document , $compile , $rootScope , $q, $injector, $$multiMap, $$stackedMap, $uibModalStack, $log, $modalSuppressWarning) { if (!$modalSuppressWarning) { $log.warn('$modalStack is now deprecated. Use $uibModalStack instead.'); } angular.extend(this, $uibModalStack);//把$uibModalStack扩展到$modalStack上 }]) .provider('$modal', ['$uibModalProvider', function($uibModalProvider) { angular.extend(this, $uibModalProvider); this.$get = ['$injector', '$log', '$modalSuppressWarning', function ($injector, $log, $modalSuppressWarning) { if (!$modalSuppressWarning) { $log.warn('$modal is now deprecated. Use instead.'); } return $injector.invoke($uibModalProvider.$get);//通过$uibModal服务的服务提供商来获取get注入到服务中 }]; }]); angular.module('ui.bootstrap.stackedMap', []) /** * A helper, internal data structure that acts as a map but also allows getting / removing * elements in the LIFO order */ .factory('$$stackedMap', function() {//$$stackedMap map数据结构,并且满足先进先出 return { createNew: function() { var stack = [];//数组对象 return { add: function(key, value) {//增加一对key-value值 stack.push({ key: key, value: value }); }, get: function(key) {//获取一对key-value值 for (var i = 0; i < stack.length; i++) { if (key == stack[i].key) { return stack[i]; } } }, keys: function() {//返回所有key的集合 var keys = []; for (var i = 0; i < stack.length; i++) { keys.push(stack[i].key); } return keys; }, top: function() {//返回栈顶的key-value return stack[stack.length - 1]; }, remove: function(key) {//移除对应的key-value var idx = -1; for (var i = 0; i < stack.length; i++) { if (key == stack[i].key) { idx = i; break; } } return stack.splice(idx, 1)[0]; }, removeTop: function() {//移除栈顶 return stack.splice(stack.length - 1, 1)[0]; }, length: function() {//返回长度 return stack.length; } }; } }; }); angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/modal/backdrop.html", "
\n" + ""); }]); angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/modal/window.html", "
\n" + "
\n" + "
\n" + ""); }]); ``` 从源码看,里面好多代码有很大的重复性,大家可以自己看一下,我就不再追溯了。 整个插件的基本思路就是,在插件调用的时候,先通过下面的方式进行参数传递,生成模态的背景和窗体 ``` angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($scope, $uibModal, $log) { $scope.items = ['item1', 'item2', 'item3']; $scope.animationsEnabled = true; $scope.open = function (size) { var modalInstance = $uibModal.open({ animation: $scope.animationsEnabled, templateUrl: 'myModalContent.html', controller: 'ModalInstanceCtrl', size: size, resolve: { items: function () { return $scope.items; } } }); modalInstance.result.then(function (selectedItem) { $scope.selected = selectedItem; }, function () { $log.info('Modal dismissed at: ' + new Date()); }); }; $scope.toggleAnimation = function () { $scope.animationsEnabled = !$scope.animationsEnabled; }; }); ``` 参数传递就是里面$uibModal.open({options})里面的参数,参数传入$uibModal.open后会与其内部默认的参数进行扩展,形成一个新的参数数组,扩展代码是 ``` modalOptions = angular.extend({}, $modalProvider.options, modalOptions); ``` 然后在加上生成的延迟对象,一起加入到$uibModalStack服务中$modalStack.open函数中 ``` $modalStack.open(modalInstance, {/ scope: modalScope, deferred: modalResultDeferred, renderDeferred: modalRenderDeferred, content: tplAndVars[0], animation: modalOptions.animation, backdrop: modalOptions.backdrop, keyboard: modalOptions.keyboard, backdropClass: modalOptions.backdropClass, windowTopClass: modalOptions.windowTopClass, windowClass: modalOptions.windowClass, windowTemplateUrl: modalOptions.windowTemplateUrl, size: modalOptions.size, openedClass: modalOptions.openedClass }); ``` 这样做的目的是,因为在$uibModalStack服务中可以把这些变量添加到一个map结构数组中,因为在$uibModalStack中实例化了map数据结构,可以调用其add方法来添加 ``` $modalStack.open = function(modalInstance, modal) { //创建一个模态框 var modalOpener = $document[0].activeElement, //获取有焦点的元素 modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS; //获取类名 toggleTopWindowClass(false);//栈顶不触发 openedWindows.add(modalInstance, {//openedWindows注入keyvalue形式默认参数 deferred: modal.deferred, renderDeferred: modal.renderDeferred, modalScope: modal.scope, backdrop: modal.backdrop, keyboard: modal.keyboard, openedClass: modal.openedClass, windowTopClass: modal.windowTopClass }); openedClasses.put(modalBodyClass, modalInstance); //openedClasses注入模态的类,和刚才注入的模态实例 var body = $document.find('body').eq(0), //获取body对象 currBackdropIndex = backdropIndex(); //获取有backdrop的属性值的索引 if (currBackdropIndex >= 0 && !backdropDomEl) {//如果存在 backdropScope = $rootScope.$new(true);//生成新的scope backdropScope.index = currBackdropIndex; var angularBackgroundDomEl = angular.element('
');//生成背景angular元素 angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass);//添加backdrop-class属性值,为modal.backdropClass if (modal.animation) {//如果modal传入了animation angularBackgroundDomEl.attr('modal-animation', 'true');//设置属性modal-animation为true } backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope);//$compile编译angulardom元素,并且传入当前scope,生成dom元素 body.append(backdropDomEl); } var angularDomEl = angular.element('
');//模态窗 angularDomEl.attr({//设置属性 'template-url': modal.windowTemplateUrl, 'window-class': modal.windowClass, 'window-top-class': modal.windowTopClass, 'size': modal.size, 'index': openedWindows.length() - 1, 'animate': 'animate' }).html(modal.content); if (modal.animation) { angularDomEl.attr('modal-animation', 'true'); } var modalDomEl = $compile(angularDomEl)(modal.scope); openedWindows.top().value.modalDomEl = modalDomEl;//获取openedWindows栈顶值的dom元素 openedWindows.top().value.modalOpener = modalOpener; body.append(modalDomEl);//添加模态框 body.addClass(modalBodyClass);//添加类名 $modalStack.clearFocusListCache(); }; ``` 可以看到内部有openedWindows.add方法可以把参数都植入到这个数据结构中,方便进行调用,在调用open的时候,动态生成了背景和模态窗口。 然后就是指令,把$uibModalStack服务传入指令uibModalWindow中,这样就可以通过element和attr来调用templateUrl中的属性来添加和消除动画

你可能感兴趣的:(angularjs中UI插件ui-bootstrap中modal模态弹出源码分析)