Kanka Marketplace Toolbar for Summernote

Adds buttons to Summernote to insert HTML elements or classes required by Marketplace themes.

当前为 2021-12-05 提交的版本,查看 最新版本

// ==UserScript==
// @name         Kanka Marketplace Toolbar for Summernote
// @namespace    http://tampermonkey.net/
// @version      4
// @description  Adds buttons to Summernote to insert HTML elements or classes required by Marketplace themes.
// @author       Salvatos
// @match        https://kanka.io/*
// @icon         https://www.google.com/s2/favicons?domain=kanka.io
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

GM_addStyle(`
.scrollable-menu {
	height: auto;
	max-height: 75vh;
    min-width: 200px !important;
	max-width: 30vw;
	overflow-x: hidden;
}
.scrollable-menu li a {
	padding: 0 5px;
	font-family: "Roboto", monospace;
	font-size: 13px;
}
.scrollable-menu li.class-group a {
	font-weight: bold;
	font-size: 13px;
	color: var(--box-header-text);
}
.class-group {
	text-align: center;
	background: var(--body-background);
	border-radius: 2px;
	border: 1px solid #090e572b;
}
.note-editor.note-frame .note-status-output {
    padding: 0;
    height: auto;
    background-color: #f4f4f4;
    transition: height 2s;
}
.note-editor.note-frame .note-status-output .fadeout {
    display: block;
    padding: 5px 10px;
    width: 100%;
    background-color: var(--code-background);
    text-align: center
}
`);

// Wait for Summernote to initialize
$('#entry').on('summernote.init', function() {

    // Prepare to check for supported themes in the campaign
    var rootFlags = getComputedStyle(document.documentElement);

	/* HTML INSERTER ARRAYS */
    // Define our supported code snippets
    let snippetArray = [];

    // Figure Box and Floats by Ornstein
    if (rootFlags.getPropertyValue('--summernote-insert-figure-box')) {
        snippetArray.push(
            {"listing": "Figure Box (base)", "code": '<div class="figure">Insert image and caption here</div>'}
        );
    }

    // Responsive Image Gallery by Salvatos
    if (rootFlags.getPropertyValue('--summernote-insert-autogallery')) {
        snippetArray.push(
            {"listing": "Responsive Image Gallery", "code": '<div class="autogallery">Insert gallery images here</div>'}
        );
    }

    // Simple Tooltips by KeepOnScrollin
    if (rootFlags.getPropertyValue('--summernote-insert-simple-tooltip')) {
        snippetArray.push(
            {"listing": "Simple Tooltips (default &ndash; top)", "code": '<span class="simple-tooltip">Tooltip trigger<span class="simple-tooltip-text">Tooltip content</span></span>'}
        );
    }

    // Build list items for our supported plugin snippets
    var themeList = "";
    for (let i = 0; i < snippetArray.length; i++) {
        themeList += '<li aria-label="' + snippetArray[i]["listing"] + '"><a href="#">' + snippetArray[i]["listing"] + '</a></li>';
    }

    /* CLASS INSERTER ARRAYS */
    // Define and group supported classes
    let classArray = [];
    classArray.push(
        {"name": "delimiter:Bootstrap", "hint": "#"},
        {"name": "pull-left", "hint": "Floats the element to the left"},
        {"name": "pull-right", "hint": "Floats the element to the right"},
        {"name": "table-hover", "hint": "Highlights table rows on hover"},
        {"name": "table-striped", "hint": "Alternating background colors on table rows"},
        {"name": "text-center", "hint": "Centers text"}
    );
    // .boxquote by Olessan
    if (rootFlags.getPropertyValue('--summernote-insert-olessan-boxquote')) {
        classArray.push(
            {"name": "delimiter:.boxquote", "hint": "https://marketplace.kanka.io/plugins/6cfb03a0-9f80-4743-9743-bd4c2b05bea2"},
            {"name": "boxquote", "hint": "Applies different styling to a boxquote element"}
        );
    }
    // Context-Aware Classes by Salvatos
    if (rootFlags.getPropertyValue('--summernote-insert-context-aware-classes')) {
        classArray.push(
            {"name": "delimiter:Context-Aware Classes", "hint": "https://marketplace.kanka.io/plugins/31910211-f33b-47b4-8db2-47dfa8dc959e"},
            {"name": "dashboard-only", "hint": "Only displays the element on a dashboard"},
            {"name": "no-dashboard", "hint": "Hides the element from dashboards"},
            {"name": "editor-only", "hint": "Only displays the element in Summernote"},
            {"name": "mobile-only", "hint": "Only displays the element at small resolutions"},
            {"name": "no-mobile", "hint": "Only displays the element at high resolutions"},
            {"name": "marker-only", "hint": "Only displays the element in map markers"},
            {"name": "marker-details-only", "hint": "Only displays the element in a map's sidebar"},
            {"name": "no-map", "hint": "Hides the elements from maps and map widgets"},
            {"name": "print-only", "hint": "Only displays the element in printed media"},
            {"name": "no-print", "hint": "Removes the element from printed media"},
            {"name": "tooltip-only", "hint": "Only displays the element in tooltips"}
        );
    }
    // Figure Box and Floats by Ornstein
    if (rootFlags.getPropertyValue('--summernote-insert-figure-box')) {
        classArray.push(
            {"name": "delimiter:Figure Box and Floats", "hint": "https://marketplace.kanka.io/plugins/a0a51dda-6a69-4e05-96a9-5fda05f160e5"},
            {"name": "l", "hint": "Left float"},
            {"name": "r", "hint": "Right float"},
            {"name": "clear", "hint": "Reset float (break line)"}
        );
    }
    // Handwritten Journal by Ornstein
    if (rootFlags.getPropertyValue('--summernote-insert-handwritten-journal')) {
        classArray.push(
            {"name": "delimiter:Handwritten Journal", "hint": "https://marketplace.kanka.io/plugins/bce5fcec-b279-4b44-ac68-273e65d30ab6"},
            {"name": "hand1", "hint": "Kalam font"},
            {"name": "hand2", "hint": "Sacramento font"},
            {"name": "hand3", "hint": "Dancing Script font"},
            {"name": "hand4", "hint": "Fondamento font"},
            {"name": "hand5", "hint": "Homemade Apple font"},
            {"name": "hand6", "hint": "Shadows Into Light font"},
            {"name": "lback", "hint": "Parchment background"},
            {"name": "letter", "hint": "Prerequisite for other classes"},
            {"name": "sig", "hint": "Double font size"}
        );
    }
    // Redacted Text by Salvatos
    if (rootFlags.getPropertyValue('--summernote-insert-salv-redacted')) {
        classArray.push(
            {"name": "delimiter:Redacted Text", "hint": "https://marketplace.kanka.io/plugins/810f9079-6a14-4267-a9f1-a7a142410116"},
            {"name": "salv-redacted", "hint": "Makes text black on black with a &quot;redacted&quot; annotation"}
        );
    }
    // Simple Tooltips by KeepOnScrollin
    if (rootFlags.getPropertyValue('--summernote-insert-simple-tooltip')) {
         classArray.push(
            {"name": "delimiter:Simple Tooltips", "hint": "https://marketplace.kanka.io/plugins/1bb9b54c-46ef-4bd9-b6d3-8e88bcb99e91"},
            {"name": "top", "hint": "Display tooltip above trigger"},
            {"name": "bottom", "hint": "Display tooltip below trigger"},
            {"name": "left", "hint": "Display tooltip left of trigger"},
            {"name": "right", "hint": "Display tooltip right of trigger"}
        );
    }
    // Tip Box by Critter
    if (rootFlags.getPropertyValue('--summernote-insert-tip-box')) {
        classArray.push(
            {"name": "delimiter:Tip Box", "hint": "https://marketplace.kanka.io/plugins/c16e9e7f-8e82-4836-84eb-cdcbf66b8bdd"},
            {"name": "tipbox-big", "hint": "Creates a wide, bordered, right-floating container"},
            {"name": "tipbox-small", "hint": "Creates a narrow, bordered, right-floating container"}
        );
    }

    // Build list items for our supported plugin classes
    var classList = "";
    for (let i = 0; i < classArray.length; i++) {
        if (classArray[i]["name"].match(/delimiter:/g)) {
            classList += '<li class="class-group" aria-label="' + classArray[i]["name"].split(":")[1] + '">';
            if (classArray[i]["name"].split(":")[1] == "Bootstrap") {
                classList += '<a title="Provided with Kanka" href="#">';
            }
            else {
                classList += '<a title="Open documentation in a new tab" target="_blank" href="' + classArray[i]["hint"] + '">';
            }
            classList += classArray[i]["name"].split(":")[1] + '</a></li>';
        }
        else {
            classList += '<li aria-label="' + classArray[i]["name"] + '" title="' + classArray[i]["hint"] + '"><a href="#">' + classArray[i]["name"] + '</a></li>';
        }
    }

    // Locate toolbar and insert our dropdown buttons
    const toolbar = document.getElementsByClassName('note-toolbar')[0];

    var buttons = `<div class="note-btn-group btn-group note-style">`;
    var snippetsButton = `
	<div class="note-btn-group btn-group">
		<button type="button" class="note-btn btn btn-default btn-sm dropdown-toggle note-codeview-keep" tabindex="-1" data-toggle="dropdown" title="Marketplace theme HTML elements" aria-expanded="false">
			<i class="fas fa-puzzle-piece"></i> <span class="note-icon-caret"></span>
		</button>
		<ul class="note-dropdown-menu dropdown-menu dropdown-snippets" aria-label="Insert HTML elements">
			` + themeList + `
		</ul>
	</div>`;
	var classesButton = `
	<div class="note-btn-group btn-group">
		<button type="button" class="note-btn btn btn-default btn-sm dropdown-toggle note-codeview-keep" tabindex="-1" data-toggle="dropdown" title="Marketplace theme classes" aria-expanded="false">
			<i class="fa fa-css3"></i> <span class="note-icon-caret"></span>
		</button>
        <ul class="note-dropdown-menu dropdown-menu dropdown-classes scrollable-menu" aria-label="Toggle CSS classes">
			` + classList + `
		</ul>
	</div>`;
    buttons += snippetsButton + classesButton + `</div>`;
    toolbar.insertAdjacentHTML("beforeend", buttons);

	/* ADD EVENTS FOR HTML INSERTER */
    // Grab our completed dropdown
    const snippetsDropdown = document.getElementsByClassName('dropdown-snippets')[0];

    // Make sure we have at least one supported theme enabled
    if (snippetsDropdown.children[0]) {
        // Add click events to editor
        for (let i = 0; i < snippetsDropdown.children.length; i++) {
            snippetsButton = snippetsDropdown.children[i];

            snippetsButton.addEventListener('click', ()=>{
                // Code editor, not supported by Summernote functions so we're making our own
                if ($('#entry + div').hasClass('codeview')) {
                    const codeEditor = $('#entry + div').find('.note-codable');
                    var cursorPos = codeEditor.prop('selectionStart');
                    var editorValue = codeEditor.val();
                    var textBefore = editorValue.substring(0, cursorPos);
                    var textAfter = editorValue.substring(cursorPos, editorValue.length);
                    var newPos = cursorPos + snippetArray[i]["code"].length + 1;
                    codeEditor.val(textBefore + '\n' + snippetArray[i]["code"] + textAfter);

                    // Return focus to textarea and select newly inserted string to make it clear to the user
                    codeEditor[0].focus();
                    codeEditor[0].setSelectionRange(cursorPos, newPos);

                    // Update Summernote’s hidden textarea in case of immediate saving
                    $('#entry').val(codeEditor.val());
                }
                // Visual editor, API has us covered here
                else {
                    var insertNode = $.parseHTML(snippetArray[i]["code"])[0];
                    $('#entry').summernote('insertNode', insertNode);
                }
            });
        }
    }
    else {
        snippetsDropdown.insertAdjacentHTML("beforeend", "<li><a href='#'><em>No supported theme found</em></a></li>");
    }

    /* ADD EVENTS FOR CLASS INSERTER */
    // Grab our completed dropdown
    const classesDropdown = $('#entry + div .dropdown-classes');

	// Add click events to editor
	for (let i = 0; i < classesDropdown.find('li').length; i++) {
		classesButton = classesDropdown.find('li')[i];

		classesButton.addEventListener('click', (e)=>{
			//e.preventDefault();
			// Code editor, not supported by Summernote functions so we're making our own?
			if ($('#entry + div').hasClass('codeview')) {
                // Grab selection from editor
                const codeEditor = document.getElementsByClassName('note-codable')[0];
                var selectedText = codeEditor.value.substring(codeEditor.selectionStart, codeEditor.selectionEnd);
                var initialStart = codeEditor.selectionStart;
                var initialEnd = codeEditor.selectionEnd;
                var initialLength = codeEditor.value.length;

                // Make (reasonably) sure we are holding a single opening tag
                if (selectedText.slice(0, 1) == '<' && selectedText.slice(-1) == '>' && selectedText.slice(1, 2) !== '/' && selectedText.match(/</g).length == 1 && selectedText.match(/>/g).length == 1) {
                    // Insert flag to locate this element further on
                    var tagEnd = (selectedText.slice(-2) == '/>') ? '/>' : '>';
                    var flaggedTag = selectedText.slice(0, -tagEnd.length) + ' data-class-inserter="target"' + tagEnd;

                    // Replace selection with modified tag
                    codeEditor.focus();
                    codeEditor.setRangeText(flaggedTag, codeEditor.selectionStart, codeEditor.selectionEnd, 'select');

                    // Create node from textarea
                    var inputNode = $.parseHTML($('#entry + div').find('.note-codable').val());

                    // Toggle class on target node
                    var targetNode = $(inputNode).closest('[data-class-inserter="target"]');
                    if (targetNode.length == 0) { // The above only finds first-level elements, and the below only finds sub-elements. Go figure.
                        targetNode = $(inputNode).find('[data-class-inserter="target"]');
                    }
                    if (targetNode.length == 0) { // Worst case scenario
                        alert("Target element was lost along the way. Please report the issue and include the source HTML.");
                    }
                    $(targetNode).toggleClass(classArray[i]["name"]);

                    // Clean up and remove class attribute if no class remains after toggle
                    if ($(targetNode).attr('class') == "") {
                     $(targetNode).removeAttr('class');
                    }

                    // Remove flag
                    $(targetNode).removeAttr('data-class-inserter');

                    // Return to HTML (wrapper needed) and pass to textarea
                    $('#entry + div').find('.note-codable').val($('<div></div>').append($(inputNode)).html());

                    // Apply changes to master copy in case of immediate save
                    $('#entry').val($('#entry + div').find('.note-codable').val());

                    // Return focus to textarea and select newly inserted string to make it clear to the user
                    var lengthDiff = $('#entry + div').find('.note-codable').val().length - initialLength;
                    var newEnd = initialEnd + lengthDiff;/*
                    console.log(initialStart);
                    console.log(initialEnd);
                    console.log(initialLength);
                    console.log(lengthDiff);
                    console.log(newEnd);*/
                    codeEditor.setSelectionRange(initialStart, newEnd);
                }
                else {
                    alert("To insert classes in Code View, you must first select the opening tag of the element (e.g. <table>).");
                }

			}
			// Visual editor, API provides cursor position
			else {
				const range = $('#entry').summernote('editor.getLastRange');
				var targetNode = range.sc.parentNode;

                // Any position outside the editor should be rejected
				if (!$('.note-editing-area').has(targetNode).length) {
					alert("Cursor position could not be found in the editor. Click the target element in the editor and try again. If the issue persists, your text may not be wrapped in any HTML element.");
				}
				else {
                    // Add class
					$(targetNode).toggleClass(classArray[i]["name"]);
                    // Apply changes to master copy in case of immediate save
                    $('#entry').val($('#entry + div').find('.note-editable').html());

                    // Print status message
                    var status = '<em class="fadeout">Class "' + classArray[i]["name"] +'"';
                    status += ($(targetNode).hasClass(classArray[i]["name"])) ? ' added to ' : ' removed from ';
                    status += '<span style="font-variant: all-petite-caps;">' + $(targetNode).prop('nodeName') + '</span> element.</em>';
                    $('.note-status-output').html(status);
                    // Remove message
                    $('.note-status-output > .fadeout').fadeIn(500).delay(10000).fadeOut(400);
				}
			}
		});
	}
});