import {dialog} from "./dialog.js";
import {CommentSystem} from "./comment/index.js";
import {Gallery} from "./gallery.js";
/**
* Mumble (碎碎念) Module
* Features: Lazy load comments, expand text, image viewer, like, comment form handling
*/
export class MumbleApp {
constructor() {
this.container = document.querySelector('.mumble-layout');
if (!this.container) {
return;
}
this.initMarkdownExpand();
this.initGalleryImages();
this.initLikeButtons();
this.initCommentToggles();
}
/**
* Handle gallery image loading states
*/
initGalleryImages() {
const galleryImages = this.container.querySelectorAll('.mumble-gallery__image');
galleryImages.forEach(img => {
const item = img.closest('.mumble-gallery__item');
if (!item || item.classList.contains('mumble-gallery__more')) return;
// If image is already loaded (cached)
if (img.complete && img.naturalHeight !== 0) {
item.classList.add('is-loaded');
} else {
// Listen for load event
img.addEventListener('load', () => {
item.classList.add('is-loaded');
}, { once: true });
// Handle error
img.addEventListener('error', () => {
item.classList.add('is-loaded');
console.warn('Failed to load image:', img.src);
}, { once: true });
}
});
}
/**
* Handle "Read More" for long content with dynamic height check
*/
initMarkdownExpand() {
const contents = document.querySelectorAll('.mumble-card__content');
const checkHeight = (content) => {
// Avoid duplicate processing if button already exists
const hasButton = content.nextElementSibling?.classList.contains('mumble-card__read-more');
// Note: If it's already collapsed, scrollHeight might be small (e.g. 300),
// but we only care if it *should* be collapsed or if the button is needed.
// If hasButton is true, we assume it's already handled, unless we want to remove the button if content shrinks (rare).
if (hasButton) return;
if (content.scrollHeight > 300) {
this.addExpandButton(content);
}
};
const observer = new ResizeObserver(entries => {
entries.forEach(entry => {
// Use requestAnimationFrame to avoid "ResizeObserver loop limit exceeded"
requestAnimationFrame(() => {
checkHeight(entry.target);
});
});
});
contents.forEach(content => {
// Initial check
checkHeight(content);
// Observe for changes (e.g. image loads)
observer.observe(content);
});
}
addExpandButton(content) {
content.classList.add('is-collapsed');
const btnContainer = document.createElement('div');
btnContainer.className = 'mumble-card__read-more';
btnContainer.style.display = 'block';
const btn = document.createElement('button');
btn.className = 'mumble-card__read-more-btn';
btn.textContent = '展开全文';
btn.addEventListener('click', () => {
const isCollapsed = content.classList.contains('is-collapsed');
if (isCollapsed) {
// Expand
content.classList.remove('is-collapsed');
btnContainer.classList.add('is-expanded');
btn.textContent = '收起';
} else {
// Collapse
content.classList.add('is-collapsed');
btnContainer.classList.remove('is-expanded');
btn.textContent = '展开全文';
// Scroll back if needed
const card = content.closest('.mumble-card');
if (card) {
const rect = card.getBoundingClientRect();
// If top of card is above viewport (scrolled past), bring it back
if (rect.top < 0) {
const offset = 80; // approximate header height + spacing
const scrollTop = window.pageYOffset + rect.top - offset;
window.scrollTo({top: scrollTop, behavior: 'smooth'});
}
}
}
});
btnContainer.appendChild(btn);
content.parentNode.insertBefore(btnContainer, content.nextSibling);
}
/**
* Handle Like Buttons
*/
initLikeButtons() {
// Check local storage for liked status
const likeButtons = this.container.querySelectorAll('[data-like-id]');
likeButtons.forEach(btn => {
const id = btn.getAttribute('data-like-id');
if (localStorage.getItem(`mumble_like_${id}`)) {
btn.classList.add('mumble-action--active');
}
});
this.container.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-like-id]');
if (!btn) return;
e.preventDefault();
const id = btn.getAttribute('data-like-id');
// Check if already liked
if (localStorage.getItem(`mumble_like_${id}`)) {
dialog.toast('您已经点赞过了', 'info');
return;
}
const countSpan = btn.querySelector('[data-like-count]');
if (btn.classList.contains('is-loading') || btn.disabled) return;
const icon = btn.querySelector('i');
const originalIconClass = icon ? icon.className : '';
try {
btn.classList.add('is-loading');
btn.disabled = true;
if (icon) {
icon.className = 'fa fa-spinner fa-spin';
}
const response = await fetch(`/mumble/like/${id}`, {
method: 'POST'
});
const result = await response.json();
let count;
if (result.success) {
count = parseInt(result["data"]);
localStorage.setItem(`mumble_like_${id}`, 'true');
} else {
count = parseInt(countSpan.textContent || '0');
count++;
}
countSpan.textContent = count;
btn.classList.add('mumble-action--active');
} catch (err) {
console.error('Failed to like', err);
} finally {
btn.classList.remove('is-loading');
btn.disabled = false;
if (icon) {
icon.className = originalIconClass;
// Animation effect after restoring icon
if (icon.className.includes('fa-thumbs-up')) {
icon.style.transform = 'scale(1.2)';
setTimeout(() => icon.style.transform = 'scale(1)', 200);
}
}
}
});
}
/**
* Toggle Comment Section
*/
initCommentToggles() {
this.container.addEventListener('click', (e) => {
const btn = e.target.closest('[data-comment-toggle]');
if (!btn) return;
e.preventDefault();
const card = btn.closest('.mumble-card');
const commentSection = card.querySelector('.mumble-comments');
if (commentSection) {
if (commentSection.classList.contains('is-hidden')) {
commentSection.classList.remove('is-hidden');
const loadUrl = commentSection.getAttribute('data-load-url');
// Only load if it hasn't been loaded properly (checking for spinner or empty)
if (loadUrl && !commentSection.querySelector('.comment-list') && !commentSection.querySelector('.comment-empty')) {
// Set loading state
btn.disabled = true;
const icon = btn.querySelector('i');
const originalIconClass = icon ? icon.className : '';
if (icon) icon.className = 'fa fa-spinner fa-spin';
this.loadComments(commentSection, loadUrl).then(() => {
const newContainer = commentSection.querySelector('.comment-section');
new CommentSystem(newContainer);
console.log('Comments loaded');
}).finally(() => {
// Restore state
btn.disabled = false;
if (icon) icon.className = originalIconClass;
});
}
} else {
commentSection.classList.add('is-hidden');
}
}
});
}
async loadComments(container, url) {
try {
const res = await fetch(url);
if (res.ok) {
container.innerHTML = await res.text();
} else {
dialog.toast('无法加载评论', 'error');
}
} catch (e) {
container.innerHTML = '<div class="text-center text-muted p-3">无法加载评论</div>';
}
}
} 评论加载中...