/**
 * Comment UI Module
 * Handles DOM manipulation and interactions
 */

import { CodeArea } from '../code-area.js';
import { updateTimeTags } from '../util.js';

export class CommentUI {
    constructor(element) {
        this.container = element || document.querySelector('.comment-section');
        this.listContainer = this.container ? this.container.querySelector('.comment-list') : null;
        this.drawer = null;
        this.drawerOverlay = null;
        
        // Icons
        this.icons = {
            loading: `<svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 11-6.219-8.56"></path></svg>`,
            send: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22,2 15,22 11,13 2,9 22,2"></polygon></svg>`,
            error: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>`
        };
    }

    /**
     * Update submit button state
     * @param {HTMLButtonElement} btn 
     * @param {'normal'|'loading'|'error'} state 
     * @param {string} [msg] 
     */
    setButtonState(btn, state, msg) {
        if (!btn) return;
        
        switch (state) {
            case 'loading':
                btn.disabled = true;
                btn.innerHTML = `${this.icons.loading} 发送中...`;
                btn.classList.add('btn-loading');
                break;
            case 'error':
                btn.disabled = false;
                btn.innerHTML = `${this.icons.error} ${msg || '重试'}`;
                btn.classList.add('btn-error');
                break;
            case 'normal':
            default:
                btn.disabled = false;
                btn.innerHTML = `${this.icons.send} 发送`;
                btn.classList.remove('btn-loading', 'btn-error');
                break;
        }
    }

    /**
     * Show skeleton loading state (replaces content temporarily)
     */
    showListSkeleton() {
        if (!this.container) return;
        
        // Use the same skeleton structure as in _GlobalCommentPartial
        const skeletonHtml = `
            <div class="comment-section-skeleton">
                <div class="comment-loading-spinner">
                    <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin">
                        <path d="M21 12a9 9 0 11-6.219-8.56"></path>
                    </svg>
                    <span>评论加载中...</span>
                </div>
            </div>
        `;
        
        const wrapper = this.container.parentElement;
        if (wrapper) {
            wrapper.innerHTML = skeletonHtml;
        }
    }

    /**
     * Show loading state for the comment list (Overlay)
     */
    showListLoading() {
        if (!this.listContainer) return;
        
        // Create overlay if not exists
        let overlay = this.listContainer.querySelector('.comment-list-loading');
        if (!overlay) {
            overlay = document.createElement('div');
            overlay.className = 'comment-list-loading';
            overlay.innerHTML = `
                <div class="comment-loading-spinner">
                    <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin">
                        <path d="M21 12a9 9 0 11-6.219-8.56"></path>
                    </svg>
                    <span>加载中...</span>
                </div>
            `;
            // Ensure relative positioning on container
            if (getComputedStyle(this.listContainer).position === 'static') {
                this.listContainer.style.position = 'relative';
            }
            this.listContainer.appendChild(overlay);
        }
        
        overlay.style.display = 'flex';
        this.listContainer.classList.add('is-loading');
    }

    /**
     * Hide loading state for the comment list
     */
    hideListLoading() {
        if (!this.listContainer) return;
        
        const overlay = this.listContainer.querySelector('.comment-list-loading');
        if (overlay) {
            overlay.style.display = 'none';
        }
        this.listContainer.classList.remove('is-loading');
    }

    /**
     * Replace the comment list content
     * @param {string} html 
     */
    updateList(html) {
        if (this.listContainer) {
            this.listContainer.innerHTML = html;
            this.refreshPlugins();
            this.listContainer.classList.remove('is-loading');
        }
    }

    /**
     * Append comments to the list (for load more)
     * @param {string} html 
     */
    appendList(html) {
        if (this.listContainer) {
            const oldLoadMore = this.listContainer.querySelector('.comment-load-more');
            if (oldLoadMore) oldLoadMore.remove();

            const temp = document.createElement('div');
            temp.innerHTML = html;
            
            while (temp.firstChild) {
                this.listContainer.appendChild(temp.firstChild);
            }
            
            this.refreshPlugins();
        }
    }

    /**
     * Update comment count
     * @param {string} count 
     */
    updateCount(count) {
        const countEl = this.container.querySelector('.comment-section__count');
        if (countEl && count) {
            countEl.textContent = count;
        }
    }

    /**
     * Refresh 
     */
    refreshPlugins() {
        if (typeof CodeArea !== 'undefined') {
            new CodeArea(this.container);
        }
        
        // Update time tags
        updateTimeTags(this.container);
        
        // Update lazy load images if LazyLoad is defined
        if (typeof LazyLoad !== 'undefined') {
             new LazyLoad({
                 container: this.container
             });
        }
    }

    /**
     * Move reply form to a specific comment
     * @param {HTMLElement} commentItem 
     * @param {string} replyId 
     */
    showReplyForm(commentItem, replyId) {
        let form = this.container.querySelector('form.comment-form');
        
        // 如果在 container 中找不到,尝试从抽屉中找(可能上次关闭时还没移回)
        if (!form && this.drawer) {
            form = this.drawer.querySelector('form.comment-form');
            // 如果表单在抽屉中,先移回原位
            if (form) {
                const originalWrapper = this.container.querySelector('.comment-form-wrapper');
                if (originalWrapper) {
                    originalWrapper.appendChild(form);
                    form.classList.remove('comment-form--reply');
                }
            }
        }
        
        if (!form) return;

        // Detect mobile
        const isMobile = window.innerWidth <= 768;

        if (isMobile) {
            this.showMobileReplyDrawer(commentItem, replyId, form);
        } else {
            this.showDesktopReplyForm(commentItem, replyId, form);
        }
    }

    /**
     * Show reply form in desktop mode (inline)
     * @private
     */
    showDesktopReplyForm(commentItem, replyId, form) {
        // Reset form state
        this.resetForm(form);
        
        // Set reply ID
        const replyInput = form.querySelector('input[name="replyId"]');
        if (replyInput) replyInput.value = replyId;

        // Show cancel button
        const cancelBtn = form.querySelector('.comment-form__cancel-btn');
        if (cancelBtn) cancelBtn.style.display = 'inline-block';

        // Move form
        const targetBody = commentItem.querySelector('.comment-item__body');
        const actionsEl = targetBody.querySelector('.comment-item__actions');
        
        if (actionsEl) {
            actionsEl.after(form);
        } else {
            targetBody.appendChild(form);
        }

        form.classList.add('comment-form--reply');
        
        // Scroll to form
        form.scrollIntoView({ behavior: 'smooth', block: 'center' });
        
        // Focus textarea
        const textarea = form.querySelector('textarea');
        if (textarea) textarea.focus();
    }

    /**
     * Show reply form in mobile drawer
     * @private
     */
    showMobileReplyDrawer(commentItem, replyId, form) {
        // Extract comment context
        const avatar = commentItem.querySelector('.comment-avatar-img');
        const author = commentItem.querySelector('.comment-item__author, .comment-item__user-info');
        const content = commentItem.querySelector('.comment-item__content');

        const contextData = {
            avatarSrc: avatar ? avatar.src : '',
            authorName: author ? author.textContent.trim() : '未知用户',
            contentText: content ? content.textContent.trim().substring(0, 100) : ''
        };

        // Create drawer if not exists
        if (!this.drawer) {
            this.createMobileDrawer();
        }

        // Update context
        this.updateDrawerContext(contextData);

        // Reset form and set reply ID
        this.resetForm(form);
        const replyInput = form.querySelector('input[name="replyId"]');
        if (replyInput) replyInput.value = replyId;

        // Move form to drawer
        const drawerBody = this.drawer.querySelector('.comment-reply-drawer__body');
        if (drawerBody) {
            drawerBody.innerHTML = '';
            drawerBody.appendChild(form);
        }

        // Show drawer
        this.openDrawer();
    }

    /**
     * Create mobile drawer structure
     * @private
     */
    createMobileDrawer() {
        // Create overlay
        this.drawerOverlay = document.createElement('div');
        this.drawerOverlay.className = 'comment-reply-drawer__overlay';
        this.drawerOverlay.addEventListener('click', () => this.closeDrawer());

        // Create drawer
        this.drawer = document.createElement('div');
        this.drawer.className = 'comment-reply-drawer';
        this.drawer.innerHTML = `
            <div class="comment-reply-drawer__header">
                <span class="comment-reply-drawer__title">回复评论</span>
                <button type="button" class="comment-reply-drawer__close" aria-label="关闭">
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <line x1="18" y1="6" x2="6" y2="18"></line>
                        <line x1="6" y1="6" x2="18" y2="18"></line>
                    </svg>
                </button>
            </div>
            <div class="comment-reply-drawer__context">
                <div class="comment-reply-drawer__context-label">回复给</div>
                <div class="comment-reply-drawer__context-item">
                    <img class="comment-reply-drawer__context-avatar" src="" alt="">
                    <div class="comment-reply-drawer__context-body">
                        <div class="comment-reply-drawer__context-author"></div>
                        <div class="comment-reply-drawer__context-text"></div>
                    </div>
                </div>
            </div>
            <div class="comment-reply-drawer__body"></div>
        `;

        const closeBtn = this.drawer.querySelector('.comment-reply-drawer__close');
        if (closeBtn) {
            closeBtn.addEventListener('click', () => this.closeDrawer());
        }

        this.container.appendChild(this.drawerOverlay);
        this.container.appendChild(this.drawer);
    }

    /**
     * Update drawer context
     * @private
     */
    updateDrawerContext(data) {
        if (!this.drawer) return;

        const avatar = this.drawer.querySelector('.comment-reply-drawer__context-avatar');
        const author = this.drawer.querySelector('.comment-reply-drawer__context-author');
        const text = this.drawer.querySelector('.comment-reply-drawer__context-text');

        if (avatar) avatar.src = data.avatarSrc;
        if (author) author.textContent = data.authorName;
        if (text) text.textContent = data.contentText;
    }

    /**
     * Open drawer
     * @private
     */
    openDrawer() {
        if (!this.drawer || !this.drawerOverlay) return;

        // Prevent body scroll
        document.body.style.overflow = 'hidden';

        this.drawerOverlay.classList.add('comment-reply-drawer__overlay--visible');
        requestAnimationFrame(() => {
            this.drawer.classList.add('comment-reply-drawer--open');
        });

        // Focus textarea after animation
        setTimeout(() => {
            const textarea = this.drawer.querySelector('.comment-form__editor');
            if (textarea) textarea.focus();
        }, 350);
    }

    /**
     * Close drawer
     */
    closeDrawer() {
        if (!this.drawer || !this.drawerOverlay) return;

        // 立即移回表单到原位,避免时序问题
        const form = this.drawer.querySelector('form.comment-form');
        if (form) {
            const originalWrapper = this.container.querySelector('.comment-form-wrapper');
            if (originalWrapper) {
                this.resetForm(form);
                originalWrapper.appendChild(form);
                form.classList.remove('comment-form--reply');
                
                const cancelBtn = form.querySelector('.comment-form__cancel-btn');
                if (cancelBtn) cancelBtn.style.display = 'none';
            }
        }

        // 关闭抽屉 UI
        this.drawer.classList.remove('comment-reply-drawer--open');
        this.drawerOverlay.classList.remove('comment-reply-drawer__overlay--visible');

        // Restore body scroll
        document.body.style.overflow = '';
    }

    /**
     * Reset form to original position
     */
    resetReplyForm() {
        const form = this.container.querySelector('form.comment-form') || 
                     (this.drawer && this.drawer.querySelector('form.comment-form'));
        const originalWrapper = this.container.querySelector('.comment-form-wrapper');
        
        if (form && originalWrapper) {
            this.resetForm(form);
            originalWrapper.appendChild(form);
            form.classList.remove('comment-form--reply');
            
            const cancelBtn = form.querySelector('.comment-form__cancel-btn');
            if (cancelBtn) cancelBtn.style.display = 'none';
        }

        // Close drawer if open (避免循环调用)
        if (this.drawer && this.drawer.classList.contains('comment-reply-drawer--open')) {
            // 只关闭 UI,不再调用 resetReplyForm
            this.drawer.classList.remove('comment-reply-drawer--open');
            if (this.drawerOverlay) {
                this.drawerOverlay.classList.remove('comment-reply-drawer__overlay--visible');
            }
            document.body.style.overflow = '';
        }
    }

    resetForm(form) {
        form.reset();
        const replyInput = form.querySelector('input[name="replyId"]');
        if (replyInput) replyInput.value = '';
        this.setButtonState(form.querySelector('button[type="submit"]'), 'normal');
        this.hideListLoading();
        const textarea = form.querySelector('.comment-form__editor');
        if (textarea) {
            this.updateCharCount(textarea);
        }
    }

    insertMarkdown(text) {
        const form = this.container.querySelector('form.comment-form');
        const textarea = form ? form.querySelector('textarea') : null;
        if (textarea) {
            const start = textarea.selectionStart;
            const end = textarea.selectionEnd;
            const value = textarea.value;
            textarea.value = value.substring(0, start) + text + value.substring(end);
            textarea.focus();
            textarea.selectionStart = textarea.selectionEnd = start + text.length;
            this.updateCharCount(textarea);
        }
    }

    /**
     * Update character count display
     * @param {HTMLTextAreaElement} textarea
     */
    updateCharCount(textarea) {
        const form = textarea.closest('.comment-form');
        if (!form) return;

        const counter = form.querySelector('.comment-form__char-count');
        if (!counter) return;

        const max = parseInt(textarea.getAttribute('maxlength')) || 1000;
        const current = textarea.value.length;
        counter.textContent = `${current}/${max}`;

        counter.classList.remove(
            'comment-form__char-count--warn',
            'comment-form__char-count--limit'
        );
        if (current >= max) {
            counter.classList.add('comment-form__char-count--limit');
        } else if (current > max * 0.9) {
            counter.classList.add('comment-form__char-count--warn');
        }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个用于处理评论系统UI交互的JavaScript模块,主要负责DOM操作和用户界面管理。让我详细解释一下这个代码的结构和功能:

主要功能概述

这个 CommentUI 类是一个评论系统的前端UI管理器,处理评论的显示、交互和状态管理。

核心组件

1. 构造函数和初始化

constructor(element) {
    this.container = element || document.querySelector('.comment-section');
    this.listContainer = this.container ? this.container.querySelector('.comment-list') : null;
    this.drawer = null;
    this.drawerOverlay = null;
    
    // 定义了常用的SVG图标
    this.icons = {
        loading: `<svg class="animate-spin">...`, // 加载图标
        send: `<svg>...`,                        // 发送图标  
        error: `<svg>...`                        // 错误图标
    };
}

2. 按钮状态管理

setButtonState(btn, state, msg)
  • 管理提交按钮的三种状态:正常、加载中、错误
  • 动态更新按钮的图标、文本和CSS类

3. 加载状态显示

  • showListSkeleton(): 显示骨架屏加载状态
  • showListLoading() / hideListLoading(): 显示/隐藏列表加载覆盖层

4. 评论列表管理

  • updateList(html): 替换整个评论列表内容
  • appendList(html): 追加新评论(用于"加载更多"功能)
  • updateCount(count): 更新评论数量显示

5. 插件刷新机制

refreshPlugins() {
    if (typeof CodeArea !== 'undefined') {
        new CodeArea(this.container);
    }
    updateTimeTags(this.container);
    if (typeof LazyLoad !== 'undefined') {
        new LazyLoad({ container: this.container });
    }
}

在内容更新后重新初始化相关插件(代码高亮、时间标签、懒加载等)

响应式回复功能

这是这个类最复杂的部分,支持桌面端和移动端的不同交互方式:

桌面端回复 (showDesktopReplyForm)

  • 将回复表单内联显示在目标评论下方
  • 平滑滚动到表单位置
  • 自动聚焦到文本区域

移动端回复 (showMobileReplyDrawer)

  • 创建底部抽屉式界面
  • 显示被回复评论的上下文信息
  • 阻止背景滚动
  • 提供关闭按钮和遮罩层交互

抽屉管理

createMobileDrawer()    // 创建抽屉DOM结构
updateDrawerContext()   // 更新上下文信息
openDrawer()           // 打开抽屉
closeDrawer()          // 关闭抽屉

表单管理

表单重置和移动

resetReplyForm()  // 将表单重置并移回原位
resetForm(form)   // 重置表单状态

Markdown支持

insertMarkdown(text)  // 在光标位置插入Markdown文本

字符计数

updateCharCount(textarea)  // 更新字符计数显示,包含警告状态

设计特点

  1. 响应式设计: 根据屏幕宽度自动切换桌面/移动端交互模式
  2. 状态管理: 完善的加载、错误、正常状态切换
  3. 用户体验: 平滑动画、自动聚焦、防止误操作
  4. 模块化: 职责清晰,易于维护和扩展
  5. 兼容性: 支持多种第三方插件的集成

这个类是一个典型的现代前端UI管理器,处理了评论系统中几乎所有的用户界面交互需求。

评论加载中...