diff --git a/public/listEditor.js b/public/listEditor.js index 461b78d..d36951b 100644 --- a/public/listEditor.js +++ b/public/listEditor.js @@ -155,31 +155,28 @@ }) } - // ── 드래그 시스템: 컨테이너 단위로 한 곳에서 관리 ───── - // drag = { type, srcIdx, srcEl, placeholder } | null - var drag = null + // ── 드래그 시스템 ─────────────────────────────────── + // 원본 요소 자체를 dragover 동안 이동시켜서 "착지 자리에 반투명 고스트" 효과를 만든다. + // placeholder/clone 방식은 source 를 display:none 으로 숨기는 순간 일부 브라우저에서 + // 드래그가 즉시 취소되는 문제가 있어 채택하지 않는다. + var drag = null // { type, srcEl } | null function attachDraggable(el, type, idx) { el.addEventListener('dragstart', function (e) { - if (!el.draggable) { e.preventDefault(); return } - var ph = el.cloneNode(true) - ph.classList.add('dragPlaceholder') - ph.removeAttribute('draggable') - // 클론 안의 입력 가능한 요소를 비활성화 (포커스 가능성 차단) - ph.querySelectorAll('[contenteditable]').forEach(function (c) { - c.removeAttribute('contenteditable') - }) - drag = { type: type, srcIdx: idx, srcEl: el, placeholder: ph } + // 편집 중인 칸을 잡고 드래그하려는 경우는 드래그를 막음. + var t = e.target + if (t && t.getAttribute && t.getAttribute('contenteditable') === 'true') { + e.preventDefault() + return + } + drag = { type: type, srcEl: el } try { e.dataTransfer.setData('text/plain', String(idx)) e.dataTransfer.effectAllowed = 'move' } catch (_) {} - // dragstart 가 완료된 직후에 원본을 숨김 (드래그 이미지 캡처 후). - // placeholder 는 같은 자리에 삽입. - var parent = el.parentNode - parent.insertBefore(ph, el) + // 드래그 이미지 캡처 이후에 ghost 스타일을 적용 (이미지에 ghost 가 묻지 않도록). setTimeout(function () { - if (drag && drag.srcEl) drag.srcEl.classList.add('hiddenWhileDragging') + if (drag && drag.srcEl) drag.srcEl.classList.add('dragGhost') }, 0) }) el.addEventListener('dragend', cleanupDrag) @@ -189,48 +186,42 @@ }) } - // 컨테이너 위에서 placeholder 의 삽입 지점을 다시 계산. function bindContainerDnd(containerId, type, orientation) { var container = document.getElementById(containerId) container.addEventListener('dragover', function (e) { if (!drag || drag.type !== type) return e.preventDefault() try { e.dataTransfer.dropEffect = 'move' } catch (_) {} - var children = [] + // 컨테이너 자식 중 source 를 제외한 나머지에서 삽입 지점을 찾는다. + var target = null for (var i = 0; i < container.children.length; i++) { var c = container.children[i] - if (c === drag.placeholder) continue if (c === drag.srcEl) continue - children.push(c) - } - var target = null - for (var j = 0; j < children.length; j++) { - var rect = children[j].getBoundingClientRect() + var rect = c.getBoundingClientRect() var mid = (orientation === 'horizontal') ? rect.left + rect.width / 2 : rect.top + rect.height / 2 var pos = (orientation === 'horizontal') ? e.clientX : e.clientY - if (pos < mid) { target = children[j]; break } + if (pos < mid) { target = c; break } } if (target) { - if (drag.placeholder.nextSibling !== target) container.insertBefore(drag.placeholder, target) + if (drag.srcEl.nextSibling !== target) container.insertBefore(drag.srcEl, target) } else { - if (drag.placeholder !== container.lastChild) container.appendChild(drag.placeholder) + if (drag.srcEl !== container.lastChild) container.appendChild(drag.srcEl) } }) container.addEventListener('drop', function (e) { if (!drag || drag.type !== type) return e.preventDefault() - // 새 인덱스 = srcEl 을 제외한 children 리스트에서 placeholder 의 위치. + // 새 인덱스 = source 의 현재 컨테이너 내 위치. var newIdx = 0 for (var i = 0; i < container.children.length; i++) { - var c = container.children[i] - if (c === drag.srcEl) continue - if (c === drag.placeholder) break - newIdx++ + if (container.children[i] === drag.srcEl) { newIdx = i; break } } var arr = (type === 'music') ? state.music : state.images - var moved = arr.splice(drag.srcIdx, 1)[0] + // 원래 인덱스: state 에서 동일 url 을 찾는 대신 data-index 가 렌더 시점의 위치이므로 사용. + var srcIdx = Number(drag.srcEl.dataset.index) + var moved = arr.splice(srcIdx, 1)[0] arr.splice(newIdx, 0, moved) cleanupDrag() if (type === 'music') renderMusic(); else renderImage() @@ -241,8 +232,7 @@ function cleanupDrag() { if (!drag) return - if (drag.placeholder && drag.placeholder.parentNode) drag.placeholder.remove() - if (drag.srcEl) drag.srcEl.classList.remove('hiddenWhileDragging') + if (drag.srcEl) drag.srcEl.classList.remove('dragGhost') drag = null } diff --git a/public/styles.css b/public/styles.css index da9cfc4..c5e6465 100644 --- a/public/styles.css +++ b/public/styles.css @@ -440,16 +440,16 @@ body.siteBody.centerLayout { } .rowDur { color: var(--text-muted); font-size: 13px; } -/* 드래그 시스템 공통: 원본은 잠시 숨기고, 같은 모양의 placeholder 가 들어갈 자리에서 반투명하게 보임 */ -.hiddenWhileDragging { display: none !important; } -.dragPlaceholder { +/* 드래그 시스템: 원본 요소가 그대로 새 위치로 이동하면서 반투명 ghost 로 보임 */ +.dragGhost { opacity: 0.45; - pointer-events: none; outline: 2px dashed var(--accent); outline-offset: -2px; background: rgba(47, 129, 247, 0.08); } -.dragPlaceholder * { pointer-events: none !important; } +.dragGhost * { pointer-events: none !important; } +.trackRow:active { cursor: grabbing; } +.imageCard:active { cursor: grabbing; } /* 사진 그리드 */ .imageGrid {