dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
mapping.js
Go to the documentation of this file.
1/*!
2 * dbxapp mapping.js
3 * Schema mapping UI: drag/drop field assignment and visual links.
4 */
5(function (window, document) {
6 "use strict";
7
8 if (!window.dbx || !window.dbx.feature) {
9 console.error("[dbx][mapping] dbx core missing");
10 return;
11 }
12
13 const dbx = window.dbx;
14
15 function readAttr(el, name, def = "") {
16 if (!el || !el.getAttribute) return def;
17 const value = el.getAttribute(name);
18 return value == null ? def : String(value).trim();
19 }
20
21 function findRoots(el) {
22 if (!el || !el.querySelectorAll) return [];
23 if (el.matches && el.matches(".dbx-schema-mapping, [data-mapping-root='1']")) return [el];
24 return Array.from(el.querySelectorAll(".dbx-schema-mapping, [data-mapping-root='1']"));
25 }
26
27 function esc(value) {
28 value = String(value || "");
29 if (window.CSS && typeof window.CSS.escape === "function") {
30 return window.CSS.escape(value);
31 }
32 return value.replace(/([^A-Za-z0-9_-])/g, "\\$1");
33 }
34
35 function sourceItems(root) {
36 return Array.from(root.querySelectorAll("[data-mapping-source]"));
37 }
38
39 function selects(root) {
40 return Array.from(root.querySelectorAll("[data-mapping-select]"));
41 }
42
43 function targetRow(select) {
44 return select && select.closest ? select.closest("[data-mapping-target]") : null;
45 }
46
47 function findSource(root, name) {
48 if (!name) return null;
49 return sourceItems(root).find(item => readAttr(item, "data-mapping-source") === name) || null;
50 }
51
52 function isCenterVisibleIn(item, container) {
53 if (!item || !container) return false;
54
55 const rect = item.getBoundingClientRect();
56 const box = container.getBoundingClientRect();
57 const x = rect.left + (rect.width / 2);
58 const y = rect.top + (rect.height / 2);
59
60 return x >= box.left
61 && x <= box.right
62 && y >= box.top
63 && y <= box.bottom;
64 }
65
66 function setCount(root) {
67 const all = selects(root);
68 const mapped = all.filter(sel => sel.value).length;
69 root.querySelectorAll("[data-mapping-count='mapped']").forEach(el => {
70 el.textContent = mapped + " / " + all.length;
71 });
72 }
73
74 function enforceUnique(root, changed) {
75 const value = changed && changed.value ? changed.value : "";
76 if (!value) return;
77
78 selects(root).forEach(sel => {
79 if (sel !== changed && sel.value === value) {
80 sel.value = "";
81 }
82 });
83 }
84
85 function drawLines(root) {
86 const svg = root.querySelector("[data-mapping-lines]");
87 const workbench = root.querySelector(".dbx-mapping-workbench");
88 const sourceList = root.querySelector(".dbx-mapping-source-list");
89 const targetList = root.querySelector(".dbx-mapping-target-list");
90 if (!svg || !workbench) return;
91
92 while (svg.firstChild) {
93 svg.removeChild(svg.firstChild);
94 }
95
96 const box = workbench.getBoundingClientRect();
97 svg.setAttribute("viewBox", "0 0 " + Math.max(1, box.width) + " " + Math.max(1, box.height));
98
99 selects(root).forEach(sel => {
100 const source = findSource(root, sel.value);
101 const row = targetRow(sel);
102 if (!source || !row) return;
103 if (!isCenterVisibleIn(source, sourceList) || !isCenterVisibleIn(row, targetList)) return;
104
105 const s = source.getBoundingClientRect();
106 const t = row.getBoundingClientRect();
107 const x1 = s.right - box.left;
108 const y1 = s.top + (s.height / 2) - box.top;
109 const x2 = t.left - box.left;
110 const y2 = t.top + (t.height / 2) - box.top;
111 const dx = Math.max(50, Math.abs(x2 - x1) * 0.45);
112
113 const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
114 path.setAttribute("d", "M " + x1 + " " + y1 + " C " + (x1 + dx) + " " + y1 + ", " + (x2 - dx) + " " + y2 + ", " + x2 + " " + y2);
115 path.setAttribute("class", "dbx-mapping-line");
116 svg.appendChild(path);
117 });
118 }
119
120 function sync(root) {
121 const used = new Set();
122
123 selects(root).forEach(sel => {
124 const row = targetRow(sel);
125 if (!row) return;
126
127 const hasValue = !!sel.value;
128 row.classList.toggle("is-mapped", hasValue);
129 row.classList.toggle("is-empty", !hasValue);
130 row.setAttribute("data-selected-source", sel.value || "");
131 if (hasValue) used.add(sel.value);
132 });
133
134 sourceItems(root).forEach(item => {
135 const name = readAttr(item, "data-mapping-source");
136 item.classList.toggle("is-used", used.has(name));
137 });
138
139 setCount(root);
140 window.requestAnimationFrame(() => drawLines(root));
141 }
142
143 function setTarget(root, target, source) {
144 const sel = selects(root).find(item => readAttr(item, "data-target") === target);
145 if (!sel) return;
146 sel.value = source || "";
147 enforceUnique(root, sel);
148 sync(root);
149 }
150
151 function clear(root) {
152 selects(root).forEach(sel => {
153 sel.value = "";
154 });
155 sync(root);
156 }
157
158 function auto(root) {
159 selects(root).forEach(sel => {
160 sel.value = readAttr(sel, "data-auto-source");
161 });
162 sync(root);
163 }
164
165 function bind(root) {
166 if (root.__dbxMappingBound) return;
167 root.__dbxMappingBound = true;
168
169 root.addEventListener("dragstart", e => {
170 const item = e.target.closest("[data-mapping-source]");
171 if (!item || !root.contains(item)) return;
172
173 const name = readAttr(item, "data-mapping-source");
174 e.dataTransfer.effectAllowed = "copy";
175 e.dataTransfer.setData("text/plain", name);
176 root.classList.add("is-dragging");
177 item.classList.add("is-drag-source");
178 });
179
180 root.addEventListener("dragend", e => {
181 root.classList.remove("is-dragging");
182 const item = e.target.closest("[data-mapping-source]");
183 if (item) item.classList.remove("is-drag-source");
184 });
185
186 root.addEventListener("dragover", e => {
187 const drop = e.target.closest("[data-mapping-drop]");
188 if (!drop || !root.contains(drop)) return;
189 e.preventDefault();
190 drop.classList.add("is-over");
191 });
192
193 root.addEventListener("dragleave", e => {
194 const drop = e.target.closest("[data-mapping-drop]");
195 if (drop) drop.classList.remove("is-over");
196 });
197
198 root.addEventListener("drop", e => {
199 const drop = e.target.closest("[data-mapping-drop]");
200 if (!drop || !root.contains(drop)) return;
201
202 e.preventDefault();
203 drop.classList.remove("is-over");
204 setTarget(root, readAttr(drop, "data-mapping-drop"), e.dataTransfer.getData("text/plain"));
205 });
206
207 root.addEventListener("change", e => {
208 const sel = e.target.closest("[data-mapping-select]");
209 if (!sel || !root.contains(sel)) return;
210 enforceUnique(root, sel);
211 sync(root);
212 });
213
214 root.addEventListener("click", e => {
215 const action = e.target.closest("[data-mapping-action]");
216 if (action && root.contains(action)) {
217 e.preventDefault();
218 if (readAttr(action, "data-mapping-action") === "clear") clear(root);
219 if (readAttr(action, "data-mapping-action") === "auto") auto(root);
220 return;
221 }
222
223 const clearBtn = e.target.closest("[data-mapping-clear-row]");
224 if (clearBtn && root.contains(clearBtn)) {
225 e.preventDefault();
226 const row = clearBtn.closest("[data-mapping-target]");
227 const sel = row ? row.querySelector("[data-mapping-select]") : null;
228 if (sel) sel.value = "";
229 sync(root);
230 }
231 });
232
233 window.addEventListener("resize", () => sync(root), { passive: true });
234 root.addEventListener("scroll", () => sync(root), true);
235 }
236
237 dbx.mapping = dbx.mapping || {};
238 dbx.mapping.init = function (el) {
239 findRoots(el).forEach(root => {
240 bind(root);
241 sync(root);
242 });
243 };
244
245 dbx.feature.register("mapping", {
246 scope: "element",
247 priority: "mid",
248 css: [
249 ["css", "design", "c-schema-mapping.css"]
250 ],
251 init(el) {
252 dbx.mapping.init(el);
253 }
254 });
255
256})(window, document);