feat(client): implement photoswipe at different points

This commit is contained in:
perf3ct 2025-08-16 17:29:25 +00:00
parent 78c27dbe04
commit 5024e27885
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
3 changed files with 116 additions and 3 deletions

View File

@ -5,6 +5,7 @@
import mediaViewer from './media_viewer.js'; import mediaViewer from './media_viewer.js';
import galleryManager from './gallery_manager.js'; import galleryManager from './gallery_manager.js';
import appContext from '../components/app_context.js';
import type { MediaItem } from './media_viewer.js'; import type { MediaItem } from './media_viewer.js';
import type { GalleryItem } from './gallery_manager.js'; import type { GalleryItem } from './gallery_manager.js';
@ -97,9 +98,11 @@ class CKEditorPhotoSwipeIntegration {
return; return;
} }
// Make image clickable // Make image clickable and mark it as PhotoSwipe-enabled
img.style.cursor = 'zoom-in'; img.style.cursor = 'zoom-in';
img.style.transition = 'opacity 0.2s'; img.style.transition = 'opacity 0.2s';
img.classList.add('photoswipe-enabled');
img.setAttribute('data-photoswipe', 'true');
// Store event handlers for cleanup // Store event handlers for cleanup
const mouseEnterHandler = () => { const mouseEnterHandler = () => {
@ -121,6 +124,24 @@ class CKEditorPhotoSwipeIntegration {
// Store handlers for cleanup // Store handlers for cleanup
(img as any)._photoswipeHandlers = { mouseEnterHandler, mouseLeaveHandler }; (img as any)._photoswipeHandlers = { mouseEnterHandler, mouseLeaveHandler };
// Add double-click handler to prevent default navigation behavior
const dblClickHandler = (e: MouseEvent) => {
// Only prevent double-click in specific contexts to avoid breaking other features
if (img.closest('.attachment-detail-wrapper') ||
img.closest('.note-detail-editable-text') ||
img.closest('.note-detail-readonly-text')) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
// Trigger the same behavior as single click (open lightbox)
img.click();
}
};
img.addEventListener('dblclick', dblClickHandler, true); // Use capture phase to ensure we get it first
(img as any)._photoswipeHandlers.dblClickHandler = dblClickHandler;
// Add click handler // Add click handler
img.addEventListener('click', (e) => { img.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
@ -194,6 +215,27 @@ class CKEditorPhotoSwipeIntegration {
} }
}, { }, {
onClose: () => { onClose: () => {
// Check if we're in attachment detail view and need to reset viewScope
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext?.viewScope?.viewMode === 'attachments') {
// Get the note ID from the image source
const attachmentMatch = img.src.match(/\/api\/attachments\/([A-Za-z0-9_]+)\/image\//);
if (attachmentMatch) {
const currentAttachmentId = activeContext.viewScope.attachmentId;
if (currentAttachmentId === attachmentMatch[1]) {
// Actually reset the viewScope instead of just logging
try {
if (activeContext.note) {
activeContext.setNote(activeContext.note.noteId, {
viewScope: { viewMode: 'default' }
});
}
} catch (error) {
console.error('Failed to reset viewScope after PhotoSwipe close:', error);
}
}
}
}
// Restore focus to the image // Restore focus to the image
img.focus(); img.focus();
} }
@ -429,6 +471,9 @@ class CKEditorPhotoSwipeIntegration {
if (handlers) { if (handlers) {
img.removeEventListener('mouseenter', handlers.mouseEnterHandler); img.removeEventListener('mouseenter', handlers.mouseEnterHandler);
img.removeEventListener('mouseleave', handlers.mouseLeaveHandler); img.removeEventListener('mouseleave', handlers.mouseLeaveHandler);
if (handlers.dblClickHandler) {
img.removeEventListener('dblclick', handlers.dblClickHandler, true);
}
delete (img as any)._photoswipeHandlers; delete (img as any)._photoswipeHandlers;
} }

View File

@ -9,6 +9,7 @@ import contentRenderer from "../services/content_renderer.js";
import toastService from "../services/toast.js"; import toastService from "../services/toast.js";
import type FAttachment from "../entities/fattachment.js"; import type FAttachment from "../entities/fattachment.js";
import type { EventData } from "../components/app_context.js"; import type { EventData } from "../components/app_context.js";
import appContext from "../components/app_context.js";
import mediaViewer from "../services/media_viewer.js"; import mediaViewer from "../services/media_viewer.js";
import type { MediaItem } from "../services/media_viewer.js"; import type { MediaItem } from "../services/media_viewer.js";
@ -114,7 +115,9 @@ const TPL = /*html*/`
<div class="attachment-actions-container"></div> <div class="attachment-actions-container"></div>
<h4 class="attachment-title"></h4> <h4 class="attachment-title"></h4>
<div class="attachment-details"></div> <div class="attachment-details"></div>
<div style="flex: 1 1;"></div> <button class="btn btn-sm back-to-note-btn" style="margin-left: auto;" title="Back to Note">
<span class="bx bx-arrow-back"></span> Back to Note
</button>
</div> </div>
<div class="attachment-deletion-warning alert alert-info" style="margin-top: 15px;"></div> <div class="attachment-deletion-warning alert alert-info" style="margin-top: 15px;"></div>
@ -150,6 +153,14 @@ export default class AttachmentDetailWidget extends BasicWidget {
this.$widget.find(".attachment-detail-wrapper").empty().append($(TPL).find(".attachment-detail-wrapper").html()); this.$widget.find(".attachment-detail-wrapper").empty().append($(TPL).find(".attachment-detail-wrapper").html());
this.$wrapper = this.$widget.find(".attachment-detail-wrapper"); this.$wrapper = this.$widget.find(".attachment-detail-wrapper");
this.$wrapper.addClass(this.isFullDetail ? "full-detail" : "list-view"); this.$wrapper.addClass(this.isFullDetail ? "full-detail" : "list-view");
// Setup back to note button (only show in full detail mode)
if (this.isFullDetail) {
const $backBtn = this.$wrapper.find('.back-to-note-btn');
$backBtn.on('click', () => this.handleBackToNote());
} else {
this.$wrapper.find('.back-to-note-btn').hide();
}
if (!this.isFullDetail) { if (!this.isFullDetail) {
const $link = await linkService.createLink(this.attachment.ownerId, { const $link = await linkService.createLink(this.attachment.ownerId, {
@ -255,6 +266,15 @@ export default class AttachmentDetailWidget extends BasicWidget {
console.log('Attachment image opened in lightbox'); console.log('Attachment image opened in lightbox');
}, },
onClose: () => { onClose: () => {
// Check if we're in attachment detail view and reset viewScope if needed
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext?.viewScope?.viewMode === 'attachments' &&
activeContext?.viewScope?.attachmentId === this.attachment.attachmentId) {
// Reset to normal note view when closing lightbox from attachment detail
activeContext.setNote(this.attachment.ownerId, {
viewScope: { viewMode: 'default' }
});
}
// Restore focus to the image // Restore focus to the image
$img.focus(); $img.focus();
} }
@ -307,6 +327,28 @@ export default class AttachmentDetailWidget extends BasicWidget {
} }
} }
async handleBackToNote() {
try {
const activeContext = appContext.tabManager.getActiveContext();
if (!activeContext) {
console.warn('No active context available for navigation');
return;
}
if (!this.attachment.ownerId) {
console.error('Cannot navigate back: no owner ID available');
return;
}
await activeContext.setNote(this.attachment.ownerId, {
viewScope: { viewMode: 'default' }
});
} catch (error) {
console.error('Failed to navigate back to note:', error);
toastService.showError('Failed to navigate back to note');
}
}
cleanup() { cleanup() {
// Remove all event handlers before cleanup // Remove all event handlers before cleanup
const $contentWrapper = this.$wrapper?.find('.attachment-content-wrapper'); const $contentWrapper = this.$wrapper?.find('.attachment-content-wrapper');
@ -318,6 +360,9 @@ export default class AttachmentDetailWidget extends BasicWidget {
} }
} }
// Remove back button handler
this.$wrapper?.find('.back-to-note-btn').off('click');
super.cleanup(); super.cleanup();
} }
} }

View File

@ -6,6 +6,7 @@ import contentRenderer from "../../services/content_renderer.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
import attributes from "../../services/attributes.js"; import attributes from "../../services/attributes.js";
import ckeditorPhotoswipeIntegration from "../../services/ckeditor_photoswipe_integration.js";
export default class AbstractTextTypeWidget extends TypeWidget { export default class AbstractTextTypeWidget extends TypeWidget {
doRender() { doRender() {
@ -35,7 +36,29 @@ export default class AbstractTextTypeWidget extends TypeWidget {
const parsedImage = await this.parseFromImage($img); const parsedImage = await this.parseFromImage($img);
if (parsedImage) { if (parsedImage) {
appContext.tabManager.getActiveContext()?.setNote(parsedImage.noteId, { viewScope: parsedImage.viewScope }); // Check if this is an attachment image and PhotoSwipe is available
if (parsedImage.viewScope?.attachmentId) {
// Instead of navigating to attachment detail, trigger PhotoSwipe
// Check if the image is already processed by PhotoSwipe
const imgElement = $img[0] as HTMLImageElement;
// Check if PhotoSwipe is integrated with this image using multiple reliable indicators
const hasPhotoSwipe = imgElement.classList.contains('photoswipe-enabled') ||
imgElement.hasAttribute('data-photoswipe') ||
imgElement.style.cursor === 'zoom-in';
if (hasPhotoSwipe) {
// Image has PhotoSwipe integration, trigger click to open lightbox
$img.trigger('click');
return;
}
// Otherwise, fall back to opening attachment detail (but with improved navigation)
appContext.tabManager.getActiveContext()?.setNote(parsedImage.noteId, { viewScope: parsedImage.viewScope });
} else {
// Regular note image, navigate normally
appContext.tabManager.getActiveContext()?.setNote(parsedImage.noteId, { viewScope: parsedImage.viewScope });
}
} else { } else {
window.open($img.prop("src"), "_blank"); window.open($img.prop("src"), "_blank");
} }