NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name mb. POWER VOTE // @version 2023.11.15 // @description musicbrainz.org: Adds some buttons to check all unvoted edits (Yes/No/Abs/None) at once in the edit search page. You can also collapse/expand (all) edits for clarity. A handy reset votes button is also available + Double click radio to vote single edit + range click with shift to vote a series of edits., Hidden (collapsed) edits will never be voted (even if range click or shift+click force vote). Fast approve with edit notes. Prevent leaving voting page with unsaved changes. Add hyperlinks after inline looked up entity green fields. // @namespace https://github.com/jesus2099/konami-command // @supportURL https://github.com/jesus2099/konami-command/labels/mb_POWER-VOTE // @downloadURL https://github.com/jesus2099/konami-command/raw/master/mb_POWER-VOTE.user.js // @author jesus2099 // @licence CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/ // @licence GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt // @since 2009-09-14; https://web.archive.org/web/20131103163355/userscripts.org/scripts/show/57765 / https://web.archive.org/web/20141011084007/userscripts-mirror.org/scripts/show/57765 // @icon data:image/gif;base64,R0lGODlhEAAQAKEDAP+/3/9/vwAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh/glqZXN1czIwOTkAIfkEAQACAwAsAAAAABAAEAAAAkCcL5nHlgFiWE3AiMFkNnvBed42CCJgmlsnplhyonIEZ8ElQY8U66X+oZF2ogkIYcFpKI6b4uls3pyKqfGJzRYAACH5BAEIAAMALAgABQAFAAMAAAIFhI8ioAUAIfkEAQgAAwAsCAAGAAUAAgAAAgSEDHgFADs= // @require https://github.com/jesus2099/konami-command/raw/45e79077994ef566d0f7f513f8d838c151f1989d/lib/CONTROL-POMME.js?version=2023.2.23 // @require https://github.com/jesus2099/konami-command/raw/de88f870c0e6c633e02f32695e32c4f50329fc3e/lib/SUPER.js?version=2022.3.24.224 // @grant none // @include /^https?:\/\/(\w+\.)?musicbrainz\.org\/[^/]+\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\/(open_)?edits\b/ // @include /^https?:\/\/(\w+\.)?musicbrainz\.org\/edit\/\d+\b/ // @include /^https?:\/\/(\w+\.)?musicbrainz\.org\/edit\/(open|subscribed(_editors)?)\b/ // @include /^https?:\/\/(\w+\.)?musicbrainz\.org\/search\/edits\b/ // @include /^https?:\/\/(\w+\.)?musicbrainz\.org\/user\/[^/]+\/(edits(\/open)?|votes)\b/ // @run-at document-end // ==/UserScript== "use strict"; var userjs = "jesus2099userjs57765"; var editform = document.querySelector("div#edits > form"); var edit_list = document.querySelectorAll("div#edits > form > div.edit-list"); var search_form = document.querySelector("div#content > form[action='/search/edits']"); var j2css = document.createElement("style"); j2css.setAttribute("type", "text/css"); document.head.appendChild(j2css); j2css = j2css.sheet; // - --- - --- - --- - START OF CONFIGURATION - --- - --- - --- - var showtop = true; var showbottom = true; var border = "thin dashed red"; // leave "" for defaults var onlySubmitTabIndexed = true; // hit tab after typed text or voted directly goes to a submit button var rangeclick = true; // multiple votes by clicking first vote then shift-clicking last radio in a range var collapseEdits = true; var voteColours = true; // - --- - --- - --- - END OF CONFIGURATION - --- - --- - --- - var str_regex_gid = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"; var FF = /firefox/i.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent); // FF has bugs if (FF) { FF = {"1": "#b1ebb0", "0": "#ebb1ba", "-1": "#f2f0a5"}; } j2css.insertRule("div.edit-list." + userjs + "force, div.edit-list." + userjs + "ninja > div.edit-actions, div.edit-list." + userjs + "ninja > div.edit-details, div.edit-list." + userjs + "ninja > div.edit-notes { overflow: hidden !important; height: 0 !important; !important; padding: 0 !important; margin: 0 !important; }", 0); j2css.insertRule("div#" + userjs + "xhrstat { position: fixed; top: 0; left: 0; border: 2px solid black; border-width: 0 2px 2px 0; background-color: #ff6; }", 0); j2css.insertRule("tr.rename-artist-credits." + userjs + "yes > th { vertical-align: middle; }", 0); j2css.insertRule("tr.rename-artist-credits." + userjs + "yes > td { color: #f00; font-weight: bolder; font-size: 2em; text-shadow: 1px 1px 0 #663; text-transform: uppercase; }", 0); j2css.insertRule("/*div#content >*/ form[action='/search/edits'] span." + userjs + "-permalink { background-color: #ffc; }", 0); j2css.insertRule("/*div#content >*/ form[action='/search/edits'] span." + userjs + "-permalink[data-gid=''] { font-style: italic; }", 0); j2css.insertRule("form[action='/edit/enter_votes'] div.voteopts > div.vote > label { user-select: none; }", 0); // Hide automod “Vote on all edits” feature, because POWER VOTE is better if (showtop) { j2css.insertRule("div.overall-vote { display: none; }", 0); } // localisation var lang = document.querySelector("html[lang]"); lang = lang && lang.getAttribute("lang") || "en"; var texts = { de: { double_click_to_vote: "doppelklicken Sie, um diese Änderung abzustimmen", edit: "Bearbeiten", editing_history: "Bearbeitungshistorie", open_edits: "Offene Bearbeitungen", order_newest_first: "die neuesten zuerst", order_oldest_first: "die ältesten zuerst", reset_votes: "Stimmen zurücksetzen", show_form: "Formular zeigen", vote_all: " // Über alle nicht abgestimmten Änderungen abstimmen (shift+Klick für alle) → ", }, en: { double_click_to_vote: "double-click to vote this edit", edit: "Edit", editing_history: "Editing history", open_edits: "Open edits", order_newest_first: "newest first", order_oldest_first: "oldest first", reset_votes: "Reset votes", show_form: "Show form", vote_all: " // Vote on all unvoted edits (" + CONTROL_POMME.shift.label + "click for all) → ", }, fr: { double_click_to_vote: "double-cliquer pour voter cette modification", edit: "Modifier", editing_history: "Historique des modifications", open_edits: "Modifications en attente", order_newest_first: "les plus récents en premier", order_oldest_first: "les plus anciens en premier", reset_votes: "Réinitialiser les votes", show_form: "Afficher le formulaire", vote_all: " // Voter pour toutes les modifications non votées (" + CONTROL_POMME.shift.label + "clic pour toutes) → ", }, it: { double_click_to_vote: "fare doppio clic per votare questa modifica", edit: "Modifica", editing_history: "Cronologia modifiche", open_edits: "Modifiche in corso", order_newest_first: "prima le più recenti", order_oldest_first: "prima le più vecchie", reset_votes: "Reimposta voti", show_form: "Mostra modulo", vote_all: " // Vota per tutte le modifiche non votate (" + CONTROL_POMME.shift.label + "clic per tutte) → ", }, nl: { double_click_to_vote: "dubbelklik om deze wijziging te stemmen", edit: "Bewerken", editing_history: "Alle bewerkingen", open_edits: "Open bewerkingen", order_newest_first: "nieuwste eerst", order_oldest_first: "oudste eerst", reset_votes: "Stemmen terugzetten", show_form: "Vorm tonen", vote_all: " // Stem op alle niet-gestemde wijzigingen (" + CONTROL_POMME.shift.label + "klik voor alles) → ", }, }; // Remind sort order in Search for Edits title if (edit_list.length > 1) { var order; if (search_form) { order = document.querySelector("div#content form#edit-search select[name='order']").selectedOptions[0].label; } else { if ( edit_list[0].querySelector("div.edit-list > div.edit-header > h2 > a[href*='/edit/']").getAttribute("href").match(/\d+$/)[0] > edit_list[1].querySelector("div.edit-list > div.edit-header > h2 > a[href*='/edit/']").getAttribute("href").match(/\d+$/)[0] ) { order = texts[lang].order_newest_first; } else { order = texts[lang].order_oldest_first; } } if (order) { document.querySelector("#content h1, #content > h2").appendChild(createTag("small", {}, [ " (", createTag("span", {s: {background: "#ffc"}}, order), ")"])); } } // Hide huge edit search form that pushes results off screen if ( search_form && location.search // not initial blank edit search form && !location.search.match(/\bform_only=yes\b/) // not form only after Refine click && edit_list.length > 0 // has 1+ results ) { j2css.insertRule("div#content." + userjs + "-hide-form > p:nth-of-type(1), div#content." + userjs + "-hide-form > p:nth-of-type(2), div#content." + userjs + "-hide-form > form#edit-search { display: none; }", 0); search_form.parentNode.classList.add(userjs + "-hide-form"); document.querySelector("#content > h1").appendChild(createTag("fragment", {}, [ " ", createTag("button", {a: {title: GM_info.script.name}, s: {background: "#fcf", cursor: "pointer"}, e: {click: function(event) { search_form.parentNode.classList.remove(userjs + "-hide-form"); removeNode(event.target); scroll_to_first_selected_options(); }, mouseover: function(event) { event.target.click(); }}}, texts[lang].show_form) ])); } else { var visible_form_waiter = setInterval(function() { if (!search_form.querySelector("span.field[style*='display: none']")) { clearInterval(visible_form_waiter); scroll_to_first_selected_options(); } }, 234); } // Search form: Add permalinks to searched entities (all except recordings, because of MBS-12560) if (search_form) { (new MutationObserver(function(mutations, observer) { for (var m = 0; m < mutations.length; m++) { var span_autocomplete; if ( mutations[m].type === "childList" && mutations[m].target.matches("span.autocomplete") && mutations[m].target.querySelector("span.autocomplete > input.name.ui-autocomplete-input.lookup-performed ~ input[type='hidden'].id[value]:not([value=''])") && (span_autocomplete = mutations[m].target) || mutations[m].type === "attributes" && mutations[m].target === mutations[m].target.parentNode.querySelector("span.autocomplete > input.name.ui-autocomplete-input.lookup-performed ~ input[type='hidden'].id") && (span_autocomplete = mutations[m].target.parentNode) ) { var type = span_autocomplete.className.match(/\b(area|artist|editor|event|instrument|label|place|recording|release-group|release|series|work)\b/); var gid = span_autocomplete.querySelector("input[type='hidden'].gid").value; var id = span_autocomplete.querySelector("input[type='hidden'].id").value; var name = span_autocomplete.querySelector("input.name.ui-autocomplete-input.lookup-performed").value; if (type && (gid || id) && name && type[1] != "recording") { // TODO: remove recording exlusion when MBS bug is fixed #703 https://tickets.metabrainz.org/browse/MBS-12560 type = type[1]; if (type == "editor") { type = "user"; gid = escape(name); } var path = "/" + type + "/" + (gid || id); var bonus_links = { open_edits: path + "/open_edits", editing_history: path + "/edits", edit: path + "/edit" }; if (type == "user") { bonus_links.open_edits = path + "/edits/open"; bonus_links.edit = "/admin/user/edit/" + gid; } var permalink = span_autocomplete.parentNode.querySelector("span." + userjs + "-permalink"); var new_permalink = createTag("span", {a: {class: userjs + "-permalink", "data-gid": gid}}, [ createTag("a", {a: { href: path, target: "_blank", title: GM_info.script.name }}, name), " <", createTag("a", {a: {href: bonus_links.open_edits, title: texts[lang].open_edits, class: userjs + "-permalink"}}, "O"), createTag("a", {a: {href: bonus_links.editing_history, title: texts[lang].editing_history, class: userjs + "-permalink"}}, "H"), createTag("a", {a: {href: bonus_links.edit, title: texts[lang].edit, class: userjs + "-permalink"}}, "E"), ">" ]); if (!permalink) { span_autocomplete.parentNode.appendChild(new_permalink); if (!gid) { // When opening a refine search with preloaded entity filters, MBID (gid) is unset // Entity page with ID URL redirects 302 to page with MBID URL, fetch MBID from there and finish permalink var xhr = new XMLHttpRequest(); xhr.id = id; xhr.type = type; xhr.updateGid = function(mbid) { var permalink_a = document.querySelector("span." + userjs + "-permalink > a[href='/" + this.type + "/" + this.id + "']"); if (permalink_a) { permalink_a.parentNode.parentNode.querySelector("input[type='hidden'].gid").value = mbid; // Manual trigger permalink_a.parentNode.parentNode.querySelector("span.autocomplete").appendChild(document.createTextNode(" ")); } }; xhr.addEventListener("readystatechange", function(event) { var MBID = this.responseURL.match(new RegExp("/" + this.type + "/(" + str_regex_gid + ")$")); if (MBID) { this.updateGid(MBID[1]); this.abort(); } }); xhr.addEventListener("load", function(event) { // Reached only if not redirected to MBID (bug with work entity type) https://tickets.metabrainz.org/browse/MBS-12562 var MBID = this.responseText.match(new RegExp("<h1>.+\\b(" + str_regex_gid + ")\\b")); if (MBID) { this.updateGid(MBID[1]); } }); xhr.open("GET", path, true); xhr.send(null); } } else if (permalink.dataset["gid"] != gid) { span_autocomplete.parentNode.replaceChild(new_permalink, permalink); } } } } })).observe(search_form, {childList: true, subtree: true, attributes: true, attributeFilter: ["value"]}); setTimeout(function() { // Deferred manual trigger for preloaded entity lookups (sometimes displayed before MutationObserver starts) for (var input of search_form.querySelectorAll("span.autocomplete > input.name.ui-autocomplete-input.lookup-performed")) { input.parentNode.appendChild(document.createTextNode(" ")); } }, 666); } // Edit list if (editform) { var radios = []; var radiosafe = []; var lastradio; var submitButton, submitClone, submitShift, inputs; var collapse = ["▼", "◀"]; var pendingXHRvote = 0; // Prevent leaving voting page with unsaved changes editform.addEventListener("input", preventLosingUnsavedChanges); // Prevent losing background voting queue editform.addEventListener("submit", function(event) { self.removeEventListener("beforeunload", preventLosingUnsavedChanges); // Allow unload on submit (volontary) event.preventDefault(); if (pendingXHRvote > 0) { if (submitShift || confirm("GOING BACKGROUND (AJAX)? (or not)\n\n" + pendingXHRvote + " background vote" + (pendingXHRvote == 1 ? " is" : "s are") + " pending,\ndo you want to add more votes to this queue?\n\n" + CONTROL_POMME.shift.label + "click on submit to bypass this confirmation next time.")) { var pendingvotes = editform.querySelectorAll("div.voteopts input[type='radio']:not([value='-2']):not([disabled])"); for (let pv = 0; pv < pendingvotes.length; pv++) { if (pendingvotes[pv].checked) { sendEvent(getParent(pendingvotes[pv], "label") || pendingvotes[pv], "dblclick"); } } } } else { this.submit(); } }, false); // Warn of destructive “Rename artist credits: Yes” artist merges, big visible red “YES” for (let rac = editform.querySelectorAll("tr.rename-artist-credits > td"), r = 0; r < rac.length; r++) { if (rac[r].textContent.match(/jah?|yes|s[íì]|oui|voor|kyllä|ναι|はい/i)) { rac[r].parentNode.classList.add(userjs + "yes"); } } // Fast approve with edit notes document.body.addEventListener("click", function(event) { if (event.target.closest("div.edit-actions") && event.target.matches("a.positive[href^='/edit/'][href*='/approve']")) { event.stopPropagation(); event.preventDefault(); var edit = event.target.closest("div.edit-list"); var editId = edit.querySelector("input[type='hidden'][name$='edit_id']"); if (edit && editId) { var editNote = edit.querySelector("textarea"); if (editNote && editNote.value.trim() !== "") { // Save edit note before approving queueVote(edit, editId.value, "-2", queueApprove); } else { queueApprove(edit, editId.value); } } } }, true); inputs = editform.querySelectorAll("div.voteopts input[type='radio']"); // Apply visible vote colours on changed votes if (voteColours) { editform.addEventListener("change", function(event) { if ( event.target !== event.currentTarget && event.target.tagName == "INPUT" && event.target.getAttribute("type") == "radio" && event.target.getAttribute("name").match(/^enter-vote\.vote\.\d+\.vote$/) ) { setTimeout(function() { var actions = getParent(this, "div", "edit-actions"); if (this.value != -2) { actions.style.setProperty("background-color", FF ? FF[this.value] : self.getComputedStyle(getParent(this, "div", "vote")).getPropertyValue("background-color")); } else { actions.style.removeProperty("background-color"); } }.bind(event.target), 0); event.stopPropagation(); } }); } for (let i = 0; i < inputs.length; i++) { if (onlySubmitTabIndexed) { inputs[i].setAttribute("tabindex", "-1"); } // remove keyboard navigation from vote radio buttons (good idea?) radios.push(inputs[i]); // Apply visible vote colours on loaded edits if (voteColours && inputs[i].checked && inputs[i].value != -2) { setTimeout(function() { sendEvent(this, "change"); }.bind(inputs[i]), 0); } // Double click to vote single edits var labinput = getParent(inputs[i], "label") || inputs[i]; labinput.setAttribute("title", texts[lang].double_click_to_vote); labinput.addEventListener("dblclick", function(event) { var edit = this.closest("div.edit-list"); var vote = (this.querySelector("input[type='radio']") || this).value; var editId = edit.querySelector("input[type='hidden'][name$='edit_id']"); if (edit && vote && editId) { queueVote(edit, editId.value, vote); } }); // Range click labinput.addEventListener("click", function(event) { var rad = this.querySelector("input[type='radio']"); if (rangeclick && (rad || this)) { if (event[CONTROL_POMME.shift.key] && lastradio && rad != lastradio && rad.value == lastradio.value) { rangeclick = false; rangeVote(event, rad.value, Math.min(radios.indexOf(rad), radios.indexOf(lastradio)), Math.max(radios.indexOf(rad), radios.indexOf(lastradio))); rangeclick = true; lastradio = null; } else { lastradio = rad; } } }, false); if (inputs[i].checked) { radiosafe.push(inputs[i]); } } // Display global vote bar when more than 1 votable edit if (radios.length > 4) { // init localised vote texts directly from MBS page for (var v = 0, votes = ["yes", "no", "abstain", "none"]; v < votes.length; v++) { texts[lang][votes[v]] = radios[v].closest("label").textContent; } if (showtop) { showtop = editform.insertBefore(shortcutsRow(), editform.firstChild.nextSibling); } if (showbottom) { showbottom = editform.insertBefore(shortcutsRow(), editform.lastChild.previousSibling); } } submitButton = editform.querySelector("div.row > span.buttons > button"); submitButton.addEventListener("click", submitShiftKey, false); submitButton.setAttribute("title", CONTROL_POMME.shift.label + "click for background voting of selected edits"); // (mass) collapse edit toggles if (collapseEdits) { for (let ed = 0; ed < edit_list.length; ed++) { if (edit_list[ed].querySelector("div.edit-description")) { var eheader = edit_list[ed].querySelector("div.edit-header"); var collexp = document.createElement("div"); var collexpa = collexp.appendChild(document.createElement("a").appendChild(document.createTextNode(collapse[0])).parentNode); collexp.style.setProperty("float", "right"); collexpa.className = userjs; if (eheader.querySelectorAll("td.vote-count > div > strong").length === 1) collexpa.classList.add("autoedit"); collexpa.style.setProperty("cursor", "pointer"); collexpa.style.setProperty("font-size", "2em"); preventDefault(collexpa, "mousedown"); collexpa.setAttribute("title", "collapse same EDITOR edits: " + CONTROL_POMME.ctrl.label.toUpperCase() + "click\n\ncollapse same TYPE edits: " + CONTROL_POMME.ctrl.label.toUpperCase() + CONTROL_POMME.shift.label.toUpperCase() + "click\n\ncollapse " + (collexpa.classList.contains("autoedit") ? "auto" : "same VOTED ") + "edits: " + CONTROL_POMME.ctrl.label.toUpperCase() + CONTROL_POMME.alt.label.toUpperCase() + "click\n\ncollapse ALL edits: " + CONTROL_POMME.shift.label.toUpperCase() + "click"); collexpa.setAttribute("rel", "collapse"); collexpa.addEventListener("click", function(event) { var expand = (this.getAttribute("rel") == "expand"); this.replaceChild(document.createTextNode(collapse[expand ? 0 : 1]), this.firstChild); this.setAttribute("title", this.getAttribute("title").replace(new RegExp(expand ? "expand" : "collapse", "g"), expand ? "collapse" : "expand")); this.setAttribute("rel", expand ? "collapse" : "expand"); ninja(event, this.closest("div.edit-list"), !expand); var editheader = getParent(this, "div", "edit-header"); var editheadersel = "div.edit-header", editor, vote; var userCSS = "div.edit-header > p.subheader > a[href*='/user/']"; var voteCSS = "div.edit-list > div.edit-actions > div.voteopts input[type='radio']:checked"; var autoedit = false; if (event[CONTROL_POMME.alt.key] && event[CONTROL_POMME.ctrl.key]) { if (this.classList.contains("autoedit")) autoedit = true; else { vote = editheader.parentNode.querySelector(voteCSS); if (vote) vote = vote.getAttribute("value"); } } else if (event[CONTROL_POMME.ctrl.key] && event[CONTROL_POMME.shift.key]) { var edittype = editheader.getAttribute("class").match(/\W([a-z-]+)$/); if (edittype) { editheadersel += "." + edittype[1]; } } else if (event[CONTROL_POMME.ctrl.key]) { if ((editor = editheader.querySelector(userCSS).getAttribute("href").match(/\/user\/(.+)$/))) { editor = editor[1]; } } if (event[CONTROL_POMME.alt.key] || event[CONTROL_POMME.ctrl.key] || event[CONTROL_POMME.shift.key]) { var others = editform.querySelectorAll(editheadersel + " a." + userjs + (autoedit ? ".autoedit" : "") + "[rel='" + (expand ? "expand" : "collapse") + "']"); for (let other = 0; other < others.length; other++) { var ovote = others[other].closest("div.edit-list").querySelector(voteCSS); if (ovote) ovote = ovote.getAttribute("value"); if ( (!editor || editor == getParent(others[other], "div", "edit-header").querySelector(userCSS).getAttribute("href").match(/\/user\/(.+)$/)[1]) && (!vote || vote == ovote) ) { sendEvent(others[other], "click"); } } } }, false); eheader.insertBefore(collexp, eheader.firstChild); } } } // If user started scrolling: scroll the page down of the height of inserted top buttons and toolbar, to avoid scroll jumps if (self.pageYOffset > 0) { var cs, offset = 0; if (submitClone && (cs = self.getComputedStyle(getParent(submitClone, "div", "row")))) { offset += parseInt(cs.getPropertyValue("height").match(/\d+/), 10); offset += parseInt(cs.getPropertyValue("margin").match(/\d+/), 10); } if (showtop.tagName && (cs = self.getComputedStyle(showtop))) { offset += parseInt(cs.getPropertyValue("height").match(/\d+/), 10); offset += parseInt(cs.getPropertyValue("margin").match(/\d+/), 10); } if (offset != 0) { self.scrollTo(0, self.pageYOffset + offset); } } } function queueApprove(edit, editId) { var xhr = new XMLHttpRequest(); xhr.editId = editId; xhr.addEventListener("load", function(event) { checkAfterQueue(this, "approving"); }); xhr.open("POST", "/edit/" + editId + "/approve", true); updateXHRstat(++pendingXHRvote); xhr.send(null); ninja(null, edit, true, "force"); } function queueVote(edit, editId, vote, callback) { var editNote = edit.querySelector("textarea"); var params = "enter-vote.vote.0.edit_id=" + editId + "&enter-vote.vote.0.vote=" + vote + "&url=" + encodeURIComponent("/edit/" + editId); if (editNote) { params += "&enter-vote.vote.0.edit_note=" + encodeURIComponent(editNote.value); } var xhr = new XMLHttpRequest(); xhr.editId = editId; xhr.addEventListener("load", function(event) { checkAfterQueue(this, "voting", callback); }); xhr.open("POST", self.location.protocol + "//" + self.location.host + "/edit/enter_votes", true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); updateXHRstat(++pendingXHRvote); xhr.send(params); ninja(null, edit, true, "force"); } function checkAfterQueue(xhr, queueType, callback) { var anotherEdit = xhr.responseText.match(/<title>\D*(\d+)\D*<\/title>/); if (anotherEdit && anotherEdit[1] != xhr.editId) { anotherEdit = anotherEdit[1]; } else { anotherEdit = false; } var notApproved = queueType == "approving" && xhr.responseText.indexOf('<a class="positive" href="/edit/' + xhr.editId + "/approve") != -1; var editEntry = document.querySelector("input[type='hidden'][name$='edit_id'][value='" + xhr.editId + "']"); if (editEntry) { editEntry = editEntry.closest("div.edit-list"); } if (xhr.status == 200 && pendingXHRvote > 0 && !anotherEdit && editEntry && !notApproved) { if (callback) callback(editEntry, xhr.editId); else removeNode(editEntry); } else { var errorMessage = "Error while " + queueType + " Edit #" + xhr.editId + " in the background.\n\n"; if (xhr.status != 200) { errorMessage += xhr.status + ": " + xhr.statusText + "\n"; } if (anotherEdit) { open("/edit/" + anotherEdit); errorMessage += "Got Edit #" + anotherEdit + " instead in return page.\n"; } if (notApproved) { errorMessage += "Edit still not approved.\n"; } if (pendingXHRvote < 1) { errorMessage += "No votes pending.\n"; } if (editEntry) { ninja(null, editEntry, false, "force"); editEntry.setAttribute("title", errorMessage); editEntry.style.setProperty("background-color", "pink"); editEntry.style.setProperty("cursor", "help"); editEntry.style.setProperty("display", "block"); } else { open("/edit/" + xhr.editId); errorMessage += "Edit block not found.\n"; alert(errorMessage); } } if (pendingXHRvote > 0) { updateXHRstat(--pendingXHRvote); } } function preventLosingUnsavedChanges(event) { switch (event.type) { case "input": editform.removeEventListener("input", preventLosingUnsavedChanges); self.addEventListener("beforeunload", preventLosingUnsavedChanges); break; case "beforeunload": var formChanged = false; // Check changed votes for (var r = 0; r < radiosafe.length; r++) { if (radiosafe[r].closest("body") && !radiosafe[r].checked) { formChanged = true; break; } } // Check typed edit notes for (var n = 0, editNotes = editform.querySelectorAll("div.edit-notes textarea.edit-note"); n < editNotes.length; n++) { if (editNotes[n].value) { formChanged = true; break; } } if (formChanged) { event.preventDefault(); return event.returnValue = "There are some unsaved changes.\nAre you sure you want to exit?"; } break; } } // From mb_NGS-MILESTONE // Show if edits are pre-NGS (NGS was released on 2011-05-16, last pre-NGS Edit #14459455, first NGS Edit #14459456) var firstNGSEdit = 14459456; // nikki work edit j2css.insertRule("div.edit-header.pre-ngs { background-image: url(data:image/gif;base64,R0lGODlhEAAQAKECAAAAAP/MAP///////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJBQAAACwAAAAAEAAQAAACIQyOF8uW2NpTcU1Q7czu8fttGTiK1YWdZISWprTCL9NGBQAh+QQJBQAAACwAAAAAEAAQAAACIIQdqXm9285TEc1QwcV1Zz19lxhmo1l2aXSqD7lKrXMWACH5BAkFAAAALAAAAAAQABAAAAIhRI4Hy5bY2lNxzVDtzO7x+20ZOIrVhZ1khJamtMIv00YFACH5BAkFAAAALAAAAAAQABAAAAIgjA2peb3bzlMRTVDDxXVnPX2XGGajWXZpdKoPuUqtcxYAOw==); }", 0); if (location.pathname.match(/\/edit\/\d+/)) { var edit = document.querySelector("div#content > div.edit-header > h1"); if (parseInt(edit.textContent.match(/\d+/), 10) < firstNGSEdit) { preNGS(edit); } } else { var edits = document.querySelectorAll("div.edit-header > h2 > a[href*='/edit/']"); for (var e = 0; e < edits.length; e++) { var edit = parseInt(edits[e].getAttribute("href").match(/\d+$/), 10); if (edit < firstNGSEdit) { preNGS(edits[e].parentNode); } } } function preNGS(editHeader) { editHeader.appendChild(document.createTextNode(" (pre‐NGS)")); editHeader.parentNode.classList.add("pre-ngs"); } function shortcutsRow() { return createTag("div", {a: {class: "edit-list"}, s: {border: border}}, [ createTag("div", {a: {class: "edit-actions c applied"}}, createTag("div", {a: {class: "voteopts buttons"}}, [ shortcut("1", texts[lang].yes), shortcut("0", texts[lang].no), shortcut("-1", texts[lang].abstain), shortcut("-2", texts[lang].none) ]) ), createTag("div", {a: {class: "edit-details"}, s: {margin: "0", textAlign: "right"}}, [ createTag("span", {a: {class: "buttons"}}, shortcut("reset-votes", texts[lang].reset_votes)), texts[lang].vote_all ]) ]); } function shortcut(vote, label) { var button = createTag("input", { a: {type: "button", value: label, class: "styled-button"}, s: {float: "none", margin: FF ? "0 3px 0 0" : "0 3px", padding: FF ? "0 2px" : "0 3px"}, e: {click: function(event) { rangeVote(event, vote); }} }); if (onlySubmitTabIndexed) { button.setAttribute("tabindex", "-1"); } // remove keyboard navigation from mass vote buttons (good idea?) return button; } function rangeVote(event, vote, min, max) { if (vote != "reset-votes") { if (event.detail === 1) { // first click for (let i = (min ? min + (FF ? 0 : 1) : 0); i < (max ? max + 1 : radios.length); i++) { // FF shift+click label NG if (radios[i].getAttribute("value") == vote && !radios[i].checked && !ninja(event, radios[i].closest("div.edit-list")) && (event[CONTROL_POMME.shift.key] || notVotedYet(radios[i]))) { sendEvent(radios[i], "click"); } } } else if (event.detail === 2) { // double click sendEvent(submitButton, "click"); } } else { for (let i = 0; i < radiosafe.length; i++) { sendEvent(radiosafe[i], "click"); } } } function notVotedYet(radiox) { return getParent(radiox, "div", "voteopts").querySelector("input[type='radio'][value='-2']").checked; } function disable(cont, dis) { var inputs = cont.querySelectorAll("input, select, textarea, button"); if (inputs.length > 0) { for (let i = 0; i < inputs.length; i++) { if (dis) { inputs[i].setAttribute("disabled", "disabled"); } else { inputs[i].removeAttribute("disabled"); } } return true; } else { return false; } } function ninja(event, edit, collapse, specificClassName) { var ninjaClassName = specificClassName ? specificClassName : "ninja"; if (typeof collapse != "undefined") { disable(edit, collapse); var allbutheader = "div.edit-actions, div.edit-notes, div.edit-details"; var editEntryContent = specificClassName ? [edit] : edit.querySelectorAll(allbutheader); for (var i = 0; i < editEntryContent.length; i++) { editEntryContent[i].style.setProperty("display", collapse ? "none" : ""); } if (collapse) edit.classList.add(userjs + ninjaClassName); else edit.classList.remove(userjs + ninjaClassName); } else return edit.classList.contains(userjs + ninjaClassName); } function updateXHRstat(nbr) { var stat = document.getElementById(userjs + "xhrstat"); if (!stat) { stat = document.body.appendChild(document.createElement("div")); stat.setAttribute("id", userjs + "xhrstat"); stat.appendChild(document.createTextNode(" ")); stat.style.setProperty("z-index", "2099"); } stat.replaceChild(document.createTextNode(nbr + " background vote" + (nbr == 1 ? "" : "s") + " pending…"), stat.firstChild); if (!editform.querySelector("div.edit-list div.edit-description")) { self.removeEventListener("beforeunload", preventLosingUnsavedChanges); // Allow reload (no more edits) self.location.reload(); } stat.style.setProperty("display", nbr > 0 ? "block" : "none"); } function submitShiftKey(event) { submitShift = event[CONTROL_POMME.shift.key]; } function preventDefault(node, eventName) { node.addEventListener(eventName, function(event) { event.preventDefault(); }, false); } function scroll_to_first_selected_options() { // Scroll multi select to make first selected option visible var multiselects = search_form.querySelectorAll("select[multiple]"); for (var s = 0; s < multiselects.length; s++) { var multiselect_size = multiselects[s].getAttribute("size"); if (multiselect_size < multiselects[s].options.length) { var first_selected_option = multiselects[s].querySelector("option[selected]"); if (first_selected_option && first_selected_option.index >= multiselect_size) { var original_scroll = {x: scrollX, y: scrollY}; first_selected_option.scrollIntoView({block: "center", behavior: "instant"}); scroll(original_scroll.x, original_scroll.y); } } } }