diff --git a/inventory/static/css/components/draw.css b/inventory/static/css/components/draw.css index d221157..df65191 100644 --- a/inventory/static/css/components/draw.css +++ b/inventory/static/css/components/draw.css @@ -1,258 +1,224 @@ -:root { - --tb-h: 34px; -} +:root { --tb-h: 34px; } -.grid-widget { - container-type: inline-size; - min-width: 375px; - height: 100%; - display: flex; - flex-direction: column; -} +/* ========================================================= + GRID WIDGET (editor uses container queries, viewer does not) + ========================================================= */ -.grid-widget .grid-wrap { - flex: 1 1 auto; - width: 100%; +/* ------------------------- + Shared basics (both modes) + ------------------------- */ + +.grid-widget { /* no container-type here */ } + +/* drawing stack */ +.grid-widget [data-grid] { position: relative; - overflow: hidden; - min-height: 375px; - z-index: 0; + margin-inline: auto; } -.grid-widget [data-toolbar].toolbar { - display: grid !important; - grid-template-rows: auto auto; - align-content: start; - gap: .5rem; - overflow: visible; +/* Overlay elements */ +.grid-widget [data-canvas], +.grid-widget [data-dot], +.grid-widget [data-coords] { position: absolute; } + +.grid-widget [data-canvas]{ + inset: 0; + width: 100%; + height: 100%; + display: block; + z-index: 1; + pointer-events: none; } -.grid-widget [data-toolbar] .toolbar-row { - display: flex; - align-items: center; - gap: .5rem; - min-width: 0; - flex-wrap: nowrap; +.grid-widget [data-dot]{ + transform: translate(-50%, -50%); + z-index: 2; + pointer-events: none; +} + +.grid-widget [data-coords]{ + bottom: 10px; + left: 10px; + pointer-events: none; +} + +/* ------------------------- + Toolbar styling + ------------------------- */ + +.grid-widget [data-toolbar].toolbar{ + display: grid !important; + grid-template-rows: auto auto; + align-content: start; + gap: 0.5rem; + overflow: visible; +} + +.grid-widget [data-toolbar] .toolbar-row{ + display: flex; + align-items: center; + gap: 0.5rem; + min-width: 0; + flex-wrap: nowrap; } .grid-widget [data-toolbar] .toolbar-row--primary, -.grid-widget [data-toolbar] .toolbar-row--secondary { +.grid-widget [data-toolbar] .toolbar-row--secondary{ + overflow-x: auto; + overflow-y: hidden; +} + +.grid-widget [data-toolbar] .toolbar-row--secondary{ opacity: 0.95; } + +/* container query only matters in editor (set below) */ +@container (min-width: 750px){ + .grid-widget [data-toolbar].toolbar{ + display: flex !important; + flex-wrap: nowrap; + align-items: center; + gap: 0.5rem; overflow-x: auto; overflow-y: hidden; + } + + .grid-widget [data-toolbar] .toolbar-row{ display: contents; } + + .grid-widget [data-toolbar] .toolbar-row--primary, + .grid-widget [data-toolbar] .toolbar-row--secondary{ overflow: visible; } } -.grid-widget [data-toolbar] .toolbar-row--secondary { - opacity: .95; -} +.grid-widget [data-toolbar]::-webkit-scrollbar{ height: 8px; } -.grid-widget[data-mode="viewer"] { - display: inline-block; - height: auto; - min-width: 0; -} - -.grid-widget[data-mode="viewer"] .grid-wrap { - flex: 0 0 auto; - min-height: 0; - width: fit-content; - overflow: visible; -} - -.grid-widget[data-mode="viewer"] [data-grid]{ - position: relative; - width: auto; - height: auto; - inset: auto; - cursor: default; -} - -/* WIDE: 1 row */ -@container (min-width: 750px) { - .grid-widget [data-toolbar].toolbar { - display: flex !important; - flex-wrap: nowrap; - align-items: center; - gap: .5rem; - overflow-x: auto; - overflow-y: hidden; - } - - .grid-widget [data-toolbar] .toolbar-row { - display: contents; - } - - .grid-widget [data-toolbar] .toolbar-row--primary, - .grid-widget [data-toolbar] .toolbar-row--secondary { - overflow: visible; - } -} - -.grid-widget [data-grid] { - position: absolute; - cursor: crosshair; - width: 100%; - height: 100%; - touch-action: none; - margin: 0 auto; - max-width: 100%; - z-index: 0; - inset: 0; -} - -.grid-widget [data-canvas], -.grid-widget [data-coords], -.grid-widget [data-dot] { - position: absolute; -} - -.grid-widget [data-toolbar]::-webkit-scrollbar { - height: 8px; - z-index: 10; -} - -.grid-widget [data-grid-wrap] { - min-width: 0; -} - -.grid-widget [data-coords] { - bottom: 10px; - pointer-events: none; - left: 10px; -} - -.grid-widget [data-canvas] { - z-index: 1; - pointer-events: none; - inset: 0; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: block; -} - -.grid-widget [data-dot] { - transform: translate(-50%, -50%); - z-index: 2; - pointer-events: none; -} - -.grid-widget .toolbar-range { - width: 120px; - margin: 0; - flex: 0 0 120px; -} - -.grid-widget .dropdown-menu { - min-width: 200px; - padding: .5rem .75rem; - border-radius: .75rem; - box-shadow: 0 10px 30px rgba(0, 0, 0, .12); - position: absolute; - z-index: 1000 !important; - pointer-events: auto; -} - -.grid-widget .dropdown-menu>*:first-child { - margin-top: 0; -} - -.grid-widget .dropdown-menu>*:last-child { - margin-bottom: 0; -} - -.grid-widget .dropdown-menu .form-range { - width: 100%; - margin: 0; -} - -.grid-widget [data-toolbar] .badge, -.grid-widget [data-toolbar] .input-group-text { - white-space: nowrap; -} - -.grid-widget [data-toolbar]>* { - align-self: center; -} - -.grid-widget [data-toolbar] .vr { - align-self: stretch; -} - -.grid-widget [data-toolbar] .btn-group, -.grid-widget [data-toolbar] .dropdown, -.grid-widget [data-toolbar] .vr { - flex: 0 0 auto; -} - -.grid-widget .toolbar-group { - display: flex; - align-items: center; - gap: .25rem; - padding: .25rem; - border: 1px solid rgba(0, 0, 0, .08); - border-radius: .5rem; - background: rgba(0, 0, 0, .02); +.grid-widget .toolbar-group{ + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem; + border: 1px solid rgba(0,0,0,0.08); + border-radius: 0.5rem; + background: rgba(0,0,0,0.02); } .grid-widget .btn, .grid-widget .form-control, -.grid-widget .badge { - height: var(--tb-h); +.grid-widget .badge{ height: var(--tb-h); } + +.grid-widget [data-toolbar] .badge, +.grid-widget [data-toolbar] .input-group-text{ white-space: nowrap; } + +.grid-widget .toolbar .btn{ + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 0.5rem; } -.grid-widget .toolbar .btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0 .5rem; +.grid-widget .toolbar .form-control-color{ width: var(--tb-h); padding: 0; } + +.grid-widget .tb-btn{ flex-direction: column; gap: 2px; line-height: 1; } +.grid-widget .tb-btn small{ font-size: 11px; opacity: 0.75; } + +.grid-widget .dropdown-toggle::after{ display: none; } + +.grid-widget .toolbar .btn-group .btn{ border-radius: 0; } +.grid-widget .toolbar .btn-group .btn:first-child{ + border-top-left-radius: 0.5rem; + border-bottom-left-radius: 0.5rem; +} +.grid-widget .toolbar .btn-group .btn:last-child{ + border-top-right-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; } -.grid-widget .toolbar .form-control-color { - width: var(--tb-h); - padding: 0; +.grid-widget .btn-check:checked + .btn{ + background: rgba(0,0,0,0.08); + border-color: rgba(0,0,0,0.18); } -.grid-widget .tb-btn { - flex-direction: column; - gap: 2px; - line-height: 1; +.grid-widget .dropdown-menu{ + min-width: 200px; + padding: 0.5rem 0.75rem; + border-radius: 0.75rem; + box-shadow: 0 10px 30px rgba(0,0,0,0.12); + position: absolute; + z-index: 1000; + pointer-events: auto; } -.grid-widget .tb-btn small { - font-size: 11px; - opacity: .75; +.grid-widget .dropdown-menu .form-range{ width: 100%; margin: 0; } + +/* ========================================================= + EDITOR MODE (needs container queries) + ========================================================= */ + +.grid-widget[data-mode="editor"]{ + container-type: inline-size; /* ONLY here */ + min-width: 375px; + height: 100%; + display: flex; + flex-direction: column; } -.grid-widget .dropdown-toggle::after { - display: none; +.grid-widget[data-mode="editor"] [data-grid-wrap]{ + flex: 1 1 auto; + width: 100%; + min-height: 375px; + position: relative; + overflow: hidden; } -.grid-widget .toolbar .btn-group .btn { - border-radius: 0; +.grid-widget[data-mode="editor"] [data-grid]{ + position: absolute; + inset: 0; + width: 100%; + height: 100%; + cursor: crosshair; + touch-action: none; + z-index: 0; } -.grid-widget .toolbar .btn-group .btn:first-child { - border-top-left-radius: .5rem; - border-bottom-left-radius: .5rem; +/* Editor: toolbar should match snapped grid width */ +.grid-widget[data-mode="editor"] [data-toolbar]{ + width: var(--grid-maxw, 100%); + margin-inline: auto; /* center it to match the centered grid */ + max-width: 100%; + align-self: center; /* don't stretch full parent width */ } -.grid-widget .toolbar .btn-group .btn:last-child { - border-top-right-radius: .5rem; - border-bottom-right-radius: .5rem; +/* ========================================================= + VIEWER MODE (must shrink-wrap like an ) + ========================================================= */ + +.grid-widget[data-mode="viewer"]{ + /* explicitly undo any containment */ + container-type: normal; /* <-- the money line */ + contain: none; + + display: inline-block; + vertical-align: middle; + width: auto; + height: auto; + min-width: 0; + flex: none; } -.grid-widget .btn-check:checked+.btn { - background: rgba(0, 0, 0, .08); - border-color: rgba(0, 0, 0, .18); +/* wrap is the sized box (JS sets px) */ +.grid-widget[data-mode="viewer"] [data-grid-wrap]{ + display: inline-block; + position: relative; + overflow: hidden; + line-height: 0; /* remove inline baseline gap */ } -.grid-widget [data-toolbar], -.grid-widget [data-grid] { - margin-inline: auto; +/* grid must be in-flow and fill wrap */ +.grid-widget[data-mode="viewer"] [data-grid]{ + display: block; + width: 100%; + height: 100%; + cursor: default; + overflow: hidden; } -.grid-widget [data-toolbar] { - max-width: var(--grid-maxw, 100%); - width: 100%; -} \ No newline at end of file +/* viewer hides editor-only overlays */ +.grid-widget[data-mode="viewer"] [data-coords], +.grid-widget[data-mode="viewer"] [data-dot]{ display: none !important; } diff --git a/inventory/static/js/components/draw.js b/inventory/static/js/components/draw.js index 03546e1..4f941f7 100644 --- a/inventory/static/js/components/draw.js +++ b/inventory/static/js/components/draw.js @@ -1,14 +1,25 @@ -(function bindGridGlobalKeydownOnce() { - if (window.__gridKeydownBound) return; - window.__gridKeydownBound = true; +(function bindGridGlobalOnce() { + if (window.__gridGlobalBound) return; + window.__gridGlobalBound = true; window.activeGridWidget = null; + // Keydown (undo/redo, escape) document.addEventListener('keydown', (e) => { const w = window.activeGridWidget; - if (!w) return; + if (!w || typeof w.handleKeyDown !== 'function') return; w.handleKeyDown(e); }); + + // Pointer finalize (for drawing finishing outside the element) + const forwardPointer = (e) => { + const w = window.activeGridWidget; + if (!w || typeof w.handleGlobalPointerUp !== 'function') return; + w.handleGlobalPointerUp(e); + }; + + window.addEventListener('pointerup', forwardPointer, { capture: true }); + window.addEventListener('pointercancel', forwardPointer, { capture: true }); })(); function initGridWidget(root, opts = {}) { @@ -190,6 +201,12 @@ function initGridWidget(root, opts = {}) { ctx = canvasEl.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.save(); + ctx.globalAlpha = 1; + ctx.fillStyle = "red"; + ctx.fillRect(0, 0, 10, 10); + ctx.restore(); + redrawAll(); } @@ -280,6 +297,7 @@ function initGridWidget(root, opts = {}) { if (!ctx || !shapes) return; clearCanvas(); + ctx.save(); if (mode !== 'editor') { ctx.translate(viewerOffset.x, viewerOffset.y); @@ -308,15 +326,15 @@ function initGridWidget(root, opts = {}) { const hCells = b ? (b.maxY - b.minY + padCells * 2) : 10; const wPx = Math.max(1, Math.ceil(wCells * cellSize)); - const wPy = Math.max(1, Math.ceil(hCells * cellSize)); + const hPx = Math.max(1, Math.ceil(hCells * cellSize)); gridEl.style.width = `${wPx}px`; - gridEl.style.height = `${wPy}px`; + gridEl.style.height = `${hPx}px`; gridWrapEl.style.width = `${wPx}px`; - gridWrapEl.style.height = `${wPy}px`; + gridWrapEl.style.height = `${hPx}px`; - if(b) { + if (b) { viewerOffset.x = (-b.minX + padCells) * cellSize; viewerOffset.y = (-b.minY + padCells) * cellSize; } else { @@ -465,6 +483,11 @@ function initGridWidget(root, opts = {}) { currentShape = null; redrawAll(); } + }, + + handleGlobalPointerUp(e) { + // Only finalize if this widget is the active one (it should be) + finishPointer(e); } }; @@ -565,6 +588,7 @@ function initGridWidget(root, opts = {}) { } function finishPointer(e) { + if (window.activeGridWidget !== api) return; if (!currentShape) return; if (activePointerId !== null && e.pointerId !== activePointerId) return; @@ -1004,8 +1028,6 @@ function initGridWidget(root, opts = {}) { } gridEl.addEventListener('pointerup', finishPointer); - window.addEventListener('pointerup', finishPointer); - window.addEventListener('pointercancel', finishPointer); root.querySelectorAll('input[data-tool]').forEach((input) => { input.addEventListener('change', () => { @@ -1289,7 +1311,12 @@ function initGridWidget(root, opts = {}) { if (mode !== 'editor') { const script = root.querySelector('[data-grid-doc]'); if (script?.textContent?.trim()) { - try { api.setDoc(JSON.parse(script.textContent)); } catch { } + try { + const parsed = JSON.parse(script.textContent); + api.setDoc(parsed); + } catch (err) { + console.error("viewer JSON.parse failed:", err, script.textContent); + } return; } diff --git a/inventory/templates/components/draw.html b/inventory/templates/components/draw.html index bdce7d8..f6b8f19 100644 --- a/inventory/templates/components/draw.html +++ b/inventory/templates/components/draw.html @@ -244,22 +244,23 @@ {% endmacro %} {% macro viewWidget(uid, json) %} -
-
+ + -
-
- -
-
+ + + + + -
+ {% endmacro %} \ No newline at end of file diff --git a/inventory/templates/testing.html b/inventory/templates/testing.html index ea2c866..e461ff4 100644 --- a/inventory/templates/testing.html +++ b/inventory/templates/testing.html @@ -143,16 +143,10 @@ ] } {% endset %} -
-{# -
- {{ draw.drawWidget('test1') }} -
-#} -
+{{ draw.drawWidget('test1') }} + I am testing a thing. {{ draw.viewWidget('test2', jsonImage) }} -
-
+ The thing has been tested. {% endblock %} {% block scriptincludes %}