2
0
mirror of https://github.com/tenrok/vue-context.git synced 2026-05-21 17:14:04 +03:00
Files
vue-context/src/js/vue-context.js
T

281 lines
6.9 KiB
JavaScript

import { directive as onClickaway } from 'vue-clickaway/index';
import { eventOff, eventOn, filterVisible, isArray, keyCodes, selectAll, setAttr } from './utils';
import { normalizeSlot } from './normalize-slot';
import '../sass/vue-context.scss';
export default {
directives: {
onClickaway
},
props: {
closeOnClick: {
type: Boolean,
default: true
},
closeOnScroll: {
type: Boolean,
default: true
},
lazy: {
type: Boolean,
default: false
},
itemSelector: {
type: [String, Array],
default: () => ['.v-context-item', '.v-context > li > a']
},
role: {
type: String,
default: 'menu'
},
tag: {
type: String,
default: 'ul'
}
},
computed: {
style() {
return this.show
? { top: `${this.top}px`, left: `${this.left}px` }
: null;
}
},
data() {
return {
top: null,
left: null,
show: false,
data: null,
localItemSelector: ''
};
},
created() {
this.localItemSelector = this.mapItemSelector(this.itemSelector);
},
beforeDestroy() {
if (this.closeOnScroll) {
this.removeScrollEventListener();
}
},
methods: {
addScrollEventListener() {
eventOn(window, 'scroll', this.close);
},
close() {
if (! this.show) {
return;
}
this.resetData();
if (this.closeOnScroll) {
this.removeScrollEventListener();
}
this.$emit('close');
},
focusItem(index, items) {
const el = items.find((el, idx) => idx === index);
if (el && el.focus) {
el.focus();
}
},
focusNext(event, up) {
if (! this.show) {
return;
}
event.preventDefault();
event.stopPropagation();
this.$nextTick(() => {
const items = this.getItems();
if (items.length < 1) {
return;
}
let index = items.indexOf(event.target);
if (up && index > 0) {
index--;
} else if (! up && index < items.length - 1) {
index++;
}
if (index < 0) {
index = 0;
}
this.focusItem(index, items);
});
},
getItems() {
return filterVisible(selectAll(this.localItemSelector, this.$el));
},
mapItemSelector(itemSelector) {
if (isArray(itemSelector)) {
itemSelector = itemSelector
.map(selector => `${selector}:not(.disabled):not([disabled])`)
.join(', ');
}
return itemSelector;
},
onClick() {
this.close();
},
onKeydown(event) {
const key = event.keyCode;
if (key === keyCodes.ESC) {
// Close on esc
this.close();
} else if (key === keyCodes.DOWN) {
// Down arrow
this.focusNext(event, false);
} else if (key === keyCodes.UP) {
// Up arrow
this.focusNext(event, true);
}
},
open(event, data) {
this.data = data;
this.show = true;
this.$nextTick(() => {
this.positionMenu(event.clientY, event.clientX);
this.$el.focus();
this.setItemRoles();
if (this.closeOnScroll) {
this.addScrollEventListener();
}
this.$emit('open', event, this.data, this.top, this.left);
});
},
positionMenu(top, left) {
const largestHeight = window.innerHeight - this.$el.offsetHeight - 25;
const largestWidth = window.innerWidth - this.$el.offsetWidth - 25;
if (top > largestHeight) {
top = largestHeight;
}
if (left > largestWidth) {
left = largestWidth;
}
this.top = top;
this.left = left;
},
removeScrollEventListener() {
eventOff(window, 'scroll', this.close);
},
resetData() {
this.top = null;
this.left = null;
this.data = null;
this.show = false;
},
setItemRoles() {
// Add role="menuitem" and tabindex="-1" to all items
selectAll(this.localItemSelector, this.$el)
.forEach(el => {
setAttr(el, 'role', 'menuitem');
setAttr(el, 'tabindex', '-1');
});
}
},
watch: {
closeOnScroll(newValue, oldValue) {
if (newValue === oldValue) {
return;
}
if (newValue && this.show) {
this.addScrollEventListener();
} else {
this.removeScrollEventListener();
}
},
itemSelector(selector, oldValue) {
if (selector !== oldValue) {
this.localItemSelector = this.mapItemSelector(selector);
}
}
},
render(h) {
if (this.lazy && ! this.show) {
return h(false);
}
// Only register the events we need
const on = {
// `!` modifier for capture
'!contextmenu': e => {
e.preventDefault();
},
keydown: this.onKeydown // up, down, esc
};
if (this.closeOnClick) {
on.click = this.onClick;
}
// Only register the directives we need
const directives = [
{
name: 'on-clickaway',
value: this.close,
rawName: 'v-on-clickaway'
}
];
if (! this.lazy) {
directives.push({
name: 'show',
value: this.show,
rawName: 'v-show',
expression: 'show'
});
}
return h(
this.tag,
{
staticClass: 'v-context',
style: this.style,
attrs: {
tabindex: '-1',
role: this.role,
'aria-hidden': this.lazy ? null : String(! this.show)
},
on,
directives
},
[normalizeSlot('default', { data: this.data }, this.$scopedSlots, this.$slots)]
);
}
};