1 /**
  2  * @fileOverview combination of menu and button ,similar to native select
  3  * @author yiminghe@gmail.com
  4  */
  5 KISSY.add("menubutton/base", function (S, Node, Button, MenuButtonRender, Menu, Component, undefined) {
  6 
  7     var win = S.Env.host;
  8 
  9     function getMenu(self, init) {
 10         var m = self.get("menu");
 11         if (m && m.xclass) {
 12             if (init) {
 13                 m = Component.create(m, self);
 14                 self.__set("menu", m);
 15             } else {
 16                 return null;
 17             }
 18         }
 19         return m;
 20     }
 21 
 22     function reposition() {
 23         var self = this,
 24             menu = getMenu(self);
 25         if (menu && menu.get("visible")) {
 26             var align = S.clone(menu.get("align"));
 27             align.node = self.get("el");
 28             S.mix(align, ALIGN, false);
 29             menu.set("align", align);
 30         }
 31     }
 32 
 33     var repositionBuffer = S.buffer(reposition, 50);
 34 
 35     function hideMenu(self) {
 36         var menu = getMenu(self);
 37         if (menu) {
 38             menu.hide();
 39         }
 40     }
 41 
 42     function showMenu(self) {
 43         var el = self.get("el"),
 44             menu = getMenu(self, 1);
 45         // 保证显示前已经 bind 好 menu 事件
 46         if (menu && !menu.get("visible")) {
 47             // 先 render,监听 width 变化事件
 48             menu.render();
 49             self.bindMenu();
 50             // 根据 el 自动调整大小
 51             if (self.get("matchElWidth")) {
 52                 menu.set("width", el.innerWidth());
 53             }
 54             menu.show();
 55             reposition.call(self);
 56             el.attr("aria-haspopup", menu.get("el").attr("id"));
 57         }
 58     }
 59 
 60     var $ = Node.all,
 61         KeyCodes = Node.KeyCodes,
 62         ALIGN = {
 63             points:["bl", "tl"],
 64             overflow:{
 65                 adjustX:1,
 66                 adjustY:1
 67             }
 68         };
 69     /**
 70      * @class
 71      * A menu button component, consist of a button and a drop down popup menu.
 72      * xclass: 'menubutton'.
 73      * @name MenuButton
 74      * @extends Button
 75      */
 76     var MenuButton = Button.extend([Component.DecorateChild],
 77         /**
 78          * @lends MenuButton.prototype
 79          */
 80         {
 81             _uiSetCollapsed:function (v) {
 82                 if (v) {
 83                     hideMenu(this);
 84                 } else {
 85                     showMenu(this);
 86                 }
 87             },
 88 
 89             /**
 90              * Bind menu to current component.
 91              * Protected, should only be overridden by subclasses.
 92              * @protected
 93              */
 94             bindMenu:function () {
 95                 var self = this,
 96                     menu = self.get("menu");
 97 
 98                 menu.on("afterActiveItemChange", function (ev) {
 99                     self.set("activeItem", ev.newVal);
100                 });
101 
102                 menu.on("click", self.handleMenuClick, self);
103 
104                 // 窗口改变大小,重新调整
105                 $(win).on("resize", repositionBuffer, self);
106 
107                 /*
108                  只绑定事件一次
109                  */
110                 self.bindMenu = S.noop;
111             },
112 
113             /**
114              * Handle click on drop down menu. Fire click event on menubutton.
115              * Protected, should only be overridden by subclasses.
116              * @param {Event.Object} e Click event object.
117              * @protected
118              */
119             handleMenuClick:function (e) {
120                 var self = this;
121                 self.fire("click", {
122                     target:e.target
123                 });
124             },
125 
126             /**
127              * Handle keydown/up event.
128              * If drop down menu is visible then handle event to menu.
129              * Returns true if the event was handled, falsy otherwise.
130              * Protected, should only be overridden by subclasses.
131              * @param {Event.Object} e key event to handle.
132              * @return {Boolean} True Whether the key event was handled.
133              * @protected
134              * @override
135              */
136             handleKeyEventInternal:function (e) {
137                 var self = this,
138                     menu = getMenu(self);
139 
140                 // space 只在 keyup 时处理
141                 if (e.keyCode == KeyCodes.SPACE) {
142                     // Prevent page scrolling in Chrome.
143                     e.preventDefault();
144                     if (e.type != "keyup") {
145                         return undefined;
146                     }
147                 } else if (e.type != "keydown") {
148                     return undefined;
149                 }
150                 //转发给 menu 处理
151                 if (menu && menu.get("visible")) {
152                     var handledByMenu = menu.handleKeydown(e);
153                     // esc
154                     if (e.keyCode == KeyCodes.ESC) {
155                         self.set("collapsed", true);
156                         return true;
157                     }
158                     return handledByMenu;
159                 }
160 
161                 // Menu is closed, and the user hit the down/up/space key; open menu.
162                 if (e.keyCode == KeyCodes.SPACE ||
163                     e.keyCode == KeyCodes.DOWN ||
164                     e.keyCode == KeyCodes.UP) {
165                     self.set("collapsed", false);
166                     return true;
167                 }
168                 return undefined;
169             },
170 
171             /**
172              * Perform default action for menubutton.
173              * Toggle the drop down menu to show or hide.
174              * Protected, should only be overridden by subclasses.
175              * @protected
176              * @override
177              */
178             performActionInternal:function () {
179                 var self = this;
180                 self.set("collapsed", !self.get("collapsed"));
181             },
182 
183             /**
184              * Handles blur event.
185              * When it loses keyboard focus, close the drop dow menu.
186              * @param {Event.Object} e Blur event.
187              * Protected, should only be overridden by subclasses.
188              * @protected
189              * @override
190              */
191             handleBlur:function (e) {
192                 var self = this;
193                 MenuButton.superclass.handleBlur.call(self, e);
194                 // such as : click the document
195                 self.set("collapsed", true);
196             },
197 
198 
199             /**
200              * Adds a new menu item at the end of the menu.
201              * @param {Menu.Item} item Menu item to add to the menu.
202              */
203             addItem:function (item, index) {
204                 var menu = getMenu(this, 1);
205                 menu.addChild(item, index);
206             },
207 
208             /**
209              * Remove a existing menu item from drop down menu.
210              * @param c {Menu.Item} Existing menu item.
211              * @param [destroy] {Boolean} Whether destroy removed menu item.
212              */
213             removeItem:function (c, destroy) {
214                 /**
215                  * @type Controller
216                  */
217                 var menu = getMenu(this);
218                 if (menu) {
219                     menu.removeChild(c, destroy);
220                 }
221             },
222 
223             /**
224              * Remove all menu items from drop down menu.
225              * @param [destroy] {Boolean} Whether destroy removed menu items.
226              */
227             removeItems:function (destroy) {
228                 var menu = this.get("menu");
229                 if (menu) {
230                     if (menu.removeChildren) {
231                         menu.removeChildren(destroy);
232                     } else if (menu.children) {
233                         menu.children = [];
234                     }
235                 }
236             },
237 
238             /**
239              * Returns the child menu item of drop down menu at the given index, or null if the index is out of bounds.
240              * @param {Number} index 0-based index.
241              */
242             getItemAt:function (index) {
243                 var menu = getMenu(this);
244                 return menu && menu.getChildAt(index);
245             },
246 
247             // 禁用时关闭已显示菜单
248             _uiSetDisabled:function (v) {
249                 var self = this;
250                 !v && self.set("collapsed", true);
251             },
252 
253             /**
254              * Decorate child element to from a child component.
255              * @param {Function} UI Child component's constructor
256              * @param {NodeList} el Child component's root element.
257              * @protected
258              * @override
259              */
260             decorateChildrenInternal:function (UI, el) {
261                 // 不能用 display:none , menu 的隐藏是靠 visibility
262                 // eg: menu.show(); menu.hide();
263                 var self = this,
264                     docBody = S.one(el[0].ownerDocument.body);
265                 docBody.prepend(el);
266                 var menu = new UI(S.mix({
267                     srcNode:el,
268                     prefixCls:self.get("prefixCls")
269                 }));
270                 self.__set("menu", menu);
271             },
272 
273             destructor:function () {
274                 var self = this;
275                 $(win).detach("resize", repositionBuffer, self);
276                 var menu = self.get("menu");
277                 if (menu.destroy) {
278                     menu.destroy();
279                 }
280             }
281 
282         },
283 
284         {
285             ATTRS:/**
286              * @lends MenuButton.prototype
287              */
288             {
289                 /**
290                  * Current active menu item.
291                  * @type Menu.Item
292                  */
293                 activeItem:{
294                     view:1
295                 },
296 
297                 /**
298                  * Whether drop down menu is same width with button.
299                  * Default: true.
300                  * @type {Boolean}
301                  */
302                 matchElWidth:{
303                     value:true
304                 },
305 
306                 /**
307                  * @private
308                  */
309                 decorateChildCls:{
310                     valueFn:function () {
311                         return this.get("prefixCls") + "popupmenu"
312                     }
313                 },
314                 /**
315                  * Drop down menu associated with this menubutton.
316                  * @type Menu
317                  */
318                 menu:{
319                     value:{
320                         xclass:'popupmenu'
321                     },
322                     setter:function (v) {
323                         if (v instanceof Menu) {
324                             v.__set("parent", this);
325                         }
326                     }
327                 },
328                 /**
329                  * Whether drop menu is shown.
330                  * @type Boolean
331                  */
332                 collapsed:{
333                     view:1
334                 },
335                 xrender:{
336                     value:MenuButtonRender
337                 }
338             }
339         }, {
340             xclass:'menu-button',
341             priority:20
342         });
343 
344     return MenuButton;
345 }, {
346     requires:[ "node", "button", "./baseRender", "menu", "component"]
347 });