文件
mynode/packages/editor/CodeMirror/utils/imageLightbox.ts
chen 301194963b
一些检查失败了
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
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

213 行
5.1 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

/**
* 图片沉浸式全屏灯箱(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;