467 lines
17 KiB
JavaScript
Executable File
467 lines
17 KiB
JavaScript
Executable File
import jsMind from '../jsmind.js'
|
|
|
|
if (!jsMind) {
|
|
throw new Error('jsMind is not defined')
|
|
}
|
|
|
|
const $ = jsMind.$
|
|
|
|
const clear_selection =
|
|
'getSelection' in $.w
|
|
? function () {
|
|
$.w.getSelection().removeAllRanges()
|
|
}
|
|
: function () {
|
|
$.d.selection.empty()
|
|
}
|
|
|
|
const DEFAULT_OPTIONS = {
|
|
line_width: 5,
|
|
line_color: 'rgba(0,0,0,0.3)',
|
|
line_color_invalid: 'rgba(255,51,51,0.6)',
|
|
lookup_delay: 200,
|
|
lookup_interval: 100,
|
|
scrolling_trigger_width: 20,
|
|
scrolling_step_length: 10,
|
|
shadow_node_class_name: 'jsmind-draggable-shadow-node',
|
|
}
|
|
|
|
class DraggableNode {
|
|
constructor(jm, options) {
|
|
var opts = {}
|
|
jsMind.util.json.merge(opts, DEFAULT_OPTIONS)
|
|
jsMind.util.json.merge(opts, options)
|
|
|
|
this.version = '0.4.0'
|
|
this.jm = jm
|
|
this.options = opts
|
|
this.e_canvas = null
|
|
this.canvas_ctx = null
|
|
this.shadow = null
|
|
this.shadow_p_x = 0
|
|
this.shadow_p_y = 0
|
|
this.shadow_w = 0
|
|
this.shadow_h = 0
|
|
this.active_node = null
|
|
this.target_node = null
|
|
this.target_direct = null
|
|
this.client_w = 0
|
|
this.client_h = 0
|
|
this.offset_x = 0
|
|
this.offset_y = 0
|
|
this.hlookup_delay = 0
|
|
this.hlookup_timer = 0
|
|
this.capture = false
|
|
this.moved = false
|
|
this.canvas_draggable = jm.get_view_draggable()
|
|
this.view_panel = jm.view.e_panel
|
|
this.view_panel_rect = null
|
|
}
|
|
init() {
|
|
this.create_canvas()
|
|
this.create_shadow()
|
|
this.event_bind()
|
|
}
|
|
resize() {
|
|
this.jm.view.e_nodes.appendChild(this.shadow)
|
|
this.e_canvas.width = this.jm.view.size.w
|
|
this.e_canvas.height = this.jm.view.size.h
|
|
}
|
|
create_canvas() {
|
|
var c = $.c('canvas')
|
|
this.jm.view.e_panel.appendChild(c)
|
|
var ctx = c.getContext('2d')
|
|
this.e_canvas = c
|
|
this.canvas_ctx = ctx
|
|
}
|
|
create_shadow() {
|
|
var s = $.c('jmnode')
|
|
s.style.visibility = 'hidden'
|
|
s.style.zIndex = '3'
|
|
s.style.cursor = 'move'
|
|
s.style.opacity = '0.7'
|
|
s.className = this.options.shadow_node_class_name
|
|
this.shadow = s
|
|
}
|
|
reset_shadow(el) {
|
|
var s = this.shadow.style
|
|
this.shadow.innerHTML = el.innerHTML
|
|
s.left = el.style.left
|
|
s.top = el.style.top
|
|
s.width = el.style.width
|
|
s.height = el.style.height
|
|
s.backgroundImage = el.style.backgroundImage
|
|
s.backgroundSize = el.style.backgroundSize
|
|
s.transform = el.style.transform
|
|
this.shadow_w = this.shadow.clientWidth
|
|
this.shadow_h = this.shadow.clientHeight
|
|
}
|
|
show_shadow() {
|
|
if (!this.moved) {
|
|
this.shadow.style.visibility = 'visible'
|
|
}
|
|
}
|
|
hide_shadow() {
|
|
this.shadow.style.visibility = 'hidden'
|
|
}
|
|
magnet_shadow(shadow_p, node_p, invalid) {
|
|
this.canvas_ctx.lineWidth = this.options.line_width
|
|
this.canvas_ctx.strokeStyle = invalid
|
|
? this.options.line_color_invalid
|
|
: this.options.line_color
|
|
this.canvas_ctx.lineCap = 'round'
|
|
this.clear_lines()
|
|
this.canvas_lineto(shadow_p.x, shadow_p.y, node_p.x, node_p.y)
|
|
}
|
|
clear_lines() {
|
|
this.canvas_ctx.clearRect(0, 0, this.jm.view.size.w, this.jm.view.size.h)
|
|
}
|
|
canvas_lineto(x1, y1, x2, y2) {
|
|
this.canvas_ctx.beginPath()
|
|
this.canvas_ctx.moveTo(x1, y1)
|
|
this.canvas_ctx.lineTo(x2, y2)
|
|
this.canvas_ctx.stroke()
|
|
}
|
|
event_bind() {
|
|
var jd = this
|
|
var container = this.jm.view.container
|
|
$.on(container, 'mousedown', function (e) {
|
|
if (e.button === 0) {
|
|
jd.dragstart.call(jd, e)
|
|
}
|
|
})
|
|
$.on(container, 'mousemove', function (e) {
|
|
if (e.movementX !== 0 || e.movementY !== 0) {
|
|
jd.drag.call(jd, e)
|
|
}
|
|
})
|
|
$.on(container, 'mouseup', function (e) {
|
|
jd.dragend.call(jd, e)
|
|
})
|
|
$.on(container, 'touchstart', function (e) {
|
|
jd.dragstart.call(jd, e)
|
|
})
|
|
$.on(container, 'touchmove', function (e) {
|
|
jd.drag.call(jd, e)
|
|
})
|
|
$.on(container, 'touchend', function (e) {
|
|
jd.dragend.call(jd, e)
|
|
})
|
|
}
|
|
dragstart(e) {
|
|
if (!this.jm.get_editable()) {
|
|
return
|
|
}
|
|
if (this.capture) {
|
|
return
|
|
}
|
|
var jview = this.jm.view
|
|
if (jview.is_editing()) {
|
|
return
|
|
}
|
|
this.active_node = null
|
|
this.view_draggable = this.jm.get_view_draggable()
|
|
|
|
var el = this.find_node_element(e.target)
|
|
if (!el) {
|
|
return
|
|
}
|
|
if (this.view_draggable) {
|
|
this.jm.disable_view_draggable()
|
|
}
|
|
var nodeid = jview.get_binded_nodeid(el)
|
|
if (!!nodeid) {
|
|
var node = this.jm.get_node(nodeid)
|
|
if (!node.isroot) {
|
|
this.reset_shadow(el)
|
|
this.view_panel_rect = this.view_panel.getBoundingClientRect()
|
|
this.active_node = node
|
|
this.offset_x =
|
|
(e.clientX || e.touches[0].clientX) / jview.zoom_current - el.offsetLeft
|
|
this.offset_y =
|
|
(e.clientY || e.touches[0].clientY) / jview.zoom_current - el.offsetTop
|
|
this.client_hw = Math.floor(el.clientWidth / 2)
|
|
this.client_hh = Math.floor(el.clientHeight / 2)
|
|
if (this.hlookup_delay != 0) {
|
|
$.w.clearTimeout(this.hlookup_delay)
|
|
}
|
|
if (this.hlookup_timer != 0) {
|
|
$.w.clearInterval(this.hlookup_timer)
|
|
}
|
|
var jd = this
|
|
this.hlookup_delay = $.w.setTimeout(function () {
|
|
jd.hlookup_delay = 0
|
|
jd.hlookup_timer = $.w.setInterval(function () {
|
|
jd.lookup_target_node.call(jd)
|
|
}, jd.options.lookup_interval)
|
|
}, this.options.lookup_delay)
|
|
jd.capture = true
|
|
}
|
|
}
|
|
}
|
|
drag(e) {
|
|
if (!this.jm.get_editable()) {
|
|
return
|
|
}
|
|
if (this.capture) {
|
|
e.preventDefault()
|
|
this.show_shadow()
|
|
this.moved = true
|
|
clear_selection()
|
|
var jview = this.jm.view
|
|
var px = (e.clientX || e.touches[0].clientX) / jview.zoom_current - this.offset_x
|
|
var py = (e.clientY || e.touches[0].clientY) / jview.zoom_current - this.offset_y
|
|
// scrolling container axisY if drag nodes exceeding container
|
|
if (
|
|
e.clientY - this.view_panel_rect.top < this.options.scrolling_trigger_width &&
|
|
this.view_panel.scrollTop > this.options.scrolling_step_length
|
|
) {
|
|
this.view_panel.scrollBy(0, -this.options.scrolling_step_length)
|
|
this.offset_y += this.options.scrolling_step_length / jview.zoom_current
|
|
} else if (
|
|
this.view_panel_rect.bottom - e.clientY < this.options.scrolling_trigger_width &&
|
|
this.view_panel.scrollTop <
|
|
this.view_panel.scrollHeight -
|
|
this.view_panel_rect.height -
|
|
this.options.scrolling_step_length
|
|
) {
|
|
this.view_panel.scrollBy(0, this.options.scrolling_step_length)
|
|
this.offset_y -= this.options.scrolling_step_length / jview.zoom_current
|
|
}
|
|
// scrolling container axisX if drag nodes exceeding container
|
|
if (
|
|
e.clientX - this.view_panel_rect.left < this.options.scrolling_trigger_width &&
|
|
this.view_panel.scrollLeft > this.options.scrolling_step_length
|
|
) {
|
|
this.view_panel.scrollBy(-this.options.scrolling_step_length, 0)
|
|
this.offset_x += this.options.scrolling_step_length / jview.zoom_current
|
|
} else if (
|
|
this.view_panel_rect.right - e.clientX < this.options.scrolling_trigger_width &&
|
|
this.view_panel.scrollLeft <
|
|
this.view_panel.scrollWidth -
|
|
this.view_panel_rect.width -
|
|
this.options.scrolling_step_length
|
|
) {
|
|
this.view_panel.scrollBy(this.options.scrolling_step_length, 0)
|
|
this.offset_x -= this.options.scrolling_step_length / jview.zoom_current
|
|
}
|
|
this.shadow.style.left = px + 'px'
|
|
this.shadow.style.top = py + 'px'
|
|
clear_selection()
|
|
}
|
|
}
|
|
dragend(e) {
|
|
if (!this.jm.get_editable()) {
|
|
return
|
|
}
|
|
if (this.view_draggable) {
|
|
this.jm.enable_view_draggable()
|
|
}
|
|
if (this.capture) {
|
|
if (this.hlookup_delay != 0) {
|
|
$.w.clearTimeout(this.hlookup_delay)
|
|
this.hlookup_delay = 0
|
|
this.clear_lines()
|
|
}
|
|
if (this.hlookup_timer != 0) {
|
|
$.w.clearInterval(this.hlookup_timer)
|
|
this.hlookup_timer = 0
|
|
this.clear_lines()
|
|
}
|
|
if (this.moved) {
|
|
var src_node = this.active_node
|
|
var target_node = this.target_node
|
|
var target_direct = this.target_direct
|
|
this.move_node(src_node, target_node, target_direct)
|
|
}
|
|
this.hide_shadow()
|
|
}
|
|
this.view_panel_rect = null
|
|
this.moved = false
|
|
this.capture = false
|
|
}
|
|
find_node_element(el) {
|
|
if (
|
|
!el ||
|
|
el === this.jm.view.e_nodes ||
|
|
el === this.jm.view.e_panel ||
|
|
el === this.jm.view.container
|
|
) {
|
|
return null
|
|
}
|
|
if (el.tagName.toLowerCase() === 'jmnode') {
|
|
return el
|
|
}
|
|
return this.find_node_element(el.parentNode)
|
|
}
|
|
lookup_target_node() {
|
|
let sx = this.shadow.offsetLeft
|
|
let sy = this.shadow.offsetTop
|
|
if (sx === this.shadow_p_x && sy === this.shadow_p_y) {
|
|
return
|
|
}
|
|
this.shadow_p_x = sx
|
|
this.shadow_p_y = sy
|
|
|
|
let target_direction =
|
|
this.shadow_p_x + this.shadow_w / 2 >= this.get_root_x()
|
|
? jsMind.direction.right
|
|
: jsMind.direction.left
|
|
let overlapping_node = this.lookup_overlapping_node_parent(target_direction)
|
|
let target_node = overlapping_node || this.lookup_close_node(target_direction)
|
|
if (!!target_node) {
|
|
let points = this.calc_point_of_node(target_node, target_direction)
|
|
let invalid = jsMind.node.inherited(this.active_node, target_node)
|
|
this.magnet_shadow(points.sp, points.np, invalid)
|
|
this.target_node = target_node
|
|
this.target_direct = target_direction
|
|
}
|
|
}
|
|
get_root_x() {
|
|
let root = this.jm.get_root()
|
|
let root_location = root.get_location()
|
|
let root_size = root.get_size()
|
|
return root_location.x + root_size.w / 2
|
|
}
|
|
|
|
lookup_overlapping_node_parent(direction) {
|
|
let shadowRect = this.shadow.getBoundingClientRect()
|
|
let x = shadowRect.x + (shadowRect.width * (1 - direction)) / 2
|
|
let deltaX = (this.jm.options.layout.hspace + this.jm.options.layout.pspace) * direction
|
|
let deltaY = shadowRect.height
|
|
let points = [
|
|
[x, shadowRect.y],
|
|
[x, shadowRect.y + deltaY / 2],
|
|
[x, shadowRect.y + deltaY],
|
|
[x + deltaX / 2, shadowRect.y],
|
|
[x + deltaX / 2, shadowRect.y + deltaY / 2],
|
|
[x + deltaX / 2, shadowRect.y + deltaY],
|
|
[x + deltaX, shadowRect.y],
|
|
[x + deltaX, shadowRect.y + deltaY / 2],
|
|
[x + deltaX, shadowRect.y + deltaY],
|
|
]
|
|
for (const p of points) {
|
|
let n = this.lookup_node_parent_by_location(p[0], p[1])
|
|
if (!!n) {
|
|
return n
|
|
}
|
|
}
|
|
}
|
|
|
|
lookup_node_parent_by_location(x, y) {
|
|
return $.d
|
|
.elementsFromPoint(x, y)
|
|
.filter(
|
|
(x) => x.tagName === 'JMNODE' && x.className !== this.options.shadow_node_class_name
|
|
)
|
|
.map((el) => this.jm.view.get_binded_nodeid(el))
|
|
.map((id) => id && this.jm.mind.nodes[id])
|
|
.map((n) => n && n.parent)
|
|
.find((n) => n)
|
|
}
|
|
|
|
lookup_close_node(direction) {
|
|
return Object.values(this.jm.mind.nodes)
|
|
.filter((n) => n.direction == direction || n.isroot)
|
|
.filter((n) => this.jm.layout.is_visible(n))
|
|
.filter((n) => this.shadow_on_target_side(n, direction))
|
|
.map((n) => ({ node: n, distance: this.shadow_to_node(n, direction) }))
|
|
.reduce(
|
|
(prev, curr) => {
|
|
return prev.distance < curr.distance ? prev : curr
|
|
},
|
|
{ node: this.jm.get_root(), distance: Number.MAX_VALUE }
|
|
).node
|
|
}
|
|
|
|
shadow_on_target_side(node, dir) {
|
|
return (
|
|
(dir == jsMind.direction.right && this.shadow_to_right_of_node(node) > 0) ||
|
|
(dir == jsMind.direction.left && this.shadow_to_left_of_node(node) > 0)
|
|
)
|
|
}
|
|
|
|
shadow_to_right_of_node(node) {
|
|
return this.shadow_p_x - node.get_location().x - node.get_size().w
|
|
}
|
|
|
|
shadow_to_left_of_node(node) {
|
|
return node.get_location().x - this.shadow_p_x - this.shadow_w
|
|
}
|
|
|
|
shadow_to_base_line_of_node(node) {
|
|
return this.shadow_p_y + this.shadow_h / 2 - node.get_location().y - node.get_size().h / 2
|
|
}
|
|
|
|
shadow_to_node(node, dir) {
|
|
let distance_x =
|
|
dir === jsMind.direction.right
|
|
? Math.abs(this.shadow_to_right_of_node(node))
|
|
: Math.abs(this.shadow_to_left_of_node(node))
|
|
let distance_y = Math.abs(this.shadow_to_base_line_of_node(node))
|
|
return distance_x + distance_y
|
|
}
|
|
|
|
calc_point_of_node(node, dir) {
|
|
let ns = node.get_size()
|
|
let nl = node.get_location()
|
|
let node_x = node.isroot
|
|
? nl.x + ns.w / 2
|
|
: nl.x + (ns.w * (1 + dir)) / 2 + this.options.line_width * dir
|
|
let node_y = nl.y + ns.h / 2
|
|
let shadow_x =
|
|
this.shadow_p_x + (this.shadow_w * (1 - dir)) / 2 - this.options.line_width * dir
|
|
let shadow_y = this.shadow_p_y + this.shadow_h / 2
|
|
return {
|
|
sp: { x: shadow_x, y: shadow_y },
|
|
np: { x: node_x, y: node_y },
|
|
}
|
|
}
|
|
|
|
move_node(src_node, target_node, target_direct) {
|
|
var shadow_h = this.shadow.offsetTop
|
|
if (!!target_node && !!src_node && !jsMind.node.inherited(src_node, target_node)) {
|
|
// lookup before_node
|
|
var sibling_nodes = target_node.children
|
|
var sc = sibling_nodes.length
|
|
var node = null
|
|
var delta_y = Number.MAX_VALUE
|
|
var node_before = null
|
|
var beforeid = '_last_'
|
|
while (sc--) {
|
|
node = sibling_nodes[sc]
|
|
if (node.direction == target_direct && node.id != src_node.id) {
|
|
var dy = node.get_location().y - shadow_h
|
|
if (dy > 0 && dy < delta_y) {
|
|
delta_y = dy
|
|
node_before = node
|
|
beforeid = '_first_'
|
|
}
|
|
}
|
|
}
|
|
if (!!node_before) {
|
|
beforeid = node_before.id
|
|
}
|
|
this.jm.move_node(src_node.id, beforeid, target_node.id, target_direct)
|
|
}
|
|
this.active_node = null
|
|
this.target_node = null
|
|
this.target_direct = null
|
|
}
|
|
jm_event_handle(type, data) {
|
|
if (type === jsMind.event_type.resize) {
|
|
this.resize()
|
|
}
|
|
}
|
|
}
|
|
|
|
var draggable_plugin = new jsMind.plugin('draggable_node', function (jm, options) {
|
|
var jd = new DraggableNode(jm, options)
|
|
jd.init()
|
|
jm.add_event_listener(function (type, data) {
|
|
jd.jm_event_handle.call(jd, type, data)
|
|
})
|
|
})
|
|
|
|
jsMind.register_plugin(draggable_plugin)
|