UNPKG

@fabiomdf/vue-hierarchy-tree

Version:

A Vue 3 component for creating interactive hierarchy trees with drag-and-drop functionality

497 lines (496 loc) 17.1 kB
import { defineComponent as z, createElementBlock as m, openBlock as B, normalizeStyle as P, normalizeClass as T, renderSlot as Y, createBlock as S, withCtx as O, createTextVNode as N, toDisplayString as $, ref as x, watch as j, nextTick as q, computed as U, toRef as J, unref as h, withDirectives as Z, createCommentVNode as F, isRef as G, vModelText as Q, onMounted as ee, reactive as H, Fragment as E, renderList as W } from "vue"; const oe = ["title"], te = /* @__PURE__ */ z({ __name: "ActionButton", props: { title: {}, position: {}, backgroundColor: {}, borderColor: {}, size: {}, shape: {}, onClick: { type: Function } }, setup(a) { const r = a, e = (n) => { n.stopPropagation(), r.onClick(n); }, o = (n) => { n.stopPropagation(); }; return (n, i) => (B(), m("button", { class: T(["action-button", `action-button--${n.position}`, `action-button--${r.shape || "circle"}`]), style: P({ backgroundColor: r.backgroundColor || "#4CAF50", borderColor: r.borderColor || "white", width: (r.size || 30) + "px", height: (r.size || 30) + "px", fontSize: Math.floor((r.size || 30) * 0.6) + "px" }), onMousedown: e, onClick: o, title: n.title }, [ Y(n.$slots, "default", {}, void 0, !0) ], 46, oe)); } }), D = (a, r) => { const e = a.__vccOpts || a; for (const [o, n] of r) e[o] = n; return e; }, V = /* @__PURE__ */ D(te, [["__scopeId", "data-v-cb73231d"]]), ne = /* @__PURE__ */ z({ __name: "AddNewDraggableBox", props: { parentId: {}, backgroundColor: {}, borderColor: {}, content: {}, size: {}, shape: {} }, emits: ["addChild"], setup(a, { emit: r }) { const e = a, o = r, n = () => { o("addChild", e.parentId); }; return (i, u) => (B(), S(V, { title: "Add Child Box", position: "bottom", backgroundColor: e.backgroundColor || "#4CAF50", borderColor: e.borderColor || "white", size: e.size || 30, shape: e.shape || "circle", onClick: n }, { default: O(() => [ N($(e.content || "+"), 1) ]), _: 1 }, 8, ["backgroundColor", "borderColor", "size", "shape"])); } }), le = /* @__PURE__ */ z({ __name: "RemoveDraggableBox", props: { boxId: {}, backgroundColor: {}, borderColor: {}, content: {}, size: {}, shape: {} }, emits: ["removeBox"], setup(a, { emit: r }) { const e = a, o = r, n = () => { o("removeBox", e.boxId); }; return (i, u) => (B(), S(V, { title: "Remove Box", position: "top", backgroundColor: e.backgroundColor || "#f44336", borderColor: e.borderColor || "white", size: e.size || 30, shape: e.shape || "circle", onClick: n }, { default: O(() => [ N($(e.content || "×"), 1) ]), _: 1 }, 8, ["backgroundColor", "borderColor", "size", "shape"])); } }); function de(a, r) { const e = x(a), o = x(!1), n = x({ x: 0, y: 0 }), i = (f) => { o.value = !0, n.value = { x: f.clientX - e.value.x, y: f.clientY - e.value.y }, window.addEventListener("mousemove", u), window.addEventListener("mouseup", g); }, u = (f) => { o.value && (e.value = { x: f.clientX - n.value.x, y: f.clientY - n.value.y }); }, g = () => { o.value = !1, window.removeEventListener("mousemove", u), window.removeEventListener("mouseup", g), r(e.value); }; return { position: e, dragging: o, startDrag: i }; } function re(a, r) { const e = typeof a == "string" ? x(a) : a, o = x(!1), n = x(e.value), i = x(null); j(e, (p) => { o.value || (n.value = p); }); const u = async () => { o.value = !0, n.value = e.value, await q(), i.value && (i.value.focus(), i.value.select()); }, g = () => { const p = n.value.trim(); p !== e.value && p !== "" && r(p), o.value = !1; }, f = () => { n.value = e.value, o.value = !1; }; return { isEditing: o, tempLabel: n, inputRef: i, startEditing: u, saveLabel: g, handleKeydown: (p) => { p.key === "Enter" ? g() : p.key === "Escape" && f(); } }; } const ae = /* @__PURE__ */ z({ __name: "DraggableBox", props: { initialPosition: {}, size: {}, id: {}, label: {}, showAddButton: { type: Boolean }, showRemoveButton: { type: Boolean }, allowLabelEdit: { type: Boolean }, enableShadow: { type: Boolean }, boxBackgroundColor: {}, boxLabelColor: {}, editFieldBorderColor: {}, editFieldBackgroundColor: {}, editFieldTextColor: {}, addButtonBackgroundColor: {}, addButtonBorderColor: {}, addButtonContent: {}, addButtonSize: {}, addButtonShape: {}, removeButtonBackgroundColor: {}, removeButtonBorderColor: {}, removeButtonContent: {}, removeButtonSize: {}, removeButtonShape: {}, isSelected: { type: Boolean }, highlightSelected: { type: Boolean }, selectedBorderColor: {}, selectedBorderWidth: {} }, emits: ["addChild", "updateLabel", "updatePosition", "removeBox", "selectBox"], setup(a, { expose: r, emit: e }) { const o = a, n = e, i = x(null), u = { width: 150, height: 100 }, g = U(() => ({ width: o.size?.width ?? u.width, height: o.size?.height ?? u.height })), { position: f, startDrag: y } = de( o.initialPosition || { x: 100, y: 100 }, (t) => n("updatePosition", o.id, t) ), { isEditing: p, tempLabel: w, inputRef: L, startEditing: R, saveLabel: k, handleKeydown: C } = re( J(o, "label"), (t) => n("updateLabel", o.id, t) ), v = (t) => { const c = t.target; c && (c.tagName === "BUTTON" || c.closest("button")) || n("selectBox", o.id); }, b = (t) => { p.value || (v(t), y(t)); }, A = (t) => { t.stopPropagation(); const c = t.target; c && (c.tagName === "BUTTON" || c.closest("button")) || o.allowLabelEdit && R(); }, l = (t) => { t.stopPropagation(); }, d = (t) => { n("addChild", t); }, s = (t) => { n("removeBox", t); }; return r({ boxRef: i }), (t, c) => (B(), m("div", { class: T(["draggable-box", { "with-shadow": o.enableShadow, selected: o.isSelected && o.highlightSelected }]), style: P({ top: h(f).y + "px", left: h(f).x + "px", width: g.value.width + "px", height: g.value.height + "px", backgroundColor: o.boxBackgroundColor || "#42b983", color: o.boxLabelColor || "white", border: o.isSelected && o.highlightSelected ? `${o.selectedBorderWidth || 3}px solid ${o.selectedBorderColor || "#ffd700"}` : "none" }), onMousedown: b, onDblclick: A, onClick: v, ref_key: "boxRef", ref: i }, [ !h(p) || !o.allowLabelEdit ? (B(), m("div", { key: 0, class: T(["box-content", { "non-editable": !o.allowLabelEdit }]) }, $(o.label), 3)) : o.allowLabelEdit ? Z((B(), m("input", { key: 1, ref_key: "inputRef", ref: L, "onUpdate:modelValue": c[0] || (c[0] = (_) => G(w) ? w.value = _ : null), class: "label-input", style: P({ backgroundColor: o.editFieldBackgroundColor || "rgba(255, 255, 255, 0.9)", borderColor: o.editFieldBorderColor || "#4CAF50", color: o.editFieldTextColor || "#333" }), onKeydown: c[1] || (c[1] = //@ts-ignore (..._) => h(C) && h(C)(..._)), onBlur: c[2] || (c[2] = //@ts-ignore (..._) => h(k) && h(k)(..._)), onClick: l }, null, 36)), [ [Q, h(w)] ]) : F("", !0), o.showAddButton ? (B(), S(ne, { key: 2, "parent-id": t.id, "background-color": o.addButtonBackgroundColor, "border-color": o.addButtonBorderColor, content: o.addButtonContent, size: o.addButtonSize, shape: o.addButtonShape, onAddChild: d }, null, 8, ["parent-id", "background-color", "border-color", "content", "size", "shape"])) : F("", !0), o.showRemoveButton ? (B(), S(le, { key: 3, "box-id": t.id, "background-color": o.removeButtonBackgroundColor, "border-color": o.removeButtonBorderColor, content: o.removeButtonContent, size: o.removeButtonSize, shape: o.removeButtonShape, onRemoveBox: s }, null, 8, ["box-id", "background-color", "border-color", "content", "size", "shape"])) : F("", !0) ], 38)); } }), ie = /* @__PURE__ */ D(ae, [["__scopeId", "data-v-c6c90518"]]), se = { class: "connection-line" }, ce = ["x1", "y1", "x2", "y2"], ue = /* @__PURE__ */ z({ __name: "ConnectionLine", props: { start: {}, end: {} }, setup(a) { const r = a, e = x(null), o = x(null), n = () => { if (r.start?.boxRef && r.end?.boxRef) { const i = r.start.boxRef.getBoundingClientRect(), u = r.end.boxRef.getBoundingClientRect(); e.value = { x: i.left + i.width / 2, y: i.top + i.height / 2 }, o.value = { x: u.left + u.width / 2, y: u.top + u.height / 2 }; } requestAnimationFrame(n); }; return ee(() => { n(); }), (i, u) => (B(), m("svg", se, [ e.value && o.value ? (B(), m("line", { key: 0, x1: e.value.x, y1: e.value.y, x2: o.value.x, y2: o.value.y, stroke: "black", "stroke-width": "2" }, null, 8, ce)) : F("", !0) ])); } }), be = /* @__PURE__ */ D(ue, [["__scopeId", "data-v-d6f9434c"]]); function he(a, r) { const e = H({ boxes: [...a.boxes], connections: [...a.connections] }), o = H({}), n = x(1), i = x(null), u = (l, d) => { d && typeof d == "object" && "boxRef" in d && (o[l] = d); }, g = () => `box_${Date.now()}_${n.value++}`, f = (l, d) => Math.abs(l.x - d.x) < r.boxWidth + 20 && Math.abs(l.y - d.y) < r.boxHeight + 20, y = (l) => { if (!r.enableCollisionAvoidance) return l; let d = { ...l }, s = 0; const t = 50, c = 50; for (; s < t; ) { if (!e.boxes.some( (X) => f(d, X.position) )) return d; const I = s * 45 % 360, M = Math.floor(s / 8) * c + c; d = { x: l.x + Math.cos(I * Math.PI / 180) * M, y: l.y + Math.sin(I * Math.PI / 180) * M }, s++; } return d; }, p = (l) => { const d = e.boxes.find((c) => c.id === l); if (!d) return { x: 100, y: 100 }; const s = e.connections.filter((c) => c.start === l).length, t = { x: d.position.x + s * 180 - 90, y: d.position.y + 150 }; return y(t); }, w = (l) => { const d = [], s = e.connections.filter((t) => t.start === l).map((t) => t.end); for (const t of s) d.push(t), d.push(...w(t)); return d; }, L = (l) => { const d = g(), s = p(l), t = { id: d, label: `Child ${d}`, position: s }; e.boxes.push(t), e.connections.push({ start: l, end: d }), v("Child added"); }, R = (l, d) => { const s = e.boxes.find((t) => t.id === l); s && (s.label = d, v("Label updated")); }, k = (l, d) => { const s = e.boxes.find((t) => t.id === l); s && (s.position = d, v("Position updated")); }, C = (l) => { const d = w(l), s = [l, ...d]; e.connections = e.connections.filter( (t) => !s.includes(t.start) && !s.includes(t.end) ), e.boxes = e.boxes.filter( (t) => !s.includes(t.id) ), s.forEach((t) => { delete o[t]; }), v("Box removed"); }, v = (l) => { r.enableConsoleLog && console.log(`${l} - FlowData:`, JSON.stringify(e, null, 2)); }, b = (l) => { i.value = l; }, A = U(() => e.connections.filter( (l) => o[l.start] && o[l.end] )); return { flowData: e, boxRefs: o, selectedBoxId: i, setBoxRef: u, addChildBox: L, updateBoxLabel: R, updateBoxPosition: k, removeBox: C, selectBox: b, validConnections: A }; } const pe = /* @__PURE__ */ z({ __name: "HierarchyTree", props: { initialData: {}, showAddButton: { type: Boolean, default: !0 }, showRemoveButton: { type: Boolean, default: !0 }, allowLabelEdit: { type: Boolean, default: !0 }, boxWidth: { default: 150 }, boxHeight: { default: 100 }, enableShadow: { type: Boolean, default: !0 }, boxBackgroundColor: { default: "#42b983" }, boxLabelColor: { default: "white" }, editFieldBorderColor: { default: "#4CAF50" }, editFieldBackgroundColor: { default: "rgba(255, 255, 255, 0.9)" }, editFieldTextColor: { default: "#333" }, addButtonBackgroundColor: { default: "#4CAF50" }, addButtonBorderColor: { default: "white" }, addButtonContent: { default: "+" }, addButtonSize: { default: 30 }, addButtonShape: { default: "circle" }, removeButtonBackgroundColor: { default: "#f44336" }, removeButtonBorderColor: { default: "white" }, removeButtonContent: { default: "×" }, removeButtonSize: { default: 30 }, removeButtonShape: { default: "circle" }, enableCollisionAvoidance: { type: Boolean, default: !0 }, enableConsoleLog: { type: Boolean, default: !1 }, highlightSelectedBox: { type: Boolean, default: !0 }, selectedBoxBorderColor: { default: "#ffd700" }, selectedBoxBorderWidth: { default: 3 }, showButtonsOnlyOnSelected: { type: Boolean, default: !1 } }, setup(a, { expose: r }) { const e = a, o = (C) => ({ width: C.size?.width ?? e.boxWidth, height: C.size?.height ?? e.boxHeight }), { flowData: n, boxRefs: i, selectedBoxId: u, setBoxRef: g, addChildBox: f, updateBoxLabel: y, updateBoxPosition: p, removeBox: w, selectBox: L, validConnections: R } = he(e.initialData, { boxWidth: e.boxWidth, boxHeight: e.boxHeight, enableCollisionAvoidance: e.enableCollisionAvoidance, enableConsoleLog: e.enableConsoleLog, highlightSelectedBox: e.highlightSelectedBox }), k = (C, v) => (v === "add" ? e.showAddButton : e.showRemoveButton) ? e.showButtonsOnlyOnSelected ? u.value === C : !0 : !1; return r({ flowData: n }), (C, v) => (B(), m(E, null, [ (B(!0), m(E, null, W(h(n).boxes, (b) => (B(), S(ie, { key: b.id, ref_for: !0, ref: (A) => h(g)(b.id, A), id: b.id, label: b.label, initialPosition: b.position, size: o(b), "show-add-button": k(b.id, "add"), "show-remove-button": k(b.id, "remove"), "allow-label-edit": e.allowLabelEdit, "enable-shadow": e.enableShadow, "box-background-color": e.boxBackgroundColor, "box-label-color": e.boxLabelColor, "edit-field-border-color": e.editFieldBorderColor, "edit-field-background-color": e.editFieldBackgroundColor, "edit-field-text-color": e.editFieldTextColor, "add-button-background-color": e.addButtonBackgroundColor, "add-button-border-color": e.addButtonBorderColor, "add-button-content": e.addButtonContent, "add-button-size": e.addButtonSize, "add-button-shape": e.addButtonShape, "remove-button-background-color": e.removeButtonBackgroundColor, "remove-button-border-color": e.removeButtonBorderColor, "remove-button-content": e.removeButtonContent, "remove-button-size": e.removeButtonSize, "remove-button-shape": e.removeButtonShape, "is-selected": h(u) === b.id, "highlight-selected": e.highlightSelectedBox, "selected-border-color": e.selectedBoxBorderColor, "selected-border-width": e.selectedBoxBorderWidth, onAddChild: h(f), onUpdateLabel: h(y), onUpdatePosition: h(p), onRemoveBox: h(w), onSelectBox: h(L) }, null, 8, ["id", "label", "initialPosition", "size", "show-add-button", "show-remove-button", "allow-label-edit", "enable-shadow", "box-background-color", "box-label-color", "edit-field-border-color", "edit-field-background-color", "edit-field-text-color", "add-button-background-color", "add-button-border-color", "add-button-content", "add-button-size", "add-button-shape", "remove-button-background-color", "remove-button-border-color", "remove-button-content", "remove-button-size", "remove-button-shape", "is-selected", "highlight-selected", "selected-border-color", "selected-border-width", "onAddChild", "onUpdateLabel", "onUpdatePosition", "onRemoveBox", "onSelectBox"]))), 128)), (B(!0), m(E, null, W(h(R), (b) => (B(), S(be, { key: `${b.start}-${b.end}`, start: h(i)[b.start], end: h(i)[b.end] }, null, 8, ["start", "end"]))), 128)) ], 64)); } }), K = /* @__PURE__ */ D(pe, [["__scopeId", "data-v-d2d3a352"]]); function Be(a) { a.component("HierarchyTree", K); } const ge = { install: Be, HierarchyTree: K }; export { K as HierarchyTree, ge as default, Be as install, de as useDrag, he as useFlowData, re as useLabelEdit };