/** _onyx.Menu_ is a subkind of onyx.Popup that displays a list of onyx.MenuItems and looks like a popup menu. It is meant to be used in conjunction with an onyx.MenuDecorator. The decorator couples the menu with an activating control, which may be a button or any other control with an _onActivate_ event. When the control is activated, the menu shows itself in the correct position relative to the activator. {kind: "onyx.MenuDecorator", components: [ {content: "Show menu"}, {kind: "onyx.Menu", components: [ {content: "1"}, {content: "2"}, {classes: "onyx-menu-divider"}, {content: "3"}, ]} ]} A menu may be floated by setting the _floating_ property to true. When a menu is not floating (the default), it will scroll with the activating control, but may be obscured by surrounding content with a higher z-index. When floating, it will never be obscured, but it will not scroll with the activating button. */ enyo.kind({ name: "onyx.Menu", kind: "onyx.Popup", modal: true, defaultKind: "onyx.MenuItem", classes: "onyx-menu", showOnTop: false, handlers: { onActivate: "itemActivated", onRequestShowMenu: "requestMenuShow", onRequestHideMenu: "requestHide" }, itemActivated: function(inSender, inEvent) { inEvent.originator.setActive(false); return true; }, showingChanged: function() { this.inherited(arguments); this.adjustPosition(true); }, requestMenuShow: function(inSender, inEvent) { if (this.floating) { var n = inEvent.activator.hasNode(); if (n) { var r = this.activatorOffset = this.getPageOffset(n); this.applyPosition({top: r.top + (this.showOnTop ? 0 : r.height), left: r.left, width: r.width}); } } this.show(); return true; }, applyPosition: function(inRect) { var s = "" for (n in inRect) { s += (n + ":" + inRect[n] + (isNaN(inRect[n]) ? "; " : "px; ")); } this.addStyles(s); }, getPageOffset: function(inNode) { // getBoundingClientRect returns top/left values which are relative to the viewport and not absolute var r = inNode.getBoundingClientRect(); // IE8 doesn't return window.page{X/Y}Offset & r.{height/width} // FIXME: Perhaps use an alternate universal method instead of conditionals var pageYOffset = (window.pageYOffset === undefined) ? document.documentElement.scrollTop : window.pageYOffset; var pageXOffset = (window.pageXOffset === undefined) ? document.documentElement.scrollLeft : window.pageXOffset; var rHeight = (r.height === undefined) ? (r.bottom - r.top) : r.height; var rWidth = (r.width === undefined) ? (r.right - r.left) : r.width; return {top: r.top + pageYOffset, left: r.left + pageXOffset, height: rHeight, width: rWidth}; }, //* @protected /* Adjusts the menu position to fit inside the current window size. belowActivator determines whether to position the top of the menu below or on top of the activator */ adjustPosition: function(belowActivator) { if (this.showing && this.hasNode()) { this.removeClass("onyx-menu-up"); //reset the left position before we get the bounding rect for proper horizontal calculation this.floating ? enyo.noop : this.applyPosition({left: "auto"}); var b = this.node.getBoundingClientRect(); var bHeight = (b.height === undefined) ? (b.bottom - b.top) : b.height; var innerHeight = (window.innerHeight === undefined) ? document.documentElement.clientHeight : window.innerHeight; var innerWidth = (window.innerWidth === undefined) ? document.documentElement.clientWidth : window.innerWidth; //position the menu above the activator if it's getting cut off, but only if there's more room above this.menuUp = (b.top + bHeight > innerHeight) && ((innerHeight - b.bottom) < (b.top - bHeight)); this.addRemoveClass("onyx-menu-up", this.menuUp); //if floating, adjust the vertical positioning if (this.floating) { var r = this.activatorOffset; //if the menu doesn't fit below the activator, move it up if (this.menuUp) { this.applyPosition({top: (r.top - bHeight + (this.showOnTop ? r.height : 0)), bottom: "auto"}); } else { //if the top of the menu is above the top of the activator and there's room to move it down, do so if ((b.top < r.top) && (r.top + (belowActivator ? r.height : 0) + bHeight < innerHeight)) { this.applyPosition({top: r.top + (this.showOnTop ? 0 : r.height), bottom: "auto"}); } } } //adjust the horizontal positioning to keep the menu from being cut off on the right if ((b.right) > innerWidth) { if (this.floating){ this.applyPosition({left: r.left-(b.left + b.width - innerWidth)}); } else { this.applyPosition({left: -(b.right - innerWidth)}); } } } }, resizeHandler: function() { this.inherited(arguments); this.adjustPosition(true); }, requestHide: function(){ this.setShowing(false); } });