dsfsdf
All checks were successful
Deploy / deploy (push) Successful in 44s

This commit is contained in:
2026-05-17 00:47:09 +02:00
parent 8647bff20f
commit f5bb37f96e
2 changed files with 174 additions and 101 deletions

View File

@@ -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) {