// ==UserScript==
// @name WK Overlay
// @namespace wkoverlay
// @version 0.3.4
// @description Overlays component information on reviews when show info is pressed.
// @author Ethan
// @match https://www.wanikani.com/review/session*
// @grant none
// ==/UserScript==
// TODO if kanji or radical follows vocab, overlay is not removed
var addStyleTag = function(){
/* Non-Javascript determined (to be put in style tag)*/
var styleText =
"@media (max-width: 767px) .wkOverlay {\r\n" +
"font-size: 3.0625em;\r\n" +
"font-weight: normal;\r\n" +
"line-height: 2.4em;\r\n" +
"}\r\n" +
".wkOverlay {\r\n" +
"display: block;\r\n" +
"position: absolute;\r\n" +
"}\r\n" +
".infoSpan {\r\n" +
"position: absolute;\r\n" +
"background-color: black;\r\n" +
"color: #fff;\r\n" +
"text-align: center;\r\n" +
"padding: 5px 10px;\r\n" +
"border-radius: 6px;\r\n" +
"display: inline-flex;\r\n" +
"font-size: 10pt;\r\n" +
"margin-right:0 2%;\r\n" +
"}\r\n" +
".infoRadSpan {\r\n" +
"position: absolute;\r\n" +
"background-color: black;\r\n" +
"color: #fff;\r\n" +
"text-align: center;\r\n" +
"padding: 5px 10px;\r\n" +
"border-radius: 6px;\r\n" +
"display: inline-flex;\r\n" +
"font-size: 10pt;\r\n" +
"}\r\n" +
".infoTop::after{\r\n" +
"content: '';\r\n" +
"position: absolute;\r\n" +
"top: 100%;\r\n" +
"left:20px;\r\n" +
"margin-left: -10px;\r\n" +
"border-width: 10px;\r\n" +
"border-style: solid;\r\n" +
"border-color: black transparent transparent transparent\r\n" +
"}\r\n" +
".infoTop{\r\n" +
"bottom: 100%;\r\n" +
"}\r\n" +
".infoBottom{\r\n" +
"top: 100%;\r\n" +
"}\r\n" +
".infoBottom::after{\r\n" +
"content: '';\r\n" +
"position: absolute;\r\n" +
"bottom: 100%;\r\n" +
"left:20px;\r\n" +
"margin-left: -10px;\r\n" +
"border-width: 10px;\r\n" +
"border-style: solid;\r\n" +
"border-color: transparent transparent black transparent\r\n" +
"} \r\n"+
"span.wkOverlayRadBox\r\n{" +
"background-color: #0af;" +
"display: inline-block;" +
"margin-right: 0.3em;" +
"width: 1.8em;" +
"height: 1.8em;" +
"color: #fff;" +
"line-height: 1.7em;" +
"text-align: center;" +
"text-shadow: 0 1px 0 rgba(0,0,0,0.3);" +
"-webkit-box-sizing: border-box;" +
"-moz-box-sizing: border-box;" +
"box-sizing: border-box;" +
"-webkit-border-radius: 3px;" +
"-moz-border-radius: 3px;" +
"border-radius: 3px;" +
"-webkit-box-shadow: 0 -3px 0 rgba(0,0,0,0.2) inset, 0 0 10px rgba(255,255,255,0.5);" +
"-moz-box-shadow: 0 -3px 0 rgba(0,0,0,0.2) inset,0 0 10px rgba(255,255,255,0.5);" +
"box-shadow: 0 -3px 0 rgba(0,0,0,0.2) inset, 0 0 10px rgba(255,255,255,0.5);" +
"}\r\n" +
"";
var styleElem = document.createElement('style');
styleElem.appendChild(document.createTextNode(styleText));
document.head.appendChild(styleElem);
};
(function() {
'use strict';
// Salt this event so future scripts don't throw multiple events and confuse the script.
// This should ensure these events are unique to this script and minimise the chances of catching events in error.
// TODO: handle possibility of multiple events being thrown by other scripts based on context (eg. multiple 'show' events without a 'hide' => 1 'show') for greater integration between scripts.
var hideEvent = "hide"+Math.trunc(Math.random()*1000000);
var showEvent = "show"+Math.trunc(Math.random()*1000000);
//---------------------------
var oldHide = $.fn.hide;
$.fn.hide = function(){this.trigger(new jQuery.Event(hideEvent)); return oldHide.apply(this, arguments);};
var oldShow = $.fn.show;
$.fn.show = function(){this.trigger(new jQuery.Event(showEvent)); return oldShow.apply(this, arguments);};
//--------------------------
addStyleTag();
var parentElement = $("#question");
var overlay = document.createElement('div');
overlay.style.display = 'none';
overlay.className = "wkOverlay";
overlay.setAttribute('lang', "ja");
var respComps = {};
var masterRads = {};
var prompt = {};
parentElement[0].appendChild(overlay);
var newItem = false;
// just for comparing prompts
var shallowCompare = function(obj1, obj2){
return ((obj1.voc && obj2.voc) || (obj1.kan && obj2.kan) || (obj1.rad && obj2.rad)) && ((obj1.voc === obj2.voc) || (obj1.kan === obj2.kan) || (obj1.rad === obj2.rad));
};
$.jStorage.listenKeyChange('currentItem', function(){
while (overlay.firstChild){
overlay.removeChild(overlay.firstChild);
}
// Runs after span is changed, but is this guaranteed?
var oldPrompt = prompt;
prompt = $.jStorage.get('currentItem');
newItem = !shallowCompare(prompt, oldPrompt);
var itemString = prompt.voc||prompt.kan; // The kanji, vocab (or soon, radical)
if (itemString){
for (var ch in itemString){ // Kanji and radical, this will only be one (radical images may need some other handling)
var chSpan = document.createElement('span');
chSpan.innerText = itemString[ch];
chSpan.style.fontSize = document.defaultView.getComputedStyle($("#character")[0], "").fontSize;
}
}
});
var showRadicalTags = function(){
console.warn("showtags");
overlay.style.display = 'block';
//Since we are currently adding the tips every time we catch a show event, we must delete them all first, or multiple hide/show events will build up more tips for every 'show'
// Clear array of nodelists
while (overlay.firstChild){
overlay.removeChild(overlay.firstChild);
}
// Get the related kanji component info for vocab from the 'show answer' page
// Create an object (respComps) indexed by Kanji characters. eg {"出":" Exit","提":" Present, Submit"}
$("#related-items .kanji a").each(function(i, comp){
//Needs love to be more robust when Wanikani makes cosmetic changes to their page
respComps[comp.childNodes[1].textContent] = comp.childNodes[2].textContent;
});
if (prompt.voc){
// Create flip flop variable so tips with multiple words don't clutter the top or bottom
var flipTopBottom = false; //true: top, false: bottom
// Values will be the same for all char, but need to be calculated
// Position absolute needs negative margins to react to number of characters
var marginLeftPercentage = (-100/prompt.voc.length) + "%";
var marginRightPercentage = "2%";
for (var ch in prompt.voc){
flipTopBottom = !flipTopBottom;
var chSpan = document.createElement('span');
chSpan.className = "wkOverlayChar";
var comp = document.createElement('span');
comp.innerText = prompt.voc[ch];
comp.style.opacity = 0;
chSpan.appendChild(comp);
console.info(respComps, prompt.voc[ch], respComps[prompt.voc[ch]]);
chSpan.style.fontSize = document.defaultView.getComputedStyle($("#character")[0], "").fontSize; // and lineHeight
// Javascript determined style values.
overlay.style.height = $("#character span").height() + 'px';
overlay.style.top = $("#character span").position().top + 'px';
overlay.style.left = $("#character span").position().left + 'px';
overlay.style.height = $("#character span").height() + 'px';
overlay.appendChild(chSpan);
//debugger;
if (respComps[prompt.voc[ch]]){ // any characters not present in the components section will fail here. There should be no Kanji in the prompt that is not here, so no Kanji should fail.
var spInf = document.createElement('span');
spInf.innerText = respComps[prompt.voc[ch]];
chSpan.appendChild(spInf);
spInf.style.marginLeft = marginLeftPercentage;
// spInf.style.marginRight = marginRightPercentage;
if(flipTopBottom){
spInf.className = "infoSpan infoTop";
}
else{
spInf.className = "infoSpan infoBottom";
}
}
}
}
if (prompt.kan){
var respRads = {};
var slug;
$("#related-items .radical a").each(function(i, comp){
//debugger;
// Needs improvement for when Wanikani changes its page structure
if (comp.childNodes[1].textContent){
slug = comp.childNodes[1].textContent;
respRads[slug] = {name: comp.childNodes[2].textContent, kanji: {}};
}
else{
slug = comp.childNodes[1].firstChild.className;// i class
respRads[slug] = {box: comp.childNodes[1].firstChild, name: comp.childNodes[2].textContent, kanji: {}};
}
//get position from browser storage if there
if ($.jStorage.get("overlayMasterRads")){
masterRads = $.jStorage.get("overlayMasterRads");
}
if (!masterRads[slug]){
masterRads[slug] = respRads[slug];
}
if (masterRads[slug].kanji[prompt.kan]){
respRads[slug].kanji[prompt.kan] = masterRads[slug].kanji[prompt.kan];
}
else{
respRads[slug].kanji[prompt.kan] = {x: 0, y: 0};
}
});
console.log("masterRads:", masterRads);
var kan = document.createElement('span');
kan.className = "wkOverlayChar";
kan.innerText = prompt.kan;
kan.style.opacity = 0.5; //debug kanji
//kan.appendChild(comp);
console.info("component object, kanji, compObj[kanji]", respComps, prompt.kan, respComps[prompt.kan]);// will probably only work on kanji that has itself as a radical (eg. 一)
kan.style.fontSize = document.defaultView.getComputedStyle($("#character")[0], "").fontSize; // and lineHeight
var overlayLeftOffset = $("#character span").position().left;
var overlayTopOffset = $("#character span").position().top;
// Javascript determined style values.
overlay.style.height = $("#character span").height() + 'px';
overlay.style.top = $("#character span").position().top + 'px';
overlay.style.left = overlayLeftOffset + 'px';
//overlay.style.right = (overlayLeftOffset+$(overlay).width()) + 'px';
//overlay.style.right = (-overlayRightOffset) + 'px';
//alert(overlay.style.right);
overlay.style.height = $("#character span").height() + 'px';
//overlay.appendChild(kan);
//respComps is radicals and their names as key->value pairs
// we need to give the radicals some more info (position, svg path around relevant part of kanji) than the kanji has
// add this info for each radical: object as scraped: {"疒":" Sick","正":" Correct"} => fn => {"疒":{name:" Sick", "症":{x:num, y:num, svg: svgElem },"正":{name:" Correct", "症":{x:num, y:num, svg: svgElem }}
// the kanji info bit: "症":{x:num, y:num, svg: svgElem }, needs to be creatable, editable, and retrievable.
var dragMouseDownHandler = function(evt){
console.info("mousedown event was heard", evt);
evt.preventDefault();
console.log('this == ', this);
this._mouseDownOrigin = {x: evt.clientX, y: evt.clientY};
//var elStyle = document.defaultView.getComputedStyle(evt.target, "");
var elStyle = document.defaultView.getComputedStyle(this, "");
var l = elStyle.left; var t = elStyle.top;
//var l = evt.target.style.left; var t = evt.target.style.top;
this._originalPosition = {x: parseFloat(l.substr(0, l.length-1)), y: parseFloat(t.substr(0, t.length-1))};
this._mouseMoveHandler = dragMouseMoveHandler.bind(this);
this._mouseUpHandler = dragMouseUpHandler.bind(this);
document.addEventListener('mousemove', this._mouseMoveHandler);
//document.addEventListener('mousemove', dragMouseMoveHandler);
document.addEventListener('mouseup', this._mouseUpHandler);
document.addEventListener('mouseout', this._mouseUpHandler);
// this.addEventListener('mouseup', dragMouseUpHandler);
// this.addEventListener('mouseout', dragMouseUpHandler);
};
var dragMouseMoveHandler = function(evt){
//nsole.log("mousemove", evt, this);
if (this._mouseDownOrigin){
var dx = evt.clientX - this._mouseDownOrigin.x;
//console.log("dx", dx);
var dy = evt.clientY - this._mouseDownOrigin.y;
var l = this.style.left||"0px"; var t = this.style.top||"0px";
if (l[l.length-1] === "%" || t[t.length-1] === "%"){
console.log("positioned with % ??");
}
else{
var newLeft = Math.max(-overlayLeftOffset, this._originalPosition.x + dx) + "px";
console.log("-overlayLeftOffset", -overlayLeftOffset, "this._originalPosition.x", this._originalPosition, "dx", dx);
var newTop = Math.max(-overlayTopOffset, this._originalPosition.y + dy) + "px";
//want the highest negative number to keep it in the document
console.log("moving div:" + newLeft + " " + newTop);
this.style.left = newLeft;
this.style.right = -'100px';
this.style.top = newTop;
}
}
};
var dragMouseUpHandler = function(evt){
if (evt.type === 'mouseup' || evt.type === 'mouseout' && (evt.toElement === null)){ // mouseout leaves entire document
// Check if this radical has a payload for the kanji, give it one if not.
masterRads[this.slug].kanji[prompt.kan] = {x: this.style.left, y:this.style.top};
//console.log("find x and y?", evt);
$.jStorage.set("overlayMasterRads", masterRads);
console.log("removing listeners", evt.type);
document.removeEventListener('mousemove', this._mouseMoveHandler);
document.removeEventListener('mouseup', this._mouseUpHandler);
document.removeEventListener('mouseout', this._mouseUpHandler);
}
};
// Order is not important for radicals since they are just like, all over the kanji, so we will just iterate.
for (var rad in respRads){
var spRadName = document.createElement('span');
var radBox = document.createElement('span');
radBox.setAttribute("lang", "ja");
radBox.className = "wkOverlayRadBox";
if (!respRads[rad].box){
radBox.appendChild(document.createTextNode(rad));
}
else{
radBox.appendChild(respRads[rad].box.cloneNode());
}
spRadName.slug = rad;
spRadName.appendChild(radBox);
spRadName.appendChild(document.createTextNode(respRads[rad].name));
overlay.appendChild(spRadName);
spRadName.style.marginLeft = (-100)*Math.random() + "%"; //more likely to see them if they are changed up a bit while we code
spRadName.style.left = respRads[rad].kanji[prompt.kan].x;
spRadName.style.top = respRads[rad].kanji[prompt.kan].y;
spRadName.style.cursor = "move";
spRadName.className = "infoRadSpan";
//-- todo keep track of these in objects, enable/disable handlers in an edit/setup mode
//masterRads.currentSlug = slug;
spRadName.addEventListener('mousedown', dragMouseDownHandler);
}
}
};
var observer = new MutationObserver(showRadicalTags);
//observer = new MutationObserver(function(mutation){console.log(mutation);});
var testObserve = new MutationObserver(function(mutations){
console.info("%cObserver", "background-color:grey");
console.info(typeof mutations);
console.groupCollapsed("Mutation List");
mutations.forEach(function(mutation){console.info(mutation.type);});
console.groupEnd();
});
// Show overlay when info is being shown.
$("#item-info").on(showEvent, function(evt){
console.info("showevt", evt);
if (newItem){
newItem = false;
//watch related items for change
observer.observe($("#item-info")[0], {childList: true, subtree:true});
}
else{
showRadicalTags();
}
});
$("#item-info").on(hideEvent, function(evt){
if (evt.target.id === "additional-content-load"){
console.info("hide evt", evt);
//debugger;
overlay.style.display = 'none';
}
});
})();