mirror of
https://github.com/tenrok/vue-context.git
synced 2026-06-15 01:42:24 +03:00
Release/v5 (#43)
This commit is contained in:
@@ -1 +1,2 @@
|
||||
export { default } from './vue-context';
|
||||
export { default as VueContext } from './vue-context';
|
||||
|
||||
+13
-1
@@ -17,7 +17,9 @@ export const isArray = Array.isArray;
|
||||
|
||||
export const keyCodes = {
|
||||
ESC: 27,
|
||||
LEFT: 37,
|
||||
UP: 38,
|
||||
RIGHT: 39,
|
||||
DOWN: 40
|
||||
};
|
||||
|
||||
@@ -51,7 +53,7 @@ export const filterVisible = elements => (elements || []).filter(isVisible);
|
||||
|
||||
// Return the Bounding Client Rect of an element
|
||||
// Returns `null` if not an element
|
||||
const getBCR = el => (isElement(el) ? el.getBoundingClientRect() : null);
|
||||
export const getBCR = el => (isElement(el) ? el.getBoundingClientRect() : null);
|
||||
|
||||
// Determine if an element is an HTML element
|
||||
const isElement = el => Boolean(el && el.nodeType === Node.ELEMENT_NODE);
|
||||
@@ -81,3 +83,13 @@ export const setAttr = (el, attr, value) => {
|
||||
el.setAttribute(attr, value);
|
||||
}
|
||||
};
|
||||
|
||||
export const parentElementByClassName = (element, className) => {
|
||||
let parentElement = element.parentElement;
|
||||
|
||||
while (parentElement !== null && !parentElement.classList.contains(className)) {
|
||||
parentElement = parentElement.parentElement;
|
||||
}
|
||||
|
||||
return parentElement;
|
||||
};
|
||||
|
||||
+119
-9
@@ -1,5 +1,15 @@
|
||||
import { directive as onClickaway } from 'vue-clickaway/index';
|
||||
import { eventOff, eventOn, filterVisible, isArray, keyCodes, selectAll, setAttr } from './utils';
|
||||
import {
|
||||
eventOff,
|
||||
eventOn,
|
||||
filterVisible,
|
||||
isArray,
|
||||
keyCodes,
|
||||
selectAll,
|
||||
setAttr,
|
||||
getBCR,
|
||||
parentElementByClassName
|
||||
} from './utils';
|
||||
import { normalizeSlot } from './normalize-slot';
|
||||
|
||||
export default {
|
||||
@@ -48,7 +58,8 @@ export default {
|
||||
left: null,
|
||||
show: false,
|
||||
data: null,
|
||||
localItemSelector: ''
|
||||
localItemSelector: '',
|
||||
activeSubMenu: null
|
||||
};
|
||||
},
|
||||
|
||||
@@ -67,12 +78,27 @@ export default {
|
||||
eventOn(window, 'scroll', this.close);
|
||||
},
|
||||
|
||||
addHoverEventListener(element) {
|
||||
element.querySelectorAll('.v-context__sub').forEach(
|
||||
subMenuNode => {
|
||||
eventOn(subMenuNode, 'mouseenter', this.openSubMenu);
|
||||
eventOn(subMenuNode, 'mouseleave', this.closeSubMenu);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
close() {
|
||||
if (! this.show) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure all sub menus are closed
|
||||
while (this.activeSubMenu !== null) {
|
||||
parentElementByClassName(this.activeSubMenu, 'v-context__sub').dispatchEvent(new Event('mouseleave'));
|
||||
}
|
||||
|
||||
this.resetData();
|
||||
this.removeHoverEventListener(this.$el);
|
||||
|
||||
if (this.closeOnScroll) {
|
||||
this.removeScrollEventListener();
|
||||
@@ -118,7 +144,8 @@ export default {
|
||||
},
|
||||
|
||||
getItems() {
|
||||
return filterVisible(selectAll(this.localItemSelector, this.$el));
|
||||
// if a sub menu is active only return the elements of the sub menu to keep the scope
|
||||
return filterVisible(selectAll(this.localItemSelector, this.activeSubMenu || this.$el));
|
||||
},
|
||||
|
||||
mapItemSelector(itemSelector) {
|
||||
@@ -147,6 +174,27 @@ export default {
|
||||
} else if (key === keyCodes.UP) {
|
||||
// Up arrow
|
||||
this.focusNext(event, true);
|
||||
} else if (key === keyCodes.RIGHT) {
|
||||
// check if a parent element which is associated with a sub menu can be found.
|
||||
const menuContainer = parentElementByClassName(event.target, 'v-context__sub');
|
||||
|
||||
// try to open a sub menu if the sub menu isn't the current sub menu
|
||||
if (menuContainer && menuContainer.getElementsByClassName('v-context')[0] !== this.activeSubMenu) {
|
||||
menuContainer.dispatchEvent(new Event('mouseenter'));
|
||||
this.focusNext(event, false);
|
||||
}
|
||||
} else if (key === keyCodes.LEFT) {
|
||||
if (!this.activeSubMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentMenu = parentElementByClassName(this.activeSubMenu, 'v-context__sub');
|
||||
parentMenu.dispatchEvent(new Event('mouseleave'));
|
||||
|
||||
const items = this.getItems(),
|
||||
index = items.indexOf(parentMenu.getElementsByTagName('a')[0]);
|
||||
|
||||
this.focusItem(index, items);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -155,9 +203,11 @@ export default {
|
||||
this.show = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.positionMenu(event.clientY, event.clientX);
|
||||
[this.top, this.left] = this.positionMenu(event.clientY, event.clientX, this.$el);
|
||||
|
||||
this.$el.focus();
|
||||
this.setItemRoles();
|
||||
this.addHoverEventListener(this.$el);
|
||||
|
||||
if (this.closeOnScroll) {
|
||||
this.addScrollEventListener();
|
||||
@@ -167,9 +217,61 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
positionMenu(top, left) {
|
||||
const largestHeight = window.innerHeight - this.$el.offsetHeight - 25;
|
||||
const largestWidth = window.innerWidth - this.$el.offsetWidth - 25;
|
||||
openSubMenu(event) {
|
||||
const subMenuElement = this.getSubMenuElementByEvent(event),
|
||||
parentMenu = parentElementByClassName(subMenuElement.parentElement, 'v-context'),
|
||||
bcr = getBCR(event.target);
|
||||
|
||||
// check if another sub menu is open. In this case make sure no other as well as no nested sub menu is open
|
||||
if (this.activeSubMenu !== parentMenu) {
|
||||
while (this.activeSubMenu !== null
|
||||
&& this.activeSubMenu !== parentMenu
|
||||
&& this.activeSubMenu !== subMenuElement
|
||||
) {
|
||||
parentElementByClassName(this.activeSubMenu, 'v-context__sub')
|
||||
.dispatchEvent(new Event('mouseleave'));
|
||||
}
|
||||
}
|
||||
|
||||
// first set the display and afterwards execute position calculation for correct element offsets
|
||||
subMenuElement.style.display = 'block';
|
||||
|
||||
let [elementTop, elementLeft] = this.positionMenu(bcr.top, bcr.right - 10, subMenuElement);
|
||||
|
||||
subMenuElement.style.left = `${elementLeft}px`;
|
||||
subMenuElement.style.top = `${elementTop}px`;
|
||||
|
||||
this.activeSubMenu = subMenuElement;
|
||||
},
|
||||
|
||||
closeSubMenu(event) {
|
||||
const subMenuElement = this.getSubMenuElementByEvent(event),
|
||||
parentMenu = parentElementByClassName(subMenuElement, 'v-context');
|
||||
|
||||
// if a sub menu is closed and it's not the currently active sub menu (eg. a lowe layered sub menu closed
|
||||
// by a mouseleave event) close all nested sub menus
|
||||
if (this.activeSubMenu !== subMenuElement) {
|
||||
while (this.activeSubMenu !== null && this.activeSubMenu !== subMenuElement) {
|
||||
parentElementByClassName(this.activeSubMenu, 'v-context__sub')
|
||||
.dispatchEvent(new Event('mouseleave'));
|
||||
}
|
||||
}
|
||||
|
||||
subMenuElement.style.display = 'none';
|
||||
|
||||
// check if a parent menu exists and the parent menu is a sub menu to keep track of the correct sub menu
|
||||
this.activeSubMenu = parentMenu && parentElementByClassName(parentMenu, 'v-context__sub')
|
||||
? parentMenu
|
||||
: null;
|
||||
},
|
||||
|
||||
getSubMenuElementByEvent (event) {
|
||||
return event.target.getElementsByTagName('ul')[0];
|
||||
},
|
||||
|
||||
positionMenu(top, left, element) {
|
||||
const largestHeight = window.innerHeight - element.offsetHeight - 25;
|
||||
const largestWidth = window.innerWidth - element.offsetWidth - 25;
|
||||
|
||||
if (top > largestHeight) {
|
||||
top = largestHeight;
|
||||
@@ -179,14 +281,22 @@ export default {
|
||||
left = largestWidth;
|
||||
}
|
||||
|
||||
this.top = top;
|
||||
this.left = left;
|
||||
return [top, left];
|
||||
},
|
||||
|
||||
removeScrollEventListener() {
|
||||
eventOff(window, 'scroll', this.close);
|
||||
},
|
||||
|
||||
removeHoverEventListener(element) {
|
||||
element.querySelectorAll('.v-context__sub').forEach(
|
||||
(subMenuNode) => {
|
||||
eventOff(subMenuNode, 'mouseenter', this.openSubMenu);
|
||||
eventOff(subMenuNode, 'mouseleave', this.closeSubMenu);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
resetData() {
|
||||
this.top = null;
|
||||
this.left = null;
|
||||
|
||||
+50
-33
@@ -1,47 +1,64 @@
|
||||
@import "config";
|
||||
|
||||
.v-context {
|
||||
background-color: $menu-bg;
|
||||
background-clip: padding-box;
|
||||
border-radius: .25rem;
|
||||
border: 1px solid $menu-border;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
min-width: 10rem;
|
||||
z-index: 1500;
|
||||
position: fixed;
|
||||
list-style: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
> li {
|
||||
&, & ul {
|
||||
background-color: $menu-bg;
|
||||
background-clip: padding-box;
|
||||
border-radius: .25rem;
|
||||
border: 1px solid $menu-border;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
min-width: 10rem;
|
||||
z-index: 1500;
|
||||
position: fixed;
|
||||
list-style: none;
|
||||
box-sizing: border-box;
|
||||
max-height: calc(100% - 50px);
|
||||
overflow-y: auto;
|
||||
|
||||
> a {
|
||||
display: block;
|
||||
padding: .5rem 1.5rem;
|
||||
font-weight: 400;
|
||||
color: $item-color;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
> li {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
> a {
|
||||
display: block;
|
||||
padding: .5rem 1.5rem;
|
||||
font-weight: 400;
|
||||
color: $item-color;
|
||||
text-decoration: none;
|
||||
color: $item-hover-color;
|
||||
background-color: $item-hover-bg;
|
||||
}
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
color: $item-hover-color;
|
||||
background-color: $item-hover-bg;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
&__sub {
|
||||
> a:after {
|
||||
content: "\2bc8";
|
||||
float: right;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
> ul {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user