bak
This commit is contained in:
1 parent
0d4d369870
commit
9c71e65041
9 files changed
+423
-16
No files matched your search
+241
-4
@@ -13,6 +13,18 @@ const bodyEl = $('detail-body');
|
||||
const metaEl = $('detail-meta');
|
||||
let morphTimer = null;
|
||||
let morphGhost = null;
|
||||
let imageFsOverlay = null;
|
||||
let imageFsImg = null;
|
||||
let fsScale = 1;
|
||||
let fsTx = 0;
|
||||
let fsTy = 0;
|
||||
let fsPanning = false;
|
||||
let fsStartX = 0;
|
||||
let fsStartY = 0;
|
||||
let fsStartTx = 0;
|
||||
let fsStartTy = 0;
|
||||
const FS_MIN_SCALE = 0.35;
|
||||
const FS_MAX_SCALE = 8;
|
||||
|
||||
// ── Zoom ──
|
||||
|
||||
@@ -41,7 +53,7 @@ scroll?.addEventListener('wheel', e => {
|
||||
|
||||
// ── Open / close ──
|
||||
|
||||
export function openDetail(id) {
|
||||
export function openDetail(id, options = {}) {
|
||||
const card = state.cards.find(c => c.id == id);
|
||||
if (!card) return;
|
||||
markRecent(id);
|
||||
@@ -51,6 +63,11 @@ export function openDetail(id) {
|
||||
|
||||
if (!detail.classList.contains('open')) {
|
||||
state.openedFromSearch = $('search')?.classList.contains('open') || false;
|
||||
state.openedFromGallery = !!options.fromGallery;
|
||||
state.galleryOrigin = options.fromGallery ? (options.galleryOrigin || null) : null;
|
||||
} else if (options.fromGallery) {
|
||||
state.openedFromGallery = true;
|
||||
state.galleryOrigin = options.galleryOrigin || state.galleryOrigin || null;
|
||||
}
|
||||
state.viewingCard = card;
|
||||
|
||||
@@ -70,7 +87,139 @@ function clearMorphGhost() {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureImageFullscreenOverlay() {
|
||||
if (imageFsOverlay) return imageFsOverlay;
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'detail-image-fs-overlay';
|
||||
overlay.innerHTML = `
|
||||
<button type="button" class="detail-image-fs-close" aria-label="close image">
|
||||
<i class="ph ph-x text-xl"></i>
|
||||
</button>
|
||||
<div class="detail-image-fs-stage">
|
||||
<img class="detail-image-fs-img" alt="">
|
||||
</div>
|
||||
`;
|
||||
overlay.addEventListener('click', e => {
|
||||
if (e.target === overlay) closeImageFullscreen();
|
||||
});
|
||||
overlay.querySelector('.detail-image-fs-close')?.addEventListener('click', closeImageFullscreen);
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
imageFsOverlay = overlay;
|
||||
imageFsImg = overlay.querySelector('.detail-image-fs-img');
|
||||
if (imageFsImg) {
|
||||
imageFsImg.draggable = false;
|
||||
imageFsImg.addEventListener('mousedown', onImageFsMouseDown);
|
||||
}
|
||||
overlay.addEventListener('wheel', onImageFsWheel, { passive: false });
|
||||
document.addEventListener('mousemove', onImageFsMouseMove);
|
||||
document.addEventListener('mouseup', onImageFsMouseUp);
|
||||
return overlay;
|
||||
}
|
||||
|
||||
function applyImageFsTransform() {
|
||||
if (!imageFsImg) return;
|
||||
imageFsImg.style.transform = `translate(${fsTx}px, ${fsTy}px) scale(${fsScale})`;
|
||||
}
|
||||
|
||||
function resetImageFsTransform() {
|
||||
fsScale = 1;
|
||||
fsTx = 0;
|
||||
fsTy = 0;
|
||||
fsPanning = false;
|
||||
imageFsOverlay?.classList.remove('dragging');
|
||||
applyImageFsTransform();
|
||||
}
|
||||
|
||||
function onImageFsWheel(e) {
|
||||
if (!imageFsOverlay?.classList.contains('open') || !imageFsImg) return;
|
||||
e.preventDefault();
|
||||
|
||||
const old = fsScale;
|
||||
const factor = Math.exp(-e.deltaY * 0.0015);
|
||||
const next = Math.min(FS_MAX_SCALE, Math.max(FS_MIN_SCALE, old * factor));
|
||||
if (Math.abs(next - old) < 0.0001) return;
|
||||
|
||||
const r = imageFsOverlay.getBoundingClientRect();
|
||||
const cx = r.left + r.width / 2;
|
||||
const cy = r.top + r.height / 2;
|
||||
const px = e.clientX - cx;
|
||||
const py = e.clientY - cy;
|
||||
|
||||
fsTx = px - ((px - fsTx) / old) * next;
|
||||
fsTy = py - ((py - fsTy) / old) * next;
|
||||
fsScale = next;
|
||||
applyImageFsTransform();
|
||||
}
|
||||
|
||||
function onImageFsMouseDown(e) {
|
||||
if (!imageFsOverlay?.classList.contains('open')) return;
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
fsPanning = true;
|
||||
fsStartX = e.clientX;
|
||||
fsStartY = e.clientY;
|
||||
fsStartTx = fsTx;
|
||||
fsStartTy = fsTy;
|
||||
imageFsOverlay.classList.add('dragging');
|
||||
}
|
||||
|
||||
function onImageFsMouseMove(e) {
|
||||
if (!fsPanning || !imageFsOverlay?.classList.contains('open')) return;
|
||||
fsTx = fsStartTx + (e.clientX - fsStartX);
|
||||
fsTy = fsStartTy + (e.clientY - fsStartY);
|
||||
applyImageFsTransform();
|
||||
}
|
||||
|
||||
function onImageFsMouseUp() {
|
||||
if (!fsPanning) return;
|
||||
fsPanning = false;
|
||||
imageFsOverlay?.classList.remove('dragging');
|
||||
}
|
||||
|
||||
function openImageFullscreen(src, alt = '') {
|
||||
if (!src) return;
|
||||
const overlay = ensureImageFullscreenOverlay();
|
||||
resetImageFsTransform();
|
||||
if (imageFsImg) {
|
||||
imageFsImg.src = src;
|
||||
imageFsImg.alt = alt || 'image';
|
||||
}
|
||||
overlay.classList.add('open');
|
||||
document.body.classList.add('detail-image-fs-open');
|
||||
}
|
||||
|
||||
function closeImageFullscreen() {
|
||||
if (!imageFsOverlay) return;
|
||||
fsPanning = false;
|
||||
imageFsOverlay.classList.remove('dragging');
|
||||
imageFsOverlay.classList.remove('open');
|
||||
document.body.classList.remove('detail-image-fs-open');
|
||||
if (imageFsImg) {
|
||||
imageFsImg.removeAttribute('src');
|
||||
imageFsImg.alt = '';
|
||||
imageFsImg.style.transform = '';
|
||||
}
|
||||
fsScale = 1;
|
||||
fsTx = 0;
|
||||
fsTy = 0;
|
||||
}
|
||||
|
||||
function resolveMorphTarget(cardId) {
|
||||
if (state.openedFromGallery) {
|
||||
const grid = $('gallery-grid');
|
||||
if (grid) {
|
||||
if (state.galleryOrigin) {
|
||||
const exact = Array.from(grid.querySelectorAll('[data-gallery-origin]'))
|
||||
.find(el => el.dataset.galleryOrigin === state.galleryOrigin);
|
||||
if (exact) return exact;
|
||||
}
|
||||
const byCard = grid.querySelector(`[data-card-id="${cardId}"]`);
|
||||
if (byCard) return byCard;
|
||||
}
|
||||
}
|
||||
|
||||
const board = $('board');
|
||||
if (!board || !cardId) return null;
|
||||
|
||||
@@ -133,7 +282,9 @@ function startCloseMorph(cardId) {
|
||||
|
||||
const frame = document.createElement('div');
|
||||
frame.className = 'detail-close-frame';
|
||||
const targetCard = state.cards.find(c => c.id == target.dataset.id);
|
||||
const targetCard = target.dataset.id
|
||||
? state.cards.find(c => c.id == target.dataset.id)
|
||||
: card;
|
||||
Object.assign(frame.style, {
|
||||
left: `${from.left}px`,
|
||||
top: `${from.top}px`,
|
||||
@@ -170,10 +321,12 @@ function startCloseMorph(cardId) {
|
||||
|
||||
export function closeDetail(options = {}) {
|
||||
const animate = options?.animate !== false;
|
||||
const restoreGallery = options?.restoreGallery !== false;
|
||||
const cardId = state.viewingCard?.id;
|
||||
|
||||
if (animate) startCloseMorph(cardId);
|
||||
else clearMorphGhost();
|
||||
closeImageFullscreen();
|
||||
|
||||
detail.classList.remove('open', 'zen');
|
||||
document.documentElement.classList.remove('detail-zen-open');
|
||||
@@ -185,6 +338,12 @@ export function closeDetail(options = {}) {
|
||||
state.openedFromSearch = false;
|
||||
$('search-input')?.focus();
|
||||
}
|
||||
if (state.openedFromGallery) {
|
||||
const galleryOpen = $('gallery')?.classList.contains('open');
|
||||
state.openedFromGallery = false;
|
||||
state.galleryOrigin = null;
|
||||
if (restoreGallery && !galleryOpen) window._openGallery?.();
|
||||
}
|
||||
}
|
||||
|
||||
export function showDetailCard(id) {
|
||||
@@ -241,6 +400,8 @@ function showContent(card) {
|
||||
$('detail-color').style.background = card.color;
|
||||
titleEl.textContent = card.title;
|
||||
bodyEl.innerHTML = renderMd(card.body);
|
||||
applyReadingResizeHandles(bodyEl);
|
||||
bindDetailImageFullscreen(bodyEl);
|
||||
|
||||
// Interactive checkboxes
|
||||
bodyEl.querySelectorAll('input[type="checkbox"]').forEach((cb, idx) => {
|
||||
@@ -280,6 +441,68 @@ function showContent(card) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyReadingResizeHandles(root) {
|
||||
if (!root) return;
|
||||
// Reading aid only: temporary per-block font-size scaling (not persisted).
|
||||
root.querySelectorAll('p, blockquote, pre').forEach(el => {
|
||||
el.classList.add('detail-reading-resize');
|
||||
|
||||
const line = document.createElement('button');
|
||||
line.type = 'button';
|
||||
line.className = 'detail-font-dragline';
|
||||
line.title = 'drag to resize text';
|
||||
line.setAttribute('aria-label', 'resize text');
|
||||
el.appendChild(line);
|
||||
|
||||
line.addEventListener('mousedown', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const startY = e.clientY;
|
||||
const startFont = parseFloat(getComputedStyle(el).fontSize) || 14;
|
||||
const startLineHeightPx = parseFloat(getComputedStyle(el).lineHeight);
|
||||
const lineRatio = Number.isFinite(startLineHeightPx) && startLineHeightPx > 0
|
||||
? startLineHeightPx / startFont
|
||||
: 1.65;
|
||||
const prevSelect = document.body.style.userSelect;
|
||||
const min = 12;
|
||||
const max = 44;
|
||||
|
||||
el.classList.add('resizing');
|
||||
document.body.style.userSelect = 'none';
|
||||
|
||||
const onMove = ev => {
|
||||
const delta = ev.clientY - startY;
|
||||
const next = Math.min(max, Math.max(min, startFont + delta * 0.08));
|
||||
el.style.fontSize = `${next.toFixed(2)}px`;
|
||||
el.style.lineHeight = `${(next * lineRatio).toFixed(2)}px`;
|
||||
};
|
||||
|
||||
const onUp = () => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
document.body.style.userSelect = prevSelect;
|
||||
el.classList.remove('resizing');
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindDetailImageFullscreen(root) {
|
||||
if (!root) return;
|
||||
root.querySelectorAll('img').forEach(img => {
|
||||
img.title = 'double-click to open fullscreen';
|
||||
img.addEventListener('dblclick', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openImageFullscreen(img.getAttribute('src'), img.getAttribute('alt') || 'image');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Event listeners ──
|
||||
|
||||
// Delegated card-link clicks (replaces inline onclick)
|
||||
@@ -290,6 +513,14 @@ detail.addEventListener('click', e => {
|
||||
});
|
||||
$('detail-close')?.addEventListener('click', closeDetail);
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (!imageFsOverlay?.classList.contains('open')) return;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
closeImageFullscreen();
|
||||
}, true);
|
||||
|
||||
// Zen mode
|
||||
$('detail-zen')?.addEventListener('click', () => {
|
||||
detail.classList.toggle('zen');
|
||||
@@ -303,8 +534,13 @@ $('detail-zen')?.addEventListener('click', () => {
|
||||
$('detail-edit')?.addEventListener('click', () => {
|
||||
if (!state.viewingCard) return;
|
||||
const card = state.viewingCard;
|
||||
const fromGallery = state.openedFromGallery;
|
||||
state.openedFromSearch = false;
|
||||
closeDetail({ animate: false });
|
||||
state.openedFromGallery = false;
|
||||
state.galleryOrigin = null;
|
||||
state.returnToGalleryAfterEdit = fromGallery;
|
||||
closeDetail({ animate: false, restoreGallery: false });
|
||||
$('gallery')?.classList.remove('open');
|
||||
window._closeSearch();
|
||||
window._openModal(card);
|
||||
});
|
||||
@@ -315,7 +551,8 @@ $('detail-delete')?.addEventListener('click', async () => {
|
||||
if (!ok) return;
|
||||
pushCardDelete('delete', snapshotCard(state.viewingCard));
|
||||
await api(`api/cards/${state.viewingCard.id}`, { method: 'DELETE' });
|
||||
closeDetail({ animate: false });
|
||||
state.galleryOrigin = null;
|
||||
closeDetail({ animate: false, restoreGallery: false });
|
||||
await window._reloadAll();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user