선택된 이미지를 드래그할 때 옮겨질 위치에 대한 카드 표시
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
let _grid, _gridContainer, _dragGhostEl, _fileExplorerEl;
|
let _grid, _gridContainer, _dragGhostEl, _fileExplorerEl;
|
||||||
let _state, _cb, _onFolderDrop;
|
let _state, _cb, _onFolderDrop;
|
||||||
|
let _placeholder = null;
|
||||||
|
let _dropTargetIdx = null;
|
||||||
|
|
||||||
let dragMode = null;
|
let dragMode = null;
|
||||||
let dragSrcIdx = null;
|
let dragSrcIdx = null;
|
||||||
@@ -18,7 +20,18 @@ export function init({ grid, gridContainer, dragGhostEl, state, callbacks, fileE
|
|||||||
_fileExplorerEl = fileExplorerEl;
|
_fileExplorerEl = fileExplorerEl;
|
||||||
_onFolderDrop = onFolderDrop;
|
_onFolderDrop = onFolderDrop;
|
||||||
|
|
||||||
|
_placeholder = document.createElement('div');
|
||||||
|
_placeholder.className = 'card card-placeholder';
|
||||||
|
const phLabel = document.createElement('div');
|
||||||
|
phLabel.className = 'card-placeholder-label';
|
||||||
|
_placeholder.appendChild(phLabel);
|
||||||
|
_placeholder.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; });
|
||||||
|
_placeholder.addEventListener('dragenter', e => e.preventDefault());
|
||||||
|
_placeholder.addEventListener('drop', e => { e.preventDefault(); _performDrop(); });
|
||||||
|
|
||||||
window.addEventListener('dragover', _onWindowDragOver);
|
window.addEventListener('dragover', _onWindowDragOver);
|
||||||
|
grid.addEventListener('dragover', _onGridDragOver);
|
||||||
|
grid.addEventListener('drop', _onGridDrop);
|
||||||
|
|
||||||
if (fileExplorerEl) {
|
if (fileExplorerEl) {
|
||||||
fileExplorerEl.addEventListener('dragover', _onExplorerDragOver);
|
fileExplorerEl.addEventListener('dragover', _onExplorerDragOver);
|
||||||
@@ -27,7 +40,6 @@ export function init({ grid, gridContainer, dragGhostEl, state, callbacks, fileE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 카드 생성 시 호출해 드래그 이벤트를 부착
|
|
||||||
export function setupCard(card) {
|
export function setupCard(card) {
|
||||||
card.addEventListener('dragstart', _onDragStart);
|
card.addEventListener('dragstart', _onDragStart);
|
||||||
card.addEventListener('dragover', _onDragOver);
|
card.addEventListener('dragover', _onDragOver);
|
||||||
@@ -53,20 +65,37 @@ function _onDragStart(e) {
|
|||||||
if (_state.selectedIdxs.has(idx) && _state.selectedIdxs.size > 1) {
|
if (_state.selectedIdxs.has(idx) && _state.selectedIdxs.size > 1) {
|
||||||
dragMode = 'multi';
|
dragMode = 'multi';
|
||||||
dragSrcIdx = idx;
|
dragSrcIdx = idx;
|
||||||
_state.selectedIdxs.forEach(i => {
|
|
||||||
const c = _getCardEl(i);
|
const count = _state.selectedIdxs.size;
|
||||||
if (c) c.classList.add('multi-dragging');
|
const toHide = [..._state.selectedIdxs].sort((a, b) => a - b);
|
||||||
});
|
_placeholder.querySelector('.card-placeholder-label').textContent = `${count}개`;
|
||||||
_showGhost(e, `📦 ${_state.selectedIdxs.size}개 이동 중`);
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||||
e.dataTransfer.setDragImage(img, 0, 0);
|
e.dataTransfer.setDragImage(img, 0, 0);
|
||||||
|
_showGhost(e, `📦 ${count}개 이동 중`);
|
||||||
|
|
||||||
|
// 브라우저가 드래그 이미지를 캡처한 뒤 카드 숨기고 플레이스홀더 삽입
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const firstCard = _getCardEl(toHide[0]);
|
||||||
|
if (firstCard) _grid.insertBefore(_placeholder, firstCard);
|
||||||
|
toHide.forEach(i => {
|
||||||
|
const c = _getCardEl(i);
|
||||||
|
if (c) c.classList.add('drag-hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
dragMode = 'single';
|
dragMode = 'single';
|
||||||
dragSrcIdx = idx;
|
dragSrcIdx = idx;
|
||||||
this.classList.add('dragging');
|
_placeholder.querySelector('.card-placeholder-label').textContent = '1개';
|
||||||
_state.selectedIdxs.clear();
|
_state.selectedIdxs.clear();
|
||||||
_cb.refreshCardSelection();
|
_cb.refreshCardSelection();
|
||||||
|
|
||||||
|
const srcCard = this;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
_grid.insertBefore(_placeholder, srcCard);
|
||||||
|
srcCard.classList.add('drag-hidden');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
@@ -80,26 +109,40 @@ function _onDragOver(e) {
|
|||||||
|
|
||||||
function _onDragEnter(e) {
|
function _onDragEnter(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const idx = parseInt(this.dataset.idx);
|
const idx = parseInt(this.dataset.idx);
|
||||||
const isSrc = dragMode === 'single' ? idx === dragSrcIdx : _state.selectedIdxs.has(idx);
|
const isSrc = dragMode === 'single' ? idx === dragSrcIdx : _state.selectedIdxs.has(idx);
|
||||||
if (!isSrc) this.classList.add('drag-over');
|
if (isSrc) return;
|
||||||
|
|
||||||
|
_dropTargetIdx = idx;
|
||||||
|
|
||||||
|
// 플레이스홀더를 삽입될 위치로 이동
|
||||||
|
if (dragMode === 'single' && dragSrcIdx < idx) {
|
||||||
|
_grid.insertBefore(_placeholder, this.nextSibling); // 오른쪽으로 이동: 타겟 뒤
|
||||||
|
} else {
|
||||||
|
_grid.insertBefore(_placeholder, this); // 왼쪽 이동 또는 multi: 타겟 앞
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _onDragLeave() {
|
function _onDragLeave() {
|
||||||
this.classList.remove('drag-over');
|
// 플레이스홀더가 위치를 표시하므로 별도 처리 없음
|
||||||
}
|
}
|
||||||
|
|
||||||
function _onDrop(e) {
|
function _onDrop(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.classList.remove('drag-over');
|
_dropTargetIdx = parseInt(this.dataset.idx);
|
||||||
const targetIdx = parseInt(this.dataset.idx);
|
_performDrop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _performDrop() {
|
||||||
|
if (_dropTargetIdx === null) return;
|
||||||
if (dragMode === 'single') {
|
if (dragMode === 'single') {
|
||||||
if (dragSrcIdx === null || dragSrcIdx === targetIdx) return;
|
if (dragSrcIdx !== null && dragSrcIdx !== _dropTargetIdx) {
|
||||||
_moveSingle(dragSrcIdx, targetIdx);
|
_moveSingle(dragSrcIdx, _dropTargetIdx);
|
||||||
|
}
|
||||||
} else if (dragMode === 'multi') {
|
} else if (dragMode === 'multi') {
|
||||||
if (_state.selectedIdxs.has(targetIdx)) return;
|
if (!_state.selectedIdxs.has(_dropTargetIdx)) {
|
||||||
_moveMulti(targetIdx);
|
_moveMulti(_dropTargetIdx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +150,11 @@ function _onDragEnd() {
|
|||||||
_stopScroll();
|
_stopScroll();
|
||||||
_hideGhost();
|
_hideGhost();
|
||||||
_clearTreeHover();
|
_clearTreeHover();
|
||||||
|
// 플레이스홀더 제거 및 숨긴 카드 복원 (_moveSingle/_moveMulti 에서 이미 처리했으면 no-op)
|
||||||
|
if (_placeholder.parentNode) _placeholder.parentNode.removeChild(_placeholder);
|
||||||
|
_dropTargetIdx = null;
|
||||||
document.querySelectorAll('.card').forEach(c =>
|
document.querySelectorAll('.card').forEach(c =>
|
||||||
c.classList.remove('dragging', 'multi-dragging', 'drag-over')
|
c.classList.remove('drag-hidden', 'drag-over')
|
||||||
);
|
);
|
||||||
dragMode = null;
|
dragMode = null;
|
||||||
dragSrcIdx = null;
|
dragSrcIdx = null;
|
||||||
@@ -171,22 +217,53 @@ function _clearTreeHover() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 그리드 빈 공간 드래그 핸들러 ─────────────────────
|
||||||
|
|
||||||
|
function _onGridDragOver(e) {
|
||||||
|
if (!dragMode) return;
|
||||||
|
if (e.target.closest('.card')) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
|
||||||
|
_dropTargetIdx = _state.files.length;
|
||||||
|
if (_grid.lastElementChild !== _placeholder) {
|
||||||
|
_grid.appendChild(_placeholder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onGridDrop(e) {
|
||||||
|
if (!dragMode) return;
|
||||||
|
if (e.target.closest('.card')) return;
|
||||||
|
e.preventDefault();
|
||||||
|
_dropTargetIdx = _state.files.length;
|
||||||
|
_performDrop();
|
||||||
|
}
|
||||||
|
|
||||||
// ── 이동 로직 ─────────────────────────────────────
|
// ── 이동 로직 ─────────────────────────────────────
|
||||||
|
|
||||||
function _moveSingle(srcIdx, targetIdx) {
|
function _moveSingle(srcIdx, targetIdx) {
|
||||||
const [moved] = _state.files.splice(srcIdx, 1);
|
const [moved] = _state.files.splice(srcIdx, 1);
|
||||||
_state.files.splice(targetIdx, 0, moved);
|
_state.files.splice(targetIdx, 0, moved);
|
||||||
|
|
||||||
const cards = Array.from(_grid.children);
|
// 플레이스홀더 제거 후 소스 카드 복원
|
||||||
const src = cards[srcIdx];
|
if (_placeholder.parentNode) _placeholder.parentNode.removeChild(_placeholder);
|
||||||
const target = cards[targetIdx];
|
const srcCard = _getCardEl(srcIdx);
|
||||||
if (srcIdx < targetIdx) {
|
const targetCard = _getCardEl(targetIdx);
|
||||||
_grid.insertBefore(src, target.nextSibling);
|
if (srcCard) srcCard.classList.remove('drag-hidden');
|
||||||
} else {
|
|
||||||
_grid.insertBefore(src, target);
|
|
||||||
}
|
|
||||||
Array.from(_grid.children).forEach((c, i) => { c.dataset.idx = i; });
|
|
||||||
|
|
||||||
|
if (srcCard) {
|
||||||
|
if (targetCard) {
|
||||||
|
if (srcIdx < targetIdx) {
|
||||||
|
_grid.insertBefore(srcCard, targetCard.nextSibling);
|
||||||
|
} else {
|
||||||
|
_grid.insertBefore(srcCard, targetCard);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_grid.appendChild(srcCard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.from(_grid.children).forEach((c, i) => { c.dataset.idx = i; });
|
||||||
_cb.updateNumberBadges();
|
_cb.updateNumberBadges();
|
||||||
_cb.updateNewNameLabels();
|
_cb.updateNewNameLabels();
|
||||||
}
|
}
|
||||||
@@ -207,9 +284,13 @@ function _moveMulti(targetIdx) {
|
|||||||
for (let i = 0; i < movedFiles.length; i++) newSel.add(adjustedTarget + i);
|
for (let i = 0; i < movedFiles.length; i++) newSel.add(adjustedTarget + i);
|
||||||
_state.selectedIdxs = newSel;
|
_state.selectedIdxs = newSel;
|
||||||
|
|
||||||
// DOM 재정렬 (이미지 재로딩 없이)
|
// 플레이스홀더 제거, 숨긴 카드 복원 후 DOM 재정렬
|
||||||
|
if (_placeholder.parentNode) _placeholder.parentNode.removeChild(_placeholder);
|
||||||
const byPath = new Map();
|
const byPath = new Map();
|
||||||
Array.from(_grid.children).forEach(c => byPath.set(c.dataset.filepath, c));
|
Array.from(_grid.children).forEach(c => {
|
||||||
|
c.classList.remove('drag-hidden');
|
||||||
|
byPath.set(c.dataset.filepath, c);
|
||||||
|
});
|
||||||
_grid.innerHTML = '';
|
_grid.innerHTML = '';
|
||||||
_state.files.forEach((f, idx) => {
|
_state.files.forEach((f, idx) => {
|
||||||
const c = byPath.get(f.path);
|
const c = byPath.get(f.path);
|
||||||
|
|||||||
@@ -243,6 +243,23 @@ main {
|
|||||||
.card.dragging,
|
.card.dragging,
|
||||||
.card.multi-dragging { opacity: .3; transform: scale(.96); cursor: grabbing; }
|
.card.multi-dragging { opacity: .3; transform: scale(.96); cursor: grabbing; }
|
||||||
.card.drag-over { border-color: var(--accent) !important; background: var(--drag-over); }
|
.card.drag-over { border-color: var(--accent) !important; background: var(--drag-over); }
|
||||||
|
.card.drag-hidden { display: none; }
|
||||||
|
|
||||||
|
.card-placeholder {
|
||||||
|
border: 2px dashed var(--accent);
|
||||||
|
background: rgba(124, 106, 247, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.card-placeholder-label {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── 썸네일: 정사각형 + object-fit: contain ── */
|
/* ── 썸네일: 정사각형 + object-fit: contain ── */
|
||||||
.card-thumb {
|
.card-thumb {
|
||||||
|
|||||||
Reference in New Issue
Block a user