diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e6eefc..3ebdb48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.2.2 + +- Drag-and-drop now supports dragging a device from the front elevation onto the rear + one (and back), reassigning its face. Front and rear share one coordinate system + (same unit height, rack offset, ...), just placed side by side on screen, so the + existing delta-based drag math is already valid for a cross-panel drop - only the + target face needed detecting (via `elementFromPoint` at drop time) and sending along. + Fixed a related bug along the way: without `pointer-events: none` on the dragged + block during the drag, `elementFromPoint` would always hit the dragged block itself + rather than the panel underneath, so the cross-face drop would never have been + detected; pointer capture keeps delivering move/up events to it regardless. + ## 0.2.1 - Rework PNG export: instead of rasterizing the SVG export via ``/`` diff --git a/README.md b/README.md index 44f321e..5b5131d 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,10 @@ Wenn die Planung steht, wird das Konzept per Knopfdruck in echte NetBox-Objekte Dazu: -* **Elevation mit Drag & Drop** – Geräte per Maus verschieben, serverseitig validiert. - Ein ungültiger Zug wird abgelehnt und ändert nichts. +* **Elevation mit Drag & Drop** – Geräte per Maus verschieben, auch von der + Front- in die Rear-Ansicht ziehen (und umgekehrt), um sie auf die andere Seite + umzuhängen. Alles serverseitig validiert; ein ungültiger Zug wird abgelehnt und + ändert nichts. * **Geräte mit Bild und Text** – ist am Gerätetyp ein Front-/Rear-Bild hinterlegt, wird es im Elevation-Block angezeigt; darüber liegt der Gerätename als weiße, schwarz konturierte Schrift (kein dunkler Balken). Ein Dropdown über der Elevation schaltet diff --git a/netbox_rack_concept/__init__.py b/netbox_rack_concept/__init__.py index 8b76b6c..f2f5a9c 100644 --- a/netbox_rack_concept/__init__.py +++ b/netbox_rack_concept/__init__.py @@ -1,6 +1,6 @@ from netbox.plugins import PluginConfig -__version__ = '0.2.1' +__version__ = '0.2.2' class RackConceptConfig(PluginConfig): diff --git a/netbox_rack_concept/locale/de/LC_MESSAGES/django.mo b/netbox_rack_concept/locale/de/LC_MESSAGES/django.mo index 1b59d06..7e2ce92 100644 Binary files a/netbox_rack_concept/locale/de/LC_MESSAGES/django.mo and b/netbox_rack_concept/locale/de/LC_MESSAGES/django.mo differ diff --git a/netbox_rack_concept/locale/de/LC_MESSAGES/django.po b/netbox_rack_concept/locale/de/LC_MESSAGES/django.po index 68aa609..2a52571 100644 --- a/netbox_rack_concept/locale/de/LC_MESSAGES/django.po +++ b/netbox_rack_concept/locale/de/LC_MESSAGES/django.po @@ -340,9 +340,6 @@ msgstr "Geräte" msgid "Dimensions" msgstr "Abmessungen" -msgid "Drag devices to move them" -msgstr "Geräte zum Verschieben ziehen" - msgid "Draw.io (.drawio)" msgstr "Draw.io (.drawio)" @@ -926,3 +923,8 @@ msgid "{{ counter }} planned device will be created per rack." msgid_plural "{{ counter }} planned devices will be created per rack." msgstr[0] "Pro Rack wird {{ counter }} geplantes Gerät angelegt." msgstr[1] "Pro Rack werden {{ counter }} geplante Geräte angelegt." + +msgid "Drag devices to move or flip them between front and rear" +msgstr "" +"Geräte ziehen, um sie zu verschieben oder zwischen Front und Rückseite zu " +"wechseln" diff --git a/netbox_rack_concept/static/netbox_rack_concept/elevation.css b/netbox_rack_concept/static/netbox_rack_concept/elevation.css index cf09968..0461105 100644 --- a/netbox_rack_concept/static/netbox_rack_concept/elevation.css +++ b/netbox_rack_concept/static/netbox_rack_concept/elevation.css @@ -10,6 +10,10 @@ height: auto; user-select: none; touch-action: none; + /* A device being dragged toward the other face's panel shouldn't just vanish the + moment it crosses this SVG's own edge - let it render past the boundary while + it's mid-drag, into whatever sits next to it (the other panel, side by side). */ + overflow: visible; } .concept-elevation .unit-slot { diff --git a/netbox_rack_concept/static/netbox_rack_concept/elevation.js b/netbox_rack_concept/static/netbox_rack_concept/elevation.js index 58671db..1e991db 100644 --- a/netbox_rack_concept/static/netbox_rack_concept/elevation.js +++ b/netbox_rack_concept/static/netbox_rack_concept/elevation.js @@ -113,10 +113,18 @@ return Math.min(Math.max(value, min), max); } + /* Which .concept-elevation panel (front or rear) the pointer is currently over - + used at drop time to tell whether a device was dragged onto the other face. */ + function panelAt(clientX, clientY) { + const el = document.elementFromPoint(clientX, clientY); + return el ? el.closest('.concept-elevation') : null; + } + container.querySelectorAll('.concept-elevation').forEach(function (panel) { const svg = panel.querySelector('svg'); const geo = geometry(svg); if (!geo) return; + const sourceFace = panel.dataset.face; panel.querySelectorAll('.device-block:not(.opposite)').forEach(function (block) { block.addEventListener('pointerdown', onPointerDown); @@ -139,6 +147,11 @@ let dy = 0; block.classList.add('dragging'); + // Without this, elementFromPoint() at drop time would always hit the + // dragged block itself (it's what's rendered under the cursor), never + // whatever panel is actually underneath it - pointer capture (below) + // keeps move/up events coming to `block` regardless, so this is safe. + block.style.pointerEvents = 'none'; block.setPointerCapture(event.pointerId); function onMove(moveEvent) { @@ -152,11 +165,12 @@ block.setAttribute('transform', 'translate(' + dx + ' ' + snappedY + ')'); } - function onUp() { + function onUp(upEvent) { block.removeEventListener('pointermove', onMove); block.removeEventListener('pointerup', onUp); block.removeEventListener('pointercancel', onUp); block.classList.remove('dragging'); + block.style.pointerEvents = ''; if (!moved) { // Treat it as a click: follow the device link. @@ -166,6 +180,14 @@ return; } + // Front and rear share one coordinate system (same UNIT_HEIGHT, + // RACK_X, ...), just placed side by side on screen - the delta + // computed against the source panel is equally valid whichever + // panel the pointer ends up over, so dropping onto the other face + // only needs to swap which face gets submitted. + const targetPanel = panelAt(upEvent.clientX, upEvent.clientY); + const targetFace = (targetPanel && targetPanel.dataset.face) || sourceFace; + const unitDelta = Math.round(dy / geo.unitHeight) * (descUnits ? 1 : -1); const lowest = startingUnit; const highest = startingUnit + rackHeight - deviceUnits; @@ -180,7 +202,8 @@ ); } - submit(block, newPosition, fraction > 1 ? horizontal : null); + submit(block, newPosition, fraction > 1 ? horizontal : null, + targetFace !== sourceFace ? targetFace : null); } block.addEventListener('pointermove', onMove); @@ -190,7 +213,7 @@ }); }); - function submit(block, position, horizontalPosition) { + function submit(block, position, horizontalPosition, face) { const move = { id: parseInt(block.dataset.deviceId, 10), position: position @@ -198,6 +221,9 @@ if (horizontalPosition !== null) { move.horizontal_position = horizontalPosition; } + if (face) { + move.face = face; + } setStatus('Saving…', false); diff --git a/netbox_rack_concept/templates/netbox_rack_concept/rackconcept.html b/netbox_rack_concept/templates/netbox_rack_concept/rackconcept.html index bd8f85a..028aaf8 100644 --- a/netbox_rack_concept/templates/netbox_rack_concept/rackconcept.html +++ b/netbox_rack_concept/templates/netbox_rack_concept/rackconcept.html @@ -138,7 +138,7 @@ {% if perms.netbox_rack_concept.change_conceptdevice %} - {% trans "Drag devices to move them" %} + {% trans "Drag devices to move or flip them between front and rear" %} {% endif %}