Fix Layout tab canvas not rendering on mobile
The placement canvas sized itself with CSS aspect-ratio plus percentage widths/heights on the widget boxes. aspect-ratio isn't supported on every mobile browser this app gets viewed from, and without it a percentage height on an absolutely-positioned child collapses to 0 against an indeterminate-height ancestor -- the canvas rendered with no visible size at all, just unstyled labels and buttons floating in normal document flow. Confirmed working on desktop, broken on the reporter's phone. Canvas and widget-box geometry is now computed and applied in pixels from JS (measuring the wrap's own width, deriving height from the grid's row/col ratio), recalculated on window resize. Also gives .widget-box's color-mix() background a plain-color fallback for the same class of older-browser gap.
This commit is contained in:
@@ -7,6 +7,14 @@
|
||||
// reloads the canvas from the server's actual state rather than trusting
|
||||
// an optimistic update -- simplest way to guarantee the canvas never
|
||||
// drifts from what a rejected request left in place.
|
||||
//
|
||||
// Widget/canvas geometry is computed and applied in *pixels* from JS,
|
||||
// not CSS percentages/aspect-ratio -- aspect-ratio isn't supported on
|
||||
// every mobile browser this app gets viewed from, and a percentage
|
||||
// height on the widget boxes silently collapses to 0 against an
|
||||
// indeterminate-height ancestor on those browsers (the canvas would
|
||||
// render with no visible size at all, which is exactly what happened
|
||||
// before this was pixel-based).
|
||||
|
||||
let gridState = null; // last-loaded GET .../widgets response
|
||||
|
||||
@@ -37,23 +45,51 @@ async function takeControl() {
|
||||
}
|
||||
document.getElementById('take-control').addEventListener('click', takeControl);
|
||||
|
||||
function cellSizePx() {
|
||||
const rect = document.getElementById('widget-canvas').getBoundingClientRect();
|
||||
return { cellW: rect.width / gridState.grid.cols, cellH: rect.height / gridState.grid.rows };
|
||||
// Cached each time the canvas is (re)laid out (see layoutCanvas) so drag
|
||||
// math doesn't re-measure the DOM on every pointermove.
|
||||
let canvasMetrics = { width: 0, height: 0, cellW: 0, cellH: 0 };
|
||||
|
||||
function layoutCanvas() {
|
||||
if (!gridState) return;
|
||||
const wrap = document.getElementById('widget-canvas-wrap');
|
||||
const canvas = document.getElementById('widget-canvas');
|
||||
const cols = gridState.grid.cols, rows = gridState.grid.rows;
|
||||
|
||||
// wrap's own width comes from ordinary CSS (100% of the card, capped
|
||||
// at max-width) -- only its height is JS-driven, from that measured
|
||||
// width, to keep the grid's aspect ratio without relying on the CSS
|
||||
// aspect-ratio property.
|
||||
const width = wrap.getBoundingClientRect().width;
|
||||
const height = width * (rows / cols);
|
||||
wrap.style.height = height + 'px';
|
||||
canvas.style.width = width + 'px';
|
||||
canvas.style.height = height + 'px';
|
||||
|
||||
const cellW = width / cols, cellH = height / rows;
|
||||
canvasMetrics = { width, height, cellW, cellH };
|
||||
wrap.style.backgroundImage =
|
||||
`linear-gradient(to right, var(--border) 1px, transparent 1px),` +
|
||||
`linear-gradient(to bottom, var(--border) 1px, transparent 1px)`;
|
||||
wrap.style.backgroundSize = `${cellW}px ${cellH}px`;
|
||||
|
||||
for (const box of canvas.children) {
|
||||
positionBox(box, box._rect);
|
||||
}
|
||||
}
|
||||
|
||||
function positionBox(box, rect) {
|
||||
const cols = gridState.grid.cols, rows = gridState.grid.rows;
|
||||
box.style.left = (rect.x / cols * 100) + '%';
|
||||
box.style.top = (rect.y / rows * 100) + '%';
|
||||
box.style.width = (rect.w / cols * 100) + '%';
|
||||
box.style.height = (rect.h / rows * 100) + '%';
|
||||
box._rect = rect;
|
||||
const { cellW, cellH } = canvasMetrics;
|
||||
box.style.left = (rect.x * cellW) + 'px';
|
||||
box.style.top = (rect.y * cellH) + 'px';
|
||||
box.style.width = (rect.w * cellW) + 'px';
|
||||
box.style.height = (rect.h * cellH) + 'px';
|
||||
}
|
||||
|
||||
function startDrag(e, widget, box, isResize) {
|
||||
e.preventDefault();
|
||||
box.setPointerCapture(e.pointerId);
|
||||
const { cellW, cellH } = cellSizePx();
|
||||
const { cellW, cellH } = canvasMetrics;
|
||||
const startX = e.clientX, startY = e.clientY;
|
||||
const orig = { x: widget.x, y: widget.y, w: widget.w, h: widget.h };
|
||||
const cols = gridState.grid.cols, rows = gridState.grid.rows;
|
||||
@@ -141,9 +177,6 @@ async function addWidget(widgetType) {
|
||||
|
||||
function renderCanvas() {
|
||||
const canvas = document.getElementById('widget-canvas');
|
||||
const wrap = document.getElementById('widget-canvas-wrap');
|
||||
wrap.style.setProperty('--grid-cols', gridState.grid.cols);
|
||||
wrap.style.setProperty('--grid-rows', gridState.grid.rows);
|
||||
canvas.innerHTML = '';
|
||||
document.getElementById('widget-canvas-empty-hint').style.display = gridState.widgets.length ? 'none' : '';
|
||||
|
||||
@@ -151,7 +184,7 @@ function renderCanvas() {
|
||||
const box = document.createElement('div');
|
||||
box.className = 'widget-box';
|
||||
box.dataset.widgetType = widget.widget_type;
|
||||
positionBox(box, widget);
|
||||
box._rect = widget;
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'widget-box-label';
|
||||
@@ -176,6 +209,7 @@ function renderCanvas() {
|
||||
|
||||
canvas.appendChild(box);
|
||||
}
|
||||
layoutCanvas();
|
||||
}
|
||||
|
||||
function renderAddButtons() {
|
||||
@@ -206,4 +240,10 @@ async function loadWidgets() {
|
||||
}
|
||||
}
|
||||
|
||||
let resizeTimer = null;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(layoutCanvas, 100);
|
||||
});
|
||||
|
||||
loadWidgets();
|
||||
|
||||
@@ -323,24 +323,26 @@ button.secondary:hover { background: var(--surface-alt); }
|
||||
.info-box.warn { background: var(--warn-bg); color: var(--warn-text); border-color: transparent; }
|
||||
|
||||
#widget-canvas-wrap {
|
||||
--grid-cols: 8;
|
||||
--grid-rows: 5;
|
||||
/* Height is set in px by frame_layout.js (measured wrap width * rows/cols)
|
||||
-- not CSS aspect-ratio, which isn't supported on every mobile browser
|
||||
this app gets viewed from, and percentage heights on the widget boxes
|
||||
below would silently collapse to 0 against an indeterminate-height
|
||||
ancestor if it weren't. Box positions/sizes are likewise set in px by
|
||||
JS, not CSS percentages, for the same cross-browser reason. */
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
aspect-ratio: var(--grid-cols) / var(--grid-rows);
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background-color: var(--surface-alt);
|
||||
background-image:
|
||||
linear-gradient(to right, var(--border) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--border) 1px, transparent 1px);
|
||||
background-size: calc(100% / var(--grid-cols)) calc(100% / var(--grid-rows));
|
||||
background-repeat: repeat;
|
||||
}
|
||||
#widget-canvas { position: relative; width: 100%; height: 100%; }
|
||||
.widget-box {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid var(--accent);
|
||||
background: var(--surface-alt); /* fallback for browsers without color-mix() support */
|
||||
background: color-mix(in srgb, var(--accent) 14%, var(--surface));
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user