/** * Menu.js * * A menu object. This is the container to which you may add {@link Common.UI.MenuItem menu items}. * * Created by Alexander Yuzhin on 1/28/14 * Copyright (c) 2014 Ascensio System SIA. All rights reserved. * */ /** * Default template * * * * A useful classes of menu position * * - `'pull-right'` using for layout menu by right side of a parent * * * Example usage: * * new Common.UI.Menu({ * items: [ * { caption: 'item 1', value: 1 }, * { caption: 'item 1', value: 2 }, * { caption: '--' }, * { caption: 'item 1', value: 3 }, * ] * }) * * @property {Object} itemTemplate * * Default template for items * * * @property {Array} items * * Arrow of the {Common.UI.MenuItem} menu items * * */ if (Common === undefined) var Common = {}; define([ 'common/main/lib/extend/Bootstrap', 'common/main/lib/component/BaseView', 'common/main/lib/component/MenuItem', 'common/main/lib/component/Scroller' ], function () { 'use strict'; Common.UI.Menu = (function(){ var manager = (function(){ var active = [], menus = {}; return { register: function(menu) { menus[menu.id] = menu; menu .on('show:after', function(m) { active.push(m); }) .on('hide:after', function(m) { var index = active.indexOf(m); if (index > -1) active.splice(index, 1); }); }, unregister: function(menu) { var index = active.indexOf(menu); delete menus[menu.id]; if (index > -1) active.splice(index, 1); menu.off('show:after').off('hide:after'); }, hideAll: function() { Common.NotificationCenter.trigger('menumanager:hideall'); if (active && active.length > 0) { _.each(active, function(menu) { menu.hide(); }); return true; } return false; } } })(); return _.extend(Common.UI.BaseView.extend({ options : { cls : '', style : '', itemTemplate: null, items : [], menuAlign : 'tl-bl', menuAlignEl : null, offset : [0, 0], cyclic : true }, template: _.template([ '' ].join('')), initialize : function(options) { Common.UI.BaseView.prototype.initialize.call(this, options); var me = this; this.id = this.options.id || Common.UI.getId(); this.itemTemplate = this.options.itemTemplate || Common.UI.MenuItem.prototype.template; this.rendered = false; this.items = []; this.offset = [0, 0]; this.menuAlign = this.options.menuAlign; this.menuAlignEl = this.options.menuAlignEl; if (!this.options.cyclic) this.options.cls += ' no-cyclic'; _.each(this.options.items, function(item) { if (item instanceof Common.UI.MenuItem) { me.items.push(item) } else { me.items.push( new Common.UI.MenuItem(_.extend({ tagName : 'li', template: me.itemTemplate }, item)) ); } }); if (this.options.el) this.render(); manager.register(this); }, remove: function() { manager.unregister(this); Common.UI.BaseView.prototype.remove.call(this); }, render: function(parentEl) { var me = this; this.trigger('render:before', this); this.cmpEl = $(this.el); if (parentEl) { this.setElement(parentEl, false); if (!me.rendered) { this.cmpEl = $(this.template({ options : me.options })); parentEl.append(this.cmpEl); } } else { if (!me.rendered) { this.cmpEl = this.template({ options : me.options }); $(this.el).append(this.cmpEl); } } var rootEl = this.cmpEl.parent(), menuRoot = (rootEl.attr('role') === 'menu') ? rootEl : rootEl.find('[role=menu]'); if (menuRoot) { if (!me.rendered) { _.each(me.items || [], function(item) { menuRoot.append(item.render().el); item.on('click', _.bind(me.onItemClick, me)); item.on('toggle', _.bind(me.onItemToggle, me)); }); } menuRoot.css({ 'max-height': me.options.maxHeight||'none', position : 'fixed', right : 'auto', left : -1000, top : -1000 }); this.parentEl = menuRoot.parent(); this.parentEl.on('show.bs.dropdown', _.bind(me.onBeforeShowMenu, me)); this.parentEl.on('shown.bs.dropdown', _.bind(me.onAfterShowMenu, me)); this.parentEl.on('hide.bs.dropdown', _.bind(me.onBeforeHideMenu, me)); this.parentEl.on('hidden.bs.dropdown', _.bind(me.onAfterHideMenu, me)); this.parentEl.on('keydown.after.bs.dropdown', _.bind(me.onAfterKeydownMenu, me)); menuRoot.on('scroll', _.bind(me.onScroll, me)); menuRoot.hover( function(e) { me.isOver = true;}, function(e) { me.isOver = false; } ); } this.rendered = true; this.trigger('render:after', this); return this; }, isVisible: function() { return this.rendered && (this.cmpEl.is(':visible')); }, show: function() { if (this.rendered && this.parentEl && !this.parentEl.hasClass('open')) { this.cmpEl.dropdown('toggle'); } }, hide: function() { if (this.rendered && this.parentEl) { if ( this.parentEl.hasClass('open') ) this.cmpEl.dropdown('toggle'); else if (this.parentEl.hasClass('over')) this.parentEl.removeClass('over'); } }, insertItem: function(index, item) { var me = this, el = this.cmpEl; if (!(item instanceof Common.UI.MenuItem)) { item = new Common.UI.MenuItem(_.extend({ tagName : 'li', template: me.itemTemplate }, item)); } if (index < 0 || index >= me.items.length) me.items.push(item); else me.items.splice(index, 0, item); if (this.rendered) { var menuRoot = (el.attr('role') === 'menu') ? el : el.find('[role=menu]'); if (menuRoot) { if (index < 0) { menuRoot.append(item.render().el); } else if (index === 0) { menuRoot.prepend(item.render().el); } else { $('li:nth-child(' + (index+1) + ')', menuRoot).before(item.render().el); } item.on('click', _.bind(me.onItemClick, me)); item.on('toggle', _.bind(me.onItemToggle, me)); } } }, doLayout: function() { if (this.options.maxHeight > 0) { if (!this.rendered) { this.mustLayout = true; return; } var me = this, el = this.cmpEl; var menuRoot = (el.attr('role') === 'menu') ? el : el.find('[role=menu]'); if (!menuRoot.is(':visible')) { var pos = [menuRoot.css('left'), menuRoot.css('top')]; menuRoot.css({ left : '-1000px', top : '-1000px', display : 'block' }); } var $items = menuRoot.find('li'); if ($items.height() * $items.length > this.options.maxHeight) { var scroll = ''; menuRoot.prepend(scroll); scroll = ''; menuRoot.append(scroll); menuRoot.css({ 'box-shadow' : 'none', 'overflow-y' : 'hidden', 'padding-top' : '18px' // 'padding-bottom' : '18px' }); menuRoot.find('> li:last-of-type').css('margin-bottom',18); var addEvent = function( elem, type, fn ) { elem.addEventListener ? elem.addEventListener( type, fn, false ) : elem.attachEvent( "on" + type, fn ); }; var eventname=(/Firefox/i.test(navigator.userAgent))? 'DOMMouseScroll' : 'mousewheel'; addEvent(menuRoot[0], eventname, _.bind(this.onMouseWheel,this)); menuRoot.find('.menu-scroll').on('click', _.bind(this.onScrollClick, this)); } if (pos) { menuRoot.css({ display : '', left : pos[0], top : pos[1] }); } } }, addItem: function(item) { this.insertItem(-1, item); }, removeItem: function(item) { var me = this, index = me.items.indexOf(item); if (index > -1) { me.items.splice(index, 1); item.off('click').off('toggle'); item.remove(); } }, removeItems: function(from, len) { if (from > this.items.length-1) return; if (from+len>this.items.length) len = this.items.length - from; for (var i=from; i ul > .menu-scroll').length) { var el = this.$el.find('li .checked')[0]; if (el) { var offset = el.offsetTop - this.options.maxHeight / 2; this.scrollMenu(offset < 0 ? 0 : offset); } } }, onBeforeHideMenu: function(e) { this.trigger('hide:before', this, e); if (Common.UI.Scroller.isMouseCapture()) e.preventDefault(); }, onAfterHideMenu: function(e) { this.trigger('hide:after', this, e); }, onAfterKeydownMenu: function(e) { if (e.keyCode == Common.UI.Keys.RETURN) { var li = $(e.target).closest('li'); if (li.length<=0) li = $(e.target).parent().find('li .dataview'); if (li.length>0) li.click(); if (!li.hasClass('dropdown-submenu')) Common.UI.Menu.Manager.hideAll(); if ( $(e.currentTarget).closest('li').hasClass('dropdown-submenu')) { e.stopPropagation(); } } else if (e.keyCode == Common.UI.Keys.UP || e.keyCode == Common.UI.Keys.DOWN) { this.fromKeyDown = true; } else if (e.keyCode == Common.UI.Keys.ESC) { // Common.NotificationCenter.trigger('menu:afterkeydown', e); // return false; } }, onScroll: function(item, e) { if (this.fromKeyDown) { var menuRoot = (this.cmpEl.attr('role') === 'menu') ? this.cmpEl : this.cmpEl.find('[role=menu]'); menuRoot.find('.menu-scroll.top').css('top', menuRoot.scrollTop() + 'px'); menuRoot.find('.menu-scroll.bottom').css('bottom', (-menuRoot.scrollTop()) + 'px'); } }, onItemClick: function(item, e) { if (!item.menu) this.isOver = false; if (item.options.stopPropagation) { e.stopPropagation(); var me = this; _.delay(function(){ me.$el.parent().parent().find('[data-toggle=dropdown]').focus(); }, 10); return; } this.trigger('item:click', this, item, e); }, onItemToggle: function(item, state, e) { this.trigger('item:toggle', this, item, state, e); }, onScrollClick: function(e) { this.scrollMenu(/top/.test(e.currentTarget.className)); return false; }, onMouseWheel: function(e) { this.scrollMenu(((e.detail && -e.detail) || e.wheelDelta) > 0); }, scrollMenu: function(up) { this.fromKeyDown = false; var menuRoot = (this.cmpEl.attr('role') === 'menu') ? this.cmpEl : this.cmpEl.find('[role=menu]'), value = typeof(up)==='boolean' ? menuRoot.scrollTop() + (up ? -20 : 20) : up; menuRoot.scrollTop(value); menuRoot.find('.menu-scroll.top').css('top', menuRoot.scrollTop() + 'px'); menuRoot.find('.menu-scroll.bottom').css('bottom', (-menuRoot.scrollTop()) + 'px'); }, setOffset: function(offsetX, offsetY) { this.offset[0] = _.isUndefined(offsetX) ? this.offset[0] : offsetX; this.offset[1] = _.isUndefined(offsetY) ? this.offset[1] : offsetY; this.alignPosition(); }, getOffset: function() { return this.offset; }, alignPosition: function() { var menuRoot = (this.cmpEl.attr('role') === 'menu') ? this.cmpEl : this.cmpEl.find('[role=menu]'), menuParent = this.menuAlignEl || menuRoot.parent(), m = this.menuAlign.match(/^([a-z]+)-([a-z]+)/), offset = menuParent.offset(), docW = Math.min($(document).width(), $('body').width()), docH = $(document).height() - 10, // Yep, it's magic number menuW = menuRoot.outerWidth(), menuH = menuRoot.outerHeight(), parentW = menuParent.outerWidth(), parentH = menuParent.outerHeight(); var posMenu = { 'tl': [0, 0], 'bl': [0, menuH], 'tr': [menuW, 0], 'br': [menuW, menuH] }; var posParent = { 'tl': [0, 0], 'tr': [parentW, 0], 'bl': [0, parentH], 'br': [parentW, parentH] }; var left = offset.left - posMenu[m[1]][0] + posParent[m[2]][0] + this.offset[0]; var top = offset.top - posMenu[m[1]][1] + posParent[m[2]][1] + this.offset[1]; if (left + menuW > docW) if (menuParent.is('li.dropdown-submenu')) { left = offset.left - menuW + 2; } else { left = docW - menuW; } if (top + menuH > docH) top = docH - menuH; if (top < 0) top = 0; if (this.options.additionalAlign) this.options.additionalAlign.call(this, menuRoot, left, top); else menuRoot.css({left: left, top: top}); } }), { Manager: (function() { return manager; })() }) })(); });