aboutsummaryrefslogtreecommitdiff
path: root/internal/admin
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-06 19:09:42 -0400
committerhistoria <[not public]>2026-07-06 19:09:42 -0400
commitcd52f9dbb8c35f37a9b52e8092bf5d722d56b044 (patch)
treef25983ee8d305169ebf210bb800268e7ddbb920d /internal/admin
parent00554b66df9310073c3b5e1aebd125ea54d9d554 (diff)
downloadthehouseoficarus-cd52f9dbb8c35f37a9b52e8092bf5d722d56b044.tar.gz
feat: shift+drag works when shift is pressed/released mid drag
Diffstat (limited to 'internal/admin')
-rw-r--r--internal/admin/api_rooms.go11
-rw-r--r--internal/admin/static/admin.css2
-rw-r--r--internal/admin/static/map.js80
3 files changed, 76 insertions, 17 deletions
diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go
index b5353c2..74143a8 100644
--- a/internal/admin/api_rooms.go
+++ b/internal/admin/api_rooms.go
@@ -47,6 +47,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
var linkFrom *int
var linkDir *string
+ var linkOneWay bool
if v, ok := m["link_from"]; ok {
if f, ok := v.(float64); ok {
lf := int(f)
@@ -61,6 +62,12 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
}
delete(m, "link_dir")
}
+ if v, ok := m["link_oneway"]; ok {
+ if b, ok := v.(bool); ok {
+ linkOneWay = b
+ }
+ delete(m, "link_oneway")
+ }
fromID := 0
if linkFrom != nil {
@@ -109,7 +116,9 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
})
}
- exits[string(opp)] = *linkFrom
+ if !linkOneWay {
+ exits[string(opp)] = *linkFrom
+ }
}
}
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css
index c620b46..82c4815 100644
--- a/internal/admin/static/admin.css
+++ b/internal/admin/static/admin.css
@@ -53,6 +53,8 @@ body{font-family:monospace;background:var(--bg);color:var(--text);display:flex;f
.room-tooltip{position:absolute;background:var(--panel);border:1px solid var(--border);padding:6px 10px;border-radius:3px;font-size:11px;pointer-events:none;z-index:20;white-space:nowrap}
.link-mode-badge{position:absolute;bottom:12px;right:12px;padding:6px 12px;border-radius:4px;font-size:12px;font-family:monospace;pointer-events:none;z-index:20;background:rgba(30,30,60,0.9);color:#ccc;border:1px solid var(--border)}
.link-mode-badge.oneway{background:rgba(80,50,20,0.92);color:#fca;border:1px solid #a64}
+#mapSvg:focus{outline:none}
+#mapSvg,#mapSvg *{user-select:none;-webkit-user-select:none;-moz-user-select:none}
.notification{position:fixed;bottom:16px;right:16px;background:var(--panel);border:1px solid var(--border);padding:10px 16px;border-radius:4px;font-size:12px;z-index:100;animation:fadeIn .2s}
.notification.error{border-color:var(--danger)}
.notification.success{border-color:var(--success)}
diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js
index f89a434..47dcb27 100644
--- a/internal/admin/static/map.js
+++ b/internal/admin/static/map.js
@@ -6,6 +6,7 @@ var dragRoom = false;
var linkDrag = false, linkFromId = null, linkTargetId = null, linkGhostDir = null;
var linkDragOneWay = false;
var shiftHeld = false;
+var delOneWay = false;
var delDrag = false, delFromId = null, delTargetId = null;
var roomMap = {}, occupied = {};
var dirChangedByUser = false;
@@ -21,12 +22,21 @@ var upTarget = {}, downTarget = {};
var _mapMouseCleanup = false;
+function isShiftKey(e) {
+ return e && (e.key === 'Shift' || e.code === 'ShiftLeft' || e.code === 'ShiftRight' || e.keyCode === 16);
+}
+
function onMapMouseMove(e) {
if (!dragging) return;
var moved = Math.abs(e.clientX - startX) >= 4 || Math.abs(e.clientY - startY) >= 4;
if (delFromId && moved && !delDrag) { delDrag = true; }
- if (delDrag && moved) { updateDelDragTarget(e); return; }
+ if (delDrag && moved) {
+ delOneWay = e.shiftKey;
+ updateDelDragTarget(e);
+ showDelDragBadge();
+ return;
+ }
if (!dragRoom && !delDrag && !delFromId) {
panX += e.clientX - prevX;
@@ -37,7 +47,8 @@ function onMapMouseMove(e) {
if (linkFromId && moved) {
linkDrag = true;
- showDragBadge(linkDragOneWay ? 'One-way link' : 'Two-way link');
+ linkDragOneWay = e.shiftKey;
+ showLinkDragBadge();
updateLinkDragTarget(e);
}
}
@@ -54,24 +65,28 @@ function onMapMouseUp(e) {
showRoomContextMenu(startX, startY, delFromId);
clearDelHighlight();
dragging = false; dragRoom = false; delFromId = null; delTargetId = null; delDrag = false;
+ if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
return;
}
if (delDrag && delFromId && delTargetId) {
- deleteLink(delFromId, delTargetId);
+ deleteLink(delFromId, delTargetId, (delOneWay || e.shiftKey) ? 'from' : '');
clearDelHighlight();
- dragging = false; dragRoom = false; delDrag = false; delFromId = null; delTargetId = null;
+ hideDragBadge();
+ dragging = false; dragRoom = false; delDrag = false; delFromId = null; delTargetId = null; delOneWay = false;
+ if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
return;
}
if (linkDrag && linkFromId) {
- if (linkTargetId) { createLink(linkFromId, linkTargetId, linkDragOneWay); }
- else if (linkGhostDir) { createRoom(linkFromId, linkGhostDir); }
+ var oneway = linkDragOneWay || e.shiftKey;
+ if (linkTargetId) { createLink(linkFromId, linkTargetId, oneway); }
+ else if (linkGhostDir) { createRoom(linkFromId, linkGhostDir, oneway); }
} else if (!moved && !delDrag) {
if (e.target.classList.contains('rm')) {
selectRoom(parseInt(e.target.getAttribute('data-id')));
} else if (e.target.classList.contains('ghost')) {
- createRoom(selectedRoom, e.target.getAttribute('data-dir'));
+ createRoom(selectedRoom, e.target.getAttribute('data-dir'), e.shiftKey);
}
}
@@ -80,7 +95,8 @@ function onMapMouseUp(e) {
hideDragBadge();
dragging = false; dragRoom = false;
linkDrag = false; linkFromId = null; linkTargetId = null; linkGhostDir = null; linkDragOneWay = false;
- delDrag = false; delFromId = null; delTargetId = null;
+ delDrag = false; delFromId = null; delTargetId = null; delOneWay = false;
+ if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
}
function cancelMapDrag() {
@@ -93,14 +109,15 @@ function cancelMapDrag() {
hideDragBadge();
dragging = false; dragRoom = false;
linkDrag = false; linkFromId = null; linkTargetId = null; linkGhostDir = null; linkDragOneWay = false;
- delDrag = false; delFromId = null; delTargetId = null;
+ delDrag = false; delFromId = null; delTargetId = null; delOneWay = false;
+ if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
}
-function showDragBadge(text) {
+function showDragBadge(text, oneway) {
var badge = document.getElementById('linkModeBadge');
if (!badge) return;
badge.textContent = text;
- badge.className = 'link-mode-badge' + (linkDragOneWay ? ' oneway' : '');
+ badge.className = 'link-mode-badge' + (oneway ? ' oneway' : '');
badge.style.display = '';
}
@@ -109,6 +126,24 @@ function hideDragBadge() {
if (badge) badge.style.display = 'none';
}
+function showLinkDragBadge() {
+ var label = linkDragOneWay ? 'One way link' : 'Two-way link';
+ var toLabel = linkTargetId ? linkTargetId : (linkGhostDir ? 'new' : null);
+ if (linkFromId && toLabel) label += ' (#' + linkFromId + ' to #' + toLabel + ')';
+ showDragBadge(label, linkDragOneWay);
+}
+
+function showDelDragBadge() {
+ var label = delOneWay ? 'One way remove' : 'Two-way remove';
+ if (delFromId && delTargetId) label += ' (#' + delFromId + ' to #' + delTargetId + ')';
+ showDragBadge(label, delOneWay);
+}
+
+function refreshDragMode(shiftDown) {
+ if (linkDrag && linkFromId) { linkDragOneWay = shiftDown; showLinkDragBadge(); }
+ if (delDrag && delFromId) { delOneWay = shiftDown; showDelDragBadge(); }
+}
+
function changeZ(dz) {
if (dz === 0) currentZ = 0; else currentZ += dz;
panX = 0; panY = 0; scale = 1;
@@ -279,6 +314,17 @@ function renderMap(data) {
svg.oncontextmenu = function(e) { e.preventDefault(); };
+ if (!svg._mapKeysWired) {
+ svg.setAttribute('tabindex', '0');
+ svg.addEventListener('keydown', function(e) {
+ if (isShiftKey(e)) { shiftHeld = true; if (dragging) refreshDragMode(true); }
+ });
+ svg.addEventListener('keyup', function(e) {
+ if (isShiftKey(e)) { shiftHeld = false; if (dragging) refreshDragMode(false); }
+ });
+ svg._mapKeysWired = true;
+ }
+
svg.onmousedown = function(e) {
var onRoom = e.target.classList.contains('rm');
var onGhost = e.target.classList.contains('ghost');
@@ -296,8 +342,9 @@ function renderMap(data) {
if (e.button === 2 && onRoom) {
delFromId = parseInt(e.target.getAttribute('data-id'));
- delDrag = false; delTargetId = null;
+ delDrag = false; delTargetId = null; delOneWay = false;
dragging = true;
+ if (svg.focus) svg.focus();
startX = e.clientX; startY = e.clientY;
prevX = e.clientX; prevY = e.clientY;
dragRoom = true; linkFromId = null; linkDrag = false; linkTargetId = null; linkGhostDir = null;
@@ -317,9 +364,9 @@ function renderMap(data) {
return;
}
- e.preventDefault();
dragRoom = onRoom || onGhost;
dragging = true;
+ if (svg.focus) svg.focus();
startX = e.clientX; startY = e.clientY;
prevX = e.clientX; prevY = e.clientY;
linkFromId = onRoom ? parseInt(e.target.getAttribute('data-id')) : null;
@@ -1863,11 +1910,12 @@ function deleteRoom(id) {
}).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); });
}
-function createRoom(fromID, dir) {
+function createRoom(fromID, dir, oneway) {
API.get('/api/next-room-id?from=' + fromID).then(function(r) {
var newID = r.id;
var name = 'Room #' + newID;
var body = { name: name, link_from: fromID, link_dir: dir };
+ if (oneway) body.link_oneway = true;
var srcRoom = roomMap[fromID];
if (srcRoom && srcRoom.color) body.color = srcRoom.color;
API.post('/api/rooms', body).then(function(resp) {
@@ -2395,6 +2443,6 @@ function reloadRoomDirs(cb) {
}
document.addEventListener('DOMContentLoaded', function() { loadRoomDirs(function() { loadMap(); }); initPanelResize();
- document.addEventListener('keydown', function(e) { if (e.key === 'Shift') shiftHeld = true; });
- document.addEventListener('keyup', function(e) { if (e.key === 'Shift') shiftHeld = false; });
+ window.addEventListener('keydown', function(e) { if (isShiftKey(e)) { shiftHeld = true; if (dragging) refreshDragMode(true); } });
+ window.addEventListener('keyup', function(e) { if (isShiftKey(e)) { shiftHeld = false; if (dragging) refreshDragMode(false); } });
});