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 **/