diff --git a/example/demo.html b/example/demo.html index 077f3c89..c7c0948a 100644 --- a/example/demo.html +++ b/example/demo.html @@ -64,6 +64,7 @@ }, }, view: { + engine: 'svg', expander_style: 'char', }, }; diff --git a/src/plugins/jsmind.draggable-node.js b/src/plugins/jsmind.draggable-node.js index d676486d..a2dc33d3 100644 --- a/src/plugins/jsmind.draggable-node.js +++ b/src/plugins/jsmind.draggable-node.js @@ -65,10 +65,14 @@ export class DraggableNode { this.jm = jm; /** @type {DraggableNodeOptions} */ this.options = opts; - /** @type {HTMLCanvasElement|null} */ + /** @type {boolean} */ + this.is_svg_engine = jm.view.opts.engine === 'svg'; + /** @type {HTMLCanvasElement|SVGSVGElement|null} */ this.e_canvas = null; /** @type {CanvasRenderingContext2D|null} */ this.canvas_ctx = null; + /** @type {SVGPathElement|null} */ + this.helper_line = null; /** @type {HTMLElement|null} */ this.shadow = null; /** @type {number} */ @@ -114,19 +118,43 @@ export class DraggableNode { this.create_shadow(); this.event_bind(); } - /** Resize canvas and shadow elements. */ + /** Resize canvas/SVG and shadow elements. */ 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; + if (this.is_svg_engine) { + this.e_canvas.setAttribute('width', this.jm.view.size.w); + this.e_canvas.setAttribute('height', this.jm.view.size.h); + } else { + this.e_canvas.width = this.jm.view.size.w; + this.e_canvas.height = this.jm.view.size.h; + } } - /** Create canvas for drawing drag lines. */ + /** Create canvas or SVG for drawing drag lines. */ 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; + if (this.is_svg_engine) { + // Create SVG element for helper lines + var svg = this._create_svg_element('svg'); + svg.setAttribute('class', 'jsmind-draggable-helper'); + svg.setAttribute('style', 'position: absolute; top: 0; left: 0; pointer-events: none;'); + this.jm.view.e_panel.appendChild(svg); + this.e_canvas = svg; + } else { + // Create Canvas element for helper lines + 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 SVG element with proper namespace. + * @param {string} tag - SVG tag name + * @returns {SVGElement} + * @private + */ + _create_svg_element(tag) { + return $.d.createElementNS('http://www.w3.org/2000/svg', tag); } create_shadow() { var s = $.c('jmnode'); @@ -171,20 +199,31 @@ export class DraggableNode { * @param {boolean} invalid - Whether current target is invalid */ 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); + var color = invalid ? this.options.line_color_invalid : this.options.line_color; + + if (this.is_svg_engine) { + this.svg_draw_line(shadow_p.x, shadow_p.y, node_p.x, node_p.y, color); + } else { + this.canvas_ctx.lineWidth = this.options.line_width; + this.canvas_ctx.strokeStyle = color; + this.canvas_ctx.lineCap = 'round'; + this.canvas_lineto(shadow_p.x, shadow_p.y, node_p.x, node_p.y); + } } - /** Clear helper lines from canvas. */ + /** Clear helper lines from canvas or SVG. */ clear_lines() { - this.canvas_ctx.clearRect(0, 0, this.jm.view.size.w, this.jm.view.size.h); + if (this.is_svg_engine) { + if (this.helper_line && this.helper_line.parentNode) { + this.e_canvas.removeChild(this.helper_line); + this.helper_line = null; + } + } else { + this.canvas_ctx.clearRect(0, 0, this.jm.view.size.w, this.jm.view.size.h); + } } /** - * Draw a straight helper line. + * Draw a straight helper line on canvas. * @param {number} x1 * @param {number} y1 * @param {number} x2 @@ -196,6 +235,60 @@ export class DraggableNode { this.canvas_ctx.lineTo(x2, y2); this.canvas_ctx.stroke(); } + /** + * Draw a helper line on SVG using bezier curve. + * Reuses the line drawing logic from SvgGraph. + * @param {number} x1 - Start x coordinate + * @param {number} y1 - Start y coordinate + * @param {number} x2 - End x coordinate + * @param {number} y2 - End y coordinate + * @param {string} color - Line color + */ + svg_draw_line(x1, y1, x2, y2, color) { + // Create SVG path element for helper line + this.helper_line = this._create_svg_element('path'); + this.helper_line.setAttribute('stroke', color); + this.helper_line.setAttribute('stroke-width', this.options.line_width); + this.helper_line.setAttribute('fill', 'transparent'); + this.helper_line.setAttribute('stroke-linecap', 'round'); + + // Draw bezier curve (same as SvgGraph._bezier_to) + this._svg_bezier_to(this.helper_line, x1, y1, x2, y2); + + // Add to SVG container + this.e_canvas.appendChild(this.helper_line); + } + /** + * Draw bezier curve to SVG path element. + * Reuses logic from SvgGraph._bezier_to. + * @param {SVGPathElement} path - SVG path element + * @param {number} x1 - Start x coordinate + * @param {number} y1 - Start y coordinate + * @param {number} x2 - End x coordinate + * @param {number} y2 - End y coordinate + * @private + */ + _svg_bezier_to(path, x1, y1, x2, y2) { + path.setAttribute( + 'd', + 'M ' + + x1 + + ' ' + + y1 + + ' C ' + + (x1 + ((x2 - x1) * 2) / 3) + + ' ' + + y1 + + ', ' + + x1 + + ' ' + + y2 + + ', ' + + x2 + + ' ' + + y2 + ); + } /** Bind mouse/touch events for dragging. */ event_bind() { var jd = this;