KameSame Open Framework - Settings module

Settings module for KameSame Open Framework

As of 2022-10-06. See the latest version.

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/451521/1101660/KameSame%20Open%20Framework%20-%20Settings%20module.js

// ==UserScript==
// @name        KameSame Open Framework - Settings module
// @namespace   timberpile
// @description Settings module for KameSame Open Framework
// @version     0.15
// @copyright   2022+, Robin Findley, Timberpile
// @license     MIT; http://opensource.org/licenses/MIT
// ==/UserScript==
(async function (global) {
    // const publish_context = false; // Set to 'true' to make context public.
    const ksof = global.ksof;
    const background_funcs = () => {
        return {
            open: () => {
                const anchor = install_anchor();
                let bkgd = anchor.find('> #ksofs_bkgd');
                if (bkgd.length === 0) {
                    bkgd = $('<div id="ksofs_bkgd" refcnt="0"></div>');
                    anchor.prepend(bkgd);
                }
                const refcnt = Number(bkgd.attr('refcnt'));
                bkgd.attr('refcnt', refcnt + 1);
            },
            close: () => {
                const bkgd = $('#ksof_ds > #ksofs_bkgd');
                if (bkgd.length === 0)
                    return;
                const refcnt = Number(bkgd.attr('refcnt'));
                if (refcnt <= 0)
                    return;
                bkgd.attr('refcnt', refcnt - 1);
            }
        };
    };
    //########################################################################
    //------------------------------
    // Constructor
    //------------------------------
    class Settings {
        constructor(config) {
            // if (!config.content) config.content = config.settings;
            // if (publish_context) this.context = context;
            this.cfg = config;
            this.config_list = {};
            this.open_dialog = $();
            this.background = background_funcs();
        }
        //------------------------------
        // Open the settings dialog.
        //------------------------------
        static save(context) {
            if (!ksof.settings)
                throw new Error('ksof.settings not defined');
            if (!ksof.Settings)
                throw new Error('ksof.Settings not defined'); // this line is used to tell the compiler that settings and Settigns definitely are defined in the following lines
            const script_id = ((typeof context === 'string') ? context : context.cfg.script_id);
            const settings = ksof.settings[script_id];
            if (!settings)
                return Promise.resolve('');
            return ksof.file_cache.save('ksof.settings.' + script_id, settings);
        }
        save() {
            return Settings.save(this);
        }
        //------------------------------
        // Open the settings dialog.
        //------------------------------
        static async load(context, defaults) {
            const script_id = ((typeof context === 'string') ? context : context.cfg.script_id);
            try {
                const settings = await ksof.file_cache.load('ksof.settings.' + script_id);
                return finish(settings);
            }
            catch (error) {
                return finish.call(null, {});
            }
            function finish(settings) {
                if (!ksof.settings)
                    throw new Error('ksof.settings not defined');
                if (!ksof.Settings)
                    throw new Error('ksof.Settings not defined'); // this line is used to tell the compiler that settings and Settigns definitely are defined in the following lines
                if (defaults)
                    ksof.settings[script_id] = deep_merge(defaults, settings);
                else
                    ksof.settings[script_id] = settings;
                return ksof.settings[script_id];
            }
        }
        load(defaults) {
            return Settings.load(this, defaults);
        }
        //------------------------------
        // Save button handler.
        //------------------------------
        save_btn() {
            if (!ksof.settings)
                throw new Error('ksof.settings not defined');
            if (!ksof.Settings)
                throw new Error('ksof.Settings not defined'); // this line is used to tell the compiler that settings and Settigns definitely are defined in the following lines
            const script_id = this.cfg.script_id;
            const settings = ksof.settings[script_id];
            if (settings) {
                const active_tabs = this.open_dialog.find('.ui-tabs-active').toArray().map(function (tab) { return '#' + tab.attributes.getNamedItem('id')?.value || ''; });
                if (active_tabs.length > 0)
                    settings.ksofs_active_tabs = active_tabs;
            }
            if (this.cfg.autosave === undefined || this.cfg.autosave === true)
                this.save();
            if (typeof this.cfg.on_save === 'function')
                this.cfg.on_save(ksof.settings[this.cfg.script_id]);
            // ksof.trigger('ksof.settings.save'); // TODO what should this do?
            this.keep_settings = true;
            this.open_dialog.dialog('close');
        }
        //------------------------------
        // Cancel button handler.
        //------------------------------
        cancel_btn() {
            if (!ksof.settings)
                throw new Error('ksof.settings not defined');
            if (!ksof.Settings)
                throw new Error('ksof.Settings not defined'); // this line is used to tell the compiler that settings and Settigns definitely are defined in the following lines
            this.open_dialog.dialog('close');
            if (typeof this.cfg.on_cancel === 'function')
                this.cfg.on_cancel(ksof.settings[this.cfg.script_id]);
        }
        //------------------------------
        // Open the settings dialog.
        //------------------------------
        open() {
            if (!ksof.settings)
                throw new Error('ksof.settings not defined');
            if (!ksof.Settings)
                throw new Error('ksof.Settings not defined'); // this line is used to tell the compiler that settings and Settigns definitely are defined in the following lines
            if (!ready)
                return;
            if (this.open_dialog.length > 0)
                return;
            install_anchor();
            if (this.cfg.background !== false)
                this.background.open();
            this.open_dialog = $('<div id="ksofs_' + this.cfg.script_id + '" class="ksof_settings" style="display:none;"></div>');
            this.open_dialog.html(config_to_html(this));
            const resize = (event, ui) => {
                const is_narrow = this.open_dialog.hasClass('narrow');
                if (is_narrow && ui.size.width >= 510) {
                    this.open_dialog.removeClass('narrow');
                }
                else if (!is_narrow && ui.size.width < 490) {
                    this.open_dialog.addClass('narrow');
                }
            };
            const tab_activated = () => {
                const wrapper = $(this.open_dialog.dialog('widget'));
                if ((wrapper.outerHeight() || 0) + wrapper.position().top > document.body.clientHeight) {
                    this.open_dialog.dialog('option', 'maxHeight', document.body.clientHeight);
                }
            };
            let width = 500;
            if (window.innerWidth < 510) {
                width = 280;
                this.open_dialog.addClass('narrow');
            }
            this.open_dialog.dialog({
                title: this.cfg.title,
                buttons: [
                    { text: 'Save', click: this.save_btn.bind(this) },
                    { text: 'Cancel', click: this.cancel_btn.bind(this) }
                ],
                width: width,
                maxHeight: document.body.clientHeight,
                modal: false,
                autoOpen: false,
                appendTo: '#ksof_ds',
                resize: resize.bind(this),
                close: (e) => { this.close(false); }
            });
            $(this.open_dialog.dialog('widget')).css('position', 'fixed');
            this.open_dialog.parent().addClass('ksof_settings_dialog');
            $('.ksof_stabs').tabs({ activate: tab_activated.bind(null) });
            const settings = ksof.settings[this.cfg.script_id];
            if (settings && settings.ksofs_active_tabs instanceof Array) {
                const active_tabs = settings.ksofs_active_tabs;
                for (let tab_idx = 0; tab_idx < active_tabs.length; tab_idx++) {
                    const tab = $(active_tabs[tab_idx]);
                    tab.closest('.ui-tabs').tabs({ active: tab.index() });
                }
            }
            const toggle_multi = (e) => {
                if (e.button != 0)
                    return true;
                const multi = $(e.currentTarget);
                const scroll = e.currentTarget.scrollTop;
                e.target.selected = !e.target.selected;
                setTimeout(function () {
                    e.currentTarget.scrollTop = scroll;
                    multi.focus(); // TODO what should this do? it's deprecated
                }, 0);
                return this.setting_changed(e);
            };
            const setting_button_clicked = (e) => {
                const name = e.target.attributes.name.value;
                const item = this.config_list[name];
                if (typeof item.on_click === 'function')
                    item.on_click.call(e, name, item, this.setting_changed.bind(this, e));
            };
            this.open_dialog.dialog('open');
            this.open_dialog.find('.setting[multiple]').on('mousedown', toggle_multi.bind(this));
            this.open_dialog.find('.setting').on('change', this.setting_changed.bind(this));
            this.open_dialog.find('form').on('submit', function () { return false; });
            this.open_dialog.find('button.setting').on('click', setting_button_clicked.bind(this));
            if (typeof this.cfg.pre_open === 'function')
                this.cfg.pre_open(this.open_dialog);
            this.reversions = deep_merge({}, ksof.settings[this.cfg.script_id]);
            this.refresh();
            //============
        }
        //------------------------------
        // Handler for live settings changes.  Handles built-in validation and user callbacks.
        //------------------------------
        setting_changed(event) {
            if (!ksof.settings)
                throw new Error('ksof.settings not defined');
            if (!ksof.Settings)
                throw new Error('ksof.Settings not defined'); // this line is used to tell the compiler that settings and Settigns definitely are defined in the following lines
            console.log(event);
            const elem = $(event.currentTarget);
            const name = elem.attr('name');
            if (!name)
                return false;
            const _item = this.config_list[name];
            // Extract the value
            let value;
            if (_item.type == 'dropdown') {
                value = elem.find(':checked').attr('name');
            }
            else if (_item.type == 'list') {
                const item = _item;
                if (item.multi === true) {
                    value = {};
                    elem.find('option').each(function (i, e) {
                        const opt_name = e.getAttribute('name') || '#' + e.index;
                        value[opt_name] = e.selected;
                    });
                }
                else {
                    value = elem.find(':checked').attr('name');
                }
            }
            else if (_item.type == 'input') {
                const item = _item;
                if (item.subtype === 'number') {
                    value = Number(elem.val());
                }
            }
            else if (_item.type == 'checkbox') {
                value = elem.is(':checked');
            }
            else if (_item.type == 'number') {
                value = Number(elem.val());
            }
            else {
                value = elem.val();
            }
            // Validation
            let valid = { valid: true, msg: '' };
            {
                const item = _item;
                if (typeof item.validate === 'function')
                    valid = item.validate.call(event.target, value, item);
                if (typeof valid === 'boolean')
                    valid = { valid: valid, msg: '' };
                else if (typeof valid === 'string')
                    valid = { valid: false, msg: valid };
                else if (valid === undefined)
                    valid = { valid: true, msg: '' };
            }
            if (_item.type == 'number') {
                const item = _item;
                if (item.min && Number(value) < item.min) {
                    valid.valid = false;
                    if (valid.msg.length === 0) {
                        if (typeof item.max === 'number')
                            valid.msg = 'Must be between ' + item.min + ' and ' + item.max;
                        else
                            valid.msg = 'Must be ' + item.min + ' or higher';
                    }
                }
                else if (item.max && Number(value) > item.max) {
                    valid.valid = false;
                    if (valid.msg.length === 0) {
                        if (typeof item.min === 'number')
                            valid.msg = 'Must be between ' + item.min + ' and ' + item.max;
                        else
                            valid.msg = 'Must be ' + item.max + ' or lower';
                    }
                }
            }
            else if (_item.type == 'text') {
                const item = _item;
                if (item.match !== undefined && value.match(item.match) === null) {
                    valid.valid = false;
                    if (valid.msg.length === 0)
                        // valid.msg = item.error_msg || 'Invalid value'; // TODO no item has a error_msg?
                        valid.msg = 'Invalid value';
                }
            }
            // Style for valid/invalid
            const parent = elem.closest('.right');
            parent.find('.note').remove();
            if (typeof valid.msg === 'string' && valid.msg.length > 0)
                parent.append('<div class="note' + (valid.valid ? '' : ' error') + '">' + valid.msg + '</div>');
            if (!valid.valid) {
                elem.addClass('invalid');
            }
            else {
                elem.removeClass('invalid');
            }
            const script_id = this.cfg.script_id;
            const settings = ksof.settings[script_id];
            if (valid.valid) {
                const item = _item;
                if (item.no_save !== true)
                    set_value(this, settings, name, value);
                set_value(this, settings, name, value);
                if (typeof item.on_change === 'function')
                    item.on_change.call(event.target, name, value, item);
                if (typeof this.cfg.on_change === 'function')
                    this.cfg.on_change.call(event.target, name, value, item);
                if (item.refresh_on_change === true)
                    this.refresh();
            }
            return false;
        }
        //------------------------------
        // Close and destroy the dialog.
        //------------------------------
        close(keep_settings) {
            if (!ksof.settings)
                throw new Error('ksof.settings not defined');
            if (!ksof.Settings)
                throw new Error('ksof.Settings not defined'); // this line is used to tell the compiler that settings and Settigns definitely are defined in the following lines
            if (!this.keep_settings && keep_settings !== true) {
                // Revert settings
                ksof.settings[this.cfg.script_id] = deep_merge({}, this.reversions || {});
                delete this.reversions;
            }
            delete this.keep_settings;
            this.open_dialog.dialog('destroy');
            this.open_dialog = $();
            if (this.cfg.background !== false)
                this.background.close();
            if (typeof this.cfg.on_close === 'function')
                this.cfg.on_close(ksof.settings[this.cfg.script_id]);
        }
        //------------------------------
        // Update the dialog to reflect changed settings.
        //------------------------------
        refresh() {
            if (!ksof.settings)
                throw new Error('ksof.settings not defined');
            if (!ksof.Settings)
                throw new Error('ksof.Settings not defined'); // this line is used to tell the compiler that settings and Settigns definitely are defined in the following lines
            const script_id = this.cfg.script_id;
            const settings = ksof.settings[script_id];
            for (const name in this.config_list) {
                const elem = this.open_dialog.find('#' + script_id + '_' + name);
                const _config = this.config_list[name];
                const value = get_value(this, settings, name);
                if (_config.type == 'dropdown') {
                    elem.find('option[name="' + value + '"]').prop('selected', true);
                }
                else if (_config.type == 'list') {
                    const config = _config;
                    if (config.multi === true) {
                        elem.find('option').each(function (i, e) {
                            const opt_name = e.getAttribute('name') || '#' + e.index;
                            e.selected = value[opt_name];
                        });
                    }
                    else {
                        elem.find('option[name="' + value + '"]').prop('selected', true);
                    }
                }
                else if (_config.type == 'checkbox') {
                    elem.prop('checked', value);
                }
                else {
                    elem.val(value);
                }
            }
            if (typeof this.cfg.on_refresh === 'function')
                this.cfg.on_refresh(ksof.settings[this.cfg.script_id]);
        }
    }
    // TODO find better name than SettingsClass. SettingsObj?
    function createSettingsObj() {
        const settings_obj = (config) => {
            return new Settings(config);
        };
        settings_obj.save = (context) => { return Settings.save(context); };
        settings_obj.load = (context, defaults) => { return Settings.load(context, defaults); };
        settings_obj.background = background_funcs();
        return settings_obj;
    }
    ksof.Settings = createSettingsObj();
    ksof.settings = {};
    //########################################################################
    let ready = false;
    //========================================================================
    function deep_merge(...objects) {
        const merged = {};
        function recursive_merge(dest, src) {
            for (const prop in src) {
                if (typeof src[prop] === 'object' && src[prop] !== null) {
                    const srcProp = src[prop];
                    if (Array.isArray(srcProp)) {
                        dest[prop] = srcProp.slice();
                    }
                    else {
                        dest[prop] = dest[prop] || {};
                        recursive_merge(dest[prop], srcProp);
                    }
                }
                else {
                    dest[prop] = src[prop];
                }
            }
            return dest;
        }
        for (const obj in objects) {
            recursive_merge(merged, objects[obj]);
        }
        return merged;
    }
    //------------------------------
    // Convert a config object to html dialog.
    //------------------------------
    /* eslint-disable no-case-declarations */
    function config_to_html(context) {
        context.config_list = {};
        if (!ksof.settings) {
            return '';
        }
        let base = ksof.settings[context.cfg.script_id];
        if (base === undefined)
            ksof.settings[context.cfg.script_id] = base = {};
        let html = '';
        const child_passback = {};
        const id = context.cfg.script_id + '_dialog';
        for (const name in context.cfg.content) {
            html += parse_item(name, context.cfg.content[name], child_passback);
        }
        if (child_passback.tabs && child_passback.pages)
            html = assemble_pages(id, child_passback.tabs, child_passback.pages) + html;
        return '<form>' + html + '</form>';
        //============
        function parse_item(name, _item, passback) {
            if (typeof _item.type !== 'string')
                return '';
            const id = context.cfg.script_id + '_' + name;
            let cname, html = '', child_passback, non_page = '';
            const _type = _item.type;
            if (_type == 'tabset') {
                const item = _item;
                child_passback = {};
                for (cname in item.content) {
                    non_page += parse_item(cname, item.content[cname], child_passback);
                }
                if (child_passback.tabs && child_passback.pages) {
                    html = assemble_pages(id, child_passback.tabs, child_passback.pages);
                }
            }
            else if (_type == 'page') {
                const item = _item;
                if (typeof item.content !== 'object')
                    item.content = {};
                if (!passback.tabs) {
                    passback.tabs = [];
                }
                if (!passback.pages) {
                    passback.pages = [];
                }
                passback.tabs.push('<li id="' + id + '_tab"' + to_title(item.hover_tip) + '><a href="#' + id + '">' + item.label + '</a></li>');
                child_passback = {};
                for (cname in item.content)
                    non_page += parse_item(cname, item.content[cname], child_passback);
                if (child_passback.tabs && child_passback.pages)
                    html = assemble_pages(id, child_passback.tabs, child_passback.pages);
                passback.pages.push('<div id="' + id + '">' + html + non_page + '</div>');
                passback.is_page = true;
                html = '';
            }
            else if (_type == 'group') {
                const item = _item;
                if (typeof item.content !== 'object')
                    item.content = {};
                child_passback = {};
                for (cname in item.content)
                    non_page += parse_item(cname, item.content[cname], child_passback);
                if (child_passback.tabs && child_passback.pages)
                    html = assemble_pages(id, child_passback.tabs, child_passback.pages);
                html = '<fieldset id="' + id + '" class="ksof_group"><legend>' + item.label + '</legend>' + html + non_page + '</fieldset>';
            }
            else if (_type == 'dropdown') {
                const item = _item;
                context.config_list[name] = item;
                let value = get_value(context, base, name);
                if (value === undefined) {
                    if (item.default !== undefined) {
                        value = item.default;
                    }
                    else {
                        value = Object.keys(item.content)[0];
                    }
                    set_value(context, base, name, value);
                }
                html = `<select id="${id}" name="${name}" class="setting"${to_title(item.hover_tip)}>`;
                for (cname in item.content)
                    html += '<option name="' + cname + '">' + escape_text(item.content[cname]) + '</option>';
                html += '</select>';
                html = make_label(item) + wrap_right(html);
                html = wrap_row(html, item.full_width, item.hover_tip);
            }
            else if (_type == 'list') {
                const item = _item;
                context.config_list[name] = item;
                let value = get_value(context, base, name);
                if (value === undefined) {
                    if (item.default !== undefined) {
                        value = item.default;
                    }
                    else {
                        if (item.multi === true) {
                            value = {};
                            Object.keys(item.content).forEach(function (key) {
                                value[key] = false;
                            });
                        }
                        else {
                            value = Object.keys(item.content)[0];
                        }
                    }
                    set_value(context, base, name, value);
                }
                let attribs = ' size="' + (item.size || Object.keys(item.content).length || 4) + '"';
                if (item.multi === true)
                    attribs += ' multiple';
                html = `<select id="${id}" name="${name}" class="setting list"${attribs}${to_title(item.hover_tip)}>`;
                for (cname in item.content)
                    html += '<option name="' + cname + '">' + escape_text(item.content[cname]) + '</option>';
                html += '</select>';
                html = make_label(item) + wrap_right(html);
                html = wrap_row(html, item.full_width, item.hover_tip);
            }
            else if (_type == 'checkbox') {
                const item = _item;
                context.config_list[name] = item;
                html = make_label(item);
                let value = get_value(context, base, name);
                if (value === undefined) {
                    value = (item.default || false);
                    set_value(context, base, name, value);
                }
                html += wrap_right('<input id="' + id + '" class="setting" type="checkbox" name="' + name + '">');
                html = wrap_row(html, item.full_width, item.hover_tip);
            }
            else if (_type == 'input') {
                const item = _item;
                const itype = item.subtype || 'text';
                context.config_list[name] = item;
                html += make_label(item);
                let value = get_value(context, base, name);
                if (value === undefined) {
                    const is_number = (item.subtype === 'number');
                    value = (item.default || (is_number ? 0 : ''));
                    set_value(context, base, name, value);
                }
                html += wrap_right(`<input id="${id}" class="setting" type="${itype}" name="${name}"${(item.placeholder ? ' placeholder="' + escape_attr(item.placeholder) + '"' : '')}>`);
                html = wrap_row(html, item.full_width, item.hover_tip);
            }
            else if (_type == 'number') {
                const item = _item;
                const itype = item.type;
                context.config_list[name] = item;
                html += make_label(item);
                let value = get_value(context, base, name);
                if (value === undefined) {
                    const is_number = (item.type === 'number');
                    value = (item.default || (is_number ? 0 : ''));
                    set_value(context, base, name, value);
                }
                html += wrap_right(`<input id="${id}" class="setting" type="${itype}" name="${name}"${(item.placeholder ? ' placeholder="' + escape_attr(item.placeholder) + '"' : '')}>`);
                html = wrap_row(html, item.full_width, item.hover_tip);
            }
            else if (_type == 'text') {
                const item = _item;
                const itype = item.type;
                context.config_list[name] = item;
                html += make_label(item);
                let value = get_value(context, base, name);
                if (value === undefined) {
                    value = (item.default || '');
                    set_value(context, base, name, value);
                }
                html += wrap_right(`<input id="${id}" class="setting" type="${itype}" name="${name}"${(item.placeholder ? ' placeholder="' + escape_attr(item.placeholder) + '"' : '')}>`);
                html = wrap_row(html, item.full_width, item.hover_tip);
            }
            else if (_type == 'color') {
                const item = _item;
                context.config_list[name] = item;
                html += make_label(item);
                let value = get_value(context, base, name);
                if (value === undefined) {
                    value = (item.default || '#000000');
                    set_value(context, base, name, value);
                }
                html += wrap_right('<input id="' + id + '" class="setting" type="color" name="' + name + '">');
                html = wrap_row(html, item.full_width, item.hover_tip);
            }
            else if (_type == 'button') {
                const item = _item;
                context.config_list[name] = item;
                html += make_label(item);
                const text = escape_text(item.text || 'Click');
                html += wrap_right('<button type="button" class="setting" name="' + name + '">' + text + '</button>');
                html = wrap_row(html, item.full_width, item.hover_tip);
            }
            else if (_type == 'divider') {
                html += '<hr>';
            }
            else if (_type == 'section') {
                const item = _item;
                html += '<section>' + (item.label || '') + '</section>';
            }
            else if (_type == 'html') {
                const item = _item;
                html += make_label(item);
                html += item.html;
                switch (item.wrapper) {
                    case 'row':
                        html = wrap_row(html, undefined, item.hover_tip);
                        break;
                    case 'left':
                        html = wrap_left(html);
                        break;
                    case 'right':
                        html = wrap_right(html);
                        break;
                }
            }
            return html;
            function make_label(item) {
                if (typeof item.label !== 'string')
                    return '';
                return wrap_left('<label for="' + id + '">' + item.label + '</label>');
            }
        }
        /* eslint-enable no-case-declarations */
        //============
        function assemble_pages(id, tabs, pages) { return '<div id="' + id + '" class="ksof_stabs"><ul>' + tabs.join('') + '</ul>' + pages.join('') + '</div>'; }
        function wrap_row(html, full, hover_tip) { return '<div class="row' + (full ? ' full' : '') + '"' + to_title(hover_tip) + '>' + html + '</div>'; }
        function wrap_left(html) { return '<div class="left">' + html + '</div>'; }
        function wrap_right(html) { return '<div class="right">' + html + '</div>'; }
        function escape_text(text) {
            return text.replace(/[<>]/g, (ch) => {
                if (ch == '<')
                    return '&lt';
                if (ch == '>')
                    return '&gt';
                return '';
            });
        }
        function escape_attr(text) { return text.replace(/"/g, '&quot;'); }
        function to_title(tip) { if (!tip)
            return ''; return ' title="' + tip.replace(/"/g, '&quot;') + '"'; }
    }
    function get_value(context, base, name) {
        const item = context.config_list[name];
        const evaluate = (item.path !== undefined);
        const path = (item.path || name);
        try {
            if (!evaluate)
                return base[path];
            return eval(path.replace(/@/g, 'base.'));
        }
        catch (e) {
            return;
        }
    }
    function set_value(context, base, name, value) {
        const item = context.config_list[name];
        const evaluate = (item.path !== undefined);
        const path = (item.path || name);
        try {
            if (!evaluate)
                return base[path] = value;
            let depth = 0;
            let new_path = '';
            let param = '';
            let c;
            for (let idx = 0; idx < path.length; idx++) {
                c = path[idx];
                if (c === '[') {
                    if (depth++ === 0) {
                        new_path += '[';
                        param = '';
                    }
                    else {
                        param += '[';
                    }
                }
                else if (c === ']') {
                    if (--depth === 0) {
                        new_path += JSON.stringify(eval(param)) + ']';
                    }
                    else {
                        param += ']';
                    }
                }
                else {
                    if (c === '@')
                        c = 'base.';
                    if (depth === 0)
                        new_path += c;
                    else
                        param += c;
                }
            }
            eval(new_path + '=value');
        }
        catch (e) {
            return;
        }
    }
    function install_anchor() {
        let anchor = $('#ksof_ds');
        if (anchor.length === 0) {
            anchor = $('<div id="ksof_ds"></div></div>');
            $('body').prepend(anchor);
            $('#ksof_ds').on('keydown keyup keypress', '.ksof_settings_dialog', function (e) {
                // Stop keys from bubbling beyond the background overlay.
                e.stopPropagation();
            });
        }
        return anchor;
    }
    //------------------------------
    // Load jquery UI and the appropriate CSS based on location.
    //------------------------------
    const css_url = ksof.support_files['jqui_ksmain.css'];
    ksof.include('Jquery');
    await ksof.ready('document, Jquery');
    await Promise.all([
        ksof.load_script(ksof.support_files['jquery_ui.js'], true /* cache */),
        ksof.load_css(css_url, true /* cache */)
    ]);
    ready = true;
    // Workaround...  https://community.wanikani.com/t/19984/55
    try {
        const temp = $.fn;
        delete temp.autocomplete;
    }
    catch (e) {
        // do nothing
    }
    // Notify listeners that we are ready.
    // Delay guarantees include() callbacks are called before ready() callbacks.
    setTimeout(function () { ksof.set_state('ksof.Settings', 'ready'); }, 0);
})(this);