JPDB add context menu in reviews

Adds a new word's context menu from its top deck in JPDB reviews

  1. // ==UserScript==
  2. // @name JPDB add context menu in reviews
  3. // @namespace jpdb.io
  4. // @version 0.1.2
  5. // @description Adds a new word's context menu from its top deck in JPDB reviews
  6. // @author daruko
  7. // @match https://jpdb.io/review*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=jpdb.io
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const debug = false;
  17.  
  18. let menuDetails = init();
  19. const observer = new MutationObserver(handleMutation);
  20. observer.observe(document.body, { childList: true, subtree: true });
  21.  
  22. function handleMutation(mutations) {
  23. if (!menuDetails || !document.body.contains(menuDetails)) {
  24. menuDetails = init();
  25. }
  26. }
  27.  
  28. function init() {
  29. const wordUri = document.querySelector('.review-reveal .answer-box a[href^="/vocabulary"]')?.href;
  30. const deckUri = document.querySelector('.review-reveal a[href^="/deck"]')?.href;
  31. if (!wordUri || !deckUri) {
  32. debug && console.debug('URI not found');
  33. return;
  34. }
  35. const [,, wordId, word] = new URL(wordUri).pathname.split('/', 4);
  36. if (!word) {
  37. debug && console.debug('Word URI not recognized');
  38. return;
  39. }
  40. return createDropdownButton(wordId, word, deckUri);
  41. }
  42.  
  43. function createDropdownButton(wordId, word, deckUri) {
  44. const sideButton = document.querySelector('.review-button-group .side-button');
  45. if (!sideButton) {
  46. debug && console.debug('Side button not found');
  47. return;
  48. }
  49. const buttonWrapper = document.createElement("div");
  50. buttonWrapper.style = "display: flex; flex-direction: column;";
  51. sideButton.style = "flex-grow: 1;" + sideButton.style;
  52. sideButton.parentNode.insertBefore(buttonWrapper, sideButton);
  53. buttonWrapper.appendChild(sideButton);
  54.  
  55. const dropdownButtonLabel = document.createElement("label");
  56. dropdownButtonLabel.className = "side-button";
  57. dropdownButtonLabel.style = "margin-top: 0;";
  58. const dropdownButtonDiv = document.createElement("div");
  59. dropdownButtonDiv.className = "dropdown right-aligned";
  60. dropdownButtonLabel.appendChild(dropdownButtonDiv);
  61. const dropdownButtonDetails = document.createElement("details");
  62. dropdownButtonDiv.appendChild(dropdownButtonDetails);
  63. const dropdownButtonSummary = document.createElement("summary");
  64. dropdownButtonSummary.style = "padding: 0; border: none;";
  65. dropdownButtonSummary.appendChild(document.createTextNode("⋮"));
  66. dropdownButtonDetails.appendChild(dropdownButtonSummary);
  67. buttonWrapper.appendChild(dropdownButtonLabel);
  68.  
  69. const onToggle = () => {
  70. useDropdown(dropdownButtonDetails, wordId, word, deckUri)
  71. .then(() => dropdownButtonDetails.removeEventListener("toggle", onToggle));
  72. };
  73. dropdownButtonDetails.addEventListener("toggle", onToggle);
  74.  
  75. return dropdownButtonDetails;
  76. }
  77.  
  78. async function useDropdown(details, wordId, word, deckUri) {
  79. const filteredDeckUri = `${deckUri}&q=${word}`;
  80. const fallbackDeckUri = `${deckUri.replace(/id=[0-9]+/, 'id=global')}&q=${word}`;
  81. let dropdownContent = await loadDropdown(filteredDeckUri, wordId);
  82. if (!dropdownContent) {
  83. debug && console.debug('Dropdown not found in the primary deck page');
  84. dropdownContent = await loadDropdown(fallbackDeckUri, wordId);
  85. if (!dropdownContent) {
  86. debug && console.debug('Dropdown not found in the global deck page');
  87. return;
  88. }
  89. }
  90. details.appendChild(dropdownContent);
  91. const offset = Math.min(details.getBoundingClientRect().left - dropdownContent.offsetWidth,
  92. details.parentElement.offsetWidth);
  93. dropdownContent.style = `bottom: 0; right: ${offset}px;` + dropdownContent.style;
  94. return dropdownContent;
  95. }
  96.  
  97. function loadDropdown(uri, wordId) {
  98. return fetch(uri)
  99. .then((response) => response.text())
  100. .then((text) => {
  101. const html = new DOMParser().parseFromString(text, "text/html");
  102. return html.querySelector(`.entry .vocabulary-spelling a[href^="/vocabulary/${wordId}"]`)
  103. ?.closest('.entry')
  104. ?.querySelector('.dropdown details .dropdown-content');
  105. })
  106. .catch((err) => {
  107. console.error('An error has occurred.', err);
  108. });
  109. }
  110. })();