clone of github.com/decent-newsroom/newsroom
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

90 lines
3.9 KiB

import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['button'];
connect() {
this.articleMain = this.element.querySelector('.article-main');
if (!this.articleMain) return;
const highlightsData = this.articleMain.getAttribute('data-highlights');
if (highlightsData) {
try {
this.highlights = JSON.parse(highlightsData);
this.applyHighlights();
} catch (e) {
console.error('Failed to parse highlights data:', e);
}
}
// Check if user has a saved preference
const enabled = localStorage.getItem('highlights-enabled') === 'true';
this.setHighlightsVisibility(enabled);
}
applyHighlights() {
if (!this.highlights || this.highlights.length === 0) return;
// Get all text nodes in the article
const walker = document.createTreeWalker(
this.articleMain,
NodeFilter.SHOW_TEXT,
null,
false
);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
// Skip if parent is already a highlight mark
if (node.parentElement.classList.contains('article-highlight')) {
continue;
}
textNodes.push(node);
}
// For each highlight, find and wrap matching text
this.highlights.forEach((highlight, index) => {
const searchText = highlight.content.trim();
if (!searchText) return;
textNodes.forEach(textNode => {
const text = textNode.textContent;
const startIndex = text.indexOf(searchText);
if (startIndex !== -1) {
// Split the text node and wrap the match
const range = document.createRange();
range.setStart(textNode, startIndex);
range.setEnd(textNode, startIndex + searchText.length);
const mark = document.createElement('mark');
mark.className = 'article-highlight';
mark.setAttribute('data-ui--highlights-toggle-target', 'highlight');
mark.setAttribute('title', 'Highlighted by others (' + new Date(highlight.created_at * 1000).toLocaleDateString() + ')');
try {
range.surroundContents(mark);
} catch (e) {
// If surroundContents fails (e.g., spans multiple nodes), try extraction
try {
const extracted = range.extractContents();
mark.appendChild(extracted);
range.insertNode(mark);
} catch (e2) {
console.warn('Failed to highlight text:', searchText, e2);
}
}
}
});
});
}
toggle() {
if (!this.hasButtonTarget) return;
const currentlyEnabled = this.buttonTarget.getAttribute('aria-pressed') === 'true';
const newState = !currentlyEnabled;
this.setHighlightsVisibility(newState);
// Save preference
localStorage.setItem('highlights-enabled', newState.toString());
}
setHighlightsVisibility(enabled) {
if (!this.hasButtonTarget) return;
this.buttonTarget.setAttribute('aria-pressed', enabled.toString());
const highlightElements = this.element.querySelectorAll('.article-highlight');
if (enabled) {
this.buttonTarget.classList.add('active');
highlightElements.forEach(el => el.classList.add('visible'));
} else {
this.buttonTarget.classList.remove('active');
highlightElements.forEach(el => el.classList.remove('visible'));
}
}
}