Skip to content

Commit d24a41f

Browse files
committed
dragmoveformobile
1 parent 7b85658 commit d24a41f

File tree

2 files changed

+80
-63
lines changed

2 files changed

+80
-63
lines changed

custom_components/reterminal_dashboard/frontend/editor.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,7 @@ textarea:focus {
599599
border: none;
600600
background: transparent;
601601
padding: 0;
602+
touch-action: none;
602603
}
603604

604605
.widget.active {
@@ -620,6 +621,7 @@ textarea:focus {
620621
align-items: center;
621622
justify-content: center;
622623
z-index: 10;
624+
pointer-events: auto;
623625
}
624626

625627
.widget-resize-handle::after {
@@ -651,6 +653,8 @@ textarea:focus {
651653
right: 0;
652654
}
653655

656+
/* Long-press visual feedback removed - replaced by Select-First interaction */
657+
654658
.right-panel h3 {
655659
margin: 4px 0 8px 0;
656660
font-size: var(--fs-base);

custom_components/reterminal_dashboard/frontend/js/core/canvas.js

Lines changed: 76 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ class Canvas {
5050
// Start a 1-second interval to update time-dependent widgets (like datetime)
5151
if (this.updateInterval) clearInterval(this.updateInterval);
5252
this.updateInterval = setInterval(() => {
53+
// SKIP auto-render during active interaction to prevent DOM detachment
54+
if (this.touchState || this.pinchState || this.dragState || this.panState) return;
55+
5356
// Only re-render if there is a datetime widget on the current page to avoid unnecessary overhead
5457
const page = AppState.getCurrentPage();
5558
if (page && page.widgets.some(w => w.type === 'datetime')) {
@@ -967,17 +970,6 @@ class Canvas {
967970
this.canvas.addEventListener("touchstart", (ev) => {
968971
const touches = ev.touches;
969972

970-
// Double-tap detection for zoom reset
971-
const now = Date.now();
972-
if (touches.length === 1 && now - this.lastTapTime < 300) {
973-
// Double-tap detected
974-
this.zoomReset();
975-
this.lastTapTime = 0;
976-
ev.preventDefault();
977-
return;
978-
}
979-
this.lastTapTime = now;
980-
981973
if (touches.length === 2) {
982974
// Two-finger: start pinch/pan mode
983975
ev.preventDefault();
@@ -989,7 +981,7 @@ class Canvas {
989981
startCenterX: (touches[0].clientX + touches[1].clientX) / 2,
990982
startCenterY: (touches[0].clientY + touches[1].clientY) / 2
991983
};
992-
this.touchState = null; // Cancel any widget drag
984+
this.touchState = null;
993985
return;
994986
}
995987

@@ -998,16 +990,15 @@ class Canvas {
998990
const widgetEl = touch.target.closest(".widget");
999991

1000992
if (widgetEl) {
1001-
// Single touch on widget: prepare for drag
993+
// TOUCHING A WIDGET: Prepare for direct manipulation
994+
// We DO NOT call selectWidget here to avoid a re-render that would
995+
// detach the element from the touch stream.
1002996
ev.preventDefault();
1003-
const widgetId = widgetEl.dataset.id;
1004-
AppState.selectWidget(widgetId);
1005997

998+
const widgetId = widgetEl.dataset.id;
1006999
const widget = AppState.getWidgetById(widgetId);
10071000
if (!widget) return;
10081001

1009-
const rect = this.canvas.getBoundingClientRect();
1010-
const zoom = AppState.zoomLevel;
10111002
const isResizeHandle = touch.target.classList.contains("widget-resize-handle");
10121003

10131004
if (isResizeHandle) {
@@ -1017,26 +1008,37 @@ class Canvas {
10171008
startX: touch.clientX,
10181009
startY: touch.clientY,
10191010
startW: widget.width,
1020-
startH: widget.height
1011+
startH: widget.height,
1012+
el: widgetEl
10211013
};
10221014
} else {
1023-
// Calculate offset for drag
10241015
this.touchState = {
10251016
mode: "move",
10261017
id: widgetId,
10271018
startTouchX: touch.clientX,
10281019
startTouchY: touch.clientY,
10291020
startWidgetX: widget.x,
10301021
startWidgetY: widget.y,
1031-
hasMoved: false // Deadzone tracking
1022+
hasMoved: false,
1023+
el: widgetEl
10321024
};
10331025
}
10341026

10351027
window.addEventListener("touchmove", this._boundTouchMove, { passive: false });
10361028
window.addEventListener("touchend", this._boundTouchEnd);
10371029
window.addEventListener("touchcancel", this._boundTouchEnd);
1030+
10381031
} else {
1039-
// Single touch on empty canvas: start panning
1032+
// TOUCHING EMPTY CANVAS: Pan or double-tap zoom reset
1033+
const now = Date.now();
1034+
if (now - this.lastTapTime < 300) {
1035+
this.zoomReset();
1036+
this.lastTapTime = 0;
1037+
ev.preventDefault();
1038+
return;
1039+
}
1040+
this.lastTapTime = now;
1041+
10401042
ev.preventDefault();
10411043
this.touchState = {
10421044
mode: "pan",
@@ -1113,12 +1115,12 @@ class Canvas {
11131115
this.panY = this.touchState.startPanY + dy;
11141116
this.applyZoom();
11151117
} else if (this.touchState.mode === "move") {
1116-
// Widget move with 10px deadzone to prevent accidental drags
1118+
// Widget move with small deadzone
11171119
const dx = touch.clientX - this.touchState.startTouchX;
11181120
const dy = touch.clientY - this.touchState.startTouchY;
11191121

1120-
if (!this.touchState.hasMoved && Math.hypot(dx, dy) < 10) {
1121-
return; // Still in deadzone
1122+
if (!this.touchState.hasMoved && Math.hypot(dx, dy) < 5) {
1123+
return; // Small deadzone
11221124
}
11231125
this.touchState.hasMoved = true;
11241126

@@ -1128,29 +1130,22 @@ class Canvas {
11281130
const dims = AppState.getCanvasDimensions();
11291131
const zoom = AppState.zoomLevel;
11301132

1131-
// Calculate new position
11321133
let x = this.touchState.startWidgetX + dx / zoom;
11331134
let y = this.touchState.startWidgetY + dy / zoom;
11341135

11351136
// Clamp to canvas
11361137
x = Math.max(0, Math.min(dims.width - widget.width, x));
11371138
y = Math.max(0, Math.min(dims.height - widget.height, y));
11381139

1139-
// Apply grid/widget snapping
1140-
const page = AppState.getCurrentPage();
1141-
if (page?.layout) {
1142-
const snapped = this._snapToGridCell(x, y, widget.width, widget.height, page.layout, dims);
1143-
x = snapped.x;
1144-
y = snapped.y;
1145-
} else {
1146-
const snapped = this.applySnapToPosition(widget, x, y, false, dims);
1147-
x = snapped.x;
1148-
y = snapped.y;
1149-
}
1150-
1140+
// Update internal state
11511141
widget.x = x;
11521142
widget.y = y;
1153-
this.render();
1143+
1144+
// Direct DOM update instead of render() to preserve touch stream
1145+
if (this.touchState.el) {
1146+
this.touchState.el.style.left = x + "px";
1147+
this.touchState.el.style.top = y + "px";
1148+
}
11541149
} else if (this.touchState.mode === "resize") {
11551150
// Widget resize
11561151
const widget = AppState.getWidgetById(this.touchState.id);
@@ -1180,30 +1175,18 @@ class Canvas {
11801175
}
11811176

11821177
// Clamp to canvas bounds
1183-
const minSize = 1;
1178+
const minSize = 20; // Ensure widget doesn't disappear
11841179
w = Math.max(minSize, Math.min(dims.width - widget.x, w));
11851180
h = Math.max(minSize, Math.min(dims.height - widget.y, h));
1186-
widget.width = Math.round(w);
1187-
widget.height = Math.round(h);
11881181

1189-
// Special handling for icons and circles
1190-
if (wtype === "icon" || wtype === "weather_icon" || wtype === "battery_icon" || wtype === "wifi_signal") {
1191-
const props = widget.props || {};
1192-
if (props.fit_icon_to_frame) {
1193-
const padding = 4;
1194-
const maxDim = Math.max(8, Math.min(widget.width - padding * 2, widget.height - padding * 2));
1195-
props.size = Math.round(maxDim);
1196-
} else {
1197-
const newSize = Math.max(8, Math.min(widget.width, widget.height));
1198-
props.size = Math.round(newSize);
1199-
}
1200-
} else if (wtype === "shape_circle") {
1201-
const size = Math.max(widget.width, widget.height);
1202-
widget.width = size;
1203-
widget.height = size;
1204-
}
1182+
widget.width = w;
1183+
widget.height = h;
12051184

1206-
this.render();
1185+
// Direct DOM update instead of render() to preserve touch stream
1186+
if (this.touchState.el) {
1187+
this.touchState.el.style.width = w + "px";
1188+
this.touchState.el.style.height = h + "px";
1189+
}
12071190
}
12081191
}
12091192
}
@@ -1214,15 +1197,45 @@ class Canvas {
12141197
_onTouchEnd(ev) {
12151198
if (this.touchState) {
12161199
const widgetId = this.touchState.id;
1217-
this.touchState = null;
1218-
this.clearSnapGuides();
1200+
const mode = this.touchState.mode;
1201+
const hasMoved = this.touchState.hasMoved;
12191202

1203+
// Handle final snapping and selection for widgets
12201204
if (widgetId) {
1221-
this._updateWidgetGridCell(widgetId);
1222-
AppState.recordHistory();
1223-
emit(EVENTS.STATE_CHANGED);
1205+
const widget = AppState.getWidgetById(widgetId);
1206+
if (widget) {
1207+
if (mode === "move" && hasMoved) {
1208+
// Apply final snapping on release
1209+
const dims = AppState.getCanvasDimensions();
1210+
const page = AppState.getCurrentPage();
1211+
if (page?.layout) {
1212+
const snapped = this._snapToGridCell(widget.x, widget.y, widget.width, widget.height, page.layout, dims);
1213+
widget.x = snapped.x;
1214+
widget.y = snapped.y;
1215+
} else {
1216+
const snapped = this.applySnapToPosition(widget, widget.x, widget.y, false, dims);
1217+
widget.x = snapped.x;
1218+
widget.y = snapped.y;
1219+
}
1220+
} else if (mode === "resize") {
1221+
// Integer rounding for final dimensions
1222+
widget.width = Math.round(widget.width);
1223+
widget.height = Math.round(widget.height);
1224+
}
1225+
1226+
// Perform selection at the end to avoid DOM detachment during gesture
1227+
AppState.selectWidget(widgetId);
1228+
}
1229+
1230+
if ((mode === "move" || mode === "resize") && hasMoved) {
1231+
this._updateWidgetGridCell(widgetId);
1232+
AppState.recordHistory();
1233+
emit(EVENTS.STATE_CHANGED);
1234+
}
12241235
}
12251236

1237+
this.touchState = null;
1238+
this.clearSnapGuides();
12261239
this.render();
12271240
}
12281241

0 commit comments

Comments
 (0)