1 /**
  2  * @fileOverview submenu model and control for kissy , transfer item's keycode to menu
  3  * @author yiminghe@gmail.com
  4  */
  5 KISSY.add("menu/submenu", function (S, Event, Component, MenuItem, SubMenuRender) {
  6 
  7     /* or precisely submenuitem */
  8 
  9     var KeyCodes = Event.KeyCodes,
 10         doc = S.Env.host.document,
 11         MENU_DELAY = 150;
 12     /**
 13      * Class representing a submenu that can be added as an item to other menus.
 14      * xclass: 'submenu'.
 15      * @constructor
 16      * @extends Menu.MenuItem
 17      * @memberOf Menu
 18      */
 19     var SubMenu = MenuItem.extend([Component.DecorateChild], {
 20 
 21             /**
 22              * Bind sub menu events.
 23              * @protected
 24              */
 25             bindSubMenu:function () {
 26                 /**
 27                  * 自己不是 menu,自己只是 menuitem,其所属的 menu 为 get("parent")
 28                  */
 29                 var self = this,
 30                     menu = self.get("menu"),
 31                     parentMenu = self.get("parent");
 32 
 33                 //当改菜单项所属的菜单隐藏后,该菜单项关联的子菜单也要隐藏
 34                 if (parentMenu) {
 35 
 36                     parentMenu.on("hide", onParentHide, self);
 37 
 38                     // 子菜单选中后也要通知父级菜单
 39                     // 不能使用 afterSelectedItemChange ,多个 menu 嵌套,可能有缓存
 40                     // 单个 menu 来看可能 selectedItem没有变化
 41                     menu.on("click", function (ev) {
 42                         parentMenu.fire("click", {
 43                             target:ev.target
 44                         });
 45                     });
 46 
 47                     // if not bind doc click for parent menu
 48                     // if already bind, then if parent menu hide, menu will hide too
 49                     // !TODO 优化此处绑定!,不要特殊标记
 50                     if (!parentMenu.__bindDocClickToHide) {
 51                         // 绑到最根部
 52                         Event.on(doc, "click", _onDocClick, self);
 53                         parentMenu.__bindDocClickToHide = 1;
 54                         // 绑到最根部
 55                         menu.__bindDocClickToHide = 1;
 56                     }
 57 
 58                     // 通知父级菜单
 59                     menu.on("afterActiveItemChange", function (ev) {
 60                         parentMenu.set("activeItem", ev.newVal);
 61                     });
 62 
 63 
 64                     menu.on("afterHighlightedItemChange", function (ev) {
 65                         if (ev.newVal) {
 66                             // 1. 菜单再次高亮时,取消隐藏
 67                             // 2. fix #160
 68                             self.set("highlighted", true);
 69                         }
 70                     });
 71 
 72                     // 只绑定一次
 73                     self.bindSubMenu = S.noop;
 74                 }
 75 
 76                 // 访问子菜单,当前 submenu 不隐藏 menu
 77                 // leave submenuitem -> enter menuitem -> menu item highlight ->
 78                 // -> menu highlight -> beforeSubMenuHighlightChange ->
 79 
 80                 // menu render 后才会注册 afterHighlightedItemChange 到 _uiSet
 81                 // 这里的 beforeSubMenuHighlightChange 比 afterHighlightedItemChange 先执行
 82                 // 保险点用 beforeHighlightedItemChange
 83                 menu.on("beforeHighlightedItemChange", beforeSubMenuHighlightChange, self);
 84             },
 85 
 86             handleMouseEnter:function (e) {
 87                 var self = this;
 88                 if (SubMenu.superclass.handleMouseEnter.call(self, e)) {
 89                     return true;
 90                 }
 91                 // 两个作用
 92                 // 1. 停止孙子菜单的层层检查,导致 highlighted false 而 buffer 的隐藏
 93                 // 2. 停止本身 highlighted false 而 buffer 的隐藏
 94                 self.clearSubMenuTimers();
 95                 self.showTimer_ = S.later(showMenu, self.get("menuDelay"), false, self);
 96             },
 97 
 98             /**
 99              * Dismisses the submenu on a delay, with the result that the user needs less
100              * accuracy when moving to sub menus.
101              * @protected
102              */
103             _uiSetHighlighted:function (e) {
104                 var self = this;
105                 if (!e) {
106                     self.dismissTimer_ = S.later(hideMenu, self.get("menuDelay"), false, self);
107                 }
108             },
109 
110             /**
111              * Clears the show and hide timers for the sub menu.
112              */
113             clearSubMenuTimers:function () {
114                 var self = this,
115                     dismissTimer_,
116                     showTimer_;
117                 if (dismissTimer_ = self.dismissTimer_) {
118                     dismissTimer_.cancel();
119                     self.dismissTimer_ = null;
120                 }
121                 if (showTimer_ = self.showTimer_) {
122                     showTimer_.cancel();
123                     self.showTimer_ = null;
124                 }
125             },
126 
127             // click ,立即显示
128             performActionInternal:function () {
129                 var self = this;
130                 self.clearSubMenuTimers();
131                 showMenu.call(self);
132                 //  trigger click event from menuitem
133                 SubMenu.superclass.performActionInternal.apply(self, arguments);
134             },
135 
136             /**
137              * Handles a key event that is passed to the menu item from its parent because
138              * it is highlighted.  If the right key is pressed the sub menu takes control
139              * and delegates further key events to its menu until it is dismissed OR the
140              * left key is pressed.
141              * @param e A key event.
142              * @protected
143              * @return {Boolean} Whether the event was handled.
144              */
145             handleKeydown:function (e) {
146                 var self = this,
147                     menu = getMenu(self),
148                     hasKeyboardControl_ = menu && menu.get("visible"),
149                     keyCode = e.keyCode;
150 
151                 if (!hasKeyboardControl_) {
152                     // right
153                     if (keyCode == KeyCodes.RIGHT) {
154                         showMenu.call(self);
155                         menu = getMenu(self);
156                         if (menu) {
157                             var menuChildren = menu.get("children");
158                             if (menuChildren[0]) {
159                                 menu.set("highlightedItem", menuChildren[0]);
160                             }
161                         }
162                     }
163                     // enter as click
164                     else if (e.keyCode == Event.KeyCodes.ENTER) {
165                         return this.performActionInternal(e);
166                     }
167                     else {
168                         return undefined;
169                     }
170                 } else if (menu.handleKeydown(e)) {
171                 }
172                 // The menu has control and the key hasn't yet been handled, on left arrow
173                 // we turn off key control.
174                 // left
175                 else if (keyCode == KeyCodes.LEFT) {
176                     hideMenu.call(self);
177                     // 隐藏后,当前激活项重回
178                     self.get("parent").set("activeItem", self);
179                 } else {
180                     return undefined;
181                 }
182                 return true;
183             },
184 
185             hideParentMenusBuffer:function () {
186                 var self = this, parentMenu = self.get("parent");
187                 self.dismissTimer_ = S.later(function () {
188                         var submenu = self,
189                             popupmenu = self.get("menu");
190                         while (popupmenu.get("autoHideOnMouseLeave")) {
191                             // 取消高亮,buffer 隐藏子菜单
192                             // 可能马上又移到上面,防止闪烁
193                             // 相当于强制 submenu mouseleave
194                             submenu.set("highlighted", false);
195                             // 原来的 submenu 在高亮
196                             // 表示越级选择 menu
197                             if (parentMenu.get("highlightedItem") != submenu) {
198                                 break;
199                             }
200                             submenu = parentMenu.get("parent");
201                             if (!submenu) {
202                                 break;
203                             }
204                             parentMenu = submenu.get("parent");
205                             popupmenu = submenu.get("menu");
206                         }
207                     },
208                     self.get("menuDelay"),
209                     false,
210                     self);
211             },
212 
213             containsElement:function (element) {
214                 var menu = getMenu(this);
215                 return menu && menu.containsElement(element);
216             },
217 
218             // 默认 addChild,这里里面的元素需要放到 menu 属性中
219             decorateChildrenInternal:function (ui, el) {
220                 // 不能用 display:none
221                 el.css("visibility", "hidden");
222                 var self = this,
223                     docBody = S.one(el[0].ownerDocument.body);
224                 docBody.prepend(el);
225                 var menu = new ui({
226                     srcNode:el,
227                     prefixCls:self.get("prefixCls")
228                 });
229                 self.__set("menu", menu);
230             },
231 
232             destructor:function () {
233                 var self = this,
234                     parentMenu = self.get("parent"),
235                     menu = getMenu(self);
236 
237                 self.clearSubMenuTimers();
238 
239                 if (menu && menu.__bindDocClickToHide) {
240                     menu.__bindDocClickToHide = 0;
241                     Event.remove(doc, "click", _onDocClick, self);
242                 }
243 
244                 //当改菜单项所属的菜单隐藏后,该菜单项关联的子菜单也要隐藏
245                 if (parentMenu) {
246                     parentMenu.detach("hide", onParentHide, self);
247                 }
248 
249                 if (menu && menu.destroy) {
250                     menu.destroy();
251                 }
252             }
253         },
254         {
255             ATTRS:{
256                 /**
257                  * The delay before opening the sub menu in milliseconds.  (This number is
258                  * arbitrary, it would be good to get some user studies or a designer to play
259                  * with some numbers).
260                  * @type {number}
261                  */
262                 menuDelay:{
263                     value:MENU_DELAY
264                 },
265                 menu:{
266                     setter:function (m) {
267                         if (m instanceof  Component.Controller) {
268                             m.__set("parent", this);
269                         }
270                     }
271                 },
272                 decorateChildCls:{
273                     valueFn:function () {
274                         return this.get("prefixCls") + "popupmenu"
275                     }
276                 },
277                 xrender:{
278                     value:SubMenuRender
279                 }
280             }
281         }, {
282             xclass:'submenu',
283             priority:20
284         });
285 
286     // # -------------------------------- private start
287 
288     function getMenu(self, init) {
289         var m = self.get("menu");
290         if (m && m.xclass) {
291             if (init) {
292                 m = Component.create(m, self);
293                 self.__set("menu", m);
294             } else {
295                 return null;
296             }
297         }
298         return m;
299     }
300 
301     function _onDocClick(e) {
302         var self = this,
303             menu = getMenu(self),
304             target = e.target,
305             parentMenu = self.get("parent"),
306             el = self.get("el");
307 
308         // only hide this menu, if click outside this menu and this menu's submenus
309         if (!parentMenu.containsElement(target)) {
310             menu && menu.hide();
311             // submenuitem should also hide
312             self.get("parent").set("highlightedItem", null);
313         }
314     }
315 
316     function showMenu() {
317         var self = this,
318             menu = getMenu(self, 1);
319         if (menu) {
320 
321             // 保证显示前已经绑定好事件
322             self.bindSubMenu();
323 
324             var align = S.clone(menu.get("align"));
325             align.node = self.get("el");
326             align.points = align.points || ['tr', 'tl'];
327             menu.set("align", align);
328             menu.show();
329             /**
330              * If activation of your menuitem produces a popup menu,
331              then the menuitem should have aria-haspopup set to the ID of the corresponding menu
332              to allow the assistive technology to follow the menu hierarchy
333              and assist the user in determining context during menu navigation.
334              */
335             self.get("el").attr("aria-haspopup",
336                 menu.get("el").attr("id"));
337         }
338     }
339 
340     function hideMenu() {
341         var menu = getMenu(this);
342         if (menu) {
343             menu.hide();
344         }
345     }
346 
347     /**
348      * Listens to the sub menus items and ensures that this menu item is selected
349      * while dismissing the others.  This handles the case when the user mouses
350      * over other items on their way to the sub menu.
351      * @param  e Highlight event to handle.
352      * @private
353      */
354     function beforeSubMenuHighlightChange(e) {
355         var self = this;
356         if (e.newVal) {
357             self.clearSubMenuTimers();
358             // superclass(menuitem).handleMouseLeave 已经把自己 highlight 去掉了
359             // 导致本类 _uiSetHighlighted 调用,又把子菜单隐藏了
360             self.get("parent").set("highlightedItem", self);
361         }
362     }
363 
364     function onParentHide() {
365         var menu = getMenu(this);
366         menu && menu.hide();
367     }
368 
369     // # ------------------------------------ private end
370 
371     return SubMenu;
372 }, {
373     requires:['event', 'component', './menuitem', './submenuRender']
374 });
375 
376 /**
377 
378  **/