mirror of
https://github.com/tenrok/vue-context.git
synced 2026-06-24 21:20:33 +03:00
Accessibility (#22)
This commit is contained in:
+4
-1
@@ -9,4 +9,7 @@ indent_size = 4
|
|||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.scss]
|
||||||
|
insert_final_newline = false
|
||||||
|
|||||||
+20
-1
@@ -2,6 +2,25 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented here.
|
All notable changes to this project will be documented here.
|
||||||
|
|
||||||
|
<a name="4.0.0"></a>
|
||||||
|
## [4.0.0](https://github.com/rawilk/vue-context/releases/tag/4.0.0)
|
||||||
|
|
||||||
|
Released 2019-05-18
|
||||||
|
|
||||||
|
### Added 4.0.0
|
||||||
|
- Added support for keyboard navigation (up and down arrows).
|
||||||
|
- Added ability to close menu on esc.
|
||||||
|
- Added `lazy` prop as an alternative to `v-show`.
|
||||||
|
- Added `tag` prop to specify menu tag (defaults to `<ul>`).
|
||||||
|
|
||||||
|
### Changes 4.0.0
|
||||||
|
- Default menu tag is now `<ul>` and menu is now the top-level element.
|
||||||
|
- Changed how the menu is styled.
|
||||||
|
|
||||||
|
### Updates 4.0.0
|
||||||
|
- Updated build process and project structure.
|
||||||
|
- Ran `npm audit fix` to fix vulnerabilities found from dependencies.
|
||||||
|
|
||||||
<a name="3.4.2"></a>
|
<a name="3.4.2"></a>
|
||||||
## [3.4.2](https://github.com/rawilk/vue-context/releases/tag/3.4.2)
|
## [3.4.2](https://github.com/rawilk/vue-context/releases/tag/3.4.2)
|
||||||
|
|
||||||
@@ -117,4 +136,4 @@ Released 2017-08-18
|
|||||||
|
|
||||||
Released 2017-08-17
|
Released 2017-08-17
|
||||||
|
|
||||||
- Initial release
|
- Initial release
|
||||||
|
|||||||
@@ -85,6 +85,5 @@ Please follow these coding conventions when contributing to the project.
|
|||||||
hash arrows.
|
hash arrows.
|
||||||
- I leave an empty line between code and return statements.
|
- I leave an empty line between code and return statements.
|
||||||
- I ALWAYS put spaces between properties in an object (`{ VueContext }`, not `{VueContext}`).
|
- I ALWAYS put spaces between properties in an object (`{ VueContext }`, not `{VueContext}`).
|
||||||
- I always a function declaration and its parameters (`methodName ()`, not `methodName()`).
|
|
||||||
- I always use single quotes over double quotes, unless it makes sense to use double quotes. If that's the case, I usually prefer to
|
- I always use single quotes over double quotes, unless it makes sense to use double quotes. If that's the case, I usually prefer to
|
||||||
use template strings instead of double quotes (`` `${variable} some text that has a single quote ' in it` `` instead of `variable + " some text that has a single quote ' in it"`).
|
use template strings instead of double quotes (`` `${variable} some text that has a single quote ' in it` `` instead of `variable + " some text that has a single quote ' in it"`).
|
||||||
|
|||||||
@@ -7,16 +7,12 @@
|
|||||||
[](https://vuejs.org)
|
[](https://vuejs.org)
|
||||||
|
|
||||||
`vue-context` provides a simple yet flexible context menu for Vue. It is styled for the standard `<ul>` tag, but any menu template can be used.
|
`vue-context` provides a simple yet flexible context menu for Vue. It is styled for the standard `<ul>` tag, but any menu template can be used.
|
||||||
The menu is lightweight with its only dependency being `vue-clickaway`. The menu has some basic styles applied to it but they can be easily
|
The menu is lightweight with its only dependencies being `vue-clickaway` and `core-js`. The menu has some basic styles applied to it but they can be easily
|
||||||
overridden by your own styles.
|
overridden by your own styles.
|
||||||
<br><br>
|
<br><br>
|
||||||
The menu disappears when you expect by utilizing `vue-clickaway` and it also optionally disappears when clicked on.
|
The menu disappears when you expect by utilizing `vue-clickaway` and it also optionally disappears when clicked on.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Note
|
|
||||||
The API has changed since the last major release. Check [v2 documentation](https://vue-context.com/docs/2.0/overview)
|
|
||||||
if you use the old version.
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@@ -63,10 +59,12 @@ Next add an element to the page that will trigger the context menu to appear, an
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<vue-context ref="menu">
|
<vue-context ref="menu">
|
||||||
<ul>
|
<li>
|
||||||
<li @click="onClick($event.target.innerText)">Option 1</li>
|
<a href="#" @click.prevent="onClick($event.target.innerText)">Option 1</a>
|
||||||
<li @click="onClick($event.target.innerText)">Option 2</li>
|
</li>
|
||||||
</ul>
|
<li>
|
||||||
|
<a href="#" @click.prevent="onClick($event.target.innerText)">Option 2</a>
|
||||||
|
</li>
|
||||||
</vue-context>
|
</vue-context>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ const inProduction = mix.inProduction();
|
|||||||
|
|
||||||
mix
|
mix
|
||||||
.setPublicPath('dist')
|
.setPublicPath('dist')
|
||||||
.js('src/index.js', 'vue-context.js')
|
.js('src/js/index.js', 'js/vue-context.js')
|
||||||
.sourceMaps(! inProduction)
|
.sourceMaps(! inProduction)
|
||||||
.webpackConfig({
|
.webpackConfig({
|
||||||
output: {
|
output: {
|
||||||
libraryTarget: 'umd',
|
libraryTarget: 'umd',
|
||||||
umdNamedDefine: true
|
umdNamedDefine: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Generated
+223
-1111
File diff suppressed because it is too large
Load Diff
+6
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-context",
|
"name": "vue-context",
|
||||||
"version": "3.4.2",
|
"version": "4.0.0",
|
||||||
"description": "A simple vue context menu component.",
|
"description": "A simple vue context menu component.",
|
||||||
"main": "dist/vue-context.js",
|
"main": "dist/js/vue-context.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --env.mixfile=build/webpack.mix.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
"dev": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --env.mixfile=build/webpack.mix.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||||
"dev-test": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --env.mixfile=build/webpack-test.mix.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
"dev-test": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --env.mixfile=build/webpack-test.mix.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||||
@@ -24,15 +24,17 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/rawilk/vue-context/issues"
|
"url": "https://github.com/rawilk/vue-context/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/rawilk/vue-context#readme",
|
"homepage": "https://vue-context.com/docs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"core-js": ">=2.6.5 <3.0.0",
|
||||||
"vue-clickaway": "^2.2.2"
|
"vue-clickaway": "^2.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-env": "^7.4.3",
|
"@babel/preset-env": "^7.4.3",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"laravel-mix": "^4.0.15",
|
"laravel-mix": "^4.0.15",
|
||||||
"node-sass": "^4.11.0",
|
"resolve-url-loader": "^2.3.1",
|
||||||
|
"sass": "^1.20.1",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-template-compiler": "^2.6.10"
|
"vue-template-compiler": "^2.6.10"
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.9 KiB |
@@ -1,3 +0,0 @@
|
|||||||
import VueContext from './vue-context';
|
|
||||||
|
|
||||||
export { VueContext };
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as VueContext } from './vue-context';
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export const normalizeSlot = (name, scope = {}, $scopedSlots = {}, $slots = {}) => {
|
||||||
|
// Note: in Vue 2.6.x, all named slots are also scoped slots
|
||||||
|
const slot = $scopedSlots[name] || $slots[name];
|
||||||
|
|
||||||
|
return typeof slot === 'function' ? slot(scope) : slot;
|
||||||
|
};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import fromPolyfill from 'core-js/library/fn/array/from';
|
||||||
|
import isArrayPolyfill from 'core-js/library/fn/array/is-array';
|
||||||
|
|
||||||
|
// --- Constants ---
|
||||||
|
const arrayFrom = Array.from || fromPolyfill;
|
||||||
|
|
||||||
|
export const isArray = Array.isArray || isArrayPolyfill;
|
||||||
|
|
||||||
|
export const keyCodes = {
|
||||||
|
ESC: 27,
|
||||||
|
UP: 38,
|
||||||
|
DOWN: 40
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Dom Utils ---
|
||||||
|
|
||||||
|
// Returns true if the parent element contains the child element
|
||||||
|
const contains = (parent, child) => {
|
||||||
|
if (! parent || typeof parent.contains !== 'function') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent.contains(child);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach an event listener to an element
|
||||||
|
export const eventOn = (el, eventName, handler) => {
|
||||||
|
if (el && el.addEventListener) {
|
||||||
|
el.addEventListener(eventName, handler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove an event listener from an element
|
||||||
|
export const eventOff = (el, eventName, handler) => {
|
||||||
|
if (el && el.removeEventListener) {
|
||||||
|
el.removeEventListener(eventName, handler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter visible elements
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Determine if an element is an HTML element
|
||||||
|
const isElement = el => Boolean(el && el.nodeType === Node.ELEMENT_NODE);
|
||||||
|
|
||||||
|
// Determine if an HTML element is visible - Faster than CSS check
|
||||||
|
const isVisible = el => {
|
||||||
|
if (! isElement(el) || ! contains(document.body, el)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.style.display === 'none') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bcr = getBCR(el);
|
||||||
|
|
||||||
|
return Boolean(bcr && bcr.height > 0 && bcr.width > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Select all elements matching a selector. Returns `[]` if none found
|
||||||
|
export const selectAll = (selector, root) =>
|
||||||
|
arrayFrom((isElement(root) ? root : document).querySelectorAll(selector));
|
||||||
|
|
||||||
|
// Set an attribute on an element
|
||||||
|
export const setAttr = (el, attr, value) => {
|
||||||
|
if (attr && isElement(el)) {
|
||||||
|
el.setAttribute(attr, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
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: this.itemSelector
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (isArray(this.localItemSelector)) {
|
||||||
|
this.localItemSelector = this.localItemSelector
|
||||||
|
.map(selector => `${selector}:not(.disabled):not([disabled])`)
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.closeOnScroll) {
|
||||||
|
this.removeScrollEventListener();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
addScrollEventListener() {
|
||||||
|
eventOn(window, 'scroll', this.close);
|
||||||
|
},
|
||||||
|
|
||||||
|
close(emit = true) {
|
||||||
|
if (! this.show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resetData();
|
||||||
|
|
||||||
|
if (this.closeOnScroll) {
|
||||||
|
this.removeScrollEventListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emit) {
|
||||||
|
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));
|
||||||
|
},
|
||||||
|
|
||||||
|
onClick() {
|
||||||
|
this.close(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
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)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
$menu-bg: #fff;
|
||||||
|
$menu-border: rgba(0, 0, 0, 0.15);
|
||||||
|
$item-color: #212529;
|
||||||
|
$item-hover-bg: #f8f9fa;
|
||||||
|
$item-hover-color: #212529;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
@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;
|
||||||
|
|
||||||
|
> li {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
> a {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: .50rem 1.5rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $item-color;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
color: $item-hover-color;
|
||||||
|
background-color: $item-hover-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="v-context"
|
|
||||||
v-show="show"
|
|
||||||
:style="style"
|
|
||||||
tabindex="-1"
|
|
||||||
v-on-clickaway="close"
|
|
||||||
@click="onClick"
|
|
||||||
@contextmenu.capture.prevent
|
|
||||||
>
|
|
||||||
<slot :data="data"></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
mixins: [clickaway],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
/**
|
|
||||||
* Close the menu on click.
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
closeOnClick: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the menu automatically on window scroll.
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
closeOnScroll: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
/**
|
|
||||||
* Generate the CSS styles for positioning the context menu.
|
|
||||||
*
|
|
||||||
* @returns {object|null}
|
|
||||||
*/
|
|
||||||
style () {
|
|
||||||
return this.show
|
|
||||||
? { top: `${this.top}px`, left: `${this.left}px` }
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
top: null,
|
|
||||||
left: null,
|
|
||||||
show: false,
|
|
||||||
data: null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeDestroy () {
|
|
||||||
if (this.closeOnScroll) {
|
|
||||||
this.removeScrollEventListener();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
/**
|
|
||||||
* Add scroll event listener to close context menu.
|
|
||||||
*/
|
|
||||||
addScrollEventListener () {
|
|
||||||
window.addEventListener('scroll', this.close);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the context menu.
|
|
||||||
*
|
|
||||||
* @param {boolean|Event} emit Used to prevent event being emitted twice from when menu is clicked and closed
|
|
||||||
*/
|
|
||||||
close (emit = true) {
|
|
||||||
if (! this.show) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.top = null;
|
|
||||||
this.left = null;
|
|
||||||
this.data = null;
|
|
||||||
this.show = false;
|
|
||||||
|
|
||||||
if (this.closeOnScroll) {
|
|
||||||
this.removeScrollEventListener();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emit) {
|
|
||||||
this.$emit('close');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the menu if `closeOnClick` is set to true.
|
|
||||||
*/
|
|
||||||
onClick () {
|
|
||||||
if (this.closeOnClick) {
|
|
||||||
this.close(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the context menu.
|
|
||||||
*
|
|
||||||
* @param {MouseEvent} event
|
|
||||||
* @param {array|object|string} data User provided data for the menu
|
|
||||||
*/
|
|
||||||
open (event, data) {
|
|
||||||
this.data = data;
|
|
||||||
this.show = true;
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.positionMenu(event.clientY, event.clientX);
|
|
||||||
this.$el.focus();
|
|
||||||
|
|
||||||
if (this.closeOnScroll) {
|
|
||||||
this.addScrollEventListener();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('open', event, this.data, this.top, this.left);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the context menu top and left positions.
|
|
||||||
*
|
|
||||||
* @param {number} top
|
|
||||||
* @param {number} 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;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the scroll event listener to close the context menu.
|
|
||||||
*/
|
|
||||||
removeScrollEventListener () {
|
|
||||||
window.removeEventListener('scroll', this.close);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
/**
|
|
||||||
* Add or remove the scroll event listener when the prop value changes.
|
|
||||||
*
|
|
||||||
* @param {boolean} closeOnScroll
|
|
||||||
* @param {boolean} oldValue
|
|
||||||
*/
|
|
||||||
closeOnScroll (closeOnScroll, oldValue) {
|
|
||||||
if (closeOnScroll === oldValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closeOnScroll && this.show) {
|
|
||||||
this.addScrollEventListener();
|
|
||||||
} else {
|
|
||||||
this.removeScrollEventListener();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
$blue600: #1e88e5;
|
|
||||||
$gray74: #bdbdbd;
|
|
||||||
$gray93: #ededed;
|
|
||||||
$gray98: #fafafa;
|
|
||||||
|
|
||||||
.v-context {
|
|
||||||
background: $gray98;
|
|
||||||
border: 1px solid $gray74;
|
|
||||||
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: 0;
|
|
||||||
position: fixed;
|
|
||||||
width: 250px;
|
|
||||||
z-index: 99999;
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 10px 0;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin: 0;
|
|
||||||
padding: 10px 35px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $blue600;
|
|
||||||
color: $gray98;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
+37
-13
@@ -3,22 +3,46 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Vue Context Test Page</title>
|
<title>Vue Context Test Page</title>
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body style="height:2000px;">
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<p @contextmenu.prevent="$refs.menu.open($event, { foo: 'bar' })">
|
<div class="container py-3 mt-5">
|
||||||
Right click on me
|
<ul class="list-group">
|
||||||
</p>
|
<li v-for="(item, index) in items" :key="index"
|
||||||
|
class="list-group-item"
|
||||||
<vue-context ref="menu" :close-on-scroll="close"
|
@contextmenu.prevent="$refs.menu.open"
|
||||||
@close="onClose"
|
>
|
||||||
@open="onOpen"
|
{{ item }}
|
||||||
>
|
</li>
|
||||||
<ul slot-scope="child">
|
|
||||||
<li @click="onClick(child.data)">Option 1 {{ child.data && child.data.foo }}</li>
|
|
||||||
<li>Option 2</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</vue-context>
|
|
||||||
|
<vue-context ref="menu">
|
||||||
|
<template slot-scope="child">
|
||||||
|
<li tabindex="0">
|
||||||
|
<a href="#" class="v-context-item"
|
||||||
|
@click.prevent="onClick('item 1')"
|
||||||
|
>
|
||||||
|
Do something
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="v-context-item"
|
||||||
|
@click.prevent="onClick('item 2')"
|
||||||
|
>
|
||||||
|
Do something else
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="v-context-item"
|
||||||
|
@click.prevent="onClick('item 3')"
|
||||||
|
>
|
||||||
|
Another option
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</vue-context>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/dist/index.js"></script>
|
<script src="js/dist/index.js"></script>
|
||||||
|
|||||||
Vendored
+713
-10
File diff suppressed because one or more lines are too long
+12
-12
@@ -1,5 +1,6 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { VueContext } from '../../../dist/vue-context';
|
import { VueContext } from '../../../src/js/index';
|
||||||
|
// import { VueContext } from '../../../dist/js/vue-context';
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
components: {
|
components: {
|
||||||
@@ -7,20 +8,19 @@ new Vue({
|
|||||||
},
|
},
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
close: true
|
close: true,
|
||||||
|
items: [
|
||||||
|
'Cras justo odio',
|
||||||
|
'Dapibus ac facilisis in',
|
||||||
|
'Morbi leo risus',
|
||||||
|
'Porta ac consectetur ac',
|
||||||
|
'Vestibulum at eros'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onClick (data) {
|
onClick (text) {
|
||||||
// console.log(data);
|
alert(text);
|
||||||
},
|
},
|
||||||
|
|
||||||
onClose () {
|
|
||||||
console.log('closing');
|
|
||||||
},
|
|
||||||
|
|
||||||
onOpen (event, data, top, left) {
|
|
||||||
console.log(data, top, left);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}).$mount('#app');
|
}).$mount('#app');
|
||||||
|
|||||||
Reference in New Issue
Block a user