From 41141ec3185a3ff48592987f9fe55a67469b70ce Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 16 Sep 2026 13:50:57 +0200 Subject: [PATCH] v0.2.2: support dragging a device between front and rear elevations Front and rear panels already share one coordinate system (same UNIT_HEIGHT, RACK_X, ...), just placed side by side on screen, so the existing delta-based drag math is equally valid for a drop on the other panel - only needed to detect which face the pointer ended up over (elementFromPoint at drop time) and include it as `face` in the submitted move; the reorder endpoint already supported changing it. Also fixed a bug this surfaced: without pointer-events: none on the dragged block during the drag, elementFromPoint() at drop time always hit the dragged block itself (it's what's rendered under the cursor), never the panel actually underneath - cross-face drops would never have been detected. Pointer capture keeps delivering move/up events to the block regardless of its own pointer-events value, so this is safe. Added overflow: visible on the elevation SVGs so a device being dragged toward the other panel doesn't just vanish at its own SVG's edge mid-drag. Updated the German translation for the changed drag-hint string. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 12 +++++++ README.md | 6 ++-- netbox_rack_concept/__init__.py | 2 +- .../locale/de/LC_MESSAGES/django.mo | Bin 21201 -> 21286 bytes .../locale/de/LC_MESSAGES/django.po | 8 +++-- .../static/netbox_rack_concept/elevation.css | 4 +++ .../static/netbox_rack_concept/elevation.js | 32 ++++++++++++++++-- .../netbox_rack_concept/rackconcept.html | 2 +- pyproject.toml | 2 +- 9 files changed, 57 insertions(+), 11 deletions(-) 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 1b59d062fd1cece0ec217c611ac7d8cd43a69a17..7e2ce92b03095868a732318699a30a74488dc2fd 100644 GIT binary patch delta 3696 zcmXZe32=@_9LMnui7QA5qX>B%NfS4T#9dbo6^*zu)ubxPOF|BEG$G|tw5qPCQXRBP zrCKFMlrd5rEo}!q93@Pz(digP)iGvVGyVSZ>`dnU?C$gI{&)Yo&vJH`UsaXg{_YLk zLB^PvP-9wQJ~l-c8{tay$4YFA8!-^~U<@9{B)o|MIIxrVd?-d!PsBA?f$HD1voXyu z44Yt&&c>}TMAOiQhGEzaQ&AJhMRk~mAy|PUa2+FjsV>T6M3unmK82hPA69Eu@bjcI`?sP{8a zE6>AXEW;qYfm+C8jK|n+#zbKM*C=SuciM)%_$>7c$e($_kG96??!7l~!_0sXSD{{} zMMLMKalcc8FUr>AP@6qAoKvG@ zPNP8s&aswQ7o#Tl76#yZsDZbkapp(dgnjtU+zw7#=ppDX5jCV=pYgBe(^%;;E#g5oVz>JO>?IXx-vc zXiP&b>ctbNRGvl+a0#`NzfpniVt@3H;$4iz$@mx#VhU%QMH>H}-h_G}b1`W+8(j>< zYuH5fdlade-X8!JD7lez4-<(3AJSbJhWo&DcLL8<@PKG=g=>3%H0L&&#g!uuLC$d46bKkBdZGYt>mbZpALq+vEr z!dleX2^!#CQDZQL`a;xAa|)A*Z~S>nD^11Ln2FC}DQe&^P+JhpO|7db6!l&Va$lQ7 zjKFNv%2!!8qHc;0aROdJWj1o4m#IPMYNhcMtVeFfX?gWaeB4x^r*Mg_ctKD>jP zU>YY@&P46;TvWgXr~u2c4Q@bf@orScs!`*fi6j5|-s@;kfcvP=r6vEEj>TBi02Qb+ zP>B<<1{J`cjZ_9gP^k?^?sGE`<1r0MlG%oIG3SxvW16!s3ph)|TnZZC5c$;#zDMot zMbyf!qh@>$+hXuAZ$c5Ml@3KsBmq0(ROCFG`KXldK=rG(^^>SfoWnloUZ$YWr^#^7 zR;bi;Km~|Et*k$4i$>Y!<8TV~si*~fhMI6KD!{j>v+x6I0_RcVT}Pd%`$#5T6HQG4 zms=}QsoIL0@E6nod3+LjZz(F!TGYfhqgMVAhT@l~z*kTK|3q!!4NO8a()%CBIBZXR zlT1M`&O*(6zO9#|_HvnZ7mla?9crMEQN}!j>BtvqN>PD7MQvdYrnOe1IA_GQs<5hFCK(lJ>Vz z8LGkh38CWDS+6G$Mh%Z_LtvtJ$U;$j+#QvwKB%uF4iz{am8ml8Qq(wWus42y{&)tp zfU~InzuWpv)WjbqvP@kxfo!M(hM@+ILIsXNbsUZXI3D#p)tYXdZJ!s}dO0faGE_z@ zF&($t_NUmGdNY@Ns?ZuW;|}Ox7>46W)Zv+hhj9yPz{$^h{jyN?xu`=}fxlukY67b{ zv5oM3)Y;gM%1|x#K=&UCI<0M!y#XUp9s8j!mf@%X$*9jL9km7X@d)n3X_zsAla2>b zhqvqnV+LRq_QcCrh5;$wxQj4Q$80r)hCJAeTLR~ypuo?bmeT;g) z(IjunI${&*4k}<2>isxuj-zdRifzxZ=ID8EUMQxZl`TPKVuh`*!b0lrU^i?qx#m%D zN?^CbB4<`!?mVX?$CvM9`bx@tz5-`fQDH%eGqWJuDe}!Ms+kaa*BPJd%kdTTa!T`^ z;#{Azu+(|gS5%yplk3Z*Lt(bB$XQsHO9Qop`tGH4pY%;_*4$!WZb^NQGGA6su`jP+ O!-UZ8HRZ!!Z2LcYx32R5 delta 3611 zcmXZd3rv<(9LMqFg}Wl6far_9aT7#DQ2`@RL@vrjvDCPv&O)Ghzv~>24lOM+8zioB zT^7uyE)8$kY%R^4MV8uJrM0oKnaRA&WixSoe?Dhp@8_KJJm>cRpXbop+Uw_*UPog4 zIFZJf2_9nxVkrjTLiEK~u`jN~FkFX0xCevVrB7pR4uL-qR!)$bZIr|Crg zj2U6fB!OvIKEiPa*h51c4WFQPasf5qkN7P9hNCdd>qcY>Y6p3!0ZUN}-+}$`BTU4@ zsPV3$HgE%n;ynz-e$F$-45KgtX)~Fq2UQr1%TXz)#}I751=xg{_yGIk*l73tY}C$6 za5cV&1MmmbMy_KXM#LBsk2x<$G(K*eGs@@HP-M>8EnE%`Vq#%D1BFJWK&7uBx^^R{?rlZCy zisKZiFvm75KrOKjeQ*P6;CfVS8c++}ZQGks{XalW)QUdXhK1OUI#s<1m<+InpvH}e zcibd;Xi)4ja6B%=x3LM8yjULk;Y8Fvp*)H@_a{+1x{UeQiF7rI$!^N#pf>P|t*=6DU@Pjg zXtcJX&OkeoYR7a?(7@MGp}vh;Ko2VC0b|?_gHbC_KWlc9i;Y-;w{RZjk)PSPA9Z%_ zqOPdeRL%!hqHdalSd3j5f~mY5%KRpm!epF_8h9@%0{2i?QxAq>*aTy^uT28RV;*Yf zORVcqH^mk#!7ovf4VdUgDiXEPceJ3tlc46MXi*o2y( z3l)I}sL=Y7SnhEXj(M1Y1jW=Nub5-V@iE=Fk+T#C2@M;5}ITV!Jlc=3tM6LKL zhT%Qbj{Kf;cj`ecWF!v5G~^tb*{F~=p!)5x^;T3Q+Hee>MtwbhS)Kb76q^4~q4djg zcNU7uQ8eoLDAYgVG}I2}pcY(%nqVdBENnn6pdK||6Y5NTh>GMNsPR&0M8{-OP^b!U zFK$5%5Wy#*7n4yFO+u|a7q#d$A~YYLE(kSIgSE-p zVm*=1v^r$p(V!e(LWSyA)aTKOn)og%MbQPW$*6%dFd3&~UtEvcz-CncownYLTKK1^ zd*%dczKaEHTLWLEK@Ua}<@E_Fk$5uZMg6g5D=TWvEgPM2@Dx#TKg+;dg0Qyls zZasrq@V5>HFNKR3hkv6E&p_gS67x|5K0yr-Jj1Pr<9O=vcpWQH3rOe0`rfchFTQ7bP;{>&17Qt%9F zf^Jmm9@~0gnVa)SRKFx^Hfm#K7_4Kzn8E-W)}k(+?WhI3Z*4_&IEu>IdGyEcQ4?N8 zz2Aw7WVda9WZV5`yUz!qo)1T*C<*&$XQ{R!4VP2T!WcX}yX{fP%%Hw&YO9yFE%Ds< lhOJ%dom;(X^~<$&)r+c^c{kNn*H$lUT;hps>&Pk(`yYy&f$abQ 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 %}