파일 드래그시 보여지는 세로 seperator 개선
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
let _grid, _gridContainer, _dragGhostEl, _fileExplorerEl;
|
let _grid, _gridContainer, _dragGhostEl, _fileExplorerEl;
|
||||||
let _state, _cb, _onFolderDrop;
|
let _state, _cb, _onFolderDrop;
|
||||||
let _placeholder = null;
|
let _dropSeparator = null;
|
||||||
let _originPlaceholders = [];
|
let _originPlaceholders = [];
|
||||||
let _dropTargetIdx = null;
|
let _dropTargetIdx = null;
|
||||||
|
|
||||||
@@ -21,14 +21,9 @@ export function init({ grid, gridContainer, dragGhostEl, state, callbacks, fileE
|
|||||||
_fileExplorerEl = fileExplorerEl;
|
_fileExplorerEl = fileExplorerEl;
|
||||||
_onFolderDrop = onFolderDrop;
|
_onFolderDrop = onFolderDrop;
|
||||||
|
|
||||||
_placeholder = document.createElement('div');
|
_dropSeparator = document.createElement('div');
|
||||||
_placeholder.className = 'card card-placeholder';
|
_dropSeparator.className = 'drop-separator';
|
||||||
const phLabel = document.createElement('div');
|
_gridContainer.appendChild(_dropSeparator);
|
||||||
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('dragover', _onGridDragOver);
|
||||||
@@ -69,7 +64,6 @@ function _onDragStart(e) {
|
|||||||
|
|
||||||
const count = _state.selectedIdxs.size;
|
const count = _state.selectedIdxs.size;
|
||||||
const toHide = [..._state.selectedIdxs].sort((a, b) => a - b);
|
const toHide = [..._state.selectedIdxs].sort((a, b) => a - b);
|
||||||
_placeholder.querySelector('.card-placeholder-label').textContent = `${count}개`;
|
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||||
@@ -81,7 +75,7 @@ function _onDragStart(e) {
|
|||||||
toHide.forEach(i => {
|
toHide.forEach(i => {
|
||||||
const c = _getCardEl(i);
|
const c = _getCardEl(i);
|
||||||
if (c) {
|
if (c) {
|
||||||
const originPh = _createOriginPlaceholder();
|
const originPh = _createOriginPlaceholder(i);
|
||||||
_grid.insertBefore(originPh, c);
|
_grid.insertBefore(originPh, c);
|
||||||
_originPlaceholders.push(originPh);
|
_originPlaceholders.push(originPh);
|
||||||
c.classList.add('drag-hidden');
|
c.classList.add('drag-hidden');
|
||||||
@@ -91,13 +85,12 @@ function _onDragStart(e) {
|
|||||||
} else {
|
} else {
|
||||||
dragMode = 'single';
|
dragMode = 'single';
|
||||||
dragSrcIdx = idx;
|
dragSrcIdx = idx;
|
||||||
_placeholder.querySelector('.card-placeholder-label').textContent = '1개';
|
|
||||||
_state.selectedIdxs.clear();
|
_state.selectedIdxs.clear();
|
||||||
_cb.refreshCardSelection();
|
_cb.refreshCardSelection();
|
||||||
|
|
||||||
const srcCard = this;
|
const srcCard = this;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const originPh = _createOriginPlaceholder();
|
const originPh = _createOriginPlaceholder(idx);
|
||||||
_grid.insertBefore(originPh, srcCard);
|
_grid.insertBefore(originPh, srcCard);
|
||||||
_originPlaceholders.push(originPh);
|
_originPlaceholders.push(originPh);
|
||||||
srcCard.classList.add('drag-hidden');
|
srcCard.classList.add('drag-hidden');
|
||||||
@@ -111,25 +104,21 @@ function _onDragStart(e) {
|
|||||||
function _onDragOver(e) {
|
function _onDragOver(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.dataTransfer.dropEffect = 'move';
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
_updateDropTarget(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _onDragEnter(e) {
|
function _onDragEnter(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const idx = parseInt(this.dataset.idx);
|
_updateDropTarget(e);
|
||||||
const isSrc = dragMode === 'single' ? idx === dragSrcIdx : _state.selectedIdxs.has(idx);
|
|
||||||
if (isSrc) return;
|
|
||||||
|
|
||||||
_dropTargetIdx = idx;
|
|
||||||
_grid.insertBefore(_placeholder, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _onDragLeave() {
|
function _onDragLeave() {
|
||||||
// 플레이스홀더가 위치를 표시하므로 별도 처리 없음
|
// separator 위치는 다음 dragover에서 갱신한다.
|
||||||
}
|
}
|
||||||
|
|
||||||
function _onDrop(e) {
|
function _onDrop(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
_dropTargetIdx = parseInt(this.dataset.idx);
|
_updateDropTarget(e);
|
||||||
_performDrop();
|
_performDrop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,9 +138,9 @@ function _performDrop() {
|
|||||||
function _onDragEnd() {
|
function _onDragEnd() {
|
||||||
_stopScroll();
|
_stopScroll();
|
||||||
_hideGhost();
|
_hideGhost();
|
||||||
|
_hideDropSeparator();
|
||||||
_clearTreeHover();
|
_clearTreeHover();
|
||||||
_clearOriginPlaceholders();
|
_clearOriginPlaceholders();
|
||||||
if (_placeholder.parentNode) _placeholder.parentNode.removeChild(_placeholder);
|
|
||||||
_dropTargetIdx = null;
|
_dropTargetIdx = null;
|
||||||
document.querySelectorAll('.card').forEach(c =>
|
document.querySelectorAll('.card').forEach(c =>
|
||||||
c.classList.remove('drag-hidden', 'drag-over')
|
c.classList.remove('drag-hidden', 'drag-over')
|
||||||
@@ -224,18 +213,15 @@ function _onGridDragOver(e) {
|
|||||||
if (e.target.closest('.card')) return;
|
if (e.target.closest('.card')) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.dataTransfer.dropEffect = 'move';
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
_updateDropTarget(e);
|
||||||
_dropTargetIdx = _state.files.length;
|
|
||||||
if (_grid.lastElementChild !== _placeholder) {
|
|
||||||
_grid.appendChild(_placeholder);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _onGridDrop(e) {
|
function _onGridDrop(e) {
|
||||||
if (!dragMode) return;
|
if (!dragMode) return;
|
||||||
if (e.target.closest('.card')) return;
|
if (e.target.closest('.card')) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
_dropTargetIdx = null; // 빈 영역 드롭 → 이동 없이 복원
|
_updateDropTarget(e);
|
||||||
|
_performDrop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 이동 로직 ─────────────────────────────────────
|
// ── 이동 로직 ─────────────────────────────────────
|
||||||
@@ -245,7 +231,7 @@ function _moveSingle(srcIdx, targetIdx) {
|
|||||||
const insertAt = srcIdx < targetIdx ? targetIdx - 1 : targetIdx;
|
const insertAt = srcIdx < targetIdx ? targetIdx - 1 : targetIdx;
|
||||||
_state.files.splice(insertAt, 0, moved);
|
_state.files.splice(insertAt, 0, moved);
|
||||||
|
|
||||||
if (_placeholder.parentNode) _placeholder.parentNode.removeChild(_placeholder);
|
_hideDropSeparator();
|
||||||
_clearOriginPlaceholders();
|
_clearOriginPlaceholders();
|
||||||
const srcCard = _getCardEl(srcIdx);
|
const srcCard = _getCardEl(srcIdx);
|
||||||
const targetCard = _getCardEl(targetIdx);
|
const targetCard = _getCardEl(targetIdx);
|
||||||
@@ -281,7 +267,7 @@ 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;
|
||||||
|
|
||||||
if (_placeholder.parentNode) _placeholder.parentNode.removeChild(_placeholder);
|
_hideDropSeparator();
|
||||||
_clearOriginPlaceholders();
|
_clearOriginPlaceholders();
|
||||||
const byPath = new Map();
|
const byPath = new Map();
|
||||||
Array.from(_grid.children).forEach(c => {
|
Array.from(_grid.children).forEach(c => {
|
||||||
@@ -364,14 +350,197 @@ function _getCardEl(idx) {
|
|||||||
return _grid.querySelector(`.card[data-idx="${idx}"]`);
|
return _grid.querySelector(`.card[data-idx="${idx}"]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _createOriginPlaceholder() {
|
// ── 드롭 위치 표시 ────────────────────────────────
|
||||||
|
|
||||||
|
function _updateDropTarget(e) {
|
||||||
|
const target = _getDropTargetFromPoint(e.clientX, e.clientY);
|
||||||
|
if (!target) {
|
||||||
|
_dropTargetIdx = null;
|
||||||
|
_hideDropSeparator();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dropTargetIdx = target.idx;
|
||||||
|
_showDropSeparator(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getDropTargetFromPoint(clientX, clientY) {
|
||||||
|
const items = _getVisualGridItems();
|
||||||
|
if (items.length === 0) return null;
|
||||||
|
const rows = _getGridRows(items);
|
||||||
|
|
||||||
|
const hovered = items.find(item =>
|
||||||
|
clientX >= item.rect.left &&
|
||||||
|
clientX <= item.rect.right &&
|
||||||
|
clientY >= item.rect.top &&
|
||||||
|
clientY <= item.rect.bottom
|
||||||
|
);
|
||||||
|
if (hovered) {
|
||||||
|
const hoveredRow = _getRowForItem(rows, hovered);
|
||||||
|
if (!hoveredRow) return null;
|
||||||
|
|
||||||
|
if (clientX <= hovered.rect.left + hovered.rect.width / 2) {
|
||||||
|
return _makeBoundaryDropTarget(hoveredRow, hovered.idx);
|
||||||
|
}
|
||||||
|
return _makeBoundaryDropTarget(hoveredRow, hovered.idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = rows.find(r => clientY >= r.top && clientY <= r.bottom) ||
|
||||||
|
_getNearestRow(rows, clientY);
|
||||||
|
if (!row) return null;
|
||||||
|
|
||||||
|
if (clientX <= row.items[0].rect.left) {
|
||||||
|
return _makeBoundaryDropTarget(row, row.items[0].idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = row.items[row.items.length - 1];
|
||||||
|
if (clientX >= last.rect.right) {
|
||||||
|
return _makeBoundaryDropTarget(row, last.idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < row.items.length - 1; i++) {
|
||||||
|
const left = row.items[i];
|
||||||
|
const right = row.items[i + 1];
|
||||||
|
if (clientX > left.rect.right && clientX < right.rect.left) {
|
||||||
|
return _makeBoundaryDropTarget(row, right.idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getVisualGridItems() {
|
||||||
|
return Array.from(_grid.children)
|
||||||
|
.filter(el => !_isHiddenDragCard(el))
|
||||||
|
.map(el => {
|
||||||
|
const idx = el.classList.contains('card-origin-placeholder')
|
||||||
|
? parseInt(el.dataset.originIdx)
|
||||||
|
: parseInt(el.dataset.idx);
|
||||||
|
if (!Number.isFinite(idx)) return null;
|
||||||
|
return { el, idx, rect: el.getBoundingClientRect() };
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => (a.rect.top - b.rect.top) || (a.rect.left - b.rect.left));
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isHiddenDragCard(el) {
|
||||||
|
return el.classList.contains('drag-hidden') || getComputedStyle(el).display === 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getGridRows(items) {
|
||||||
|
const rows = [];
|
||||||
|
for (const item of items) {
|
||||||
|
const row = rows.find(r => Math.abs(r.top - item.rect.top) < 4);
|
||||||
|
if (row) {
|
||||||
|
row.items.push(item);
|
||||||
|
row.top = Math.min(row.top, item.rect.top);
|
||||||
|
row.bottom = Math.max(row.bottom, item.rect.bottom);
|
||||||
|
} else {
|
||||||
|
rows.push({
|
||||||
|
top: item.rect.top,
|
||||||
|
bottom: item.rect.bottom,
|
||||||
|
items: [item]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.map(row => {
|
||||||
|
row.items.sort((a, b) => a.rect.left - b.rect.left);
|
||||||
|
row.rect = {
|
||||||
|
top: row.top,
|
||||||
|
bottom: row.bottom,
|
||||||
|
height: row.bottom - row.top
|
||||||
|
};
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getNearestRow(rows, clientY) {
|
||||||
|
if (rows.length === 0) return null;
|
||||||
|
return rows.reduce((nearest, row) => {
|
||||||
|
const rowDistance = clientY < row.top ? row.top - clientY : clientY - row.bottom;
|
||||||
|
const nearestDistance = clientY < nearest.top ? nearest.top - clientY : clientY - nearest.bottom;
|
||||||
|
return rowDistance < nearestDistance ? row : nearest;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getRowForItem(rows, item) {
|
||||||
|
return rows.find(row => row.items.includes(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function _makeBoundaryDropTarget(row, idx) {
|
||||||
|
const clampedIdx = Math.max(0, Math.min(_state.files.length, idx));
|
||||||
|
const rightIndex = row.items.findIndex(item => item.idx >= clampedIdx);
|
||||||
|
const gap = _getRowGap(row);
|
||||||
|
|
||||||
|
let clientX;
|
||||||
|
if (rightIndex === -1) {
|
||||||
|
const last = row.items[row.items.length - 1];
|
||||||
|
clientX = last.rect.right + gap / 2;
|
||||||
|
} else if (rightIndex <= 0) {
|
||||||
|
clientX = row.items[0].rect.left - gap / 2;
|
||||||
|
} else if (rightIndex >= row.items.length) {
|
||||||
|
const last = row.items[row.items.length - 1];
|
||||||
|
clientX = last.rect.right + gap / 2;
|
||||||
|
} else {
|
||||||
|
const left = row.items[rightIndex - 1];
|
||||||
|
const right = row.items[rightIndex];
|
||||||
|
clientX = (left.rect.right + right.rect.left) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _makeDropTarget(clampedIdx, clientX, row.rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getRowGap(row) {
|
||||||
|
const gaps = [];
|
||||||
|
for (let i = 0; i < row.items.length - 1; i++) {
|
||||||
|
const gap = row.items[i + 1].rect.left - row.items[i].rect.right;
|
||||||
|
if (gap > 0) gaps.push(gap);
|
||||||
|
}
|
||||||
|
if (gaps.length > 0) return Math.min(...gaps);
|
||||||
|
|
||||||
|
const style = getComputedStyle(_grid);
|
||||||
|
return parseFloat(style.columnGap || style.gap) || 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _makeDropTarget(idx, clientX, rect) {
|
||||||
|
return {
|
||||||
|
idx: Math.max(0, Math.min(_state.files.length, idx)),
|
||||||
|
clientX,
|
||||||
|
clientY: rect.top,
|
||||||
|
height: rect.height || (rect.bottom - rect.top)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function _showDropSeparator(target) {
|
||||||
|
const containerRect = _gridContainer.getBoundingClientRect();
|
||||||
|
_dropSeparator.style.display = 'block';
|
||||||
|
_dropSeparator.style.left = `${target.clientX - containerRect.left - 1}px`;
|
||||||
|
_dropSeparator.style.top = `${target.clientY - containerRect.top + _gridContainer.scrollTop}px`;
|
||||||
|
_dropSeparator.style.height = `${target.height}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hideDropSeparator() {
|
||||||
|
if (_dropSeparator) _dropSeparator.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _createOriginPlaceholder(idx) {
|
||||||
const ph = document.createElement('div');
|
const ph = document.createElement('div');
|
||||||
ph.className = 'card card-origin-placeholder';
|
ph.className = 'card card-origin-placeholder';
|
||||||
ph.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; });
|
ph.dataset.originIdx = idx;
|
||||||
ph.addEventListener('dragenter', e => e.preventDefault());
|
ph.addEventListener('dragover', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
_updateDropTarget(e);
|
||||||
|
});
|
||||||
|
ph.addEventListener('dragenter', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
_updateDropTarget(e);
|
||||||
|
});
|
||||||
ph.addEventListener('drop', e => {
|
ph.addEventListener('drop', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
_dropTargetIdx = null; // 원본 위치에 드롭 → 이동 없이 복원
|
_updateDropTarget(e);
|
||||||
|
_performDrop();
|
||||||
});
|
});
|
||||||
return ph;
|
return ph;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-14
@@ -251,20 +251,16 @@ main {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-placeholder {
|
.drop-separator {
|
||||||
border: 2px dashed var(--accent);
|
position: absolute;
|
||||||
background: rgba(124, 106, 247, 0.08);
|
z-index: 20;
|
||||||
display: flex;
|
display: none;
|
||||||
align-items: center;
|
width: 3px;
|
||||||
justify-content: center;
|
min-height: 48px;
|
||||||
border-radius: 8px;
|
border-radius: 999px;
|
||||||
pointer-events: auto;
|
background: var(--accent);
|
||||||
}
|
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16), 0 0 16px rgba(124, 106, 247, 0.55);
|
||||||
.card-placeholder-label {
|
pointer-events: none;
|
||||||
color: var(--accent);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 썸네일: 정사각형 + object-fit: contain ── */
|
/* ── 썸네일: 정사각형 + object-fit: contain ── */
|
||||||
|
|||||||
Reference in New Issue
Block a user