前言
氣泡組件在實際工作中非常普遍,無論是網頁中還是app中,比如:
我們這里所謂氣泡組件是指列表型氣泡組件,這里就其dom實現,css實現,js實現做一個討論,最后對一些細節點做一些說明,希望對各位有用
小釵最近初學CSS,這里做一個專題,便於自身CSS提升,文章有不少問題與可優化點,請各位指導
組件分類
單由氣泡組件來說,他仍然屬於“彈出層”類組件,也就是說其會具有這些特性:
① 布局為脫離文檔流
② 可以具有mask蒙版,並且可配置點擊蒙版是否關閉的特性
③ 可選的特性有點擊瀏覽器回退關閉組件以及動畫的顯示與隱藏動畫特性
其中比較不同的是:
① 不是居中定位
② 具有一個箭頭標識,並且可以設置再上或者在下
③ 因為具有箭頭,而且這個箭頭是相對於一個元素的,一般意義上我們任務是相對某個按鈕,所以說具有一個triggerEL
所以單從這里論述來說,我們的組件名為BubbleLayer,其應該繼承與一個通用的Layer
但是,就由Layer來說,其最少會具有以下通用特性:
① 創建——create
② 顯示——show
③ 隱藏——hide
④ 摧毀——destroy
而以上特性並不是Layer組件所特有的,而是所有組件所特有,所以在Layer之上還應該存在一個AbstractView的抽象組件
至此繼承關系便出來了,拋開多余的接口不看,簡單來說是這樣的:
組件dom層面實現
最簡單實現
單從dom實現來說,其實一個簡單的ul便可以完成任務
1 <ul class="cui-bubble-layer" style="position: absolute; top: 110px; left: 220px;"> 2 <li data-index="0" data-flag="c">價格:¥35</li> 3 <li data-index="1" data-flag="c">評分:80</li> 4 <li data-index="2" data-flag="c">級別:5</li> 5 </ul>
當然這里要有相關的css
1 .cui-bubble-layer { 2 background: #f2f2f2; 3 border: #bcbcbc 1px solid; 4 border-radius: 3px 5 }
至此形成的效果是醬紫滴:

1 <!doctype html> 2 <html> 3 <head> 4 <meta charset="utf-8" /> 5 <title>Blade Demo</title> 6 <meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> 7 <meta content="telephone=no" name="format-detection" /> 8 <meta name="apple-mobile-web-app-capable" content="yes" /> 9 <style type="text/css"> 10 body, button, input, select, textarea { font: 400 14px/1.5 Arial, "Lucida Grande" ,Verdana, "Microsoft YaHei" ,hei; } 11 body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input, textarea, p, blockquote, th, td, hr, button, article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { margin: 0; padding: 0; } 12 body { background: #f5f5f5; } 13 ul, ol { list-style: none; } 14 15 .cui-bubble-layer { background: #f2f2f2; border: #bcbcbc 1px solid; border-radius: 3px; } 16 </style> 17 </head> 18 <body> 19 <ul class="cui-bubble-layer" style="position: absolute; top: 110px; left: 220px;"> 20 <li data-index="0" data-flag="c">價格:¥35</li> 21 <li data-index="1" data-flag="c">評分:80</li> 22 <li data-index="2" data-flag="c">級別:5</li> 23 </ul> 24 </body> 25 </html>
這個時候在為其加一個偽類,做點樣式上的調整,便基本實現了,這里用到了偽類的知識點:
cui-bubble-layer:before {
position: absolute; content: ""; width: 10px; height: 10px; -webkit-transform: rotate(45deg);
background: #f2f2f2;
border-top: #bcbcbc 1px solid;
border-left: #bcbcbc 1px solid;
top: -6px; left: 50%; margin-left: -5px; z-index: 1;
}
這里設置了一個絕對定位的矩形框,為其兩個邊框設置了值,然后變形偏斜45度形成小三角,然后大家都知道了

<!doctype html> <html> <head> <meta charset="utf-8" /> <title>Blade Demo</title> <meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta content="telephone=no" name="format-detection" /> <meta name="apple-mobile-web-app-capable" content="yes" /> <style type="text/css"> body, button, input, select, textarea { font: 400 14px/1.5 Arial, "Lucida Grande" ,Verdana, "Microsoft YaHei" ,hei; } body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input, textarea, p, blockquote, th, td, hr, button, article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { margin: 0; padding: 0; } body { background: #f5f5f5; } ul, ol { list-style: none; } .cui-bubble-layer { background: #f2f2f2; border: #bcbcbc 1px solid; border-radius: 3px; } .cui-bubble-layer > li { padding: 5px 10px; } .cui-bubble-layer:before { position: absolute; content: ""; width: 10px; height: 10px; -webkit-transform: rotate(45deg); background: #f2f2f2; border-top: #bcbcbc 1px solid; border-left: #bcbcbc 1px solid; top: -6px; left: 50%; margin-left: -5px; z-index: 1;</style> </head> <body> <ul class="cui-bubble-layer" style="position: absolute; top: 110px; left: 220px;"> <li data-index="0" data-flag="c">價格:¥35</li> <li data-index="1" data-flag="c">評分:80</li> <li data-index="2" data-flag="c">級別:5</li> </ul> </body> </html>
http://sandbox.runjs.cn/show/9ywitfn8
不足與擴展
上面作為基本實現,沒有什么問題,但是其實際應用場景會有以下不足:
① 基本的ul層級需要一個包裹層,包裹層具有一個up或者down的class,然后在決定那個箭頭是向上還是向下
② 我們這里不能使用偽類,其原因是,我們的小三角標簽並不是一定在中間,其具有一定滑動的特性,也就是說,這個小三角需要被js控制其左右位置,他需要是一個標簽
根據以上所述,我們的結構似乎應該是這個樣子滴:
1 <section class="cui-bubble-layer up-or-down-class"> 2 <i class="cui-icon-triangle"></i> 3 <ul> 4 <li data-index="0" data-flag="c">價格:¥35</li> 5 <li data-index="1" data-flag="c">評分:80</li> 6 <li data-index="2" data-flag="c">級別:5</li> 7 </ul> 8 </section>
① 根元素上我們可以設置當前應該是up還是down的樣式
② i標簽根據根元素的up或者down選擇是向上還是向下,並且該標簽可被js操作
到此,似乎整個組件便比較完全了,但是真實的情況卻不是如此,怎么說了,上面的結構太局限了
該組件需要一個容器,這個容器標簽應該位於ul之上,這個時候容器內部所裝載的dom結構便可以不是ul而是其他什么結構了
其次,在手機上,我們可視項目在4S手機上不會超過5個,往往是4個,所以我們應該在其容器上設置類似overflow之類的可滾動屬性
組件回歸·最終結構
由上所述,基於其是繼承至Layer的事實,我們可以形成這樣的結構:
1 <section class="cui-pop cui-bubble-layer"> 2 <i class="cui-pop-triangle"></i> 3 <div class="cui-pop-head"> 4 </div> 5 <div class="cui-pop-body"> 6 <ul> 7 <li data-index="0" data-flag="c">價格:¥35</li> 8 <li data-index="1" data-flag="c">評分:80</li> 9 <li data-index="2" data-flag="c">級別:5</li> 10 </ul> 11 </div> 12 <div class="cui-pop-footer"> 13 </div> 14 </section>
這個也可以是我們整個彈出層類的基本結構,我們可以在此上做很多擴展,但是這里我們不扯太多,單就氣泡組件做論述
就氣泡組件,其結構是:
1 <section class="cui-pop cui-bubble-layer"> 2 <i class="cui-pop-triangle"></i> 3 <div class="cui-pop-body"> 4 <ul> 5 <li data-index="0" data-flag="c">價格:¥35</li> 6 <li data-index="1" data-flag="c">評分:80</li> 7 <li data-index="2" data-flag="c">級別:5</li> 8 </ul> 9 </div> 10 </section>
js層面的實現
這里仍然是采用的blade中的那一套繼承機制,如果有不明白又有點興趣的同學請移步:【blade的UI設計】理解前端MVC與分層思想
關於模板
因為我們這一部分的主題為重構相關,所以我們這里的關注點是CSS,我們首先生成我們的模板:
1 <section class="cui-pop <%=wrapperClass %> <%if(dir == 'up'){ %> <%=upClass %> <% } else { %> <%=downClass %> <% } %>"> 2 <i class="cui-pop-triangle"></i> 3 <div class="cui-pop-body"> 4 <ul class="cui-pop-list <%=itemStyleClass %>"> 5 <% for(var i = 0, len = data.length; i < len; i++) { %> 6 <% var itemData = data[i]; %> 7 <li data-index="<%=i%>" data-flag="c" class="<% if(index == i){ %><%=curClass %><%} %>" > 8 <%if(typeof itemFn == 'function') { %><%=itemFn.call(itemData) %> <% } else { %><%=itemData.name%><%} %> 9 <% } %> 10 </ul> 11 </div> 12 </section>
這里給出了幾個關鍵的定制化點:
① wrapperClass用以添加業務團隊定制化的class以改變根元素的class,如此的好處是便於業務團隊定制化氣泡組件的樣式
② 給出了項目列表Ul的可定制化className,通用單單只是方便業務團隊做樣式改變
③ 默認情況下返回的是傳入項目的name字段,但是用戶可傳入一個itemFn的回調,定制化返回
以上模板基本可滿足條件,如果不滿足,便可把整個模板作為參數傳入了
關於js實現
由於繼承的實現,我們大部分工作已經被做了,我們只需要在幾個關鍵地方編寫代碼即可

1 define(['UILayer', getAppUITemplatePath('ui.bubble.layer')], function (UILayer, template) { 2 return _.inherit(UILayer, { 3 propertys: function ($super) { 4 $super(); 5 //html模板 6 this.template = template; 7 this.needMask = false; 8 9 this.datamodel = { 10 data: [], 11 wrapperClass: 'cui-bubble-layer', 12 upClass: 'cui-pop--triangle-up', 13 downClass: 'cui-pop--triangle-down', 14 curClass: 'active', 15 itemStyleClass: '', 16 needBorder: true, 17 index: -1, 18 dir: 'up' //箭頭方向默認值 19 }; 20 21 this.events = { 22 'click .cui-pop-list>li': 'clickAction' 23 }; 24 25 this.onClick = function (data, index, el, e) { 26 console.log(arguments); 27 // this.setIndex(index); 28 }; 29 30 this.width = null; 31 32 //三角圖標偏移量 33 this.triangleLeft = null; 34 this.triangleRight = null; 35 36 this.triggerEl = null; 37 38 }, 39 40 initialize: function ($super, opts) { 41 $super(opts); 42 }, 43 44 createRoot: function (html) { 45 this.$el = $(html).hide().attr('id', this.id); 46 }, 47 48 clickAction: function (e) { 49 var el = $(e.currentTarget); 50 var i = el.attr('data-index'); 51 var data = this.datamodel.data[i]; 52 this.onClick.call(this, data, i, el, e); 53 }, 54 55 initElement: function () { 56 this.el = this.$el; 57 this.triangleEl = this.$('.cui-pop-triangle'); 58 this.windowWidth = $(window).width(); 59 }, 60 61 setIndex: function (i) { 62 var curClass = this.datamodel.curClass; 63 i = parseInt(i); 64 if (i < 0 || i > this.datamodel.data.length || i == this.datamodel.index) return; 65 this.datamodel.index = i; 66 67 //這里不以datamodel改變引起整個dom變化了,不划算 68 this.$('.cui-pop-list li').removeClass(curClass); 69 this.$('li[data-index="' + i + '"]').addClass(curClass); 70 }, 71 72 //位置定位 73 reposition: function () { 74 if (!this.triggerEl) return; 75 var offset = this.triggerEl.offset(); 76 var step = 6, w = offset.width - step; 77 var top = 0, left = 0, right; 78 if (this.datamodel.dir == 'up') { 79 top = (offset.top + offset.height + 8) + 'px'; 80 } else { 81 top = (offset.top - this.el.offset().height - 8) + 'px'; 82 } 83 84 left = (offset.left + 2) + 'px'; 85 86 if (offset.left + (parseInt(this.width) || w) > this.windowWidth) { 87 this.el.css({ 88 width: this.width || w, 89 top: top, 90 right: '2px' 91 }); 92 } else { 93 this.el.css({ 94 width: this.width || w, 95 top: top, 96 left: left 97 }); 98 } 99 100 if (this.triangleLeft) { 101 this.triangleEl.css({ 'left': this.triangleLeft, 'right': 'auto' }); 102 } 103 if (this.triangleRight) { 104 this.triangleEl.css({ 'right': this.triangleRight, 'left': 'auto' }); 105 } 106 }, 107 108 addEvent: function ($super) { 109 $super(); 110 this.on('onCreate', function () { 111 this.$el.removeClass('cui-layer'); 112 this.$el.css({ position: 'absolute' }); 113 }); 114 this.on('onShow', function () { 115 this.setzIndexTop(this.el); 116 }); 117 } 118 119 }); 120 121 });
這里開始調用的,便可做簡單實現:
1 'click .demo1': function (e) { 2 if (!this.demo1) { 3 var data = [{ name: '<span class="center">普通會員</span>' }, 4 { name: '<span class="center">vip</span>' }, 5 { name: '<span class="center">高級vip</span>' }, 6 { name: '<span class="center">鑽石vip</span>'}]; 7 this.list = new UIBubbleLayer({ 8 datamodel: { 9 data: data 10 }, 11 triggerEl: $(e.currentTarget), 12 width: '150px', 13 triangleLeft: '20px' 14 }); 15 } 16 this.list.show(); 17 }
稍作修改便可形成另一種樣子:
只不過我們還得考慮這個場景的發生,在項目過多過長時我們仍需要做處理:
這里有很多辦法可以處理,第一個是直接傳入maxHeight,如果高度超出的話便出現滾動條,第二個是動態在組件內部計算,查看組件與可視區域的關系
我們這里還是采用可視區域計算吧,於是對原組件做一些改造,加一個接口:
this.checkHeightOverflow();
就這一簡單接口其實可分為幾個段落的實現
第一個接口為檢測可視區域,這個可以被用戶重寫
isSizeOverflow
第二個接口是如果可視區域超出,也就是第一個接口返回true時的處理邏輯
handleSizeOverflow
考慮到超出的未必是高度,所以這里height改為了Size
當然,這里會存在資源銷毀的工作,所以會新增一個hide接口
1 isSizeOverflow: function () { 2 if (!this.el) return false; 3 if (this.el.height() > this.windowHeight * 0.8) return true; 4 return false; 5 }, 6 7 handleSizeOverflow: function () { 8 if (!this.isSizeOverflow()) return; 9 10 this.listWrapper.css({ 11 height: (parseInt(this.windowHeight * 0.8) + 'px'), 12 overflow: 'hidden', 13 position: 'relative' 14 }); 15 16 this.listEl.css({ position: 'absolute', width: '100%' }); 17 18 //調用前需要重置位置 19 this.reposition(); 20 21 this.scroll = new UIScroll({ 22 wrapper: this.listWrapper, 23 scroller: this.listEl 24 }); 25 }, 26 27 checkSizeOverflow: function () { 28 this.handleSizeOverflow(); 29 }, 30 31 addEvent: function ($super) { 32 $super(); 33 this.on('onCreate', function () { 34 this.$el.removeClass('cui-layer'); 35 this.$el.css({ position: 'absolute' }); 36 }); 37 this.on('onShow', function () { 38 39 //檢查可視區域是否超出; 40 this.checkSizeOverflow(); 41 this.setzIndexTop(this.el); 42 }); 43 this.on('onHide', function () { 44 if (this.scroll) this.scroll.destroy(); 45 }); 46 }
到此,我們的功能也基本結束了,最后實現一個定制化一點的功能,將我們的氣泡組件變成黑色:
結語
今天的學習到此為止,因為小釵css也算是初學,若是文中有誤,請提出
該組件的動畫以來我准備做到Layer基類上,而是會介紹css3的動畫技術,這里便不介紹了
下一期,我們就mobile的整體布局,以及header組件的實現做說明學習
代碼地址:https://github.com/yexiaochai/cssui/tree/gh-pages
demo地址:http://yexiaochai.github.io/cssui/demo/debug.html#bubble.layer