您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
on unwrangled rels, build the proper canonical tag name with minimal typing
// ==UserScript== // @name AO3: [Wrangling] Rel Helper // @namespace https://greasyfork.org/en/users/906106-escctrl // @description on unwrangled rels, build the proper canonical tag name with minimal typing // @author escctrl // @version 1.0 // @match *://*.archiveofourown.org/tags/*/edit // @grant none // @license MIT // @require https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js // @require https://update.greasyfork.org/scripts/491896/1516188/Copy%20Text%20and%20HTML%20to%20Clipboard.js // ==/UserScript== /* global Sortable, copy2Clipboard */ 'use strict'; // utility to reduce verboseness const q = (selector, node=document) => node.querySelector(selector); const qa = (selector, node=document) => node.querySelectorAll(selector); const ins = (n, l, html) => n.insertAdjacentHTML(l, html); /*************** CONFIGURATION ***************/ // set this to either "syn" (to get a button that sets the rel as the Synonym Of and create canonicals that way) // or "copy" (to get buttons for copying the rel and chars manually into a new tag) let cfg = 'wrangleRelHelp'; let WORKFLOW = localStorage.getItem(cfg) || "syn"; // if no other script has created it yet, write out a "Userscripts" option to the main navigation if (qa('#scriptconfig').length === 0) { qa('#header nav[aria-label="Site"] li.search')[0] // insert as last li before search .insertAdjacentHTML('beforebegin', `<li class="dropdown" id="scriptconfig"> <a class="dropdown-toggle" href="/" data-toggle="dropdown" data-target="#">Userscripts</a> <ul class="menu dropdown-menu"></ul></li>`); } // then add this script's config option to navigation dropdown ins(q('#scriptconfig .dropdown-menu'), 'beforeend', `<li><a href="#" id="relhelp_workflow">Rel Helper Workflow: <span class="switch"><input type="checkbox" ${WORKFLOW === "syn" ? 'checked="checked"' : ""}><span class="slider round"></span></span> <span id="relhelp_workflow_status">${WORKFLOW === "syn" ? "SynOf" : "Copy"}</span></a></li>`); ins(q("head"), 'beforeend', `<style tyle="text/css"> #relhelp_workflow { /* The switch - the box around the slider */ .switch { position: relative; display: inline-block; width: 2em; height: 1em; vertical-align: -0.2em; } /* Hide default HTML checkbox */ .switch input { opacity: 0; width: 0; height: 0; } /* The slider */ .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; -webkit-transition: .4s; transition: .4s; } .slider:before { position: absolute; content: ""; height: 0.8em; width: 0.8em; left: 0.2em; bottom: 0.1em; background-color: white; -webkit-transition: .4s; transition: .4s; } input:checked + .slider { background-color: currentColor; } input:focus + .slider { box-shadow: 0 0 1px currentColor; } input:checked + .slider:before { -webkit-transform: translateX(0.8em); -ms-transform: translateX(0.8em); transform: translateX(0.8em); } /* Rounded sliders */ .slider.round { border-radius: 1em; } .slider.round:before { border-radius: 50%; } } .sortable .tag a { cursor: default; } #relhelp_preview { margin: 0.643em auto; }</style>`); // on either click on link or change of checkbox (depending where exactly user clicked), toggle between SynOf and Copy q('#relhelp_workflow').addEventListener('click', (e)=>{ e.preventDefault(); if (e.currentTarget.tagName==="A") { WORKFLOW = WORKFLOW === "syn" ? "copy" : "syn"; // toggle localStorage.setItem(cfg, WORKFLOW); q('input', e.currentTarget).checked = WORKFLOW === "syn" ? true : false; q('#relhelp_workflow_status', e.currentTarget).innerText = WORKFLOW === "syn" ? "SynOf" : "Copy"; location.reload(); } }); /************* CONFIGURATION END *************/ // function to get current list of added tags based on autocomplete input fields e.g. get_added(inputs.char) const get_added = (me) => qa('.added.tag', me.parentElement.parentElement); const inputs = { tag: q('#tag_name'), syn: q('#tag_syn_string_autocomplete'), char: q('#tag_character_string_autocomplete') }; if (q('#tag_character_string') === null) return; // works only on relationships if (q('#tag_canonical').checked) return; // works only if not canonical if (get_added(inputs.syn).length > 0) return; // works only if not a synonym already const chars_ul = inputs.char.parentElement.parentElement; const individuals = inputs.tag.value.split(/\/|&|\band\b/g); let fillCount = 0; let newRel = ""; // make the characters <ul> sortable let sortable = new Sortable(chars_ul, { draggable: ".tag", // instead of filtering .input, we make only .tag draggable to avoid problems (autocomplete selection by mouse, dragging without clicking outside the <input>) onEnd: updatePreview // reorder in preview immediately }); chars_ul.classList.add('sortable'); // add some buttons (automatically select the / or & based on tagname) ins(chars_ul, 'afterend', `<div id="relhelp_btns"> <button type="button" id="relhelp_addchars" title="search for the next character from the rel in the autocomplete">Add Char</button> <button type="button" id="relhelp_settype" title="switch between / and & rels" data-relhelptype="${inputs.tag.value.indexOf('/') !== -1 ? "/" : " & "}">${inputs.tag.value.indexOf('/') !== -1 ? "&" : "/"}</button> <button type="button" id="relhelp_remdisambig" title="switch between keeping and removing fandom disambigs" data-relhelpdisambig="keep">Remove Disambigs</button> · ${ WORKFLOW === "syn" ? `<button type="button" id="relhelp_setsyn" title="set the proposed tag in the Synonym Of field">Set Syn Of</button>` : `<button type="button" id="relhelp_copy" title="copy the proposed tag to clipboard">Copy Rel</button> <button type="button" id="relhelp_copychars" title="copy the selected chars to clipboard">Copy Chars</button>` }<br /> <input type="text" id="relhelp_preview" title="preview of the proposed tag you're building" style="max-width: 30em;" /> <span id="relhelp_error"></span> </div>`); chars_ul.parentElement.addEventListener('click', (e) => { if (e.target.id === "relhelp_addchars") { // with each click fill autocomplete search with the next char inputs.char.value = individuals[fillCount].trim(); inputs.char.focus(); fillCount = (fillCount+1 === individuals.length) ? 0 : fillCount+1; } else if (e.target.id === "relhelp_settype") { // with each click toggle between / and & rels e.target.dataset.relhelptype = e.target.dataset.relhelptype === "/" ? " & " : "/"; e.target.innerText = (e.target.dataset.relhelptype === "/" ? "&" : "/"); updatePreview(); } else if (e.target.id === "relhelp_remdisambig") { // with each click toggle between keeping and removing the disambigs e.target.dataset.relhelpdisambig = e.target.dataset.relhelpdisambig === "keep" ? "rem" : "keep"; e.target.innerText = (e.target.dataset.relhelpdisambig === "keep" ? "Remove" : "Keep") + " Disambigs"; updatePreview(); } else if (e.target.id === "relhelp_copy") { // copy final rel to clipboard copy2Clipboard(e, "txt", newRel); } else if (e.target.id === "relhelp_copychars") { // copy CSV of chars to clipboard, and remove the chars in one go let allchars = Array.from(get_added(inputs.char)); // grab all current chars allchars = allchars.map((el) => el.innerText.slice(0,-1).trim()); // remove the x delete button copy2Clipboard(e, "txt", allchars.join(',')); for (let char of get_added(inputs.char)) { q('.delete a', char).click(); } } else if (e.target.id === "relhelp_setsyn") { // populate the Synonym Of field inputs.syn.focus(); inputs.syn.value = newRel; inputs.syn.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 13, key: "Enter" })); } }); q('#relhelp_preview').addEventListener('input', (e) => { // when user makes manual changes newRel = e.target.value; checkTagValidity(); }); // update preview when a char gets added/deleted const observer = new MutationObserver(function(mutList, obs) { for (const mut of mutList) { // check if the added/removed node is an .added.tag LI for (const node of mut.addedNodes) { if (node.classList.contains('added')) { // make content of the LI into a link (don't need to stop observer bc it's not listening to subtree) let oldNode = node.childNodes[0]; let newNode = document.createElement("a"); newNode.innerText = oldNode.textContent; newNode.href = encodeURI("https://archiveofourown.org/tags/" + oldNode.textContent.trim().replace('/', '*s*').replace('&', '*a*').replace('#', '*h*').replace('.', '*d*').replace('?', '*q*') + "/wrangle?show=relationships&status=canonical"); newNode.target = "_blank"; node.replaceChild(newNode, oldNode); updatePreview(); } } for (const node of mut.removedNodes) { if (node.classList.contains('added')) updatePreview(); } } }); // listening to as few changes as possible: only direct children of UL observer.observe(chars_ul, { attributes: false, childList: true, subtree: false }); function updatePreview() { let allchars = Array.from(get_added(inputs.char)); // grab all current chars allchars = allchars.map((el) => { el = el.innerText.slice(0,-1).trim(); // remove the x delete button // remove fandom disambigs aka the last parenthesis if asked - doesn't remove (s) in Character(s) or (ren) in Child(ren) if (q('#relhelp_remdisambig').dataset.relhelpdisambig === "rem" && el.indexOf("(") !== -1 && el.slice(-3) !== "(s)" && el.slice(-5) !== "(ren)") { el = el.slice(0, el.lastIndexOf("(")).trim(); } return el; }); newRel = allchars.join(q('#relhelp_settype').dataset.relhelptype); // join with the rel type we're supposed to use q('#relhelp_preview').value = newRel; // write out into preview field checkTagValidity(); } function checkTagValidity() { // warn if new tag name too long or same as this tag let preview = q('#relhelp_preview'); let error = ""; if (newRel.length > 150) error = "proposed canonical is too long"; if (newRel === inputs.tag.value) error = "proposed canonical is same as this tag"; if (error === "") { preview.classList.remove('error'); preview.title = "preview of the proposed tag you're building"; q('#relhelp_error').innerText = ""; qa('#relhelp_copy, #relhelp_copychars, #relhelp_setsyn').forEach((b) => { b.style.display = "inline-block"; }); // show the buttons } else { preview.classList.add('error'); preview.title = error; q('#relhelp_error').innerText = error; qa('#relhelp_copy, #relhelp_copychars, #relhelp_setsyn').forEach((b) => { b.style.display = "none"; }); // hide the buttons } }