WaniKani hide mnemonics

Adds a possiblity to hide meaning and reading mnemonics.

  1. // ==UserScript==
  2. // @name WaniKani hide mnemonics
  3. // @namespace wkhidem
  4. // @description Adds a possiblity to hide meaning and reading mnemonics.
  5. // @version 1.8
  6. // @author Niklas Barsk
  7. // @include https://www.wanikani.com/review/session*
  8. // @include https://www.wanikani.com/lesson/session*
  9. // @include https://www.wanikani.com/radicals/*
  10. // @include https://www.wanikani.com/kanji/*
  11. // @include https://www.wanikani.com/vocabulary/*
  12. // @run-at document-end
  13. // ==/UserScript==
  14.  
  15. /*
  16. * This script is licensed under the MIT licence.
  17. */
  18.  
  19. if (isReview() || isLesson())
  20. {
  21. // review/lessons quiz
  22. var mo = new MutationObserver(initQuiz);
  23. mo.observe(document.getElementById("item-info-col2"), {'childList': true});
  24. }
  25.  
  26. if (isLesson())
  27. {
  28. // Call init whenever the main-info class attribute is updated.
  29. // This happens whenever the user switch to a new item on the
  30. // lesson/learning part.
  31. var mo = new MutationObserver(init);
  32. mo.observe(document.getElementById("main-info"), {'attributes': true});
  33. }
  34.  
  35. if (isLookup())
  36. {
  37. if (document.getElementById("progress").style.display != "none")
  38. {
  39. // only run the script on items that has been unlocked since it's
  40. // not possible to add user mnemonics on locked items.
  41. init();
  42. }
  43. }
  44.  
  45. function initQuiz(allmutations)
  46. {
  47. if (allmutations[0].addedNodes.length > 0)
  48. {
  49. // Ignore the mutation if no nodes are added.
  50. // When going from one question to the next all elements in
  51. // item-info-col2 is first removed as one mutation and then
  52. // the new content is added as a second mutation. So mutations
  53. // without any added nodes should be ignored
  54. init();
  55. }
  56. }
  57.  
  58. function init()
  59. {
  60. if (!sanityCheckPassed())
  61. {
  62. // Don't try to run the script if the HTML can't be parsed.
  63. console.warn("WaniKani hide mnemonics need to be updated to support the latest version of WaniKani.");
  64. return;
  65. }
  66.  
  67. setCorrectText();
  68. setCorrectVisibility();
  69.  
  70. if (isQuiz())
  71. {
  72. // Update visibility state when the "Show All Information" button is pressed.
  73. document.getElementById("all-info").addEventListener("click", setCorrectVisibility);
  74. }
  75.  
  76. // Setup listeners for changes to the note-meaning/reading.
  77. var mo = new MutationObserver(onNoteChanged);
  78. var options = {'childList': true};
  79. mo.observe(getNoteElement("meaning"), options);
  80.  
  81. if (!isRadical())
  82. {
  83. mo.observe(getNoteElement("reading"), options);
  84. }
  85. }
  86.  
  87.  
  88. /**
  89. * Called whenever the note-reading or note-meaning elements children
  90. * are updated.
  91. */
  92. function onNoteChanged(allmutations)
  93. {
  94. // 1 children for the edit note form and 0 children when the
  95. // note is being displayed. Update visibility when form is closed
  96. // and note is shown again.
  97. if (allmutations[0].target.children.length == 0)
  98. {
  99. // example id for quiz: 'meaning-note'
  100. // example id for learning: 'supplement-voc-meaning-notes'
  101. var index = isLesson() && !isQuiz() ? 2 : 1;
  102. var which = allmutations[0].target.parentNode.id.split('-')[index];
  103. setCorrectVisibilityFor(which);
  104. }
  105. }
  106.  
  107. /**
  108. * Set the correct text for the meaning and reading headers depending on
  109. * the current state.
  110. */
  111. function setCorrectText()
  112. {
  113. setCorrectTextFor("meaning");
  114. if (!isRadical())
  115. {
  116. setCorrectTextFor("reading");
  117. }
  118. }
  119.  
  120. /**
  121. * Returns true if the mnemonic is hidden for the current character
  122. * and the given type.
  123. */
  124. function isHidden(which)
  125. {
  126. return isManuallyHidden(which) || isAutomaticallyHidden(which);
  127. }
  128.  
  129. /**
  130. * Returns true if the user has hidden the mnemonic with the hide link.
  131. */
  132. function isManuallyHidden(which)
  133. {
  134. return localStorage.getItem(getStorageKey(which)) == "0";
  135. }
  136.  
  137. /**
  138. * Returns true if the mnemonic has been hidden because there
  139. * is a note present.
  140. */
  141. function isAutomaticallyHidden(which)
  142. {
  143. return hasNote(which) &&
  144. localStorage.getItem(getStorageKey(which)) != "1";
  145. }
  146.  
  147. /**
  148. * Set hidden status for the current character in the localStorage
  149. * for the give type.
  150. * @param which "reading" or "meaning" depending on which key is desired.
  151. * @param what new value for the current character:
  152. * 0: user has manually hidden the mnemonic.
  153. * 1: user has shown an automatically hidden mnemonic.
  154. */
  155. function setStorage(which, what)
  156. {
  157. localStorage.setItem(getStorageKey(which), what);
  158. }
  159.  
  160. /**
  161. * Remove the stored information about the current character from
  162. * the localStorage for the give type.
  163. * @param which "reading" or "meaning" depending on which key is desired.
  164. */
  165. function clearStorage(which)
  166. {
  167. localStorage.removeItem(getStorageKey(which));
  168. }
  169.  
  170. /**
  171. * Get the key that the removed state for the current character is
  172. * stored under in the localStorage.
  173. * @param "reading" or "meaning" depending on which key is desired.
  174. */
  175. function getStorageKey(which)
  176. {
  177. return getCharacterType() + "_" + getCharacter() + "_" + which;
  178. }
  179.  
  180. /**
  181. * Return a textual representation of the current character.
  182. * For vocabulary, kanji and normal radicals it is the vocabulary,
  183. * kanji, or radical itself. For radicals that are just an image
  184. * it is the file name of the image.
  185. */
  186. function getCharacter()
  187. {
  188. var element;
  189. if (isLookup())
  190. {
  191. element = document.getElementsByClassName("japanese-font-styling-correction")[0];
  192. }
  193. else
  194. {
  195. element = document.getElementById("character");
  196. }
  197.  
  198. var character = element.textContent.trim();
  199. if (character == "") // Radical with image instead of text.
  200. {
  201. var img = element.children[0];
  202. // During quiz the image is inside a span, during lessons
  203. // the image is directly under the character div.
  204. if (isQuiz())
  205. {
  206. img = img.children[0]
  207. }
  208. character = img.getAttribute("src").split("/").pop()
  209. }
  210. return character
  211. }
  212.  
  213. /**
  214. * Return the type of the character the page is for: a string containing
  215. * "vocabulary", "kanji" or "radical".
  216. */
  217. function getCharacterType()
  218. {
  219. if (isLesson())
  220. {
  221. return document.getElementById("main-info").className.trim();
  222. }
  223. else if (isReview())
  224. {
  225. return document.getElementById("character").className.trim();
  226. }
  227. else if (isLookup())
  228. {
  229. var character = document.getElementsByClassName("japanese-font-styling-correction")[0];
  230. var cn = character.parentElement.className;
  231. return cn.substr(0, cn.indexOf("-"));
  232. }
  233. }
  234.  
  235. /**
  236. * Return true if the current page is for a radical.
  237. */
  238. function isRadical()
  239. {
  240. return getCharacterType() == "radical";
  241. }
  242.  
  243. /**
  244. * Return true if the current page is for vocabulary.
  245. */
  246. function isVocabulary()
  247. {
  248. return getCharacterType() == "vocabulary";
  249. }
  250.  
  251. function isLookup()
  252. {
  253. return document.URL.indexOf("/radicals/") != -1 ||
  254. document.URL.indexOf("/kanji/") != -1 ||
  255. document.URL.indexOf("/vocabulary/") != -1;
  256. }
  257.  
  258. /**
  259. * Returns true if the current page is a lesson.
  260. */
  261. function isLesson()
  262. {
  263. return document.URL.indexOf("lesson") != -1;
  264. }
  265.  
  266. /**
  267. * Returns true if the current page is a review.
  268. */
  269. function isReview()
  270. {
  271. return document.URL.indexOf("review") != -1;
  272. }
  273.  
  274. /**
  275. * Returns true if the user is currently doing a quiz.
  276. */
  277. function isQuiz()
  278. {
  279. if (isReview())
  280. {
  281. return true;
  282. }
  283. if (isLesson())
  284. {
  285. var mainInfo = document.getElementById("main-info");
  286. return mainInfo.parentElement.className == "quiz";
  287. }
  288. return false;
  289. }
  290.  
  291. /**
  292. * Returns true if the current item has a note set.
  293. * @param which specifies if it's the reading or meaning
  294. * note that is of interest.
  295. */
  296. function hasNote(which)
  297. {
  298. return getNoteElement(which).textContent.trim() != "Click to add note";
  299. }
  300.  
  301. /**
  302. * Set the correct visibility of the reading and meaning sections.
  303. */
  304. function setCorrectVisibility()
  305. {
  306. setCorrectVisibilityFor("meaning");
  307. if (!isRadical())
  308. {
  309. setCorrectVisibilityFor("reading");
  310. }
  311. }
  312.  
  313. /**
  314. * Set the correct visibility for the specified header depending on the current state.
  315. * @param which The header that should be updated, either "reading" or "meaning".
  316. */
  317. function setCorrectVisibilityFor(which)
  318. {
  319. if (hiddenByWaniKani(which))
  320. {
  321. // Don't touch visibility for things hidden by WaniKani.
  322. return;
  323. }
  324.  
  325. if (isHidden(which)) // In this case, should be hidden
  326. {
  327. hide(which);
  328. }
  329. else
  330. {
  331. show(which);
  332. }
  333. }
  334.  
  335. /**
  336. * When doing a quiz WaniKani only shows the info that was being asked for
  337. * to see all info the user need to press a button to display it.
  338. * This method returns true if the given reading/meaning is currently hidden.
  339. *
  340. * @param which "reading" or "meaning"
  341. */
  342. function hiddenByWaniKani(which)
  343. {
  344. if (!isQuiz())
  345. {
  346. return false;
  347. }
  348. var infoHidden = document.getElementById("all-info").style.display != "none";
  349. var questionType = document.getElementById("question-type").className;
  350. return which != questionType && infoHidden;
  351. }
  352.  
  353. /**
  354. * Set the correct state in local storage for the current item
  355. * and mnenemonic based on the given action.
  356. * @param action "hide" or "show"
  357. * @param which "reading" or "meaning"
  358. */
  359. function setCorrectStorage(action, which)
  360. {
  361. var note = hasNote(which);
  362. if (action == "show" && !note ||
  363. action == "hide" && note)
  364. {
  365. // Default state, cleare any storage
  366. clearStorage(which);
  367. }
  368. else if (action == "show" && note)
  369. {
  370. // Force section to be visible
  371. setStorage(which, 1);
  372. }
  373. else if (action == "hide" && !note)
  374. {
  375. // Force section to be hidden
  376. setStorage(which, 0);
  377. }
  378. }
  379.  
  380. /**
  381. * Hide the specified section.
  382. * @param which The section that should be hidden, either "reading" or "meaning".
  383. */
  384. function hide(which)
  385. {
  386. setCorrectStorage("hide", which);
  387. setDisplayStyle(which, "none");
  388. setCorrectText();
  389. }
  390.  
  391. /**
  392. * Show the specified section.
  393. * @param which The section that should be shown, either "reading" or "meaning".
  394. */
  395. function show(which)
  396. {
  397. setCorrectStorage("show", which);
  398. setDisplayStyle(which, "");
  399. setCorrectText();
  400. }
  401.  
  402. /**
  403. * Set the display style of the hidable section.
  404. * @param which The section that should be updated, either "reading" or "meaning".
  405. * @param display The new value of the display css property.
  406. */
  407. function setDisplayStyle(which, display)
  408. {
  409. var children = getHidableElements(which);
  410. for (i = 0; i < children.length; ++i)
  411. {
  412. children[i].style.display = display;
  413. }
  414. }
  415.  
  416. /**
  417. * Returns an array with all elements that should be hidden or
  418. * shown when the hide/show link is clicked.
  419. * @param Specifies if it's the "reading" or "meaning" that should be hidden
  420. */
  421. function getHidableElements(which)
  422. {
  423. // return an array of items to hide/show
  424. var ret = [];
  425. if (isQuiz())
  426. {
  427. ret.push(getMnemonicContainer(which));
  428. }
  429. else if (isLesson())
  430. {
  431. var children = getLearningContainer(which).children;
  432. for (i = 0; i < children.length - 2; ++i) // note section is last 2 elements.
  433. {
  434. ret.push(children[i]);
  435. }
  436. }
  437. else if (isLookup())
  438. {
  439. if (isRadical())
  440. {
  441. ret.push(getLookupMnemonicContainer(which));
  442. }
  443. else
  444. {
  445. var children = getLookupMnemonicContainer(which).children;
  446. for (i = 0; i < children.length - 1; ++i) // note section is the last element.
  447. {
  448. ret.push(children[i]);
  449. }
  450. }
  451. }
  452. return ret;
  453. }
  454.  
  455. /**
  456. * Set the correct text for reading/meaning/note header with the apropriate
  457. * show/hide link depending on what the current state is.
  458. *
  459. * @param which Specifies which header should be updated, the "reading" or "meaning" header.
  460. * @param action Specifies what happens when the header is pressed, either "show" or "hide".
  461. * @param headerID The ID of the header that should be updated.
  462. * @param header The DOM element which should have its text updated.
  463. */
  464. function textForHeader(which, action, header)
  465. {
  466. // Add the show/hide link to the header.
  467. header.innerHTML = header.firstChild.textContent + getLinkHTML(which, action);
  468.  
  469. // Set either hide(which) or show(which) as onclick handler for the new link.
  470. document.getElementById(getLinkId(which, action)).onclick = function()
  471. {
  472. if (action == "show")
  473. {
  474. show(which);
  475. }
  476. else
  477. {
  478. hide(which);
  479. }
  480. };
  481. }
  482.  
  483. /**
  484. * Get the HTML for the show/hide link.
  485. * @param which Specifies if the link is for "reading" or "meaning".
  486. * @param action Specifies if the link is "hide" or "show".
  487. */
  488. function getLinkHTML(which, action)
  489. {
  490. // Examples of what the html looks like:
  491. // <span id="show-reading">(show original meaning)</span>
  492. // <span id="hide-meaning">(hide)</span>
  493.  
  494. var linkText = action;
  495. if (action == "show")
  496. {
  497. if (isVocabulary())
  498. {
  499. linkText += " original explanation";
  500. }
  501. else
  502. {
  503. linkText += " original mnemonic";
  504. }
  505. }
  506.  
  507. return "<span id=\"" + getLinkId(which, action) + "\"> (" + linkText + ")</span>";
  508. }
  509.  
  510. /**
  511. * Return the id of the show/hide link.
  512. */
  513. function getLinkId(which, action)
  514. {
  515. var quiz = isQuiz() ? "-q" : "";
  516. return action + "-" + which + "-" + getCharacterType() + quiz;
  517. }
  518.  
  519. /**
  520. * Set the correct text for the specified header depending on the current state.
  521. * @param which The header that should be updated, either "reading" or "meaning".
  522. */
  523. function setCorrectTextFor(which)
  524. {
  525. if (isHidden(which))
  526. {
  527. // Display the "show" link in the note.
  528. textForHeader(which, "show", getNoteHeader(which));
  529. }
  530. else
  531. {
  532. // Display the "hide" link in the header.
  533. textForHeader(which, "hide", getMnemonicHeader(which));
  534.  
  535. // Make sure the default version of the Note header is displayed.
  536. var nh = getNoteHeader(which);
  537. nh.innerHTML = nh.firstChild.textContent;
  538. }
  539. }
  540.  
  541. /**
  542. * Get the DOM element that contains the mnemonic.
  543. * @param which Specifies if the header for the reading or meaning should
  544. * be returned. The parameter is ignored for radicals since
  545. * they only have one mnemonic.
  546. */
  547. function getMnemonicContainer(which)
  548. {
  549. if (isRadical())
  550. {
  551. return document.getElementById("item-info-col2").children[0];
  552. }
  553. else
  554. {
  555. return document.getElementById("item-info-" + which + "-mnemonic");
  556. }
  557. }
  558.  
  559. /**
  560. * Return the element that contains the mnemonics for the lookup pages.
  561. */
  562. function getLookupMnemonicContainer(which)
  563. {
  564. if (isRadical())
  565. {
  566. return document.getElementById("note-" + which).previousElementSibling;
  567. }
  568. else
  569. {
  570. return document.getElementById("note-" + which).parentElement;
  571. }
  572. }
  573.  
  574. /**
  575. * Get the DOM element for the mnemonic header.
  576. * @param which Specifies if the header for the reading or meaning should
  577. * be returned. The parameter is ignored for radicals since
  578. * they only have one mnemonic.
  579. */
  580. function getMnemonicHeader(which)
  581. {
  582. if (isQuiz())
  583. {
  584. return getMnemonicContainer(which).children[0];
  585. }
  586. else if (isLesson())
  587. {
  588. return getLearningContainer(which).children[0];
  589. }
  590. else if(isLookup())
  591. {
  592. return getLookupMnemonicContainer(which).children[0];
  593. }
  594. }
  595.  
  596. /**
  597. * Get the DOM element for the user notes header.
  598. * @param which Specifies if the notes header for the reading or meaning
  599. * should be returned.
  600. */
  601. function getNoteHeader(which)
  602. {
  603. if (isQuiz() || isLookup())
  604. {
  605. return document.getElementById("note-" + which).children[0];
  606. }
  607. else if (isLesson())
  608. {
  609. var container = getLearningContainer(which);
  610. return container.children[container.children.length - 2];
  611. }
  612. }
  613.  
  614. /**
  615. * Returns the DOM element that holds the user note.
  616. * @param which Specifies if the notes element for the reading or meaning
  617. * should be returned.
  618. */
  619. function getNoteElement(which)
  620. {
  621. var element;
  622. if (isLesson() && !isQuiz())
  623. {
  624. var id;
  625. if (isRadical())
  626. {
  627. id = "supplement-rad-name-notes";
  628. }
  629. else
  630. {
  631. id = "supplement-" + getCharacterType().substring(0,3) + "-" + which + "-notes";
  632. }
  633. element = document.getElementById(id).children[0];
  634. }
  635. else
  636. {
  637. element = document.getElementById("note-" + which).children[1];
  638. }
  639. return element;
  640. }
  641.  
  642. /**
  643. * Get the container element of the mnemonics and notes in the learning
  644. * part of lessons. There is no id available for the actual headers and
  645. * mnemonics like in the quiz so the container element is the closest
  646. * we get.
  647. *
  648. * @param which Specifies if it's the container for "reading" or "meaning"
  649. * that is desired.
  650. */
  651. function getLearningContainer(which)
  652. {
  653. var id = "supplement-" + getCharacterType().substring(0,3) + "-";
  654. var className = "pure-u-3-4";
  655. if (isRadical())
  656. {
  657. id += "name";
  658. className = "pure-u-1"
  659. }
  660. else
  661. {
  662. id += which;
  663. }
  664.  
  665. return document.getElementById(id).getElementsByClassName(className)[0];
  666. }
  667.  
  668. /**
  669. * Return true if critical assumptions made about the HTML code holds.
  670. */
  671. function sanityCheckPassed()
  672. {
  673. try
  674. {
  675. if (isLookup())
  676. {
  677. sanityCheckLookup();
  678. }
  679.  
  680. if (isQuiz())
  681. {
  682. sanityCheckQuiz();
  683. }
  684.  
  685. if (isLesson())
  686. {
  687. sanityCheckLesson();
  688. }
  689.  
  690. // Make sure we can get a correct character type.
  691. var ct = getCharacterType();
  692. if (ct != "radical" && ct != "vocabulary" && ct != "kanji")
  693. {
  694. throw new Error("Unknown character type: " + ct);
  695. }
  696.  
  697. // Make sure we can get a correct storage key
  698. var parts = getStorageKey("meaning").split("_");
  699. if (parts.length != 3 || parts[0] == "" ||
  700. parts[1] == "" || parts[2] == "")
  701. {
  702. throw new Error("Unable to generate a correct storage key: " + key);
  703. }
  704. }
  705. catch (e)
  706. {
  707. console.error(e.toString());
  708. return false;
  709. }
  710. return true;
  711. }
  712.  
  713. /**
  714. * Throws an exception if the critical assumptions made about the
  715. * HTML code in the lookup related code are wrong.
  716. */
  717. function sanityCheckLookup()
  718. {
  719. if (document.getElementsByClassName("japanese-font-styling-correction").length == 0)
  720. {
  721. throw new Error("No element with class 'japanese-font-styling-correction' exists");
  722. }
  723.  
  724. ensureElementExists("note-meaning");
  725. if (!isRadical())
  726. {
  727. ensureElementExists("note-reading");
  728. }
  729. }
  730.  
  731. /**
  732. * Throws an exception if the critical assumptions made about the
  733. * HTML code in the quiz related code are wrong.
  734. */
  735. function sanityCheckQuiz()
  736. {
  737. ensureElementExists("character");
  738. ensureElementExists("all-info");
  739. ensureElementExists("item-info-col2");
  740. ensureElementExists("note-meaning");
  741. var questionType = ensureElementExists("question-type");
  742. questionType = questionType.className;
  743.  
  744. if (questionType != "reading" && questionType != "meaning")
  745. {
  746. throw new Error("'question-type' is neither \"reading\" nor \"meaning\", it is \"" + questionType + "\"");
  747. }
  748.  
  749. if (!isRadical())
  750. {
  751. ensureElementExists("item-info-reading-mnemonic");
  752. ensureElementExists("item-info-meaning-mnemonic");
  753. ensureElementExists("note-reading");
  754. }
  755. }
  756.  
  757. /**
  758. * Throws an exception if the critical assumptions made about the
  759. * HTML code in the lesson related code are wrong.
  760. */
  761. function sanityCheckLesson()
  762. {
  763. ensureElementExists("character");
  764. var mainInfo = ensureElementExists("main-info");
  765.  
  766. // Make sure assumptions for lessons in isQuiz() holds.
  767. var cn = mainInfo.parentElement.className;
  768. if (cn != "" && cn != "quiz")
  769. {
  770. throw new Error("Parent of 'main-info' is neither empty nor \"quiz\"");
  771. }
  772.  
  773. if (!isQuiz())
  774. {
  775. ensureElementExists("supplement-rad-name-notes");
  776. ensureElementExists("supplement-kan-meaning-notes");
  777. ensureElementExists("supplement-voc-meaning-notes");
  778. ensureElementExists("supplement-kan-reading-notes");
  779. ensureElementExists("supplement-voc-reading-notes");
  780.  
  781. ensureElementExistsAndHasClass("supplement-voc-reading", "pure-u-3-4");
  782. ensureElementExistsAndHasClass("supplement-voc-meaning", "pure-u-3-4");
  783. ensureElementExistsAndHasClass("supplement-kan-reading", "pure-u-3-4");
  784. ensureElementExistsAndHasClass("supplement-kan-meaning", "pure-u-3-4");
  785. ensureElementExistsAndHasClass("supplement-rad-name", "pure-u-1");
  786. }
  787. }
  788.  
  789. /**
  790. * Throws an exception if the given id doesn't exist in the DOM tree.
  791. * @return the element if it exist
  792. */
  793. function ensureElementExists(id)
  794. {
  795. var element = document.getElementById(id);
  796. if (element == null)
  797. {
  798. throw new Error(id + " does not exist");
  799. }
  800. return element;
  801. }
  802.  
  803. /**
  804. * Throws an exception if the given id doesn't exist in the DOM tree.
  805. */
  806. function ensureElementExistsAndHasClass(id, className)
  807. {
  808. var element = ensureElementExists(id);
  809. if (element.getElementsByClassName(className)[0] == null)
  810. {
  811. throw new Error(id + " does not contain any element with class: " + className);
  812. }
  813. }