window.addEventListener('DOMContentLoaded', (event) => { const quotes = document.getElementsByClassName('quote'); let hoverLoading = {}; let hovering = false; let lastHover; const toggleDottedUnderlines = (hoveredPost, id) => { let uniqueQuotes = new Set(); hoveredPost.querySelectorAll('.post-message .quote').forEach(q => uniqueQuotes.add(q.href)); if (uniqueQuotes.size > 1) { const matchingQuotes = hoveredPost.querySelectorAll(`.post-message .quote[href$="${id}"]`); for (let i = 0; i < matchingQuotes.length; i++) { const mq = matchingQuotes[i]; mq.style.borderBottom = mq.style.borderBottom == '' ? '1px dashed' : ''; mq.style.textDecoration = mq.style.textDecoration == '' ? 'none' : ''; } } } const isVisible = (e) => { const top = e.getBoundingClientRect().top; const bottom = e.getBoundingClientRect().bottom; const height = window.innerHeight; return top >= 38 && bottom <= height; } const setFloatPos = (quote, float, xpos, ypos) => { const quotepos = quote.getBoundingClientRect(); const post = float.firstChild; const iw = document.body.offsetWidth-10; const ih = window.innerHeight; const left = xpos < iw/2; if (left) { float.style.left = `${quotepos.right+10}px`; if (quotepos.right+10+post.offsetWidth >= iw) { float.style.right = '5px'; } } else { float.style.right = `${iw-quotepos.left+15}px`; if (quotepos.left-15 < post.offsetWidth) { float.style.left = '5px'; } } const top = ypos < ih/2; if (top && quotepos.bottom+post.offsetHeight < ih) { float.style.top = `${quotepos.top}px`; } else if (!top && post.offsetHeight < ypos) { float.style.top = `${quotepos.bottom-post.offsetHeight}px`; } else { float.style.top = '42px'; } } const floatPost = (quote, post, xpos, ypos) => { const clone = document.createElement('div'); clone.id = 'float'; clone.classList.remove('hoverhighlighted'); clone.appendChild(post.cloneNode(true)); document.body.appendChild(clone); setFloatPos(quote, clone, xpos, ypos); return clone; }; const toggleHighlightPost = async function (e) { hovering = e.type === 'mouseover'; let jsonParts = this.pathname.replace(/\.html$/, '.json').split('/'); let jsonPath; if ((isManage || isModView) && jsonParts.length === 5) { jsonParts.splice(2,1); //remove manage from json url } jsonPath = jsonParts.join('/'); if (!this.hash) { return; //non-post number board quote } const float = document.getElementById('float'); if (float) { document.body.removeChild(float); } const parentPost = this.closest('.post-container'); let thisId = 0; if (parentPost) { thisId = parentPost.dataset.postId; } const loading = Date.now(); lastHover = loading; const hash = this.hash.substring(1); const anchor = document.getElementById(hash); let hoveredPost, postJson; if (anchor && jsonPath.split('/')[1] === anchor.nextSibling.dataset.board) { hoveredPost = anchor.nextSibling; } else { let hovercache = localStorage.getItem(`hovercache-${jsonPath}`); if (hovercache) { hovercache = JSON.parse(hovercache); if (hovercache.postId == hash) { postJson = hovercache; } else if (hovercache.replies.length > 0) { postJson = hovercache.replies.find(r => r.postId == hash); } } if (!postJson) {//wasnt cached or cache outdates this.style.cursor = 'wait'; let json; try { if (!hoverLoading[jsonPath]) { hoverLoading[jsonPath] = fetch(jsonPath).then(res => res.json()); } json = await hoverLoading[jsonPath]; } catch (e) { return console.error(e); } finally { this.style.cursor = ''; } if (json) { setLocalStorage(`hovercache-${jsonPath}`, JSON.stringify(json)); hoverCacheList.value = Object.keys(localStorage).filter(k => k.startsWith('hovercache')); if (json.postId == hash) { postJson = json; } else { postJson = json.replies.find(r => r.postId == hash); } } else { return localStorage.removeItem(`hovercache-${jsonPath}`); //thread deleted } } if (lastHover !== loading) { return; //dont show for ones not hovering } if (!postJson) { return; //post was deleted or missing } const postHtml = post({ post: postJson }); const wrap = document.createElement('div'); wrap.innerHTML = postHtml; hoveredPost = wrap.firstChild.nextSibling; } if (hovering && !isVisible(hoveredPost)) { hoveredPost = floatPost(this, hoveredPost, e.clientX, e.clientY); } else { hovering ? hoveredPost.classList.add('hoverhighlighted') : hoveredPost.classList.remove('hoverhighlighted'); } if (postJson) { //need this event so handlers like post hiding still apply to hover introduced posts const newPostEvent = new CustomEvent('addPost', { detail: { json: postJson, post: hoveredPost, postId: postJson.postId, hover: true } }); window.dispatchEvent(newPostEvent); } toggleDottedUnderlines(hoveredPost, thisId); } for (let i = 0; i < quotes.length; i++) { quotes[i].addEventListener('mouseover', toggleHighlightPost, false); quotes[i].addEventListener('mouseout', toggleHighlightPost, false); } window.addEventListener('addPost', function(e) { if (e.detail.hover) { return; //dont need to handle hovered posts for this } const post = e.detail.post; const newquotes = document.getElementsByClassName('quote'); //to get backlinks from replying posts. just an easy way. could make more efficient and only do necessary ones later. for (let i = 0; i < newquotes.length; i++) { newquotes[i].removeEventListener('mouseover', toggleHighlightPost); newquotes[i].removeEventListener('mouseout', toggleHighlightPost); newquotes[i].addEventListener('mouseover', toggleHighlightPost, false); newquotes[i].addEventListener('mouseout', toggleHighlightPost, false); } }); window.addEventListener('updatePostMessage', function(e) { const newquotes = e.detail.post.getElementsByClassName('quote'); for (let i = 0; i < newquotes.length; i++) { newquotes[i].addEventListener('mouseover', toggleHighlightPost, false); newquotes[i].addEventListener('mouseout', toggleHighlightPost, false); } }); }); window.addEventListener('settingsReady', function(e) { hoverCacheList = document.getElementById('hovercachelist-setting'); hoverCacheList.value = Object.keys(localStorage).filter(k => k.startsWith('hovercache')); const hoverCacheListClearButton = document.getElementById('hovercachelist-clear'); const clearHoverCacheList = () => { deleteStartsWith('hovercache'); hoverCacheList.value = ''; console.log('cleared cache'); } hoverCacheListClearButton.addEventListener('click', clearHoverCacheList, false); });