import { EVENT_OPTION_SET, OBJECT_MAGIC_PROP_PREFIX, } from './config.js'; export class Options { sim = undefined; options = undefined; values = {}; undefinedObj = {[OBJECT_MAGIC_PROP_PREFIX + 'undefined']: true}; nullObj = {[OBJECT_MAGIC_PROP_PREFIX + 'null']: true}; getStorageKey(path) { return `${path}:options`; } constructor(sim, options) { this.sim = sim; this.options = options; // Global methods to get/set current option values this.sim.getOption = (path) => this.getOption(path); this.sim.setOption = (path, value) => this.setOption(path, value); this.sim.onOptionSet = (path, cb) => this.onOptionSet(path, cb); // Initialize values from localStorage for (const groupName of Object.keys(options)) { for (const [name, [, , defaultValue]] of Object.entries(this.options[groupName])) { const path = [groupName, name].join('.'); let value = this.getFromLocalStorage(path); if (value === undefined) { value = defaultValue; } this.values[path] = value; } } } toStored(value) { if (value === undefined) { // Do we want to interpret this as removing from storage? // Let's just treat it as a value for now; // Semantically it works because when retrieved, it will return undefined, // which is the same result you get if the key is not set return JSON.stringify(this.undefinedObj); } else if (value === null) { return JSON.stringify(this.nullObj); } return JSON.stringify(value); } // value: string fromStored(value) { if (value === null) { return undefined; } else if (value === JSON.stringify(this.undefinedObj)) { return undefined; } else if (value === JSON.stringify(this.nullObj)) { return null; } return JSON.parse(value); } getFromLocalStorage(path) { const storageKey = this.getStorageKey(path); const value = this.fromStored(window.localStorage.getItem(storageKey)); this.values[path] = value; return value; } getOption(path) { const [groupName, name] = path.split('.'); const group = this.options[groupName]; const item = group[name]; const [,type] = item; const value = this.values[path]; switch (type) { case 'number': return Number(value); case 'boolean': return value === true || value === 'true'; default: { console.error({ path, groupName, name, group, item, type, value }); throw new Error('unknown option type'); } } } setOption(path, value) { this.values[path] = value; const storageKey = this.getStorageKey(path); window.localStorage.setItem(storageKey, this.toStored(value)); const e = new CustomEvent(EVENT_OPTION_SET, {detail: {path, value}}); this.sim.div.dispatchEvent(e); } // cb: ({path, value}) => undefined onOptionSet(path, cb) { this.sim.div.addEventListener(EVENT_OPTION_SET, (e) => { if (!path || path === e.detail.path) { cb({ path, value: e.detail.value }); } }); } getSection(sectionName) { const section = this.options[sectionName]; const group = { type: 'group', name: sectionName, title: section._title, items: [], }; for (const name in section) { if (name.startsWith('_')) continue; const [title, type, defaultValue, opts] = section[name]; group.items.push({ name, type, title, default: defaultValue, ...opts }) } return group; } }