【HTML5&CSS3進階學習01】氣泡元件的實現

範大腳腳發表於2017-11-02

前言

氣泡元件在實際工作中非常普遍,無論是網頁中還是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>
View Code

這個時候在為其加一個偽類,做點樣式上的調整,便基本實現了,這裡用到了偽類的知識點:

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>
View Code

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 });
View Code

這裡開始呼叫的,便可做簡單實現:

 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

相關文章