Tree Frame 2

A script for interfacing with my Tree Frame UI.

Versione datata 31/07/2022. Vedi la nuova versione l'ultima versione.

Questo script non dovrebbe essere installato direttamente. È una libreria per altri script da includere con la chiave // @require https://update.greasyfork.org/scripts/446506/1076104/Tree%20Frame%202.js

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name        Tree Frame 2
// @author      Callum Latham <[email protected]> (https://github.com/ctl2/key-based-config)
// @exclude     *
// @description A script for interfacing with my Tree Frame UI.
// ==/UserScript==

const [getForest, editForest] = (() => {
    const USER_STYLE_STORAGE_KEY = 'TREE_FRAME_USER_STYLES';

    const URL = {
        'SCHEME': 'https',
        'HOST': 'callumlatham.com',
        'PATH': 'tree-frame'
    };

    const STYLE = {
        'position': 'fixed',
        'height': '100vh',
        'width': '100vw'
    };

    let isOpen = false;

    function getStrippedForest(children) {
        const stripped = [];

        for (const child of children) {
            if (child.isActive === false) {
                continue;
            }

            const data = {};

            if ('value' in child) {
                data.value = child.value;
            }

            if ('label' in child) {
                data.label = child.label;
            }

            if ('children' in child) {
                data.children = getStrippedForest(child.children);
            }

            stripped.push(data);
        }

        return stripped;
    }

    return [
        (storageKey, defaultTree) => new Promise(async (resolve, reject) => {
            if (typeof GM.getValue !== 'function') {
                reject('The GM.getValue permission is required to retrieve data.');
            }

            GM.getValue(storageKey, defaultTree)
                .then((tree) => {
                    resolve(getStrippedForest(tree.children))
                });
        }),

        (storageKey, rawDefaultTree, title, defaultStyle = {}, frameStyle = {}) => new Promise(async (resolve, reject) => {
            if (isOpen) {
                reject('A Tree-Frame iFrame is already open.');
            } else if (typeof GM.getValue !== 'function' || typeof GM.setValue !== 'function') {
                reject('The tree-frame script requires GM.getValue and GM.setValue permissions.');
            } else if (typeof storageKey !== 'string' || storageKey === '') {
                reject(`'${storageKey}' is not a valid storage key.`);
            } else {
                isOpen = true;

                // Setup frame

                const [targetWindow, frame] = (() => {
                    const frame = document.createElement('iframe');

                    frame.src = `${URL.SCHEME}://${URL.HOST}/${URL.PATH}`;

                    for (const [property, value] of Object.entries({...STYLE, ...frameStyle})) {
                        frame.style[property] = value;
                    }

                    let targetWindow = window;

                    while (targetWindow !== targetWindow.parent) {
                        targetWindow = targetWindow.parent;
                    }

                    targetWindow.document.body.appendChild(frame);

                    return [targetWindow, frame];
                })();

                // Retrieve data & await frame load

                const onMessage = (callback) => {
                    const listener = async (message) => {
                        if (message.origin === `${URL.SCHEME}://${URL.HOST}`) {
                            if (await callback(message.data)) {
                                window.removeEventListener('message', listener);
                            }
                        }
                    };

                    targetWindow.addEventListener('message', listener);
                };

                const [userTree, userStyles, {'events': EVENTS, password}] = await Promise.all([
                    GM.getValue(storageKey),
                    GM.getValue(USER_STYLE_STORAGE_KEY, []),
                    new Promise((resolve) => onMessage((data) => {
                        // Wait for a 'ready' response from the iFrame
                        resolve(data);

                        return true;
                    }))
                ]);

                // Listen for post-init communication

                const sendMessage = (message) => {
                    frame.contentWindow.postMessage(message, `${URL.SCHEME}://${URL.HOST}`);
                };

                const closeFrame = () => {
                    return new Promise(resolve => {
                        isOpen = false;

                        frame.remove();

                        // Wait for the DOM to update
                        setTimeout(resolve, 1);
                    });
                };

                // Remove functions from tree to enable postMessage transmission
                const [defaultTree, predicates] = (() => {
                    const getNumberedPredicates = (node, predicateCount) => {
                        const predicates = [];
                        const replacements = {};

                        for (const property of ['predicate', 'childPredicate', 'descendantPredicate']) {
                            switch (typeof node[property]) {
                                case 'number':
                                    throw new Error('Numbers aren\'t valid predicates');

                                case 'function':
                                    replacements[property] = predicateCount++;

                                    predicates.push(node[property]);
                            }
                        }

                        if (Array.isArray(node.children)) {
                            replacements.children = [];

                            for (const child of node.children) {
                                const [replacement, childPredicates] = getNumberedPredicates(child, predicateCount);

                                predicateCount += childPredicates.length;

                                predicates.push(...childPredicates);

                                replacements.children.push(replacement);
                            }
                        }

                        if ('seed' in node) {
                            const [replacement, seedPredicates] = getNumberedPredicates(node.seed, predicateCount);

                            predicates.push(...seedPredicates);

                            replacements.seed = replacement;
                        }

                        return [{...node, ...replacements}, predicates];
                    };

                    return getNumberedPredicates(rawDefaultTree, 0);
                })();

                onMessage(async (data) => {
                    switch (data.event) {
                        case EVENTS.ERROR:
                            await closeFrame();

                            reject(data.reason);

                            return true;

                        case EVENTS.PREDICATE:
                            sendMessage({
                                'messageId': data.messageId,
                                'predicateResponse': predicates[data.predicateId](
                                    Array.isArray(data.arg) ? getStrippedForest(data.arg) : data.arg
                                )
                            });

                            return false;

                        case EVENTS.STOP:
                            await closeFrame();

                            // Save changes
                            GM.setValue(storageKey, data.tree);
                            GM.setValue(USER_STYLE_STORAGE_KEY, data.styles);

                            resolve(getStrippedForest(data.tree.children));

                            return true;

                        default:
                            reject(`Message observed from tree-frame site with unrecognised 'event' value: ${data.event}`);

                            return false;
                    }
                });

                // Pass data

                try {
                    const config = {password, title, defaultTree, userStyles, defaultStyle};

                    if (userTree) {
                        config.userTree = userTree;
                    }

                    sendMessage(config);
                } catch (e) {
                    reject(e.message);
                }
            }
        })
    ];
})();