Greasy Fork 还支持 简体中文。

LibraryThing better "Combine Works" button

Improvements to the "Combine works" button on combination pages

  1. // ==UserScript==
  2. // @name LibraryThing better "Combine Works" button
  3. // @namespace https://greasyfork.org/en/users/11592-max-starkenburg
  4. // @description Improvements to the "Combine works" button on combination pages
  5. // @include http*://*librarything.tld/combine.php?*
  6. // @include http*://*librarything.com/combine.php?*
  7. // @version 3
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. // Some variables reused in multiple places
  12. var body = document.getElementsByTagName("body")[0];
  13. var combineForm = document.getElementsByName("works")[0];
  14. var noneSelected = "<li style='padding-left: 10px;'><i>No items currently selected</i></li>";
  15.  
  16. // Set some styling for various new features
  17. var head = document.getElementsByTagName("head")[0];
  18. var style = document.createElement("style");
  19. style.type = "text/css";
  20. style.textContent = '\
  21. .gm-frozen{ color: #888 !important; background-color: #f9f9f9 !important; transition: background-color 1s, color 1s; } \
  22. .gm-frozen a, .alwaysblue .gm-frozen a { color: #888 !important; transition: color 1s; }\
  23. #gm-new-buttons { position: fixed; right: 0; bottom: 0; margin: 0; width: 430px; padding: 10px 15px; background-color: #f7f7f7; border: solid #999; border-width: 1px 0 0 1px; border-radius: 3px; box-shadow: 0 0 10px #ddd; }\
  24. #gm-selected-list { padding-left: 0; list-style-type: none; }\
  25. #gm-selected-list li.gm-item{ max-width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; border: dotted #E7EE82; padding: 0 5px; border-width: 2px; margin-top: -2px; background-color: #F8FF93; }\
  26. #gm-dim { position: fixed; top: 0; left: 0; z-index: 1999; opacity: .8; width: 100%; height: 100%; background-color: #999; display: none; }\
  27. #gm-confirmation { border: 1px solid #999; background-color: white; box-shadow: 0 0 15px 0px #777; position: fixed; z-index: 2000; left: 50%; transition: top .4s; overflow: auto; }\
  28. #gm-confirmation-contents { margin: 20px; }\
  29. #gm-close-x { float: right; cursor: pointer; font-size: 1.3em; margin: 10px 15px 0 0; }\
  30. form #gm-spinner { position: absolute; margin-left: 5px; }\
  31. ';
  32. head.appendChild(style);
  33.  
  34. // Clone the element containing the existing buttons
  35. var newButtons = combineForm.children[3].cloneNode(true);
  36. newButtons.id = "gm-new-buttons";
  37.  
  38. // Make some adjustments to the new "Combine" button
  39. var newCombine = newButtons.children[0];
  40. newCombine.removeAttribute("onclick");
  41. newCombine.type = "button"; // without this, it was submitting the form anyway even if howmanychecked returned < 2
  42. newCombine.addEventListener("click", loadConfirmation, false);
  43.  
  44. // Change "Reset" to "Clear selected", since I thought it seemed clearer (also, it's not a true reset, since it won't unfreeze "frozen" works)
  45. newButtons.children[1].value = "Clear selected";
  46.  
  47. // Have a dedicated (if redundant) button to refresh page, since it's not totally intuitive how to see updates on "frozen" works
  48. newRefresh = document.createElement("input");
  49. newRefresh.name = "refresh";
  50. newRefresh.type = "button";
  51. newRefresh.value = "Get updates (refresh page)";
  52. newRefresh.addEventListener("click",specialRefresh,false);
  53. newButtons.appendChild(newRefresh);
  54.  
  55. // "Special" so that works get unselected (since they might not be quite the same post-combination),
  56. // but it's a "soft" refresh so you're not thrown all the way back up to the top again, losing your place, or at least in some browsers
  57. function specialRefresh(){
  58. enableInputs();
  59. document.getElementsByName("reset")[0].click(); // not using .reset() on form because there's a conflict with the existing button's name
  60. // without the timeout, it wasn't always completing the above lines
  61. setTimeout( function() { window.location.reload(false) }, 100); // false to make it a soft refresh
  62. }
  63.  
  64. // Make a list and header for the currently selected items
  65. var selectedList = document.createElement("ul");
  66. selectedList.id = "gm-selected-list";
  67. selectedList.innerHTML = noneSelected;
  68. newButtons.insertBefore(selectedList,newButtons.firstChild);
  69. var selectedHeader = document.createElement("b");
  70. selectedHeader.textContent = "Selected works ";
  71. newButtons.insertBefore(selectedHeader, selectedList);
  72. // Have a running count of how many works are currently selected
  73. var currentCount = document.createElement('span');
  74. currentCount.textContent = "(0)";
  75. newButtons.insertBefore(currentCount, selectedList);
  76. // Append the new buttons and list to the page (as descendant of form)
  77. combineForm.appendChild(newButtons);
  78.  
  79. // Helper function to reenable the inputs that change their disabled status at certain points
  80. var allinputs = document.getElementsByTagName("input");
  81. function enableInputs() {
  82. for (var i=0; i<allinputs.length; i++) {
  83. allinputs[i].disabled = false;
  84. }
  85. }
  86.  
  87. // This function that populates the list when works are selected
  88. function showSelected() {
  89. // Make a count/total
  90. var currentlySelected = document.querySelectorAll('.combinetable input[type="checkbox"]:checked');
  91. var currentLength = currentlySelected.length;
  92. var currentHTML = currentLength == 0 ? "(0)" : "<b>(" + currentLength + ")</b>";
  93. currentCount.innerHTML = currentHTML;
  94. // Add the names of the currently selected works to the list
  95. var worksTitles = "";
  96. for (var i=0; i<currentLength; i++) {
  97. var title = currentlySelected[i].parentElement.nextSibling.childNodes[0].childNodes[1].nodeValue;
  98. var title = title.replace(/'/g,"&#39;"); // escape single quotes so title attr doesn't get truncated
  99. worksTitles += "<li class='gm-item' title='" + title + "'>" + title + "</li>";
  100. }
  101. selectedList.innerHTML = currentLength == 0 ? noneSelected : worksTitles;
  102. }
  103. // Run this initially even on page load, since sometimes works are still selected or disabled after a soft reload
  104. showSelected();
  105. enableInputs();
  106. // Make the reset/clear buttons empty the new selected list display (without unfreezing any frozen works)
  107. var resets = document.querySelectorAll("input[type='reset']");
  108. for (var i=0; i<resets.length; i++) {
  109. resets[i].removeAttribute("onclick");
  110. resets[i].addEventListener("click", function() {
  111. // Alteration of resetcb() on page, to prevent unfreezing any currently frozen works on a form reset
  112. for (var j=1; j<numberofrows+1; j++) { // numberofrows is set in the existing html page
  113. var currentRow = document.getElementById("r"+j);
  114. if (currentRow.className != 'gm-frozen') currentRow.className = 'c_n';
  115. }
  116. currentCount.textContent = "(0)";
  117. selectedList.innerHTML = noneSelected;
  118. });
  119. }
  120. // Make clicking any of the works' checkboxes populate the list in the fixed box
  121. workBoxes = document.getElementsByClassName("combinetable")[0].getElementsByTagName("tr");
  122. for (k=0; k<workBoxes.length; k++) {
  123. workBoxes[k].addEventListener("click", showSelected);
  124. }
  125.  
  126. // Create a pop-up like div that will get populated with the confirmation form
  127. var confirmation = document.createElement("div");
  128. confirmation.id = "gm-confirmation";
  129. // with an "x" to close, same as cancel, but perhaps more intuitive sometimes
  130. var closeX = document.createElement("span");
  131. closeX.title = "close";
  132. closeX.textContent = "×";
  133. closeX.id = "gm-close-x";
  134. confirmation.appendChild(closeX);
  135. closeX.addEventListener("click", hideConfirmation);
  136. // and with a child div for the sake of some small implementation details
  137. var confirmationContents = document.createElement("div");
  138. confirmationContents.id = "gm-confirmation-contents";
  139. confirmation.appendChild(confirmationContents);
  140. body.appendChild(confirmation);
  141.  
  142. // Stuff to handle the sizing and modal qualities of the confirmation pop-up div
  143. var dim = document.createElement("div");
  144. dim.id = "gm-dim";
  145. body.appendChild(dim);
  146. var hideDistance = "-1000px";
  147. function confirmationSize() {
  148. // expand it to most of the window so that it's the only focus at the moment
  149. var width = window.innerWidth - 200;
  150. var height = window.innerHeight - 150;
  151. confirmation.style.width = width + "px";
  152. confirmation.style.marginLeft = "-" + (width / 2) + "px";
  153. confirmation.style.height = height + "px";
  154. hideDistance = "-" + (height + 50) + "px";
  155. if (confirmation.style.top != "75px") {
  156. confirmation.style.top = hideDistance;
  157. }
  158. }
  159. confirmationSize();
  160. window.addEventListener("resize", confirmationSize, false);
  161.  
  162. // Helper function for adding a spinner image to give you the reassuring illusion that things are processing
  163. function addSpinner(appendToMe) {
  164. var spinner = document.createElement("img");
  165. spinner.id = "gm-spinner";
  166. spinner.src = "/pics/blog/spinner_mediumblack.gif";
  167. appendToMe.appendChild(spinner);
  168. }
  169.  
  170. // Pull down the div, stuff it with the contents of the confirmation page you'd get it you had just clicked old-style "Confirm"
  171. function loadConfirmation() {
  172. if (howmanychecked()) { // Make sure there are more than 2 checked
  173. dim.style.display = "block";
  174. addSpinner(confirmationContents);
  175. confirmation.style.top = '75px';
  176. // Though I originally added the timeout to avoid, a bug, I thought the delay also helped with not making things _too_ fast
  177. // (like after a while I could start to just click through without paying enough attention)
  178. setTimeout(function(){
  179. var xhr = new XMLHttpRequest();
  180. xhr.open('POST', 'work_combineworks.php', true); // go get the confirmation page's HTML
  181. xhr.onload = function () {
  182. // Stuff the confirmation page's HTML into the innerHTML of a new document
  183. // I tried a few other ways to convert the HTML into reusable DOM, but none were working out for me
  184. var newDoc = document.implementation.createHTMLDocument();
  185. newDoc.documentElement.innerHTML = this.responseText;
  186. // Pull in the styling info from the <head> of the confirmation page's HTML.
  187. // Prepend an ID ancestor to its selectors to prevent conflicts with that styling vs. the current page's styling
  188. // Append this modified CSS to current page (unless that's already been done before)
  189. if (!document.contains(document.getElementById("gm-localized-style"))) {
  190. var externalStyles = newDoc.getElementsByTagName("style")[0].sheet.cssRules; // newDoc.styleSheets[0] is undefined in Chrome
  191. var localizedRules = [], prefixedRule;
  192. for (var i=0; i<externalStyles.length; i++) {
  193. prefixedRule = "#gm-confirmation " + externalStyles[i].cssText;
  194. localizedRules.push(prefixedRule);
  195. }
  196. var localizedStyle = document.createElement("style");
  197. localizedStyle.id = "gm-localized-style";
  198. localizedStyle.textContent = localizedRules.join(" ");
  199. head.appendChild(localizedStyle);
  200. }
  201. // Remove the spinner image
  202. confirmationContents.removeChild(confirmationContents.firstChild);
  203. // Inject the body of the confirmation page's HTML, minus header and footer, into the pop-up div
  204. var confirmCode = newDoc.getElementsByClassName("content")[0];
  205. confirmationContents.appendChild(confirmCode);
  206.  
  207. // Don't submit as normal, else you'd get redirected
  208. var confirmButton = confirmation.getElementsByTagName("input")[0];
  209. confirmButton.type = "button";
  210. confirmButton.removeAttribute("onclick");
  211. confirmButton.addEventListener("click", submitConfirmation);
  212.  
  213. // Change cancel button to hide the pop-up div instead of going back one page
  214. var cancelButton = confirmation.getElementsByTagName("input")[1];
  215. cancelButton.removeAttribute("onclick");
  216. cancelButton.addEventListener("click", hideConfirmation);
  217.  
  218. };
  219. xhr.send(new FormData(combineForm)); // send the appropriate input through with the POST
  220. }, 1500);
  221. }
  222. }
  223. // Submit the form in the background, and make the relevant works "frozen" to prevent possibility of "recombining" them,
  224. // or combining using a work number that's now redirected (not sure what would happen in that case, but I'd rather not even try)
  225. function submitConfirmation() {
  226. var confirmForm = document.getElementsByName("confirm")[0];
  227. var xhr = new XMLHttpRequest();
  228. xhr.open('POST', 'work_combineworks_submit.php', true); // submit the confirmation to LT
  229. xhr.onloadstart = function () {
  230. // disable the buttons after "Combine" has been clicked, to prevent multiple submissions, and since post-sumbission Cancel doesn't accomplish anything
  231. confirmInputs = confirmForm.getElementsByTagName("input");
  232. for (var i=0; i<2; i++) { // Just disable the first two inputs (Confirm and Cancel buttons), not the hidden ones
  233. confirmInputs[i].disabled = true;
  234. }
  235. addSpinner(confirmForm.firstElementChild);
  236. // Originally was going to do the rest in onload or onloadend, but since that will wait for the responseText
  237. // which might be take a while for a popular author with lots of works, just create the illusion with a timeout
  238. setTimeout(function(){
  239. // freeze the combined works
  240. var currentlySelected = document.querySelectorAll(".lit"); // querySelectorAll instead of getelementsbyclass becuase else it breaks after first iteration of resetting the classes
  241. for (var j=0; j<currentlySelected.length; j++) {
  242. var currentWork = currentlySelected[j];
  243. var currentCheckbox = currentWork.getElementsByTagName("input")[0];
  244. currentCheckbox.checked = false;
  245. currentCheckbox.disabled = true;
  246. currentWork.getElementsByTagName("td")[1].removeAttribute("onclick");
  247. currentWork.className = 'gm-frozen';
  248. }
  249. // reset the "currently selected" list:
  250. currentCount.textContent = "(0)";
  251. selectedList.innerHTML = noneSelected;
  252. // slide the box bax up (amongst other things):
  253. hideConfirmation();
  254. }, 1500);
  255. }
  256. xhr.send(new FormData(confirmForm)); // send the appropriate input through with the POST
  257. }
  258.  
  259. // Reset the confirmation box's HTML, send it back up offscreen
  260. function hideConfirmation() {
  261. // Empty the contents (to avoid accidental clicks if :focus is somehow there, and so it's fresh each time it comes down)
  262. while (confirmationContents.firstChild) { // apparently more performant than setting innerHTML = ""
  263. confirmationContents.removeChild(confirmationContents.lastChild);
  264. }
  265. confirmation.style.top = hideDistance;
  266. dim.style.display = "none";
  267. }