最近UI上经常遇到点击一个按钮,出现一个带指向箭头的面板的情况,如图:
于是就写了个flyout-panel的组件,但此flyout-panel(飞出面板)只是个点击面板,更像tooltip,不是严格意义上的飞出面板(如我博客左侧“分享到...”面板),只是不太会取名字,估且先叫flyout-panel;
先上代码,再讲逻辑:
(function (){
var reqs = [
"Handlebars",
"jquery",
"text!mr-fp/flyout-panel.html", //支持行内样式时需要的模板
"css!mr-fp/flyout-panel.css"//支持外链样式时需要的样式
];
define(reqs ,function (Handlebars,$,fpHTML) {
var FlyOutPanel = function(options){
var styles = {//面板默认样式,参数名顾名思义,标*号为必选参数
"panelClass" : "flyout-panel",
"borderWidth" : "1px",
"borderColor" : "#000",
"bgColor" : "#fff",
"panelWidth" : "100px",//*
"panelHeight" : "100px",
"arrowLeft" : "50%",//*arrowLeft与arrowRight是互斥的(这里有点别扭)
"arrowRight" : "auto",//*当前三角箭头相对于面板的位置是left:50%
"arrowBorderWidth" : "10px",//*三角箭头的边宽,该三角形所在的div元素现为:20px*20px
"contentPadding" : "auto"
};
this.options = {
"id": "",
"styleInline" : true,//默认支持行内样式
"container" : document.body,//default is document.body, a dom element;
"content" : null,//面板中的内容元素,jquery元素
"triggerBtn" : null,//点击显示面板的button,juqery元素,如果未传,则要显式的为相应按钮绑定显示panel事件
"styles" : styles,
"timer" : 1000 // 默认面板显示1秒后隐藏
};
$.extend(true,this.options,options);
this.styles = this.options.styles;
this.hideTimer = null;
this._create();
};
FlyOutPanel.prototype = {
_create : function(){
var tpl;
if(this.options.styleInline){
var source = $(fpHTML).html();
var template = Handlebars.compile(source);
tpl = template(this.styles);
}else{
tpl = ['<div>',
'<div class="fp-arrow">',
'<div class="fp-a1"></div>',
'<div class="fp-a2"></div>',
'</div>',
'<div class="fp-content"></div>',
'</div>'].join('');
}
var _el = this._el = $(tpl).addClass(this.styles.panelClass);
_el.unselectable();
this.containerEl = $(this.options.container).append(_el);
this.arrowEl = _el.find('.fp-arrow');
_el.width(this.styles.panelWidth);
_el.height(this.styles.panelHeight);
this.contentEl = _el.find('.fp-content').append(this.options.content.show());
this.triggerBtn = $(this.options.triggerBtn);
this.bindEvents();
},
setPosition : function(offset){//设置面板的位置以及三角箭头的位置
var styles = this.styles;
var _el = this._el;
var _w= _el.width() + parseInt(styles.borderWidth)*2;
var arrowLeftPos = _w/2;
var arrowLeft = styles.arrowLeft,
arrowRight = styles.arrowRight,
_arrowBorderWidth = parseInt(styles.arrowBorderWidth,"10");
console.log(arrowLeft + arrowRight);
if(!isNaN(parseInt(arrowLeft,10))){
var _lv = parseInt(arrowLeft,10);
if(arrowLeft.indexOf("%")>0){
arrowLeftPos = _w*_lv/100;
}else{
arrowLeftPos = _lv;
}
}
if(!isNaN(parseInt(arrowRight,10))){
var _rv = parseInt(arrowRight,10);
if(arrowRight.indexOf("%")>0){
arrowLeftPos = _w*(100-_rv)/100;
}else{
arrowLeftPos = _w - _rv;
}
}
if(_w - arrowLeftPos - _arrowBorderWidth <0){
arrowLeftPos = _w - _arrowBorderWidth;
this.arrowEl.css("right",_arrowBorderWidth + "px").css("left","auto");
}
_el.css({"top":offset.top + _arrowBorderWidth,"left":offset.left - arrowLeftPos});
_el.show();
this.hideTimer = setTimeout(function(){
_el.hide();
},this.options.timer);
},
bindEvents : function(){
var self = this,
_el = this._el;
_el.bind('mouseleave.hide',function(){
self.hideTimer = setTimeout(function(){
_el.hide();
},self.options.timer);
}).bind('mouseenter',function(){
clearTimeout(self.hideTimer);
});
var btn = this.triggerBtn;
if(btn && btn.length){
btn.bind('click.show',function(){
var pos = calculatePosition(btn);
self.setPosition(pos);
});
}
},
setArrowPositon :function(){
},
};
function calculatePosition($btn){
var offset = $btn.offset(),
top = offset.top + $btn.height(),
left = offset.left + $btn.width() /2;
return {'top' : top,'left' : left};
}
return FlyOutPanel;
});
})();
"Handlebars",
"jquery",
"text!mr-fp/flyout-panel.html", //支持行内样式时需要的模板
"css!mr-fp/flyout-panel.css"//支持外链样式时需要的样式
];
define(reqs ,function (Handlebars,$,fpHTML) {
var FlyOutPanel = function(options){
var styles = {//面板默认样式,参数名顾名思义,标*号为必选参数
"panelClass" : "flyout-panel",
"borderWidth" : "1px",
"borderColor" : "#000",
"bgColor" : "#fff",
"panelWidth" : "100px",//*
"panelHeight" : "100px",
"arrowLeft" : "50%",//*arrowLeft与arrowRight是互斥的(这里有点别扭)
"arrowRight" : "auto",//*当前三角箭头相对于面板的位置是left:50%
"arrowBorderWidth" : "10px",//*三角箭头的边宽,该三角形所在的div元素现为:20px*20px
"contentPadding" : "auto"
};
this.options = {
"id": "",
"styleInline" : true,//默认支持行内样式
"container" : document.body,//default is document.body, a dom element;
"content" : null,//面板中的内容元素,jquery元素
"triggerBtn" : null,//点击显示面板的button,juqery元素,如果未传,则要显式的为相应按钮绑定显示panel事件
"styles" : styles,
"timer" : 1000 // 默认面板显示1秒后隐藏
};
$.extend(true,this.options,options);
this.styles = this.options.styles;
this.hideTimer = null;
this._create();
};
FlyOutPanel.prototype = {
_create : function(){
var tpl;
if(this.options.styleInline){
var source = $(fpHTML).html();
var template = Handlebars.compile(source);
tpl = template(this.styles);
}else{
tpl = ['<div>',
'<div class="fp-arrow">',
'<div class="fp-a1"></div>',
'<div class="fp-a2"></div>',
'</div>',
'<div class="fp-content"></div>',
'</div>'].join('');
}
var _el = this._el = $(tpl).addClass(this.styles.panelClass);
_el.unselectable();
this.containerEl = $(this.options.container).append(_el);
this.arrowEl = _el.find('.fp-arrow');
_el.width(this.styles.panelWidth);
_el.height(this.styles.panelHeight);
this.contentEl = _el.find('.fp-content').append(this.options.content.show());
this.triggerBtn = $(this.options.triggerBtn);
this.bindEvents();
},
setPosition : function(offset){//设置面板的位置以及三角箭头的位置
var styles = this.styles;
var _el = this._el;
var _w= _el.width() + parseInt(styles.borderWidth)*2;
var arrowLeftPos = _w/2;
var arrowLeft = styles.arrowLeft,
arrowRight = styles.arrowRight,
_arrowBorderWidth = parseInt(styles.arrowBorderWidth,"10");
console.log(arrowLeft + arrowRight);
if(!isNaN(parseInt(arrowLeft,10))){
var _lv = parseInt(arrowLeft,10);
if(arrowLeft.indexOf("%")>0){
arrowLeftPos = _w*_lv/100;
}else{
arrowLeftPos = _lv;
}
}
if(!isNaN(parseInt(arrowRight,10))){
var _rv = parseInt(arrowRight,10);
if(arrowRight.indexOf("%")>0){
arrowLeftPos = _w*(100-_rv)/100;
}else{
arrowLeftPos = _w - _rv;
}
}
if(_w - arrowLeftPos - _arrowBorderWidth <0){
arrowLeftPos = _w - _arrowBorderWidth;
this.arrowEl.css("right",_arrowBorderWidth + "px").css("left","auto");
}
_el.css({"top":offset.top + _arrowBorderWidth,"left":offset.left - arrowLeftPos});
_el.show();
this.hideTimer = setTimeout(function(){
_el.hide();
},this.options.timer);
},
bindEvents : function(){
var self = this,
_el = this._el;
_el.bind('mouseleave.hide',function(){
self.hideTimer = setTimeout(function(){
_el.hide();
},self.options.timer);
}).bind('mouseenter',function(){
clearTimeout(self.hideTimer);
});
var btn = this.triggerBtn;
if(btn && btn.length){
btn.bind('click.show',function(){
var pos = calculatePosition(btn);
self.setPosition(pos);
});
}
},
setArrowPositon :function(){
},
};
function calculatePosition($btn){
var offset = $btn.offset(),
top = offset.top + $btn.height(),
left = offset.left + $btn.width() /2;
return {'top' : top,'left' : left};
}
return FlyOutPanel;
});
})();
调用方式:
_createFlyoutPanels : function(){
var self = this;
var _el = this.el;
var $eyePanel = _el.find('.eye-panel'),
$eyeBtn = this._settingEl.find('.eye-btn');
this.eyePanel = new FlyoutPanel({
"styleInline" : false,
'container' : _el,
'content' : $eyePanel,
'triggerBtn' : $eyeBtn,
'styles' : {
'panelWidth' : "160px",
'panelHeight' : "100px",
}
});
},
var self = this;
var _el = this.el;
var $eyePanel = _el.find('.eye-panel'),
$eyeBtn = this._settingEl.find('.eye-btn');
this.eyePanel = new FlyoutPanel({
"styleInline" : false,
'container' : _el,
'content' : $eyePanel,
'triggerBtn' : $eyeBtn,
'styles' : {
'panelWidth' : "160px",
'panelHeight' : "100px",
}
});
},
UI逻辑:
1,动态的创建一个带箭头的面板,将要显示的内容append进面板;
2,绑定面板事件,主要是mouseleave,mouseenter事件
3,绑定按钮事件,主要指点击button,计算面板位置和箭头位置,显示面板;
创建带箭头的面板
箭头可以用图片实现,这里用dom来模拟,模拟方式大致是这样的:http://cssarrowplease.com/,
考虑到主要是为了支持行内样式,所以就用两个div来代替:before,:after伪类生成的dom。
两种方式生成面板的Dom结构:
var tpl;
if(this.options.styleInline){//行内样式Dom结构
var source = $(fpHTML).html();
var template = Handlebars.compile(source);
tpl = template(this.styles);
}else{//外部样式Dom结构
tpl = ['<div>',
'<div class="fp-arrow">',
'<div class="fp-a1"></div>',
'<div class="fp-a2"></div>',
'</div>',
'<div class="fp-content"></div>',
'</div>'].join('');
}
if(this.options.styleInline){//行内样式Dom结构
var source = $(fpHTML).html();
var template = Handlebars.compile(source);
tpl = template(this.styles);
}else{//外部样式Dom结构
tpl = ['<div>',
'<div class="fp-arrow">',
'<div class="fp-a1"></div>',
'<div class="fp-a2"></div>',
'</div>',
'<div class="fp-content"></div>',
'</div>'].join('');
}
行内样式方式的html模板(handlebars template):
<script id="fp-template" type="text/x-handlebars-template">
<div class="{{panelClass}}" style="position:fixed;border:{{borderWidth}} solid {{borderColor}};background-color:{{bgColor}};width:{{panelWidth}};height:{{panelHeight}};display:none;-moz-box-sizing : border-box;box-sizing : border-box;">
<div class="fp-arrow" style="position:absolute;width:0;left:{{arrowLeft}};right:{{arrowRight}};">
<div class="fp-a1" style="position:absolute;border:solid transparent;height:0;width:0;bottom:0;border-bottom-color:{{borderColor}};border-width:{{arrowBorderWidth}};left:50%;margin-left:-{{arrowBorderWidth}}"></div>
<div class="fp-a2" style="position:absolute;border:solid transparent;height:0;width:0;bottom:-{{borderWidth}};border-bottom-color:{{bgColor}};border-width:{{arrowBorderWidth}};left:50%;margin-left:-{{arrowBorderWidth}}"></div>
</div>
<div class="fp-content" style="padding:{{contentPadding}}"></div>
</div>
</script>
<div class="{{panelClass}}" style="position:fixed;border:{{borderWidth}} solid {{borderColor}};background-color:{{bgColor}};width:{{panelWidth}};height:{{panelHeight}};display:none;-moz-box-sizing : border-box;box-sizing : border-box;">
<div class="fp-arrow" style="position:absolute;width:0;left:{{arrowLeft}};right:{{arrowRight}};">
<div class="fp-a1" style="position:absolute;border:solid transparent;height:0;width:0;bottom:0;border-bottom-color:{{borderColor}};border-width:{{arrowBorderWidth}};left:50%;margin-left:-{{arrowBorderWidth}}"></div>
<div class="fp-a2" style="position:absolute;border:solid transparent;height:0;width:0;bottom:-{{borderWidth}};border-bottom-color:{{bgColor}};border-width:{{arrowBorderWidth}};left:50%;margin-left:-{{arrowBorderWidth}}"></div>
</div>
<div class="fp-content" style="padding:{{contentPadding}}"></div>
</div>
</script>
外部样式方式则提供一个flyout-panel.less样式文件来动态生成样式:
@borderColor: #000;
@borderWidth: 5px;
@bgColor : #fff;
@arrowBorderWidth : 10px;
@panelWidth : 100px;
@panelHeight : 100px;
@arrowOffset : 1px;
@arrowBorderWidth : 10px;
@arrowLeft : 20%;
@arrowRight : auto;
@contentPadding : auto;
.flyout-panel{
position:fixed;
border: @borderWidth solid @borderColor;
display:none;
background: @bgColor;
width : @panelWidth;
height: @panelHeight;
.fp-arrow {
position:fixed;
width : 0;
left : @arrowLeft;
right: @arrowRight;
>div{
position:absolute;
border: solid transparent;
height: 0;
width: 0;
bottom:0;
}
.fa-a1{
border-bottom-color : @borderColor;
border-width : @arrowBorderWidth;
left:50%;
margin-left: -@arrowBorderWidth;
}
.fa-a2{
border-bottom-color : @bgColor;
border-width : @arrowBorderWidth;
left:50%;
top:@borderWidth;
margin-left: -@arrowBorderWidth;
}
}
}
@borderWidth: 5px;
@bgColor : #fff;
@arrowBorderWidth : 10px;
@panelWidth : 100px;
@panelHeight : 100px;
@arrowOffset : 1px;
@arrowBorderWidth : 10px;
@arrowLeft : 20%;
@arrowRight : auto;
@contentPadding : auto;
.flyout-panel{
position:fixed;
border: @borderWidth solid @borderColor;
display:none;
background: @bgColor;
width : @panelWidth;
height: @panelHeight;
.fp-arrow {
position:fixed;
width : 0;
left : @arrowLeft;
right: @arrowRight;
>div{
position:absolute;
border: solid transparent;
height: 0;
width: 0;
bottom:0;
}
.fa-a1{
border-bottom-color : @borderColor;
border-width : @arrowBorderWidth;
left:50%;
margin-left: -@arrowBorderWidth;
}
.fa-a2{
border-bottom-color : @bgColor;
border-width : @arrowBorderWidth;
left:50%;
top:@borderWidth;
margin-left: -@arrowBorderWidth;
}
}
}
附上demo:http://flowerszhong.shop.co/flyout-panel/demo.html