Vue.js Events: How to detect if key is pressed while the mouse is over a certain element? - vue.js

I am currently working on a multi item selection component in Vue.js 3.
Each item is represented by a <div> element containing a checkbox.
For a feature I want to implement I need to detect when the Shift key is pressed while the mouse is over a <div>. (My goal is to have all the items between the item checked last and the item currently under the mouse highlighted while the Shift key is pressed, and then have them all checked with Shift + Click - but that is beside the point).
I tried to achieve this using #mouseover.shift like so:
<div v-for="item in items"
class="item"
#mouseover.shift="onMouseOverShift(item)"
>
Unfortunately, while #mouseover.shift works fine to detect whether Shift is pressed when the mouse enters the <div>, but does not trigger when the key is pressed while the mouse is already inside of the <div>.
Do you know what would be the best way to achieve what i am looking for?

I think you can use like this:
<div v-for="item in items"
class="item"
#mouseenter="isMouseOver = true"
#mouseleave="isMouseOver = false"
#keypress.shift="onMouseOverShift(item)"
/>
Then on the method onMouseOverShift you check if isMouseOver

I don't think you can detect a keypress on a div, or at least not reliably. You best bet is probably to combine the mouse events with a global key watcher.
In Vue 3 composition API, this would look something like this:
In your component setup:
const hoveredItem = ref(null)
const shiftPressed = useShiftKeyWatcher()
const selectedItem = computed(() => shiftPressed.value ? hoveredItem.value : null)
In the template:
<div v-for="item in items"
class="item"
#mouseenter="selectedItem = item"
#mouseleave="selectedItem = null"
>...</div>
Then the composable for useShiftKeyWatcher would look like:
import { ref, onMounted, onUnmounted } from "vue";
export function useShiftKeyWatcher() {
const shiftPressed = ref(false);
function checkKeyDown(event) {
shiftPressed.value = event.key === "Shift";
}
function checkKeyUp(event) {
shiftPressed.value &&= event.key !== "Shift";
}
onMounted(() => window.addEventListener("keydown", checkKeyDown));
onMounted(() => window.addEventListener("keyup", checkKeyUp));
onUnmounted(() => window.removeEventListener("keydown", checkKeyDown));
onUnmounted(() => window.removeEventListener("keyup", checkKeyUp));
return shiftPressed;
}
Here is a sandbox (you need to click into the preview area once to have key events go to the inner document)

Related

How to wait for click button to continue function

I have a function with a popup dialog window with button. I want to wait with executing the rest of the function until the button is clicked. I tried it with Promise and AddEventListener, but cannot find out why is it not working. Could someone help me? (I use Vue3 and Quasar)
I have an error for "const confirm" - Object is possibly 'null'.ts(2531)
Thank you for any advices.
Here is a part of my template:
<q-dialog persistent v-model="phoneDialogBank">
<q-card>
<q-card-section>
<div class="items" v-for="formField in dynamicDialogFieldsVisible" :key="formField?.Key">
<dynamic-form-field :form-field="formField"></dynamic-form-field>
</div>
<div class="row items-center justify-center">
<q-btn color="primary" class="confirm-phone-dialog" v-close-popup>
{{ t('signing-table.phoneDialogBank.ok') }}
</q-btn>
</div>
</q-card-section>
</q-card>
</q-dialog>
Here is my function:
async function dynamicDialogBeforeSubmit() {
const params = getParamsFromAdvisorDeviceBoolean();
if (params && params.hasOwnProperty('dialogBeforeSubmit') && params.dialogBeforeSubmit) {
phoneDialogBank.value = true;
const confirm = document.querySelector('.confirm-phone-dialog');
const waitForButtonClick = new Promise((resolve) => { confirm.addEventListener('click', resolve); });
await waitForButtonClick.then(() => {
dynamicDialogSave();
});
return;
}
dynamicDialogSave();
}
That error is because your button is within a dialog that renders conditionally. So if there is no dialog, that DOM element does not exist. I'm curious why you are not using some of the great features of Vue? Like putting the click handler on the element and using refs to target the DOM instead of using query selectors and event listeners.
<q-btn color="primary" class="confirm-phone-dialog" #click.prevent="dynamicDialogBeforeSubmit" v-close-popup>
{{ t('signing-table.phoneDialogBank.ok') }}
</q-btn>
Your function is a bit mixed up between async and Promises. async / await is just syntactic sugar for a Promise. The way you have it written now, you are wrapping a Promise within another Promise.
async function dynamicDialogBeforeSubmit() {
const params = getParamsFromAdvisorDeviceBoolean();
if (params && params.hasOwnProperty('dialogBeforeSubmit') && params.dialogBeforeSubmit) {
phoneDialogBank.value = true;
await dynamicDialogSave();
} else {
console.log('There was a problem')
}
}
I should note that you will likely need to pass an object to dynamicDialogSave(someObject) for it to actually save something. I assume this, not knowing what that function actually looks like.
Event listeners are not asynchronous and you wouldn't want to wrap them in a promise anyways. If you wish to write it as you have, it would be best to declare the click listener as a separate function that fires onMounted, then call it from any async function. You should also detach native event listeners onUnmounted to avoid memory leaks with Vue.

How to stop v-click-outside from triggering on internal v-select components

I've been trying to make a card component that closes if you click anywhere on the outside. But the issue is that v-click-outside gets triggered when I click on an item in the v-select dropdown menu inside of that card. How do I prevent child components from triggering v-click-outside?
<v-card v-click-outside="handleClickOutside">
<v-select
:items="items"
v-model="currentItem"
></v-select>
</v-card>
According to v-click-outside documentation, it accepts "A callback or options object.". Object looks like this:
{
handler: (e: Event) => void,
closeConditional ? : (e: Event) => boolean,
include ? : () => HTMLElement[]
}
For handler pass your function and for include you have to specify HTML element (example). To prevent trigger v-click-outside when you are using v-select you need to include .v-select-list.
In your case the solution will look like this:
<v-card v-click-outside="{
handler: handleClickOutside,
include: includeFunction
}">
...
<script>
...
methods: {
includeFunction(){
//visible elements
const tempArray = [document.querySelector('.included')]
if (document.querySelector('.v-select-list')) {
//push the element to array when it is visible
arr.push(document.querySelector('.v-select-list'))
}
return tempArray
}
...
}
</script>

Impossible to click on a specific button part of a class with vue-test-utils

I want to test an editable table with vue-test-utils. There are edit buttons for each row, and when an edit button is clicked, the table row expands and allow the user to input information.
Each edit button has the class "editAddress", this is what I've tried so far :
describe('Customers Addresses Table', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(AddressesTable);
});
it('can edit an address',() => {
const firstEditAddressButton = wrapper.findAll('button.editAddress').at(0);
firstEditAddressButton.trigger('click');
console.log(firstEditAddressButton.html());
expect(wrapper.html()).toContain('Close');
});
});
The test fails because it doesn't seem the button has been clicked..
The html code of my table looks like this (I use bootstrap-vue table here):
<div class="panel panel-default">
<b-table>
Address Edition-->
<template v-slot:cell(name)="data">
<div>
<button class="editAddress" #click="data.toggleDetails(); buttonEditClicked(data.item)">{{ data.item.name }}</button>
</div>
</template>
</b-table>
</div>
After the edit button being clicked, the table should transform like this :
Maybe I am not fetching the first button of the class "editAddress" correctly, I don't have any idea..
Thanks for your help !
Try that
it('can edit an address', async () => {
const firstEditAddressButton = wrapper.findAll('button.editAddress').at(0);
firstEditAddressButton.trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.html()).toContain('Close');
});

Mutating a Prop in Vue JS outside component

This is my first Vue app so go easy on the code :P
I have a list of users on one side and a component to edit those user details on the other. When selecting a user you see their details and can then click 'edit details'. I want to hide the edit details and show the user details when a new user is selected. Inside the component, I have a ShowEdit variable that is true or false and will either show or hide the edit area. I am sending a prop from the parent into this component when a new user is selected to hide the edit if it is open. I feel I am close as it is currently working perfect but I would like to get rid of the error
"Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders...."
Here are the important bits:
Home
<transition name="component-fade" mode="out-in">
<sidebar-client-content :theClient="selectedUser" :showEditHP="showEditHP"></sidebar-client-content>
</transition>
activate: function(el) {
this.showEditHP = true // Hide edit if new user selected
},
Component
<div id="sidebar-client-content">
<div class="staff_header">
<a v-if="showEdit" v-on:click="showEdit = !showEdit"><i class="fal fa-edit"></i>Edit</a>
<a v-if="!showEdit" v-on:click="showEdit = !showEdit"><i class="far fa-times"></i>Close</a>
</div>
<transition-group name="fadeHeight" mode="out-in">
<div class="client_information" v-if="showEdit">
<!-- The Client Details -->
</div>
<div class="client_information" v-if="!showEdit">
<!-- Client Edit Form -->
</div>
</transition-group>
</div>
export default {
props: [
'showEditHP', // Close edit if new user selected
],
computed: {
showEdit: {
get: function() {
return this.showEditHP
},
set: function (newValue) {
this.showEditHP = newValue
}
}
},
I understand the this.showEditHP = newValue line is where I make the edit but I can't seem to get it to work any other way. I want the parent to be able to overwrite it as the error says. Is there a way to achieve this and have the error removed?
Thanks.
As Nguyun said you can use $emit to send the value back to your parent to keep the values in sync. You can only change your parent data with emit otherwise it will just remain either true or false while the child continues to make it's changes. Keep the values in sync with emit and then use watch to check if the parent makes it's change.
<transition name="component-fade" mode="out-in">
<sidebar-client-content #clicked="onClickChild" :theClient="selectedUser" :showEditHP="showEditHP"></sidebar-client-content>
</transition>
onClickChild (value) {
// I am the child value inside the parent!
},
Then in the child
<div class="staff_header">
<a v-if="showEdit" v-on:click="showEdit = !showEdit; $emit('clicked', showEdit)"><i class="fal fa-edit"></i>Edit</a>
<a v-if="!showEdit" v-on:click="showEdit = !showEdit; $emit('clicked', showEdit)"><i class="far fa-times"></i>Close</a></i>Close</a>
</div>
The warning is really clear
In your child component, you're trying to mutate the value of showEditHP:
set: function (newValue) {
this.showEditHP = newValue
}
However, this kind of mutating is discouraged. Instead, we should emit an event from the child component and then let the parent component listen to that event and mutate the property itself.
In other words, the property showEditHP belongs to the parent component, well then the parent component should be the one which mutates it.

How to focus and activate a vue multiselect?

When the user double-clicks this:
<div
class="open-select"
v-if="!editable"
#dblclick="editable=true">{{ name }}
</div>
I'd like this multiselect to be open and focused:
<multiselect
v-else
v-model="name"
:options="names"
track-by="id"
tabindex="0"
autofocus
#select="editable=false"
></multiselect>
The double-click event shows the multiselect element fine, but the multiselect still requires the user to click it to open. I'd like it to open automatically after appearing.
Things I've tried:
focusing the multiselect:
tabindex="0"
autofocus
When I try to select the focused item in jQuery, $(':focus')[0], I get 'undefined'
Heyo!
You can put a ref on the component and then trigger focus which will open the dropdown.
<multiselect ref="vms" v-bind="yourAttributes" />
And then in your created hook you add
this.$refs.vms.$el.focus()
A simplest solution - Toggle Vue Multiselect dropdown
You can use 2 events (#open, #close) on VueMultiselect
anda ref in multiselect
like
ref="multiselect"
keep in
data(){
isOpen: false
}
then add to the 2 events
#close="isOpen = false"
#open="isOpen = true"
and use a method
toggle() {
if (this.isOpen) {
this.$refs.multiselect.$el.blur()
this.isOpen = false
} else {
this.$refs.multiselect.$el.focus()
this.isOpen = true
}
}
Finally figured out how to do this (ref didn't work for me):
STEP 1: I was focusing on the wrong element.
So a vue-multiselect element is structured like this (shorthand to only show important parts):
<div class="multiselect"> // <= This is the element to focus on
<div class="multiselect__tags">
<input> // <= Not the input
</div>
</div>
Typically you want to put your tabindex on the input, instead you should put it on the parent with a class of multiselect. This also goes for things like jQuery's focus(). So...
No: $('.multiselect input').focus()
Yes: $('.multiselect').focus()
STEP 2: Correcting tabindex.
Another issue is when vue-multiselect puts a tabindex="-1" on all .multiselect elements. This removes them from the natural order of tabindexes, so you need to reassign all the tabindexes:
In mounted and updated (if necessary) you need code to reassign all the tabindexes:
mounted: function() {
$(document).ready(()=>{
// you may need to delete all tabindexes first.
$('[tabindex]').each( (i,v) => {
$(v).removeAttr('tabindex');
return true;
});
// add new tabindexes
$('input, textarea, button').each(( i,v ) => {
var isMultiSelect = $(v).hasClass('multiselect__input');
if(isMultiSelect){
$(v).parents('.multiselect:first').attr('tabindex', i + 1);
else{
$(v).attr('tabindex', i + 1);
}
});
});
}