Skip to content

Instantly share code, notes, and snippets.

@enjalot
Created August 6, 2013 23:54
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save enjalot/6170003 to your computer and use it in GitHub Desktop.
Save enjalot/6170003 to your computer and use it in GitHub Desktop.
drag and drop in derby (by @ishbu)
helpers = require('../util').helpers
module.exports = Draggable = (e, containerEl, handle, dragSelector, handleSelector, component, @moveCallback, startX, startY) ->
items = containerEl.querySelectorAll dragSelector
if dragSelector == handleSelector
el = handle
else
handles = containerEl.querySelectorAll handleSelector
index = nodeIndex handles, handle
el = items[index]
@el = el
@container = cloneEl el
@container.classList.add 'dragging'
el.style.opacity = 0.5
rect = el.getBoundingClientRect()
startX ?= e.clientX
startY ?= e.clientY
@offsetLeft = rect.left - startX
@offsetTop = rect.top - startY
@onMove e
helpers.appendToBody @container, component
return
Draggable::finish = (cancel) ->
@el.style.opacity = ''
unless cancel
@moveCallback @el
document.body.removeChild @container
Draggable::onMove = (e) ->
@container.style.left = (e.clientX + window.pageXOffset + @offsetLeft) + 'px'
@container.style.top = (e.clientY + window.pageYOffset + @offsetTop) + 'px'
nodeIndex = (nodeList, node) ->
for child, i in nodeList
return i if child == node
return -1
cloneEl = (el) ->
parent = el.parentNode
# Need table if we are dragging something like a TR
if parent.tagName == 'TBODY' || parent.tagName == 'THEAD'
container = parent.parentNode.cloneNode false
container.removeAttribute 'id'
parentClone = parent.cloneNode false
parentClone.removeAttribute 'id'
container.appendChild parentClone
else
container = parentClone = parent.cloneNode false
container.removeAttribute 'id'
clone = el.cloneNode true
clone.removeAttribute 'id'
parentClone.appendChild clone
container.style.width = window.getComputedStyle(parent).width
container.style.position = 'absolute'
clone.style = window.getComputedStyle el
return container
exports.appendToBody = (el, component, stopPropagation) ->
return unless el
# Put the element at the top level of the body so that it can't be
# cut off if the menu is in a container with overflow:hidden
document.body.appendChild el
return if el._hasDestroyListener || !component
el._hasDestroyListener = true
if stopPropagation
# Bubbling events may be problematic, since the element is
# no longer in the original DOM location
component.dom.addListener el, 'click', (e) ->
e.stopPropagation()
# Make sure that the element is removed if its parent component is destroyed
component.on 'destroy', ->
el.parentNode.removeChild el if el.parentNode
###*
* Enables you to create a set of draggables tied to a set of droppables
* When an item is being dragged, droppables will add class .hovered as the mouse moves over them
* When an item is dropped into a droppabel target, a 'drop' event is emitted of form
* (draggedItemEl, dropTargetEl)
* Example:
* <ui:droppable
* container=".list-content" # The outer container that contains draggable items
* draggable="tr.profile" # Selector for draggable elements must be inside container
* droppable=".stage-target" # Selector for droppable elements
* bind="drop: lists.changeStage">
###
exports.name = "droppable"
Draggable = require './Draggable'
exports.create = (model, dom) ->
@_dragSelector = model.get('draggable') || '.draggable-item'
@_handleSelector = model.get('handle') || @_dragSelector
@_dropSelector = model.get('droppable') || '.droppable-target'
@_containerSelector = model.get('container') || '.draggable-container'
@_delayDistance = model.get('delay-distance') || 10
@_draggables = document.querySelectorAll @_dragSelector
@_droppables = document.querySelectorAll @_dropSelector
@_container = document.querySelector @_containerSelector
@_handle = null
@_draggable = null
@_dropTarget = null
@_lastDown = null
@_startPos = null
dom.addListener document, 'mousemove', (e) =>
if @_startPos && @_handle && !@_draggable
startX = @_startPos.clientX
startY = @_startPos.clientY
distanceX = e.clientX - startX
distanceY = e.clientY - startY
return if Math.sqrt(distanceX * distanceX + distanceY * distanceY) < @_delayDistance
onDrop = (itemEl) =>
return unless itemEl && @_dropTarget
@emit 'drop', itemEl, @_dropTarget
model.set 'dragging', true
@_draggable = new Draggable e, @_container, @_handle, @_dragSelector, @_handleSelector, this, onDrop, startX, startY
return unless @_draggable
@_draggable?.onMove e
onMove e
, true
dom.addListener document, 'mouseup', (e) =>
@_startPos = null
@_handle = null
return unless @_draggable
@_finishDragging(false, e)
# If the mouse is in the same spot as when down was fired, we click
target = document.elementFromPoint(e.clientX, e.clientY)
if @_lastDown == target
@_lastDown.click()
, true
dom.addListener window, 'blur', (e) =>
@_finishDragging(true)
, true
dom.addListener document, 'mousedown', exports._down.bind(this), true
onMove = (e) =>
clientX = e.clientX
clientY = e.clientY
for droppable in @_droppables
rect = droppable.getBoundingClientRect()
if clientX > rect.left && clientX < rect.right
if clientY > rect.top && clientY < rect.bottom
@_dropTarget?.classList.remove('hovered')
droppable.classList.add('hovered')
@_dropTarget = droppable
break
exports._finishDragging = (cancel, e) ->
return unless @_draggable
@_draggable.finish cancel
@_handle = null
@_draggable = null
@_dropTarget = null
@_lastDown = null
@_startPos = null
@model.set 'dragging', false
# Clean up any hover states
hovered = document.querySelectorAll @_dropSelector + '.hovered'
for hover in hovered
hover.classList.remove('hovered')
exports._down = (e) ->
return if e.button != 0 # Left click only
handle = containedBy @_container, e.target, @_handleSelector
return unless handle # Is what I'm clicking on in the container?
@_handle = handle
@_lastDown = el = e.target
clientX = e.clientX
clientY = e.clientY
@_startPos = {clientX, clientY}
# OPTIMIZE: This preventDefault is critical for some unknown performance reason, find out why
e.preventDefault?()
containedBy = (root, target, selector) ->
while target != root
return target if target.matches? selector
target = target.parentNode
return unless target
return
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment