2
0
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:
Randall Wilk
2019-11-14 13:38:00 -06:00
committed by GitHub
parent 39adea968a
commit a799c3788a
17 changed files with 303 additions and 356 deletions
+1
View File
@@ -1 +1,2 @@
export { default } from './vue-context';
export { default as VueContext } from './vue-context';
+13 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}
}