UNPKG

tweak-tools

Version:

Tweak your React projects until awesomeness

302 lines (301 loc) 12.6 kB
"use strict"; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.useCreateStore = exports.tweakStore = exports.Store = void 0; const react_1 = require("react"); const zustand_1 = __importDefault(require("zustand")); const middleware_1 = require("zustand/middleware"); const utils_1 = require("./utils"); const types_1 = require("./types"); const eventEmitter_1 = require("./eventEmitter"); exports.Store = function () { const store = (0, zustand_1.default)((0, middleware_1.subscribeWithSelector)(() => ({ data: {} }))); const eventEmitter = (0, eventEmitter_1.createEventEmitter)(); this.storeId = (0, utils_1.getUid)(); this.useStore = store; /** * Folders will hold the folder settings for the pane. * @note possibly make this reactive */ const folders = {}; /** * OrderedPaths will hold all the paths in a parent -> children order. * This will ensure we can display the controls in a predictable order. */ const orderedPaths = new Set(); /** * For a given data structure, gets all paths for which inputs have * a reference __refCount superior to zero. This function is used by the * root pane to only display the inputs that are consumed by mounted * components. * * @param data */ this.getVisiblePaths = () => { const data = this.getData(); const paths = Object.keys(data); // identifies hiddenFolders const hiddenFolders = []; Object.entries(folders).forEach(([path, settings]) => { if ( // the folder settings have a render function settings.render && // and the folder path matches a data path // (this can happen on first mount and could probably be solved if folder settings // were set together with the store data. In fact, the store data is set in useEffect // while folders settings are set in useMemo). paths.some((p) => p.indexOf(path) === 0) && // the folder settings is supposed to be hidden !settings.render(this.get)) // then folder is hidden hiddenFolders.push(path + '.'); }); const visiblePaths = []; orderedPaths.forEach((path) => { if (path in data && // if input is mounted data[path].__refCount > 0 && // if it's not included in a hidden folder hiddenFolders.every((p) => path.indexOf(p) === -1) && // if its render functions doesn't exists or returns true (!data[path].render || data[path].render(this.get))) { // then the input path is visible visiblePaths.push(path); } }); return visiblePaths; }; // adds paths to OrderedPaths this.setOrderedPaths = (newPaths) => { newPaths.forEach((p) => orderedPaths.add(p)); }; this.orderPaths = (paths) => { this.setOrderedPaths(paths); return paths; }; /** * When the useTweak hook unmmounts, it will call this function that will * decrease the __refCount of all the inputs. When an input __refCount reaches 0, it * should no longer be displayed in the panel. * * @param paths */ this.disposePaths = (paths) => { store.setState((s) => { const data = s.data; paths.forEach((path) => { if (path in data) { const input = data[path]; input.__refCount--; if (input.__refCount === 0 && input.type in types_1.SpecialInputs) { // this makes sure special inputs such as buttons are properly // refreshed. This might need some attention though. delete data[path]; } } }); return { data }; }); }; this.dispose = () => { store.setState(() => { return { data: {} }; }); }; this.getFolderSettings = (path) => { return folders[path] || {}; }; // Shorthand to get zustand store data this.getData = () => { return store.getState().data; }; /** * Merges the data passed as an argument with the store data. * If an input path from the data already exists in the store, * the function doesn't update the data but increments __refCount * to keep track of how many components use that input key. * * Uses depsChanged to trigger a recompute and update inputs * settings if needed. * * @param newData the data to update * @param depsChanged to keep track of dependencies */ this.addData = (newData, override) => { store.setState((s) => { const data = s.data; Object.entries(newData).forEach(([path, newInputData]) => { let input = data[path]; // If an input already exists compare its values and increase the reference __refCount. if (!!input) { // @ts-ignore const { type, value } = newInputData, rest = __rest(newInputData, ["type", "value"]); if (type !== input.type) { console.warn(utils_1.TweakErrors.INPUT_TYPE_OVERRIDE, input.type, type); } else { if (input.__refCount === 0 || override) { Object.assign(input, rest); } // Else we increment the ref count input.__refCount++; } } else { data[path] = Object.assign(Object.assign({}, newInputData), { __refCount: 1 }); } }); // Since we're returning a new object, direct mutation of data // Should trigger a re-render so we're good! return { data }; }); }; /** * Shorthand function to set the value of an input at a given path. * * @param path path of the input * @param value new value of the input */ this.setValueAtPath = (path, value, fromPanel) => { store.setState((s) => { const data = s.data; //@ts-expect-error (we always update inputs with a value) (0, utils_1.updateInput)(data[path], value, path, this, fromPanel); return { data }; }); }; this.setSettingsAtPath = (path, settings) => { store.setState((s) => { const data = s.data; //@ts-expect-error (we always update inputs with settings) data[path].settings = Object.assign(Object.assign({}, data[path].settings), settings); return { data }; }); }; this.disableInputAtPath = (path, flag) => { store.setState((s) => { const data = s.data; //@ts-expect-error (we always update inputs with a value) data[path].disabled = flag; return { data }; }); }; this.set = (values, fromPanel) => { store.setState((s) => { const data = s.data; Object.entries(values).forEach(([path, value]) => { try { //@ts-expect-error (we always update inputs with a value) (0, utils_1.updateInput)(data[path], value, undefined, undefined, fromPanel); } catch (e) { if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line no-console console.warn(`[This message will only show in development]: \`set\` for path ${path} has failed.`, e); } } }); return { data }; }); }; this.getInput = (path) => { try { return this.getData()[path]; } catch (e) { console.warn(utils_1.TweakErrors.PATH_DOESNT_EXIST, path, "null"); } }; this.get = (path) => { var _a; return (_a = this.getInput(path)) === null || _a === void 0 ? void 0 : _a.value; }; this.emitOnEditStart = (path) => { eventEmitter.emit(`onEditStart:${path}`, this.get(path), path, Object.assign(Object.assign({}, this.getInput(path)), { get: this.get })); }; this.emitOnEditEnd = (path) => { eventEmitter.emit(`onEditEnd:${path}`, this.get(path), path, Object.assign(Object.assign({}, this.getInput(path)), { get: this.get })); }; this.subscribeToEditStart = (path, listener) => { const _path = `onEditStart:${path}`; eventEmitter.on(_path, listener); return () => eventEmitter.off(_path, listener); }; this.subscribeToEditEnd = (path, listener) => { const _path = `onEditEnd:${path}`; eventEmitter.on(_path, listener); return () => eventEmitter.off(_path, listener); }; /** * Recursively extract the data from the schema, sets folder initial * preferences and normalize the inputs (normalizing an input means parsing the * input object, identify its type and normalize its settings). * * @param schema * @param rootPath used for recursivity */ const _getDataFromSchema = (schema, rootPath, mappedPaths) => { const data = {}; Object.entries(schema).forEach(([key, rawInput]) => { // if the key is empty, skip schema parsing and prompt an error. if (key === '') return console.warn(utils_1.TweakErrors.EMPTY_KEY, "", ""); let newPath = (0, utils_1.join)(rootPath, key); // If the input is a folder, then we recursively parse its schema and assign // it to the current data. if (rawInput.type === types_1.SpecialInputs.FOLDER) { const newData = _getDataFromSchema(rawInput.schema, newPath, mappedPaths); Object.assign(data, newData); // Sets folder preferences if it wasn't set before if (!(newPath in folders)) folders[newPath] = rawInput.settings; } else if (key in mappedPaths) { // if a key already exists, prompt an error. console.warn(utils_1.TweakErrors.DUPLICATE_KEYS, key, newPath); } else { const normalizedInput = (0, utils_1.normalizeInput)(rawInput, key, newPath, data); if (normalizedInput) { const { type, options, input } = normalizedInput; // @ts-ignore const { onChange, transient, onEditStart, onEditEnd } = options, _options = __rest(options, ["onChange", "transient", "onEditStart", "onEditEnd"]); data[newPath] = Object.assign(Object.assign(Object.assign({ type }, _options), input), { fromPanel: true }); mappedPaths[key] = { path: newPath, onChange, transient, onEditStart, onEditEnd }; } else { console.warn(utils_1.TweakErrors.UNKNOWN_INPUT, newPath, rawInput); } } }); return data; }; this.getDataFromSchema = (schema) => { const mappedPaths = {}; const data = _getDataFromSchema(schema, '', mappedPaths); return [data, mappedPaths]; }; }; exports.tweakStore = new exports.Store(); function useCreateStore() { return (0, react_1.useMemo)(() => new exports.Store(), []); } exports.useCreateStore = useCreateStore; if (process.env.NODE_ENV === 'development' && typeof window !== 'undefined') { // TODO remove store from window // @ts-expect-error window.__STORE = exports.tweakStore; }