Files
netwatch/app/assets/js/floor-infrastructure-edit.js
fixclean 3bc5a2ca04 Verhalten im Infrastruktur-Modul ist jetzt wie gewünscht:
Nur das aktiv bearbeitete Objekt ist verschiebbar.
Alle anderen Objekte auf derselben Etage werden als halbtransparente Referenz angezeigt.
In der Listenansicht sind Marker nur noch Vorschau (nicht verschiebbar).
Geänderte Dateien:

edit.php

Referenzdaten für Patchpanels und Wandbuchsen geladen.
Diese Daten als data-* am Canvas hinterlegt.
Hinweise im UI angepasst.
Styles ergänzt:
.floor-plan-marker.is-active
.floor-plan-reference (+ Varianten für Panel/Outlet)
Inline-Script entfernt und auf externe JS-Datei umgestellt (CSP-sicher):
<script src="/assets/js/floor-infrastructure-edit.js" defer></script>
floor-infrastructure-edit.js (neu)

Drag/Drop nur für den aktiven Marker.
Referenzmarker je Stockwerk rendern (halbtransparent, nicht interaktiv).
Aktiven Marker aus Referenzmenge ausschließen.
Referenzen bei Stockwerk-/Raumwechsel neu rendern.
list.php

Kartenansicht auf reine Vorschau umgestellt.
Drag/Save-Logik entfernt.
Marker per CSS nicht interaktiv (pointer-events: none, cursor: default).
Hinweistext entsprechend angepasst.
2026-02-16 10:04:12 +01:00

295 lines
10 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('floor-plan-canvas');
const marker = document.getElementById('floor-plan-marker');
const positionLabel = document.getElementById('floor-plan-position');
if (!canvas || !marker) {
return;
}
const xFieldName = canvas.dataset.xField;
const yFieldName = canvas.dataset.yField;
const xField = xFieldName ? document.querySelector(`input[name="${xFieldName}"]`) : null;
const yField = yFieldName ? document.querySelector(`input[name="${yFieldName}"]`) : null;
if (!xField || !yField) {
return;
}
const markerWidth = Math.max(1, Number(canvas.dataset.markerWidth) || marker.offsetWidth);
const markerHeight = Math.max(1, Number(canvas.dataset.markerHeight) || marker.offsetHeight);
const markerType = canvas.dataset.markerType || '';
const activeId = Number(canvas.dataset.activeId || 0);
const panelReferences = JSON.parse(canvas.dataset.referencePanels || '[]');
const outletReferences = JSON.parse(canvas.dataset.referenceOutlets || '[]');
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
marker.classList.add('is-active');
const updatePositionLabel = (x, y) => {
if (positionLabel) {
positionLabel.textContent = `${Math.round(x)} x ${Math.round(y)}`;
}
};
const setMarkerPosition = (rawX, rawY) => {
const rect = canvas.getBoundingClientRect();
const maxX = Math.max(0, rect.width - markerWidth);
const maxY = Math.max(0, rect.height - markerHeight);
const left = clamp(rawX, 0, maxX);
const top = clamp(rawY, 0, maxY);
marker.style.left = `${left}px`;
marker.style.top = `${top}px`;
xField.value = Math.round(left);
yField.value = Math.round(top);
updatePositionLabel(left, top);
};
const updateFromInputs = () => {
setMarkerPosition(Number(xField.value) || 0, Number(yField.value) || 0);
};
updateFromInputs();
let dragging = false;
let offsetX = 0;
let offsetY = 0;
const startDrag = (clientX, clientY) => {
const markerRect = marker.getBoundingClientRect();
offsetX = clientX - markerRect.left;
offsetY = clientY - markerRect.top;
};
marker.addEventListener('pointerdown', (event) => {
event.preventDefault();
dragging = true;
startDrag(event.clientX, event.clientY);
marker.setPointerCapture(event.pointerId);
});
marker.addEventListener('pointermove', (event) => {
if (!dragging) {
return;
}
const rect = canvas.getBoundingClientRect();
setMarkerPosition(event.clientX - rect.left - offsetX, event.clientY - rect.top - offsetY);
});
const stopDrag = (event) => {
if (!dragging) {
return;
}
dragging = false;
if (marker.hasPointerCapture(event.pointerId)) {
marker.releasePointerCapture(event.pointerId);
}
};
['pointerup', 'pointercancel', 'pointerleave'].forEach((evt) => {
marker.addEventListener(evt, stopDrag);
});
canvas.addEventListener('pointerdown', (event) => {
if (event.target !== canvas) {
return;
}
const rect = canvas.getBoundingClientRect();
setMarkerPosition(event.clientX - rect.left - markerWidth / 2, event.clientY - rect.top - markerHeight / 2);
});
[xField, yField].forEach((input) => {
input.addEventListener('input', () => {
updateFromInputs();
});
});
window.addEventListener('resize', () => {
updateFromInputs();
});
const panelLocationSelect = document.getElementById('panel-location-select');
const panelBuildingSelect = document.getElementById('panel-building-select');
const panelFloorSelect = document.getElementById('panel-floor-select');
const outletRoomSelect = document.getElementById('outlet-room-select');
const floorPlanSvg = document.getElementById('floor-plan-svg');
const panelPlacementFields = document.getElementById('panel-placement-fields');
const panelFloorPlanGroup = document.getElementById('panel-floor-plan-group');
const panelFloorMissingHint = document.getElementById('panel-floor-missing-hint');
const buildingOptions = panelBuildingSelect ? Array.from(panelBuildingSelect.options).filter((option) => option.value !== '') : [];
const floorOptions = panelFloorSelect ? Array.from(panelFloorSelect.options).filter((option) => option.value !== '') : [];
const getCurrentFloorId = () => {
const floorOption = panelFloorSelect?.selectedOptions?.[0];
if (floorOption?.value) {
return Number(floorOption.value);
}
const roomOption = outletRoomSelect?.selectedOptions?.[0];
return Number(roomOption?.dataset?.floorId || 0);
};
const renderReferenceMarkers = () => {
canvas.querySelectorAll('.floor-plan-reference').forEach((node) => node.remove());
const currentFloorId = getCurrentFloorId();
if (!currentFloorId) {
return;
}
const appendReference = (entry, cssClass, width, height) => {
const markerRef = document.createElement('div');
markerRef.className = `floor-plan-reference ${cssClass}`;
markerRef.style.left = `${Number(entry.x || entry.pos_x || 0)}px`;
markerRef.style.top = `${Number(entry.y || entry.pos_y || 0)}px`;
if (width > 0) {
markerRef.style.width = `${width}px`;
}
if (height > 0) {
markerRef.style.height = `${height}px`;
}
markerRef.title = entry.name || '';
canvas.appendChild(markerRef);
};
panelReferences.forEach((entry) => {
if (Number(entry.floor_id) !== currentFloorId) {
return;
}
if (markerType === 'patchpanel' && Number(entry.id) === activeId) {
return;
}
appendReference(entry, 'panel-marker', Math.max(1, Number(entry.width) || 140), Math.max(1, Number(entry.height) || 40));
});
outletReferences.forEach((entry) => {
if (Number(entry.floor_id) !== currentFloorId) {
return;
}
if (markerType === 'outlet' && Number(entry.id) === activeId) {
return;
}
appendReference(entry, 'outlet-marker', 32, 32);
});
};
const updateFloorPlanImage = () => {
if (!floorPlanSvg) {
return;
}
const floorOption = panelFloorSelect?.selectedOptions?.[0];
const roomOption = outletRoomSelect?.selectedOptions?.[0];
const svgUrl = floorOption?.dataset?.svgUrl || roomOption?.dataset?.floorSvgUrl || '';
if (svgUrl) {
floorPlanSvg.src = svgUrl;
floorPlanSvg.hidden = false;
} else {
floorPlanSvg.removeAttribute('src');
floorPlanSvg.hidden = true;
}
renderReferenceMarkers();
};
if (floorPlanSvg) {
floorPlanSvg.addEventListener('error', () => {
floorPlanSvg.removeAttribute('src');
floorPlanSvg.hidden = true;
});
}
const updatePanelPlacementVisibility = () => {
if (!panelFloorSelect || !panelPlacementFields || !panelFloorPlanGroup) {
return;
}
const hasFloor = !!panelFloorSelect.value;
panelPlacementFields.hidden = !hasFloor;
panelFloorPlanGroup.hidden = !hasFloor;
if (panelFloorMissingHint) {
panelFloorMissingHint.hidden = hasFloor;
}
};
const filterFloorOptions = () => {
if (!panelFloorSelect) {
return;
}
const buildingValue = panelBuildingSelect?.value || '';
let firstMatch = '';
floorOptions.forEach((option) => {
const matches = !buildingValue || option.dataset.buildingId === buildingValue;
option.hidden = !matches;
option.disabled = !matches;
if (matches && !firstMatch) {
firstMatch = option.value;
}
});
const selectedOption = panelFloorSelect.querySelector(`option[value="${panelFloorSelect.value}"]`);
const isSelectedHidden = selectedOption ? selectedOption.hidden : true;
if (buildingValue && (!panelFloorSelect.value || isSelectedHidden) && firstMatch) {
panelFloorSelect.value = firstMatch;
}
updatePanelPlacementVisibility();
updateFloorPlanImage();
};
const filterBuildingOptions = () => {
if (!panelBuildingSelect) {
return;
}
const locationValue = panelLocationSelect?.value || '';
let firstMatch = '';
buildingOptions.forEach((option) => {
const matches = !locationValue || option.dataset.locationId === locationValue;
option.hidden = !matches;
option.disabled = !matches;
if (matches && !firstMatch) {
firstMatch = option.value;
}
});
const selectedOption = panelBuildingSelect.querySelector(`option[value="${panelBuildingSelect.value}"]`);
const isSelectedHidden = selectedOption ? selectedOption.hidden : true;
if (locationValue && (!panelBuildingSelect.value || isSelectedHidden) && firstMatch) {
panelBuildingSelect.value = firstMatch;
}
};
if (panelLocationSelect) {
panelLocationSelect.addEventListener('change', () => {
filterBuildingOptions();
filterFloorOptions();
});
}
if (panelBuildingSelect) {
panelBuildingSelect.addEventListener('change', () => {
filterFloorOptions();
});
}
if (panelFloorSelect) {
panelFloorSelect.addEventListener('change', () => {
updatePanelPlacementVisibility();
updateFloorPlanImage();
});
}
if (outletRoomSelect) {
outletRoomSelect.addEventListener('change', () => {
updateFloorPlanImage();
});
}
if (panelLocationSelect) {
filterBuildingOptions();
filterFloorOptions();
} else {
updateFloorPlanImage();
}
updatePanelPlacementVisibility();
});