3

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.

Community
  • 1
  • 1
plsnoban
  • 351
  • 5
  • 17

1 Answers1

0

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:

  1. Your ondragleave and ondragenter handlers should simply set some appropriate "state" attributes in your controller/viewModel/stores

  2. When the handler is resolved, this generally triggers a redraw in Mithril. Internally m.startComputation() starts, your handler is called, then m.endComputation()

  3. 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.

Tony
  • 395
  • 3
  • 10