欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

AngularJS自定义插件实现网站用户引导功能示例

程序员文章站 2023-11-14 11:36:10
本文实例讲述了angularjs自定义插件实现网站用户引导功能。分享给大家供大家参考,具体如下: 最近由于项目进行了较大的改版,为了让用户能够适应这次新的改版,因此在系统...

本文实例讲述了angularjs自定义插件实现网站用户引导功能。分享给大家供大家参考,具体如下:

最近由于项目进行了较大的改版,为了让用户能够适应这次新的改版,因此在系统中引入了“用户引导”功能,对于初次进入系统的用户一些简单的使用培训training。对于大多数网站来说,这是一个很常见的功能。所以在开发这个任务之前,博主尝试将其抽象化,独立于现有系统的业务逻辑,将其封装为一个通用的插件,使得代码更容易扩展和维护。

无图无真相,先上图:

AngularJS自定义插件实现网站用户引导功能示例

关于这款trainning插件的使用很简单,它采用了类似angular路由一样的配置,只需要简单的配置其每一步training信息。

title:step的标题信息;
template/templateurl: step的内容模板信息。这类可以配置html元素,或者是模板的url地址,同时templateurl也支持angular route一样的function语法;
controller: step的控制器配置;在controller中可注入如下参数:当前step – currentstep、所有step的配置 – trainnings、当前step的配置 – currenttrainning、以及下一步的操作回调 – trainninginstance(其中nextstep:为下一步的回调,cancel为取消用户引导回调);
controlleras: controller的别名;
resolve:在controller初始化前的数据配置,同angular路由中的resolve;
locals:本地变量,和resolve相似,可以传递到controller中。区别之处在于它不支持function调用,对于常量书写会比resolve更方便;
placement: step容器上三角箭头的显示方位,
position: step容器的具体显示位置,这是一个绝对坐标;可以传递{left: 100, top: 100}的绝对坐标,也可以是#steppanelhost配置相对于此元素的placement位置。同时它也支持自定义function和注入angular的其他组件语法。并且默认可注入:所有step配置 – trainnings,当前步骤 – step,当前step的配置 – currenttrainning,以及step容器节点 – steppanel;
backdrop:是否需要显示遮罩层,默认显示,除非显示声明为false配置,则不会显示遮罩层;
stepclass:每一个step容器的样式信息;
backdropclass: 每一个遮罩层的样式信息。

了解了这些配置后,并根据特定需求定制化整个用户引导的配置信息后,我们就可以使用trainningservice的trainning方法来在特定实际启动用户引导,传入参数为每一步step的配置信息。并可以注册其done或者cancel事件:

trainningservice.trainning(trainningcourses.courses)
.done(function() {
 vm.isdone = true;
});

下面是一个演示的配置信息:

.constant('trainningcourses', {
  courses: [{
   title: 'step 1:',
   templateurl: 'trainning-content.html',
   controller: 'steppanelcontroller',
   controlleras: 'steppanel',
   placement: 'left',
   position: '#blogcontrol'
  },{
   title: 'step 3:',
   templateurl: 'trainning-content.html',
   controller: 'steppanelcontroller',
   controlleras: 'steppanel',
   placement: 'top',
   position: {
    top: 200,
    left: 100
   }
  },
   ...
  {
   stepclass: 'last-step',
   backdropclass: 'last-backdrop',
   templateurl: 'trainning-content-done.html',
   controller: 'steppanelcontroller',
   controlleras: 'steppanel',
   position: ['$window', 'steppanel', function($window, steppanel) {
    // 自定义函数,使其屏幕居中
    var win = angular.element($window);
    return {
     top: (win.height() - steppanel.height()) / 2,
     left: (win.width() - steppanel.width()) / 2
    }
   }]
  }]
})

本文插件源码和演示效果唯一codepen上,效果图如下:

AngularJS自定义插件实现网站用户引导功能示例

在trainning插件的源码设计中,包含如下几个要点:

提供service api。因为关于trainning这个插件,它是一个全局的插件,正好在angular中所有的service也是单例的,所以将用户引导逻辑封装到angular的service中是一个不错的设计。但对于trainning的每一步展示内容信息则是dom操作,在angular的处理中它不该存在于service中,最佳的方式是应该把他封装到directive中。所以这里采用directive的定义,并在service中compile,然后append到body中。

对于每一个这类独立的插件应该封装一个独立的scope,这样便于在后续的销毁,以及不会与现有的scope变量的冲突。

$q对延时触发的结果包装。对于像该trainning插件或者modal这类操作结果采用promise的封装,是一个不错的选择。它取代了回调参数的复杂性,并以流畅api的方式展现,更利于代码的可读性。同时也能与其他angular service统一返回api。

对于controller、controlleras、resolve、template、templateurl这类类似路由的处理代码,完全可以移到到你的同类插件中去。它们可以增加插件的更多定制化扩展。关于这部分代码的解释,博主将会在后续文章中为大家推送。

利用$injector.invoke动态注入和调用angular service,这样既能获取angular其他service注入的扩展性,也能获取到函数的动态性。如上例中的屏幕居中的自定义扩展方式。

这类设计要点,同样可以运用到想modal、alert、overload这类全局插件中。有兴趣的读者,你可以在博主的codepen笔记中阅读这段代码http://codepen.io/greengerong/pen/pjwxqw#0

上述代码摘录如下:

html:

<div ng-app="com.github.greengerong" ng-controller="democontroller as demo">
 <div class="alert alert-success fade in" ng-if='demo.isdone'>
  <strong>all trainning setps done!</strong>
 </div>
 <button id="startagain" class="btn btn-primary start-again" ng-click="demo.trainning()">you can start trainning again</button>
 <div class="blog">
  <form class="form-inline">
   <div class="form-group">
    <label class="sr-only" for="exampleinputamount">blog :</label>
    <div class="input-group">
     <input id="blogcontrol" type="text" class="form-control" />
    </div>
   </div>
   <button id="submitblog" class="btn btn-primary" ng-click="demo.backdrop()">public blog</button>
  </form>
 </div>
 <script type="text/ng-template" id="modal-backdrop.html">
  <div class="modal-backdrop fade in {{backdropclass}}" ng-style="{'z-index': zindex || 1040}"></div>
 </script>
 <script type="text/ng-template" id="trainning-step.html">
  <div class="trainning-step">
   <div style="display:block; z-index:1080;left:-1000px;top:-1000px;" ng-style="positionstyle" class="step-panel {{currenttrainning.placement}} fade popover in {{currenttrainning.stepclass}}" ng-show="!isprogressing">
    <div class="arrow"></div>
    <div class="popover-inner">
     <h3 class="popover-title" ng-if='currenttrainning.title'>{{currenttrainning.title}}</h3>
     <div class="popover-content">
     </div>
    </div>
   </div>
   <ui-backdrop backdrop-class="currenttrainning.backdropclass" ng-if="currenttrainning.backdrop !== false"></ui-backdrop>
  </div>
 </script>
 <script type="text/ng-template" id="trainning-content.html">
  <div class="step-content">
   <div>{{ steppanel.texts[steppanel.currentstep - 1]}}</div>
   <div class="next-step">
    <ul class="step-progressing">
    <li data-ng-repeat="item in steppanel.trainnings.length | range"
     data-ng-class="{active: steppanel.currentstep == item}">
    </li>
   </ul>
    <button type="button" class="btn btn-link btn-next pull-right" ng-click="steppanel.trainninginstance.nextstep({$event:$event, step:step});">next</button>
   </div>
  </div>
 </script>
 <script type="text/ng-template" id="trainning-content-done.html">
  <div class="step-content">
    <div>
 {{ steppanel.texts[steppanel.currentstep - 1]}}
   </div>
   <div class="next-step">
    <ul class="step-progressing">
    <li data-ng-repeat="item in steppanel.trainnings.length | range"
     data-ng-class="{active: steppanel.currentstep == item}">
    </li>
   </ul>
    <button type="button" class="btn btn-link pull-right" ng-click="nextstep({$event:$event, step:step});">got it</button>
   </div>
  </div>
 </script>
</div>

css:

.last-step{
 /* background-color: blue;*/
}
.last-backdrop{
 background-color: #ffffff;
}
.blog{
 position: absolute;
 left: 300px;
 top: 100px;
}
.start-again{
 position: absolute;
 left: 400px;
 top: 250px;
}
.next-step {
 .step-progressing {
  margin: 10px 0px;
  display: inline-block;
  li {
  margin-right: 5px;
  border: 1px solid #fff;
  background-color: #6e6e6e;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  display: inline-block;
  &.active {
   background-color: #0000ff;
  }
  }
 }
}

js:

//please set step content to fixed width when complex content or dynamic loading.
angular.module('com.github.greengerong.backdrop', [])
 .directive('uibackdrop', ['$document', function($document) {
  return {
   restrict: 'ea',
   replace: true,
   templateurl: 'modal-backdrop.html',
   scope: {
    backdropclass: '=',
    zindex: '='
   }
   /* ,link: function(){
    $document.bind('keydown', function(evt){
     evt.preventdefault();
     evt.stoppropagation();
    });
    scope.$on('$destroy', function(){
     $document.unbind('keydown');
    });
    }*/
  };
 }])
 .service('modalbackdropservice', ['$rootscope', '$compile', '$document', function($rootscope, $compile, $document) {
  var self = this;
  self.backdrop = function(backdropclass, zindex) {
   var $backdrop = angular.element('<ui-backdrop></ui-backdrop>')
    .attr({
     'backdrop-class': 'backdropclass',
     'z-index': 'zindex'
    });
   var backdropscope = $rootscope.$new(true);
   backdropscope.backdropclass = backdropclass;
   backdropscope.zindex = zindex;
   $document.find('body').append($compile($backdrop)(backdropscope));
   return function() {
    $backdrop.remove();
    backdropscope.$destroy();
   };
  };
 }]);
angular.module('com.github.greengerong.trainning', ['com.github.greengerong.backdrop', 'ui.bootstrap'])
 .directive('trainningstep', ['$timeout', '$http', '$templatecache', '$compile', '$position', '$injector', '$window', '$q', '$controller', function($timeout, $http, $templatecache, $compile, $position, $injector, $window, $q, $controller) {
  return {
   restrict: 'ea',
   replace: true,
   templateurl: 'trainning-step.html',
   scope: {
    step: '=',
    trainnings: '=',
    nextstep: '&',
    cancel: '&'
   },
   link: function(steppanelscope, elm) {
    var steppanel = elm.find('.step-panel');
    steppanelscope.$watch('step', function(step) {
     if (!step) {
      return;
     }
     steppanelscope.currenttrainning = steppanelscope.trainnings[steppanelscope.step - 1];
     var contentscope = steppanelscope.$new(false);
     loadstepcontent(contentscope, {
      'currentstep': steppanelscope.step,
      'trainnings': steppanelscope.trainnings,
      'currenttrainning': steppanelscope.currenttrainning,
      'trainninginstance': {
       'nextstep': steppanelscope.nextstep,
       'cancel': steppanelscope.cancel
      }
     }).then(function(tplandvars) {
      elm.find('.popover-content').html($compile(tplandvars[0])(contentscope));
     }).then(function() {
      var pos = steppanelscope.currenttrainning.position;
      adjustposition(steppanelscope, steppanel, pos);
     });
    });
    angular.element($window).bind('resize', function() {
     adjustposition(steppanelscope, steppanel, steppanelscope.currenttrainning.position);
    });
    steppanelscope.$on('$destroy', function() {
     angular.element($window).unbind('resize');
    });
    function getpositiononelement(stepscope, setppos) {
     return $position.positionelements(angular.element(setppos), steppanel, stepscope.currenttrainning.placement, true);
    }
    function positiononelement(stepscope, setppos) {
     var targetpos = angular.isstring(setppos) ? getpositiononelement(stepscope, setppos) : setppos;
     var positionstyle = stepscope.currenttrainning || {};
     positionstyle.top = targetpos.top + 'px';
     positionstyle.left = targetpos.left + 'px';
     stepscope.positionstyle = positionstyle;
    }
    function adjustposition(stepscope, steppanel, pos) {
     if (!pos) {
      return;
     }
     var setppos = angular.isfunction(pos) || angular.isarray(pos) ? $injector.invoke(pos, null, {
      trainnings: stepscope.trainnings,
      step: stepscope.setp,
      currenttrainning: stepscope.currenttrainning,
      steppanel: steppanel
     }) : pos;
     //get postion should wait for content setup
     $timeout(function() {
      positiononelement(stepscope, setppos);
     });
    }
    function loadstepcontent(contentscope, ctrllocals) {
     var trainningoptions = contentscope.currenttrainning,
      gettemplatepromise = function(options) {
       return options.template ? $q.when(options.template) :
        $http.get(angular.isfunction(options.templateurl) ? (options.templateurl)() : options.templateurl, {
         cache: $templatecache
        }).then(function(result) {
         return result.data;
        });
      },
      getresolvepromises = function(resolves) {
       var promisesarr = [];
       angular.foreach(resolves, function(value) {
        if (angular.isfunction(value) || angular.isarray(value)) {
         promisesarr.push($q.when($injector.invoke(value)));
        }
       });
       return promisesarr;
      },
      controllerloader = function(trainningoptions, trainningscope, ctrllocals, tplandvars) {
       var ctrlinstance;
       ctrllocals = angular.extend({}, ctrllocals || {}, trainningoptions.locals || {});
       var resolveiter = 1;
       if (trainningoptions.controller) {
        ctrllocals.$scope = trainningscope;
        angular.foreach(trainningoptions.resolve, function(value, key) {
         ctrllocals[key] = tplandvars[resolveiter++];
        });
        ctrlinstance = $controller(trainningoptions.controller, ctrllocals);
        if (trainningoptions.controlleras) {
         trainningscope[trainningoptions.controlleras] = ctrlinstance;
        }
       }
       return trainningscope;
      };
     var templateandresolvepromise = $q.all([gettemplatepromise(trainningoptions)].concat(getresolvepromises(trainningoptions.resolve || {})));
     return templateandresolvepromise.then(function resolvesuccess(tplandvars) {
      controllerloader(trainningoptions, contentscope, ctrllocals, tplandvars);
      return tplandvars;
     });
    }
   }
  };
 }])
 .service('trainningservice', ['$compile', '$rootscope', '$document', '$q', function($compile, $rootscope, $document, $q) {
  var self = this;
  self.trainning = function(trainnings) {
   var trainningscope = $rootscope.$new(true),
    defer = $q.defer(),
    $stepelm = angular.element('<trainning-step></trainning-step>')
    .attr({
     'step': 'step',
     'trainnings': 'trainnings',
     'next-step': 'nextstep($event, step);',
     'cancel': 'cancel($event, step)'
    }),
    destroytrainningpanel = function(){
     if (trainningscope) {
      $stepelm.remove();
      trainningscope.$destroy();
     }
    };
   trainningscope.cancel = function($event, step){
    defer.reject('cancel');
   };
   trainningscope.nextstep = function($event, step) {
    if (trainningscope.step === trainnings.length) {
     destroytrainningpanel();
     return defer.resolve('done');
    }
    trainningscope.step++;
   };
   trainningscope.trainnings = trainnings;
   trainningscope.step = 1;
   $document.find('body').append($compile($stepelm)(trainningscope));
   trainningscope.$on('$locationchangestart', destroytrainningpanel);
   return {
    done: function(func) {
     defer.promise.then(func);
     return this;
    },
    cancel: function(func) {
     defer.promise.then(null, func);
     return this;
    }
   };
  };
 }]);
angular.module('com.github.greengerong', ['com.github.greengerong.trainning'])
.filter('range', [function () {
   return function (len) {
    return _.range(1, len + 1);
 };
 }])
 .controller('steppanelcontroller', ['currentstep', 'trainnings', 'trainninginstance', 'trainnings', function(currentstep, trainnings, trainninginstance, trainnings) {
  var vm = this;
  vm.currentstep = currentstep;
  vm.trainninginstance = trainninginstance;
  vm.trainnings = trainnings;
  vm.texts = ['write your own sort blog.', 'click button to public your blog.', 'view your blog info on there.', 'click this button, you can restart this trainning when .', 'all trainnings done!'];
  return vm;
 }])
 .constant('trainningcourses', {
  courses: [{
   title: 'step 1:',
   templateurl: 'trainning-content.html',
   controller: 'steppanelcontroller',
   controlleras: 'steppanel',
   placement: 'left',
   position: '#blogcontrol'
  }, {
   title: 'step 2:',
   templateurl: 'trainning-content.html',
   controller: 'steppanelcontroller',
   controlleras: 'steppanel',
   placement: 'right',
   backdrop: false,
   position: '#submitblog'
  }, {
   title: 'step 3:',
   templateurl: 'trainning-content.html',
   controller: 'steppanelcontroller',
   controlleras: 'steppanel',
   placement: 'top',
   position: {
    top: 200,
    left: 100
   }
  }, {
   title: 'step 4:',
   templateurl: 'trainning-content.html',
   controller: 'steppanelcontroller',
   controlleras: 'steppanel',
   placement: 'bottom',
   position: '#startagain'
  }, {
   stepclass: 'last-step',
   backdropclass: 'last-backdrop',
   templateurl: 'trainning-content-done.html',
   controller: 'steppanelcontroller',
   controlleras: 'steppanel',
   position: ['$window', 'steppanel', function($window, steppanel) {
    var win = angular.element($window);
    return {
     top: (win.height() - steppanel.height()) / 2,
     left: (win.width() - steppanel.width()) / 2
    }
   }]
  }]
 })
 .controller('democontroller', ['trainningservice', 'trainningcourses', 'modalbackdropservice', function(trainningservice, trainningcourses, modalbackdropservice) {
  var vm = this;
  vm.trainning = function() {
   //call this service should wait your really document ready event.
   trainningservice.trainning(trainningcourses.courses)
    .done(function() {
     vm.isdone = true;
    });
  };
  var backdropinstance = angular.noop;
  vm.backdrop = function() {
   modalbackdropservice.backdrop();
  };
  vm.trainning();
  return vm;
 }]);

希望本文所述对大家angularjs程序设计有所帮助。