팝업창 제목 스타일 적용, 단축키 추가
This commit is contained in:
@@ -2,17 +2,16 @@ import { state } from './state.js';
|
||||
import { setStatus, showToast } from './ui.js';
|
||||
import { renderGrid, showEmptyGrid } from './grid.js';
|
||||
import * as history from './history.js';
|
||||
import { openModal, closeModal } from './modal.js';
|
||||
|
||||
const deleteModal = document.getElementById('delete-modal');
|
||||
const deleteFileList = document.getElementById('delete-file-list');
|
||||
const deleteCancelBtn = document.getElementById('delete-cancel');
|
||||
const deleteConfirmBtn = document.getElementById('delete-confirm');
|
||||
const btnRename = document.getElementById('btn-rename');
|
||||
|
||||
let _pendingFiles = null;
|
||||
|
||||
deleteCancelBtn.addEventListener('click', () => {
|
||||
deleteModal.style.display = 'none';
|
||||
deleteModal.addEventListener('modal:closed', () => {
|
||||
_pendingFiles = null;
|
||||
});
|
||||
|
||||
@@ -20,7 +19,7 @@ deleteConfirmBtn.addEventListener('click', async () => {
|
||||
if (!_pendingFiles) return;
|
||||
const files = _pendingFiles;
|
||||
_pendingFiles = null;
|
||||
deleteModal.style.display = 'none';
|
||||
closeModal(deleteModal);
|
||||
await _doDelete(files);
|
||||
});
|
||||
|
||||
@@ -35,7 +34,7 @@ export function deleteSelected() {
|
||||
return li;
|
||||
}));
|
||||
_pendingFiles = filesToDelete;
|
||||
deleteModal.style.display = 'flex';
|
||||
openModal(deleteModal);
|
||||
}
|
||||
|
||||
export function init(gridEl) {
|
||||
@@ -63,7 +62,7 @@ export function init(gridEl) {
|
||||
return li;
|
||||
}));
|
||||
_pendingFiles = filesToDelete;
|
||||
deleteModal.style.display = 'flex';
|
||||
openModal(deleteModal);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+18
-13
@@ -79,58 +79,63 @@
|
||||
<div id="toast"></div>
|
||||
|
||||
<div id="move-modal" class="modal-overlay" style="display:none;">
|
||||
<div class="modal-box">
|
||||
<div class="modal-box modal-box-with-title">
|
||||
<div class="modal-titlebar">이미지 이동</div>
|
||||
<p class="modal-msg">대상 폴더에 이미 같은 이름의 파일이 존재합니다.<br>새로운 이름으로 파일들을 이동하시겠습니까?</p>
|
||||
<div class="modal-fields">
|
||||
<label>파일명 포맷<input type="text" id="move-prefix" value="photo" placeholder="접두사" /></label>
|
||||
<label>자리 순번 숫자<input type="number" id="move-digits" value="4" min="1" max="6" /></label>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button id="move-cancel" class="btn btn-ghost">취소</button>
|
||||
<button id="move-confirm" class="btn btn-success">실행</button>
|
||||
<button id="move-cancel" class="btn btn-ghost modal-close">취소</button>
|
||||
<button id="move-confirm" class="btn btn-success modal-action">실행</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rename-confirm-modal" class="modal-overlay" style="display:none;">
|
||||
<div class="modal-box modal-box-with-title">
|
||||
<div class="modal-titlebar" id="rename-confirm-title"></div>
|
||||
<div class="modal-titlebar rename-title-selected" hidden>선택된 이미지 저장</div>
|
||||
<div class="modal-titlebar rename-title-all">전체 이미지 저장</div>
|
||||
<p class="modal-msg" id="rename-confirm-msg"></p>
|
||||
<ul id="rename-confirm-list" class="modal-conflict-list modal-preview-list"></ul>
|
||||
<p class="modal-warning" id="rename-confirm-selection-warning" style="display:none;">선택된 파일들만 저장합니다</p>
|
||||
<div class="modal-actions">
|
||||
<button id="rename-confirm-cancel" class="btn btn-ghost">취소</button>
|
||||
<button id="rename-confirm-ok" class="btn btn-success">실행</button>
|
||||
<button id="rename-confirm-cancel" class="btn btn-ghost modal-close">취소</button>
|
||||
<button id="rename-confirm-ok" class="btn btn-success modal-action">실행</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rename-conflict-modal" class="modal-overlay" style="display:none;">
|
||||
<div class="modal-box">
|
||||
<div class="modal-box modal-box-with-title">
|
||||
<div class="modal-titlebar modal-titlebar-warning">이름 충돌</div>
|
||||
<p class="modal-msg">변경할 이름의 파일이 이미 존재합니다.</p>
|
||||
<ul id="rename-conflict-list" class="modal-conflict-list"></ul>
|
||||
<div class="modal-actions">
|
||||
<button id="rename-conflict-close" class="btn btn-ghost">닫기</button>
|
||||
<button id="rename-conflict-close" class="btn btn-ghost modal-close">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="delete-modal" class="modal-overlay" style="display:none;">
|
||||
<div class="modal-box">
|
||||
<div class="modal-box modal-box-with-title">
|
||||
<div class="modal-titlebar modal-titlebar-danger">이미지 삭제</div>
|
||||
<p class="modal-msg">아래 파일을 삭제합니다.</p>
|
||||
<ul id="delete-file-list" class="modal-conflict-list"></ul>
|
||||
<div class="modal-actions">
|
||||
<button id="delete-cancel" class="btn btn-ghost">취소</button>
|
||||
<button id="delete-confirm" class="btn btn-warning">확인</button>
|
||||
<button id="delete-cancel" class="btn btn-ghost modal-close">취소</button>
|
||||
<button id="delete-confirm" class="btn btn-success modal-action">실행</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="move-error-modal" class="modal-overlay" style="display:none;">
|
||||
<div class="modal-box">
|
||||
<div class="modal-box modal-box-with-title">
|
||||
<div class="modal-titlebar modal-titlebar-warning">이동 오류</div>
|
||||
<p class="modal-msg">새로운 이름의 파일도 존재합니다.</p>
|
||||
<div class="modal-actions">
|
||||
<button id="move-error-close" class="btn btn-ghost">닫기</button>
|
||||
<button id="move-error-close" class="btn btn-ghost modal-close">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
export function initModalControls() {
|
||||
document.addEventListener('click', e => {
|
||||
const closeBtn = e.target.closest('.modal-close');
|
||||
if (!closeBtn) return;
|
||||
|
||||
const modal = closeBtn.closest('.modal-overlay');
|
||||
if (!modal) return;
|
||||
|
||||
closeModal(modal);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
const modal = getActiveModal();
|
||||
if (!modal) return;
|
||||
|
||||
if (e.code === 'Escape') {
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
if (!closeBtn) return;
|
||||
e.preventDefault();
|
||||
closeBtn.click();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.code === 'Enter') {
|
||||
const actionBtn = modal.querySelector('.modal-action');
|
||||
if (!actionBtn) return;
|
||||
e.preventDefault();
|
||||
actionBtn.click();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.code === 'Tab') {
|
||||
trapFocus(e, modal);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function openModal(modal) {
|
||||
modal.style.display = 'flex';
|
||||
const primaryBtn = modal.querySelector('.modal-action');
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
(primaryBtn || closeBtn)?.focus();
|
||||
}
|
||||
|
||||
export function closeModal(modal) {
|
||||
modal.style.display = 'none';
|
||||
modal.dispatchEvent(new CustomEvent('modal:closed'));
|
||||
}
|
||||
|
||||
function getActiveModal() {
|
||||
const modals = [...document.querySelectorAll('.modal-overlay')];
|
||||
return modals.find(modal => modal.style.display === 'flex') || null;
|
||||
}
|
||||
|
||||
function trapFocus(e, modal) {
|
||||
const focusable = getFocusableElements(modal);
|
||||
if (!focusable.length) {
|
||||
e.preventDefault();
|
||||
modal.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
const active = document.activeElement;
|
||||
|
||||
if (!modal.contains(active)) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey && active === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && active === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function getFocusableElements(container) {
|
||||
const selector = [
|
||||
'button:not([disabled])',
|
||||
'input:not([disabled])',
|
||||
'select:not([disabled])',
|
||||
'textarea:not([disabled])',
|
||||
'a[href]',
|
||||
'[tabindex]:not([tabindex="-1"])',
|
||||
].join(',');
|
||||
|
||||
return [...container.querySelectorAll(selector)]
|
||||
.filter(el => !el.hidden && el.offsetParent !== null);
|
||||
}
|
||||
+13
-18
@@ -2,53 +2,48 @@ import { state } from './state.js';
|
||||
import { showLoading, hideLoading, setStatus, showToast } from './ui.js';
|
||||
import { renderGrid, updateSelCount, showEmptyGrid } from './grid.js';
|
||||
import * as history from './history.js';
|
||||
import { openModal, closeModal } from './modal.js';
|
||||
|
||||
const btnRename = document.getElementById('btn-rename');
|
||||
const moveModal = document.getElementById('move-modal');
|
||||
const movePrefixInput = document.getElementById('move-prefix');
|
||||
const moveDigitsInput = document.getElementById('move-digits');
|
||||
const moveCancelBtn = document.getElementById('move-cancel');
|
||||
const moveConfirmBtn = document.getElementById('move-confirm');
|
||||
const moveErrorModal = document.getElementById('move-error-modal');
|
||||
const moveErrorCloseBtn = document.getElementById('move-error-close');
|
||||
|
||||
let _pendingMoveTarget = null;
|
||||
let _pendingMoveFiles = null;
|
||||
|
||||
moveCancelBtn.addEventListener('click', () => {
|
||||
moveModal.style.display = 'none';
|
||||
moveModal.addEventListener('modal:closed', () => {
|
||||
_pendingMoveTarget = null;
|
||||
_pendingMoveFiles = null;
|
||||
});
|
||||
|
||||
moveConfirmBtn.addEventListener('click', async () => {
|
||||
if (!_pendingMoveTarget || !_pendingMoveFiles) return;
|
||||
|
||||
const prefix = movePrefixInput.value.trim() || 'photo';
|
||||
const digits = Math.max(1, Math.min(6, parseInt(moveDigitsInput.value) || 4));
|
||||
const targetPath = _pendingMoveTarget;
|
||||
const pendingFiles = _pendingMoveFiles;
|
||||
|
||||
const newNames = _pendingMoveFiles.map((f, i) => {
|
||||
const newNames = pendingFiles.map((f, i) => {
|
||||
const ext = f.name.split('.').pop().toLowerCase();
|
||||
return `${prefix}_${String(i + 1).padStart(digits, '0')}.${ext}`;
|
||||
});
|
||||
|
||||
const conflicts = await window.api.checkFilesExist(_pendingMoveTarget, newNames);
|
||||
const conflicts = await window.api.checkFilesExist(targetPath, newNames);
|
||||
if (conflicts.length) {
|
||||
moveModal.style.display = 'none';
|
||||
moveErrorModal.style.display = 'flex';
|
||||
closeModal(moveModal);
|
||||
openModal(moveErrorModal);
|
||||
return;
|
||||
}
|
||||
|
||||
moveModal.style.display = 'none';
|
||||
const targetPath = _pendingMoveTarget;
|
||||
const filesToMove = _pendingMoveFiles.map((f, i) => ({ ...f, newName: newNames[i] }));
|
||||
_pendingMoveTarget = null;
|
||||
_pendingMoveFiles = null;
|
||||
const filesToMove = pendingFiles.map((f, i) => ({ ...f, newName: newNames[i] }));
|
||||
closeModal(moveModal);
|
||||
await _doMoveFiles(targetPath, filesToMove);
|
||||
});
|
||||
|
||||
moveErrorCloseBtn.addEventListener('click', () => {
|
||||
moveErrorModal.style.display = 'none';
|
||||
});
|
||||
|
||||
export async function handleFolderDrop(targetPath, fileIndices) {
|
||||
if (!fileIndices.length || !state.currentFolder) return;
|
||||
if (targetPath === state.currentFolder) return;
|
||||
@@ -66,7 +61,7 @@ export async function handleFolderDrop(targetPath, fileIndices) {
|
||||
_pendingMoveTarget = targetPath;
|
||||
_pendingMoveFiles = selectedFiles;
|
||||
movePrefixInput.value = 'temp';
|
||||
moveModal.style.display = 'flex';
|
||||
openModal(moveModal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-14
@@ -2,32 +2,28 @@ import { state } from './state.js';
|
||||
import { showLoading, hideLoading, setStatus, showToast } from './ui.js';
|
||||
import { renderGrid } from './grid.js';
|
||||
import * as history from './history.js';
|
||||
import { openModal, closeModal } from './modal.js';
|
||||
|
||||
const inputPrefix = document.getElementById('input-prefix');
|
||||
const inputDigits = document.getElementById('input-digits');
|
||||
const renameConfirmModal = document.getElementById('rename-confirm-modal');
|
||||
const renameConfirmTitle = document.getElementById('rename-confirm-title');
|
||||
const renameConfirmSelectedTitle = renameConfirmModal.querySelector('.rename-title-selected');
|
||||
const renameConfirmAllTitle = renameConfirmModal.querySelector('.rename-title-all');
|
||||
const renameConfirmMsg = document.getElementById('rename-confirm-msg');
|
||||
const renameConfirmWarning = document.getElementById('rename-confirm-selection-warning');
|
||||
const renameConfirmList = document.getElementById('rename-confirm-list');
|
||||
const renameConfirmCancelBtn = document.getElementById('rename-confirm-cancel');
|
||||
const renameConfirmOkBtn = document.getElementById('rename-confirm-ok');
|
||||
const renameConflictModal = document.getElementById('rename-conflict-modal');
|
||||
const renameConflictCloseBtn = document.getElementById('rename-conflict-close');
|
||||
const renameConflictList = document.getElementById('rename-conflict-list');
|
||||
|
||||
let _renameConfirmResolve = null;
|
||||
|
||||
renameConfirmCancelBtn.addEventListener('click', () => {
|
||||
closeRenameConfirm(false);
|
||||
});
|
||||
|
||||
renameConfirmOkBtn.addEventListener('click', () => {
|
||||
closeRenameConfirm(true);
|
||||
});
|
||||
|
||||
renameConflictCloseBtn.addEventListener('click', () => {
|
||||
renameConflictModal.style.display = 'none';
|
||||
renameConfirmModal.addEventListener('modal:closed', () => {
|
||||
resolveRenameConfirm(false);
|
||||
});
|
||||
|
||||
export async function doRename() {
|
||||
@@ -58,7 +54,7 @@ export async function doRename() {
|
||||
li.textContent = n;
|
||||
return li;
|
||||
}));
|
||||
renameConflictModal.style.display = 'flex';
|
||||
openModal(renameConflictModal);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -108,7 +104,8 @@ export async function doRename() {
|
||||
}
|
||||
|
||||
function showRenameConfirm({ count, examples, more, isPartial }) {
|
||||
renameConfirmTitle.textContent = isPartial ? '선택된 이미지 저장' : '전체 이미지 저장';
|
||||
renameConfirmSelectedTitle.hidden = !isPartial;
|
||||
renameConfirmAllTitle.hidden = isPartial;
|
||||
renameConfirmMsg.textContent = `${count}개 파일을 이름 변경합니다. 계속하시겠습니까?`;
|
||||
renameConfirmWarning.style.display = isPartial ? 'block' : 'none';
|
||||
|
||||
@@ -120,8 +117,7 @@ function showRenameConfirm({ count, examples, more, isPartial }) {
|
||||
return li;
|
||||
}));
|
||||
|
||||
renameConfirmModal.style.display = 'flex';
|
||||
renameConfirmOkBtn.focus();
|
||||
openModal(renameConfirmModal);
|
||||
|
||||
return new Promise(resolve => {
|
||||
_renameConfirmResolve = resolve;
|
||||
@@ -129,7 +125,11 @@ function showRenameConfirm({ count, examples, more, isPartial }) {
|
||||
}
|
||||
|
||||
function closeRenameConfirm(confirmed) {
|
||||
renameConfirmModal.style.display = 'none';
|
||||
resolveRenameConfirm(confirmed);
|
||||
closeModal(renameConfirmModal);
|
||||
}
|
||||
|
||||
function resolveRenameConfirm(confirmed) {
|
||||
if (_renameConfirmResolve) {
|
||||
_renameConfirmResolve(confirmed);
|
||||
_renameConfirmResolve = null;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from './grid.js';
|
||||
import { doRename } from './rename.js';
|
||||
import { handleFolderDrop } from './move.js';
|
||||
import { initModalControls } from './modal.js';
|
||||
import * as deleteHandler from './delete.js';
|
||||
import * as pinchZoom from './handlers/pinch-zoom.js';
|
||||
import * as selection from './handlers/selection.js';
|
||||
@@ -37,6 +38,7 @@ const fileExplorerContainer = document.querySelector('.file-explorer');
|
||||
|
||||
// ── 핸들러 초기화 ─────────────────────────────────
|
||||
history.setOnUpdate(updateHistoryButtons);
|
||||
initModalControls();
|
||||
|
||||
const callbacks = {
|
||||
refreshCardSelection, updateSelCount, updateNumberBadges, updateNewNameLabels,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
--accent2: #e96af7;
|
||||
--success: #6af7a0;
|
||||
--warning: #f7c26a;
|
||||
--danger: #ff6b7a;
|
||||
--info: #6ac8f7;
|
||||
--text: #e8e8f0;
|
||||
--muted: #8f8fb8;
|
||||
@@ -548,6 +549,16 @@ footer span { font-size: 11px; color: var(--muted); }
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.modal-titlebar-warning {
|
||||
color: var(--warning);
|
||||
background: rgba(247,194,106,.12);
|
||||
border-bottom-color: rgba(247,194,106,.35);
|
||||
}
|
||||
.modal-titlebar-danger {
|
||||
color: var(--danger);
|
||||
background: rgba(255,107,122,.12);
|
||||
border-bottom-color: rgba(255,107,122,.35);
|
||||
}
|
||||
.modal-msg {
|
||||
font-size: 14px; color: var(--text); line-height: 1.7;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user