coffeescript-ui
Version:
Coffeescript User Interface System
723 lines (548 loc) • 17.6 kB
text/coffeescript
###
* coffeescript-ui - Coffeescript User Interface System (CUI)
* Copyright (c) 2013 - 2016 Programmfabrik GmbH
* MIT Licence
* https://github.com/programmfabrik/coffeescript-ui, http://www.coffeescript-ui.org
###
class CUI.Draggable extends CUI.DragDropSelect
@cls = "draggable"
initOpts: ->
super()
@addOpts
dragClass:
default: "cui-dragging"
check: String
helper:
default: "clone"
check: (v) ->
v == "clone" or CUI.util.isElement(v) or CUI.util.isFunction(v) or null
helper_contain_element:
check: (v) ->
CUI.util.isElement(v)
helper_set_pos:
check: Function
get_cursor:
check: Function
support_touch:
check: Boolean
dragend:
check: Function
dragstop:
check: Function
dragstart:
check: Function
dragging:
check: Function
create:
default: -> true
check: Function
axis:
check: ["x", "y"]
# helper_remove_always:
# check: Boolean
# helper_parent:
# default: "document"
# check: ["document", "parent"]
threshold:
default: 2
check: (v) ->
v >= 0
# ms:
# default: 0
# check: (v) ->
# # must be multiple of MouseIsDownListener.interval_ms or 0
# v % CUI.MouseIsDownListener.interval_ms == 0
selector:
check: (v) =>
CUI.util.isString(v) or CUI.util.isFunction(v)
readOpts: ->
super()
@__autoRepeatTimeout = null
if @supportTouch()
@__event_types =
start: ["mousedown", "touchstart"]
end: ["mouseup", "touchend"]
move: ["mousemove", "touchmove"]
else
@__event_types =
start: ["mousedown"]
end: ["mouseup"]
move: ["mousemove"]
@
getClass: ->
if not @_selector
"cui-draggable "+super()
else
super()
supportTouch: ->
!!@_support_touch
__killTimeout: ->
if @__autoRepeatTimeout
CUI.clearTimeout(@__autoRepeatTimeout)
@__autoRepeatTimeout = null
@
__cleanup: ->
@__killTimeout()
if @__ref
CUI.Events.ignore(instance: @__ref)
@__ref = null
if CUI.globalDrag?.instance == @
CUI.globalDrag = null
return
destroy: ->
super()
CUI.dom.remove(CUI.globalDrag?.helperNode)
@__cleanup()
@
init: ->
# console.debug "Draggable", @options.selector
CUI.util.assert(not @_helper_contain_element or CUI.dom.closest(@_element, @_helper_contain_element), "new CUI.sDraggable", "opts.helper_contain_element needs to be parent of opts.element", opts: @opts)
CUI.Events.listen
type: @__event_types.start
node: @element
# capture: true
instance: @
selector: @_selector
call: (ev) =>
if ev.getButton() > 0 and ev.getType() == "mousedown"
# ignore if not the main button
return
if CUI.globalDrag
# ignore if dragging is in progress
return
# console.debug CUI.util.getObjectClass(@), "[mousedown]", ev.getUniqueId(), @element
# hint possible click event listeners like Sidebar to
# not execute the click anymore...
#
position = CUI.util.elementGetPosition(CUI.util.getCoordinatesFromEvent(ev), ev.getTarget())
dim = CUI.dom.getDimensions(ev.getTarget())
if dim.clientWidthScaled > 0 and position.left - dim.scrollLeftScaled > dim.clientWidthScaled
console.warn("Mousedown on a vertical scrollbar, not starting drag.")
return
if dim.clientHeightScaled > 0 and position.top - dim.scrollTopScaled > dim.clientHeightScaled
console.warn("Mousedown on a horizontal scrollbar, not starting drag.")
return
target = ev.getCurrentTarget()
target_dim = CUI.dom.getDimensions(target)
if not CUI.dom.isInDOM(target) or target_dim.clientWidth == 0 or target_dim.clientHeight == 0
return
if CUI.dom.closest(ev.getTarget(), "input,textarea,select")
return
$target = target
# console.debug "attempting to start drag", ev, $target
@init_drag(ev, $target)
return
init_drag: (ev, $target) ->
if not $target
# if subclasses screw with $target, this can happen
return
CUI.globalDrag = @_create?(ev, $target)
# ev.getMousedownEvent?().preventDefault()
if CUI.globalDrag == false
# console.debug("not creating drag handle, opts.create returned 'false'.", ev, @)
return
# ev.preventDefault()
if CUI.util.isNull(CUI.globalDrag) or CUI.globalDrag == true
CUI.globalDrag = {}
CUI.util.assert(CUI.util.isPlainObject(CUI.globalDrag), "CUI.Draggable.init_drag", "returned data must be a plain object", data: CUI.globalDrag)
point = CUI.util.getCoordinatesFromEvent(ev)
position = CUI.util.elementGetPosition(point, $target)
init =
$source: $target
startEvent: ev
startCoordinates: point
instance: @
startScroll:
top: $target.scrollTop
left: $target.scrollLeft
start: position # offset to the $target
threshold: @_threshold
for k, v of init
CUI.globalDrag[k] = v
ev.stopPropagation()
# ev.preventDefault()
@before_drag(ev, $target)
@__ref = new CUI.Dummy() # instance to easily remove events
dragover_count = 0
moveEvent = null
dragover_scroll = =>
# during a dragover scroll, the original target
# might be not available any more, we need to recalculate it
pointTarget = moveEvent.getPointTarget() or moveEvent.getTarget()
CUI.Events.trigger
type: "dragover-scroll"
node: pointTarget
count: dragover_count
originalEvent: moveEvent
dragover_count = dragover_count + 1
@__killTimeout()
@__autoRepeatTimeout = CUI.setTimeout
ms: 100
track: false
call: dragover_scroll
CUI.Events.listen
node: document
type: @__event_types.move
instance: @__ref
call: (ev) =>
if not CUI.globalDrag
return
# this prevents chrome from focussing element while
# we drag
ev.preventDefault()
$target = ev.getTarget()
if not $target
return
if CUI.globalDrag.ended
return
coordinates = CUI.util.getCoordinatesFromEvent(ev)
diff =
x: coordinates.pageX - CUI.globalDrag.startCoordinates.pageX
y: coordinates.pageY - CUI.globalDrag.startCoordinates.pageY
eventPoint: coordinates
switch @get_axis()
when "x"
diff.y = 0
when "y"
diff.x = 0
diff.bare_x = diff.x
diff.bare_y = diff.y
diff.x += CUI.globalDrag.$source.scrollLeft - CUI.globalDrag.startScroll.left
diff.y += CUI.globalDrag.$source.scrollTop - CUI.globalDrag.startScroll.top
if Math.abs(diff.x) >= CUI.globalDrag.threshold or
Math.abs(diff.y) >= CUI.globalDrag.threshold or
CUI.globalDrag.dragStarted
CUI.globalDrag.dragDiff = diff
if not CUI.globalDrag.dragStarted
CUI.globalDrag.startEvent.preventDefault()
@__startDrag(ev, $target, diff)
if @_get_cursor
document.body.setAttribute("data-cursor", @_get_cursor(CUI.globalDrag))
else
document.body.setAttribute("data-cursor", @getCursor())
moveEvent = ev
dragover_scroll()
@do_drag(ev, $target, diff)
@_dragging?(ev, CUI.globalDrag, diff)
return
# Stop is used by ESC button to stop the dragging.
end_drag = (ev, stop = false) =>
start_target = CUI.globalDrag.$source
start_target_parents = CUI.dom.parents(start_target)
CUI.globalDrag.ended = true
document.body.removeAttribute("data-cursor")
if stop
CUI.globalDrag.stopped = true
@stop_drag(ev)
@_dragstop?(ev, CUI.globalDrag, @)
else
@end_drag(ev)
@_dragend?(ev, CUI.globalDrag, @)
if @isDestroyed()
# this can happen if any of the
# callbacks cleanup / reload
return
noClickKill = CUI.globalDrag.noClickKill
@__cleanup()
if noClickKill
return
has_same_parents = =>
parents_now = CUI.dom.parents(start_target)
for p, idx in start_target_parents
if parents_now[idx] != p
return false
return true
if not has_same_parents or not CUI.dom.isInDOM(ev.getTarget())
return
CUI.Events.listen
type: "click"
capture: true
only_once: true
node: window
call: (ev) ->
# console.error "Killing click after drag", ev.getTarget()
return ev.stop()
return
CUI.Events.listen
node: document
type: ["keyup"]
capture: true
instance: @__ref
call: (ev) =>
if not CUI.globalDrag.dragStarted
@__cleanup()
return
if ev.keyCode() == 27
# console.error "stopped.."
end_drag(ev, true)
return ev.stop()
return
CUI.Events.listen
node: document
type: @__event_types.end
capture: true
instance: @__ref
call: (ev) =>
# console.debug "event received: ", ev.getType()
# console.debug "draggable", ev.type
if not CUI.globalDrag
return
if not CUI.globalDrag.dragStarted
@__cleanup()
return
end_drag(ev)
return ev.stop()
# console.debug "mouseup, resetting drag stuff"
#
return
getCursor: ->
"grabbing"
__startDrag: (ev, $target, diff) ->
# It's ok to stop the events here, the "mouseup" and "keyup"
# we need to end the drag are initialized before in init drag,
# so they are executed before
# console.debug "start drag", diff
@_dragstart?(ev, CUI.globalDrag)
@init_helper(ev, $target, diff)
CUI.dom.addClass(CUI.globalDrag.$source, @_dragClass)
@start_drag(ev, $target, diff)
CUI.globalDrag.dragStarted = true
# call after first mousedown
before_drag: ->
start_drag: (ev, $target, diff) ->
# do drag
# first call
do_drag: (ev, $target, diff) ->
# position helper
@position_helper(ev, $target, diff)
if CUI.globalDrag.dragoverTarget and CUI.globalDrag.dragoverTarget != $target
CUI.Events.trigger
type: "cui-dragleave"
node: CUI.globalDrag.dragoverTarget
info:
globalDrag: CUI.globalDrag
originalEvent: ev
CUI.globalDrag.dragoverTarget = null
if not CUI.globalDrag.dragoverTarget
CUI.globalDrag.dragoverTarget = $target
# console.debug "target:", $target
CUI.Events.trigger
type: "cui-dragenter"
node: CUI.globalDrag.dragoverTarget
info:
globalDrag: CUI.globalDrag
originalEvent: ev
# trigger our own dragover event on the correct target
CUI.Events.trigger
node: CUI.globalDrag.dragoverTarget
type: "cui-dragover"
info:
globalDrag: CUI.globalDrag
originalEvent: ev
return
cleanup_drag: (ev) ->
if @isDestroyed()
return
CUI.dom.removeClass(CUI.globalDrag.$source, @_dragClass)
CUI.dom.remove(CUI.globalDrag.helperNode)
stop_drag: (ev) ->
@__finish_drag(ev)
@cleanup_drag(ev)
__finish_drag: (ev) ->
if not CUI.globalDrag.dragoverTarget
return
# console.debug "sending pf_dragleave", CUI.globalDrag.dragoverTarget
# console.debug "pf_dragleave.event", CUI.globalDrag.dragoverTarget
CUI.Events.trigger
node: CUI.globalDrag.dragoverTarget
type: "cui-dragleave"
info:
globalDrag: CUI.globalDrag
originalEvent: ev
if not CUI.globalDrag.stopped
# console.error "cui-drop", ev
CUI.Events.trigger
type: "cui-drop"
node: CUI.globalDrag.dragoverTarget
info:
globalDrag: CUI.globalDrag
originalEvent: ev
CUI.Events.trigger
node: CUI.globalDrag.dragoverTarget
type: "cui-dragend"
info:
globalDrag: CUI.globalDrag
originalEvent: ev
CUI.globalDrag.dragoverTarget = null
@
end_drag: (ev) ->
# console.debug CUI.globalDrag.dragoverTarget, ev.getType(), ev
if @isDestroyed()
return
@__finish_drag(ev)
@cleanup_drag(ev)
@
get_helper_pos: (ev, gd, diff) ->
top: CUI.globalDrag.helperNodeStart.top + diff.y
left: CUI.globalDrag.helperNodeStart.left + diff.x
width: CUI.globalDrag.helperNodeStart.width
height: CUI.globalDrag.helperNodeStart.height
get_helper_contain_element: ->
@_helper_contain_element
position_helper: (ev, $target, diff) ->
# console.debug "position helper", CUI.globalDrag.helperNodeStart, ev, $target, diff
if not CUI.globalDrag.helperNode
return
helper_pos = @get_helper_pos(ev, CUI.globalDrag, diff)
pos =
x: helper_pos.left
y: helper_pos.top
w: helper_pos.width
h: helper_pos.height
helper_contain_element = @get_helper_contain_element(ev, $target, diff)
if helper_contain_element
dim_contain = CUI.dom.getDimensions(helper_contain_element)
if dim_contain.clientWidth == 0 or dim_contain.clientHeight == 0
console.warn('Draggable[position_helper]: Containing element has no dimensions.', helper_contain_element);
# pos is changed in place
CUI.Draggable.limitRect pos,
min_x: dim_contain.viewportLeft + dim_contain.borderLeftWidth
max_x: dim_contain.viewportRight - dim_contain.borderRightWidth - CUI.globalDrag.helperNodeStart.marginHorizontal
min_y: dim_contain.viewportTop + dim_contain.borderTopWidth
max_y: dim_contain.viewportBottom - dim_contain.borderBottomWidth - CUI.globalDrag.helperNodeStart.marginVertical
else
dim_contain = CUI.globalDrag.helperNodeStart.body_dim
CUI.Draggable.limitRect pos,
min_x: dim_contain.borderLeftWidth
max_x: dim_contain.scrollWidth - dim_contain.borderRightWidth - CUI.globalDrag.helperNodeStart.marginHorizontal
min_y: dim_contain.borderTopWidth
max_y: dim_contain.scrollHeight - dim_contain.borderBottomWidth - CUI.globalDrag.helperNodeStart.marginVertical
# console.debug "limitRect", CUI.util.dump(pos), dim_contain
helper_pos.top = pos.y
helper_pos.left = pos.x
helper_pos.dragDiff =
x: helper_pos.left - CUI.globalDrag.helperNodeStart.left
y: helper_pos.top - CUI.globalDrag.helperNodeStart.top
if helper_pos.width != CUI.globalDrag.helperNodeStart.width
new_width = helper_pos.width
if helper_pos.height != CUI.globalDrag.helperNodeStart.height
new_height = helper_pos.height
CUI.dom.setStyle CUI.globalDrag.helperNode,
transform: CUI.globalDrag.helperNodeStart.transform+" translateX("+helper_pos.dragDiff.x+"px) translateY("+helper_pos.dragDiff.y+"px)"
CUI.dom.setDimensions CUI.globalDrag.helperNode,
borderBoxWidth: new_width
borderBoxHeight: new_height
CUI.globalDrag.helperPos = helper_pos
return
getCloneSourceForHelper: ->
CUI.globalDrag.$source
get_axis: ->
@_axis
get_helper: (ev, gd, diff) ->
@_helper
get_init_helper_pos: (node, gd, offset = top: 0, left: 0) ->
top: gd.startCoordinates.pageY - offset.top
left: gd.startCoordinates.pageX - offset.left
init_helper: (ev, $target, diff) ->
helper = @get_helper(ev, CUI.globalDrag, diff)
if not helper
return
if helper == "clone"
clone_source = @getCloneSourceForHelper()
hn = clone_source.cloneNode(true)
hn.classList.remove("cui-selected")
# offset the layer to the click
offset =
top: CUI.globalDrag.start.top
left: CUI.globalDrag.start.left
else if CUI.util.isFunction(helper)
hn = CUI.globalDrag.helperNode = helper(CUI.globalDrag)
set_dim = null
else
hn = CUI.globalDrag.helperNode = helper
if not hn
return
CUI.globalDrag.helperNode = hn
CUI.dom.addClass(hn, "cui-drag-drop-select-helper")
document.body.appendChild(hn)
start = @get_init_helper_pos(hn, CUI.globalDrag, offset)
CUI.dom.setStyle(hn, start)
if helper == "clone"
# set width & height
set_dim = CUI.dom.getDimensions(clone_source)
# console.error "measureing clone", set_dim.marginBoxWidth, CUI.globalDrag.$source, dim
CUI.dom.setDimensions hn,
marginBoxWidth: set_dim.marginBoxWidth
marginBoxHeight: set_dim.marginBoxHeight
dim = CUI.dom.getDimensions(hn)
start.width = dim.borderBoxWidth
start.height = dim.borderBoxHeight
start.marginTop = dim.marginTop
start.marginLeft = dim.marginLeft
start.marginVertical = dim.marginVertical
start.marginHorizontal = dim.marginHorizontal
start.transform = dim.computedStyle.transform
if start.transform == 'none'
start.transform = ''
start.body_dim = CUI.dom.getDimensions(document.body)
CUI.globalDrag.helperNodeStart = start
# keep pos inside certain constraints
# pos.fix is an Array containing any of "n","w","e","s"
# limitRect: min_w, min_h, max_w, max_h, min_x, max_x, min_y, max_y
# !!! The order of the parameters is how we want them, in Movable it
# is different for compability reasons
@limitRect: (pos, limitRect, defaults={}) ->
pos.fix = pos.fix or []
for k, v of defaults
if CUI.util.isUndef(pos[k])
pos[k] = v
# console.debug "limitRect", pos, defaults, limitRect
for key in [
"min_w"
"max_w"
"min_h"
"max_h"
"min_x"
"max_x"
"min_y"
"max_y"
]
value = limitRect[key]
if CUI.util.isUndef(value)
continue
CUI.util.assert(not isNaN(value), "#{CUI.util.getObjectClass(@)}.limitRect", "key #{key} in pos isNaN", pos: pos, defaults: defaults, limitRect: limitRect)
skey = key.substring(4)
mkey = key.substring(0,3)
if key == "max_x"
value -= pos.w
if key == "max_y"
value -= pos.h
diff = pos[skey] - value
if mkey == "min"
if diff >= 0
continue
if mkey == "max"
if diff <= 0
continue
if skey == "y" and "n" in pos.fix
pos.h -= diff
continue
if skey == "x" and "w" in pos.fix
pos.w -= diff
continue
# console.debug "correcting #{skey} by #{diff} from #{pos[skey]}"
pos[skey]-=diff
if skey == "h" and "s" in pos.fix
# console.debug "FIX y"
pos.y += diff
if skey == "w" and "e" in pos.fix
# console.debug "FIX x"
pos.x += diff
if skey == "x" and "e" in pos.fix
# console.debug "FIX w"
pos.w += diff
if skey == "y" and "s" in pos.fix
# console.debug "FIX h"
pos.h += diff
# console.debug "limitRect AFTER", pos, diff
return pos