Add log signature

Automatically adds your signature when creating logs on geocaching.com. With configurable content and desired log types.

// ==UserScript==
// @name                Add log signature
// @version				0.0.1
// @namespace	        https://github.com/todeit02/geocaching-com_add_log_signature
// @description	        Automatically adds your signature when creating logs on geocaching.com. With configurable content and desired log types.
// @grant				GM_registerMenuCommand
// @require             https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant               GM_getValue
// @grant               GM_setValue
// @grant				GM_notification
// @grant               GM.getValue
// @grant               GM.setValue
// @grant				GM.notification
// @include				/^https:\/\/www\.geocaching\.com\/seek\/log\.aspx.*$/
// @include				/^https:\/\/www\.geocaching\.com\/play\/geocache\/.+\/log$/
// @run-at              document-end
// @connect				*
// ==/UserScript==


"use strict";

const OLD_LOG_PAGE_PATHNAME = "/seek/log.aspx"
const NEW_LOG_PAGE_PATHNAME_REGEX = /\/play\/geocache\/.+\/log/;

const LOG_TYPES = [
    {
        id: 2,
        name: "Found it"
    },
    {
        id: 3,
        name: "Didn't find it"
    },
    {
        id: 4,
        name: "Write note"
    },
    {
        id: 7,
        name: "Needs Archived"
    },
    {
        id: 9,
        name: "Will attend"
    },
    {
        id: 45,
        name: "Needs Maintenance"
    }
];
const LOG_TYPE_CONFIG_FIELD_PREFIX = "logType";

(async () =>
{
    const isNewLogPage = NEW_LOG_PAGE_PATHNAME_REGEX.test(window.location.pathname);
    const isOldLogPage = (window.location.pathname === OLD_LOG_PAGE_PATHNAME);
    if(!isNewLogPage && !isOldLogPage) throw new Error("Unrecognized log page.");

    let logInputElement = null;
    let logTypeSelect = null;
    if(isNewLogPage)
    {
        logInputElement = document.querySelector("#LogText");
        logTypeSelect = await waitForNewLogTypeSelect();
    }
    else if(isOldLogPage)
    {
        logInputElement = document.querySelector("#ctl00_ContentBody_LogBookPanel1_uxLogInfo");
        logTypeSelect = document.querySelector("#ctl00_ContentBody_LogBookPanel1_ddLogType");
    }

	const configPanel = await initializeConfigurability(LOG_TYPES);    
    
    GM_registerMenuCommand("Configuration", () =>
    {
        configPanel.open();
    });

	if(!configPanel.get("configHasBeenOpen"))
	{
		showMissingConfigurationNotification(configPanel);
		return;
	}

	const signature = configPanel.get("signature");
	const onlyInNewLogs = configPanel.get("onlyInNewLogs");

    const logInput = LogInput(logInputElement, signature);

    logTypeSelect.addEventListener("change", handleLogTypeChange);

    const selectedLogTypeId = parseInt(logTypeSelect.value);
    if(isNaN(selectedLogTypeId)) throw new UnrecognizedLogTypeError(logTypeSelect.value);

    const currentText = logInput.domElement.value;
    if(shouldSignLog(selectedLogTypeId, currentText, onlyInNewLogs)) logInput.appendSignature();


    function shouldSignLog(currentLogTypeId, currentText, onlyInNewLogs)
    {
        return shouldSignLogType(currentLogTypeId) && shouldSignLogText(currentText, onlyInNewLogs);
    }


    function shouldSignLogType(logTypeId)
    {
        const logTypeIsUnselected = logTypeId < 0;
        if(logTypeIsUnselected) return true;

        return configPanel.get(LOG_TYPE_CONFIG_FIELD_PREFIX + logTypeId);
    }


    function shouldSignLogText(logText, onlyInNewLogs)
    {
        return !logInput.logIsSigned() && (!onlyInNewLogs || (logText.length === 0));
    }


    function handleLogTypeChange()
    {
        const logTypeId = parseInt(logTypeSelect.value);
        if(isNaN(logTypeId)) throw new UnrecognizedLogTypeError(logTypeSelect.value);

        if(logInput.containsOnlySignature()) logInput.domElement.value = ''

        const currentText = logInput.domElement.value;
        if(shouldSignLog(logTypeId, currentText, onlyInNewLogs)) logInput.appendSignature();
    }


	function showMissingConfigurationNotification(configPanel)
	{
		const notificationOptions = Object.freeze(
		{
			title: "Missing Signature Text",
			text: "You haven't specified a signature yet.\n\nClick here to configure.",
			silent: true,
			onclick: () => void configPanel.open(),
		});
		
		GM_notification(notificationOptions);
	}


    function LogInput(inputElement, signature)
    {
        function containsOnlySignature()
        {
            return (inputElement.value === signature);
        }

        function appendSignature()
        {
            const currentText = inputElement.value;
            const texts = [currentText, signature].filter(text => !!text);
            inputElement.value = texts.join("\n");
        }

        function logIsSigned()
        {
            const currentText = inputElement.value;
            currentText.endsWith(signature);
        }

        return {
            appendSignature,
            logIsSigned,
            containsOnlySignature,
            domElement: inputElement,
        };
    }


    function waitForNewLogTypeSelect()
    {
        return new Promise(resolve =>
        {
            const intervalId = window.setInterval(() =>
            {
                const element = document.querySelector("select.log-types");
                if(element)
                {
                    window.clearInterval(intervalId);
                    resolve(element);
                }
            }, 100);
        });
    }


    function initializeConfigurability(logTypes)
    {
        const logTypeFieldEntries = logTypes.map((logType, i) =>
        {
            const fieldName = LOG_TYPE_CONFIG_FIELD_PREFIX + logType.id;
            const fieldEntry =
            {
                section: (i === 0) ? ["Log types to sign"] : undefined,
                label: logType.name,
                type: "checkbox",
                default: true,
            };

            return [fieldName, fieldEntry];
        });
        const logTypeFields = Object.fromEntries(logTypeFieldEntries);

        const fields = Object.freeze(
        {
            "signature":
            {
                "label": 'Signature',
                "type": 'textarea',
                "section": ["General settings"],
            },
            "onlyInNewLogs":
            {
                "label": 'Insert only for new logs',
                "type": 'checkbox',
                "default": true,
            },
            ...logTypeFields,
            "configHasBeenOpen":
            {
                'type': 'hidden',
                'value': '',
            },
        });

        return new Promise(resolve =>
        {
            const gmConfigConfig =
            {
                id: 'geocaching-log-singature',
                title: "Log Signature Settings",
                fields,
                events:
                {
                    open: () => configPanel.set("configHasBeenOpen", "yes"),
                    init: () => resolve(configPanel),
                },
            };
        
            const configPanel = new GM_config(gmConfigConfig);
        });
    }


    class UnrecognizedLogTypeError extends Error
    {
        constructor(logType)
        {
            super(`Unrecognized log type ID "${logType}".`);
        }
    }
})();