| 1 |
/** |
| 2 |
* BubbleMenu |
| 3 |
* A JavaScript class for creating customizable side menus. |
| 4 |
* |
| 5 |
* @version 4.0.0 |
| 6 |
* @license MIT License |
| 7 |
* @author Dmytro Lobov |
| 8 |
* @url https://wow-estore.com/item/bubble-menu-pro/ |
| 9 |
*/ |
| 10 |
|
| 11 |
'use strict'; |
| 12 |
|
| 13 |
|
| 14 |
const BubbleMenu = () => { |
| 15 |
const menus = document.querySelectorAll('.bubble-menu'); |
| 16 |
if (menus.length === 0) return; |
| 17 |
|
| 18 |
const ITEM_PADDING = 15; |
| 19 |
const BASE_ANGLE_NON_POSITIONED = Math.PI; |
| 20 |
const BASE_ANGLE_POSITIONED = Math.PI / 2; |
| 21 |
|
| 22 |
menus.forEach(menu => { |
| 23 |
initializeMenu(menu); |
| 24 |
}); |
| 25 |
|
| 26 |
function initializeMenu(menu) { |
| 27 |
menu.classList.add('-show'); |
| 28 |
const toggle = getElement(menu, '.bm-toggle'); |
| 29 |
const items = getElements(menu, '.bm-item'); |
| 30 |
const links = getElements(menu, '.bm-link'); |
| 31 |
const itemWidth = getItemWidth(items); |
| 32 |
const isPositioned = isPositionedMenu(menu); |
| 33 |
const angleIncrement = calculateAngleIncrement(items.length, isPositioned); |
| 34 |
let radius = calculateOptimalRadius(itemWidth, angleIncrement); |
| 35 |
const minRadius = itemWidth * 1.5; |
| 36 |
|
| 37 |
if(radius < minRadius) { |
| 38 |
radius = minRadius; |
| 39 |
} |
| 40 |
|
| 41 |
setMenuProperties(menu, items, angleIncrement, radius); |
| 42 |
updateMenuStatus(menu, links); |
| 43 |
attachEventListeners(menu, toggle, links); |
| 44 |
} |
| 45 |
|
| 46 |
function getElement(parent, selector) { |
| 47 |
return parent.querySelector(selector); |
| 48 |
} |
| 49 |
|
| 50 |
function getElements(parent, selector) { |
| 51 |
return parent.querySelectorAll(selector); |
| 52 |
} |
| 53 |
|
| 54 |
function getItemWidth(items) { |
| 55 |
return items[0].offsetWidth + ITEM_PADDING; |
| 56 |
} |
| 57 |
|
| 58 |
function isPositionedMenu(menu) { |
| 59 |
const positions = ['-top-left', '-top-right', '-bottom-left', '-bottom-right']; |
| 60 |
return positions.some(position => menu.classList.contains(position)); |
| 61 |
} |
| 62 |
|
| 63 |
function calculateAngleIncrement(itemCount, isPositioned) { |
| 64 |
const baseAngle = isPositioned ? BASE_ANGLE_POSITIONED : BASE_ANGLE_NON_POSITIONED; |
| 65 |
return baseAngle / (itemCount - 1); |
| 66 |
} |
| 67 |
|
| 68 |
function calculateOptimalRadius(itemWidth, angleIncrement) { |
| 69 |
return (itemWidth / 2) / Math.sin(angleIncrement / 2); |
| 70 |
} |
| 71 |
|
| 72 |
function setMenuProperties(menu, items, angleIncrement, radius) { |
| 73 |
items.forEach((item, index) => { |
| 74 |
const reverseIndex = items.length - 1 - index; |
| 75 |
const angle = determineAngleForMenu(menu, angleIncrement, index, reverseIndex); |
| 76 |
styleMenuItem(item, menu, radius, angle, index); |
| 77 |
}); |
| 78 |
} |
| 79 |
|
| 80 |
function determineAngleForMenu(menu, angleIncrement, index, reverseIndex) { |
| 81 |
if (menu.classList.contains('-top')) return angleIncrement * reverseIndex; |
| 82 |
if (menu.classList.contains('-top-right')) return angleIncrement * reverseIndex + Math.PI / 2; |
| 83 |
if (menu.classList.contains('-top-left')) return angleIncrement * index; |
| 84 |
if (menu.classList.contains('-left')) return angleIncrement * index - Math.PI / 2; |
| 85 |
if (menu.classList.contains('-right')) return angleIncrement * reverseIndex + Math.PI / 2; |
| 86 |
if (menu.classList.contains('-bottom')) return angleIncrement * reverseIndex * -1; |
| 87 |
if (menu.classList.contains('-bottom-right')) return angleIncrement * reverseIndex + Math.PI; |
| 88 |
if (menu.classList.contains('-bottom-left')) return angleIncrement * index - Math.PI / 2; |
| 89 |
return angleIncrement * index - Math.PI / 2; |
| 90 |
} |
| 91 |
|
| 92 |
function styleMenuItem(item, menu, radius, angle, index) { |
| 93 |
const labelPosition = determineLabelPosition(menu, angle); |
| 94 |
const link = item.querySelector('.bm-link'); |
| 95 |
link.classList.add(labelPosition); |
| 96 |
applyToggleClass(menu); |
| 97 |
const x = radius * Math.cos(angle); |
| 98 |
const y = radius * Math.sin(angle); |
| 99 |
item.style.setProperty('--x', `${x}px`); |
| 100 |
item.style.setProperty('--y', `${y}px`); |
| 101 |
if (index > 0) { |
| 102 |
item.style.setProperty('--delay', `0.${index}s`); |
| 103 |
} |
| 104 |
} |
| 105 |
|
| 106 |
function applyToggleClass(menu) { |
| 107 |
const toggle = menu.querySelector('.bm-toggle'); |
| 108 |
if (menu.classList.contains('-top-right') || menu.classList.contains('-bottom-right') || menu.classList.contains('-right')) { |
| 109 |
toggle.classList.add('-left'); |
| 110 |
} else { |
| 111 |
toggle.classList.add('-right'); |
| 112 |
} |
| 113 |
} |
| 114 |
|
| 115 |
function determineLabelPosition(menu, angle) { |
| 116 |
if (menu.classList.contains('-bottom')) return angle < -1.571 ? '-top-left' : '-top-right'; |
| 117 |
if (menu.classList.contains('-top')) return angle < 1.571 ? '-bottom-right' : '-bottom-left'; |
| 118 |
if (menu.classList.contains('-bottom-left')) return '-top-right'; |
| 119 |
if (menu.classList.contains('-top-left')) return '-bottom-right'; |
| 120 |
if (menu.classList.contains('-bottom-right')) return '-top-left'; |
| 121 |
if (menu.classList.contains('-top-right')) return '-bottom-left'; |
| 122 |
if (menu.classList.contains('-right')) return angle < 2.36 ? '-bottom-left' : angle < 3.92 ? '-left' : '-top-left'; |
| 123 |
if (menu.classList.contains('-left')) return angle < -0.78 ? '-top-right' : angle < 0.78 ? '-right' : '-bottom-right'; |
| 124 |
} |
| 125 |
|
| 126 |
function updateMenuStatus(menu, links) { |
| 127 |
const isActive = menu.classList.contains('-active'); |
| 128 |
links.forEach(link => link.setAttribute('tabindex', isActive ? '0' : '-1')); |
| 129 |
const updateVisibility = (bubble) => { |
| 130 |
if(!bubble.classList.contains('-active')) { |
| 131 |
bubble.classList.remove('-show'); |
| 132 |
} else { |
| 133 |
bubble.classList.add('-show'); |
| 134 |
} |
| 135 |
}; |
| 136 |
|
| 137 |
if(isActive && menu.hasAttribute('data-bubbles-hide')) { |
| 138 |
menus.forEach(updateVisibility); |
| 139 |
} else { |
| 140 |
menus.forEach(bubble => bubble.classList.add('-show')); |
| 141 |
} |
| 142 |
|
| 143 |
document.addEventListener('click', function(event) { |
| 144 |
if (!event.target.closest('[data-bubble-toggle]')) { |
| 145 |
menus.forEach((bubble) => { |
| 146 |
if(bubble.hasAttribute('data-bubble-toggle')) { |
| 147 |
bubble.classList.remove('-active'); |
| 148 |
} |
| 149 |
bubble.classList.add('-show'); |
| 150 |
}); |
| 151 |
} |
| 152 |
}); |
| 153 |
} |
| 154 |
|
| 155 |
function attachEventListeners(menu, toggle, links) { |
| 156 |
toggle.addEventListener('click', () => { |
| 157 |
menu.classList.toggle('-active'); |
| 158 |
updateMenuStatus(menu, links); |
| 159 |
}); |
| 160 |
|
| 161 |
document.addEventListener('keydown', event => { |
| 162 |
if ((event.key === 'Enter' || event.key === ' ') && document.activeElement === toggle) { |
| 163 |
menu.classList.toggle('-active'); |
| 164 |
updateMenuStatus(menu, links); |
| 165 |
event.preventDefault(); |
| 166 |
} |
| 167 |
}); |
| 168 |
} |
| 169 |
}; |
| 170 |
|
| 171 |
BubbleMenu(); |
| 172 |
|