Use in-place source move for drag instead of placeholder + display:none
The clone-placeholder approach hid the source element with display:none, which some browsers treat as drag cancellation. The drag appeared to "not respond at all" to mouse press. Switch to a simpler approach: keep the source element in the DOM and move it directly during dragover. Apply a .dragGhost class after the drag image is captured (via setTimeout 0) so the source becomes a translucent dashed placeholder at the prospective drop position. drop just compares the source's current DOM index to its original data-index. Also add cursor:grabbing on :active for visible press feedback. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user