Kanka Mention Previewer

Adds the ability to load a preview of any mentioned entity in a modal.

当前为 2023-06-21 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Kanka Mention Previewer
// @namespace    http://tampermonkey.net/
// @license      MIT
// @version      0.5
// @description  Adds the ability to load a preview of any mentioned entity in a modal.
// @author       Salvatos
// @match        https://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(/campaign\/(\d+)/)[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://kanka.io/en/campaign/${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.entity_notes; // todo: change to "data.posts" if removed from Kanka or next time I update the script

                    // 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) {
                            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);
                } 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: hidden;
    }

	/* 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;
    }
` );