Tree Frame 2

A script for interfacing with my Tree Frame UI.

Verzia zo dňa 31.07.2022. Pozri najnovšiu verziu.

Tento skript by nemal byť nainštalovaný priamo. Je to knižnica pre ďalšie skripty, ktorú by mali používať cez meta príkaz // @require https://update.greasyfork.org/scripts/446506/1076104/Tree%20Frame%202.js

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==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);
                }
            }
        })
    ];
})();