LibraryThing better "Combine Works" button

Improvements to the "Combine works" button on combination pages

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        LibraryThing better "Combine Works" button
// @namespace   https://greasyfork.org/en/users/11592-max-starkenburg
// @description Improvements to the "Combine works" button on combination pages
// @include     http*://*librarything.tld/combine.php?*
// @include     http*://*librarything.com/combine.php?*
// @version     3
// @grant       none
// ==/UserScript==

// Some variables reused in multiple places
var body = document.getElementsByTagName("body")[0];
var combineForm = document.getElementsByName("works")[0];
var noneSelected = "<li style='padding-left: 10px;'><i>No items currently selected</i></li>";

// Set some styling for various new features
var head = document.getElementsByTagName("head")[0];
var style = document.createElement("style");
style.type = "text/css";
style.textContent = '\
    .gm-frozen{ color: #888 !important; background-color: #f9f9f9 !important; transition: background-color 1s, color 1s; } \
    .gm-frozen a, .alwaysblue .gm-frozen a { color: #888 !important; transition: color 1s; }\
    #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; }\
    #gm-selected-list { padding-left: 0; list-style-type: none; }\
    #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; }\
    #gm-dim { position: fixed; top: 0; left: 0; z-index: 1999; opacity: .8; width: 100%; height: 100%; background-color: #999; display: none; }\
    #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; }\
    #gm-confirmation-contents { margin: 20px; }\
    #gm-close-x { float: right; cursor: pointer; font-size: 1.3em; margin: 10px 15px 0 0; }\
    form #gm-spinner { position: absolute; margin-left: 5px; }\
    ';
head.appendChild(style);

// Clone the element containing the existing buttons
var newButtons = combineForm.children[3].cloneNode(true); 
newButtons.id = "gm-new-buttons";

// Make some adjustments to the new "Combine" button
var newCombine = newButtons.children[0];
newCombine.removeAttribute("onclick");
newCombine.type = "button"; // without this, it was submitting the form anyway even if howmanychecked returned < 2
newCombine.addEventListener("click", loadConfirmation, false);

// 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)
newButtons.children[1].value = "Clear selected";

// Have a dedicated (if redundant) button to refresh page, since it's not totally intuitive how to see updates on "frozen" works  
newRefresh = document.createElement("input");
newRefresh.name = "refresh";
newRefresh.type = "button";
newRefresh.value = "Get updates (refresh page)";
newRefresh.addEventListener("click",specialRefresh,false);
newButtons.appendChild(newRefresh);

// "Special" so that works get unselected (since they might not be quite the same post-combination), 
// 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
function specialRefresh(){
  enableInputs();
  document.getElementsByName("reset")[0].click(); // not using .reset() on form because there's a conflict with the existing button's name
  // without the timeout, it wasn't always completing the above lines
  setTimeout( function() { window.location.reload(false) }, 100); // false to make it a soft refresh
}

// Make a list and header for the currently selected items
var selectedList = document.createElement("ul");
selectedList.id = "gm-selected-list";
selectedList.innerHTML = noneSelected;
newButtons.insertBefore(selectedList,newButtons.firstChild);
var selectedHeader = document.createElement("b");
selectedHeader.textContent = "Selected works ";
newButtons.insertBefore(selectedHeader, selectedList);
  
// Have a running count of how many works are currently selected
var currentCount = document.createElement('span');
currentCount.textContent = "(0)";
newButtons.insertBefore(currentCount, selectedList);  
 
// Append the new buttons and list to the page (as descendant of form)
combineForm.appendChild(newButtons);

// Helper function to reenable the inputs that change their disabled status at certain points
var allinputs = document.getElementsByTagName("input");
function enableInputs() {
  for (var i=0; i<allinputs.length; i++) {
    allinputs[i].disabled = false;
  }
}

// This function that populates the list when works are selected
function showSelected() {
  // Make a count/total
  var currentlySelected = document.querySelectorAll('.combinetable input[type="checkbox"]:checked');
  var currentLength = currentlySelected.length;
  var currentHTML = currentLength == 0 ? "(0)" : "<b>(" + currentLength + ")</b>";
  currentCount.innerHTML = currentHTML;
  // Add the names of the currently selected works to the list
  var worksTitles = "";
  for (var i=0; i<currentLength; i++) {
    var title = currentlySelected[i].parentElement.nextSibling.childNodes[0].childNodes[1].nodeValue;
    var title = title.replace(/'/g,"&#39;"); // escape single quotes so title attr doesn't get truncated
    worksTitles += "<li class='gm-item' title='" + title + "'>" + title + "</li>";
  }
  selectedList.innerHTML = currentLength == 0 ? noneSelected : worksTitles;
}
  
// Run this initially even on page load, since sometimes works are still selected or disabled after a soft reload
showSelected();
enableInputs();
  
// Make the reset/clear buttons empty the new selected list display (without unfreezing any frozen works)
var resets = document.querySelectorAll("input[type='reset']");
for (var i=0; i<resets.length; i++) {
  resets[i].removeAttribute("onclick");
  resets[i].addEventListener("click", function() {
    // Alteration of resetcb() on page, to prevent unfreezing any currently frozen works on a form reset
    for (var j=1; j<numberofrows+1; j++) { // numberofrows is set in the existing html page
      var currentRow = document.getElementById("r"+j);
      if (currentRow.className != 'gm-frozen') currentRow.className = 'c_n';
    }
    currentCount.textContent = "(0)";
    selectedList.innerHTML = noneSelected;
  });
}
  
// Make clicking any of the works' checkboxes populate the list in the fixed box
workBoxes = document.getElementsByClassName("combinetable")[0].getElementsByTagName("tr");
for (k=0; k<workBoxes.length; k++) {
  workBoxes[k].addEventListener("click", showSelected);
}

// Create a pop-up like div that will get populated with the confirmation form
var confirmation = document.createElement("div");
confirmation.id = "gm-confirmation";
// with an "x" to close, same as cancel, but perhaps more intuitive sometimes
var closeX = document.createElement("span");
closeX.title = "close";
closeX.textContent = "×";
closeX.id = "gm-close-x";
confirmation.appendChild(closeX);
closeX.addEventListener("click", hideConfirmation);
// and with a child div for the sake of some small implementation details
var confirmationContents = document.createElement("div");
confirmationContents.id = "gm-confirmation-contents";
confirmation.appendChild(confirmationContents);
body.appendChild(confirmation);

// Stuff to handle the sizing and modal qualities of the confirmation pop-up div
var dim = document.createElement("div");
dim.id = "gm-dim";
body.appendChild(dim);
var hideDistance = "-1000px";
function confirmationSize() {
  // expand it to most of the window so that it's the only focus at the moment
  var width = window.innerWidth - 200;
  var height = window.innerHeight - 150;
  confirmation.style.width = width + "px";
  confirmation.style.marginLeft = "-" + (width / 2) + "px";
  confirmation.style.height = height + "px";
  hideDistance = "-" + (height + 50) + "px";
  if (confirmation.style.top != "75px") {
    confirmation.style.top = hideDistance;
  }
}
confirmationSize();
window.addEventListener("resize", confirmationSize, false);

// Helper function for adding a spinner image to give you the reassuring illusion that things are processing
function addSpinner(appendToMe) {
  var spinner = document.createElement("img");
  spinner.id = "gm-spinner";
  spinner.src = "/pics/blog/spinner_mediumblack.gif";
  appendToMe.appendChild(spinner);
}

// Pull down the div, stuff it with the contents of the confirmation page you'd get it you had just clicked old-style "Confirm"
function loadConfirmation() {
  if (howmanychecked()) { // Make sure there are more than 2 checked
    dim.style.display = "block";
    addSpinner(confirmationContents);
    confirmation.style.top = '75px';
    // Though I originally added the timeout to avoid, a bug, I thought the delay also helped with not making things _too_ fast 
    // (like after a while I could start to just click through without paying enough attention)
    setTimeout(function(){ 
      var xhr = new XMLHttpRequest();
      xhr.open('POST', 'work_combineworks.php', true); // go get the confirmation page's HTML
      xhr.onload = function () {
        // Stuff the confirmation page's HTML into the innerHTML of a new document
        // I tried a few other ways to convert the HTML into reusable DOM, but none were working out for me
        var newDoc = document.implementation.createHTMLDocument();
        newDoc.documentElement.innerHTML = this.responseText;
        // Pull in the styling info from the <head> of the confirmation page's HTML.
        // Prepend an ID ancestor to its selectors to prevent conflicts with that styling vs. the current page's styling
        // Append this modified CSS to current page (unless that's already been done before)
        if (!document.contains(document.getElementById("gm-localized-style"))) {
          var externalStyles = newDoc.getElementsByTagName("style")[0].sheet.cssRules; // newDoc.styleSheets[0] is undefined in Chrome
          var localizedRules = [], prefixedRule;
          for (var i=0; i<externalStyles.length; i++) {
            prefixedRule = "#gm-confirmation " + externalStyles[i].cssText;
            localizedRules.push(prefixedRule);
          }
          var localizedStyle = document.createElement("style");
          localizedStyle.id = "gm-localized-style";
          localizedStyle.textContent = localizedRules.join(" ");
          head.appendChild(localizedStyle);
        }
        // Remove the spinner image
        confirmationContents.removeChild(confirmationContents.firstChild); 
        // Inject the body of the confirmation page's HTML, minus header and footer, into the pop-up div
        var confirmCode = newDoc.getElementsByClassName("content")[0];
        confirmationContents.appendChild(confirmCode);

        // Don't submit as normal, else you'd get redirected
        var confirmButton = confirmation.getElementsByTagName("input")[0];
        confirmButton.type = "button";
        confirmButton.removeAttribute("onclick");
        confirmButton.addEventListener("click", submitConfirmation);

        // Change cancel button to hide the pop-up div instead of going back one page
        var cancelButton = confirmation.getElementsByTagName("input")[1];
        cancelButton.removeAttribute("onclick");
        cancelButton.addEventListener("click", hideConfirmation);

      };
      xhr.send(new FormData(combineForm)); // send the appropriate input through with the POST
    }, 1500); 
  }
}
  
// Submit the form in the background, and make the relevant works "frozen" to prevent possibility of "recombining" them, 
// 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)
function submitConfirmation() {
  var confirmForm = document.getElementsByName("confirm")[0];
  var xhr = new XMLHttpRequest();
  xhr.open('POST', 'work_combineworks_submit.php', true); // submit the confirmation to LT
  xhr.onloadstart = function () {
    // disable the buttons after "Combine" has been clicked, to prevent multiple submissions, and since post-sumbission Cancel doesn't accomplish anything
    confirmInputs = confirmForm.getElementsByTagName("input");
    for (var i=0; i<2; i++) { // Just disable the first two inputs (Confirm and Cancel buttons), not the hidden ones
      confirmInputs[i].disabled = true;
    }
    addSpinner(confirmForm.firstElementChild);
    // Originally was going to do the rest in onload or onloadend, but since that will wait for the responseText 
    // which might be take a while for a popular author with lots of works, just create the illusion with a timeout
    setTimeout(function(){
      // freeze the combined works
      var currentlySelected = document.querySelectorAll(".lit"); // querySelectorAll instead of getelementsbyclass becuase else it breaks after first iteration of resetting the classes
      for (var j=0; j<currentlySelected.length; j++) {
        var currentWork = currentlySelected[j];
        var currentCheckbox = currentWork.getElementsByTagName("input")[0];
        currentCheckbox.checked = false;
        currentCheckbox.disabled = true;
        currentWork.getElementsByTagName("td")[1].removeAttribute("onclick");
        currentWork.className = 'gm-frozen';
      }
      // reset the "currently selected" list:
      currentCount.textContent = "(0)";
      selectedList.innerHTML = noneSelected;
      // slide the box bax up (amongst other things):
      hideConfirmation();
    }, 1500);
  }
  xhr.send(new FormData(confirmForm)); // send the appropriate input through with the POST
}

// Reset the confirmation box's HTML, send it back up offscreen
function hideConfirmation() {
  // Empty the contents (to avoid accidental clicks if :focus is somehow there, and so it's fresh each time it comes down)
  while (confirmationContents.firstChild) { // apparently more performant than setting innerHTML = ""
    confirmationContents.removeChild(confirmationContents.lastChild);
  }
  confirmation.style.top = hideDistance;
  dim.style.display = "none";
}