drag & drop

A lightweight library that leverages HTML5 drag and drop behavior.

To use it, simply call dragAndDrop.init() (it only needs to be called once, but calling it again is harmless).

import { dragAndDrop } from 'tosijs-ui'

dragAndDrop.init()

This module sets up some global event handlers and just works™ (arguably, it merely does things that the browser should do, such as add a CSS selector for drop zones that are compatible with what's being dragged). Drop zones added dynamically during a drag (e.g. menu items) are automatically detected and marked.

You can use dragAndDrop.draggedElement() to get the element being dragged (if it's actually from the page you're in).

The beauty of HTML5 drag-and-drop

The nice thing about HTML5 drag-and-drop is that it leverages the OS's drag and drop support. This means you can drag from one window to another, from the desktop to your app and vice versa. It's all a matter of configuring the DOM elements.

This module uses but does not define the following class selectors:

You may also wish to create style rules for:

Note draggable is has to be set to "true", see documentation on draggable.

Draggable Objects

To create a draggable element, add draggable="true".

<div draggable="true">Drag Me</div>

To specify the type(s) of content that will be dragged, use the data-drag attribute:

<div draggable="true" data-drag="text/plain">Drag Me</div>

To specify the content dragged, use a data-drag-content attribute.

<div
  draggable="true"
  data-drag="text/plain"
  data-drag-content="Surprise!"
>Drag Me</div>

Drop Zones

To create a drop zone, use the data-drop attribute set to a semicolon-delimited list of mime types:

<div data-drop="text/plain">
  Drop plain text here
</div>
<div data-drop="text/plain;text/html">
  Drop html or plain text here
</div>

Finally, you can override default drop behavior (which is to copy the dragged node into the drop zone node) by adding your own drop event handler.

E.g.

element.addEventListener('drop', (event) => {
  // event.target is the dragged element
  ...

  event.stopPropagation()
  event.preventDefault()
})

And of course elementCreator()s provide syntax sugar for this:

elements.div({
  onDrop(event) {
    // ...
  }
})

Typed Drop Zones Example

In this example, the types are set using data-drag attributes and the drop zones are set using data-drop attributes, but everything else is default behavior. You can also drop the draggable objects to another window or the desktop, and similarly you can drag appropriate stuff into the drop zones. (You can test this out by opening this page in a second browser window—event a different browser.)

<div style="display: grid; grid-template-columns: 50% 50%">
  <div>
    <h4>Draggable</h4>
    <a class="drag" href="javascript: alert('I don't do anything)">Links are draggable by default</a>
    <p draggable="true">
      Just adding the <code>draggable="true"</code>
      makes this paragraph draggable (as text/html by default)
    </p>
    <p draggable="true" data-drag="text/html">
      Draggable as <i>text/html</i>
    </p>
    <p draggable="true" data-drag="text/plain" data-drag-content="Surprise!">
      Draggable as <i>text/plain</i>, with <b>custom content</b>
    </p>
    <p draggable="true" data-drag="text/html;text/plain">
      Draggable as <i>text/html</i> or <i>text/plain</i>
    </p>
    <p draggable="true" data-drag="text/plain">
      Draggable as <i>text/plain</i>
    </p>
  </div>
  <div>
    <h4>Drop Targets</h4>
    <div data-drop="text/html">
      You can drop stuff here
    </div>
    <div data-drop="text/html">
      You can drop HTML here
    </div>
    <div data-drop="text/*">
      You can drop any text
    </div>
    <div data-drop="text/html;url">
      You can drop HTML or urls here
    </div>
    <div
      data-drop="special/any"
      data-event="drop:_component_.drop"
    >
      I accept anything and have special drop handling
    </div>
  </div>
</div>
.drag-source {
  box-shadow: 0 0 2px 2px orange;
  opacity: 0.5;
}
.drag-target {
  min-height: 10px;
  background: rgba(0,0,255,0.25);
}
.drag-target.drag-over {
  background: rgba(0,0,255,0.5);
}
:not([data-drop]) > .drag,
[draggable="true"] {
  border: 1px solid rgba(255,192,0,0.5);
  cursor: pointer;
  display: block;
}

:not([data-drop]) > .drag,
[data-drop],
[draggable="true"] {
  padding: 4px;
  margin: 4px;
  border-radius: 5px;
}
import { dragAndDrop } from 'tosijs-ui'

dragAndDrop.init()

Note that you can drag between two browser tabs containing this example (or between two different browsers) and it will work.

Reorderable List Example

This example uses a custom drop event handler. When you sort the spectrum into the correct order you "win" and then the items are reshuffled.

Also notice that the data-drag and data-drop values are set to a random dragId so you cannot drag to another window or the desktop.

import { elements, tosi, getListItem } from 'tosijs'
import { dragAndDrop, TosiDialog } from 'tosijs-ui'

dragAndDrop.init()

const shuffle = (deck) => {
  var shuffled = [];
  for( const card of deck ){
    shuffled.splice( Math.floor( Math.random() * (1 + shuffled.length) ), 0, card );
  }
  return shuffled;
}

const colors = [
  'red',
  'orange',
  'yellow',
  'green',
  'blue',
  'indigo',
  'violet',
]

let spectrum

const start = () => {
  ({ spectrum } =  tosi({
    spectrum: shuffle(colors).map(color => ({color}))
  }))
}

start()

let dragged = null

const dropColor = (event) => {
  const dropped = getListItem(event.target)
  const draggedIndex = spectrum.indexOf(dragged)
  const droppedIndex = spectrum.indexOf(dropped)
  spectrum.splice(draggedIndex, 1)
  spectrum.splice(droppedIndex, 0, dragged)

  if (JSON.stringify(spectrum.map(c => c.color)) === JSON.stringify(colors)) {
    TosiDialog.alert('You win!').then(start)
  }

  console.log({dragged, draggedIndex, dropped, droppedIndex})

  event.preventDefault()
  event.stopPropagation()
}

const dragId = 'spectrum/' + Math.floor(Math.random() * 1e9)

const { div, button, template } = elements

preview.append(
  div(
    {
      bindList: { value: spectrum, idPath: 'color' }
    },
    template(
      div({
        class: 'spectrum',
        bindText: '^.color',
        draggable: 'true',
        dataDrag: dragId,
        dataDrop: dragId,
        onDrop: dropColor,
        bind: {
          value: '^.color',
          binding(element, value) {
            element.style.backgroundColor = value
          }
        },
        onDragstart(event) {
          dragged = getListItem(event.target)
        }
      })
    )
  ),
)
.spectrum {
  height: 36px;
  color: white;
  font-weight: 700;
  text-shadow: 1px 2px 0 black;
  padding-left: 10px;
}

.spectrum.drag-over {
  box-shadow: 0 0 0 4px blue;
}

To prevent this example from allowing drags between windows (which wouldn't make sense) a random dragId is assigned to data-drag and data-drop in this example. )