205 lines
5.7 KiB
JavaScript
205 lines
5.7 KiB
JavaScript
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
|
import { SIDEBAR_COLLAPSED_CLASS } from './contextual_sidebar';
|
|
|
|
const HIDE_INTERVAL_TIMEOUT = 300;
|
|
const COLLAPSED_PANEL_WIDTH = 48;
|
|
const IS_OVER_CLASS = 'is-over';
|
|
const IS_ABOVE_CLASS = 'is-above';
|
|
const IS_SHOWING_FLY_OUT_CLASS = 'is-showing-fly-out';
|
|
let currentOpenMenu = null;
|
|
let menuCornerLocs;
|
|
let timeoutId;
|
|
let sidebar;
|
|
|
|
export const mousePos = [];
|
|
|
|
export const setSidebar = (el) => {
|
|
sidebar = el;
|
|
};
|
|
export const getOpenMenu = () => currentOpenMenu;
|
|
export const setOpenMenu = (menu = null) => {
|
|
currentOpenMenu = menu;
|
|
};
|
|
|
|
export const slope = (a, b) => (b.y - a.y) / (b.x - a.x);
|
|
|
|
export const getHeaderHeight = () => sidebar?.offsetTop || 0;
|
|
|
|
export const isSidebarCollapsed = () =>
|
|
sidebar && sidebar.classList.contains(SIDEBAR_COLLAPSED_CLASS);
|
|
|
|
export const canShowActiveSubItems = (el) => {
|
|
if (el.classList.contains('active') && !isSidebarCollapsed()) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
export const canShowSubItems = () => ['md', 'lg', 'xl'].includes(bp.getBreakpointSize());
|
|
|
|
export const getHideSubItemsInterval = () => {
|
|
if (!currentOpenMenu || !mousePos.length) return 0;
|
|
|
|
const currentMousePos = mousePos[mousePos.length - 1];
|
|
const prevMousePos = mousePos[0];
|
|
const currentMousePosY = currentMousePos.y;
|
|
const [menuTop, menuBottom] = menuCornerLocs;
|
|
|
|
if (currentMousePosY < menuTop.y || currentMousePosY > menuBottom.y) return 0;
|
|
|
|
if (
|
|
slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) &&
|
|
slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)
|
|
) {
|
|
return HIDE_INTERVAL_TIMEOUT;
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
export const calculateTop = (boundingRect, outerHeight) => {
|
|
const windowHeight = window.innerHeight;
|
|
const bottomOverflow = windowHeight - (boundingRect.top + outerHeight);
|
|
|
|
return bottomOverflow < 0
|
|
? boundingRect.top - outerHeight + boundingRect.height
|
|
: boundingRect.top;
|
|
};
|
|
|
|
export const hideMenu = (el) => {
|
|
if (!el) return;
|
|
|
|
const parentEl = el.parentNode;
|
|
|
|
el.style.display = '';
|
|
el.style.transform = '';
|
|
el.classList.remove(IS_ABOVE_CLASS);
|
|
el.classList.remove('fly-out-list');
|
|
parentEl.classList.remove(IS_OVER_CLASS);
|
|
parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);
|
|
|
|
setOpenMenu();
|
|
};
|
|
|
|
export const moveSubItemsToPosition = (el, subItems) => {
|
|
const hasSubItems = subItems.parentNode.querySelector('.has-sub-items');
|
|
const header = subItems.querySelector('.fly-out-top-item');
|
|
const boundingRect = el.getBoundingClientRect();
|
|
const left = sidebar ? sidebar.offsetWidth : COLLAPSED_PANEL_WIDTH;
|
|
let top = calculateTop(boundingRect, subItems.offsetHeight);
|
|
const isAbove = top < boundingRect.top;
|
|
if (hasSubItems) {
|
|
top = isAbove ? top : top - header.offsetHeight;
|
|
} else {
|
|
top = boundingRect.top;
|
|
}
|
|
|
|
subItems.classList.add('fly-out-list');
|
|
subItems.style.transform = `translate3d(${left}px, ${Math.floor(top) - getHeaderHeight()}px, 0)`; // eslint-disable-line no-param-reassign
|
|
const subItemsRect = subItems.getBoundingClientRect();
|
|
|
|
menuCornerLocs = [
|
|
{
|
|
x: subItemsRect.left, // left position of the sub items
|
|
y: subItemsRect.top, // top position of the sub items
|
|
},
|
|
{
|
|
x: subItemsRect.left, // left position of the sub items
|
|
y: subItemsRect.top + subItemsRect.height, // bottom position of the sub items
|
|
},
|
|
];
|
|
|
|
if (isAbove) {
|
|
subItems.classList.add(IS_ABOVE_CLASS);
|
|
}
|
|
};
|
|
|
|
export const showSubLevelItems = (el) => {
|
|
const subItems = el.querySelector('.sidebar-sub-level-items');
|
|
const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only');
|
|
|
|
if (!canShowSubItems() || !canShowActiveSubItems(el)) return;
|
|
|
|
el.classList.add(IS_OVER_CLASS);
|
|
|
|
if (!subItems || (!isSidebarCollapsed() && isIconOnly)) return;
|
|
|
|
subItems.style.display = 'block';
|
|
el.classList.add(IS_SHOWING_FLY_OUT_CLASS);
|
|
|
|
setOpenMenu(subItems);
|
|
moveSubItemsToPosition(el, subItems);
|
|
};
|
|
|
|
export const mouseEnterTopItems = (el, timeout = getHideSubItemsInterval()) => {
|
|
clearTimeout(timeoutId);
|
|
|
|
timeoutId = setTimeout(() => {
|
|
if (currentOpenMenu) hideMenu(currentOpenMenu);
|
|
|
|
showSubLevelItems(el);
|
|
}, timeout);
|
|
};
|
|
|
|
export const mouseLeaveTopItem = (el) => {
|
|
const subItems = el.querySelector('.sidebar-sub-level-items');
|
|
|
|
if (
|
|
!canShowSubItems() ||
|
|
!canShowActiveSubItems(el) ||
|
|
(subItems && subItems === currentOpenMenu)
|
|
)
|
|
return;
|
|
|
|
el.classList.remove(IS_OVER_CLASS);
|
|
};
|
|
|
|
export const documentMouseMove = (e) => {
|
|
mousePos.push({
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
});
|
|
|
|
if (mousePos.length > 6) mousePos.shift();
|
|
};
|
|
|
|
export const subItemsMouseLeave = (relatedTarget) => {
|
|
clearTimeout(timeoutId);
|
|
|
|
if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
|
|
hideMenu(currentOpenMenu);
|
|
}
|
|
};
|
|
|
|
export default () => {
|
|
sidebar = document.querySelector('.nav-sidebar');
|
|
|
|
if (!sidebar) return;
|
|
|
|
const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
|
|
|
|
const topItems = sidebar.querySelector('.sidebar-top-level-items');
|
|
if (topItems) {
|
|
sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
|
|
clearTimeout(timeoutId);
|
|
|
|
timeoutId = setTimeout(() => {
|
|
if (currentOpenMenu) hideMenu(currentOpenMenu);
|
|
}, getHideSubItemsInterval());
|
|
});
|
|
}
|
|
|
|
items.forEach((el) => {
|
|
const subItems = el.querySelector('.sidebar-sub-level-items');
|
|
|
|
if (subItems) {
|
|
subItems.addEventListener('mouseleave', (e) => subItemsMouseLeave(e.relatedTarget));
|
|
}
|
|
|
|
el.addEventListener('mouseenter', (e) => mouseEnterTopItems(e.currentTarget));
|
|
el.addEventListener('mouseleave', (e) => mouseLeaveTopItem(e.currentTarget));
|
|
});
|
|
|
|
document.addEventListener('mousemove', documentMouseMove);
|
|
};
|