// ==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,"'"); // 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";
}