node_module = require './node' util = require './util' Position = node_module.Position $ = jQuery class DragAndDropHandler constructor: (tree_widget) -> @tree_widget = tree_widget @hovered_area = null @$ghost = null @hit_areas = [] @is_dragging = false @current_item = null mouseCapture: (position_info) -> $element = $(position_info.target) if not @mustCaptureElement($element) return null if @tree_widget.options.onIsMoveHandle and not @tree_widget.options.onIsMoveHandle($element) return null node_element = @tree_widget._getNodeElement($element) if node_element and @tree_widget.options.onCanMove if not @tree_widget.options.onCanMove(node_element.node) node_element = null @current_item = node_element return (@current_item != null) mouseStart: (position_info) -> @refresh() offset = $(position_info.target).offset() node = @current_item.node if @tree_widget.options.autoEscape node_name = util.html_escape(node.name) else node_name = node.name @drag_element = new DragElement( node_name, position_info.page_x - offset.left, position_info.page_y - offset.top, @tree_widget.element ) @is_dragging = true @current_item.$element.addClass('jqtree-moving') return true mouseDrag: (position_info) -> @drag_element.move(position_info.page_x, position_info.page_y) area = @findHoveredArea(position_info.page_x, position_info.page_y) can_move_to = @canMoveToArea(area) if can_move_to and area if !area.node.isFolder() @stopOpenFolderTimer() if @hovered_area != area @hovered_area = area # If this is a closed folder, start timer to open it if @mustOpenFolderTimer(area) @startOpenFolderTimer(area.node) else @stopOpenFolderTimer() @updateDropHint() else @removeHover() @removeDropHint() @stopOpenFolderTimer() if not area if @tree_widget.options.onDragMove? @tree_widget.options.onDragMove(@current_item.node, position_info.original_event) return true mustCaptureElement: ($element) -> return not $element.is('input,select,textarea') canMoveToArea: (area) -> if not area return false else if @tree_widget.options.onCanMoveTo position_name = Position.getName(area.position) return @tree_widget.options.onCanMoveTo(@current_item.node, area.node, position_name) else return true mouseStop: (position_info) -> @moveItem(position_info) @clear() @removeHover() @removeDropHint() @removeHitAreas() current_item = @current_item if @current_item @current_item.$element.removeClass('jqtree-moving') @current_item = null @is_dragging = false if not @hovered_area and current_item if @tree_widget.options.onDragStop? @tree_widget.options.onDragStop(current_item.node, position_info.original_event) return false refresh: -> @removeHitAreas() if @current_item @generateHitAreas() @current_item = @tree_widget._getNodeElementForNode(@current_item.node) if @is_dragging @current_item.$element.addClass('jqtree-moving') removeHitAreas: -> @hit_areas = [] clear: -> @drag_element.remove() @drag_element = null removeDropHint: -> if @previous_ghost @previous_ghost.remove() removeHover: -> @hovered_area = null generateHitAreas: -> hit_areas_generator = new HitAreasGenerator( @tree_widget.tree, @current_item.node, @getTreeDimensions().bottom ) @hit_areas = hit_areas_generator.generate() findHoveredArea: (x, y) -> dimensions = @getTreeDimensions() if ( x < dimensions.left or y < dimensions.top or x > dimensions.right or y > dimensions.bottom ) return null low = 0 high = @hit_areas.length while (low < high) mid = (low + high) >> 1 area = @hit_areas[mid] if y < area.top high = mid else if y > area.bottom low = mid + 1 else return area return null mustOpenFolderTimer: (area) -> node = area.node return ( node.isFolder() and not node.is_open and area.position == Position.INSIDE ) updateDropHint: -> if not @hovered_area return # remove previous drop hint @removeDropHint() # add new drop hint node_element = @tree_widget._getNodeElementForNode(@hovered_area.node) @previous_ghost = node_element.addDropHint(@hovered_area.position) startOpenFolderTimer: (folder) -> openFolder = => @tree_widget._openNode( folder, @tree_widget.options.slide, => @refresh() @updateDropHint() ) @stopOpenFolderTimer() @open_folder_timer = setTimeout(openFolder, @tree_widget.options.openFolderDelay) stopOpenFolderTimer: -> if @open_folder_timer clearTimeout(@open_folder_timer) @open_folder_timer = null moveItem: (position_info) -> if ( @hovered_area and @hovered_area.position != Position.NONE and @canMoveToArea(@hovered_area) ) moved_node = @current_item.node target_node = @hovered_area.node position = @hovered_area.position previous_parent = moved_node.parent if position == Position.INSIDE @hovered_area.node.is_open = true doMove = => @tree_widget.tree.moveNode(moved_node, target_node, position) @tree_widget.element.empty() @tree_widget._refreshElements() event = @tree_widget._triggerEvent( 'tree.move', move_info: moved_node: moved_node target_node: target_node position: Position.getName(position) previous_parent: previous_parent do_move: doMove original_event: position_info.original_event ) doMove() unless event.isDefaultPrevented() getTreeDimensions: -> # Return the dimensions of the tree. Add a margin to the bottom to allow # for some to drag-and-drop the last element. offset = @tree_widget.element.offset() return { left: offset.left, top: offset.top, right: offset.left + @tree_widget.element.width(), bottom: offset.top + @tree_widget.element.height() + 16 } class VisibleNodeIterator constructor: (tree) -> @tree = tree iterate: -> is_first_node = true _iterateNode = (node, next_node) => must_iterate_inside = ( (node.is_open or not node.element) and node.hasChildren() ) if node.element $element = $(node.element) if not $element.is(':visible') return if is_first_node @handleFirstNode(node, $element) is_first_node = false if not node.hasChildren() @handleNode(node, next_node, $element) else if node.is_open if not @handleOpenFolder(node, $element) must_iterate_inside = false else @handleClosedFolder(node, next_node, $element) if must_iterate_inside children_length = node.children.length for child, i in node.children if i == (children_length - 1) _iterateNode(node.children[i], null) else _iterateNode(node.children[i], node.children[i+1]) if node.is_open @handleAfterOpenFolder(node, next_node, $element) _iterateNode(@tree, null) handleNode: (node, next_node, $element) -> # override handleOpenFolder: (node, $element) -> # override # return # - true: continue iterating # - false: stop iterating handleClosedFolder: (node, next_node, $element) -> # override handleAfterOpenFolder: (node, next_node, $element) -> # override handleFirstNode: (node, $element) -> # override class HitAreasGenerator extends VisibleNodeIterator constructor: (tree, current_node, tree_bottom) -> super(tree) @current_node = current_node @tree_bottom = tree_bottom generate: -> @positions = [] @last_top = 0 @iterate() return @generateHitAreas(@positions) getTop: ($element) -> return $element.offset().top addPosition: (node, position, top) -> area = { top: top node: node position: position } @positions.push(area) @last_top = top handleNode: (node, next_node, $element) -> top = @getTop($element) if node == @current_node # Cannot move inside current item @addPosition(node, Position.NONE, top) else @addPosition(node, Position.INSIDE, top) if ( next_node == @current_node or node == @current_node ) # Cannot move before or after current item @addPosition(node, Position.NONE, top) else @addPosition(node, Position.AFTER, top) handleOpenFolder: (node, $element) -> if node == @current_node # Cannot move inside current item # Stop iterating return false # Cannot move before current item if node.children[0] != @current_node @addPosition(node, Position.INSIDE, @getTop($element)) # Continue iterating return true handleClosedFolder: (node, next_node, $element) -> top = @getTop($element) if node == @current_node # Cannot move after current item @addPosition(node, Position.NONE, top) else @addPosition(node, Position.INSIDE, top) # Cannot move before current item if next_node != @current_node @addPosition(node, Position.AFTER, top) handleFirstNode: (node, $element) -> if node != @current_node @addPosition(node, Position.BEFORE, @getTop($(node.element))) handleAfterOpenFolder: (node, next_node, $element) -> if ( node == @current_node.node or next_node == @current_node.node ) # Cannot move before or after current item @addPosition(node, Position.NONE, @last_top) else @addPosition(node, Position.AFTER, @last_top) generateHitAreas: (positions) -> previous_top = -1 group = [] hit_areas = [] for position in positions if position.top != previous_top and group.length if group.length @generateHitAreasForGroup( hit_areas, group, previous_top, position.top ) previous_top = position.top group = [] group.push(position) @generateHitAreasForGroup( hit_areas, group, previous_top, @tree_bottom ) return hit_areas generateHitAreasForGroup: (hit_areas, positions_in_group, top, bottom) -> # limit positions in group position_count = Math.min(positions_in_group.length, 4) area_height = Math.round((bottom - top) / position_count) area_top = top i = 0 while (i < position_count) position = positions_in_group[i] hit_areas.push( top: area_top, bottom: area_top + area_height, node: position.node, position: position.position ) area_top += area_height i += 1 return null class DragElement constructor: (node_name, offset_x, offset_y, $tree) -> @offset_x = offset_x @offset_y = offset_y @$element = $("#{ node_name }") @$element.css("position", "absolute") $tree.append(@$element) move: (page_x, page_y) -> @$element.offset( left: page_x - @offset_x, top: page_y - @offset_y ) remove: -> @$element.remove() module.exports = DragAndDropHandler: DragAndDropHandler DragElement: DragElement HitAreasGenerator: HitAreasGenerator