一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
213 行
5.1 KiB
TypeScript
213 行
5.1 KiB
TypeScript
/**
|
||
* 图片沉浸式全屏灯箱(Image Lightbox / Quick Look)
|
||
* 模拟 Apple Notes 与 macOS Quick Look 的流畅看图体验:
|
||
* - 毛玻璃暗色背景与平滑淡入缩放
|
||
* - 支持 ESC 键与点击背景退出
|
||
* - 单击图片切换「适应屏幕」与「100% 原始尺寸」
|
||
*/
|
||
|
||
interface LightboxState {
|
||
container: HTMLElement;
|
||
img: HTMLImageElement;
|
||
isZoomedOriginal: boolean;
|
||
keydownListener: (e: KeyboardEvent) => void;
|
||
}
|
||
|
||
let activeLightbox: LightboxState | null = null;
|
||
|
||
export const closeImageLightbox = (): void => {
|
||
if (!activeLightbox) return;
|
||
|
||
const { container, keydownListener } = activeLightbox;
|
||
window.removeEventListener('keydown', keydownListener);
|
||
|
||
container.style.opacity = '0';
|
||
container.style.pointerEvents = 'none';
|
||
|
||
setTimeout(() => {
|
||
if (container.parentElement) {
|
||
container.parentElement.removeChild(container);
|
||
}
|
||
}, 200);
|
||
|
||
activeLightbox = null;
|
||
};
|
||
|
||
export const showImageLightbox = (src: string, alt = ''): HTMLElement => {
|
||
if (activeLightbox) {
|
||
closeImageLightbox();
|
||
}
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'cm-ext-image-lightbox-overlay';
|
||
overlay.setAttribute('role', 'dialog');
|
||
overlay.setAttribute('aria-modal', 'true');
|
||
overlay.setAttribute('aria-label', alt || 'Image Preview');
|
||
|
||
// 样式:固定全屏、深色毛玻璃、居中对齐
|
||
Object.assign(overlay.style, {
|
||
position: 'fixed',
|
||
top: '0',
|
||
left: '0',
|
||
width: '100vw',
|
||
height: '100vh',
|
||
backgroundColor: 'rgba(0, 0, 0, 0.82)',
|
||
backdropFilter: 'blur(16px)',
|
||
WebkitBackdropFilter: 'blur(16px)',
|
||
zIndex: '99999',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
opacity: '0',
|
||
transition: 'opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1)',
|
||
cursor: 'zoom-out',
|
||
padding: '24px',
|
||
boxSizing: 'border-box',
|
||
});
|
||
|
||
// 顶部操作栏
|
||
const headerBar = document.createElement('div');
|
||
headerBar.className = 'cm-lightbox-header';
|
||
Object.assign(headerBar.style, {
|
||
position: 'absolute',
|
||
top: '16px',
|
||
right: '20px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '12px',
|
||
zIndex: '100001',
|
||
});
|
||
|
||
// 关闭按钮
|
||
const closeBtn = document.createElement('button');
|
||
closeBtn.className = 'cm-lightbox-close-btn';
|
||
closeBtn.title = '关闭 (ESC)';
|
||
closeBtn.innerHTML = '✕';
|
||
Object.assign(closeBtn.style, {
|
||
background: 'rgba(255, 255, 255, 0.15)',
|
||
border: 'none',
|
||
color: '#ffffff',
|
||
width: '32px',
|
||
height: '32px',
|
||
borderRadius: '50%',
|
||
fontSize: '16px',
|
||
fontWeight: 'bold',
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
transition: 'background 0.15s ease',
|
||
});
|
||
closeBtn.addEventListener('mouseenter', () => {
|
||
closeBtn.style.background = 'rgba(255, 255, 255, 0.3)';
|
||
});
|
||
closeBtn.addEventListener('mouseleave', () => {
|
||
closeBtn.style.background = 'rgba(255, 255, 255, 0.15)';
|
||
});
|
||
closeBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
closeImageLightbox();
|
||
});
|
||
headerBar.appendChild(closeBtn);
|
||
overlay.appendChild(headerBar);
|
||
|
||
// 图片容器与图片元素
|
||
const imgWrapper = document.createElement('div');
|
||
imgWrapper.className = 'cm-lightbox-img-wrapper';
|
||
Object.assign(imgWrapper.style, {
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
maxWidth: '100%',
|
||
maxHeight: '100%',
|
||
overflow: 'auto',
|
||
});
|
||
|
||
const img = document.createElement('img');
|
||
img.className = 'cm-lightbox-img';
|
||
img.src = src;
|
||
img.alt = alt;
|
||
|
||
const applyFitStyle = () => {
|
||
Object.assign(img.style, {
|
||
maxWidth: '92vw',
|
||
maxHeight: '88vh',
|
||
width: 'auto',
|
||
height: 'auto',
|
||
objectFit: 'contain',
|
||
borderRadius: '8px',
|
||
boxShadow: '0 20px 48px rgba(0, 0, 0, 0.6)',
|
||
cursor: 'zoom-in',
|
||
transition: 'transform 0.2s cubic-bezier(0.16, 1, 0.3, 1)',
|
||
});
|
||
};
|
||
|
||
const applyOriginalStyle = () => {
|
||
Object.assign(img.style, {
|
||
maxWidth: 'none',
|
||
maxHeight: 'none',
|
||
width: 'auto',
|
||
height: 'auto',
|
||
objectFit: 'none',
|
||
cursor: 'zoom-out',
|
||
transition: 'none',
|
||
});
|
||
};
|
||
|
||
applyFitStyle();
|
||
|
||
// 单击图片切换适应/原图
|
||
let isZoomedOriginal = false;
|
||
img.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
isZoomedOriginal = !isZoomedOriginal;
|
||
if (isZoomedOriginal) {
|
||
applyOriginalStyle();
|
||
} else {
|
||
applyFitStyle();
|
||
}
|
||
if (activeLightbox) {
|
||
activeLightbox.isZoomedOriginal = isZoomedOriginal;
|
||
}
|
||
});
|
||
|
||
imgWrapper.appendChild(img);
|
||
overlay.appendChild(imgWrapper);
|
||
|
||
// 点击背景遮罩关闭
|
||
overlay.addEventListener('click', (e) => {
|
||
if (e.target === overlay || e.target === imgWrapper) {
|
||
closeImageLightbox();
|
||
}
|
||
});
|
||
|
||
// ESC 键盘监听
|
||
const keydownListener = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
closeImageLightbox();
|
||
}
|
||
};
|
||
window.addEventListener('keydown', keydownListener);
|
||
|
||
document.body.appendChild(overlay);
|
||
|
||
// 渐显进入
|
||
requestAnimationFrame(() => {
|
||
overlay.style.opacity = '1';
|
||
});
|
||
|
||
activeLightbox = {
|
||
container: overlay,
|
||
img,
|
||
isZoomedOriginal: false,
|
||
keydownListener,
|
||
};
|
||
|
||
return overlay;
|
||
};
|
||
|
||
export const getActiveLightbox = (): LightboxState | null => activeLightbox;
|