oxjs/source/Ox.UI/js/Menu/Ox.Menu.js

688 lines
22 KiB
JavaScript

// vim: et:ts=4:sw=4:sts=4:ft=js
/**
options
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
parent the supermenu, if any
selected the position of the selected item
side open to 'bottom' or 'right'
size 'large', 'medium' or 'small'
events:
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) {
var self = self || {},
that = new Ox.Element({}, self)
.defaults({
element: null,
id: '',
items: [],
mainmenu: null,
offset: {
left: 0,
top: 0
},
parent: null,
selected: -1,
side: 'bottom',
size: 'medium',
})
.options(options || {})
.addClass(
'OxMenu Ox' + Ox.toTitleCase(self.options.side) +
' Ox' + Ox.toTitleCase(self.options.size)
)
.click(click)
.mouseenter(mouseenter)
.mouseleave(mouseleave)
.mousemove(mousemove)
.bindEvent({
key_up: selectPreviousItem,
key_down: selectNextItem,
key_left: selectSupermenu,
key_right: selectSubmenu,
key_escape: hideMenu,
key_enter: clickSelectedItem
}),
itemHeight = self.options.size == 'small' ? 12 : (self.options.size == 'medium' ? 16 : 20),
// menuHeight,
scrollSpeed = 1,
$item; // fixme: used?
// fixme: attach all private vars to self
// construct
that.items = [];
that.submenus = {};
that.$scrollbars = [];
that.$top = $('<div>')
.addClass('OxTop')
.appendTo(that.$element);
that.$scrollbars.up = constructScrollbar('up')
.appendTo(that.$element);
that.$container = $('<div>')
.addClass('OxContainer')
.appendTo(that.$element);
that.$content = $('<table>')
.addClass('OxContent')
.appendTo(that.$container);
constructItems(self.options.items);
that.$scrollbars.down = constructScrollbar('down')
.appendTo(that.$element);
that.$bottom = $('<div>')
.addClass('OxBottom')
.appendTo(that.$element);
that.$layer = $('<div>')
.addClass(self.options.mainmenu ? 'OxMainMenuLayer' : 'OxMenuLayer')
.click(click);
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) {
var item = that.items[position],
menu = self.options.mainmenu || self.options.parent || that,
toggled;
that.hideMenu();
if (!item.options('items').length) {
if (that.options('parent')) {
that.options('parent').hideMenu().triggerEvent('click');
}
if (item.options('checked') !== null) {
if (item.options('group')) {
//Ox.print('has group', item.options('group'))
toggled = self.optionGroups[item.options('group')].toggle(position);
//Ox.print('toggled', toggled)
if (toggled.length) {
toggled.forEach(function(pos) {
that.items[pos].toggleChecked();
});
//Ox.print('--triggering change event--');
menu.triggerEvent('change', {
id: item.options('group'),
checked: $.map(self.optionGroups[item.options('group')].checked(), function(v, i) {
return {
id: that.items[v].options('id'),
title: Ox.stripTags(that.items[v].options('title')[0])
};
})
});
}
} else {
item.toggleChecked();
menu.triggerEvent('change', {
checked: item.options('checked'),
id: item.options('id'),
title: Ox.stripTags(item.options('title')[0])
});
}
} else {
menu.triggerEvent('click', {
id: item.options('id'),
title: Ox.stripTags(item.options('title')[0])
});
}
if (item.options('title').length == 2) {
item.toggleTitle();
}
}
}
function clickSelectedItem() {
// called on key.enter
if (self.options.selected > -1) {
clickItem(self.options.selected);
} else {
that.hideMenu();
}
}
function constructItems(items) {
that.$content.empty();
scrollMenuUp();
self.optionGroups = {};
items.forEach(function(item, i) {
if (item.group) {
items[i] = $.map(item.items, function(v, i) {
return $.extend(v, {
group: item.group
});
});
self.optionGroups[item.group] = new Ox.OptionGroup(
items[i],
'min' in item ? item.min : 1,
'max' in item ? item.max : 1
);
}
});
items = Ox.flatten(items);
that.items = [];
items.forEach(function(item) {
var position;
if ('id' in item) {
that.items.push(new Ox.MenuItem($.extend(item, {
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] = new 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(constructSpace());
that.$content.append(constructLine());
that.$content.append(constructSpace());
}
});
if (!that.is(':hidden')) {
that.hideMenu();
that.showMenu();
}
}
function constructLine() {
return $('<tr>').append(
$('<td>', {
'class': 'OxLine',
colspan: 5
})
);
}
function constructScrollbar(direction) {
var interval,
speed = direction == 'up' ? -1 : 1;
return $('<div/>', {
'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() {
scrollSpeed = 2;
return false;
},
mouseenter: function() {
var $otherScrollbar = that.$scrollbars[direction == 'up' ? 'down' : 'up'];
$(this).addClass('OxSelected');
if ($otherScrollbar.is(':hidden')) {
$otherScrollbar.show();
that.$container.height(that.$container.height() - itemHeight);
if (direction == 'down') {
that.$content.css({
top: -itemHeight + 'px'
});
}
}
scrollMenu(speed);
interval = setInterval(function() {
scrollMenu(speed);
}, 100);
},
mouseleave: function() {
$(this).removeClass('OxSelected');
clearInterval(interval);
},
mouseup: function() {
scrollSpeed = 1;
return false;
}
});
}
function constructSpace() {
return $('<tr>').append(
$('<td>', {
'class': 'OxSpace',
colspan: 5
})
);
}
function getElement(id) {
// fixme: needed?
return $('#' + Ox.toCamelCase(options.id + '/' + id));
}
function getItemPositionById(id) {
var position;
Ox.forEach(that.items, function(item, i) {
if (item.options('id') == id) {
position = i;
return false;
}
});
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')) {
return ret = false;
}
});
return ret;
}
function isLastEnabledItem() {
var ret = true;
Ox.forEach(that.items, function(item, i) {
if (i > self.options.selected && !item.options('disabled')) {
return ret = false;
}
});
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();
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') && position != self.options.selected) {
selectItem(position);
}
} else {
mouseleave();
}
}
function scrollMenu(speed) {
var containerHeight = that.$container.height(),
contentHeight = that.$content.height(),
top = parseInt(that.$content.css('top')) || 0,
min = containerHeight - contentHeight + itemHeight,
max = 0;
top += speed * scrollSpeed * -itemHeight;
if (top <= min) {
top = min;
that.$scrollbars.down.hide().trigger('mouseleave');
that.$container.height(containerHeight + itemHeight);
that.items[that.items.length - 1].trigger('mouseover');
} else if (top >= max - itemHeight) {
top = max;
that.$scrollbars.up.hide().trigger('mouseleave');
that.$container.height(containerHeight + 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() + itemHeight);
}
}
}
function selectItem(position) {
var item;
if (self.options.selected > -1) {
//Ox.print('s.o.s', self.options.selected, that.items)
item = that.items[self.options.selected]
item.removeClass('OxSelected');
/* 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();
return false;
}
});
item.options('items').length && that.submenus[item.options('id')].showMenu(); // fixme: do we want to switch to this style?
item.addClass('OxSelected');
/* disabled
that.triggerEvent('select', {
id: item.options('id'),
title: Ox.stripTags(item.options('title')[0])
});
*/
}
self.options.selected = position;
}
function selectNextItem() {
var offset,
selected = self.options.selected;
//Ox.print('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 + 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() - itemHeight);
offset += itemHeight;
}
if (selected == that.items.length - 1) {
that.$scrollbars.down.hide();
that.$container.height(that.$container.height() + itemHeight);
} else {
that.$content.css({
top: ((parseInt(that.$content.css('top')) || 0) - offset) + 'px'
});
}
}
}
}
function selectPreviousItem() {
var offset,
selected = self.options.selected;
//Ox.print('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() - itemHeight);
}
if (selected == 0) {
that.$scrollbars.up.hide();
that.$container.height(that.$container.height() + itemHeight);
}
that.$content.css({
top: ((parseInt(that.$content.css('top')) || 0) - offset) + 'px'
});
}
}
}
function selectSubmenu() {
//Ox.print('selectSubmenu', self.options.selected)
if (self.options.selected > -1) {
var submenu = that.submenus[that.items[self.options.selected].options('id')];
//Ox.print('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.print('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.onChange = function(key, value) {
if (key == 'items') {
constructItems(value);
} else if (key == 'selected') {
that.$content.find('.OxSelected').removeClass('OxSelected');
selectItem(value);
}
}
that.addItem = function(item, position) {
};
that.addItemAfter = function(item, id) {
};
that.addItemBefore = function(item, id) {
};
that.checkItem = function(id) {
var item = that.getItem(id);
if (item.options('group')) {
var position = getItemPositionById(id),
toggled = self.optionGroups[item.options('group')].toggle(position);
if (toggled.length) {
toggled.forEach(function(pos) {
that.items[pos].toggleChecked();
});
}
} else {
item.options({
checked: true
});
}
};
that.getItem = function(id) {
//Ox.print('id', id)
var ids = id.split('_'),
item;
if (ids.length == 1) {
Ox.forEach(that.items, function(v) {
if (v.options('id') == id) {
item = v;
return false;
}
});
if (!item) {
Ox.forEach(that.submenus, function(submenu) {
item = submenu.getItem(id);
return !item;
});
}
} else {
item = that.submenus[ids.shift()].getItem(ids.join('_'));
}
return item;
};
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.print('getSubmenu', id, submenu);
return submenu;
}
that.hasEnabledItems = function() {
var ret = false;
Ox.forEach(that.items, function(item) {
if (!item.options('disabled')) {
return ret = true;
}
});
return ret;
};
that.hideMenu = function() {
if (that.is(':hidden')) {
return;
}
Ox.forEach(that.submenus, function(submenu) {
if (submenu.is(':visible')) {
submenu.hideMenu();
return false;
}
});
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
});
}
that.hide()
.loseFocus()
.triggerEvent('hide');
that.$layer.hide();
return that;
};
that.removeItem = function() {
};
that.selectFirstItem = function() {
selectNextItem();
};
that.showMenu = function() {
if (!that.is(':hidden')) {
return;
}
if (!self.options.parent && !that.$layer.parent().length) {
that.$layer.appendTo(Ox.UI.$body);
}
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 - itemHeight - 8); // margin
that.$scrollbars.down.show();
} else {
that.$container.height(menuHeight);
}
if (!self.options.parent) {
that.gainFocus();
}
that.$layer.show();
return that;
//that.triggerEvent('show');
};
that.toggleMenu = function() {
that.is(':hidden') ? that.showMenu() : that.hideMenu();
};
return that;
};