A script for interfacing with my Tree Frame UI.
Verzia zo dňa
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
// ==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); } } }) ]; })();