WaniKani: Clickable Jisho.org Link (Tampermonkey Script)
// ==UserScript== | |
// @name WaniKani: Clickable Jisho.org Link & Popover | |
// @namespace http://tryforceful.com/ | |
// @version 0.2.5 | |
// @description Clickable Jisho.org Link & Popover | |
// @author tryforceful | |
// @match htt*://www.wanikani.com/review/session | |
// @match htt*://www.wanikani.com/lesson/session | |
// @grant GM_xmlhttpRequest | |
// @grant GM_addStyle | |
// @run-at document-end | |
// @connect cors-anywhere.herokuapp.com | |
// @connect jisho.org | |
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js | |
// @require https://gist.githubusercontent.com/tryforceful/77ffe9e6951b862744b9a51bb6df9a83/raw/a976201b5f7c61909bbb68a9b8a1cdb4db4a767a/tryf-just-popover-tooltip-bootstrap.js | |
// ==/UserScript== | |
// For showing logs: | |
const verbose = false; | |
GM_addStyle(" .popover { color: black; } div#character a {color:unset; text-decoration:none; cursor: pointer } .kunyomi {color: red;} .onyomi {color: blue;}"); | |
// streamlined Bootstrap, just Popover & Tooltip | |
GM_addStyle(` | |
/*! | |
* Generated using the Bootstrap Customizer (https://getbootstrap.com/docs/3.4/customize/) | |
*//*! | |
* Bootstrap v3.4.1 (https://getbootstrap.com/) | |
* Copyright 2011-2019 Twitter, Inc. | |
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) | |
*//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;opacity:0}.tooltip.in{opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#2e2e2e}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#2e2e2e}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#2e2e2e}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#2e2e2e}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#2e2e2e}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#2e2e2e}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#2e2e2e}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#2e2e2e}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#2e2e2e;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:14px;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden} | |
`); | |
//from https://stackoverflow.com/questions/3219758/detect-changes-in-the-dom | |
const observeDOM = (function(){ | |
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver; | |
return function( obj, callback ){ | |
if( !obj || !obj.nodeType === 1 ) return; // validation | |
if( MutationObserver ){ | |
// define a new observer | |
var obs = new MutationObserver(function(mutations, observer){ | |
callback(mutations); | |
}) | |
// have the observer observe foo for changes in children | |
obs.observe( obj, { childList:true, subtree:true }); | |
} | |
else if( window.addEventListener ){ | |
obj.addEventListener('DOMNodeInserted', callback, false); | |
obj.addEventListener('DOMNodeRemoved', callback, false); | |
} | |
} | |
})(); | |
function setPopover (title, content, anchorDiv, divForPlacement) { | |
$(anchorDiv).attr({ | |
'data-toggle': 'popover', | |
'title': title, | |
'data-content': content | |
}); | |
$(anchorDiv).popover({ | |
trigger: 'hover', | |
html: true, | |
container: divForPlacement, | |
placement: 'right' | |
}); | |
} | |
const addLink = (function () { | |
const PAGE_TYPES = Object.freeze({ | |
UNKNOWN: 0, | |
REVIEW: 1, | |
LESSON: 2, | |
}); | |
let currentPage = PAGE_TYPES.UNKNOWN; | |
if (/\/review\/./.test(document.URL)) { | |
currentPage = PAGE_TYPES.REVIEW; | |
} else if (/\/lesson/.test(document.URL)) { | |
currentPage = PAGE_TYPES.LESSON; | |
} | |
if(currentPage == PAGE_TYPES.UNKNOWN) | |
{ | |
// We don't know what kind of page we're on. | |
// To be safe, just exit, returning an empty fn. | |
return () => {}; | |
} | |
let previousJapaneseText = ''; // this acts as a state tracker to prevent infinite recursion | |
let f = 0; | |
return function() { | |
'use strict'; | |
console.log("Add Link was called"); | |
let elemAroundJapaneseText = currentPage == PAGE_TYPES.REVIEW ? | |
document.querySelector("div#question div#character span") : | |
document.querySelector("header div#main-info div#character") //LESSON | |
let elemWithTypeOfVocabClassName = currentPage == PAGE_TYPES.REVIEW ? | |
document.querySelector("div#question div#character") : | |
document.querySelector("header div#main-info") //LESSON | |
let addedADiv = elemAroundJapaneseText.querySelector("a"); | |
let japaneseText = addedADiv ? addedADiv.innerText : elemAroundJapaneseText.innerText; | |
// console.log('----------------') | |
// console.log('previousJapaneseText',previousJapaneseText) | |
// console.log('elemAroundJapaneseText',elemAroundJapaneseText) | |
// console.log('elemAroundJapaneseText.innerText',elemAroundJapaneseText.innerText) | |
// console.log('elemAroundJapaneseText.innerHTML',elemAroundJapaneseText.innerHTML) | |
// console.log('elemWithTypeOfVocabClassName',elemWithTypeOfVocabClassName) | |
// console.log('japaneseText',japaneseText) | |
// console.log('addedADiv',addedADiv) | |
// console.log("test 1: prev v char_content",previousJapaneseText, (z ? z : elemAroundJapaneseText.innerText), previousJapaneseText != (z ? z : elemAroundJapaneseText.innerText)); | |
// console.log("test 1: prev v japanesetext",previousJapaneseText, japaneseText, previousJapaneseText != japaneseText); | |
if(f >= 100) return; | |
if(previousJapaneseText != japaneseText || addedADiv == null) | |
{ | |
if(verbose) console.log("We made it past the gatekeeper"); | |
f++; | |
if(elemAroundJapaneseText.querySelector("img") != null) | |
{ | |
//this is not a 'real' character; it's an <img>, so don't process | |
//just remove the <a> from the previous character | |
if(addedADiv) { | |
$(addedADiv).unwrap(); | |
} | |
return; | |
} | |
//set historical state so we know when to proceed | |
previousJapaneseText = japaneseText; | |
//make it a kanji-style search | |
const isKanji = ["kanji", "radical"].includes( elemWithTypeOfVocabClassName.className ); | |
if(verbose) console.log('we made it here', "[", addedADiv, "]") | |
if(addedADiv == null) | |
{ | |
//make the inner link | |
let theLink = document.createElement("a"); | |
theLink.setAttribute("href", "http://www.jisho.org/search/" + japaneseText + (isKanji ? "%20%23kanji" : "")); | |
theLink.setAttribute("target", "_blank"); | |
theLink.setAttribute("data-innerText", japaneseText); | |
$(elemAroundJapaneseText).wrapInner(theLink); | |
//now this should come back | |
addedADiv = elemAroundJapaneseText.querySelector("a"); | |
if(verbose) console.log('we made it here 2', addedADiv, theLink ) | |
} | |
else { | |
//just manipulate the inner link | |
$(addedADiv).attr({ | |
"href": "http://www.jisho.org/search/" + japaneseText + (isKanji ? "%20%23kanji" : ""), | |
"data-innerText": japaneseText | |
}) | |
if(verbose) console.log('we made it here 3', "[", addedADiv, "]") | |
} | |
if(isKanji) { | |
const callb_kanjiapi = (resp) => { | |
if(verbose) console.log(resp.response); | |
let engResults = '<p>' + resp.response.meanings.join(', ') + '</p>'; | |
let jpnReading = '', | |
onReading = '', | |
kunReading = ''; | |
if(resp.response.on_readings.length) { | |
onReading = `<span class='onyomi'>` + resp.response.on_readings.join(', ') + '</span>'; | |
jpnReading += onReading; | |
} | |
if(resp.response.kun_readings.length) { | |
if(!!onReading) { | |
jpnReading += '<br/>' | |
} | |
kunReading = `<span class='kunyomi'>` + resp.response.kun_readings.join(', ') + '</span>'; | |
jpnReading += kunReading; | |
} | |
setPopover (jpnReading, engResults, addedADiv, elemAroundJapaneseText); | |
} | |
let params = { | |
method: 'GET', | |
url: 'https://cors-anywhere.herokuapp.com/https://kanjiapi.dev/v1/kanji/' + japaneseText, | |
headers: {'X-Requested-With': 'XMLHttpRequest'}, | |
responseType: 'json', | |
onload: callb_kanjiapi, | |
onerror: (x) => {console.error(x);}, | |
}; | |
GM_xmlhttpRequest(params); | |
} else { | |
const callb_jisho = (resp) => { | |
if(verbose) console.log(resp.response.data[0]); | |
let engResults, jpnReading; | |
if(resp.response.data[0]) { | |
engResults = resp.response.data[0].senses.map(x => '<p>'+x.english_definitions.join(', ')+'</p>').join(''); | |
jpnReading = [...new Set(resp.response.data[0].japanese.map(x => x.reading))].join(', '); | |
} | |
//console.log(engResults); | |
setPopover (jpnReading, engResults, addedADiv, elemAroundJapaneseText); | |
} | |
let params = { | |
method: 'GET', | |
url: 'https://cors-anywhere.herokuapp.com/https://jisho.org/api/v1/search/words?keyword=' + japaneseText, | |
headers: {'X-Requested-With': 'XMLHttpRequest'}, | |
responseType: 'json', | |
onload: callb_jisho, | |
onerror: (x) => {console.error(x);}, | |
}; | |
GM_xmlhttpRequest(params); | |
} | |
} | |
}; | |
})(); | |
observeDOM(document.querySelector("div#character"), addLink); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment