UNPKG

coffeescript-ui

Version:
723 lines (548 loc) 17.6 kB
### * 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