JPDB add context menu in reviews

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

目前为 2023-07-29 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name JPDB add context menu in reviews
  3. // @namespace jpdb.io
  4. // @version 0.1
  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. loadDropdown(dropdownButtonDetails, wordId, word, deckUri)
  71. .then(() => dropdownButtonDetails.removeEventListener("toggle", onToggle));
  72. };
  73. dropdownButtonDetails.addEventListener("toggle", onToggle);
  74.  
  75. return dropdownButtonDetails;
  76. }
  77.  
  78. function loadDropdown(details, wordId, word, deckUri) {
  79. const filteredDeckUri = `${deckUri}&q=${word}&show_only=new,locked`;
  80. return fetch(filteredDeckUri)
  81. .then((response) => response.text())
  82. .then((text) => {
  83. const html = new DOMParser().parseFromString(text, "text/html");
  84. const dropdownContent = html.querySelector(`.entry .vocabulary-spelling a[href^="/vocabulary/${wordId}"]`)
  85. ?.closest('.entry')
  86. ?.querySelector('.dropdown details .dropdown-content');
  87. if (!dropdownContent) {
  88. debug && console.debug('Dropdown not found in the deck page');
  89. return;
  90. }
  91. dropdownContent.style = "bottom: 0;" + dropdownContent.style;
  92. details.appendChild(dropdownContent);
  93. })
  94. .catch((err) => {
  95. console.error('An error has occurred.', err);
  96. });
  97. }
  98. })();