mirror of
https://github.com/JannisX11/blockbench.git
synced 2024-11-21 01:13:37 +08:00
64caffaa28
Fix hover issue with menu item side menu
625 lines
17 KiB
JavaScript
625 lines
17 KiB
JavaScript
var open_menu = null;
|
|
|
|
function handleMenuOverflow(node) {
|
|
node = node.get(0);
|
|
if (!node) return;
|
|
function offset(amount) {
|
|
let top = parseInt(node.style.top);
|
|
let offset = top - $(node).offset().top;
|
|
top = Math.clamp(
|
|
top + amount,
|
|
window.innerHeight - node.clientHeight + offset,
|
|
offset + 26
|
|
);
|
|
node.style.top = `${top}px`;
|
|
}
|
|
if (Blockbench.isTouch) {
|
|
node.addEventListener('touchstart', e1 => {
|
|
e1.stopPropagation();
|
|
convertTouchEvent(e1);
|
|
let last_y = e1.clientY;
|
|
let move = e2 => {
|
|
convertTouchEvent(e2);
|
|
offset(e2.clientY - last_y);
|
|
last_y = e2.clientY;
|
|
}
|
|
let stop = e2 => {
|
|
document.removeEventListener('touchmove', move);
|
|
document.removeEventListener('touchend', stop);
|
|
}
|
|
document.addEventListener('touchmove', move);
|
|
document.addEventListener('touchend', stop);
|
|
})
|
|
}
|
|
node.addEventListener('wheel', e => {
|
|
e.stopPropagation();
|
|
offset(-e.deltaY);
|
|
})
|
|
}
|
|
class Menu {
|
|
constructor(id, structure, options) {
|
|
if (typeof id !== 'string') {
|
|
options = structure;
|
|
structure = id;
|
|
}
|
|
this.id = typeof id == 'string' ? id : '';
|
|
this.children = [];
|
|
this.node = document.createElement('ul');
|
|
this.node.classList.add('contextMenu');
|
|
this.structure = structure;
|
|
this.options = options || {};
|
|
this.onOpen = this.options.onOpen;
|
|
this.onClose = this.options.onClose;
|
|
}
|
|
hover(node, event, expand) {
|
|
if (event) event.stopPropagation()
|
|
$(open_menu.node).find('li.focused').removeClass('focused')
|
|
$(open_menu.node).find('li.opened').removeClass('opened')
|
|
var obj = $(node)
|
|
obj.addClass('focused')
|
|
obj.parents('li.parent, li.hybrid_parent').addClass('opened')
|
|
|
|
if (obj.hasClass('parent') || (expand && obj.hasClass('hybrid_parent'))) {
|
|
var childlist = obj.find('> ul.contextMenu.sub')
|
|
|
|
if (expand) obj.addClass('opened');
|
|
|
|
var p_width = obj.outerWidth()
|
|
childlist.css('left', p_width + 'px')
|
|
var el_width = childlist.width()
|
|
var offset = childlist.offset()
|
|
var el_height = childlist.height()
|
|
|
|
if (offset.left + el_width > window.innerWidth) {
|
|
if (Blockbench.isMobile) {
|
|
childlist.css('visibility', 'hidden');
|
|
setTimeout(() => {
|
|
childlist.css('left', 0);
|
|
childlist.css('visibility', 'visible');
|
|
}, 100);
|
|
} else {
|
|
childlist.css('left', -el_width + 'px')
|
|
}
|
|
}
|
|
|
|
let window_height = window.innerHeight - 26;
|
|
|
|
if (el_height > window_height) {
|
|
childlist.css('margin-top', '0').css('top', '0')
|
|
childlist.css('top', (-childlist.offset().top + 26) + 'px')
|
|
handleMenuOverflow(childlist);
|
|
|
|
} else if (offset.top + el_height > window_height) {
|
|
childlist.css('margin-top', 26-childlist.height() + 'px')
|
|
if (childlist.offset().top < 26) {
|
|
childlist.offset({top: 26})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
reveal(path) {
|
|
|
|
}
|
|
keyNavigate(e) {
|
|
var scope = this;
|
|
var used;
|
|
var obj = $(this.node)
|
|
if (e.which >= 37 && e.which <= 40) {
|
|
|
|
let is_menu_bar = scope.type === 'bar_menu' && e.which%2;
|
|
if (obj.find('li.focused').length || is_menu_bar) {
|
|
var old = obj.find('li.focused'), next;
|
|
switch (e.which) {
|
|
case 37: next = old.parent('ul').parent('li'); break;//<
|
|
case 38: next = old.prevAll('li:not(.menu_separator)').first(); break;//UP
|
|
case 39: next = old.find('ul li:first-child'); break;//>
|
|
case 40: next = old.nextAll('li:not(.menu_separator)').first(); break;//DOWN
|
|
}
|
|
|
|
if (!next.length && e.which%2 == 0) {
|
|
var siblings = old.siblings('li:not(.menu_separator)')
|
|
if (e.which === 38) {
|
|
next = siblings.last()
|
|
} else {
|
|
next = siblings.first()
|
|
}
|
|
}
|
|
if (next && next.length) {
|
|
old.removeClass('focused')
|
|
scope.hover(next.get(0))
|
|
} else if (is_menu_bar) {
|
|
var index = MenuBar.keys.indexOf(scope.id)
|
|
index += (e.which == 39 ? 1 : -1)
|
|
if (index < 0) {
|
|
index = MenuBar.keys.length-1
|
|
} else if (index >= MenuBar.keys.length) {
|
|
index = 0;
|
|
}
|
|
MenuBar.menus[MenuBar.keys[index]].open()
|
|
}
|
|
} else {
|
|
obj.find('> li:first-child').addClass('focused')
|
|
}
|
|
used = true;
|
|
} else if (Keybinds.extra.confirm.keybind.isTriggered(e)) {
|
|
obj.find('li.focused').click()
|
|
if (scope && !this.options.keep_open) {
|
|
//scope.hide()
|
|
}
|
|
used = true;
|
|
} else if (Keybinds.extra.cancel.keybind.isTriggered(e)) {
|
|
scope.hide()
|
|
used = true;
|
|
}
|
|
return used;
|
|
}
|
|
open(position, context) {
|
|
if (this.onOpen) this.onOpen(position, context);
|
|
|
|
if (position && position.changedTouches) {
|
|
convertTouchEvent(position);
|
|
}
|
|
let last_context = context;
|
|
var scope = this;
|
|
var ctxmenu = $(this.node)
|
|
if (open_menu) {
|
|
open_menu.hide()
|
|
}
|
|
$('body').append(ctxmenu)
|
|
|
|
ctxmenu.children().detach()
|
|
|
|
function createChildList(object, node, list) {
|
|
if (!list && typeof object.children == 'function') {
|
|
list = object.children(context)
|
|
} else if (!list) {
|
|
list = object.children
|
|
}
|
|
node.find('ul.contextMenu.sub').detach();
|
|
if (list.length) {
|
|
var childlist = $('<ul class="contextMenu sub"></ul>')
|
|
|
|
populateList(list, childlist, object.searchable);
|
|
|
|
if ((typeof object.click == 'function' || object instanceof Tool) && (object instanceof Action == false || object.side_menu)) {
|
|
if (node.find('> .menu_more_button').length == 0) {
|
|
node.addClass('hybrid_parent');
|
|
let more_button = Interface.createElement('div', {class: 'menu_more_button'}, Blockbench.getIconNode('more_horiz'));
|
|
node.append(more_button);
|
|
more_button.addEventListener('mouseenter', e => {
|
|
scope.hover(node.get(0), e, true);
|
|
})
|
|
more_button.addEventListener('mouseleave', e => {
|
|
if (node.is(':hover') && !childlist.is(':hover')) {
|
|
scope.hover(node, e);
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
node.addClass('parent');
|
|
}
|
|
node.append(childlist)
|
|
return childlist.children().length;
|
|
}
|
|
return 0;
|
|
}
|
|
function populateList(list, menu_node, searchable) {
|
|
|
|
if (searchable) {
|
|
let input = Interface.createElement('input', {type: 'text', placeholder: tl('generic.search'), inputmode: 'search'});
|
|
let search_button = Interface.createElement('div', {}, Blockbench.getIconNode('search'));
|
|
let search_bar = Interface.createElement('li', {class: 'menu_search_bar'}, [input, search_button]);
|
|
menu_node.append(search_bar);
|
|
menu_node.append(Interface.createElement('li', {class: 'menu_separator'}));
|
|
|
|
let object_list = [];
|
|
list.forEach(function(s2, i) {
|
|
let jq_node = getEntry(s2, menu_node);
|
|
if (!jq_node) return;
|
|
object_list.push({
|
|
object: s2,
|
|
node: jq_node[0] || jq_node,
|
|
id: s2.id,
|
|
name: s2.name,
|
|
description: s2.description,
|
|
})
|
|
})
|
|
search_button.onclick = (e) => {
|
|
input.value = '';
|
|
input.oninput(e);
|
|
}
|
|
input.oninput = (e) => {
|
|
let search_term = input.value.toUpperCase();
|
|
search_button.firstElementChild.replaceWith(Blockbench.getIconNode(search_term ? 'clear' : 'search'));
|
|
|
|
object_list.forEach(item => {
|
|
$(item.node).detach();
|
|
})
|
|
object_list.forEach(item => {
|
|
if (
|
|
typeof item.object == 'string' ||
|
|
item.object.always_show ||
|
|
(item.id && item.id.toUpperCase().includes(search_term)) ||
|
|
(item.name && item.name.toUpperCase().includes(search_term)) ||
|
|
(item.description && item.description.toUpperCase().includes(search_term))
|
|
) {
|
|
menu_node.append(item.node);
|
|
}
|
|
})
|
|
}
|
|
if (menu_node == ctxmenu) {
|
|
input.focus();
|
|
}
|
|
|
|
} else {
|
|
list.forEach((object) => {
|
|
getEntry(object, menu_node);
|
|
})
|
|
}
|
|
let nodes = menu_node.children();
|
|
if (nodes.length && nodes.last().hasClass('menu_separator')) {
|
|
nodes.last().remove();
|
|
}
|
|
|
|
if (!nodes.toArray().find(node => node.classList.contains('parent') || node.classList.contains('hybrid_parent'))) {
|
|
menu_node.addClass('scrollable');
|
|
}
|
|
}
|
|
|
|
function getEntry(s, parent) {
|
|
|
|
if (s.context) {
|
|
last_context = context;
|
|
context = s.context;
|
|
}
|
|
let scope_context = context;
|
|
var entry;
|
|
if (s === '_') {
|
|
entry = new MenuSeparator().menu_node
|
|
var last = parent.children().last()
|
|
if (last.length && !last.hasClass('menu_separator')) {
|
|
parent.append(entry)
|
|
}
|
|
return entry;
|
|
}
|
|
if (typeof s == 'string' && BarItems[s]) {
|
|
s = BarItems[s];
|
|
}
|
|
if (!Condition(s.condition, scope_context)) return;
|
|
|
|
if (s instanceof Action) {
|
|
|
|
entry = $(s.menu_node)
|
|
|
|
entry.removeClass('focused')
|
|
entry.off('click')
|
|
entry.off('mouseenter mousedown')
|
|
entry.on('mouseenter mousedown', function(e) {
|
|
if (this == e.target) {
|
|
scope.hover(this, e)
|
|
}
|
|
})
|
|
//Submenu
|
|
if (typeof s.children == 'function' || typeof s.children == 'object') {
|
|
createChildList(s, entry)
|
|
} else {
|
|
entry.on('click', (e) => {
|
|
if (!(e.target == entry[0] || e.target.parentElement == entry[0])) return;
|
|
s.trigger(e)
|
|
});
|
|
if (s.side_menu instanceof Menu) {
|
|
let content_list = typeof s.side_menu.structure == 'function' ? s.side_menu.structure(scope_context) : s.side_menu.structure;
|
|
createChildList(s, entry, content_list);
|
|
|
|
} else if (s.side_menu instanceof Dialog) {
|
|
createChildList(s, entry, [
|
|
{
|
|
name: 'menu.options',
|
|
icon: 'web_asset',
|
|
click() {
|
|
s.side_menu.show();
|
|
}
|
|
}
|
|
]);
|
|
}
|
|
}
|
|
|
|
parent.append(entry)
|
|
|
|
} else if (s instanceof BarSelect) {
|
|
|
|
if (typeof s.icon === 'function') {
|
|
var icon = Blockbench.getIconNode(s.icon(scope_context), s.color)
|
|
} else {
|
|
var icon = Blockbench.getIconNode(s.icon, s.color)
|
|
}
|
|
entry = $(Interface.createElement('li', {title: s.description && tl(s.description), menu_item: s.id}, Interface.createElement('span', {}, tl(s.name))));
|
|
entry.prepend(icon)
|
|
|
|
//Submenu
|
|
var children = [];
|
|
for (var key in s.options) {
|
|
|
|
let val = s.options[key];
|
|
if (val) {
|
|
(function() {
|
|
var save_key = key;
|
|
children.push({
|
|
name: s.getNameFor(key),
|
|
id: key,
|
|
icon: val.icon || ((s.value == save_key) ? 'far.fa-dot-circle' : 'far.fa-circle'),
|
|
condition: val.condition,
|
|
click: (e) => {
|
|
s.set(save_key);
|
|
if (s.onChange) {
|
|
s.onChange(s, e);
|
|
}
|
|
}
|
|
})
|
|
})()
|
|
}
|
|
}
|
|
|
|
let child_count = createChildList({children}, entry)
|
|
|
|
if (child_count !== 0 || typeof s.click === 'function') {
|
|
parent.append(entry)
|
|
}
|
|
entry.mouseenter(function(e) {
|
|
scope.hover(this, e)
|
|
})
|
|
|
|
/*} else if (s instanceof NumSlider) {
|
|
|
|
let icon = Blockbench.getIconNode(s.icon, s.color);
|
|
let numeric_input = new Interface.CustomElements.NumericInput(s.id, {
|
|
value: s.get(),
|
|
min: s.settings?.min, max: s.settings?.max,
|
|
onChange(value) {
|
|
if (typeof s.onBefore === 'function') {
|
|
s.onBefore()
|
|
}
|
|
s.change(() => value);
|
|
if (typeof s.onAfter === 'function') {
|
|
s.onAfter()
|
|
}
|
|
s.update();
|
|
}
|
|
});
|
|
entry = Interface.createElement('li', {title: s.description && tl(s.description), menu_item: s.id}, [
|
|
Interface.createElement('span', {}, tl(s.name)),
|
|
numeric_input.node
|
|
]);
|
|
entry.prepend(icon);
|
|
|
|
parent.append(entry);
|
|
|
|
$(entry).mouseenter(function(e) {
|
|
scope.hover(this, e)
|
|
})
|
|
*/
|
|
} else if (s instanceof HTMLElement) {
|
|
parent.append(s);
|
|
|
|
} else if (typeof s === 'object') {
|
|
|
|
let child_count;
|
|
if (typeof s.icon === 'function') {
|
|
var icon = Blockbench.getIconNode(s.icon(scope_context), s.color)
|
|
} else {
|
|
var icon = Blockbench.getIconNode(s.icon, s.color)
|
|
}
|
|
entry = $(Interface.createElement('li', {title: s.description && tl(s.description), menu_item: s.id}, Interface.createElement('span', {}, tl(s.name))));
|
|
entry.prepend(icon);
|
|
if (s.keybind) {
|
|
let label = document.createElement('label');
|
|
label.classList.add('keybinding_label')
|
|
label.innerText = s.keybind || '';
|
|
entry.append(label);
|
|
}
|
|
if (typeof s.click === 'function') {
|
|
entry.on('click', e => {
|
|
if (e.target == entry.get(0)) {
|
|
s.click(scope_context, e)
|
|
}
|
|
})
|
|
}
|
|
//Submenu
|
|
if (typeof s.children == 'function' || typeof s.children == 'object') {
|
|
child_count = createChildList(s, entry);
|
|
}
|
|
if (child_count !== 0 || typeof s.click === 'function') {
|
|
parent.append(entry)
|
|
}
|
|
entry.mouseenter(function(e) {
|
|
scope.hover(this, e)
|
|
})
|
|
}
|
|
//Highlight
|
|
if (scope.highlight_action == s && entry) {
|
|
let obj = entry;
|
|
while (obj[0] && obj[0].nodeName == 'LI') {
|
|
obj.addClass('highlighted');
|
|
obj = obj.parent().parent();
|
|
}
|
|
}
|
|
if (s.context && last_context != context) context = last_context;
|
|
return entry;
|
|
}
|
|
|
|
let content_list = typeof this.structure == 'function' ? this.structure(context) : this.structure;
|
|
populateList(content_list, ctxmenu, this.options.searchable);
|
|
|
|
var el_width = ctxmenu.width()
|
|
var el_height = ctxmenu.height()
|
|
let window_height = window.innerHeight - 26;
|
|
|
|
if (position && position.clientX !== undefined) {
|
|
var offset_left = position.clientX
|
|
var offset_top = position.clientY+1
|
|
|
|
} else if (position == document.body) {
|
|
var offset_left = (document.body.clientWidth-el_width)/2
|
|
var offset_top = (document.body.clientHeight-el_height)/2
|
|
|
|
} else if (position == 'mouse') {
|
|
var offset_left = mouse_pos.x;
|
|
var offset_top = mouse_pos.y;
|
|
|
|
} else {
|
|
if (!position && scope.type === 'bar_menu') {
|
|
position = scope.label
|
|
} else if (position && position.parentElement.classList.contains('tool')) {
|
|
position = position.parentElement;
|
|
}
|
|
var offset_left = $(position).offset().left;
|
|
var offset_top = $(position).offset().top + position.offsetHeight;
|
|
}
|
|
|
|
if (offset_left > window.innerWidth - el_width) {
|
|
offset_left -= el_width
|
|
if (position && position.clientWidth) offset_left += position.clientWidth;
|
|
if (offset_left < 0) offset_left = 0;
|
|
}
|
|
if (offset_top > window_height - el_height ) {
|
|
if (el_height < offset_top - 50) {
|
|
// Snap to element top
|
|
offset_top -= el_height;
|
|
if (position instanceof HTMLElement) {
|
|
offset_top -= position.clientHeight;
|
|
}
|
|
} else {
|
|
// Move up
|
|
offset_top = window_height - el_height;
|
|
}
|
|
}
|
|
offset_top = Math.max(offset_top, 26);
|
|
|
|
ctxmenu.css('left', offset_left+'px')
|
|
ctxmenu.css('top', offset_top +'px')
|
|
|
|
if (el_height > window_height) {
|
|
handleMenuOverflow(ctxmenu);
|
|
}
|
|
|
|
scope.node.onclick = (ev) => {
|
|
if (
|
|
ev.target.classList.contains('parent') ||
|
|
(ev.target.parentNode && ev.target.parentNode.classList.contains('parent')) ||
|
|
ev.target.classList.contains('menu_search_bar') ||
|
|
(ev.target.parentNode && ev.target.parentNode.classList.contains('menu_search_bar'))
|
|
) {} else {
|
|
if (this.options.keep_open) {
|
|
this.hide()
|
|
this.open(position, context);
|
|
} else {
|
|
this.hide()
|
|
}
|
|
}
|
|
}
|
|
|
|
if (scope.type === 'bar_menu') {
|
|
MenuBar.open = scope
|
|
$(scope.label).addClass('opened')
|
|
}
|
|
open_menu = scope;
|
|
return scope;
|
|
}
|
|
show(...args) {
|
|
return this.open(...args);
|
|
}
|
|
hide() {
|
|
if (this.onClose) this.onClose();
|
|
$(this.node).find('li.highlighted').removeClass('highlighted');
|
|
$(this.node).detach()
|
|
open_menu = null;
|
|
return this;
|
|
}
|
|
conditionMet() {
|
|
return Condition(this.condition);
|
|
}
|
|
addAction(action, path) {
|
|
if (this.structure instanceof Array == false) return;
|
|
if (path === undefined) path = '';
|
|
if (typeof path !== 'string') path = path.toString();
|
|
var track = path.split('.')
|
|
|
|
function traverse(arr, layer) {
|
|
if (track.length === layer || track[layer] === '' || !isNaN(parseInt(track[layer]))) {
|
|
var index = arr.length;
|
|
if (track[layer] !== '' && track.length !== layer) {
|
|
index = parseInt(track[layer])
|
|
}
|
|
arr.splice(index, 0, action)
|
|
} else {
|
|
for (var i = 0; i < arr.length; i++) {
|
|
var item = arr[i]
|
|
if (item.children instanceof Array && item.id === track[layer] && layer < 20) {
|
|
traverse(item.children, layer+1)
|
|
i = 1000
|
|
}
|
|
}
|
|
}
|
|
}
|
|
traverse(this.structure, 0)
|
|
if (action && action.menus) {
|
|
action.menus.push({menu: this, path});
|
|
}
|
|
}
|
|
removeAction(path) {
|
|
if (this.structure instanceof Array == false) return;
|
|
var scope = this;
|
|
if (path instanceof Action) {
|
|
let action = path;
|
|
this.structure.remove(action);
|
|
this.structure.remove(action.id);
|
|
action.menus.remove(this);
|
|
}
|
|
if (path === undefined) path = '';
|
|
if (typeof path == 'string') path = path.split('.');
|
|
|
|
function traverse(arr, layer) {
|
|
if (!isNaN(parseInt(path[layer]))) {
|
|
result = arr[parseInt(path[layer])]
|
|
|
|
} else if (typeof path[layer] === 'string') {
|
|
var i = arr.length-1;
|
|
while (i >= 0) {
|
|
var item = arr[i]
|
|
if (item.id === path[layer] && layer < 20) {
|
|
if (layer === path.length-1) {
|
|
var action = arr.splice(i, 1)[0]
|
|
if (action instanceof Action) {
|
|
for (var i = action.menus.length-1; i >= 0; i--) {
|
|
if (action.menus[i].menu == scope) {
|
|
action.menus.splice(i, 1)
|
|
}
|
|
}
|
|
}
|
|
} else if (item.children) {
|
|
traverse(item.children, layer+1)
|
|
}
|
|
}
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
traverse(this.structure, 0)
|
|
}
|
|
deleteItem(rm_item) {
|
|
var scope = this;
|
|
|
|
function traverse(arr, layer) {
|
|
arr.forEachReverse((item, i) => {
|
|
if (item === rm_item || item === rm_item.id) {
|
|
arr.splice(i, 1)
|
|
} else if (item && item.children instanceof Array) {
|
|
traverse(item.children)
|
|
}
|
|
})
|
|
}
|
|
traverse(this.structure, 0)
|
|
rm_item.menus.remove(scope)
|
|
}
|
|
}
|