/** @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; });