@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
JavaScript
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
};