auto-sync: 2026-05-05 20:10:01
This commit is contained in:
@@ -266,8 +266,24 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: relative;
|
||||
}
|
||||
.wl-item:last-child { border-bottom: none; }
|
||||
.wl-drag-handle {
|
||||
width: 20px; height: 28px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: var(--text3); cursor: grab; flex-shrink: 0;
|
||||
touch-action: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.wl-drag-handle svg { width: 16px; height: 16px; }
|
||||
.wl-item.dragging {
|
||||
opacity: 0.4;
|
||||
background: var(--surface);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.wl-item.drag-over-top { border-top: 2px solid var(--accent); }
|
||||
.wl-item.drag-over-bottom { border-bottom: 2px solid var(--accent); }
|
||||
.wl-pin { flex-shrink: 0; display: flex; align-items: center; }
|
||||
.wl-label {
|
||||
flex: 1; font-size: 13px; color: var(--text);
|
||||
|
||||
128
tasks/enduro-trails/prototype/static/app.js
vendored
128
tasks/enduro-trails/prototype/static/app.js
vendored
@@ -532,15 +532,18 @@ async function renderWaypointsList() {
|
||||
const list = document.getElementById('waypoints-list');
|
||||
if (!routeWaypoints.length) { list.innerHTML = ''; return; }
|
||||
|
||||
const gripSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></svg>`;
|
||||
|
||||
let html = routeWaypoints.map((wp, i) => {
|
||||
const isStart = i === 0;
|
||||
const isEnd = i === routeWaypoints.length - 1;
|
||||
const label = isStart ? 'S' : isEnd ? 'F' : String(i);
|
||||
const color = isStart ? '#2EA043' : isEnd ? '#FF3B1F' : '#0066ff';
|
||||
const coordText = `${wp.lat.toFixed(3)}, ${wp.lon.toFixed(3)}`;
|
||||
return `<div class="wl-item" id="wl-item-${i}">
|
||||
return `<div class="wl-item" id="wl-item-${i}" data-idx="${i}">
|
||||
<div class="wl-pin">${waypointPinSvg(label, color)}</div>
|
||||
<span class="wl-label" id="wl-label-${i}">${coordText}</span>
|
||||
<div class="wl-drag-handle" data-idx="${i}">${gripSvg}</div>
|
||||
<button class="wl-remove" onclick="removeWaypoint(${i})" title="Удалить">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
@@ -563,6 +566,129 @@ async function renderWaypointsList() {
|
||||
const el = document.getElementById(`wl-label-${i}`);
|
||||
if (el) el.textContent = name;
|
||||
});
|
||||
|
||||
// Touch drag-and-drop (mobile only)
|
||||
_initWaypointDragHandles(list);
|
||||
}
|
||||
|
||||
function _initWaypointDragHandles(list) {
|
||||
let dragIdx = -1;
|
||||
let startY = 0;
|
||||
let dragging = false;
|
||||
let lastOverEl = null;
|
||||
let lastOverPos = null;
|
||||
|
||||
function getItemEls() {
|
||||
return Array.from(list.querySelectorAll('.wl-item[data-idx]'));
|
||||
}
|
||||
|
||||
function clearHighlights() {
|
||||
getItemEls().forEach(el => {
|
||||
el.classList.remove('drag-over-top', 'drag-over-bottom', 'dragging');
|
||||
});
|
||||
}
|
||||
|
||||
function getDropTarget(clientY) {
|
||||
const items = getItemEls();
|
||||
for (const el of items) {
|
||||
const idx = parseInt(el.dataset.idx, 10);
|
||||
if (idx === dragIdx) continue;
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (clientY >= rect.top && clientY <= rect.bottom) {
|
||||
const mid = rect.top + rect.height / 2;
|
||||
return { el, idx, pos: clientY < mid ? 'top' : 'bottom' };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function startDrag(clientY, idx) {
|
||||
dragIdx = idx;
|
||||
startY = clientY;
|
||||
dragging = false;
|
||||
lastOverEl = null;
|
||||
lastOverPos = null;
|
||||
const dragEl = document.getElementById(`wl-item-${idx}`);
|
||||
if (dragEl) dragEl.classList.add('dragging');
|
||||
}
|
||||
|
||||
function moveDrag(clientY) {
|
||||
if (dragIdx < 0) return;
|
||||
const dy = Math.abs(clientY - startY);
|
||||
if (dy > 5) dragging = true;
|
||||
if (!dragging) return;
|
||||
clearHighlights();
|
||||
const target = getDropTarget(clientY);
|
||||
if (target) {
|
||||
lastOverEl = target.el;
|
||||
lastOverPos = target.pos;
|
||||
target.el.classList.add(target.pos === 'top' ? 'drag-over-top' : 'drag-over-bottom');
|
||||
} else {
|
||||
lastOverEl = null;
|
||||
lastOverPos = null;
|
||||
}
|
||||
}
|
||||
|
||||
function endDrag(finalClientY) {
|
||||
if (dragIdx < 0) return;
|
||||
clearHighlights();
|
||||
const dy = Math.abs(finalClientY - startY);
|
||||
|
||||
if (dragging && dy > 30 && lastOverEl !== null) {
|
||||
const dropIdx = parseInt(lastOverEl.dataset.idx, 10);
|
||||
let insertAt = lastOverPos === 'top' ? dropIdx : dropIdx + 1;
|
||||
const moved = routeWaypoints.splice(dragIdx, 1)[0];
|
||||
if (insertAt > dragIdx) insertAt--;
|
||||
routeWaypoints.splice(insertAt, 0, moved);
|
||||
rebuildWaypointMarkers();
|
||||
renderWaypointsList();
|
||||
if (routeWaypoints.length >= 2) debounceBuildRoute();
|
||||
updateMiniRouteCard();
|
||||
}
|
||||
|
||||
dragIdx = -1;
|
||||
dragging = false;
|
||||
lastOverEl = null;
|
||||
lastOverPos = null;
|
||||
}
|
||||
|
||||
// Touch (mobile)
|
||||
list.addEventListener('touchstart', (e) => {
|
||||
const handle = e.target.closest('.wl-drag-handle');
|
||||
if (!handle) return;
|
||||
startDrag(e.touches[0].clientY, parseInt(handle.dataset.idx, 10));
|
||||
}, { passive: true });
|
||||
|
||||
list.addEventListener('touchmove', (e) => {
|
||||
if (dragIdx < 0) return;
|
||||
moveDrag(e.touches[0].clientY);
|
||||
e.preventDefault();
|
||||
}, { passive: false });
|
||||
|
||||
list.addEventListener('touchend', (e) => {
|
||||
endDrag(e.changedTouches[0].clientY);
|
||||
}, { passive: true });
|
||||
|
||||
// Mouse (desktop)
|
||||
list.addEventListener('mousedown', (e) => {
|
||||
const handle = e.target.closest('.wl-drag-handle');
|
||||
if (!handle) return;
|
||||
e.preventDefault();
|
||||
startDrag(e.clientY, parseInt(handle.dataset.idx, 10));
|
||||
document.addEventListener('mousemove', _onDragMouse);
|
||||
document.addEventListener('mouseup', _onDropMouse);
|
||||
});
|
||||
|
||||
function _onDragMouse(e) {
|
||||
if (dragIdx < 0) return;
|
||||
moveDrag(e.clientY);
|
||||
}
|
||||
|
||||
function _onDropMouse(e) {
|
||||
endDrag(e.clientY);
|
||||
document.removeEventListener('mousemove', _onDragMouse);
|
||||
document.removeEventListener('mouseup', _onDropMouse);
|
||||
}
|
||||
}
|
||||
|
||||
function removeWaypoint(idx) {
|
||||
|
||||
Reference in New Issue
Block a user