'use strict'; /*@ Ox.Menu Menu Object ([options[, self]]) -> Menu Object options Options object element the element the menu is attached to id the menu id items array of menu items mainmenu the main menu this menu is part of, if any offset offset of the menu, in px left left top top parent the supermenu, if any selected the position of the selected item side open to 'bottom' or 'right' size 'large', 'medium' or 'small' self shared private variable change_groupId {id, value} checked item of a group has changed click_itemId item not belonging to a group was clicked click_menuId {id, value} item not belonging to a group was clicked deselect_menuId {id, value} item was deselected not needed, not implemented hide_menuId menu was hidden select_menuId {id, value} item was selected @*/ Ox.Menu = function(options, self) { self = self || {}; var that = Ox.Element({}, self) .defaults({ element: null, id: '', items: [], mainmenu: null, maxWidth: 0, offset: { left: 0, top: 0 }, parent: null, selected: -1, side: 'bottom', // FIXME: should be 'edge' size: 'medium' // fixme: remove }) .options(options || {}) .addClass( 'OxMenu Ox' + Ox.toTitleCase(self.options.side) + ' Ox' + Ox.toTitleCase(self.options.size) ) .bind({ click: click, mouseenter: mouseenter, mouseleave: mouseleave, mousemove: mousemove, mousewheel: mousewheel }) .bindEvent({ key_up: selectPreviousItem, key_down: selectNextItem, key_left: selectSupermenu, key_right: selectSubmenu, key_escape: hideMenu, key_enter: clickSelectedItem }); self.itemHeight = self.options.size == 'small' ? 12 : self.options.size == 'medium' ? 16 : 20; self.scrollSpeed = 1; // render that.items = []; that.submenus = {}; that.$scrollbars = []; that.$top = $('
') .addClass('OxTop') .appendTo(that.$element); that.$scrollbars.up = renderScrollbar('up') .appendTo(that.$element); that.$container = $('
') .addClass('OxContainer') .appendTo(that.$element); that.$content = $('') .addClass('OxContent') .appendTo(that.$container); renderItems(self.options.items); that.$scrollbars.down = renderScrollbar('down') .appendTo(that.$element); that.$bottom = $('
') .addClass('OxBottom') .appendTo(that.$element); function click(event) { var item, position, $target = $(event.target), $parent = $target.parent(); // necessary for highlight if ($parent.is('.OxCell')) { $target = $parent; $parent = $target.parent(); } if ($target.is('.OxCell')) { position = $parent.data('position'); item = that.items[position]; if (!item.options('disabled')) { clickItem(position); } else { that.hideMenu(); } } else { that.hideMenu(); } } function clickItem(position, files) { var item = that.items[position], group = item.options('group'), menu = self.options.mainmenu || self.options.parent || that, offset, toggled; that.hideMenu(); if (!item.options('items').length) { if (self.options.parent) { self.options.parent.hideMenu(true).triggerEvent('click'); } if (item.options('checked') !== null) { if (group) { offset = self.optionGroupOffset[group]; toggled = self.optionGroup[group].toggle(position - offset); if (toggled.length) { toggled.forEach(function(pos) { that.items[pos + offset].toggleChecked(); }); menu.triggerEvent('change', { id: item.options('group'), checked: self.optionGroup[group].checked().map(function(pos) { return { id: that.items[pos + offset].options('id'), title: Ox.isString(that.items[pos + offset].options('title')[0]) ? Ox.stripTags(that.items[pos + offset].options('title')[0]) : '' }; }) }); } } else { item.toggleChecked(); menu.triggerEvent('change', { checked: item.options('checked'), id: item.options('id'), title: Ox.isString(item.options('title')[0]) ? Ox.stripTags(item.options('title')[0]) : '' }); } } else { menu.triggerEvent('click', Ox.extend({ id: item.options('id'), title: Ox.stripTags(item.options('title')[0]) }, files ? {files: files} : {})); } if (item.options('title').length == 2) { item.toggleTitle(); } } } function clickLayer() { that.hideMenu(); } function clickSelectedItem() { // called on key.enter if (self.options.selected > -1) { clickItem(self.options.selected); } else { that.hideMenu(); } } function getElement(id) { // fixme: needed? return $('#' + Ox.toCamelCase(options.id + '/' + id)); } function getItemPositionById(id) { // fixme: this exists in ox.js by now var position; Ox.forEach(that.items, function(item, i) { if (item.options('id') == id) { position = i; Ox.break(); } }); return position; } function hideMenu() { // called on key_escape that.hideMenu(); } function isFirstEnabledItem() { var ret = true; Ox.forEach(that.items, function(item, i) { if (i < self.options.selected && !item.options('disabled')) { ret = false; Ox.break(); } }); return ret; } function isLastEnabledItem() { var ret = true; Ox.forEach(that.items, function(item, i) { if (i > self.options.selected && !item.options('disabled')) { ret = false; Ox.break(); } }); return ret; } function mouseenter() { that.gainFocus(); } function mouseleave() { if (self.options.selected > -1 && !that.items[self.options.selected].options('items').length) { selectItem(-1); } } function mousemove(event) { var item, position, $target = $(event.target), $parent = $target.parent(), $grandparent = $parent.parent(); if ($parent.is('.OxCell')) { $target = $parent; $parent = $target.parent(); } else if ($grandparent.is('.OxCell')) { $target = $grandparent; $parent = $target.parent(); } if ($target.is('.OxCell')) { position = $parent.data('position'); item = that.items[position]; if (!item.options('disabled') && position != self.options.selected) { selectItem(position); } } else { mouseleave(); } } function mousewheel(e, delta, deltaX, deltaY) { var $scrollbar; if (deltaY && !$(e.target).is('.OxScrollbar')) { $scrollbar = that.$scrollbars[deltaY < 0 ? 'down' : 'up']; Ox.loop(0, Math.abs(deltaY), function() { if ($scrollbar.is(':visible')) { $scrollbar.trigger('mouseenter').trigger('mouseleave'); } }); } } function renderItems(items) { var offset = 0; that.$content.empty(); scrollMenuUp(); self.optionGroup = {}; self.optionGroupOffset = {}; items.forEach(function(item, i) { if (item.group) { items[i] = item.items.map(function(v) { return Ox.extend(v, { group: item.group }); }); self.optionGroup[item.group] = new Ox.OptionGroup( items[i].filter(function(v) { return 'id' in v; }), 'min' in item ? item.min : 1, 'max' in item ? item.max : 1 ); self.optionGroupOffset[item.group] = offset; offset += items[i].length; } else if ('id' in item) { offset += 1; } }); items = Ox.flatten(items); that.items = []; items.forEach(function(item) { var position; if ('id' in item) { that.items.push( Ox.MenuItem(Ox.extend(item, { maxWidth: self.options.maxWidth, menu: that, position: position = that.items.length })) .data('position', position) .appendTo(that.$content) ); // fixme: jquery bug when passing {position: position}? does not return the object?; if (item.items) { that.submenus[item.id] = Ox.Menu({ element: that.items[position], id: Ox.toCamelCase(self.options.id + '/' + item.id), items: item.items, mainmenu: self.options.mainmenu, offset: { left: 0, top: -4 }, parent: that, side: 'right', size: self.options.size, }); } } else { that.$content.append(renderSpace()); that.$content.append(renderLine()); that.$content.append(renderSpace()); } }); if (!that.is(':hidden')) { that.hideMenu(); that.showMenu(); } } function renderLine() { return $('
').append( $('').append( $('
', { 'class': 'OxLine', colspan: 5 }) ); } function renderScrollbar(direction) { var interval, speed = direction == 'up' ? -1 : 1; return $('
', { 'class': 'OxScrollbar Ox' + Ox.toTitleCase(direction), html: Ox.UI.symbols['triangle_' + direction], click: function() { // fixme: do we need to listen to click event? return false; }, mousedown: function() { self.scrollSpeed = 2; return false; }, mouseenter: function() { self.scrollSpeed = 1; var $otherScrollbar = that.$scrollbars[direction == 'up' ? 'down' : 'up']; $(this).addClass('OxSelected'); if ($otherScrollbar.is(':hidden')) { $otherScrollbar.show(); that.$container.height(that.$container.height() - self.itemHeight); if (direction == 'down') { that.$content.css({ top: -self.itemHeight + 'px' }); } } scrollMenu(speed); interval = setInterval(function() { scrollMenu(speed); }, 100); }, mouseleave: function() { self.scrollSpeed = 1; $(this).removeClass('OxSelected'); clearInterval(interval); }, mouseup: function() { self.scrollSpeed = 1; return false; } }); } function renderSpace() { return $('
', {'class': 'OxSpace', colspan: 5}) ); } function scrollMenu(speed) { var containerHeight = that.$container.height(), contentHeight = that.$content.height(), top = parseInt(that.$content.css('top')) || 0, min = containerHeight - contentHeight + self.itemHeight, max = 0; top += speed * self.scrollSpeed * -self.itemHeight; if (top <= min) { top = min; that.$scrollbars.down.hide().trigger('mouseleave'); that.$container.height(containerHeight + self.itemHeight); that.items[that.items.length - 1].trigger('mouseover'); } else if (top >= max - self.itemHeight) { top = max; that.$scrollbars.up.hide().trigger('mouseleave'); that.$container.height(containerHeight + self.itemHeight); that.items[0].trigger('mouseover'); } that.$content.css({ top: top + 'px' }); } function scrollMenuUp() { if (that.$scrollbars.up.is(':visible')) { that.$content.css({ top: '0px' }); that.$scrollbars.up.hide(); if (that.$scrollbars.down.is(':hidden')) { that.$scrollbars.down.show(); } else { that.$container.height(that.$container.height() + self.itemHeight); } } } function selectItem(position) { var item; if (self.options.selected > -1) { item = that.items[self.options.selected]; if (item) { item.removeClass('OxSelected'); if (item.options('file')) { item.$button.blurButton(); that.bindEvent({key_enter: clickSelectedItem}) } } /* disabled that.triggerEvent('deselect', { id: item.options('id'), title: Ox.stripTags(item.options('title')[0]) }); */ } if (position > -1) { item = that.items[position]; Ox.forEach(that.submenus, function(submenu, id) { if (!submenu.is(':hidden')) { submenu.hideMenu(); Ox.break(); } }); item.options('items').length && that.submenus[item.options('id')].showMenu(); item.addClass('OxSelected'); if (item.options('file')) { item.$button.focusButton(); that.unbindEvent('key_enter'); } that.triggerEvent('select', { id: item.options('id'), title: Ox.isString(item.options('title')[0]) ? Ox.stripTags(item.options('title')[0]) : '' }); } self.options.selected = position; } function selectNextItem() { var offset, selected = self.options.selected; //Ox.Log('Menu', 'sNI', selected) if (!isLastEnabledItem()) { if (selected == -1) { scrollMenuUp(); } else { that.items[selected].removeClass('OxSelected'); } do { selected++; } while (that.items[selected].options('disabled')) selectItem(selected); offset = that.items[selected].offset().top + self.itemHeight - that.$container.offset().top - that.$container.height(); if (offset > 0) { if (that.$scrollbars.up.is(':hidden')) { that.$scrollbars.up.show(); that.$container.height(that.$container.height() - self.itemHeight); offset += self.itemHeight; } if (selected == that.items.length - 1) { that.$scrollbars.down.hide(); that.$container.height(that.$container.height() + self.itemHeight); } else { that.$content.css({ top: ((parseInt(that.$content.css('top')) || 0) - offset) + 'px' }); } } } } function selectPreviousItem() { var offset, selected = self.options.selected; //Ox.Log('Menu', 'sPI', selected) if (selected > - 1) { if (!isFirstEnabledItem()) { that.items[selected].removeClass('OxSelected'); do { selected--; } while (that.items[selected].options('disabled')) selectItem(selected); } offset = that.items[selected].offset().top - that.$container.offset().top; if (offset < 0) { if (that.$scrollbars.down.is(':hidden')) { that.$scrollbars.down.show(); that.$container.height(that.$container.height() - self.itemHeight); } if (selected == 0) { that.$scrollbars.up.hide(); that.$container.height(that.$container.height() + self.itemHeight); } that.$content.css({ top: ((parseInt(that.$content.css('top')) || 0) - offset) + 'px' }); } } } function selectSubmenu() { //Ox.Log('Menu', 'selectSubmenu', self.options.selected) if (self.options.selected > -1) { var submenu = that.submenus[that.items[self.options.selected].options('id')]; //Ox.Log('Menu', 'submenu', submenu, that.submenus); if (submenu && submenu.hasEnabledItems()) { submenu.gainFocus(); submenu.selectFirstItem(); } else if (self.options.mainmenu) { self.options.mainmenu.selectNextMenu(); } } else if (self.options.mainmenu) { self.options.mainmenu.selectNextMenu(); } } function selectSupermenu() { //Ox.Log('Menu', 'selectSupermenu', self.options.selected) if (self.options.parent) { self.options.selected > -1 && that.items[self.options.selected].trigger('mouseleave'); scrollMenuUp(); self.options.parent.gainFocus(); } else if (self.options.mainmenu) { self.options.mainmenu.selectPreviousMenu(); } } self.setOption = function(key, value) { if (key == 'items') { renderItems(value); } else if (key == 'selected') { that.$content.find('.OxSelected').removeClass('OxSelected'); selectItem(value); } } /*@ addItem @*/ that.addItem = function(item, position) { }; /*@ addItemAfter @*/ that.addItemAfter = function(item, id) { }; /*@ addItemBefore addItemBefore @*/ that.addItemBefore = function(item, id) { }; /*@ checkItem checkItem (id, checked) -> check item, checked can be undefined/true or false @*/ that.checkItem = function(id, checked) { Ox.Log('Menu', 'checkItem id', id) var group, ids = id.split('_'), item, offset, position, toggled; checked = Ox.isUndefined(checked) ? true : checked; if (ids.length == 1) { item = that.getItem(id); group = item.options('group'); if (group) { offset = self.optionGroupOffset[group]; position = getItemPositionById(id); toggled = self.optionGroup[item.options('group')].toggle(position - offset); if (toggled.length) { toggled.forEach(function(pos) { that.items[pos + offset].toggleChecked(); }); } } else { item.options({checked: checked}); } } else { that.submenus[ids.shift()].checkItem(ids.join('_')); } }; /*@ clickItem clickItem (position, files) -> click item at position @*/ that.clickItem = function(position, files) { clickItem(position, files); }; /*@ getItem getItem (id) -> get item @*/ that.getItem = function(id) { //Ox.Log('Menu', 'getItem id', id) var ids = id.split('_'), item; if (ids.length == 1) { Ox.forEach(that.items, function(v) { if (v.options('id') == id) { item = v; Ox.break(); } }); if (!item) { Ox.forEach(that.submenus, function(submenu) { item = submenu.getItem(id); item && Ox.break(); }); } } else { item = that.submenus[ids.shift()].getItem(ids.join('_')); } return item; }; /*@ getSubmenu getSubmenu (id) -> get submenu by id @*/ that.getSubmenu = function(id) { var ids = id.split('_'), submenu; if (ids.length == 1) { submenu = that.submenus[id]; } else { submenu = that.submenus[ids.shift()].getSubmenu(ids.join('_')); } //Ox.Log('Menu', 'getSubmenu', id, submenu); return submenu; } /*@ hasEnabledItems hasEditableItems () -> menu has editable items @*/ that.hasEnabledItems = function() { var ret = false; Ox.forEach(that.items, function(item) { if (!item.options('disabled')) { ret = true; Ox.break(); } }); return ret; }; /*@ hideMenu hideMenu () -> Menu Object @*/ that.hideMenu = function(hideParent) { if (that.is(':hidden')) { return; } Ox.forEach(that.submenus, function(submenu) { if (submenu.is(':visible')) { submenu.hideMenu(); Ox.break(); } }); selectItem(-1); scrollMenuUp(); that.$scrollbars.up.is(':visible') && that.$scrollbars.up.hide(); that.$scrollbars.down.is(':visible') && that.$scrollbars.down.hide(); if (self.options.parent) { //self.options.element.removeClass('OxSelected'); self.options.parent.options({ selected: -1 }); hideParent && self.options.parent.hideMenu(true); } that.$layer && that.$layer.hide(); that.hide().loseFocus().triggerEvent('hide'); return that; }; /*@ remove remove () -> remove menu @*/ self.superRemove = that.remove; that.remove = function() { Ox.forEach(that.submenus, function(submenu) { submenu.remove(); }); self.superRemove(); }; /*@ removeItem removeItem @*/ that.removeItem = function() { }; /*@ selectFirstItem selectFirstItem @*/ that.selectFirstItem = function() { selectNextItem(); return that; }; /*@ setItemTitle setItemTitle (id, title) -> set item title @*/ that.setItemTitle = function(id, title) { var item = getItem(id); item && item.options({title: title}); return that; }; /*@ showMenu showMenu () -> Menu Object @*/ that.showMenu = function() { if (!that.is(':hidden')) { return; } that.parent().length == 0 && that.appendTo(Ox.UI.$body); that.css({ left: '-1000px', top: '-1000px', }).show(); var offset = self.options.element.offset(), width = self.options.element.outerWidth(), height = self.options.element.outerHeight(), left = Ox.limit( offset.left + self.options.offset.left + (self.options.side == 'bottom' ? 0 : width), 0, Ox.UI.$window.width() - that.width() ), top = offset.top + self.options.offset.top + (self.options.side == 'bottom' ? height : 0), menuHeight = that.$content.outerHeight(), // fixme: why is outerHeight 0 when hidden? menuMaxHeight = Math.floor(Ox.UI.$window.height() - top - 16); if (self.options.parent) { if (menuHeight > menuMaxHeight) { top = Ox.limit(top - menuHeight + menuMaxHeight, self.options.parent.offset().top, top); menuMaxHeight = Math.floor(Ox.UI.$window.height() - top - 16); } } that.css({ left: left + 'px', top: top + 'px' }); if (menuHeight > menuMaxHeight) { that.$container.height(menuMaxHeight - self.itemHeight - 8); // margin that.$scrollbars.down.show(); } else { that.$container.height(menuHeight); } if (!self.options.parent) { that.gainFocus(); that.$layer = Ox.Layer({type: 'menu'}) .css({top: self.options.mainmenu ? '20px' : 0}) .bindEvent({click: clickLayer}) .show(); } return that; //that.triggerEvent('show'); }; /*@ toggleMenu toggleMenu @*/ that.toggleMenu = function() { return that.is(':hidden') ? that.showMenu() : that.hideMenu(); }; /*@ uncheckItem uncheckItem (id) -> uncheck item @*/ that.uncheckItem = function(id) { that.checkItem(id, false); }; return that; };