/**
* @ignore
* submenu controller for kissy, transfer item's keycode to menu
* @author yiminghe@gmail.com
*/
KISSY.add("menu/submenu", function (S, Event, Component, MenuItem, SubMenuRender) {
/* or precisely submenuitem */
var KeyCodes = Event.KeyCodes,
doc = S.Env.host.document,
MENU_DELAY = 0.15;
* Class representing a submenu that can be added as an item to other menus.
* xclass: 'submenu'.
* @extends KISSY.Menu.Item
* @class KISSY.Menu.SubMenu
*/
var SubMenu = MenuItem.extend([Component.DecorateChild], {
* Bind sub menu events.
* Protected for subclass overridden.
* @protected
*/
bindSubMenu: function () {
* 自己不是 menu,自己只是 menuitem,其所属的 menu 为 get("parent")
*/
var self = this,
menu = self.get("menu"),
parentMenu = self.get("parent");
//当改菜单项所属的菜单隐藏后,该菜单项关联的子菜单也要隐藏
if (parentMenu) {
parentMenu.on("hide", onParentHide, self);
// if not bind doc click for parent menu
// if already bind, then if parent menu hide, menu will hide too
// !TODO 优化此处绑定!,不要特殊标记
if (!parentMenu.__bindDocClickToHide) {
// 绑到最根部
Event.on(doc, "click", _onDocClick, self);
parentMenu.__bindDocClickToHide = 1;
// 绑到最根部
menu.__bindDocClickToHide = 1;
}
// 通知父级菜单
menu.on("afterActiveItemChange", function (ev) {
parentMenu.set("activeItem", ev.newVal);
ev.stopPropagation();
});
menu.on("afterHighlightedItemChange", function (ev) {
if (ev.newVal) {
// 1. 菜单再次高亮时,取消隐藏
// 2. fix #160
self.set("highlighted", true);
}
ev.stopPropagation();
});
// 只绑定一次
self.bindSubMenu = S.noop;
}
// 访问子菜单,当前 submenu 不隐藏 menu
// leave submenuitem -> enter menuitem -> menu item highlight ->
// -> menu highlight -> beforeSubMenuHighlightChange ->
// menu render 后才会注册 afterHighlightedItemChange 到 _onSet
// 这里的 beforeSubMenuHighlightChange 比 afterHighlightedItemChange 先执行
// 保险点用 beforeHighlightedItemChange
menu.on("beforeHighlightedItemChange",
beforeSubMenuHighlightChange, self);
},
handleMouseEnter: function (e) {
var self = this;
if (SubMenu.superclass.handleMouseEnter.call(self, e)) {
return true;
}
// 两个作用
// 1. 停止孙子菜单的层层检查,导致 highlighted false 而 buffer 的隐藏
// 2. 停止本身 highlighted false 而 buffer 的隐藏
self.clearSubMenuTimers();
self.showTimer_ = S.later(showMenu, self.get("menuDelay") * 1000, false, self);
return undefined;
},
* Dismisses the submenu on a delay, with the result that the user needs less
* accuracy when moving to sub menus.
* @protected
*/
_onSetHighlighted: function (e) {
var self = this;
if (!e) {
self.dismissTimer_ = S.later(hideMenu, self.get("menuDelay") * 1000, false, self);
}
},
* Clears the show and hide timers for the sub menu.
* @private
*/
clearSubMenuTimers: function () {
var self = this,
dismissTimer_,
showTimer_;
if (dismissTimer_ = self.dismissTimer_) {
dismissTimer_.cancel();
self.dismissTimer_ = null;
}
if (showTimer_ = self.showTimer_) {
showTimer_.cancel();
self.showTimer_ = null;
}
},
// click ,立即显示
performActionInternal: function () {
var self = this;
self.clearSubMenuTimers();
showMenu.call(self);
// trigger click event from menuitem
SubMenu.superclass.performActionInternal.apply(self, arguments);
},
/**
* Handles a key event that is passed to the menu item from its parent because
* it is highlighted. If the right key is pressed the sub menu takes control
* and delegates further key events to its menu until it is dismissed OR the
* left key is pressed.
* Protected for subclass overridden.
* @param {KISSY.Event.DOMEventObject} e key event.
* @protected
* @return {Boolean|undefined} Whether the event was handled.
*/
handleKeydown: function (e) {
var self = this,
menu = getMenu(self),
hasKeyboardControl_ = menu && menu.get("visible"),
keyCode = e.keyCode;
if (!hasKeyboardControl_) {
// right
if (keyCode == KeyCodes.RIGHT) {
showMenu.call(self);
menu = getMenu(self);
if (menu) {
var menuChildren = menu.get("children");
if (menuChildren[0]) {
menu.set("highlightedItem", menuChildren[0]);
}
}
}
// enter as click
else if (e.keyCode == Event.KeyCodes.ENTER) {
return this.performActionInternal(e);
}
else {
return undefined;
}
} else if (menu.handleKeydown(e)) {
}
// The menu has control and the key hasn't yet been handled, on left arrow
// we turn off key control.
// left
else if (keyCode == KeyCodes.LEFT) {
hideMenu.call(self);
// 隐藏后,当前激活项重回
self.get("parent").set("activeItem", self);
} else {
return undefined;
}
return true;
},
hideParentMenusBuffer: function () {
var self = this, parentMenu = self.get("parent");
self.dismissTimer_ = S.later(function () {
var submenu = self,
popupmenu = self.get("menu");
while (popupmenu.get("autoHideOnMouseLeave")) {
// 取消高亮,buffer 隐藏子菜单
// 可能马上又移到上面,防止闪烁
// 相当于强制 submenu mouseleave
submenu.set("highlighted", false);
// 原来的 submenu 在高亮
// 表示越级选择 menu
if (parentMenu.get("highlightedItem") != submenu) {
break;
}
submenu = parentMenu.get("parent");
if (!submenu) {
break;
}
parentMenu = submenu.get("parent");
popupmenu = submenu.get("menu");
}
},
self.get("menuDelay") * 1000,
false,
self);
},
containsElement: function (element) {
var menu = getMenu(this);
return menu && menu.containsElement(element);
},
// 默认 addChild,这里里面的元素需要放到 menu 属性中
decorateChildrenInternal: function (UI, el) {
// 不能用 display:none
el.css("visibility", "hidden");
var self = this,
docBody = S.one(el[0].ownerDocument.body);
docBody.prepend(el);
var menu = new UI({
srcNode: el,
prefixCls: self.get("prefixCls")
});
self.setInternal("menu", menu);
},
destructor: function () {
var self = this,
parentMenu = self.get("parent"),
menu = getMenu(self);
self.clearSubMenuTimers();
if (menu && menu.__bindDocClickToHide) {
menu.__bindDocClickToHide = 0;
Event.remove(doc, "click", _onDocClick, self);
}
//当改菜单项所属的菜单隐藏后,该菜单项关联的子菜单也要隐藏
if (parentMenu) {
parentMenu.detach("hide", onParentHide, self);
}
if (menu && menu.destroy) {
menu.destroy();
}
}
},
{
ATTRS: {
* The delay before opening the sub menu in seconds. (This number is
* arbitrary, it would be good to get some user studies or a designer to play
* with some numbers).
* Defaults to: 0.15
* @cfg {Number} menuDelay
*/
* @ignore
*/
menuDelay: {
value: MENU_DELAY
},
* Menu config or instance.
* @cfg {KISSY.Menu|Object} menu
*/
* Menu config or instance.
* @property menu
* @type {KISSY.Menu|Object}
*/
* @ignore
*/
menu: {
setter: function (m) {
if (m instanceof Component.Controller) {
m.setInternal("parent", this);
}
}
},
defaultChildXClass: {
value: 'popupmenu'
},
decorateChildCls: {
valueFn: function () {
return this.get("prefixCls") + "popupmenu"
}
},
xrender: {
value: SubMenuRender
}
}
}, {
xclass: 'submenu',
priority: 20
});
// # -------------------------------- private start
function getMenu(self, init) {
var m = self.get("menu");
if (m && !m.isController) {
if (init) {
m = Component.create(m, self);
self.setInternal("menu", m);
} else {
return null;
}
}
return m;
}
function _onDocClick(e) {
var self = this,
menu = getMenu(self),
target = e.target,
parentMenu = self.get("parent"),
el = self.get("el");
// only hide this menu, if click outside this menu and this menu's submenus
if (!parentMenu.containsElement(target)) {
menu && menu.hide();
// sub menuitem should also hide
self.get("parent").set("highlightedItem", null);
}
}
function showMenu() {
var self = this,
menu = getMenu(self, 1);
if (menu) {
// 保证显示前已经绑定好事件
self.bindSubMenu();
var align = S.clone(menu.get("align"));
align.node = self.get("el");
align.points = align.points || ['tr', 'tl'];
menu.set("align", align);
menu.show();
/*
If activation of your menuitem produces a popup menu,
then the menuitem should have aria-haspopup set to the ID of the corresponding menu
to allow the assist technology to follow the menu hierarchy
and assist the user in determining context during menu navigation.
*/
self.get("el").attr("aria-haspopup",
menu.get("el").attr("id"));
}
}
function hideMenu() {
var menu = getMenu(this);
if (menu) {
menu.hide();
}
}
* Listens to the sub menus items and ensures that this menu item is selected
* while dismissing the others. This handles the case when the user mouses
* over other items on their way to the sub menu.
* @param e Highlight event to handle.
* @private
*/
function beforeSubMenuHighlightChange(e) {
var self = this;
if (e.newVal) {
self.clearSubMenuTimers();
// superclass(menuitem).handleMouseLeave 已经把自己 highlight 去掉了
// 导致本类 _onSetHighlighted 调用,又把子菜单隐藏了
self.get("parent").set("highlightedItem", self);
}
e.stopPropagation();
}
function onParentHide(e) {
var menu = getMenu(this);
menu && menu.hide();
e.stopPropagation();
}
// # ------------------------------------ private end
return SubMenu;
}, {
requires: ['event', 'component/base', './menuitem', './submenu-render']
});