您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds the ability to load a preview of any mentioned entity in a modal.
// ==UserScript== // @name Kanka Mention Previewer // @namespace http://tampermonkey.net/ // @license MIT // @version 0.8 // @description Adds the ability to load a preview of any mentioned entity in a modal. // @author Salvatos // @match https://app.kanka.io/* // @icon https://www.google.com/s2/favicons?domain=kanka.io // @grant GM_addStyle // @connect kanka.io // ==/UserScript== // Get campaign ID from URL const campaignID = window.location.href.match(/w\/(.+)/)[1].split("/")[0]; // Create empty dialog to be populated const MentionPreviewer = ` <dialog id="MentionPreviewer" class="box-entity-entry"> <div class="box-header with-border"> <h1 class="box-title"></h1> <div class="box-tools"> <form method="dialog"> <button class="bg-gray">Close</button> </form> </div> </div> <section id="MentionPreviewer-content"> <article id="MentionPreviewer-entry" class="box-body entity-content"></article> <aside id="MentionPreviewer-attributes"></aside> </section> </dialog> `; document.querySelector('body').insertAdjacentHTML("afterbegin", MentionPreviewer); let PreviewDialog = document.getElementById('MentionPreviewer'); // Add listener to close the dialog when clicking outside of it PreviewDialog.addEventListener("click", function (event) { if (event.target === PreviewDialog) { PreviewDialog.close(); } }); // Process each mention at page load, excluding entity tags in headers $( "a[data-toggle='tooltip-ajax']:not([data-tag-slug])" ).each(runThroughMentions); // On pages with child lists, e.g. organisation members, we need to watch for the list to be loaded in if (document.getElementById("datagrid-parent")) { function setObserver() { // Set and run the observer until datagrid is loaded in let observer = new MutationObserver(function(mutations) { if ($('#datagrid-parent tr').length) { observer.disconnect(); // Process each mention in the table body $( "#datagrid-parent tbody a[data-toggle='tooltip-ajax']" ).each(runThroughMentions); // Restart observer if more pages exist if ($('#datagrid-parent .pagination').length) { setObserver(); } } }); observer.observe(document.getElementById("datagrid-parent"), {attributes: false, childList: true, characterData: false, subtree:false}); } // Run at page load setObserver(); } function runThroughMentions () { let $this = $(this); // optimization (don’t create a new $(object) every time) var apiURL = `https://app.kanka.io/w/${campaignID}/entities/` + $this.attr("data-id") + `/json-export`; var mentionURL = $this.attr("href"); var mentionTags = $this.attr("data-entity-tags"); /* // Clone the mention to destroy its native event listeners var thisMention = $this.clone().insertAfter( $this ); $this.remove(); // Prepare the dropdown of options let dropdown = ` <ul class="MentionPreviewer-options dropdown-menu dropdown-menu-right" role="menu">`+ <li> <a href="${mentionURL}" title=""> <i class="fa-solid fa-square-arrow-up-right"></i> Open entity </a> </li> <li> <a href="${mentionURL}" target="_blank" title=""> <i class="fa-solid fa-arrow-up-right-from-square"></i> Open entity in new tab </a> </li> <li> <a class="MentionPreviewer-loader" title=""> <i class="fa-solid fa-eye"></i> View entry in modal </a> </li> </ul> */ // Prepare the loader icon const loader = `<i class="fa-solid fa-magnifying-glass-arrow-right MentionPreviewer-loader" title="View entry in modal"></i>`; // In entity grid view, wrap the mention and loader together as one child of the flexbox if ($this.parent('.entities-grid')) { $this.wrap( "<span></span>" ); } // Attach loader $this.after( $( loader ) ); // Add event listener to each mention loader $this.next(".MentionPreviewer-loader").click(function(event) { //event.preventDefault(); // Add or update target entity tags for custom styling PreviewDialog.setAttribute("data-entity-tags", mentionTags); // Remove title, header image and attributes if present document.querySelector('#MentionPreviewer .box-title').innerHTML = ""; document.querySelector('#MentionPreviewer .box-header').classList.remove("has-header"); document.querySelector('#MentionPreviewer .box-header').style.backgroundImage = ""; document.getElementById('MentionPreviewer-attributes').innerHTML = ""; // Display loading message document.getElementById('MentionPreviewer-entry').innerHTML = "<em>Loading entry...</em>"; // Open modal if closed if (!PreviewDialog.open) { PreviewDialog.showModal(); } // Request JSON for the target entity var xhr = new XMLHttpRequest(); xhr.open("GET", apiURL, true); xhr.responseType = 'json'; xhr.onload = function (e) { if (xhr.readyState === 4) { if (xhr.status === 200) { let entityName = xhr.response.data.name, entityPortrait = xhr.response.data.image_full, entityHeader = xhr.response.data.header_full, entityEntryParsed = xhr.response.data.entry_parsed, entityRawAttributes = xhr.response.data.attributes, entityNotes = xhr.response.data.posts; // Prepend portrait to pinned attributes, if present var entityPinnedAttributes = (entityPortrait) ? `<a href="${entityPortrait}" class="portrait"><img src="${entityPortrait}" /></a>` : ""; // Format pinned attributes for (let i = 0; i < entityRawAttributes.length; i++) { if (entityRawAttributes[i].is_star === true) { // For range attributes, strip the range syntax out of the name if (entityRawAttributes[i].name.match(/\[range:/)) { entityRawAttributes[i].name = entityRawAttributes[i].name.replace(/\[range:.*\]/, ''); } entityPinnedAttributes += `<dt>${entityRawAttributes[i].name}</dt><dd>${entityRawAttributes[i].parsed}</dd>`; } } /* I could get pinned relations too, but I only have the ID so I would need another xhr for each to get their names... Seems excessive. // Prepare pinned relations for (let i = 0; i < entityRawRelations.length; i++) { if (entityRawRelations[i].is_star === true) { entityPinnedAttributes += `<dt>${entityRawRelations[i].relation}</dt><dd>${entityRawRelations[i].target_id}...</dd>`; } } */ // Links and files could also be considered // Wrap attributes block if not empty entityPinnedAttributes = (entityPinnedAttributes == "") ? "" : `<dl>${entityPinnedAttributes}</dl>`; // Prepare posts let postsParsed = ""; for (let i = 0; i < entityNotes.length; i++) { postsParsed += ` <hr /> <div class="entity-note"> <h2 class="box-title">${entityNotes[i].name}</h2> <div class="entity-content entity-note-body">${entityNotes[i].entry_parsed}</div> </div>`; } let entityEntries = (entityEntryParsed) ? entityEntryParsed + postsParsed : postsParsed; // Replace dialog title with current mention document.querySelector('#MentionPreviewer .box-title').innerHTML = `<a href="${mentionURL}" class="entity-name">${entityName}</a>`; // Replace dialog title background with header image if present PreviewDialog.style.setProperty('--header-image', "url("+entityHeader+")"); //document.querySelector('#MentionPreviewer .box-header .box-title').style.backgroundImage = (entityHeader.length > 0) ? `url("${entityHeader}")` : ""; if (entityHeader.length > 0) { document.querySelector('#MentionPreviewer .box-header').classList.add("has-header"); } // Replace dialog content with current entry document.getElementById('MentionPreviewer-entry').innerHTML = (entityEntries) ? entityEntries : ""; // Replace dialog content with current attributes document.getElementById('MentionPreviewer-attributes').innerHTML = `${entityPinnedAttributes}`; // If attributes are shown, enable 2-column layout document.getElementById('MentionPreviewer-content').className = (entityPinnedAttributes.length > 0 || entityPortrait) ? "with-attributes" : ""; // Run through the entry’s mentions to add loaders inside the modal $( "#MentionPreviewer" ).find( "a[data-toggle='tooltip-ajax']" ).each(runThroughMentions); // Request tooltips for the new mentions $('#MentionPreviewer [data-toggle="tooltip"]').tooltip(); unsafeWindow.ajaxTooltip(); // (unsafeWindow is how you use window. functions in scripts where @grant is not set to 'none' } else { console.error(xhr.statusText); } } }; xhr.onerror = function (e) { console.error(xhr.statusText); }; xhr.send(null); // In nested entity lists, prevent the trigger from drilling down to child entities event.stopPropagation(); }); } GM_addStyle ( ` /* Keep loader on same line as transcluded entity title */ .mention-field-entry .entity-mention-name { display: inline-block; } .MentionPreviewer-loader { font-size: 11px; margin-left: 3px; cursor: pointer; } .entities-grid .MentionPreviewer-loader { float: right; margin-right: 5px; margin-top: -15px; transform: translateY(15px); /* bit of an odd trick to keep it in the corner of the tile without screwing with the flexbox’s proportions */ } /* Modal */ #MentionPreviewer[open] ~ #app { filter: grayscale(0.3) blur(1px); } #MentionPreviewer { width: auto; max-width: 80vw; max-height: 90vh; background-color: #222; color: var(--theme-main-text); padding: 0; border-radius: 10px; border: 2px solid var(--theme-border); overflow: visible; /* For mention tooltips; hopefully this doesn’t break other things */ } /* header */ #MentionPreviewer .box-header { display: grid; grid-template-columns: 1fr auto; justify-items: center; align-items: center; height: 80px; background: var(--content-wrapper-background); /* To ensure a default background on this without overriding the value for the rest of the campaign */ } #MentionPreviewer .box-header.has-header { background-image: var(--header-image); background-size: 100%; background-position-y: center; height: 120px; } .box-header::before, .box-header::after { display: none; } /* title */ #MentionPreviewer .box-title .entity-name { text-align: center; font-size: 25px; } #MentionPreviewer .has-header .box-title .entity-name { font-size: 30px; color: #f9f9f9; text-shadow: #111 2px 2px 5px; background-image: radial-gradient(#1e1e1e6b, #60605c00, #56595600); padding: 5px 25px; } /* close button */ #MentionPreviewer .box-header > .box-tools { align-self: start; margin-top: 5px; } #MentionPreviewer button { float: right; margin: 5px; padding: 0 5px; border-radius: 5px; height: min-content; } #MentionPreviewer button:hover { background: #eee !important; color: #444 !important; } /* main content */ #MentionPreviewer-content { padding: 5px 15px; background: var(--box-background); max-height: calc(90vh - 80px); overflow: auto; } .has-header + #MentionPreviewer-content { max-height: calc(90vh - 150px); } #MentionPreviewer-content.with-attributes { display: grid; grid-auto-flow: column; grid-template-columns: auto 200px; justify-content: space-between; grid-gap: 10px; } /* posts */ h2.box-title { text-decoration: underline; } /* attributes */ #MentionPreviewer-content.with-attributes .portrait img { width: calc(100% + 10px); margin-bottom: 15px; } .with-attributes #MentionPreviewer-attributes { border-left: 2px solid gray; padding: 10px; } #MentionPreviewer-attributes dd { margin-bottom: 5px; } @media print { i.MentionPreviewer-loader { display: none; } } ` );