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;
}
}
` );
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址