This commit is contained in:
@@ -9,6 +9,8 @@ const state = {
|
||||
dragPlacementId: null,
|
||||
pointerDrag: null,
|
||||
selectedComponentId: null,
|
||||
libraryDragComponentId: null,
|
||||
rackDropPreviewY: null,
|
||||
};
|
||||
|
||||
const ui = {};
|
||||
@@ -64,6 +66,7 @@ function bindEvents() {
|
||||
ui.exportBom.addEventListener("click", copyBomCsv);
|
||||
document.addEventListener("pointermove", handlePointerMove);
|
||||
document.addEventListener("pointerup", handlePointerUp);
|
||||
document.addEventListener("dragend", handleLibraryDragEnd);
|
||||
}
|
||||
|
||||
async function loadBootstrap() {
|
||||
@@ -152,7 +155,7 @@ function renderLibrary() {
|
||||
ui.componentLibrary.innerHTML = filtered
|
||||
.map(
|
||||
(component) => `
|
||||
<article class="component-card component-card--${escapeHtml(component.category)}">
|
||||
<article class="component-card component-card--${escapeHtml(component.category)}" draggable="true" data-component-drag-id="${escapeHtml(component.id)}">
|
||||
<div class="component-card__preview">
|
||||
${renderComponentFace(component, "library")}
|
||||
</div>
|
||||
@@ -169,7 +172,7 @@ function renderLibrary() {
|
||||
<span>${formatCurrency(component.priceNet, component.currency)}</span>
|
||||
</div>
|
||||
<button type="button" data-action="select-component" data-component-id="${escapeHtml(component.id)}">
|
||||
${state.selectedComponentId === component.id ? "Ausgewaehlt" : "Zum Einstecken waehlen"}
|
||||
${state.selectedComponentId === component.id ? "Bereit zum Ziehen" : "Ins Rack ziehen"}
|
||||
</button>
|
||||
</article>
|
||||
`
|
||||
@@ -182,40 +185,21 @@ function renderLibrary() {
|
||||
renderAll();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addComponentToRack(componentId) {
|
||||
const component = state.components.find((entry) => entry.id === componentId);
|
||||
const rack = getCurrentRackTemplate();
|
||||
if (!component || !rack) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = findFirstFreePosition(component.heightU);
|
||||
if (position === null) {
|
||||
alert("Keine freie Position fuer diese Komponente im aktuellen Rack.");
|
||||
return;
|
||||
}
|
||||
|
||||
state.placedItems.push({
|
||||
placementId: `p${state.nextPlacementId++}`,
|
||||
componentId: component.id,
|
||||
y: position,
|
||||
x: getDefaultComponentX(component),
|
||||
ui.componentLibrary.querySelectorAll("[data-component-drag-id]").forEach((card) => {
|
||||
card.addEventListener("dragstart", (event) => beginLibraryDrag(event, card.dataset.componentDragId));
|
||||
});
|
||||
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function renderSelectionInfo() {
|
||||
const component = getComponent(state.selectedComponentId);
|
||||
const component = getComponent(state.libraryDragComponentId || state.selectedComponentId);
|
||||
if (!component) {
|
||||
ui.selectedComponentInfo.textContent = "Kein Element ausgewaehlt";
|
||||
ui.selectedComponentInfo.classList.remove("is-active");
|
||||
return;
|
||||
}
|
||||
|
||||
ui.selectedComponentInfo.textContent = `${component.name} · ${component.heightU}U · freie HE-Zone anklicken`;
|
||||
ui.selectedComponentInfo.textContent = `${component.name} · ${component.heightU}U · aus der Bibliothek ins Rack ziehen`;
|
||||
ui.selectedComponentInfo.classList.add("is-active");
|
||||
}
|
||||
|
||||
@@ -251,6 +235,10 @@ function renderRack() {
|
||||
|
||||
const layer = document.getElementById("rack-items-layer");
|
||||
layer.style.height = "100%";
|
||||
const bay = ui.rackGrid.querySelector(".rack-grid__bay");
|
||||
bay.addEventListener("dragover", handleRackDragOver);
|
||||
bay.addEventListener("drop", handleRackDrop);
|
||||
bay.addEventListener("dragleave", handleRackDragLeave);
|
||||
|
||||
const insertionLayer = document.getElementById("rack-insertion-layer");
|
||||
insertionLayer.innerHTML = renderInsertionZones(rack);
|
||||
@@ -282,7 +270,7 @@ function renderRack() {
|
||||
<strong>${escapeHtml(component.name)}</strong>
|
||||
<div class="rack-item__meta">${component.heightU}U · ${component.depthMm} mm · ${formatCurrency(component.priceNet, component.currency)}</div>
|
||||
</div>
|
||||
<span class="chip">${formatRackPosition(item.y)}</span>
|
||||
<span class="chip">${formatRackPosition(item.y, component)}</span>
|
||||
</div>
|
||||
<div class="rack-item__drag-hint">Ziehen zum verschieben</div>
|
||||
<button type="button" class="rack-item__remove" data-action="remove" aria-label="Komponente entfernen">×</button>
|
||||
@@ -300,19 +288,20 @@ function renderRack() {
|
||||
}
|
||||
|
||||
function insertSelectedComponentAt(y) {
|
||||
if (!state.selectedComponentId) {
|
||||
const componentId = state.libraryDragComponentId || state.selectedComponentId;
|
||||
if (!componentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const component = getComponent(state.selectedComponentId);
|
||||
const component = getComponent(componentId);
|
||||
const rack = getCurrentRackTemplate();
|
||||
if (!component || !rack) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextRect = {
|
||||
left: getDefaultComponentX(component),
|
||||
top: y,
|
||||
left: 0,
|
||||
top: snapToUnit(y),
|
||||
width: getComponentWidthPercent(component),
|
||||
height: getComponentHeightPx(component),
|
||||
};
|
||||
@@ -340,9 +329,11 @@ function insertSelectedComponentAt(y) {
|
||||
state.placedItems.push({
|
||||
placementId: `p${state.nextPlacementId++}`,
|
||||
componentId: component.id,
|
||||
y,
|
||||
x: getDefaultComponentX(component),
|
||||
y: snapToUnit(y),
|
||||
x: 0,
|
||||
});
|
||||
state.libraryDragComponentId = null;
|
||||
state.rackDropPreviewY = null;
|
||||
state.selectedComponentId = null;
|
||||
renderAll();
|
||||
}
|
||||
@@ -442,7 +433,7 @@ function renderCableSelectors() {
|
||||
if (!component) {
|
||||
return "";
|
||||
}
|
||||
return `<option value="${escapeHtml(item.placementId)}">${escapeHtml(component.name)} @ ${formatRackPosition(item.y)}</option>`;
|
||||
return `<option value="${escapeHtml(item.placementId)}">${escapeHtml(component.name)} @ ${formatRackPosition(item.y, component)}</option>`;
|
||||
})
|
||||
)
|
||||
.join("");
|
||||
@@ -628,43 +619,6 @@ async function copyBomCsv() {
|
||||
}
|
||||
}
|
||||
|
||||
function findFirstFreePosition(heightU) {
|
||||
const rack = getCurrentRackTemplate();
|
||||
if (!rack) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const unitHeight = getUnitHeightPx();
|
||||
const testHeight = heightU * unitHeight;
|
||||
for (let y = 0; y <= getRackHeightPx(rack) - testHeight; y += unitHeight) {
|
||||
const blocked = state.placedItems.some((item) => {
|
||||
const otherComponent = getComponent(item.componentId);
|
||||
if (!otherComponent) {
|
||||
return false;
|
||||
}
|
||||
return rectanglesOverlap(
|
||||
{
|
||||
left: getDefaultComponentX(component),
|
||||
top: y,
|
||||
width: getComponentWidthPercent(component),
|
||||
height: testHeight,
|
||||
},
|
||||
{
|
||||
left: item.x,
|
||||
top: item.y,
|
||||
width: getComponentWidthPercent(otherComponent),
|
||||
height: getComponentHeightPx(otherComponent),
|
||||
}
|
||||
);
|
||||
});
|
||||
if (!blocked) {
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCurrentRackTemplate() {
|
||||
return state.bootstrap?.rackTemplates.find((template) => template.id === state.rackTemplateId) ?? null;
|
||||
}
|
||||
@@ -701,9 +655,7 @@ function beginPointerDrag(event, item, component) {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
state.pointerDrag = {
|
||||
placementId: item.placementId,
|
||||
offsetX: event.clientX - rect.left,
|
||||
offsetY: event.clientY - rect.top,
|
||||
widthPercent: getComponentWidthPercent(component),
|
||||
heightPx: getComponentHeightPx(component),
|
||||
};
|
||||
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||
@@ -727,12 +679,10 @@ function handlePointerMove(event) {
|
||||
}
|
||||
|
||||
const layerRect = layer.getBoundingClientRect();
|
||||
const widthPx = (state.pointerDrag.widthPercent / 100) * layerRect.width;
|
||||
const xPx = clamp(event.clientX - layerRect.left - state.pointerDrag.offsetX, 0, layerRect.width - widthPx);
|
||||
const yPx = clamp(event.clientY - layerRect.top - state.pointerDrag.offsetY, 0, getRackHeightPx(rack) - state.pointerDrag.heightPx);
|
||||
|
||||
item.x = (xPx / layerRect.width) * 100;
|
||||
item.y = yPx;
|
||||
item.x = 0;
|
||||
item.y = snapToUnit(yPx);
|
||||
renderAll();
|
||||
}
|
||||
|
||||
@@ -857,7 +807,11 @@ function renderSocketStrip(component) {
|
||||
}
|
||||
|
||||
function renderInsertionZones(rack) {
|
||||
const component = getComponent(state.selectedComponentId);
|
||||
if (!state.libraryDragComponentId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const component = getComponent(state.libraryDragComponentId);
|
||||
if (!component) {
|
||||
return "";
|
||||
}
|
||||
@@ -872,27 +826,28 @@ function renderInsertionZones(rack) {
|
||||
}
|
||||
return rectanglesOverlap(
|
||||
{
|
||||
left: getDefaultComponentX(component),
|
||||
left: 0,
|
||||
top: y,
|
||||
width: getComponentWidthPercent(component),
|
||||
width: 100,
|
||||
height: getComponentHeightPx(component),
|
||||
},
|
||||
{
|
||||
left: item.x,
|
||||
left: 0,
|
||||
top: item.y,
|
||||
width: getComponentWidthPercent(other),
|
||||
width: 100,
|
||||
height: getComponentHeightPx(other),
|
||||
}
|
||||
);
|
||||
});
|
||||
const isPreview = state.rackDropPreviewY !== null && Math.abs(state.rackDropPreviewY - y) < 1;
|
||||
|
||||
zones.push(`
|
||||
<button
|
||||
type="button"
|
||||
class="rack-insert-zone${occupied ? " is-disabled" : ""}"
|
||||
class="rack-insert-zone${occupied ? " is-disabled" : ""}${isPreview ? " is-preview" : ""}"
|
||||
data-action="insert-component"
|
||||
data-insert-y="${y}"
|
||||
style="top:${y}px;height:${getComponentHeightPx(component)}px;left:${getDefaultComponentX(component)}%;width:${getComponentWidthPercent(component)}%;"
|
||||
style="top:${y}px;height:${getComponentHeightPx(component)}px;left:0;width:100%;"
|
||||
${occupied ? "disabled" : ""}
|
||||
>
|
||||
<span class="rack-insert-zone__ear rack-insert-zone__ear--left"></span>
|
||||
@@ -921,11 +876,11 @@ function getComponentHeightPx(component) {
|
||||
}
|
||||
|
||||
function getComponentWidthPercent(component) {
|
||||
return component.rackStandard === "10_inch" ? 58 : 86;
|
||||
return 100;
|
||||
}
|
||||
|
||||
function getDefaultComponentX(component) {
|
||||
return (100 - getComponentWidthPercent(component)) / 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getPlacementRect(item, component) {
|
||||
@@ -948,6 +903,17 @@ function formatRackPosition(y) {
|
||||
return `${(y / getUnitHeightPx() + 1).toFixed(1)}U`;
|
||||
}
|
||||
|
||||
function formatRackPosition(y, component) {
|
||||
const rack = getCurrentRackTemplate();
|
||||
if (!rack) {
|
||||
return "1U";
|
||||
}
|
||||
const rowIndex = Math.round(y / getUnitHeightPx());
|
||||
const componentHeightU = component?.heightU ?? 1;
|
||||
const u = rack.totalU - rowIndex - componentHeightU + 1;
|
||||
return `${Math.max(1, u)}U`;
|
||||
}
|
||||
|
||||
function getPlacementOverlaps() {
|
||||
const overlaps = [];
|
||||
for (let index = 0; index < state.placedItems.length; index += 1) {
|
||||
@@ -992,6 +958,86 @@ function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function snapToUnit(value) {
|
||||
const unitHeight = getUnitHeightPx();
|
||||
return Math.round(value / unitHeight) * unitHeight;
|
||||
}
|
||||
|
||||
function beginLibraryDrag(event, componentId) {
|
||||
state.libraryDragComponentId = componentId;
|
||||
state.selectedComponentId = componentId;
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
event.dataTransfer.setData("text/plain", componentId);
|
||||
}
|
||||
window.requestAnimationFrame(() => {
|
||||
renderAll();
|
||||
});
|
||||
}
|
||||
|
||||
function handleRackDragOver(event) {
|
||||
if (!state.libraryDragComponentId) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const rack = getCurrentRackTemplate();
|
||||
const component = getComponent(state.libraryDragComponentId);
|
||||
const layer = document.getElementById("rack-items-layer");
|
||||
if (!rack || !component || !layer) {
|
||||
return;
|
||||
}
|
||||
const rect = layer.getBoundingClientRect();
|
||||
const y = clamp(event.clientY - rect.top - getComponentHeightPx(component) / 2, 0, getRackHeightPx(rack) - getComponentHeightPx(component));
|
||||
const snappedY = snapToUnit(y);
|
||||
if (state.rackDropPreviewY !== snappedY) {
|
||||
state.rackDropPreviewY = snappedY;
|
||||
const insertionLayer = document.getElementById("rack-insertion-layer");
|
||||
if (insertionLayer) {
|
||||
insertionLayer.innerHTML = renderInsertionZones(rack);
|
||||
ui.rackGrid.querySelectorAll("[data-action='insert-component']").forEach((element) => {
|
||||
element.addEventListener("click", () => insertSelectedComponentAt(Number(element.dataset.insertY)));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleRackDrop(event) {
|
||||
if (!state.libraryDragComponentId) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (state.rackDropPreviewY !== null) {
|
||||
insertSelectedComponentAt(state.rackDropPreviewY);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRackDragLeave(event) {
|
||||
if (!state.libraryDragComponentId) {
|
||||
return;
|
||||
}
|
||||
if (event.currentTarget.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
state.rackDropPreviewY = null;
|
||||
const rack = getCurrentRackTemplate();
|
||||
const insertionLayer = document.getElementById("rack-insertion-layer");
|
||||
if (rack && insertionLayer) {
|
||||
insertionLayer.innerHTML = renderInsertionZones(rack);
|
||||
ui.rackGrid.querySelectorAll("[data-action='insert-component']").forEach((element) => {
|
||||
element.addEventListener("click", () => insertSelectedComponentAt(Number(element.dataset.insertY)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleLibraryDragEnd() {
|
||||
if (!state.libraryDragComponentId && state.rackDropPreviewY === null) {
|
||||
return;
|
||||
}
|
||||
state.libraryDragComponentId = null;
|
||||
state.rackDropPreviewY = null;
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function applyRackColorTheme(element, color) {
|
||||
const rgb = hexToRgb(color);
|
||||
if (!rgb) {
|
||||
|
||||
Reference in New Issue
Block a user