/** @module menu */ //todo: instance, options, auto-options-by-node, fast reCalc, fast reInit, userEventManager, collection __PluginManager__.register('menu',function(module, exports, require) { var align = require('align'); var libModule = require('lib'); var utils = require('utils'); /** * @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.hideNode = function () { // console.log('Menu: hideNode'); this.hideNodeDeferred(); }; Menu.prototype.hideNodeDeferred = function () { this.deferredHideCancel(); this.deferredHideID = setTimeout(this.hideNodeNow.bind(this), this.options.hideDelay); // console.log('hideNodeDeferred ' + this.deferredHideID); }; /** * @returns {Menu} */ Menu.prototype.deferredHideCancel = function () { if(this.deferredHideID) { clearTimeout(this.deferredHideID); this.deferredHideID = 0; } return this; }; /** * @returns {Menu} */ 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); } }; /** * @returns {Menu} */ Menu.prototype.hide = function () { this.hideNodeNow(); var tmp = this; while(tmp instanceof MenuClass) { tmp = tmp.hideNodeNow().getParentMenu(); } return this; }; Menu.prototype.calcStyle = function (recursive = false) { // 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'); if(recursive) { this.calcStyleChildMenu(true, MenuList); } }; Menu.prototype.calcStyleChildMenu = function(recursive = false, MenuList = null){ var i, MenuListActive = []; for(i in MenuList){ if(!MenuList.hasOwnProperty(i) || !(MenuList[i] instanceof Menu)) { continue; } if(MenuList[i].bActive && this !== MenuList[i]) { MenuListActive.push(MenuList[i]); } } for(i in MenuListActive){ if(MenuListActive[i].getParentMenu() === this) { MenuListActive[i].calcStyle(); if(recursive) { MenuListActive[i].calcStyleChildMenu(true, MenuListActive); } } } }; /** * @return {Menu} | null */ Menu.prototype.getParentMenu = function() { if(this.getParent() && this.getParent().kitMenuItem) { return this.getParent().kitMenuItem.getMenu(); } return undefined; if( this.getParent() && (this.getParent() instanceof Element) && this.getParent().hasOwnProperty('kitMenuItem') && (this.getParent().kitMenuItem instanceof MenuItemClass) ) { return this.getParent().kitMenuItem.getMenu(); } return undefined; }; 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.toggleParent = function() { this.node.classList.toggle('active'); return this.node.classList.contains('active'); }; 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(); this.node.remove(); this.render(); this.options.parent.prepend(this.getNode()); if(this.bActive) { this.calcStyle(recursiveCalcStyle); this.showNode(); } else { this.hideNode(); } this.addNodeEventListener(); this.addParentEventListener(); //this.addDocumentEventListener(); }; Menu.prototype.toggle = function() { if(this.toggleParent()) { this.showNode(); } else { this.hideNode(); } }; Menu.prototype.parentAction = function (event) { event.stopPropagation(); // event.stopImmediatePropagation(); switch (event.type) { case 'mouseout': if (this.needHideByParent(event.relatedTarget)) { this.hideNode(); } 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.hideNode(); } 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.kitMenuItem = 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 Menu) { 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 getActiveMenuIdList = function(){ var idList = []; MenuList.forEach(function(element, index, array){ var pos, posId, id, parentId; if((element instanceof MenuClass) && element.bInit && element.bActive) { id = element.id; parentId = false; element.getParentMenu() && (parentId = element.getParentMenu().id); !(parentId!==false&&(((pos=idList.indexOf(parentId))!==-1)?((idList.indexOf(id)===-1)&&idList.splice(pos +1,0,id)):(((posId=idList.indexOf(id))!==-1)?idList.splice(posId,0,parentId):(idList.push(parentId), idList.push(id))),0))&&(idList.indexOf(id)===-1)&&(idList.push(id)); } }); return idList }; var reInitAllMenu = function(){ var idList = getActiveMenuIdList(); idList.forEach(function(id, i, arr) { MenuList[id].reInit(); }); }; var reCalcAllMenu = function(){ console.log('reCalcAllMenu'); var idList = getActiveMenuIdList(); idList.forEach(function(id, i, arr) { MenuList[id].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 utils.StackExecutionLimiter(this, reCalcAllMenu, 200); 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, MenuList: MenuList, }; return module.exports = menu; });