// Paragraph Comments Module (function() { var POPUP_ID = 'selection-popup'; // CSS for Paragraph Comments var style = document.createElement('style'); style.innerHTML = ` /* Selection Popup */ #${POPUP_ID} { position: absolute; display: none; background: var(--artalk-editer-color, #333); color: #fff; padding: 8px 16px; border-radius: 50px; font-size: 13px; cursor: pointer; z-index: 2147483647; transform: translate(-50%, -120%); box-shadow: 0 4px 20px rgba(0,0,0,0.25); transition: opacity 0.2s, transform 0.2s; pointer-events: auto; align-items: center; gap: 6px; white-space: nowrap; animation: popIn 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28); border: 1px solid rgba(255,255,255,0.1); font-weight: 500; } #${POPUP_ID}::after { content: ''; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: var(--artalk-editer-color, #333) transparent transparent transparent; } @keyframes popIn { 0% { transform: translate(-50%, -100%) scale(0.9); opacity: 0; } 100% { transform: translate(-50%, -120%) scale(1); opacity: 1; } } /* Paragraph Comment Marker - Pure Text, No Styles */ .paragraph-comment-marker { text-decoration: none !important; border-bottom: none !important; background: transparent !important; box-shadow: none !important; cursor: text; color: inherit; } /* Comment Count Bubble - Minimal SVG Style */ .paragraph-comment-count { display: inline-flex; align-items: center; justify-content: center; height: 16px; padding: 0 4px; background: transparent; color: var(--erji-color, #999); border-radius: 4px; font-size: 11px; font-weight: 500; margin-left: 2px; vertical-align: top; line-height: 1; cursor: pointer; position: relative; top: 2px; user-select: none; z-index: 10; transition: all 0.2s ease; gap: 2px; } .paragraph-comment-count svg { width: 12px; height: 12px; fill: currentColor; opacity: 0.7; } .paragraph-comment-count:hover { color: var(--theme-color, #4a90e2); background: rgba(0,0,0,0.03); } .paragraph-comment-count:hover svg { opacity: 1; transform: scale(1.1); } /* Comments Preview Modal */ .pc-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999; display: flex; justify-content: center; align-items: center; opacity: 0; pointer-events: none; transition: opacity 0.3s; } .pc-modal.show { opacity: 1; pointer-events: auto; } .pc-modal-content { background: var(--background-color, #fff); width: 90%; max-width: 500px; max-height: 80vh; border-radius: 12px; display: flex; flex-direction: column; box-shadow: 0 10px 25px rgba(0,0,0,0.2); transform: translateY(20px); transition: transform 0.3s; } .pc-modal.show .pc-modal-content { transform: translateY(0); } .pc-header { padding: 15px; border-bottom: 1px solid var(--frame-color, #eee); display: flex; justify-content: space-between; align-items: center; font-weight: bold; } .pc-body { flex: 1; overflow-y: auto; padding: 15px; } .pc-close { cursor: pointer; font-size: 20px; opacity: 0.6; } .pc-close:hover { opacity: 1; } .pc-quote-text { padding: 10px; background: var(--bankuai-color, #f6f6f6); border-left: 3px solid var(--text-highlight-color, #666); margin-bottom: 15px; font-size: 0.9em; color: var(--erji-color, #666); } .pc-comment-item { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid var(--frame-color, #eee); } .pc-comment-item:last-child { border-bottom: none; margin-bottom: 0; } .pc-user { font-weight: bold; font-size: 0.9em; margin-bottom: 5px; } .pc-content { font-size: 0.95em; } [data-theme="dark"] .pc-modal-content { background: var(--frame-dark-color, #1f2933); color: var(--text-content-dark-color, #d1d5db); } [data-theme="dark"] .pc-quote-text { background: rgba(0,0,0,0.2); } `; document.head.appendChild(style); // Create Popup Element var popup = document.createElement('div'); popup.id = POPUP_ID; // 使用 Remix Icon 的引用图标 popup.innerHTML = ' 引用评论'; document.body.appendChild(popup); // Create Modal Element var modal = document.createElement('div'); modal.className = 'pc-modal'; modal.innerHTML = `
`; document.body.appendChild(modal); // State var selectedText = ''; var range = null; var isInitialized = false; // Initialization function init() { if (!isInitialized) { document.addEventListener('mouseup', handleSelection); document.addEventListener('touchend', handleSelection); // Mobile support popup.addEventListener('click', handleQuoteClick); modal.querySelector('.pc-close').addEventListener('click', closeModal); modal.addEventListener('click', function(e) { if (e.target === modal) closeModal(); }); isInitialized = true; } // Hook into Artalk tryHookArtalk(); } function tryHookArtalk() { if (window.artalk) { setupArtalkHook(window.artalk); } else { // Performance: Check less frequently (1s) and stop after 10s var checkCount = 0; var checkArtalk = setInterval(function() { checkCount++; if (window.artalk) { clearInterval(checkArtalk); setupArtalkHook(window.artalk); } if (checkCount > 10) clearInterval(checkArtalk); // 10s timeout }, 1000); } } // Safe Get Comment List Helper function getCommentListSafe(artalkInstance) { var inst = artalkInstance || window.artalk; if (!inst) return []; if (typeof inst.getCommentList === 'function') return inst.getCommentList(); if (typeof inst.getComments === 'function') return inst.getComments(); // Try context access (Artalk v2) if (inst.ctx && typeof inst.ctx.get === 'function') { var list = inst.ctx.get('list'); if (list && typeof list.getComments === 'function') { return list.getComments(); } } return []; } function setupArtalkHook(artalk) { // Initial load var list = getCommentListSafe(artalk); if (list && list.length > 0) { processComments(); } // Listen for updates if (typeof artalk.on === 'function') { artalk.on('list-loaded', function() { setTimeout(processComments, 500); }); } } // Performance Optimized: Build Index Once function buildDocumentIndex(root) { var textNodes = []; var allText = ""; var nodeOffsets = []; var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false); var node; while (node = walker.nextNode()) { if (node.parentNode && ( node.parentNode.tagName === 'SCRIPT' || node.parentNode.tagName === 'STYLE' || node.parentNode.classList.contains('paragraph-comment-marker') || node.parentNode.classList.contains('paragraph-comment-count') )) continue; // Skip code blocks if (node.parentElement.closest('pre') || node.parentElement.closest('code')) continue; nodeOffsets.push(allText.length); textNodes.push(node); allText += node.nodeValue; } return { textNodes, allText, nodeOffsets }; } function processComments() { if (!window.artalk) return; // Cleanup old markers document.querySelectorAll('.paragraph-comment-marker').forEach(el => { var parent = el.parentNode; if (parent) { parent.replaceChild(document.createTextNode(el.innerText), el); parent.normalize(); } }); document.querySelectorAll('.paragraph-comment-count').forEach(el => el.remove()); var comments = getCommentListSafe(window.artalk); var quoteMap = {}; // Group comments by quote comments.forEach(c => { var content = c.content_marked || c.content; // Extract text from HTML blockquotes var tempDiv = document.createElement('div'); tempDiv.innerHTML = content; var blockquotes = tempDiv.querySelectorAll('blockquote'); blockquotes.forEach(bq => { var quoteText = bq.textContent.trim(); if (quoteText) { if (!quoteMap[quoteText]) quoteMap[quoteText] = []; if (!quoteMap[quoteText].some(existing => existing.id === c.id)) { quoteMap[quoteText].push(c); } } }); // Regex fallback if (!blockquotes.length && c.content && !c.content.includes('<')) { var match = c.content.match(/^> (.*?)(?:\n|$)/gm); if (match) { match.forEach(m => { var q = m.replace(/^> /, '').trim(); if (q) { if (!quoteMap[q]) quoteMap[q] = []; if (!quoteMap[q].some(existing => existing.id === c.id)) { quoteMap[q].push(c); } } }); } } }); var postContent = document.querySelector('.post-content') || document.querySelector('.article-content') || document.querySelector('article'); if (!postContent) return; // Build Index ONCE var docData = buildDocumentIndex(postContent); Object.keys(quoteMap).forEach(quote => { if (quote.length < 2) return; highlightQuote(docData, quote, quoteMap[quote]); }); } // Improved Highlight Logic using Pre-built Index function highlightQuote(docData, quote, comments) { if (!quote || quote.length < 5) return; var { allText, textNodes, nodeOffsets } = docData; // Fuzzy Match var cleanQuote = quote.trim().replace(/\s+/g, ' '); var cleanAllText = allText.replace(/\s+/g, ' '); var matchIndex = cleanAllText.indexOf(cleanQuote); if (matchIndex === -1) { matchIndex = allText.indexOf(quote); if (matchIndex === -1) matchIndex = allText.indexOf(quote.trim()); } if (matchIndex === -1) return; // Map back to raw index var currentCleanIndex = 0; var rawStart = -1; var rawEnd = -1; var inWhitespace = false; for (var i = 0; i < allText.length; i++) { var char = allText[i]; var isSpace = /\s/.test(char); if (isSpace) { if (!inWhitespace) { if (currentCleanIndex === matchIndex) rawStart = i; currentCleanIndex++; } inWhitespace = true; } else { if (currentCleanIndex === matchIndex) rawStart = i; currentCleanIndex++; inWhitespace = false; } if (rawStart !== -1 && currentCleanIndex === matchIndex + cleanQuote.length) { rawEnd = i + 1; break; } } if (rawStart !== -1 && rawEnd !== -1) { rangeFromOffsets(textNodes, nodeOffsets, rawStart, rawEnd - rawStart, comments); } } function rangeFromOffsets(textNodes, nodeOffsets, start, length, comments) { var end = start + length; var range = document.createRange(); var startNode = null, startOffset = 0; var endNode = null, endOffset = 0; for (var i = 0; i < textNodes.length; i++) { var nodeStart = nodeOffsets[i]; var nodeEnd = nodeStart + textNodes[i].nodeValue.length; if (!startNode && start >= nodeStart && start < nodeEnd) { startNode = textNodes[i]; startOffset = start - nodeStart; } if (!endNode && end > nodeStart && end <= nodeEnd) { endNode = textNodes[i]; endOffset = end - nodeStart; } if (startNode && endNode) break; } if (startNode && endNode) { try { range.setStart(startNode, startOffset); range.setEnd(endNode, endOffset); wrapRange(range, comments); } catch (e) { // Ignore } } } function wrapRange(range, comments) { try { var contents = range.extractContents(); var span = document.createElement('span'); span.className = 'paragraph-comment-marker'; span.appendChild(contents); range.insertNode(span); addBubble(span, comments.length, comments); } catch (e) { // Ignore } } function addBubble(marker, count, comments) { var bubble = document.createElement('sup'); bubble.className = 'paragraph-comment-count'; // Simple Chat Icon SVG bubble.innerHTML = ` ${count}`; marker.appendChild(bubble); bubble.addEventListener('click', function(e) { e.stopPropagation(); showCommentsModal(null, comments); }); } function handleSelection(e) { setTimeout(function() { var sel = window.getSelection(); if (sel.rangeCount > 0) { var r = sel.getRangeAt(0); var text = sel.toString().trim(); var postContent = document.querySelector('.post-content') || document.querySelector('.article-content') || document.querySelector('article'); if (!postContent) return; var isInside = postContent.contains(r.commonAncestorContainer); if (text.length > 0 && isInside) { selectedText = text; range = r; showPopup(r); } else { hidePopup(); } } else { hidePopup(); } }, 10); } function showPopup(r) { var rect = r.getBoundingClientRect(); popup.style.display = 'flex'; var top = rect.top + window.scrollY - 40; var left = rect.left + window.scrollX + (rect.width / 2); popup.style.top = top + 'px'; popup.style.left = left + 'px'; popup.style.opacity = 1; } function hidePopup() { popup.style.display = 'none'; popup.style.opacity = 0; } // Custom Smooth Scroll with Easing function smoothScrollTo(targetPosition, duration) { var startPosition = window.scrollY; var distance = targetPosition - startPosition; var startTime = null; function animation(currentTime) { if (startTime === null) startTime = currentTime; var timeElapsed = currentTime - startTime; var run = ease(timeElapsed, startPosition, distance, duration); window.scrollTo(0, run); if (timeElapsed < duration) requestAnimationFrame(animation); } // Ease-in-out Cubic function ease(t, b, c, d) { t /= d / 2; if (t < 1) return c / 2 * t * t * t + b; t -= 2; return c / 2 * (t * t * t + 2) + b; } requestAnimationFrame(animation); } function handleQuoteClick() { if (!window.artalk) return; var commentsEl = document.getElementById('Comments'); if (commentsEl) { // Target the editor area directly if possible, or the top of comments // Adjust offset to show the editor more clearly (e.g. 100px from top) var targetEl = document.querySelector('.atk-main-editor') || commentsEl; var elementPosition = targetEl.getBoundingClientRect().top + window.scrollY; // Align to 1/3 of the screen height (more comfortable for reading/typing) var offsetPosition = elementPosition - (window.innerHeight / 3); // Use custom smooth scroll (800ms) smoothScrollTo(offsetPosition, 800); // Highlight effect after scroll (Subtle Scale only, no blue border) setTimeout(function() { var editorEl = document.querySelector('.atk-main-editor'); if (editorEl) { editorEl.style.transition = 'transform 0.3s ease'; editorEl.style.transform = 'scale(1.02)'; setTimeout(function() { editorEl.style.transform = ''; }, 600); } }, 800); } var quoteContent = `> ${selectedText}\n\n`; var editor = window.artalk.editor; if (editor && typeof editor.setContent === 'function' && typeof editor.getContent === 'function') { editor.setContent(quoteContent + editor.getContent()); editor.focus(); } else { var textarea = document.querySelector('.atk-textarea'); if (textarea) { textarea.value = quoteContent + textarea.value; textarea.focus(); textarea.dispatchEvent(new Event('input', { bubbles: true })); textarea.dispatchEvent(new Event('change', { bubbles: true })); } } hidePopup(); window.getSelection().removeAllRanges(); } function showCommentsModal(quote, comments) { var body = modal.querySelector('.pc-body'); body.innerHTML = ''; if (!quote && comments.length > 0) { var firstContent = comments[0].content_marked || comments[0].content; var match = firstContent.match(/(.*?)<\/blockquote>/) || firstContent.match(/^> (.*?)(?:\n|$)/); if (match) quote = match[1]; } if (quote) { body.innerHTML += `${quote}`; } comments.forEach(c => { var item = document.createElement('div'); item.className = 'pc-comment-item'; var cleanContent = c.content.replace(/^> .*?\n+/g, '').replace(/.*?<\/blockquote>/g, ''); var badgeHtml = ''; if (c.badge) { badgeHtml = `${c.badge}`; } item.innerHTML = `${c.nick}${badgeHtml} ${c.date}${cleanContent}`; body.appendChild(item); }); modal.classList.add('show'); } function closeModal() { modal.classList.remove('show'); } window.initParagraphComments = init; init(); })();