Tree Frame 2

A script for interfacing with my Tree Frame UI.

31.07.2022 itibariyledir. En son verisyonu görün.

Bu script direkt olarak kurulamaz. Başka scriptler için bir kütüphanedir ve meta yönergeleri içerir // @require https://update.greasyfork.org/scripts/446506/1076104/Tree%20Frame%202.js

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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