Skip to content

Instantly share code, notes, and snippets.

@alexispurslane
Last active August 27, 2025 20:20
Show Gist options
  • Save alexispurslane/8ad9b33d6f71f2c6522a3c18e7d33c87 to your computer and use it in GitHub Desktop.
Save alexispurslane/8ad9b33d6f71f2c6522a3c18e7d33c87 to your computer and use it in GitHub Desktop.
Not mine, totally vibe coded, but a useful little totally local and self-contained thing you can embed into any HTML page to make it so that users can highlight and add comments and export/import them to share.
<style>
/* --- Commenting UI Styles --- */
:root {
--comment-highlight-bg: rgba(51, 255, 153, 0.4);
--comment-ui-bg: #1a2a1a;
--comment-ui-text: #e8f8e8;
--comment-ui-accent: #33ff99;
--comment-ui-border: #6a826a;
}
.comment-highlight {
background-color: var(--comment-highlight-bg);
border-bottom: 2px dashed var(--comment-ui-accent);
cursor: pointer;
color: inherit;
}
/* --- Main Comment Creation Drawer --- */
#comment-ui-container {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: var(--comment-ui-bg);
color: var(--comment-ui-text);
box-shadow: 0 -2px 10px rgba(0,0,0,0.5);
z-index: 10000;
transform: translateY(100%);
transition: transform 0.3s ease-in-out;
padding: 1rem;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 0.75rem;
border-top: 1px solid var(--comment-ui-border);
}
#comment-ui-container.visible {
transform: translateY(0);
}
#comment-ui-container button {
background-color: var(--comment-ui-accent);
color: var(--bg-color);
font-weight: bold;
border: none;
padding: 0.6rem 1rem;
border-radius: 5px;
cursor: pointer;
font-size: 0.9rem;
font-family: sans-serif;
text-align: center;
}
#comment-ui-container button.secondary {
background-color: var(--comment-ui-border);
color: var(--comment-ui-text);
font-weight: normal;
}
#comment-ui-container button:hover { opacity: 0.85; }
#comment-input-area {
width: 100%;
background-color: #0c140c;
color: var(--comment-ui-text);
border: 1px solid var(--comment-ui-border);
border-radius: 5px;
padding: 0.5rem;
font-size: 1rem;
font-family: sans-serif;
min-height: 80px;
box-sizing: border-box;
resize: vertical;
}
#comment-form {
display: none;
flex-direction: column;
gap: 0.75rem;
}
#comment-form .form-buttons {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
/* --- Tooltip --- */
#comment-tooltip {
position: absolute;
display: none;
background-color: var(--comment-ui-bg);
color: var(--comment-ui-text);
border: 1px solid var(--comment-ui-border);
border-radius: 5px;
padding: 0.8rem;
max-width: 300px;
font-size: 0.9rem;
z-index: 10001;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
overflow-wrap: break-word;
font-family: sans-serif;
line-height: 1.5;
}
.delete-comment-btn {
background: var(--comment-ui-border);
color: var(--comment-ui-text);
border: none;
padding: 4px 8px;
margin-top: 8px;
border-radius: 3px;
cursor: pointer;
display: block;
width: 100%;
font-size: 0.8rem;
}
.delete-comment-btn:hover { background: #c13434; }
/* --- Management Widget in Corner --- */
#comment-widget-container {
position: fixed;
bottom: 15px;
right: 15px;
z-index: 9999;
display: flex;
flex-direction: column-reverse;
align-items: flex-end;
}
#comment-counter {
background-color: rgba(26, 42, 26, 0.8);
backdrop-filter: blur(5px);
color: var(--comment-ui-text);
padding: 5px 12px;
margin-top: 8px;
border-radius: 15px;
font-size: 0.85rem;
font-family: sans-serif;
display: none;
box-shadow: 0 1px 4px rgba(0,0,0,0.4);
border: 1px solid var(--comment-ui-border);
cursor: default;
transition: transform 0.2s ease-out;
}
#comment-widget-prompt {
background-color: rgba(26, 42, 26, 0.8);
backdrop-filter: blur(5px);
color: var(--comment-ui-text);
padding: 5px 10px;
margin-top: 8px;
border-radius: 15px;
font-size: 1rem;
display: none; /* Controlled by JS */
box-shadow: 0 1px 4px rgba(0,0,0,0.4);
border: 1px solid var(--comment-ui-border);
cursor: default;
transition: transform 0.2s ease-out;
line-height: 1;
}
#comment-management-wrapper {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
opacity: 0;
visibility: hidden;
transform: translateY(10px);
transition: all 0.2s ease-out;
}
#comment-management-flyout {
display: flex;
gap: 0.5rem;
padding: 8px;
background-color: rgba(26, 42, 26, 0.9);
backdrop-filter: blur(5px);
border: 1px solid var(--comment-ui-border);
border-radius: 8px;
}
#comment-management-help-text {
color: var(--comment-ui-border);
font-size: 0.75rem;
font-family: sans-serif;
text-align: right;
padding: 0 4px;
max-width: 250px;
}
#comment-widget-container:hover #comment-management-wrapper {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
#comment-widget-container:hover #comment-counter,
#comment-widget-container:hover #comment-widget-prompt {
transform: scale(1.05);
}
#comment-management-flyout button,
#comment-management-flyout .file-input-label {
background-color: var(--comment-ui-accent);
color: var(--bg-color);
font-weight: bold;
border: none;
padding: 0.5rem 0.8rem;
border-radius: 5px;
cursor: pointer;
font-size: 0.8rem;
font-family: sans-serif;
white-space: nowrap;
/* Ensure consistent size */
display: flex;
align-items: center;
justify-content: center;
}
#comment-management-flyout button.secondary {
background-color: var(--comment-ui-border);
color: var(--comment-ui-text);
font-weight: normal;
}
#comment-management-flyout button:hover,
#comment-management-flyout .file-input-label:hover {
opacity: 0.85;
}
#copy-link-btn {
background-color: #4A5D4A; /* A slightly different shade */
}
#import-comments-input { display: none; }
/* --- Help Text and Keybinding Styles --- */
.comment-help-text {
font-size: 0.8rem;
color: var(--comment-ui-border);
text-align: center;
width: 100%;
}
.comment-help-text kbd {
background-color: #0c140c;
border: 1px solid var(--comment-ui-border);
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 0.75rem;
}
/* --- Comment Summary Section Styles --- */
#comment-summary-section {
max-width: 60em;
margin: 5em auto;
padding: 2em;
border-top: 2px solid var(--comment-ui-border);
color: var(--comment-ui-text);
}
#comment-summary-section h2 {
font-family: var(--font-heading);
color: var(--comment-ui-accent);
text-transform: uppercase;
margin-top: 0;
border-bottom: none;
margin-bottom: 1em;
}
#comment-summary-list {
list-style: none;
padding: 0;
margin: 0;
}
#comment-summary-list li {
padding: 1.5em;
border-bottom: 1px solid var(--comment-ui-border);
cursor: pointer;
transition: background-color 0.2s ease;
position: relative;
}
#comment-summary-list li:hover {
background-color: var(--comment-ui-bg);
}
#comment-summary-list li blockquote {
margin: 0 0 0.5em 0;
padding-left: 1em;
border-left: 3px solid var(--comment-ui-accent);
color: var(--comment-ui-text);
font-style: italic;
opacity: 0.8;
}
#comment-summary-list li p {
margin: 0;
color: var(--comment-ui-text);
font-family: sans-serif;
}
.summary-delete-btn {
position: absolute;
top: 5px;
right: 5px;
font-size: 1.5rem;
color: var(--comment-ui-border);
transition: color 0.2s ease;
line-height: 1;
padding: 10px;
}
.summary-delete-btn:hover {
color: #c13434;
}
/* --- Mobile Responsiveness --- */
@media (min-width: 600px) {
#comment-ui-container {
width: auto;
left: 50%;
transform: translateX(-50%) translateY(120%);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
#comment-ui-container.visible { transform: translateX(-50%) translateY(0); }
}
</style>
<!-- The Tooltip and Counter now live outside the main UI container -->
<div id="comment-tooltip"></div>
<!-- New Widget Container -->
<div id="comment-widget-container">
<div id="comment-counter"></div>
<div id="comment-management-wrapper">
<div id="comment-management-flyout">
<!-- NEW BUTTON HERE -->
<button id="copy-link-btn">Copy Link</button>
<label for="import-comments-input" class="file-input-label">Import</label>
<input type="file" id="import-comments-input" accept=".json"/>
<button id="export-comments-btn">Export</button>
<button id="clear-comments-btn" class="secondary">Clear All</button>
</div>
<small id="comment-management-help-text">
Comments are stored locally in your browser. Use Export/Import to share.
</small>
</div>
</div>
<!-- Simplified Comment Creation Drawer -->
<div id="comment-ui-container">
<button id="add-comment-btn" style="display: none;">Add Comment</button>
<div id="add-comment-help-text" class="comment-help-text" style="display: none;">
Press <kbd>Shift</kbd> + <kbd>Enter</kbd> to add comment
</div>
<form id="comment-form">
<textarea id="comment-input-area" placeholder="Type your comment here..."></textarea>
<div class="comment-help-text">
<kbd>Shift</kbd> + <kbd>Enter</kbd> to save, <kbd>Esc</kbd> to cancel
</div>
<div class="form-buttons">
<button type="button" id="cancel-comment-btn" class="secondary">Cancel</button>
<button type="submit" id="save-comment-btn">Save</button>
</div>
</form>
</div>
<section id="comment-summary-section" style="display: none;">
<h2>Comment Summary</h2>
<ol id="comment-summary-list"></ol>
</section>
<script>
// A guard to prevent initializing the system more than once.
window.commentingSystemInitialized = false;
window.initializeCommentingSystem = function(rootElementId) {
if (window.commentingSystemInitialized) {
console.warn("Commenting system has already been initialized. Aborting.");
return;
}
// --- STATE MANAGEMENT ---
let comments = [];
let currentSelectionData = null;
// --- DOM ELEMENTS ---
const commentContainer = document.getElementById('comment-ui-container');
const addCommentBtn = document.getElementById('add-comment-btn');
const addCommentHelpText = document.getElementById('add-comment-help-text');
const commentForm = document.getElementById('comment-form');
const commentInput = document.getElementById('comment-input-area');
const exportBtn = document.getElementById('export-comments-btn');
const importInput = document.getElementById('import-comments-input');
const clearBtn = document.getElementById('clear-comments-btn');
const copyLinkBtn = document.getElementById('copy-link-btn');
const tooltip = document.getElementById('comment-tooltip');
const commentCounter = document.getElementById('comment-counter');
const commentWidgetPrompt = document.getElementById('comment-widget-prompt');
const commentSummarySection = document.getElementById('comment-summary-section');
const commentSummaryList = document.getElementById('comment-summary-list');
const contentRoot = document.getElementById(rootElementId) || document.body;
if (!contentRoot) {
console.error(`Root element with ID "${rootElementId}" not found. Commenting system cannot start.`);
return;
}
// --- URL Data Handling & Compression Helpers ---
const toUrlSafeBase64 = (buffer) => {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
};
const fromUrlSafeBase64 = (str) => {
str = str.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4) { str += '='; }
const binaryString = atob(str);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
};
const compressData = async (data) => {
const stream = new Blob([JSON.stringify(data)], { type: 'application/json' })
.stream().pipeThrough(new CompressionStream('gzip'));
return await new Response(stream).arrayBuffer();
};
const decompressData = async (compressedBuffer) => {
try {
const stream = new Blob([compressedBuffer], { type: 'application/gzip' })
.stream().pipeThrough(new DecompressionStream('gzip'));
const decompressed = await new Response(stream).json();
return decompressed;
} catch (e) {
console.error("Decompression failed:", e);
return null;
}
};
// --- CORE FUNCTIONS ---
const getNodePath = (node) => {
const path = [];
while (node !== contentRoot && node) {
let sibling = node; let index = 0;
while ((sibling = sibling.previousSibling) != null) { index++; }
path.unshift(index);
node = node.parentNode;
}
return path;
};
const getNodeFromPath = (path) => {
let node = contentRoot;
for (const index of path) {
if (node && node.childNodes[index]) {
node = node.childNodes[index];
} else { return null; }
}
return node;
};
const serializeRange = (range) => {
if (!range) return null;
return {
startPath: getNodePath(range.startContainer),
startOffset: range.startOffset,
endPath: getNodePath(range.endContainer),
endOffset: range.endOffset,
text: range.toString()
};
};
const deserializeRange = (savedRange) => {
const startContainer = getNodeFromPath(savedRange.startPath);
const endContainer = getNodeFromPath(savedRange.endPath);
if (startContainer && endContainer) {
try {
const range = document.createRange();
const startOffset = Math.min(savedRange.startOffset, startContainer.textContent.length);
const endOffset = Math.min(savedRange.endOffset, endContainer.textContent.length);
range.setStart(startContainer, startOffset);
range.setEnd(endContainer, endOffset);
return range;
} catch (e) { console.error("Error setting range from path, falling back.", e, savedRange); }
}
console.warn("Path failed for highlight. Falling back to text search for:", savedRange.text);
const normalizedText = savedRange.text.replace(/\s+/g, ' ').trim();
if (!normalizedText) return null;
const walker = document.createTreeWalker(contentRoot, NodeFilter.SHOW_TEXT);
let node;
while (node = walker.nextNode()) {
const normalizedNodeText = node.nodeValue.replace(/\s+/g, ' ');
const index = normalizedNodeText.indexOf(normalizedText);
if (index !== -1) {
const range = document.createRange();
range.setStart(node, index);
range.setEnd(node, index + normalizedText.length);
return range;
}
}
console.error("Could not find text for highlight anywhere on the page:", savedRange);
return null;
};
const renderCommentSummary = () => {
commentSummaryList.innerHTML = '';
if (comments.length === 0) {
commentSummarySection.style.display = 'none';
return;
}
commentSummarySection.style.display = 'block';
const commentsWithElements = comments.map(comment => {
const el = document.querySelector(`.comment-highlight[data-comment-id="${comment.id}"]`);
return { comment, el };
}).filter(item => item.el);
commentsWithElements.sort((a, b) => a.el.offsetTop - b.el.offsetTop);
commentsWithElements.forEach(({ comment }) => {
const li = document.createElement('li');
li.dataset.commentId = comment.id;
const deleteBtn = document.createElement('span');
deleteBtn.className = 'summary-delete-btn';
deleteBtn.innerHTML = '&times;';
deleteBtn.dataset.commentId = comment.id;
const blockquote = document.createElement('blockquote');
const quoteText = comment.range.text.length > 100 ? comment.range.text.substring(0, 97) + '...' : comment.range.text;
blockquote.textContent = quoteText;
const p = document.createElement('p');
p.textContent = comment.text;
li.appendChild(deleteBtn);
li.appendChild(blockquote);
li.appendChild(p);
commentSummaryList.appendChild(li);
});
};
const getLastHighlightScrollPercent = () => {
const highlights = Array.from(document.querySelectorAll('.comment-highlight'));
if (highlights.length === 0) return null;
const lastHighlight = highlights.reduce((last, current) => current.offsetTop > last.offsetTop ? current : last);
const docHeight = document.documentElement.scrollHeight;
const viewportHeight = window.innerHeight;
const scrollableHeight = docHeight - viewportHeight;
if (scrollableHeight <= 0) return 100;
const percentage = (lastHighlight.offsetTop / scrollableHeight) * 100;
return Math.min(100, Math.round(percentage));
};
const updateCounter = () => {
const count = comments.length;
if (count > 0) {
const percentage = getLastHighlightScrollPercent();
let text = `${count} Comment${count > 1 ? 's' : ''}`;
if (percentage !== null) {
text += ` (${percentage}%)`;
}
commentCounter.textContent = text;
commentCounter.style.display = 'block';
commentWidgetPrompt.style.display = 'none';
} else {
commentCounter.style.display = 'none';
commentWidgetPrompt.style.display = 'block';
}
};
const applyHighlight = (range, commentId) => {
if (!range || range.collapsed) return;
try {
const marker = document.createElement('mark');
marker.className = 'comment-highlight';
marker.dataset.commentId = commentId;
range.surroundContents(marker);
addHighlightEventListeners(marker);
} catch (e) {
console.warn("surroundContents failed, likely a multi-element selection. Using robust fallback.", e);
const walker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_TEXT);
const textNodes = [];
while(walker.nextNode()) {
if(range.intersectsNode(walker.currentNode)) {
textNodes.push(walker.currentNode);
}
}
for (const node of textNodes) {
const highlightRange = document.createRange();
highlightRange.setStart(node, node === range.startContainer ? range.startOffset : 0);
highlightRange.setEnd(node, node === range.endContainer ? range.endOffset : node.textContent.length);
if (!highlightRange.collapsed) {
const marker = document.createElement('mark');
marker.className = 'comment-highlight';
marker.dataset.commentId = commentId;
highlightRange.surroundContents(marker);
addHighlightEventListeners(marker);
}
}
}
};
const saveCommentsToLocal = () => {
try { localStorage.setItem('userComments', JSON.stringify(comments)); }
catch (e) { console.error("Could not save comments to localStorage.", e); }
};
const applyAllHighlights = (commentsToApply) => {
const itemsWithRanges = commentsToApply.map(comment => ({
comment, range: deserializeRange(comment.range)
}));
itemsWithRanges.sort((a, b) => {
if (!a.range || !b.range) return 0;
return b.range.compareBoundaryPoints(Range.START_TO_START, a.range)
});
for (const item of itemsWithRanges) {
if (item.range) {
try {
applyHighlight(item.range, item.comment.id);
} catch (e) {
console.error("Failed to apply a highlight during import:", item.comment, e);
}
}
}
};
const loadInitialComments = async () => {
const hash = window.location.hash.slice(1);
if (hash) {
console.log("Loading comments from URL...");
const buffer = fromUrlSafeBase64(hash);
const data = await decompressData(buffer);
if (data && Array.isArray(data)) {
comments = data;
applyAllHighlights(comments);
saveCommentsToLocal();
history.replaceState(null, '', window.location.pathname + window.location.search);
}
} else {
console.log("Loading comments from localStorage...");
const savedCommentsJSON = localStorage.getItem('userComments');
if (savedCommentsJSON) {
try {
const savedComments = JSON.parse(savedCommentsJSON);
if (Array.isArray(savedComments)) {
comments = savedComments;
applyAllHighlights(comments);
}
} catch (e) {
console.error("Could not parse comments from localStorage.", e);
localStorage.removeItem('userComments');
}
}
}
updateCounter();
renderCommentSummary();
};
const deleteComment = (commentId) => {
if (!confirm('Are you sure you want to delete this comment?')) return;
const commentIndex = comments.findIndex(c => c.id === commentId);
if (commentIndex > -1) {
comments.splice(commentIndex, 1);
saveCommentsToLocal();
document.querySelectorAll(`.comment-highlight[data-comment-id="${commentId}"]`).forEach(removeHighlight);
updateCounter();
hideTooltip();
renderCommentSummary();
}
};
const showTooltip = (highlightEl) => {
const commentId = highlightEl.dataset.commentId;
const comment = comments.find(c => c.id === commentId);
if (!comment) return;
tooltip.innerHTML = `
<span>${comment.text.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</span>
<button class="delete-comment-btn" data-comment-id="${commentId}">Delete</button>
`;
tooltip.querySelector('.delete-comment-btn').addEventListener('click', (e) => {
e.stopPropagation();
deleteComment(e.target.dataset.commentId);
});
tooltip.style.display = 'block';
tooltip.dataset.activeCommentId = commentId;
const rect = highlightEl.getBoundingClientRect();
tooltip.style.left = `${rect.left + window.scrollX}px`;
tooltip.style.top = `${rect.bottom + window.scrollY + 5}px`;
const tooltipRect = tooltip.getBoundingClientRect();
if (tooltipRect.right > window.innerWidth) {
tooltip.style.left = `${window.innerWidth - tooltipRect.width - 10 + window.scrollX}px`;
}
if (tooltipRect.top < 0) {
tooltip.style.top = `${rect.top + window.scrollY - tooltipRect.height - 5}px`;
}
};
const hideTooltip = () => {
tooltip.style.display = 'none';
tooltip.dataset.activeCommentId = '';
};
const addHighlightEventListeners = (marker) => {
marker.addEventListener('click', (e) => {
e.stopPropagation();
if (tooltip.dataset.activeCommentId === marker.dataset.commentId) {
hideTooltip();
} else {
showTooltip(marker);
}
});
};
const removeHighlight = (el) => {
const parent = el.parentNode;
while (el.firstChild) parent.insertBefore(el.firstChild, el);
parent.removeChild(el);
};
const resetUI = () => {
commentContainer.classList.remove('visible');
addCommentBtn.style.display = 'none';
addCommentHelpText.style.display = 'none';
commentForm.style.display = 'none';
commentInput.value = '';
currentSelectionData = null;
};
const handleSelectionEnd = (e) => {
if (commentContainer.contains(e.target) || e.target.closest('.comment-highlight') || e.target.closest('#comment-widget-container')) return;
setTimeout(() => {
const selection = window.getSelection();
if (selection && !selection.isCollapsed && selection.rangeCount > 0) {
if (contentRoot.contains(selection.anchorNode) && contentRoot.contains(selection.focusNode)) {
commentContainer.classList.add('visible');
addCommentBtn.style.display = 'block';
addCommentHelpText.style.display = 'block';
commentForm.style.display = 'none';
}
} else {
if (document.activeElement !== commentInput) resetUI();
}
}, 10);
};
contentRoot.addEventListener('mouseup', handleSelectionEnd);
contentRoot.addEventListener('touchend', handleSelectionEnd);
addCommentBtn.addEventListener('click', () => {
const selection = window.getSelection();
if (selection && !selection.isCollapsed) {
currentSelectionData = serializeRange(selection.getRangeAt(0));
addCommentBtn.style.display = 'none';
addCommentHelpText.style.display = 'none';
commentForm.style.display = 'flex';
commentInput.focus();
} else { resetUI(); }
});
document.getElementById('cancel-comment-btn').addEventListener('click', () => {
resetUI();
if (window.getSelection) window.getSelection().removeAllRanges();
});
commentForm.addEventListener('submit', (e) => {
e.preventDefault();
const commentText = commentInput.value.trim();
if (commentText && currentSelectionData) {
const rangeToHighlight = deserializeRange(currentSelectionData);
if (!rangeToHighlight) { resetUI(); return; }
const newComment = {
id: `comment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
text: commentText,
range: currentSelectionData
};
comments.push(newComment);
saveCommentsToLocal();
applyHighlight(rangeToHighlight, newComment.id);
resetUI();
updateCounter();
renderCommentSummary();
if (window.getSelection) window.getSelection().removeAllRanges();
}
});
commentInput.addEventListener('focus', () => { document.body.style.overflow = 'hidden'; });
commentInput.addEventListener('blur', () => { document.body.style.overflow = ''; });
commentInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); commentForm.requestSubmit(); }
if (e.key === 'Escape') {
e.preventDefault();
resetUI();
if (window.getSelection) window.getSelection().removeAllRanges();
}
});
exportBtn.addEventListener('click', () => {
if (comments.length === 0) { return; }
const dataStr = JSON.stringify(comments, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const a = document.createElement('a'); a.href = url; a.download = 'comments.json';
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(url);
});
importInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const importedComments = JSON.parse(event.target.result);
if (Array.isArray(importedComments)) {
document.querySelectorAll('.comment-highlight').forEach(removeHighlight);
comments = importedComments;
applyAllHighlights(comments);
saveCommentsToLocal();
updateCounter();
renderCommentSummary();
}
} catch (err) {
console.error(err);
alert('Error: Could not parse the JSON file.');
}
};
reader.readAsText(file);
e.target.value = '';
});
clearBtn.addEventListener('click', () => {
if (confirm('Are you sure you want to permanently delete all comments from this page?')) {
comments = [];
localStorage.removeItem('userComments');
document.querySelectorAll('.comment-highlight').forEach(removeHighlight);
updateCounter();
renderCommentSummary();
}
});
copyLinkBtn.addEventListener('click', async () => {
if (comments.length === 0) return;
const originalText = copyLinkBtn.textContent;
copyLinkBtn.textContent = 'Compressing...';
copyLinkBtn.disabled = true;
try {
const compressed = await compressData(comments);
const base64 = toUrlSafeBase64(compressed);
const url = new URL(window.location);
url.hash = base64;
// NEW: URL Length Check
const URL_LENGTH_LIMIT = 2097152;
if (url.href.length > URL_LENGTH_LIMIT) {
alert("Warning: The generated link is very long and may not work in all browsers or applications.\n\nExporting a JSON file is recommended for many comments.");
}
await navigator.clipboard.writeText(url.href);
copyLinkBtn.textContent = 'Copied!';
} catch (err) {
console.error("Failed to copy link:", err);
copyLinkBtn.textContent = 'Error!';
} finally {
setTimeout(() => {
copyLinkBtn.textContent = originalText;
copyLinkBtn.disabled = false;
}, 2000);
}
});
document.addEventListener('click', (e) => {
if (!e.target.closest('.comment-highlight') && !e.target.closest('#comment-tooltip')) {
hideTooltip();
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.shiftKey && document.activeElement !== commentInput) {
if (addCommentBtn.style.display === 'block') {
e.preventDefault();
addCommentBtn.click();
}
}
});
commentSummaryList.addEventListener('click', (e) => {
const deleteButton = e.target.closest('.summary-delete-btn');
if (deleteButton) {
e.stopPropagation();
deleteComment(deleteButton.dataset.commentId);
return;
}
const li = e.target.closest('li');
if (li && li.dataset.commentId) {
const commentId = li.dataset.commentId;
const highlightElement = document.querySelector(`.comment-highlight[data-comment-id="${commentId}"]`);
if (highlightElement) {
highlightElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => { showTooltip(highlightElement); }, 300);
}
}
});
loadInitialComments();
window.commentingSystemInitialized = true;
console.log("Commenting system initialized successfully.");
};
document.addEventListener('DOMContentLoaded', () => {
initializeCommentingSystem('content');
});
</script>
<!-- END: Text Highlighting and Commenting Feature -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment