diff --git a/src/index.html b/src/index.html index 235963a..2602f76 100644 --- a/src/index.html +++ b/src/index.html @@ -1587,6 +1587,10 @@ } .layerItemRow { display:flex; align-items:stretch; gap:3px; } .layerItemRow .layerItem { flex:1; min-width:0; } + .layerItemRow[draggable="true"], .layerGroupBlock[draggable="true"] { cursor:grab; } + .layerItemRow.dragOverBefore, .layerGroupBlock.dragOverBefore { box-shadow:0 -2px 0 var(--accent); } + .layerItemRow.dragOverAfter, .layerGroupBlock.dragOverAfter { box-shadow:0 2px 0 var(--accent); } + .layerItemRow.draggingLayerRow, .layerGroupBlock.draggingLayerRow { opacity:.45; } .layerEyeToggle { flex:0 0 auto; width:30px; border:1px solid var(--line); border-radius:0; background:var(--panel); color:var(--ink); cursor:pointer; font-size:.8rem; line-height:1; } .layerEyeToggle:hover { background:rgba(127,127,127,.12); } .layerItemRow.layerHiddenRow .layerItem { opacity:.5; } @@ -9650,9 +9654,10 @@ } // A layer-list row: the selectable item button + a visibility eye toggle. - function makeLayerListRow(layer) { + function makeLayerListRow(layer, orderedLayers = []) { const row = document.createElement('div'); row.className = `layerItemRow${layer.hidden ? ' layerHiddenRow' : ''}`; + setupLayerPanelDragHandle(row, `layer:${layer.id}`, orderedLayers); const item = document.createElement('button'); item.type = 'button'; const layerSelected = isLayerSelected(layer.id); @@ -9684,7 +9689,7 @@ return row; } - function makeLayerGroupListBlock(groupId, layers) { + function makeLayerGroupListBlock(groupId, layers, orderedLayers = []) { const group = ensureLayerGroups()[groupId] || { id: groupId, name: 'Layer group' }; const selectedIds = selectedLayerIds(); const layerIds = layers.map(l => l.id); @@ -9692,6 +9697,7 @@ const groupSelected = selectedCount === layerIds.length && layerIds.length > 0; const block = document.createElement('div'); block.className = `layerGroupBlock${group.collapsed ? ' collapsed' : ''}`; + setupLayerPanelDragHandle(block, `group:${groupId}`, orderedLayers); const header = document.createElement('div'); header.className = 'layerGroupHeader'; @@ -9733,9 +9739,9 @@ if (renderedGroups.has(layer.groupId)) continue; const groupLayers = orderedLayers.filter(l => l.groupId === layer.groupId); renderedGroups.add(layer.groupId); - listWrap.appendChild(makeLayerGroupListBlock(layer.groupId, groupLayers)); + listWrap.appendChild(makeLayerGroupListBlock(layer.groupId, groupLayers, orderedLayers)); } else { - listWrap.appendChild(makeLayerListRow(layer)); + listWrap.appendChild(makeLayerListRow(layer, orderedLayers)); } } } @@ -11186,6 +11192,7 @@ } let _layerPanelSelectionAnchor = null; + let _layerPanelDrag = null; function layerPanelRowsFromOrderedLayers(orderedLayers) { const rows = []; @@ -11259,6 +11266,89 @@ _layerPanelSelectionAnchor = { key }; } + function layerPanelReorderRowsFromOrderedLayers(orderedLayers) { + const rows = []; + const renderedGroups = new Set(); + for (const layer of orderedLayers) { + if (layer.groupId) { + if (renderedGroups.has(layer.groupId)) continue; + const groupLayers = orderedLayers.filter(l => l.groupId === layer.groupId); + renderedGroups.add(layer.groupId); + rows.push({ key: `group:${layer.groupId}`, layerIds: groupLayers.map(l => l.id) }); + } else { + rows.push({ key: `layer:${layer.id}`, layerIds: [layer.id] }); + } + } + return rows; + } + + function clearLayerPanelDragOver() { + document.querySelectorAll('.dragOverBefore, .dragOverAfter').forEach((el) => { + el.classList.remove('dragOverBefore', 'dragOverAfter'); + }); + } + + function reorderLayerPanelRows(rows, sourceKey, targetKey, placeAfter) { + const from = rows.findIndex(row => row.key === sourceKey); + const target = rows.findIndex(row => row.key === targetKey); + if (from === -1 || target === -1 || from === target) return false; + const next = rows.slice(); + const [moved] = next.splice(from, 1); + let insertAt = next.findIndex(row => row.key === targetKey); + if (insertAt === -1) return false; + if (placeAfter) insertAt += 1; + next.splice(insertAt, 0, moved); + + const flattenedTopDown = next.flatMap(row => row.layerIds); + const zById = new Map(); + flattenedTopDown.slice().reverse().forEach((id, index) => zById.set(id, index + 1)); + for (const layer of state.layers || []) { + if (zById.has(layer.id)) layer.z = zById.get(layer.id); + } + return true; + } + + function setupLayerPanelDragHandle(el, rowKey, orderedLayers) { + const rows = layerPanelReorderRowsFromOrderedLayers(orderedLayers); + if (rows.length < 2) return; + el.draggable = true; + el.dataset.layerPanelRowKey = rowKey; + el.addEventListener('dragstart', (event) => { + _layerPanelDrag = { rowKey, rows }; + el.classList.add('draggingLayerRow'); + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text/plain', rowKey); + }); + el.addEventListener('dragend', () => { + _layerPanelDrag = null; + el.classList.remove('draggingLayerRow'); + clearLayerPanelDragOver(); + }); + el.addEventListener('dragover', (event) => { + if (!_layerPanelDrag || _layerPanelDrag.rowKey === rowKey) return; + if (!_layerPanelDrag.rows.find(row => row.key === rowKey)) return; + event.preventDefault(); + clearLayerPanelDragOver(); + const rect = el.getBoundingClientRect(); + const after = event.clientY > rect.top + rect.height / 2; + el.classList.add(after ? 'dragOverAfter' : 'dragOverBefore'); + event.dataTransfer.dropEffect = 'move'; + }); + el.addEventListener('drop', (event) => { + if (!_layerPanelDrag || _layerPanelDrag.rowKey === rowKey) return; + event.preventDefault(); + const rect = el.getBoundingClientRect(); + const after = event.clientY > rect.top + rect.height / 2; + pushHistory('reorder layers'); + const changed = reorderLayerPanelRows(_layerPanelDrag.rows, _layerPanelDrag.rowKey, rowKey, after); + _layerPanelDrag = null; + clearLayerPanelDragOver(); + if (changed) { + render(); + } + }); + } + let _pendingLayerGroupIds = []; function openLayerGroupModal() { @@ -19151,6 +19241,24 @@ render(); } + document.addEventListener('keydown', (e) => { + if (isEditableFocus() || _penDraft) return; + const k = (e.key || '').toLowerCase(); + const deleteKey = k === 'delete' || k === 'del' || k === 'backspace' || e.code === 'Delete' || e.code === 'Backspace'; + const mod = e.ctrlKey || e.metaKey; + if (!mod && !e.altKey && deleteKey && selectedLayerIds().length) { + e.preventDefault(); + e.stopPropagation(); + deleteSelectedLayer(); + return; + } + if (mod && !e.shiftKey && !e.altKey && k === 'g') { + e.preventDefault(); + e.stopPropagation(); + groupSelectedLayers(); + } + }, true); + // Sidebar panels start collapsed; click a panel's legend to toggle it. // (Selecting a layer also auto-opens the Layers panel — see render().) function setupCollapsiblePanels() {