mirror of
https://github.com/tenrok/vue-context.git
synced 2026-05-21 17:14:04 +03:00
281 lines
6.9 KiB
JavaScript
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)]
|
|
);
|
|
}
|
|
};
|