Files
kit/assets/menu/menu.js
T
2021-10-05 23:13:01 +03:00

949 lines
33 KiB
JavaScript

/** @module menu */
//todo: instance, options, auto-options-by-node, userEventManager
__PluginManager__.register('menu',function(module, exports, require) {
var align = require('align');
var libModule = require('lib');
var dataStructures = require('dataStructures');
/**
* @class kitMenuClassDefaultOptions
*/
var defaultMenuOptions = {
parent: null,
nodeProperty: 'menu',
class: 'kit-menu',
eventType: 'hover',
animation: '',
hideDelay: 200,
alignOffset: [0, 0],
click: (function (event, menu) {
console.log('Menu click');
}),
hover: (function (event, menu) {
console.log('Menu hover');
}),
dblclick: (function (event, menu) {
console.log('Menu dblclick');
}),
hide: (function () {
})
};
var MenuList = [];
var MenuListIncrement = 0;
var MenuClass = (function () {
/**
* @typedef kitMenuItemHandler
* @param event {MouseEvent}
* @param menuItem {MenuItem}
* @param menu {Menu}
*/
/**
* @param options
* @param items
* @constructor
*/
function Menu(options = {}, items = [])
{
//console.log('Menu: constructor');
var _this = this;
this.parentActionHandler = this.parentAction.bind(this);
this.documentListenerHandler = this.documentListener.bind(this);
this.nodeActionHandler = this.nodeAction.bind(this);
this.id = ++MenuListIncrement;
MenuList[this.id] = this;
this.itemList = {};
this.align = new align.Class();
this.node = null;
this.bInit = false;
this.deferredHideID = 0;
this.bActive = false;
this.options = defaultMenuOptions;
this.createNode(); //с пула нужна базовая нода
this.setOptions(options);
this.addItemList(items);
this.handleEvent = function(event) {
// console.log('Menu handleEvent');
// console.log(event.type);
switch(event.type) {
case 'click':
this.options.click(event, _this);
break;
case 'mouseover':
this.options.hover(event, _this);
break;
}
};
}
Menu.prototype.getItemById = function(itemId) {
if(this.itemList.hasOwnProperty(itemId)) {
return this.itemList[itemId];
}
return false;
};
Menu.prototype.getNode = function () {
if(this.node.innerHTML === '') {
this.render();
}
return this.node;
};
Menu.prototype.getParent = function () {
return this.options.parent;
};
Menu.prototype.createNode = function () {
this.node = document.createElement('div');
this.node.classList.add(this.options.class);
this.node.style.display = 'none';
this.node.setAttribute('data-menu-id', this.id);
this.node.kit = this.node.kit || {};
this.node.kit[this.options.nodeProperty] = this;
};
Menu.prototype.showNode = function () {
// console.log('Menu: showNode ' + this.id, this.bActive);
this.deferredHideCancel();
if(!this.bActive) {
this.node.style.display = '';
this.node.classList.add('active');
this.options.parent.classList.add('active');
this.calcStyle();
this.bActive = true;
this.addDocumentEventListener();
}
this.addAnimation();
};
Menu.prototype.hide = function () {
getChainActiveMenuDataList(this.id).reverse().forEach(function (data) {
data.menu.hideNodeNow();
});
return this;
};
Menu.prototype.hideNode = function () {
this.hideNodeNow();
};
Menu.prototype.hideNodeDeferred = function () {
this.deferredHideCancel();
this.deferredHideID = setTimeout(this.hideNodeNow.bind(this), this.options.hideDelay);
};
Menu.prototype.deferredHideCancel = function () {
if(this.deferredHideID) {
clearTimeout(this.deferredHideID);
this.deferredHideID = 0;
}
return this;
};
Menu.prototype.hideNodeNow = function () {
// console.log('Menu: hideNodeNow');
this.deferredHideCancel();
this.node.style.display = 'none';
this.removeAnimation();
this.node.classList.remove('active');
this.options.parent.classList.remove('active');
this.bActive = false;
this.removeDocumentEventListener();
return this;
};
Menu.prototype.removeAnimation = function(){
if(this.options.animation && this.node && this.node.classList.contains(this.options.animation)){
this.node.classList.remove(this.options.animation);
}
};
Menu.prototype.addAnimation = function(){
if(this.options.animation && this.node && !this.node.classList.contains(this.options.animation)){
this.node.classList.add(this.options.animation);
}
};
Menu.prototype.calcStyle = function () {
// console.log('calcStyle ' + this.id);
if(!(this.options.parent instanceof Element)) {
console.warn('Menu.options.parent NOT instanceof Element');
return false;
}
this.node.style.position = 'absolute';
this.node.style.width = 'fit-content';
//var parentNode = this.getParentMenuNode() || this.getParent();
var parentNode = this.getParent();
var hDirection = 'right';
var parentMenu = this.getParentMenu();
if(parentMenu && parentMenu instanceof Menu) {
hDirection = parentMenu.align.status.horizontal.direction;
}
var hAlign = 'right';
if(parentMenu && parentMenu instanceof Menu) {
hAlign = parentMenu.align.status.horizontal.align;
}
this.align.offset = this.options.alignOffset;
this.align
.setNode(this.node)
.setParentNode(parentNode)
.setHorizontal(hDirection, hAlign)
.setVertical('bottom', 'top');
};
Menu.prototype.calcStyleChain = function(){
getChainActiveMenuDataList(this.id).forEach(function (data) {
data.menu.calcStyle();
})
};
/**
* @return {Menu} | null
*/
Menu.prototype.getParentMenu = function() {
var p = this.getParent();
if(p instanceof Element && p.hasOwnProperty('kit') && p.kit.menuItem instanceof MenuItemClass) {
return p.kit.menuItem.getMenu();
}
};
Menu.prototype.getParentMenuNode = function() {
var parentMenu = this.getParentMenu(),
parentMenuNode;
if(parentMenu) {
parentMenuNode = parentMenu.getNode();
}
return parentMenuNode;
};
/**
*
* @param options {kitMenuItemOptions}
* @param returnItem
* @return {Menu}
*/
Menu.prototype.addItem = function (options = {}, returnItem = false) {
var listIndex;
if(options instanceof MenuItemClass) {
options.parentId = this.id;
listIndex = options.options.id;
this.itemList[listIndex] = options;
} else if(options instanceof Object){
if(!options.id) {
options.id = this.uniqueId(8);
}
listIndex = options.id;
this.itemList[listIndex] = (new this.MenuItem(this.id)).setOptions(options).init();
}
if(returnItem) {
return this.itemList[listIndex];
}
return this;
};
Menu.prototype.addItemList = function(itemList = []){
itemList.forEach((function(element, index, array){
this.addItem(element);
}).bind(this));
return this;
};
Menu.prototype.sortItems = function() {
this.itemList = Object.entries(this.itemList)
.sort(function(a, b){
return a[1].options.sort - b[1].options.sort;
})
.reduce((r, [k, v]) => ({ ...r, [k]: v }), {});
};
Menu.prototype.render = function() {
//console.log('Menu: render');
this.sortItems();
this.node.innerHTML = '';
for (var i in this.itemList) {
if(this.itemList.hasOwnProperty(i)) {
this.node.append(this.itemList[i].getNode());
}
}
return this;
};
Menu.prototype.uniqueId = function (idStrLen = 32) {
var idStr = '';
idStrLen && (idStr = (Math.floor((Math.random() * 25)) + 10).toString(36));
do {
idStr += (Math.floor((Math.random() * 35))).toString(36);
} while (idStr.length < idStrLen);
return (idStr);
};
Menu.prototype.setParent = function(parentNode) {
// console.log('Menu: setParent ' + parentNode);
if(typeof parentNode === 'string') {
parentNode = document.querySelector(parentNode);
}
if(!(parentNode instanceof Element)) {
//console.warn('Menu.setParent: Не удалось получить parent. ' + parentNode);
return false;
}
this.removeParentEventListener();
this.options.parent = parentNode;
if(this.bInit) {
this.reInit(true);
}
};
Menu.prototype.setEventType = function(type = 'hover') {
this.options.eventType = type;
if(this.bInit) {
this.reInit();
}
};
Menu.prototype.addDocumentEventListener = function() {
document.addEventListener('click', this.documentListenerHandler, false);
};
Menu.prototype.removeDocumentEventListener = function() {
document.removeEventListener('click', this.documentListenerHandler, false);
};
Menu.prototype.removeParentEventListener = function() {
// console.log('Menu: removeParentEventListener');
if(this.options.parent instanceof Element) {
this.options.parent.classList.remove('active');
this.options.parent.removeEventListener('click', this.parentActionHandler, false);
this.options.parent.removeEventListener('mouseover', this.parentActionHandler, false);
this.options.parent.removeEventListener('mouseout', this.parentActionHandler, false);
}
};
Menu.prototype.addParentEventListener = function() {
if(this.options.parent instanceof Element) {
if(this.options.eventType === 'hover') {
this.options.parent.addEventListener('mouseover', this.parentActionHandler, false);
this.options.parent.addEventListener('mouseout', this.parentActionHandler, false);
}
if(this.options.eventType === 'click') {
this.options.parent.addEventListener(this.options.eventType, this.parentActionHandler, false);
}
}
};
Menu.prototype.addNodeEventListener = function() {
if(!this.node) {
return;
}
this.node.addEventListener('mouseenter', this.nodeActionHandler, false);
this.node.addEventListener('mouseleave', this.nodeActionHandler, false);
};
Menu.prototype.removeNodeEventListener = function() {
if(!this.node) {
return;
}
this.node.removeEventListener('mouseenter', this.nodeActionHandler, false);
this.node.removeEventListener('mouseleave', this.nodeActionHandler, false);
};
Menu.prototype.insertToNode = function (parent) {
if(!(parent instanceof Element)) {
console.warn('Menu.insertToNode - node NOT instanceof Element');
return false;
}
this.setParent(parent);
this.options.parent.prepend(this.getNode());
};
Menu.prototype.init = function () {
if(this.bInit === true) {
return;
}
if(!this.options.parent instanceof Element) {
console.warn('Menu.init: Не установлен parent');
}
this.setParent(this.options.parent);
this.render();
this.options.parent.append(this.getNode());
this.addParentEventListener();
this.addNodeEventListener();
//document.addEventListener('click', this.documentListenerHandler, false);
this.node.setAttribute('data-kit-menu-init', 1);
this.bInit = true;
};
Menu.prototype.reInit = function (recursiveCalcStyle = false) {
console.warn('Menu: reInit ' + this.id);
if(this.bInit === false) {
return;
}
this.removeNodeEventListener();
this.removeParentEventListener();
//this.removeDocumentEventListener();
//this.removeAnimation();
if(this.node.parentElement !== this.options.parent) {
this.removeAnimation();
this.node.remove();
this.options.parent.prepend(this.node);
}
this.render();
if(this.bActive) {
if(recursiveCalcStyle) {
this.calcStyleChain();
} else {
this.calcStyle();
}
this.showNode();
} else {
this.hideNode();
}
this.addNodeEventListener();
this.addParentEventListener();
//this.addDocumentEventListener();
};
Menu.prototype.toggle = function() {
if(this.node.classList.toggle('active')) {
this.showNode();
} else {
this.hide();
}
};
Menu.prototype.parentAction = function (event) {
event.stopPropagation();
// event.stopImmediatePropagation();
switch (event.type) {
case 'mouseout':
if (this.needHideByParent(event.relatedTarget)) {
this.hideNodeDeferred();
}
break;
case 'mouseover':
this.showNode();
break;
case 'click':
this.toggle();
break;
}
};
Menu.prototype.nodeAction = function (event) {
event.stopPropagation();
// event.stopImmediatePropagation();
switch (event.type) {
case 'mouseout':
break;
case 'mouseover':
break;
case 'mouseleave':
if(this.options.eventType === 'hover') {
this.hideNodeDeferred();
}
break;
case 'mouseenter':
this.deferredHideCancel();
break;
}
};
Menu.prototype.needHideByParent = function(target) {
var its_menu = target === this.node || this.node.contains(target);
var its_parent = target === this.options.parent;
var is_active_parent = this.node.classList.contains('active');
return !its_menu && !its_parent && is_active_parent;
};
Menu.prototype.documentListener = function(event) {
// console.log('documentListener');
// console.log(event);
if(this.options.eventType === 'click') {
if (this.needHideByParent(event.target)) {
this.toggle();
}
}
};
/**
* @param options {kitMenuClassDefaultOptions}
* @return {Menu}
*/
Menu.prototype.setOptions = function (options = {}) {
this.options = Object.assign({}, this.options, options);
this.setParent(this.options.parent);
return this;
};
var MenuItemClass = function () {
function MenuItem(parentId) {
var _thisMenuItem = this;
this.parentId = Number(parentId);
this.node = Element;
this.bInit = false;
this.bActive = false;
this.child = null;
/**
* @class kitMenuItemOptions
*/
this.options = {
id: '',
icon: libModule.svg.options,
arrow: libModule.svg.arrowMenu,
label: 'menu',
href: 'javascript:void(0);',
class: 'kit-menu-item',
sort: 500,
visible: true,
/** @type {kitMenuItemHandler} */
hover: this.defaultHoverHandler,
/** @type {kitMenuItemHandler} */
click: this.defaultClickHandler,
dblclick: this.defaultDblClickHandler,
};
this.handleEvent = function(event) {
event.stopPropagation();
switch(event.type) {
case 'mouseover':
this.options.hover(event, _thisMenuItem, MenuList[_thisMenuItem.parentId]);
break;
case 'click':
this.options.click(event, _thisMenuItem, MenuList[_thisMenuItem.parentId]);
break;
case 'dblclick':
this.options.dblclick(event, _thisMenuItem, MenuList[_thisMenuItem.parentId]);
break;
}
};
this.createNode();
}
/**
* @param event {MouseEvent}
* @param menuItem {MenuItem}
* @param menu {Menu}
*/
MenuItem.prototype.defaultHoverHandler = function (event, menuItem, menu) {
console.log('hover: ' + menuItem.options.label);
};
/**
* @param event {MouseEvent}
* @param menuItem {MenuItem}
* @param menu {Menu}
*/
MenuItem.prototype.defaultClickHandler = function (event, menuItem, menu) {
console.log('click: ' + menuItem.options.label);
menu.hide();
};
/**
* @param event {MouseEvent}
* @param menuItem {MenuItem}
* @param menu {Menu}
*/
MenuItem.prototype.defaultDblClickHandler = function (event, menuItem, menu) {
console.log('dblclick: ' + menuItem.options.label);
};
MenuItem.prototype.getMenu = function () {
return MenuList[this.parentId];
};
MenuItem.prototype.getParent = function () {
if(MenuList[this.parentId] instanceof Menu) {
return MenuList[this.parentId].getNode();
}
return null;
};
MenuItem.prototype.getId = function () {
return this.options.id;
};
/**
* @returns HTMLElement
*/
MenuItem.prototype.getNode = function () {
return this.node;
};
MenuItem.prototype.createNode = function () {
this.node = document.createElement('a');
this.node.style.position = 'relative';
this.node.dataset.parentId = this.parentId;
this.node.kit = this.node.kit || {};
this.node.kit.menuItem = this;
};
/**
* @param content HTMLElement | string
* @returns {MenuItem}
*/
MenuItem.prototype.setLabel = function (content = '') {
this.options.label = content;
var wrap = this.getNodeScopeElementByClassName('kit-menu-item__label');
this.setContent(wrap, content);
return this;
};
/**
* @param href
* @returns {MenuItem}
*/
MenuItem.prototype.setHref = function (href = 'javascript:void(0);') {
this.options.href = href;
this.getNode().setAttribute('href', href);
return this;
};
/**
* @param content HTMLElement | string
* @returns {MenuItem}
*/
MenuItem.prototype.setIcon = function (content = '') {
this.options.icon = content;
var wrap = this.getNodeScopeElementByClassName('kit-menu-item__icon');
this.setContent(wrap, content);
return this;
};
/**
* @param sort int
* @returns {MenuItem}
*/
MenuItem.prototype.setSort = function (sort = 500) {
this.options.sort = Number(sort);
return this;
};
/**
* @param content HTMLElement | string
* @returns {MenuItem}
*/
MenuItem.prototype.setArrow = function (content = '') {
this.options.arrow = content;
var wrap = this.getNodeScopeElementByClassName('kit-menu-item__arrow');
this.setContent(wrap, content);
return this;
};
MenuItem.prototype.setId = function (id = '') {
if(id) {
this.options.id = id;
}
this.node.id = this.options.id;
return this;
};
MenuItem.prototype.setVisible = function (visible = true) {
this.options.visible = visible;
this.node.style.display = this.options.visible?'':'none';
return this;
};
/**
* @param {Menu} menu
* @return {MenuItem}
*/
MenuItem.prototype.setChild = function (menu) {
if(menu instanceof Menu) {
menu.setParent(this.node);
menu.init();
}
else if( Array.isArray(menu) ) {
var menuClass;
menuClass = new Menu();
menu.forEach(function (item, i) {
if(typeof item === 'object') {
menuClass.addItem(item);
}
});
menuClass.setParent(this.node);
menuClass.init();
}
return this;
};
MenuItem.prototype.addClass = function (addClass = '') {
this.node.classList.add(addClass);
return this;
};
/**
* @param options Object
* @return {MenuItem}
*/
MenuItem.prototype.setOptions = function (options = {}) {
this.options = Object.assign({}, this.options, options);
return this;
};
/**
* @return {MenuItem}
*/
MenuItem.prototype.render = function () {
this.setLabel(this.options.label);
this.setHref(this.options.href);
this.setId(this.options.id);
this.addClass(this.options.class);
this.setChild(this.options.child);
this.setIcon(this.options.icon);
this.setArrow(this.options.arrow);
this.setVisible(this.options.visible);
this.setSort(this.options.sort);
return this;
};
MenuItem.prototype.init = function () {
if(this.bInit) {
return this;
}
this.render();
this.node.removeEventListener('click', this, false);
this.node.removeEventListener('dblclick', this, false);
this.node.addEventListener('click', this, false);
this.node.addEventListener('dblclick', this, false);
this.bInit = true;
return this;
};
MenuItem.prototype.reInit = function () {
this.render();
this.node.removeEventListener('click', this, false);
this.node.removeEventListener('dblclick', this, false);
this.node.addEventListener('click', this, false);
this.node.addEventListener('dblclick', this, false);
return this;
};
MenuItem.prototype.remove = function () {
this.node.removeEventListener('click', this, false);
this.node.removeEventListener('dblclick', this, false);
var id = this.options.id;
//console.log('remove item' + id);
setTimeout((function(parentId, id){
return function() {
if(MenuList.hasOwnProperty(parentId) && MenuList[parentId].itemList.hasOwnProperty(id)) {
delete MenuList[parentId].itemList[id];
MenuList[parentId].reInit();
}
};
})(this.parentId, this.options.id),0);
};
/**
* @param className
* @returns HTMLElement
*/
MenuItem.prototype.getNodeScopeElementByClassName = function(className = ''){
var wrap;
this.node && (wrap = this.node.querySelector(':scope > .' + className));
if(!wrap) {
wrap = document.createElement('div');
wrap.classList.add(className);
this.node.append(wrap);
}
return wrap;
};
/**
* @param node HTMLElement
* @param content HTMLElement | string
* @param isText boolean
* @return boolean
*/
MenuItem.prototype.setContent = function(node, content, isText = false){
if(!(node instanceof Element)) {
return false;
}
while (node.firstChild) {
node.removeChild(node.firstChild);
}
if((content instanceof Element) || isText) {
node.append(content);
} else {
node.innerHTML = content;
}
return true;
};
return MenuItem;
}();
Menu.prototype.MenuItem = MenuItemClass;
return Menu;
})();
var getInstance = function(options = {}, items = []) {
if(options instanceof MenuClass) {
return options;
}
if(typeof options === 'string') {
options = document.querySelector(options);
}
if((options instanceof HTMLElement) && options.kit && options.kit.menu) {
return options.kit.menu;
}
return new MenuClass(options, items);
};
var getActiveMenuDataList = function(){
function valueEx(value) {
if(typeof value === 'function') {
return value();
}
return value;
}
var idList = [];
var dataList = [];
var refList = [];
function getParentId(id, element) {
if(!element) {
element = MenuList[id];
}
var parentId = false, parentMenu;
if(!(refList.hasOwnProperty(id) && refList[id].parentMenu)) {
if(element) {
refList[id] = refList[id] || {};
parentMenu = refList[id].parentMenu = element.getParentMenu();
}
parentMenu && (parentId = parentMenu.id);
return parentId;
}
return refList[id].parentMenu.id;
}
MenuList.forEach(function(element){
var pos, posId, id, parentId;
if(!((element instanceof MenuClass) && element.bInit && element.bActive)) {
return;
}
id = element.id;
refList[id] = refList[id] || {};
refList[id].menu = element;
parentId = getParentId(id, element);
var depth = function (id){
return function() {
return 1 + valueEx(refList[refList[id].data.parentId].data.depth)
};
};
!(
parentId!==false
&&
(
((pos=idList.indexOf(parentId))!==-1)
?
(
(idList.indexOf(id)===-1)&&(idList.splice(pos+1,0,id), dataList.splice(pos+1,0, refList[id].data = {id:id, depth: depth(id), parentId: parentId, type: 1}))
)
:
(
(
refList[parentId] = refList[parentId] || {},
(posId=idList.indexOf(id))!==-1
)?
(
idList.splice(posId,0,parentId),
dataList.splice(posId,0,refList[parentId].data = {id:parentId, depth: depth(parentId), parentId: getParentId(parentId), type: 5})
):
(
idList.push(parentId),
dataList.push(refList[parentId].data = {id:parentId, depth: depth(parentId), parentId: getParentId(parentId), type: 2}),
idList.push(id),
dataList.push(refList[id].data = {id:id, depth: depth(id), parentId: parentId, type: 3})
)
),
0
)
)
&&(idList.indexOf(id)===-1)&&(idList.push(id), dataList.push(refList[id].data = {id:id, depth: 1, parentId: 0, type: 4}));
});
dataList.forEach(function (data) {
data.depth = valueEx(data.depth);
data.menu = refList[data.id].menu;
});
return dataList;
};
var getChainActiveMenuDataList = function (id) {
var dataList = getActiveMenuDataList(), result = [], depth = 0;
dataList.forEach(function(data) {
if(data.id === id) {
depth = data.depth;
}
if(depth === data.depth && data.id !== id) {
depth = 0;
}
if(depth) {
result.push(data);
}
});
return result;
};
var reInitAllMenu = function(){
var dataList = getActiveMenuDataList();
dataList.forEach(function(data) {
data.menu.reInit();
});
};
var reCalcAllMenu = function(){
console.log('reCalcAllMenu');
var dataList = getActiveMenuDataList();
dataList.forEach(function(data) {
data.menu.calcStyle();
});
};
var autoload = function(doc = document, selector = '.kit-menu'){
if(!(doc instanceof Node)) {
return false;
}
var menuList = doc.querySelectorAll(selector);
menuList.forEach(
function(menuNode, currentIndex, listObj) {
if(!menuNode.kit || !menuNode.kit.hasOwnProperty(defaultMenuOptions.nodeProperty)) {
}
}
);
};
var reCalcAllMenuLimiter = new dataStructures.StackExecutionLimiter(this, reCalcAllMenu, 250);
var globalHandlers = {
resize: function(){
reCalcAllMenuLimiter.exec();
},
};
window.addEventListener('resize', globalHandlers.resize);
/**
* @memberof kitCore
* @instance
*/
var menu = {
Class: MenuClass,
getInstance: getInstance,
reInitAllMenu: reInitAllMenu,
reCalcAllMenu: reCalcAllMenu,
autoload: autoload,
getActiveMenuDataList: getActiveMenuDataList,
getChainActiveMenuDataList: getChainActiveMenuDataList,
MenuList: MenuList,
};
return module.exports = menu;
});