blockbench/js/interface/menu.js
JannisX11 a159a177db Fix issue with moving locators
Fix issue with centering meshes
Fix transform gizmo not updating when dragging elements in outliner
Fix close button on Quick Setup not working
Fix lag when using mirror painting settings menu multiple times
2022-09-23 17:47:37 +02:00

560 lines
16 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);
}
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 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).mouseenter(e => {
scope.hover(node.get(0), e, true);
})
}
} 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')});
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);
})
}
var last = menu_node.children().last();
if (last.length && last.hasClass('menu_separator')) {
last.remove()
}
}
function getEntry(s, parent) {
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, 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) => {s.trigger(e)});
if (s.side_menu) {
let content_list = typeof s.side_menu.structure == 'function' ? s.side_menu.structure(context) : s.side_menu.structure;
createChildList(s, entry, content_list);
}
}
parent.append(entry)
} else if (s instanceof BarSelect) {
if (typeof s.icon === 'function') {
var icon = Blockbench.getIconNode(s.icon(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 HTMLElement) {
parent.append(s);
} else if (typeof s === 'object') {
let child_count;
if (typeof s.icon === 'function') {
var icon = Blockbench.getIconNode(s.icon(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(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();
}
}
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 ) {
offset_top -= el_height;
if (position instanceof HTMLElement) {
offset_top -= position.clientHeight;
}
}
offset_top = Math.clamp(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.className.includes('parent') ||
(ev.target.parentNode && ev.target.parentNode.className.includes('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(position) {
return this.open(position);
}
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 && item.children.length > 0 && 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)
}
}