Delegating Draggable Events to Parent Elements - javascript

I have draggable li elements nested in a ul in turn nested in a div, as seen below:
<div class='group'>
<div class='header'>
// some stuff here
</div>
<ul>
<li draggable='true'>
Stuff I want to drag and drop to another div.group
</li>
</ul>
</div>
There are multiple of these div elements and I am trying to implement a drag & drop functionality to move the li elements of one div group to another.
I have hooked up the ondragenter, ondragleave callbacks here:
// controller using mithril.js
ctrl.onDragLeave = function () {
return function (event) {
var target;
// Using isDropzone to recursively search for the appropriate div.group
// parent element, as event.target is always the children inside it
if ((target = isDropzone(event.target)) != null) {
target.style.background = "";
}
}
};
ctrl.onDragEnter = function () {
return function (event) {
var target;
if ((target = isDropzone(event.target)) != null) {
target.style.background = "purple";
}
};
};
function isDropzone(elem){
if(elem == null){
return null;
}
return elem.className == 'group' ? elem: isDropzone(elem.parentNode)
}
The problem comes when the event.target of the callbacks are always the nested child elements inside the div, such as li, and thus the callbacks are constantly fired. In this case I'm changing the color of the div.group with my callbacks, resulting in the div.group blinking undesirably.
Is there a way to delegate events and only allow the div grand parent of li to handle the events? Or any other way to work around this?
EDIT: Would still love to find out if there's a way to do this, but right now I'm using the workaround I found here.

So this is going to fit into the "you need to approach this from a different angle" category of answers.
Avoid- as much as possible- manipulating the DOM from event.target/event.currentTarget in your attached handlers.
A couple things differently:
Your ondragleave and ondragenter handlers should simply set some appropriate "state" attributes in your controller/viewModel/stores
When the handler is resolved, this generally triggers a redraw in Mithril. Internally m.startComputation() starts, your handler is called, then m.endComputation()
Your "view function" runs again. It then reflects the changed models. Your actions don't change the views, your views call actions which affect the models, and then react to those changes. MVC, not MVVM
Model
In your controller, set up a model which tracks all the state you need to show your drag and drop ui
ctrl.dragging = m.prop(null)
ctrl.groups = m.prop([
{
name: 'Group A',
dragOver: false,
items: ['Draggable One', 'Draggable Two']
},
...
// same structure for all groups
])
View
In your view, set up a UI that reflects your models state. Have event handlers that pass sufficient information about the actions to the controller- enough that it can properly respond to the actions an manipulate the model accordingly
return ctrl.groups.map(function (group, groupIdx) {
return m('.group',[
m('.header', group.name),
m('ul',
{
style: { background: (group.dragOver ? 'blue' : '')},
ondragleave: function () {ctrl.handleDragLeave(groupIdx)},
ondragenter: function () {ctrl.handleDragEnter(groupIdx)},
ondrop: function () {ctrl.handleDrop(groupIdx)},
ondragover: function (e) {e.preventDefault()}
},
group.items.map(function (item, itemIdx) {
return m('li',
{
draggable: true,
ondragstart: function () {ctrl.handleDragStart(itemIdx, groupIdx)}
},
item
})
)
])
})
Now its set up so that the group can properly display by reacting to state/model changes in your controller. We don't need to manipulate the dom to say a group has a new item, a group needs a new background color, or anything. We just need to attach event handlers so the controller can manipulate your model, and then the view will redraw based on that model.
Controller
Your controller therefore can have handlers that have all the info from actions needed to update the model.
Here's what some handlers on your controller will look like:
ctrl.handleDragStart = function (itemIdx, groupIdx) {
ctrl.dragging({itemIdx: itemIdx, groupIdx: groupIdx})
}
ctrl.handleDragEnter = function (groupIdx) {
ctrl.groups()[groupIdx].dragOver = true
}
ctrl.handleDragLeave = function (groupIdx) {
ctrl.groups()[groupIdx].dragOver = false
}
ctrl.handleDrop = function (toGroupIdx) {
var groupIdx = ctrl.dragging().groupIdx
var itemIdx = ctrl.dragging().itemIdx
var dropped = ctrl.groups()[groupIdx].items.splice(itemIdx, 1)[0]
ctrl.groups()[toGroupIdx].items.push(dropped)
ctrl.groups()[toGroupIdx].dragOver = false
ctrl.dragging(null)
}
Try to stick with Mithril's MVC model
event handlers call actions on your controller, which manipulates the model. The view then reacts to changes in those models. This bypasses the need to get entangled with the specifics of DOM events.
Here's a full JSbin example showing what you're trying to get to:
https://jsbin.com/pabehuj/edit?js,console,output
I get the desired effect without having to worry about event delegation at all.
Also, notice that in the JSbin, the ondragenter handler:
ondragenter: function () {
if (ctrl.dragging().groupIdx !== groupIdx) {
ctrl.handleDragEnter(groupIdx)
}
}
This is so the droppable area doesn't change color on its own draggable, which is one of the things I think you're looking for in your answer.

Related

Detect component hover on scroll in React

As you may already know, mouseenter and mouseleave events are NOT triggered if the mouse doesn't move, which means that if you scroll over an element without moving the mouse, hover effects are ignored.
This answer describes the strategy to overcome this:
1: Add a scroll listener to the window.
2: In the handler, call document.elementsFromPoint.
3: Manually call the actual mouseover handler for those elements.
4: Manually call the actual mouseleave handler for elements no longer being hovered.
We also must take into account that registering a listener for each component we want to detect the hover of, is a waste of resource.
My idea is to create a singleton object and subscribe to it from each component.
I'm fairly new to react so I will try to write pseudo-react-code to describe my idea:
// A global singleton object that registers the listeners ONCE.
class Singleton {
subscribedElements = {}
subscribe(element, isHovered, setIsHovered) {
subscribedElements[element] = {
isHovered: isHovered,
setIsHovered: setIsHovered
}
}
unsubscribe(element) { ..... }
constructor() {
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('mousemove', this.handleMove);
}
handleMove() {
//omitted
}
handleScroll() {
hoveredElements = document.elementsFromPoint(mousePosition)
// Iterate every element that is subscribed and check if they are in the list of elements that are hovered.
subscribedElements.map( e => {
if (hoveredElements.contains(e)) {
subscribedElements[e].setIsHovered(true)
} else {
subscribedElements[e].setIsHovered(false)
}
})
}
}
// a hook to be used in all elements that want to detect if they are hovered
function useHovered(element) {
[isHovered, setIsHovered] = useState(false);
Singleton.subscribe(element, isHovered, setIsHovered);
return isHovered;
}
// All components that want to check if they are being hovered would do this
function MyComponent(props) {
isHovered = useHovered(this);
return <div>{ isHovered ? 'hovered' : 'not hovered'</div>
}
Now, what is the most efficient and clean way to do something like this?
I have read about useContext but I'm not sure how to apply it to this solution.
I'm not sure how to create this singleton. Does it have to be a component? Can it be done in the new functional way?

DOM - timing of simultaneous events vs setTimeout

Suppose I have an element containing several children and want to run some code whenever the mouse enters or leaves the container. If I naively write:
var onHover = function (el, f) {
el.addEventListener('mouseover', function () {
f(true);
});
el.addEventListener('mouseout', function () {
f(false);
});
};
Then I get the desired behavior in some cases - depending on the nature of the callback f. However, when the mouse moves from child to child within the container, f(false) runs immediately followed by f(true). I don't want this to happen - I only want f to be run when the mouse enters or leaves the container as a whole, not called machine-gun style as the user drags their mouse over the elements that are inside the container.
Here's the solution that I came up with:
var onHover = function (el, f) {
var previousMouseover = false;
var receivedMouseover = false;
var pushing = false;
var pushEv = function () {
if (pushing) { return; }
pushing = true;
setTimeout(function () {
pushing = false;
if (previousMouseover !== receivedMouseover) {
f(receivedMouseover);
previousMouseover = receivedMouseover;
}
});
};
el.addEventListener('mouseover', function () {
receivedMouseover = true;
pushEv();
});
el.addEventListener('mouseout', function () {
receivedMouseover = false;
pushEv();
});
};
This solution, like the first solution, assumes and works by the virtue that the mouseout event is sent before the mouseover event is. I would also like to know whether that is formally specified by any W3C documentation, but that is not the topic of this question, and even if it were not the case, it would be easy to write a functioning algorithm in spite of that by setting two separate variables, say receivedMouseover and receivedMouseout inside of the mouseover and mouseout callbacks, both of which are then inspected inside of the setTimeout callback.
The question is: Is it required that both the mouseover and mouseout events be processed before any setTimeout callbacks signed up by either event are run?
Use the mouseenter and mouseleave events instead of mouseover and mouseout.
Since you have attached the event listener to the parent element you may compare the event origin (event.target) with the parent element (this or event.currentTarget) before you take an action. You may do as follows;
var onHover = function (el, f) {
el.addEventListener('mouseover', function (evt) {
this === evt.target && f(true);
});
el.addEventListener('mouseout', function (evt) {
this === evt.target && f(false);
});
};
Most of the elements bubble so at some point this might be the right way to do this job.
Edit: As mentioned in the comments the mouseover and mouseout events can be problematic under some circumstances such as when the parent element has no padding or margins defined and children cover all the parent. Even if they don't the speed of the mouse could be fast enough to make the JS engine fail to sample the mouse over the parent element. This fact is beautifuly explained in this article.
So, as mentioned in the accepted answer, i suppose the mouseenter and mouseleave events are there to solve this problem. Accordingly the right code should be like;
var onHover = function (el, f) {
el.addEventListener('mouseenter', () => f(true));
el.addEventListener('mouseleave', () => f(false));
};
Edit 2: Well... Actually there is a safe way of using mouseover and mouseout in this particular condition. It's about using CSS pointer-events property on the children which disables them from event emitting for mouse activity.
var container = document.getElementById('container');
container.addEventListener('mouseover', function (ev) {
console.log(container === ev.target);
});
container.addEventListener('mouseout', function (ev) {
console.log(container === ev.target);
});
#container * {
pointer-events: none
}
<div id="container">
<div>
<span>text</span>
</div>
</div>

How to bind event in knockout.js in dynamically added content?

I want to add some content to my page after data binding, e.g.:
$("<li>
<div>text</div>
<div data-bind='event: { click: selectContact }'></div>
</li>")
.appendTo($("#userClientGroup")
.find("#searched-client-ul-UCG"));
However, in this case the click event is not working; can any one can give me solution?
You can use ko.applybindings(viewModel, $('#yourNewElement')).Just be careful not to try binding an element already bound, or you'll have an error.
The best approach would be to avoid using jQuery (or any DOM method) to append new elements, in order to avoid having to bind your viewmodel against these elements. You can solve the problem either with existing bindings in your HTML or with a custom binding, or a combination. Your bindings should handle the DOM manipulation, not your other code (which shouldn't need to be aware of the DOM).
Another approach is to use a delegated event handler. I use the following custom binding:
ko.bindingHandlers.delegatedEvent = {
init: function (element, valueAccessor) {
var options = ko.unwrap(valueAccessor()) || {},
setupEventHandler = function (settings) {
if (settings.data) {
$(element).on(settings.event, settings.target, settings.data, settings.handler);
} else {
$(element).on(settings.event, settings.target, settings.handler);
}
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).off(settings.event, settings.target, settings.handler);
});
};
if ($.isArray(options)) {
$.each(options, function () {
setupEventHandler(this);
});
} else {
setupEventHandler(options);
}
}
};
Use this on the <ul> you're inserting the li into as such:
<ul data-bind="delegatedEvent: { event: click, target: '.contact-select', handler: selectContact }">
Add the class in your original insertion code, and remove the data-bind there.
$('<li><div>text</div><div class="contact-select"></div></li>')
.appendTo($("#userClientGroup").find("#searched-client-ul-UCG"));
Not only have you solved the problem, but you've replaced potentially lots of event handlers with just one.

Jquery event being called multiple times

So I have a form to submit photos (to a total of 8), and I'm trying to apply a small effect: once you choose a photo, the button hides and the file name is displayed along with a 'X' to remove its selection.
However, when I add multiple photos and try to remove one, the event gets called multiple times, and the more I click, more multiple events are fired, all from the same element.
Can anyone figure it out?
var Upload = {
init: function ( config ) {
this.config = config;
this.bindEvents();
this.counter = 1;
},
/**
* Binds all events triggered by the user.
*/
bindEvents: function () {
this.config.photoContainer.children('li').children('input[name=images]').off();
this.config.photoContainer.children('li').children('input[name=images]').on("change", this.photoAdded);
this.config.photoContainer.children('li').children('p').children('a.removePhoto').on('click', this.removePhoto);
},
/**
* Called when a new photo is selected in the input.
*/
photoAdded: function ( evt ) {
var self = Upload,
file = this.files[0];
$(this).hide();
$(this).parent().append('<p class="photo" style="background-color: gray; color: white;">' + file.name + ' <a class="removePhoto" style="color: red;" href="#">X</a></p>');
if(self.counter < 8) { // Adds another button if needed.
Upload.config.photoContainer.append( '<li><input type="file" name="images"></li>');
self.counter++;
}
Upload.bindEvents();
},
/**
* Removes the <li> from the list.
*/
removePhoto: function ( evt ) {
var self = Upload;
evt.preventDefault();
$(this).off();
$(this).parent().parent().remove();
if(self.counter == 8) { // Adds a new input, if necessary.
Upload.config.photoContainer.append( '<li><input type="file" name="images"></li>');
}
self.counter--;
Upload.bindEvents();
}
}
Upload.init({
photoContainer: $('ul#photo-upload')
});
From what I see, you are trying to attach/remove event handlers based on what the user selects. This is inefficient and prone to errors.
In your case, you are calling Upload.bindEvents() each time a photo is added, without cleaning all the previous handlers. You could probably debug until you don't leak event listeners anymore, but it's not worth it.
jQuery.on is very powerful and allows you to attach handlers to elements that are not yet in the DOM. You should be able to do something like this:
init: function ( config ) {
this.config = config;
this.counter = 1;
this.config.photoContainer.on('change', 'li > input[name=images]', this.photoAdded);
this.config.photoContainer.on('click', 'li > p > a.removePhoto', this.removePhoto);
},
You attach just one handler to photoContainer, which will catch all events bubbling up from the children, regardless of when they were added. If you want to disable the handler on one of the elements, you just need to remove the removePhoto class (so that it doesn't match the filter).
You are doing a lot of: Upload.bindEvents();
You need to unbind events for those 'li's before you bind them again. Otherwise, you add more click events. That's why you are seeing more and more clicks being fired.

Backbone.js button click event is fired for all instances of the button instead of just the one that is clicked. Why?

I am learning backbone.js and am quite new. I have a view that acts as a button:
simpleButton = Backbone.View.extend({
template: "<button class='${classes}'>${text}</button>",
el: $("body"),
events: {
"click": "onClick",
"focus": "onFocus",
"blur": "onBlur"
},
initialize: function (args) {
_.bindAll(this, 'render');
this.rendered = false;
this.text = args.text || 'button';
this.classes = args.classes || [];
this.classes.push('ui-button');
//console.debug("Wh.views.simpleButton.initialize classes ",this.classes);
if (args.autoRender === true) this.render();
},
render: function () {
//console.debug("Wh.views.simpleButton.render classes ",this.classes);
if (this.rendered === false) {
$.tmpl(
this.template, {
classes: this.classes.join(' '),
text: this.text
}
).appendTo(this.el);
this.rendered = true;
}
},
//event handlers
onClick: function (ev) {
console.debug(this);
alert("click on ", ev, this);
},
onFocus: function (ev) {
////console.debug(ev);
},
onBlur: function (ev) {
}
});
My problem is that if I create two buttons, and click just one of them, I get the alert box two times, and the debug showing me "this" shows the first button first, and the second button next.
Am I missing something?
The events you define are bound to the "el" property of your view. In your case it is "body" so when you fire up click with 2 simpleButton views instantiated, you have 2 of them listening for the same event.
Each view you instantiate should represent one and only one DOM element defined by the el property. So if you want to create a button view (not sure this is 'best practice' in a real program) you could have :
SimpleButton = Backbone.View.extend({
template : "<button class='${classes}'>${text}</button>",
tagName : "div", // defines the html tag that will wrap your template
className: ".buttonbox",
...
});
mybtn = new SimpleButton();
mybtn.render().appendTo('body')
That way your click event will only concern the one div.buttonbox inside of which your button lives.
Notice : Backbone idea of the render function is creating an html string you'll use afterwards to append prepend or whatever in the DOM. That way if you create many you can do it so you only refresh the DOM once (refreshing the DOM is expensive)...
Use this in your View .it will unbind the click events
initialize : function() {
$(this.el).unbind("click");
}
Just a thought that creating a Backbone.View for each and every button in your app could be a performance overkill and you can't leverage the "delegate" feature in jQuery. I'd instead create a Backbone.View for the parent element of those buttons instead.
Of course, if you have a few special buttons with complicated logic then they probably do deserve their own View classes. :)
Give your buttons unique ids, for example <button id="button1"> and <button id="button2">, then in your events hash, you need to specify the click event and the id of the button you want to handle that event for, e.g:
events : {
"click #button1" : "onClick",
"click #button2" : "doSomethingElse"
}
Now this will call onClick() only when you click on the button with id=button1 and call doSomethingElse() when you click on the button with id=button2

Categories