星级评分功能插件的实现原理:
原理分析
HTML{
ul.rating#rating>li.rating-item*5
}
CSS {
两张背景图片(点亮的星星/灰色的星星)
}
JS行为 {
遍历所有的星星,在循环中去点亮指定位置的星星,熄灭剩余星星
mouseover事件:{
鼠标移入的时候mouseover(遍历所有的星星颗数){
click事件: 点亮指定星星
}
}
mouseout事件:{
鼠标离开的时候mouseoout(遍历所有的星星颗数){
熄灭剩余星星
}
}
}
代码实现:
<script>
var num =2, //初始化默认点亮星星颗数
$rating = $('#rating'),
$item = $rating.find(".rating-item");
//点亮星星
var lightOn = function (num) {
$item.each(function (index) {
if(index < num) {
$(this).css('background-position', '-46px -10px'); //亮星星
}else {
$(this).css('background-position', '-10px -10px'); //灰星星
}
});
};
lightOn(num); //初始化函数
//事件绑定
$item.on('mouseover', function () {//鼠标移入时
lightOn($(this).index() + 1); //点亮当前指定的星星颗数
}).on('click', function () {//点击鼠标
num = $(this).index() + 1; //获取当前星星的索引值
});
$rating.on('mouseout', function () {//鼠标移出时 要在父容器上绑定mouseout
lightOn(num); //因为上一步里num值已经改变
});
</script>
但是,以上这段代码有问题:
1. 暴露的全局变量太多,不利于代码拓展
解决办法1:独立命名空间。
解决办法2: 立即执行的匿名函数的写法(function(){})(), 使得我们的变量变成局部作用域。
2. 事件绑定的写法是为每一颗星星都绑定了每一次事件,这样会造成事件浪费。
3. 事件无法复用。
针对以上发现的问题,对JS代码进行改进:
<body>
<ul class="rating" id="rating">
<li class="rating-item" title="极差"></li>
<li class="rating-item" title="很差"></li>
<li class="rating-item" title="一般"></li>
<li class="rating-item" title="好"></li>
<li class="rating-item" title="极好"></li>
</ul>
<ul class="rating" id="rating1">
<li class="rating-item" title="极差"></li>
<li class="rating-item" title="很差"></li>
<li class="rating-item" title="一般"></li>
<li class="rating-item" title="好"></li>
<li class="rating-item" title="极好"></li>
</ul>
<ul class="rating" id="rating2">
<li class="rating-item" title="极差"></li>
<li class="rating-item" title="很差"></li>
<li class="rating-item" title="一般"></li>
<li class="rating-item" title="好"></li>
<li class="rating-item" title="极好"></li>
</ul>
</body>
<style>
body, ul , li {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
.rating {
width: 130px;
height: 26px;
margin: 100px auto;
}
.rating-item {
float: left;
width: 26px;
height: 26px;
background: url("img/css_sprites.png") no-repeat;
cursor: pointer;
}
</style>
<script>
<!-- 闭包/立即执行函数 -->
var rating = (function () {
//点亮星星
var lightOn = function ($item, num) {
$item.each(function (index) {
if(index < num) {
$(this).css('background-position', '-46px -10px'); //亮星星
}else {
$(this).css('background-position', '-10px -10px'); //灰星星
}
});
};
//让函数可复用
var init = function (el, num) {
var $rating = $(el), //可复用:将这里变成通过外界传递进来的参数
$item = $rating.find(".rating-item");
lightOn($item, num); //初始化函数
//事件委托
$rating.on('mouseover', '.rating-item', function () {//鼠标移入时
lightOn($item, $(this).index() + 1); //点亮当前指定的星星颗数
}).on('click', '.rating-item', function () {//点击鼠标
num = $(this).index() + 1; //获取当前星星的索引值
}).on('mouseout', function () {//鼠标移出时
lightOn($item, num); //因为上一步里num值已经改变
});
};
//生成 jQuery 插件
$.fn.extend({
rating: function (num) {
return this.each(function () {
init(this, num);
})
}
});
return {//返回一个对象
init: init //对象de方法
};
})();
//复用
rating.init('#rating1', 1);
rating.init('#rating2', 3);
//调用jQuery 插件
$('#rating').rating(2);
</script>
以为到此就算圆满的话,那就太幼稚了,试想一下,一两个月后,产品经理会告诉你:"我们需要增加新功能,比如用户可以选中半颗星...."。
好吧,那只能继续改改改......
这里就有引出一个设计模式,那就是【开放封闭原则】:即对拓展是开放的,而对修改是封闭的:
改写JS:
<!-- 模板方法模式: 点亮整颗星 -->
<script>
var rating = (function () {
//点亮整颗星
var LightEntire = function (el, options) {
this.$el = $(el);
this.$item = this. $el.find('.rating-item');
this.opts = options;
};
LightEntire.prototype.init = function () {
this.lightOn(this.opts.num);
if (!this.opts.readOnly){
this.bindEvent();
}
};
LightEntire.prototype.lightOn = function (num) {
num = parseInt(num);
this.$item.each(function (index) {
if (index < num) {
$(this).css('background-position', '-46px -10px'); //亮星星
} else {
$(this).css('background-position', '-10px -10px'); //灰星星
}
});
};
LightEntire.prototype.bindEvent = function () {
var self = this,
itemLength = self.$item.length;
self.$el.on('mouseover', '.rating-item', function () {
var num = $(this).index() + 1;
self.lightOn(num);
(typeof self.opts.select === 'function') &&
self.opts.select.call(self.opts.num, itemLength); //判断/改变this指向
//触发事件
self.$el.trigger('select', [self.opts.num, itemLength]);
}).on('click', '.rating-item', function () {
self.opts.num = $(this).index() + 1;
(typeof self.opts.chosen === 'function') &&
self.opts.chosen.call(self.opts.num, itemLength); //判断/改变this指向
//触发事件
self.$el.trigger('chosen', [self.opts.num, itemLength]);
}).on('mouseout', function () {
self.lightOn(self.opts.num);
});
};
//默认参数
var defaults = {
num: 0,
readOnly: false,
select: function () {
},
chosen: function () {
}
};
//初始化
var init = function (el, options) {
options = $.extend({}, defaults, options);
new LightEntire(el, options).init();
};
return {
init: init
};
})();
rating.init('#rating', {
num: 1,
select: function (num, total) {
console.log(this);
console.log(num + '/' + total); //打印当前第几颗/总共多少颗星
}
});
$('#rating').on('select', function (e, num , total) {
console.log(num + '/' + total);
}).on('chosen', function (e, num , total) {
console.log(num + '/' + total);
})
</script>
代码拓展:点亮半颗星功能
原理分析:
利用mousemove
e.pageX
$().offset().left
e.pageX - $().offset().left
$().width()/2
<!-- 模板方法模式 支持 点亮半颗星 -->
<script>
var rating = (function () {
//点亮整颗星
var LightEntire = function (el, options) {
this.$el = $(el);
this.$item = this. $el.find('.rating-item');
this.opts = options;
};
LightEntire.prototype.init = function () {
this.lightOn(this.opts.num);
if (!this.opts.readOnly){
this.bindEvent();
}
};
LightEntire.prototype.lightOn = function (num) {
num = parseInt(num);
this.$item.each(function (index) {
if (index < num) {
$(this).css('background-position', '-46px -10px'); //亮星星
} else {
$(this).css('background-position', '-10px -10px'); //灰星星
}
});
};
LightEntire.prototype.bindEvent = function () {
var self = this,
itemLength = self.$item.length;
self.$el.on('mouseover', '.rating-item', function () {
var num = $(this).index() + 1;
self.lightOn(num);
(typeof self.opts.select === 'function') &&
self.opts.select.call(self.opts.num, itemLength); //判断/改变this指向
//触发事件
self.$el.trigger('select', [self.opts.num, itemLength]);
}).on('click', '.rating-item', function () {
self.opts.num = $(this).index() + 1;
(typeof self.opts.chosen === 'function') &&
self.opts.chosen.call(self.opts.num, itemLength); //判断/改变this指向
//触发事件
self.$el.trigger('chosen', [self.opts.num, itemLength]);
}).on('mouseout', function () {
self.lightOn(self.opts.num);
});
};
//点亮半颗星星
var LightHalf = function (el, options) {
this.$el = $(el);
this.$item = this. $el.find('.rating-item');
this.opts = options;
this.add = 1;
};
LightHalf.prototype.init = function () {
this.lightOn(this.opts.num);
if (!this.opts.readOnly){
this.bindEvent();
}
};
LightHalf.prototype.lightOn = function (num) {
var count = parseInt(num),
isHalf = count !== num; //判断是不是小数
this.$item.each(function (index) {
if (index < count) {
$(this).css('background-position', '-46px -10px'); //亮星星
} else {
$(this).css('background-position', '-10px -10px'); //灰星星
}
});
if(isHalf) {
this.$item.eq(count).css('background-position', '-10px -46px');
}
};
LightHalf.prototype.bindEvent = function () {
var self = this,
itemLength = self.$item.length;
self.$el.on('mousemove', '.rating-item', function (e) {
var $this = $(this),
num = 0;
if (e.pageX - $this.offset().left < $this.width() / 2) {//判断是否半颗星
self.add = 0.5;
} else {//整颗星
self.add = 1;
}
num = $this.index() + self.add;
self.lightOn(num);
(typeof self.opts.select === 'function') &&
self.opts.select.call(self.opts.num, itemLength); //判断/改变this指向
//触发事件
self.$el.trigger('select', [self.opts.num, itemLength]);
}).on('click', '.rating-item', function () {
self.opts.num = $(this).index() + self.add;
(typeof self.opts.chosen === 'function') &&
self.opts.chosen.call(self.opts.num, itemLength); //判断/改变this指向
//触发事件
self.$el.trigger('chosen', [self.opts.num, itemLength]);
}).on('mouseout', function () {
self.lightOn(self.opts.num);
});
};
//默认参数
var defaults = {
mode: 'LightEntire',
num: 0,
readOnly: false,
select: function () {},
chosen: function () {}
};
var mode = {
'LightEntire': LightEntire,
'LightHalf': LightHalf
};
//初始化
var init = function (el, options) {
options = $.extend({}, defaults, options);
if (!mode[options.mode]){
options.mode = 'LightEntire';
};
// new LightEntire(el, options).init();
// new LightHalf(el, options).init();
new mode[options.mode](el, options).init();
};
return {
init: init
};
})();
rating.init('#rating', {
mode: 'LightHalf',
num: 2.5,
select: function (num, total) {
console.log(this);
console.log(num + '/' + total); //打印当前第几颗/总共多少颗星
}
});
</script>
最后,抽象出父类:
<script>
var rating = (function () {
//继承
var extend = function (subClass, superClass) {
var F = function () {};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
};
//抽象父类
var Light = function (el, options) {
this.$el = $(el);
this.$item = this. $el.find('.rating-item');
this.opts = options;
this.add = 1;
this.selectEvent = 'mouseover';
};
Light.prototype.init = function () {
this.lightOn(this.opts.num);
if (!this.opts.readOnly){
this.bindEvent();
}
};
Light.prototype.lightOn = function (num) {
num = parseInt(num);
this.$item.each(function (index) {
if (index < num) {
$(this).css('background-position', '-46px -10px'); //亮星星
} else {
$(this).css('background-position', '-10px -10px'); //灰星星
}
});
};
Light.prototype.bindEvent = function () {
var self = this,
itemLength = self.$item.length;
self.$el.on(self.selectEvent, '.rating-item', function (e) {
var $this = $(this),
num = 0;
self.select(e, $this);
num = $(this).index() + self.add;
self.lightOn(num);
(typeof self.opts.select === 'function') &&
self.opts.select.call(self.opts.num, itemLength); //判断/改变this指向
//触发事件
self.$el.trigger('select', [self.opts.num, itemLength]);
}).on('click', '.rating-item', function () {
self.opts.num = $(this).index() + self.add;
(typeof self.opts.chosen === 'function') &&
self.opts.chosen.call(self.opts.num, itemLength); //判断/改变this指向
//触发事件
self.$el.trigger('chosen', [self.opts.num, itemLength]);
}).on('mouseout', function () {
self.lightOn(self.opts.num);
});
};
Light.prototype.select = function () {
throw new Error('子类必须重写方法');
};
//点亮整颗星
var LightEntire = function (el, options) {
Light.call(this, el, options);
this.selectEvent = 'mouseover';
};
extend(LightEntire, Light);
LightEntire.prototype.lightOn = function (num) {
Light.prototype.lightOn.call(this, num);
};
LightEntire.prototype.select = function () {
self.add = 1;
};
//点亮半颗星星
var LightHalf = function (el, options) {
Light.call(this, el, options);
this.selectEvent = 'mousemove';
};
extend(LightHalf, Light);
LightHalf.prototype.lightOn = function (num) {
var count = parseInt(num),
isHalf = count !== num; //判断是不是小数
Light.prototype.lightOn.call(this, count);
if(isHalf) {
this.$item.eq(count).css('background-position', '-10px -46px');
}
};
LightHalf.prototype.select = function (e, $this) {
if (e.pageX - $this.offset().left < $this.width() / 2) {//判断是否半颗星-->
this.add = 0.5;
} else {//整颗星
this.add = 1;
}
}
//默认参数
var defaults = {
mode: 'LightEntire',
num: 0,
readOnly: false,
select: function () {},
chosen: function () {}
};
var mode = {
'LightEntire': LightEntire,
'LightHalf': LightHalf
};
//初始化
var init = function (el, options) {
options = $.extend({}, defaults, options);
if (!mode[options.mode]){
options.mode = 'LightEntire';
};
// new LightEntire(el, options).init();
// new LightHalf(el, options).init();
new mode[options.mode](el, options).init();
};
return {
init: init
};
})();
rating.init('#rating', {
mode: 'LightHalf',
num: 2.5,
select: function (num, total) {
console.log(this);
console.log(num + '/' + total); //打印当前第几颗/总共多少颗星
}
});
</script>
然鹅,问题又来了,正常情况下,用户点选好星星后,选中结果就会发送出去,用户就不能再继续点选其他剩余几颗星星的,
所以,以上代码依然不够满足业务场景,继续修改:
<html>
<ul class="rating" id="rating">
<li class="rating-item" title="极差"></li>
<li class="rating-item" title="很差"></li>
<li class="rating-item" title="一般"></li>
<li class="rating-item" title="好"></li>
<li class="rating-item" title="极好"></li>
</ul>
<ul class="rating" id="rating1">
<li class="rating-item" title="极差"></li>
<li class="rating-item" title="很差"></li>
<li class="rating-item" title="一般"></li>
<li class="rating-item" title="好"></li>
<li class="rating-item" title="极好"></li>
</ul>
</html>
<!-- 功能扩展:选中星星后就不能再继续选择 -->
<script>
var rating = (function () {
//继承
var extend = function (subClass, superClass) {
var F = function () {};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
};
//抽象父类
var Light = function (el, options) {
this.$el = $(el);
this.$item = this. $el.find('.rating-item');
this.opts = options;
this.add = 1;
this.selectEvent = 'mouseover';
};
Light.prototype.init = function () {
this.lightOn(this.opts.num);
if (!this.opts.readOnly){
this.bindEvent();
}
};
Light.prototype.lightOn = function (num) {
num = parseInt(num);
this.$item.each(function (index) {
if (index < num) {
$(this).css('background-position', '-46px -10px'); //亮星星
} else {
$(this).css('background-position', '-10px -10px'); //灰星星
}
});
};
Light.prototype.bindEvent = function () {
var self = this,
itemLength = self.$item.length;
self.$el.on(self.selectEvent, '.rating-item', function (e) {
var $this = $(this),
num = 0;
self.select(e, $this);
num = $(this).index() + self.add;
self.lightOn(num);
(typeof self.opts.select === 'function') &&
self.opts.select.call(self.opts.num, itemLength); //判断/改变this指向
//触发事件
self.$el.trigger('select', [self.opts.num, itemLength]);
}).on('click', '.rating-item', function () {
self.opts.num = $(this).index() + self.add;
(typeof self.opts.chosen === 'function') &&
self.opts.chosen.call(self.opts.num, itemLength); //判断/改变this指向
//触发事件
self.$el.trigger('chosen', [self.opts.num, itemLength]);
}).on('mouseout', function () {
self.lightOn(self.opts.num);
});
};
Light.prototype.select = function () {
throw new Error('子类必须重写方法');
};
Light.prototype.unbindEvent = function () {
this.$el.off();
};
//点亮整颗星
var LightEntire = function (el, options) {
Light.call(this, el, options);
this.selectEvent = 'mouseover';
};
extend(LightEntire, Light);
LightEntire.prototype.lightOn = function (num) {
Light.prototype.lightOn.call(this, num);
};
LightEntire.prototype.select = function () {
self.add = 1;
};
//点亮半颗星星
var LightHalf = function (el, options) {
Light.call(this, el, options);
this.selectEvent = 'mousemove';
};
extend(LightHalf, Light);
LightHalf.prototype.lightOn = function (num) {
var count = parseInt(num),
isHalf = count !== num; //判断是不是小数
Light.prototype.lightOn.call(this, count);
if(isHalf) {
this.$item.eq(count).css('background-position', '-10px -46px');
}
};
LightHalf.prototype.select = function (e, $this) {
if (e.pageX - $this.offset().left < $this.width() / 2) {//判断是否半颗星-->
this.add = 0.5;
} else {//整颗星
this.add = 1;
}
}
//默认参数
var defaults = {
mode: 'LightEntire',
num: 0,
readOnly: false,
select: function () {},
chosen: function () {}
};
var mode = {
'LightEntire': LightEntire,
'LightHalf': LightHalf
};
//初始化
var init = function (el, option) {//option 可支持字符串
var $el = $(el),
rating = $el.data('rating'),
options = $.extend({}, defaults, typeof option === 'object' && option);
if (!mode[options.mode]){
options.mode = 'LightEntire';
};
// new LightEntire(el, options).init();
// new LightHalf(el, options).init();
if(!rating) {
$el.data('rating', (rating = new mode[options.mode](el, options)));
rating.init();
}
if(typeof option === 'string')rating[option]();
};
//jQuery 插件
$.fn.extend({
rating: function (option) {
return this.each(function () {
init(this, option);
});
}
});
return {
init: init
};
})();
//用jQ的方式调用
$('#rating').rating({
mode: 'LightEntire',
num: 2
});
$('#rating1').rating({
mode: 'LightHalf',
num: 3.5
}).on('chosen', function () {
$('#rating1').rating('unbindEvent');
});
// rating.init('#rating', {
// mode: 'LightHalf',
// num: 2.5,
// // select: function (num, total) {
// // console.log(this);
// // console.log(num + '/' + total); //打印当前第几颗/总共多少颗星
// // },
// chosen: function () {
// rating.init('#rating', 'unbindEvent');
// }
// });
</script>