您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Settings module for KameSame Open Framework
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/451521/1101524/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.2 // @copyright 2022+, Robin Findley, Timberpile // @license MIT; http://opensource.org/licenses/MIT // ==/UserScript== (function (global) { const publish_context = false; // Set to 'true' to make context public. const ksof = global.ksof; //######################################################################## //------------------------------ // Constructor //------------------------------ class Settings { constructor(config) { const context = { self: this, cfg: config, }; if (!config.content) config.content = config.settings; if (publish_context) this.context = context; // Create public methods bound to context. this.cancel = cancel_btn.bind(context, context); this.open = open.bind(context, context); this.close = close.bind(context, context); this.load = load_settings.bind(context, context); this.save = save_settings.bind(context, context); this.refresh = refresh.bind(context, context); this.background = Settings.background; } //------------------------------ // Open the settings dialog. //------------------------------ save_settings(context) { 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); } } global.ksof.Settings = Settings; global.ksof.settings = {}; // Settings.save = save_settings; Settings.load = load_settings; Settings.background = { open: open_background, close: close_background, }; //######################################################################## 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) { if (Array.isArray(src[prop])) { dest[prop] = src[prop].slice(); } else { dest[prop] = dest[prop] || {}; recursive_merge(dest[prop], src[prop]); } } 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 = {}; 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) 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 = '', value, child_passback, non_page = ''; switch (item.type) { case 'tabset': child_passback = {}; for (cname in item.content) non_page += parse_item(cname, item.content[cname], child_passback); if (child_passback.tabs) html = assemble_pages(id, child_passback.tabs, child_passback.pages); break; case 'page': if (typeof item.content !== 'object') item.content = {}; if (!passback.tabs) { passback.tabs = []; 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) 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 = ''; break; case 'group': 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) 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>'; break; case 'dropdown': case 'list': let classes = 'setting'; let attribs = ''; context.config_list[name] = item; 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); } if (item.type === 'list') { classes += ' list'; attribs += ' size="' + (item.size || Object.keys(item.content).length || 4) + '"'; if (item.multi === true) attribs += ' multiple'; } html = '<select id="' + id + '" name="' + name + '" class="' + classes + '"' + 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); break; case 'checkbox': context.config_list[name] = item; html = make_label(item); 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); break; case 'input': case 'number': case 'text': const itype = item.type; if (itype === 'input') itype = item.subtype || 'text'; context.config_list[name] = item; html += make_label(item); value = get_value(context, base, name); if (value === undefined) { const is_number = (item.type === 'number' || item.subtype === 'number'); value = (item.default || (is_number === '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); break; case 'color': context.config_list[name] = item; html += make_label(item); 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); break; case 'button': 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); break; case 'divider': html += '<hr>'; break; case 'section': html += '<section>' + (item.label || '') + '</section>'; break; case 'html': html += make_label(item); html += item.html; switch (item.wrapper) { case 'row': html = wrap_row(html, null, item.hover_tip); break; case 'left': html = wrap_left(html); break; case 'right': html = wrap_right(html); break; } 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, function (ch) { const map = { '<': '<', '>': '>' }; return map[ch]; }); } function escape_attr(text) { return text.replace(/"/g, '"'); } function to_title(tip) { if (!tip) return ''; return ' title="' + tip.replace(/"/g, '"') + '"'; } } //------------------------------ // Open the settings dialog. //------------------------------ function open(context) { // todo investigate why the dialog doesn't render properly if (!ready) return; if ($('#ksofs_' + context.cfg.script_id).length > 0) return; install_anchor(); if (context.cfg.background !== false) open_background(); const dialog = $('<div id="ksofs_' + context.cfg.script_id + '" class="ksof_settings" style="display:none;"></div>'); dialog.html(config_to_html(context)); let width = 500; if (window.innerWidth < 510) { width = 280; dialog.addClass('narrow'); } dialog.dialog({ title: context.cfg.title, buttons: [ { text: 'Save', click: save_btn.bind(context, context) }, { text: 'Cancel', click: cancel_btn.bind(context, context) } ], width: width, maxHeight: document.body.clientHeight, modal: false, autoOpen: false, appendTo: '#ksof_ds', resize: resize.bind(context, context), close: close.bind(context, context) }); $(dialog.dialog('widget')).css('position', 'fixed'); dialog.parent().addClass('ksof_settings_dialog'); $('.ksof_stabs').tabs({ activate: tab_activated.bind(null, context) }); const settings = ksof.settings[context.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() }); } } dialog.dialog('open'); const dialog_elem = $('#ksofs_' + context.cfg.script_id); dialog_elem.find('.setting[multiple]').on('mousedown', toggle_multi.bind(null, context)); dialog_elem.find('.setting').on('change', setting_changed.bind(null, context)); dialog_elem.find('form').on('submit', function () { return false; }); dialog_elem.find('button.setting').on('click', setting_button_clicked.bind(null, context)); if (typeof context.cfg.pre_open === 'function') context.cfg.pre_open(dialog); context.reversions = deep_merge({}, ksof.settings[context.cfg.script_id]); refresh(context); //============ function tab_activated(context, event, ui) { const dialog = $('#ksofs_' + context.cfg.script_id); const wrapper = $(dialog.dialog('widget')); if (wrapper.outerHeight() + wrapper.position().top > document.body.clientHeight) { dialog.dialog('option', 'maxHeight', document.body.clientHeight); } } function resize(context, event, ui) { const dialog = $('#ksofs_' + context.cfg.script_id); const is_narrow = dialog.hasClass('narrow'); if (is_narrow && ui.size.width >= 510) dialog.removeClass('narrow'); else if (!is_narrow && ui.size.width < 490) dialog.addClass('narrow'); } function toggle_multi(context, 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(); }, 0); return setting_changed(context, e); } function setting_button_clicked(context, e) { const name = e.target.attributes.name.value; const item = context.config_list[name]; if (typeof item.on_click === 'function') item.on_click.call(e, name, item, setting_changed.bind(context, context, e)); } } //------------------------------ // Open the settings dialog. //------------------------------ function load_settings(context, defaults) { const script_id = (typeof context === 'string' ? context : context.cfg.script_id); return ksof.file_cache.load('ksof.settings.' + script_id) .then(finish, finish.bind(null, {})); function finish(settings) { if (defaults) ksof.settings[script_id] = deep_merge(defaults, settings); else ksof.settings[script_id] = settings; return ksof.settings[script_id]; } } //------------------------------ // Save button handler. //------------------------------ function save_btn(context, e) { const script_id = context.cfg.script_id; const dialog = $('#ksofs_' + script_id); const settings = ksof.settings[script_id]; if (settings) { const active_tabs = dialog.find('.ui-tabs-active').toArray().map(function (tab) { return '#' + tab.attributes.id.value; }); if (active_tabs.length > 0) settings.ksofs_active_tabs = active_tabs; } if (context.cfg.autosave === undefined || context.cfg.autosave === true) save_settings(context); if (typeof context.cfg.on_save === 'function') context.cfg.on_save(ksof.settings[context.cfg.script_id]); ksof.trigger('ksof.settings.save'); context.keep_settings = true; dialog.dialog('close'); } //------------------------------ // Cancel button handler. //------------------------------ function cancel_btn(context) { const dialog = $('#ksofs_' + context.cfg.script_id); dialog.dialog('close'); if (typeof context.cfg.on_cancel === 'function') context.cfg.on_cancel(ksof.settings[context.cfg.script_id]); } //------------------------------ // Close and destroy the dialog. //------------------------------ function close(context, keep_settings) { const dialog = $('#ksofs_' + context.cfg.script_id); if (!context.keep_settings && keep_settings !== true) { // Revert settings ksof.settings[context.cfg.script_id] = deep_merge({}, context.reversions); delete context.reversions; } delete context.keep_settings; dialog.dialog('destroy'); if (context.cfg.background !== false) close_background(); if (typeof context.cfg.on_close === 'function') context.cfg.on_close(ksof.settings[context.cfg.script_id]); } //------------------------------ // Update the dialog to reflect changed settings. //------------------------------ function refresh(context) { const script_id = context.cfg.script_id; const settings = ksof.settings[script_id]; const dialog = $('#ksofs_' + script_id); for (const name in context.config_list) { const elem = dialog.find('#' + script_id + '_' + name); const config = context.config_list[name]; const value = get_value(context, settings, name); switch (config.type) { case 'dropdown': case 'list': 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); } break; case 'checkbox': elem.prop('checked', value); break; default: elem.val(value); break; } } if (typeof context.cfg.on_refresh === 'function') context.cfg.on_refresh(ksof.settings[context.cfg.script_id]); } //------------------------------ // Handler for live settings changes. Handles built-in validation and user callbacks. //------------------------------ function setting_changed(context, event) { const elem = $(event.currentTarget); const name = elem.attr('name'); const item = context.config_list[name]; // Extract the value let value; const itype = ((item.type === 'input' && item.subtype === 'number') ? 'number' : item.type); switch (itype) { case 'dropdown': case 'list': 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'); } break; case 'checkbox': value = elem.is(':checked'); break; case 'number': value = Number(elem.val()); break; default: value = elem.val(); break; } // Validation let valid = { valid: true, msg: '' }; 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: '' }; switch (itype) { case 'number': if (typeof item.min === 'number' && 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 (typeof item.max === 'number' && 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'; } } if (!valid) // TODO intended? break; // eslint-disable-next-line no-fallthrough case 'text': if (item.match !== undefined && value.match(item.match) === null) { valid.valid = false; if (valid.msg.length === 0) valid.msg = item.error_msg || 'Invalid value'; } break; } // 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 = context.cfg.script_id; const settings = ksof.settings[script_id]; if (valid.valid) { if (item.no_save !== true) set_value(context, settings, name, value); if (typeof item.on_change === 'function') item.on_change.call(event.target, name, value, item); if (typeof context.cfg.on_change === 'function') context.cfg.on_change.call(event.target, name, value, item); if (item.refresh_on_change === true) refresh(context); } return false; } 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; var depth = 0, new_path = '', param, c; for (var 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() { const 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; } function open_background() { 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); } function close_background() { 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); } //------------------------------ // Load jquery UI and the appropriate CSS based on location. //------------------------------ const css_url = ksof.support_files['jqui_ksmain.css']; ksof.include('Jquery'); ksof.ready('document, Jquery') .then(function () { return Promise.all([ ksof.load_script(ksof.support_files['jquery_ui.js'], true /* cache */), ksof.load_css(css_url, true /* cache */) ]); }) .then(function (data) { ready = true; // Workaround... https://community.wanikani.com/t/19984/55 try { delete $.fn.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);