How to prevent selection of a SortableJS item when clicking on one of its child elements? - vue.js

The Issue
I'm using SortableJS to build a draggable tree component. Which means each of my sortable-items has a toggle-arrow as a child element that opens and closes a sub-tree (if there is one).
I'm attempting to use stopPropagation() to prevent the selection of the parent sortable-item if the toggle-arrow is clicked, but it's not working.
It looks like this when closed:
And looks like this when open:
The blue highlight you see in the open state (the second image) is the styling I've chosen for the selectedClass option when using the multiDrag plugin.
This is illustrating that when I click on the toggle-arrow it results in the parent sortable-item being selected.
I don't want this to happen.
The Code
The code for an item in my SortableJS tree component looks like so (using Vue.js, and Pug syntax):
div.sortable-item
div.content
div.toggle-arrow(#click.stop="toggleTree($event)")
div.icon
div.title
div.sub-tree
And then I've got a handler for the #click binding on my toggle-arrow element:
toggleTree = function($event) {
$event.stopPropagation()
/// Code for handling the sub-tree toggling goes here.
/// The sub-tree toggling itself works just fine.
}
You can see that I'm declaring #click.stop as the event binding, which should stop the click event from bubbling up from the toggle-arrow child element, but it's not working.
I'm even attempting to use $event.stopPropagation within the handler. But, the event seems to continue to bubble, and thus the parent sortable-item element ends up in a selected state.
I've also tried declaring #click.native.stop as the event binding, but it simply prevents my toggleTree method from firing at all. I'm assuming there's another event handler somewhere within SortableJS that's interfering with the #click.native.stop binding.
Questions
How do I stop propagation of an event when a child element of my sortable-item is clicked?
How is selection handled by the multiDrag plugin? I dug through the code and saw that the select event is fired within the handler of the drop event of the sortable-item, but I'm confused by that. Why is the drop event handler being used to toggle selection of a sortable-item?
Thanks in advance for any light you may be able to shed on this.

Wrong Event
Looking at the source of SortableJS it seems that the event you want to stop from bubbling is not the click event, but rather the mouseup event.
The "Stuck" Drag Item Problem
As indicated in the comments of this answer, stopping propagation on the mouseup event causes an issue where the drag is started unintentionally, and the sortable-item becomes "stuck" to the pointer.
It seems that the "drag initiation" is triggered by either pointerdown, mousedown, or touchstart events, depending on the device.
It can be safely assumed that the pointerdown event is the one that does the triggering according to caniuse.com.
The Solution
So the actual way to solve this is to use a #pointerdown.stop event binding to trigger your toggleTree method without triggering either selection of the sortable-item, or the unintentional drag initiation.
div.sortable-item
div.content
div.toggle-arrow(#pointerdown.stop="toggleTree($event)")
div.icon
div.title
div.sub-tree

Change
div.toggle-arrow(#click.stop="toggleTree($event)")
to
div.toggle-arrow(#click.native.stop="toggleTree($event)")
If all you did in toggleTree was stopPropagation, you could have changed it to:
div.toggle-arrow(#click.native.stop)
Docs.
In short, you're currently stopping propagation on any emitted clicks from the child component (a.k.a. custom Vue event, which doesn't actually need propagation stopped as it doesn't bubble by default). What you want to do is call event.stopPropagation() on the native click event.
An alternative would be to use:
div.toggle-arrow(#click.native="toggleTree($event)")
... and call .stopPropagation() inside toggleTree. Which is precisely what .stop modifier does.

Related

Extending vuetify v-btn component, adding custom click event

I am trying to create component which would extend v-btn in such a way that every time I click a button, it should emit short beep, and disable the button for 5 seconds.
It would be ideal for the button to change color while disabled.
This is a problem, since color is a property, and I can't overwrite it's value...
Also, when I try to invoke super.click(e), I get an error.
You can check example here: https://codesandbox.io/s/elegant-glade-pnhqx
Your Btn component should just "use" v-btn rather than extending it.
v-bind="$attrs" is to copy any <btn>'s attribute onto <v-btn>.
#click event is captured and reemited as-is after doing what needs to be done
See https://codesandbox.io/s/immutable-paper-w1wck?file=/src/components/Btn.vue:41-56

Detect when child component is rerendered

I have a link element injected into translation string that I later consume via aurelia-i18n plugin. I need to listen for a click event on this link. As far as I researched it is not possible to add click.trigger="function() to an html element in translation string.
So I ended up just manually assigning a listener to the link DOM element in attached method of parent component. However when language updates, link updates as well and my listener is gone. Is there a way to know when component has been updated so I could reassign the listener there?
Or is a better solution to this issues?

Firing an event from parent to child element in Polymer 2.0

Can somebody provide an example of sending an event from parent to child in Polymer 2.o custom element?
I tried with following:
<child-element>
this.addEventListener('dbinit', this._evdbInitStatus);
and
<parent-element>
this.dispatchEvent(new CustomEvent('dbinit', {detail: {kicked: true}}));
The call back does not get invoked.
This is not really related to Polymer itself, as you can see in the documentation on how events work, here, events capturing stops at the element that triggered the event and the event bubbling (as the name suggests, the events bubble up) starts from the element and goes up in the tree.
So, in other words, an event fired by a parent element won't be captured by a child element. You will need to use a data binding to pass data down.
To keep a consistent, predictable flow of data, in general, it's better anyway if data travels down via data binding and up via events.

How to communicate between components within a certain component?

Suppose I have a tree of component like this:
<widget>
<widget-header>
<panel-toggle></panel-toggle>
</widget-header>
<widget-body>
<panel></panel>
</widget-body>
</widget>
Now supposed I want the panel-toggle component to be able to toggle the visibility of the panel component. I could have it affect a prop passed down from widget through to each component, but that didn't seem like the best solution. I tried sending an event with this.$emit(eventName) but the event is only picked up by the immediate parent of the element emitting the event. In this case, that would be panel-toggle emitting the event and only widget-header being able to pick it up. I tried sending the event across the root element with this.$root.$emit(eventName) and picking it up with this.$root.$on(eventName), but then it is picked up by all widget components and that is no good. What I ended up doing is sending the event with this.$parent.$parent.$emit(eventName) and then picking it up from panel with this.$parent.$parent.$on(eventName). While that worked, it doesn't seem like the right way to go about this.
What would be the correct way to achieve this communication between components within the component widget only with Vue? Is the answer somehow related to the ref feature?
Since you're concerned (and with good reason) about the globalness of a global event bus, the solution is a localized event bus. Create a data item in the parent:
panelBus: new Vue()
and pass it to each of the children as a prop. Now they have a private communcation channel for just the two of them.

Using DOMElement.addEventListener within Vue component is capturing preceding event

I have a Vue component which within its created lifecycle method adds an event listener to the document to capture any clicks that are outside of root $el. Let's call this Popup
When I click an element which summons and mounts the above Popup, the document click event is immediately captured.
Here is a jsfiddle with simplified code: https://jsfiddle.net/awei01/5vuqjcxd/
And, in contrast, here is a pure js version which correctly binds the click event: https://jsfiddle.net/awei01/qzqku0w9/
As a cross reference, here is the vue forums post: https://forum.vuejs.org/t/document-addeventlistener-captures-a-click-preceding-listeners-creation/11558
Any insight is appreciated.
Credit goes to Vue core team member LinusBorg.
Need a setTimeout
https://forum.vuejs.org/t/document-addeventlistener-captures-a-click-preceding-listeners-creation/11558
Here is a working example with the setTimeout implemented: https://jsfiddle.net/awei01/ovr9sr6k/
Explanation in english:
Click event occurs and enters capture phase. parent Vue component handles this event.
parent component sets internal $data flag to show the popup module
popup module gets instantiated and mounted. In the created function, the document.addEventListener event gets attached.
The click event capture phase completes and starts bubbling up the DOM.
The click event is now captured by the document because of the event listener we've just added. It fires the callback and it looks like nothing has occurred.
Solution:
Click event occurs and enters capture phase. parent Vue component handles this event.
parent component sets internal $data flag to show the popup module
popup module gets instantiated and mounted. In the created function, attach the document.addEventListener in a setTimeout function so that it gets attached after the click event is fully complete.
The click event capture phase completes and starts bubbling up the DOM.
The bubble phase of the event completes
The callback within setTimeout now runs and document now listens to clicks
Any subsequent clicks that bubble up to document will be captured.