// ==UserScript== // @name Mangadex Post Autocomplete // @description Autocompletes @mention usernames. Maintains a small history of user posts you recently viewed and searches that for matches. Example image shown in additional info // @namespace https://github.com/Brandon-Beck // @version 0.0.3 // @grant unsafeWindow // @grant GM.getValue // @grant GM.setValue // @grant GM_getValue // @grant GM_setValue // @require https://greasyfork.org/scripts/372223-mangadex-common/code/Mangadex%20Common.js?version=631899 // @require https://cdn.rawgit.com/component/textarea-caret-position/af904838644c60a7c48b21ebcca8a533a5967074/index.js // @match https://mangadex.org/* // @downloadURL none // ==/UserScript== let xp = new XPath(); let posts=xp.new('//tr').with(xp.new().contains('@class','post')); // userid = Your user ID function UserHistory({read_posts_history=[],user_id,username}={}) { let uhist = this; if (!( uhist instanceof UserHistory) ) { return new UserHistory(); } function clipText(text,max_length){ return (text.length > max_length) ? text.substr(0, max_length - 1) + '…' : text; }; function getVisibleText( node ) { if( node.nodeType === Node.TEXT_NODE ) return node.textContent; let style = getComputedStyle( node ); if( style && style.display === 'none' ) return ''; let text = ''; for( let i=0; i { if (this.history.size > this.max_size){ //delete(this.history.entries().next().value[0]); this.history.shift(); } }; this.user_id=user_id; this.username=""; // get from userid this.max_size=200; this.history=read_posts_history; /* postid: { userid, username, userimg, excerpt, }*/ this.push = (post)=> { let post_id=parseInt(post.id.replace(/^post_/,'')); //this.history.delete(post_id); //this.history.set(post_id,{user_id:user_id,user_img:user_img,excerpt:excerpt}); //this.history.filter((e)=> { e.thread_id === thread_id } ); function array_move(arr, old_index, new_index) { arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); return arr; }; let exists=false; this.history.some((e,k) => { if (e.post_id === post_id) { exists=true; array_move(this.history,k,0); return true; } return false; }); if (exists) { return false; } let time = xp.new('.//span').with( xp.new('./span').with(xp.new().contains('@class','fa-clock')) ).getElement(post).title; let thread=xp.new('./td/span/a').with( xp.new('preceding-sibling::span').with(xp.new().contains('@class','fa-clock')) ).getElement(post).href; let thread_id = parseInt(thread.match(/\/thread\/(\d+)\//)[1]); let user=xp.new('./td/a[starts-with(@href,"/user/")]').getElement(post); let user_name=user.textContent; let user_id=parseInt(user.href.match(/\/user\/(\d+)\//)[1]); let user_img=xp.new('./td/img').with(xp.new().contains('@class','avatar')).getElement(post).src; let postContents=xp.new('./td/div').with('preceding-sibling::hr').getElement(post); let did_mention=Boolean(xp.new(`.//a[@href="https://mangadex.org/user/${uhist.user_id}"]`).getElement(postContents)); // cleanText. Hide spoilers and other invisible crap let cleanText=getVisibleText(postContents); let excerpt=clipText(cleanText,100); this.history.unshift({ thread_id:thread_id, user_name:user_name, did_mention:did_mention, post_id:post_id, user_id:user_id, user_img:user_img, excerpt:excerpt, time:time }); cleanupHistory(); }; this.autoComplete = (partial_name,{thread_id=0,case_sensitive=false,fuzzy=true}={})=> { let matches = this.history.filter( (e) => { // If this user is already marked as the highest priority match, dont process them anymore. let regex_partial_name = new RegExp(`${fuzzy ? '' : '^'}${partial_name}`,`${case_sensitive ? '' : 'i'}`); if (e.user_name.match(regex_partial_name)) { return true; } return false; } ).sort( (a,b) => { // List those from this thread before other threads { let am = a.thread_id === thread_id; let bm = b.thread_id === thread_id; if (am!==bm) { return bm; }; } // List people whos names start with partial before those with partial anywhere in name if (fuzzy) { let regex_partial_name = new RegExp(`^${partial_name}`,`${case_sensitive ? '' : 'i'}`); let am = a.user_name.match(regex_partial_name) != null; let bm = b.user_name.match(regex_partial_name) != null; if (am!==bm) { return bm; }; } // List those who mentioned us before those who did not. if (a.did_mention!==b.did_mention) { return b.did_mention; }; } ); let seen = {}; matches = matches.filter( (e) => { if (seen[e.user_id]) { return false; } seen[e.user_id]=true; return true; } ); return matches; }; return this; } function getCurrentUserID() { xp.new('id("navbarSupportedContent")').with(xp.new().contains('@class','navbarSupportedContent')); let current_user_id = xp.new('id("navbarSupportedContent")//a[contains(@href,"/user/")]').getElement().href.match(/\/user\/(\d+)\//)[1]; return parseInt(current_user_id); } let displayAutocomplete_html = htmlToElement(` `); //displayAutocomplete_html.tabIndex=1; function clearAutocomplete() { while (displayAutocomplete_html.firstChild) { displayAutocomplete_html.removeChild(displayAutocomplete_html.firstChild); } if (displayAutocomplete_html.parentNode) { displayAutocomplete_html.parentNode.removeChild(displayAutocomplete_html); } displayAutocomplete_html.dataset.selected=-1; } function displayAutocomplete({textarea,recommendations,prefix_startpos,prefix_stoppos,thread_id}) { clearAutocomplete(); for (let recommendation of recommendations) { //${recommendation.user_name} //recommendation.user_img.classList.add("mh-100"); let item_html = htmlToElement(` `); //item_html.tabIndex=1; item_html.onclick=() => { clearAutocomplete(); replaceTextareaInput({ textarea:textarea, replacement:recommendation.user_name + " ", prefix_startpos:prefix_startpos, prefix_stoppos:prefix_stoppos, }); }; displayAutocomplete_html.appendChild(item_html); } let caret = getCaretCoordinates(textarea,textarea.selectionEnd); displayAutocomplete_html.style.top = caret.top + caret.height + "px"; displayAutocomplete_html.style.left = caret.left + "px"; displayAutocomplete_html.style.position='absolute'; displayAutocomplete_html.style.display='inline'; textarea.parentNode.style.position="relative"; textarea.parentNode.appendChild(displayAutocomplete_html); } function onTextareaInput({textarea,uhist,thread_id}) { clearAutocomplete(); let start = textarea.selectionStart; let end = textarea.selectionEnd; let v = textarea.value; let textBefore = v.substring(0, start); let textAfter = v.substring(end, v.length); if (start === end) { let has_prefix = textBefore.match(/@(\S*)$/); if ( has_prefix ) { let prefix = has_prefix[1] + textAfter; prefix = prefix.match(/^(\S*)/)[1]; let prefix_startpos = has_prefix.index + 1; let prefix_stoppos = prefix_startpos + prefix.length; let recommendations=uhist.autoComplete(prefix,{thread_id:thread_id}); displayAutocomplete({ textarea:textarea, recommendations:recommendations, prefix_startpos:prefix_startpos, prefix_stoppos:prefix_stoppos, thread_id:thread_id }); } } } function onTextareaKeyDown(e) { // Down let activexp = xp.new('./').with(xp.new().contains('@class','active')); function doIncrement(increment) { let should_preventDefault = false; if (displayAutocomplete_html.hasChildNodes()) { let cur_selection = displayAutocomplete_html.dataset.selected || -1; cur_selection=parseInt(cur_selection); if (increment === 0) { // prevent default if something is selected should_preventDefault = cur_selection !== -1; return should_preventDefault; } if (displayAutocomplete_html.children[cur_selection]) { displayAutocomplete_html.children[cur_selection].classList.remove('active'); } should_preventDefault=true; cur_selection += increment; if (cur_selection >= displayAutocomplete_html.children.length ) { cur_selection = displayAutocomplete_html.children.length - 1; } if (cur_selection >=0 ) { displayAutocomplete_html.children[cur_selection].classList.add("active"); let topPos = displayAutocomplete_html.children[cur_selection].offsetTop; displayAutocomplete_html.scrollTop=topPos; } else if (cur_selection < -1 ) { should_preventDefault=false; cur_selection = -1; } displayAutocomplete_html.dataset.selected = cur_selection; } return should_preventDefault; } let selection = parseInt(displayAutocomplete_html.dataset.selected); if (e.keyCode == keycodes.downarrow || (selection === -1 && e.keyCode == keycodes.tab)) { if (doIncrement(1)) { e.preventDefault(); } } // Up else if (e.keyCode == keycodes.uparrow) { if (doIncrement(-1)) { e.preventDefault(); } else { clearAutocomplete(); } } // right else if (e.keyCode == keycodes.rightarrow || e.keyCode == keycodes.enter || e.keyCode == keycodes.tab || e.keyCode == keycodes.space) { if (doIncrement(0)) { displayAutocomplete_html.children[selection].onclick(); e.preventDefault(); } //else { // clearAutocomplete(); //} } else { clearAutocomplete(); } } function replaceTextareaInput({ textarea, replacement, prefix_startpos, prefix_stoppos, }) { let start = prefix_startpos || textarea.selectionStart; let end = prefix_stoppos || textarea.selectionEnd; let v = textarea.value; let textBefore = v.substring(0, start); let textAfter = v.substring(end, v.length); let textSelected = v.substring(start, end); textarea.value = `${textBefore}${replacement}${textAfter}`; textarea.selectionStart = start + replacement.length; textarea.selectionEnd = start + replacement.length; textarea.focus(); } function disableAutocompletion() { let textarea = xp.new('id("text")').getElement(); //textarea.removeEventListener("input",() => onTextareaInput ); } function main({read_posts_history}) { let user_id=getCurrentUserID(); let uhist = new UserHistory({read_posts_history:read_posts_history,user_id:user_id}); // Add current page's posts to history. let thread=xp.new(posts).append('//td/span/a').with( xp.new('preceding-sibling::span').with(xp.new().contains('@class','fa-clock')) ).getElement() if (thread) { thread = thread.href; let thread_id = parseInt(thread.match(/\/thread\/(\d+)\//)[1]); if (location.pathname.startsWith('/thread/')) { for(let [i,post] = [posts.getItter()]; (()=>{post=i.iterateNext(); return post;})();) { uhist.push(post) }; } else { // Consider more efficient approch let snap = posts.getSnapshot(); for ( let i=snap.snapshotLength - 1 ; i >= 0; i-- ) { uhist.push(snap.snapshotItem(i)); }; } setUserValues({read_posts_history:uhist.history}); xp.new('//textarea[@id="text"]').forEachElement( (textarea) => { textarea.addEventListener("input",() => onTextareaInput({ textarea:textarea, uhist:uhist, thread_id:thread_id, } ) ); textarea.addEventListener("keydown", onTextareaKeyDown); }); // For debugging unsafeWindow.uhist=uhist; } } getUserValues({ read_posts_history: [] },main);