/
vegas-dev
/
VGMenuAim
Обзор
Документация
Войти
/
vegas-dev
/
VGMenuAim
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
master
menuAim.js
310 строк
8 KB
adios
create: menuAim.js
26 апр 2026, 16:27
Верифицирован
26 апр 2026, 16:27
2b9e044
Код
Авторство
О чём код?
const MOUSE_LOCS_TRACKED = 3; const ACTIVATION_DELAY = 300; const defaultSettings = { rowSelector: '> li', submenuSelector: '*', submenuDirection: 'right', tolerance: 75, // больше = больше настроек при входе в подменю enter: function () {}, exit: function () {}, activate: function () {}, deactivate: function () {}, exitMenu: function () {} }; class VGMenuAim { constructor(element, opts = {}) { this.element = element; this.activeRow = null; this.mouseLocs = []; this.lastDelayLoc = null; this.timeoutId = null; this.options = mergeDeepObject(defaultSettings, opts); this.onDocumentMousemove = this.onDocumentMousemove.bind(this); this.onMenuMouseleave = this.onMenuMouseleave.bind(this); this.onMenuMouseover = this.onMenuMouseover.bind(this); this.onMenuMouseout = this.onMenuMouseout.bind(this); this.onMenuClick = this.onMenuClick.bind(this); this.init(); } init() { this.element.addEventListener('mouseleave', this.onMenuMouseleave); this.element.addEventListener('mouseover', this.onMenuMouseover); this.element.addEventListener('mouseout', this.onMenuMouseout); this.element.addEventListener('click', this.onMenuClick); document.addEventListener('mousemove', this.onDocumentMousemove, {passive: true}); } destroy() { if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } this.element.removeEventListener('mouseleave', this.onMenuMouseleave); this.element.removeEventListener('mouseover', this.onMenuMouseover); this.element.removeEventListener('mouseout', this.onMenuMouseout); this.element.removeEventListener('click', this.onMenuClick); document.removeEventListener('mousemove', this.onDocumentMousemove); } onDocumentMousemove(e) { this.mouseLocs.push({x: e.pageX, y: e.pageY}); if (this.mouseLocs.length > MOUSE_LOCS_TRACKED) { this.mouseLocs.shift(); } } onMenuMouseleave() { if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } // Если указано меню выхода и возвращается значение true, деактивируйте // активную в данный момент строку при выходе из меню. if (this.options.exitMenu(this.element)) { if (this.activeRow) { this.options.deactivate(this.activeRow); } this.activeRow = null; } } onMenuMouseover(e) { const row = this.getRowFromNode(e.target); if (!row) { return; } const fromRow = this.getRowFromNode(e.relatedTarget); if (row === fromRow) { return; } if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } this.options.enter(row); this.possiblyActivate(row); } onMenuMouseout(e) { const row = this.getRowFromNode(e.target); if (!row) { return; } const toRow = this.getRowFromNode(e.relatedTarget); if (row === toRow) { return; } this.options.exit(row); } onMenuClick(e) { const row = this.getRowFromNode(e.target); if (!row) { return; } this.activate(row); } getRowFromNode(node) { if (!(node instanceof Element)) { return null; } let current = node; while (current && current !== this.element) { if (this.isRow(current)) { return current; } current = current.parentElement; } return null; } isRow(node) { if (!(node instanceof Element)) { return false; } const rowSelector = (this.options.rowSelector || '').trim(); if (rowSelector.startsWith('>')) { const childSelector = rowSelector.replace(/^>\s*/, ''); return node.parentElement === this.element && node.matches(childSelector); } return rowSelector ? node.matches(rowSelector) : false; } hasActiveSubmenu() { if (!this.activeRow) { return false; } const submenuSelector = (this.options.submenuSelector || '').trim(); if (!submenuSelector || submenuSelector === '*') { return true; } return Boolean(this.activeRow.querySelector(submenuSelector)); } activate(row) { if (row === this.activeRow) { return; } if (this.activeRow) { this.options.deactivate(this.activeRow); } this.options.activate(row); this.activeRow = row; } possiblyActivate(row) { const delay = this.activationDelay(); if (delay) { this.timeoutId = setTimeout(() => { this.possiblyActivate(row); }, delay); return; } this.activate(row); } activationDelay() { if (!this.activeRow || !this.hasActiveSubmenu()) { // Если ни одна другая строка подменю не активирована, то // активируйте ее немедленно. return 0; } const rect = this.element.getBoundingClientRect(); const offset = { top: rect.top + window.scrollY, left: rect.left + window.scrollX }; const upperLeft = { x: offset.left, y: offset.top - this.options.tolerance }; const upperRight = { x: offset.left + this.element.clientWidth, y: upperLeft.y }; const lowerLeft = { x: offset.left, y: offset.top + this.element.clientHeight + this.options.tolerance }; const lowerRight = { x: offset.left + this.element.clientWidth, y: lowerLeft.y }; const loc = this.mouseLocs[this.mouseLocs.length - 1]; let prevLoc = this.mouseLocs[0]; if (!loc) { return 0; } if (!prevLoc) { prevLoc = loc; } if ( prevLoc.x < offset.left || prevLoc.x > lowerRight.x || prevLoc.y < offset.top || prevLoc.y > lowerRight.y ) { // Если предыдущее положение мыши было за пределами всего меню, // немедленно активируйте его. return 0; } if (this.lastDelayLoc && loc.x === this.lastDelayLoc.x && loc.y === this.lastDelayLoc.y) { // Если мышь не двигалась с момента последней проверки // для получения статуса активации немедленно активируйте. return 0; } let decreasingCorner = upperRight; let increasingCorner = lowerRight; // Мы ожидаем уменьшения или увеличения значений наклона // зависит от того, в каком направлении открывается подменю относительно // главного меню. if (this.options.submenuDirection === 'left') { decreasingCorner = lowerLeft; increasingCorner = upperLeft; } else if (this.options.submenuDirection === 'below') { decreasingCorner = lowerRight; increasingCorner = lowerLeft; } else if (this.options.submenuDirection === 'above') { decreasingCorner = upperLeft; increasingCorner = upperRight; } const decreasingSlope = this.slope(loc, decreasingCorner); const increasingSlope = this.slope(loc, increasingCorner); const prevDecreasingSlope = this.slope(prevLoc, decreasingCorner); const prevIncreasingSlope = this.slope(prevLoc, increasingCorner); if (decreasingSlope < prevDecreasingSlope && increasingSlope > prevIncreasingSlope) { // Мышь перемещается с предыдущего места к // активированному в данный момент подменю. this.lastDelayLoc = loc; return ACTIVATION_DELAY; } this.lastDelayLoc = null; return 0; } slope(a, b) { return (b.y - a.y) / (b.x - a.x); } } export default VGMenuAim; function mergeDeepObject(...objects) { const isObject = obj => obj && typeof obj === 'object'; if (!isObject) return; return objects.reduce((prev, obj) => { Object.keys(obj).forEach(key => { const pVal = prev[key]; const oVal = obj[key]; if (Array.isArray(pVal) && Array.isArray(oVal)) { prev[key] = pVal.concat(...oVal); } else if (isObject(pVal) && isObject(oVal)) { prev[key] = mergeDeepObject(pVal, oVal); } else { prev[key] = oVal; } }); return prev; }, {}); }