3 * Schema mapping UI: drag/drop field assignment and visual links.
5(function (window, document) {
8 if (!window.dbx || !window.dbx.feature) {
9 console.error("[dbx][mapping] dbx core missing");
13 const dbx = window.dbx;
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();
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']"));
28 value = String(value || "");
29 if (window.CSS && typeof window.CSS.escape === "function") {
30 return window.CSS.escape(value);
32 return value.replace(/([^A-Za-z0-9_-])/g, "\\$1");
35 function sourceItems(root) {
36 return Array.from(root.querySelectorAll("[data-mapping-source]"));
39 function selects(root) {
40 return Array.from(root.querySelectorAll("[data-mapping-select]"));
43 function targetRow(select) {
44 return select && select.closest ? select.closest("[data-mapping-target]") : null;
47 function findSource(root, name) {
48 if (!name) return null;
49 return sourceItems(root).find(item => readAttr(item, "data-mapping-source") === name) || null;
52 function isCenterVisibleIn(item, container) {
53 if (!item || !container) return false;
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);
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;
74 function enforceUnique(root, changed) {
75 const value = changed && changed.value ? changed.value : "";
78 selects(root).forEach(sel => {
79 if (sel !== changed && sel.value === value) {
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;
92 while (svg.firstChild) {
93 svg.removeChild(svg.firstChild);
96 const box = workbench.getBoundingClientRect();
97 svg.setAttribute("viewBox", "0 0 " + Math.max(1, box.width) + " " + Math.max(1, box.height));
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;
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);
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);
120 function sync(root) {
121 const used = new Set();
123 selects(root).forEach(sel => {
124 const row = targetRow(sel);
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);
134 sourceItems(root).forEach(item => {
135 const name = readAttr(item, "data-mapping-source");
136 item.classList.toggle("is-used", used.has(name));
140 window.requestAnimationFrame(() => drawLines(root));
143 function setTarget(root, target, source) {
144 const sel = selects(root).find(item => readAttr(item, "data-target") === target);
146 sel.value = source || "";
147 enforceUnique(root, sel);
151 function clear(root) {
152 selects(root).forEach(sel => {
158 function auto(root) {
159 selects(root).forEach(sel => {
160 sel.value = readAttr(sel, "data-auto-source");
165 function bind(root) {
166 if (root.__dbxMappingBound) return;
167 root.__dbxMappingBound = true;
169 root.addEventListener("dragstart", e => {
170 const item = e.target.closest("[data-mapping-source]");
171 if (!item || !root.contains(item)) return;
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");
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");
186 root.addEventListener("dragover", e => {
187 const drop = e.target.closest("[data-mapping-drop]");
188 if (!drop || !root.contains(drop)) return;
190 drop.classList.add("is-over");
193 root.addEventListener("dragleave", e => {
194 const drop = e.target.closest("[data-mapping-drop]");
195 if (drop) drop.classList.remove("is-over");
198 root.addEventListener("drop", e => {
199 const drop = e.target.closest("[data-mapping-drop]");
200 if (!drop || !root.contains(drop)) return;
203 drop.classList.remove("is-over");
204 setTarget(root, readAttr(drop, "data-mapping-drop"), e.dataTransfer.getData("text/plain"));
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);
214 root.addEventListener("click", e => {
215 const action = e.target.closest("[data-mapping-action]");
216 if (action && root.contains(action)) {
218 if (readAttr(action, "data-mapping-action") === "clear") clear(root);
219 if (readAttr(action, "data-mapping-action") === "auto") auto(root);
223 const clearBtn = e.target.closest("[data-mapping-clear-row]");
224 if (clearBtn && root.contains(clearBtn)) {
226 const row = clearBtn.closest("[data-mapping-target]");
227 const sel = row ? row.querySelector("[data-mapping-select]") : null;
228 if (sel) sel.value = "";
233 window.addEventListener("resize", () => sync(root), { passive: true });
234 root.addEventListener("scroll", () => sync(root), true);
237 dbx.mapping = dbx.mapping || {};
238 dbx.mapping.init = function (el) {
239 findRoots(el).forEach(root => {
245 dbx.feature.register("mapping", {
249 ["css", "design", "c-schema-mapping.css"]
252 dbx.mapping.init(el);