jQuery 插件開發——Menu(導航菜單)


  故事背景:由於最近太忙了,已經很久沒有寫jquery插件開發系列了。但是憑着自己對這方面的愛好,我還是抽了一些時間來過一下插件癮的。今天的主題是導航菜單,這個我相信不管做B/S還是做C/S都非常熟悉一個功能模塊。其實大家有沒有發現,我們開發插件的目的是為了重用,既然是需要重用的肯定也是開發中常用的,所以說白了,我們開發插件的需求來自開發中常用的功能。只要你想,你仔細分析,相信絕大部分常用功能都可以分裝出來做插件的。額。。。有種秀智商的趕腳啊,呵呵,不好意思,想到哪里就說道哪里了。相信大家還是能清楚啥時需要開發插件的。本篇文章其實需求來源是來源於我現在做的一個項目,但是后期我又做了優化,和原有需求不同。當然,我改的這個版本的樣式就沒有那么炫了。但是代碼肯定優化了。

  還是我一直提到的,你開發插件,你肯定要清楚該插件是做啥的,啥時用。也就是需求分析要做好。相信有人會說又裝13了,其實這不是裝,因為menu是大家所熟知的,但是我也相信就算大家熟知的事情你也不一定就了解它的所有功能。開發的插件是根據業務來的,不同的業務需求對導航菜單的要求也不同。不管是樣式還是功能。例如面包削,這個就是菜單的“附贈”品,很多網站需要有,但是也有很多網站不需要。所以,請大家也不要裝,除非你真的是大牛,可以目空一切。但是一般大牛都好像很謙虛的,很深奧的樣子,至少我看到的都不錯,^_^ 

  對了,其實有一句好我很想說的就是,如果你喜歡或者有意向開發jquery插件的,請你熟悉一下div+css頁面布局,如果你這方面不熟悉,其實是苦惱的。相信開發過的人都知道。很多人會說我們公司有前端專門做樣式的,但是我想說的是,多學點沒什么壞處。這樣方便你開發,能提高自己寫的代碼質量。

  好了,感覺一扯就像吃了炫邁似的,根本停不下來,其實也就是說開發需要扯。。。^_^。。。我是想看文章的人也很累,讓大家輕松一點。

  故事主題:jquery插件開發——Menu,導航菜單開發。

  正常的menu功能:1、實現菜單的切換  2、實現切換內容的加載  3、控制菜單的收縮  4、控制樣式變更

  附加功能:面包削導航

  本次開發用了大量的遞歸思想,其實好的遞歸可以為你節省很多很多代碼,但是說實話,復雜的遞歸在錯誤排查上還是很繁瑣的。所以我們要量力而行,當然還是希望大家能熟練運用遞歸,畢竟你將來是要成為牛X的猿,所以你就必須會各種算法。

  當然本次和上次開發的插件想必又添加了委托思想 事件句柄。當然這個我也得感謝我的一個同事,是在他的提醒下,我添加的,這樣寫的確實現了元素和事件間的解耦。當然這個也是模仿面向對象思想中的開發了。

  其實當你真正去多次開發插件時候,你就會發現,其實開發插件就分三步走。

  第一步:定義插件和參數  var menu = function () {this.defaultParams = {};};

  第二步:定義插件屬性、方法    menu.prototype = {constructor: menu,init:function (params){}};

  第三步:對外分裝  $.menu = new menu();  

  其實就是這三步,然后寫好每一步實現就好了。很簡單吧。^_^我感覺這三步就像一個系統的架構一樣,大的方向定下來,下面就是向框架中填充東西,實現功能即可。當然,開發中你要把公共部分先剝離出來,下面具體講解開發的代碼。分為以下幾個部分。

  第一部分:這部分是公共部分,比上一次寫的多了delegate,這個下面注冊事件的時候會用到,理解就像面向對象語言中理解一樣。如果對委托不是很清楚的可以百度看看,相信這種思想已經為大部分人所知了。

  代碼如下:

 1 $(function () {
 2     // 說明:創建委托函數
 3     //      context:函數上下文
 4     //      params:參數【必須是數組形式】,可以為空
 5     Function.prototype.delegate = function (context, params) {
 6         var func = this;
 7         return function () {
 8             if (params == null) {
 9                 return func.apply(context);
10             }
11             return func.apply(context, params);
12         };
13     };
14     var menuCommon = {
15         coverObject: function (obj1, obj2) {
16             var o = this.cloneObject(obj1, false);
17             var name;
18             for (name in obj2) {
19                 if (obj2.hasOwnProperty(name)) {
20                     o[name] = obj2[name];
21                 }
22             }
23             return o;
24         },
25         cloneObject: function (obj, deep) {
26             if (obj === null) {
27                 return null;
28             }
29             var con = new obj.constructor();
30             var name;
31             for (name in obj) {
32                 if (!deep) {
33                     con[name] = obj[name];
34                 } else {
35                     if (typeof (obj[name]) == "object") {
36                         con[name] = $.cloneObject(obj[name], deep);
37                     } else {
38                         con[name] = obj[name];
39                     }
40                 }
41             }
42             return con;
43         },
44         // 說明:實現委托
45         delegate: function (func, context, params) {
46             if ($.isFunction(func)) {
47                 return func.delegate(context, params);
48             } else {
49                 return $.noop;
50             }
51         },
52         getParam: function (param) {
53             if (typeof (param) == "undefined") {
54                 return "";
55             } else {
56                 return param;
57             }
58         }
59     };
60 });

    第二部分:定義導航默認參數,其中的data參數的格式我已經給出。

 1 var menu = function () {
 2         //參數定義
 3         this.defaultParams = {
 4             id: "",     //導航容器ID
 5             data: "",   //數據  包含title、depth、recordId、parentId、children
 6             //格式:[
 7             //       {title: "第一級——1",
 8             //        depth:1,
 9             //        recordId:1,
10             //        parentId:0,
11             //        children:[
12             //                  {title: "第二級",
13             //                   depth:2,
14             //                   recordId:3,
15             //                   parentId:1,
16             //                   children:[]
17             //                  }]
18             //       },
19             //       {title: "第一級——2",
20             //        depth:1,
21             //        recordId:2,
22             //        parentId:0,
23             //        children:[
24             //                  {title: "第二級",
25             //                   depth:2,
26             //                   recordId:4,
27             //                   parentId:2,
28             //                   children:[]
29             //                  }]
30             //       }]
31             boolBreadCut: true,    //是否要面包削
32             breadCutId: "",         //面包削ID
33             navClickCallback: $.noop     //導航點擊回調事件
34         };
35         this.options = {};
36  };

    第三部分:定義屬性、方法,並代碼實現。這部分很重要,封裝了各種方法,包括我說的事件句柄、遞歸等思想都在這里體現。代碼中有注釋。其中createMenu、getNodeById  getBreadCutNameList這個三個方法是用遞歸實現的。

  1 menu.prototype = {
  2         constructor: menu,
  3         init: function (params) {
  4             this.options = $.coverObject(this.defaultParams, params);
  5             this._init();
  6         },
  7         _init: function () {
  8             this._initMenu();
  9         },
 10         _initMenu: function () {
 11             if (this.options.data == null) {
 12                 return;
 13             }
 14             if (this.options.data.length < 1) {
 15                 return;
 16             }
 17             var htmlStr = this.createMenu(this.options.data, this.options.id, "");
 18             $("#" + this.options.id).html(htmlStr);
 19             //注冊導航事件
 20             this._registeNavClick();
 21         },
 22 
 23         //生成菜單的Html元素
 24         createMenu: function (data, id, htmlStr) {
 25             $.each(data, function (i, item) {
 26                 var depth = item.depth;
 27                 var recordId = item.recordId;
 28                 var parentId = item.parentId;
 29                 var marginLeft = parseInt(item.depth) * 20;
 30 
 31                 htmlStr += "<div class='zwsMenu' depth='" + depth + "'>";
 32 
 33                 if (depth === 1) {
 34                     htmlStr += "<div id='" + recordId + "' isShow='true' depth='" + depth + "' parentId='" + parentId + "' class='menu_depth_1' >";
 35                     htmlStr += "    <div class='menu_depth_1_icon' attrIcon='firstLevel' style ='margin-left:" + marginLeft + "px;'></div>";//第一級小圖標
 36                     htmlStr += "    <div class ='meun_title' >" + item.title + "</div>";//標題
 37                     htmlStr += "</div>";
 38                 } else {
 39                     htmlStr += "<div id='" + recordId + "' isShow='true' depth='" + depth + "' parentId='" + parentId + "' class='menu_depth_other' >";
 40                     htmlStr += "    <div class='menu_depth_other_icon' attrIcon='otherLevel' style ='margin-left:" + marginLeft + "px;'></div>";//其他級小圖標
 41                     htmlStr += "    <div class ='meun_title' >" + item.title + "</div>";//標題
 42                     htmlStr += "</div>";
 43                 }
 44 
 45                 if (item.children != null && item.children.length > 0) {
 46                     htmlStr += "<div class='meun_navArea' depth='" + item.children[0].depth + "' parentId='" + recordId + "' isShow='false' navArea=''>";
 47 
 48                     htmlStr = menu.prototype.createMenu(item.children, id, htmlStr);
 49 
 50                     htmlStr += "</div>";
 51                 }
 52 
 53                 htmlStr += "</div>";
 54             });
 55 
 56             return htmlStr;
 57         },
 58 
 59         /*****************(注冊事件 begin)*****************/
 60 
 61         //說明:
 62         //      注冊導航事件
 63         _registeNavClick: function () {
 64             var options = this.options;
 65 
 66             $("div[depth][isShow='true']").each(function (i, item) {
 67                 var itemClick = $.delegate(menu.prototype._handleNavClick, this, [{ item: item }]);//樣式改變
 68                 var itemClickCallBack = $.delegate(options.navClickCallback, this);//回調事件
 69                 var itemShowBreadCut = $.delegate(menu.prototype.createBreadCut, this, [{ item: item, options: options }]);//面包削
 70 
 71                 $(item).click(itemClick);
 72                 $(item).click(itemClickCallBack);
 73                 $(item).click(itemShowBreadCut);
 74 
 75             });
 76         },
 77 
 78         //說明:
 79         //      注冊面包削事件
 80         _registeBreadCutClick: function () {
 81             $(".meun_breadCut_name").each(function (i, item) {
 82                 var breadCutId = $(this).attr("id");
 83                 var itemClick = $.delegate(menu.prototype._handleBreadCutClick, this, [{ breadCutId: breadCutId }]);//樣式改變
 84                 $(item).click(itemClick);
 85             });
 86         },
 87 
 88         /*****************(注冊事件 end)*****************/
 89 
 90         /*****************(事件句柄 begin)*****************/
 91 
 92         //說明:
 93         //      導航事件句柄
 94         //      params:導航每行元素
 95         _handleNavClick: function (params) {
 96             var id = params.item.id;
 97 
 98             var isShow = $("div[navArea][parentId='" + id + "']").attr("isShow");
 99             var depth = parseInt($("#" + id).attr("depth"));
100             var parentId = parseInt($("#" + id).attr("parentId"));
101 
102             //當前深度級的導航
103             var currDepthLevel = $("div[parentId='" + parentId + "'][depth='" + depth + "']");
104 
105             //獲取下級導航區域
106             var navHide = currDepthLevel.next("div[depth='" + (depth + 1) + "'][navArea]");
107             navHide.attr("isShow", "false").css("display", "none");
108 
109             //將所有導航都置成 未選中狀態
110             var navHideIconOtherLevel = $("div[attrIcon='otherLevel']");
111             var meunTitle = $("div.meun_title");
112             navHideIconOtherLevel.removeClass("menu_depth_other_icon_selected").addClass("menu_depth_other_icon");
113             meunTitle.removeClass("meun_title_color");
114 
115             //當點擊第一級導航時候
116             if (depth === 1) {
117                 var navHideIconFirstLevel = $("div[attrIcon='firstLevel']");
118                 navHideIconFirstLevel.removeClass("menu_depth_1_icon_selected").addClass("menu_depth_1_icon");
119             }
120 
121             //獲取第一級導航和其他級導航中圖標
122             var iconFirst = $("#" + id).find("div[attrIcon='firstLevel']");
123             var iconOther = $("#" + id).find("div[attrIcon='otherLevel']");
124             var currTitle = $("#" + id).find("div.meun_title");
125 
126             //控制當前點擊的導航的下級導航是否顯示
127             var navCurr = $("div[parentId='" + id + "']");
128             if (isShow == "true") {
129                 navCurr.attr("isShow", "false").css("display", "none");
130 
131                 currTitle.removeClass("meun_title_color");
132                 iconFirst.removeClass("menu_depth_1_icon_selected").addClass("menu_depth_1_icon");
133                 iconOther.removeClass("menu_depth_other_icon_selected").addClass("menu_depth_other_icon");
134             }
135             else {
136                 navCurr.attr("isShow", "true").css("display", "block");
137 
138                 currTitle.addClass("meun_title_color");
139                 iconFirst.removeClass("menu_depth_1_icon").addClass("menu_depth_1_icon_selected");
140                 iconOther.removeClass("menu_depth_other_icon").addClass("menu_depth_other_icon_selected");
141             }
142         },
143 
144         //說明:
145         //      面包削事件句柄
146         _handleBreadCutClick: function (params) {
147             var breadCutId = params.breadCutId;
148             var navId = breadCutId.substr(9, breadCutId.length - 9);
149             $("#" + navId).click();
150         },
151 
152         /*****************(事件句柄 end)*****************/
153 
154         //說明:
155         //     驗證面包削
156         validateBreadCut: function (options) {
157             if (!options.boolBreadCut) {
158                 return false;
159             }
160             var breadCutObj = $("#" + options.breadCutId);  //面包削區域
161             if (options.breadCutId == "" || breadCutObj.length < 1) {
162                 return false;
163             }
164             return true;
165         },
166 
167         //說明:
168         //      創建面包削
169         createBreadCut: function (params) {
170             var item = params.item;
171             var itemId = item.id;
172             var options = params.options;
173             var optionData = options.data;
174 
175             if (!menu.prototype.validateBreadCut(options)) {
176                 return;
177             }
178 
179             var depth = parseInt($("#" + itemId).attr("depth"));
180             var separator = "<div class='meun_breadCut_separator'> &gt; </div>";//分隔符
181             var breadCutHtml = "";
182             breadCutHtml += "<div class='meun_breadCut'>";
183 
184             var itemNode = menu.prototype.getNodeById(itemId, optionData);
185 
186             var breadCutNodeList = menu.prototype.getBreadCutNodeList(itemNode, optionData, []);
187 
188             for (var i = 1; i <= depth; i++) {
189                 breadCutHtml += "<div id='breadCut_" + breadCutNodeList[depth - i].recordId + "' class='meun_breadCut_name'>";
190                 breadCutHtml += breadCutNodeList[depth - i].title;
191                 breadCutHtml += "</div>";
192                 if (i != depth) {
193                     breadCutHtml += separator;
194                 }
195             }
196             breadCutHtml += "</div>";
197 
198             $("#" + options.breadCutId).html(breadCutHtml);
199 
200             //注冊事件
201             menu.prototype._registeBreadCutClick();
202 
203         },
204 
205         //說明:
206         //      獲取面包削列表
207         //      item:當前點擊的導航
208         //      optionsData:數據源
209         //      breadCutNameList:返回列表
210         getBreadCutNodeList: function (item, optionData, breadCutNameList) {
211             if (item != null && item.parentId >= 0) {
212                 var node = menu.prototype.getNodeById(item.recordId, optionData);
213                 breadCutNameList.push(node);//獲得列表
214 
215                 item = menu.prototype.getNodeById(item.parentId, optionData);
216                 menu.prototype.getBreadCutNodeList(item, optionData, breadCutNameList);
217             }
218             return breadCutNameList;
219         },
220 
221         //說明:
222         //      根據ID獲取節點
223         //      id:節點ID
224         //      optionsData:數據源
225         getNodeById: function (id, optionsData) {
226             if (id < 1) {
227                 return null;
228             }
229             $.each(optionsData, function (i, v) {
230                 if (v.recordId == id) {
231                     nodeTS = v;
232                     return false;
233                 }
234                 if (v.children.length > 0) {
235                     menu.prototype.getNodeById(id, v.children);
236                 }
237             });
238             return typeof (nodeTS) !== "undefined" ? nodeTS : null;
239         }
240     };
menu.prototype 代碼

   第四部分:這部分很簡單,就是對外封裝,一句話而已。

1 $.menu = new menu();

   第五部分:這部分當然是調用啦^_^,具體的參數說明在定義默認參數的時候都用注釋,這里就不再累述。

1 $.menu.init({
2       id: "leftMenu",
3       data: data,//注意格式
4       navClickCallback: function () {
5       },
6       boolBreadCut: true,
7       breadCutId: "mbx"
8 });

     第六部分:樣式表,本次樣式比較簡單、少,所以可以貼出來。

 1 .menu_depth_1 {
 2     width: 200px;cursor: pointer;height: 33px;line-height: 33px;
 3     margin-top: 2px;background-image: url("../Images/MenuImg/bg.png");
 4 }
 5 .menu_depth_1_icon {
 6     width: 11px;height: 11px;background-image: url("../Images/MenuImg/right-depth1.png");
 7     margin-top: 10px;margin-right: 3px;float: left;
 8 }
 9 .menu_depth_1_icon_selected {
10     width: 11px;height: 11px;background-image: url("../Images/MenuImg/down-depth1.png");
11     margin-top: 10px;margin-right: 3px;float: left;
12 }
13 .menu_depth_other {
14     width: 200px;cursor: pointer;background-color: #FFFFFF;height: 33px;line-height: 33px;display: none;
15     border-bottom: 1px dashed #E0E0E0;
16 }
17 .menu_depth_other_icon {
18     width: 7px;height: 7px;background-image: url("../Images/MenuImg/right-depth2-1.png");
19     margin-top: 13px;margin-right: 3px;float: left;
20 }
21 .menu_depth_other_icon_selected {
22     width: 7px;height: 7px;background-image: url("../Images/MenuImg/right-depth2-2.png");
23     margin-top: 13px;margin-right: 3px;float: left;
24 }
25 .meun_title {
26     width: auto;height: 100%;float: left;
27 }
28 .meun_title_color {
29     color: #FF6600;
30 }
31 .meun_navArea {
32     width: 100%;height: auto;
33 }
34 
35 /*面包削*/
36 .meun_breadCut {
37     width: 100%;height: 30px;line-height: 30px;
38 }
39 .meun_breadCut_name {
40     width: auto;height: 30px;line-height: 30px;cursor: pointer;float: left;
41 }
42 .meun_breadCut_separator {
43     width: auto;height: 30px;line-height: 30px;margin: 0px 5px;float: left;
44 }

     第七部分:當然是最后測試了啊,測試很重要,要相信好的代碼是測出來的。哈哈。。。

  第八部分:效果圖

  本來是沒有添加這部分內容的,原因是當時我沒有好的圖片做效果(本人隨熟練布局,但是不會ps。。。),今天我請我公司的UI設計師給我簡單的畫了幾張圖,在此也表示很感謝我的那位同事。下面是我截的效果圖,效果圖上也有些說明。可能不是很好看,但是功能杠杠的,哈哈^_^

  總結:其實本次開發比較急,按照我上兩篇文章,其實我應該在添加一個主題部分的,當然這里就是為什么說我要大家學習div+css的原因了,如果你會布局,你可以做出各種你喜歡的主題風格。這次我偷懶了,沒有加上,后期我會補上。文章比較長,很多知識點我也沒有寫詳細。如果有需要源碼的或者想共同探討的同仁,隨時聯系我,QQ:296319075 ,注明園友就好,同時也希望大家也能提出寶貴意見,不吝賜教。秉承共同探討、共同進步!如有轉載,請注明出處,謝謝!^_^

   

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM