אין להתקין סקריפט זה ישירות. זוהי ספריה עבור סקריפטים אחרים // @require https://update.greatest.deepsurf.us/scripts/38576/1091793/Wanikani%20Open%20Framework%20-%20Settings%20module.js
- // ==UserScript==
- // @name Wanikani Open Framework - Settings module
- // @namespace rfindley
- // @description Settings module for Wanikani Open Framework
- // @version 1.0.20
- // @copyright 2022+, Robin Findley
- // @license MIT; http://opensource.org/licenses/MIT
- // ==/UserScript==
-
- (function(global) {
-
- const publish_context = false; // Set to 'true' to make context public.
-
- //########################################################################
- //------------------------------
- // Constructor
- //------------------------------
- function Settings(config) {
- var 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;
- };
-
- global.wkof.Settings = Settings;
- Settings.save = save_settings;
- Settings.load = load_settings;
- Settings.background = {
- open: open_background,
- close: close_background,
- }
- //########################################################################
-
- wkof.settings = {};
- var ready = false;
-
- //========================================================================
- function deep_merge(...objects) {
- let merged = {};
- function recursive_merge(dest, src) {
- for (let 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 (let obj in objects) {
- recursive_merge(merged, objects[obj]);
- }
- return merged;
- }
-
- //------------------------------
- // Convert a config object to html dialog.
- //------------------------------
- function config_to_html(context) {
- context.config_list = {};
- var base = wkof.settings[context.cfg.script_id];
- if (base === undefined) wkof.settings[context.cfg.script_id] = base = {};
-
- var html = '', item, child_passback = {};
- var id = context.cfg.script_id+'_dialog';
- for (var name in context.cfg.content) {
- var item = context.cfg.content[name];
- 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 '';
- var id = context.cfg.script_id+'_'+name;
- var 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="wkof_group"><legend>'+item.label+'</legend>'+html+non_page+'</fieldset>';
- break;
-
- case 'dropdown':
- case 'list':
- var classes = 'setting', 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':
- var 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) {
- var 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);
- var 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>');
- }
- }
-
- //============
- function assemble_pages(id, tabs, pages) {return '<div id="'+id+'" class="wkof_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) {var 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) {
- if (!ready) return;
- if ($('#wkofs_'+context.cfg.script_id).length > 0) return;
- install_anchor();
- if (context.cfg.background !== false) open_background();
- var dialog = $('<div id="wkofs_'+context.cfg.script_id+'" class="wkof_settings" style="display:none;"></div>');
- dialog.html(config_to_html(context));
-
- var 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: '#wkof_ds',
- resize: resize.bind(context,context),
- close: close.bind(context,context)
- });
- $(dialog.dialog('widget')).css('position','fixed');
- dialog.parent().addClass('wkof_settings_dialog');
-
- $('.wkof_stabs').tabs({activate:tab_activated.bind(null,context)});
- var settings = wkof.settings[context.cfg.script_id];
- if (settings && settings.wkofs_active_tabs instanceof Array) {
- var active_tabs = settings.wkofs_active_tabs;
- for (var tab_idx = 0; tab_idx < active_tabs.length; tab_idx++) {
- var tab = $(active_tabs[tab_idx]);
- tab.closest('.ui-tabs').tabs({active:tab.index()});
- }
- }
-
- dialog.dialog('open');
- var dialog_elem = $('#wkofs_'+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({}, wkof.settings[context.cfg.script_id]);
- refresh(context);
-
- //============
- function tab_activated(context, event, ui) {
- var dialog = $('#wkofs_'+context.cfg.script_id);
- var 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){
- var dialog = $('#wkofs_'+context.cfg.script_id);
- var 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;
- var multi = $(e.currentTarget);
- var 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) {
- var name = e.target.attributes.name.value;
- var 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 save_settings(context) {
- var script_id = (typeof context === 'string' ? context : context.cfg.script_id);
- var settings = wkof.settings[script_id];
- if (!settings) return Promise.resolve();
- return wkof.file_cache.save('wkof.settings.'+script_id, settings);
- }
-
- //------------------------------
- // Open the settings dialog.
- //------------------------------
- function load_settings(context, defaults) {
- var script_id = (typeof context === 'string' ? context : context.cfg.script_id);
- return wkof.file_cache.load('wkof.settings.'+script_id)
- .then(finish, finish.bind(null,{}));
-
- function finish(settings) {
- if (defaults)
- wkof.settings[script_id] = deep_merge(defaults, settings);
- else
- wkof.settings[script_id] = settings;
- return wkof.settings[script_id];
- }
- }
-
- //------------------------------
- // Save button handler.
- //------------------------------
- function save_btn(context, e) {
- var script_id = context.cfg.script_id;
- var dialog = $('#wkofs_'+script_id);
- var settings = wkof.settings[script_id];
- if (settings) {
- var active_tabs = dialog.find('.ui-tabs-active').toArray().map(function(tab){return '#'+tab.attributes.id.value});
- if (active_tabs.length > 0) settings.wkofs_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(wkof.settings[context.cfg.script_id]);
- wkof.trigger('wkof.settings.save');
- context.keep_settings = true;
- dialog.dialog('close');
- }
-
- //------------------------------
- // Cancel button handler.
- //------------------------------
- function cancel_btn(context) {
- var dialog = $('#wkofs_'+context.cfg.script_id);
- dialog.dialog('close');
- if (typeof context.cfg.on_cancel === 'function') context.cfg.on_cancel(wkof.settings[context.cfg.script_id]);
- }
-
- //------------------------------
- // Close and destroy the dialog.
- //------------------------------
- function close(context, keep_settings) {
- var dialog = $('#wkofs_'+context.cfg.script_id);
- if (!context.keep_settings && keep_settings !== true) {
- // Revert settings
- wkof.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(wkof.settings[context.cfg.script_id]);
- }
-
- //------------------------------
- // Update the dialog to reflect changed settings.
- //------------------------------
- function refresh(context) {
- var script_id = context.cfg.script_id;
- var settings = wkof.settings[script_id];
- var dialog = $('#wkofs_'+script_id);
- for (var name in context.config_list) {
- var elem = dialog.find('#'+script_id+'_'+name);
- var config = context.config_list[name];
- var value = get_value(context, settings, name);
- switch (config.type) {
- case 'dropdown':
- case 'list':
- if (config.multi === true) {
- elem.find('option').each(function(i,e){
- var 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(wkof.settings[context.cfg.script_id]);
- }
-
- //------------------------------
- // Handler for live settings changes. Handles built-in validation and user callbacks.
- //------------------------------
- function setting_changed(context, event) {
- var elem = $(event.currentTarget);
- var name = elem.attr('name');
- var item = context.config_list[name];
- var config;
-
- // Extract the value
- var value;
- var 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){
- var 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
- var 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)
- break;
-
- 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
- var 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');
- }
-
- var script_id = context.cfg.script_id;
- var settings = wkof.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){
- var item = context.config_list[name];
- var evaluate = (item.path !== undefined);
- var 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) {
- var item = context.config_list[name];
- var evaluate = (item.path !== undefined);
- var 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() {
- var anchor = $('#wkof_ds');
- if (anchor.length === 0) {
- anchor = $('<div id="wkof_ds"></div></div>');
- $('body').prepend(anchor);
- $('#wkof_ds').on('keydown keyup keypress', '.wkof_settings_dialog', function(e) {
- // Stop keys from bubbling beyond the background overlay.
- e.stopPropagation();
- });
- }
- return anchor;
- }
-
- function open_background() {
- var anchor = install_anchor();
- var bkgd = anchor.find('> #wkofs_bkgd');
- if (bkgd.length === 0) {
- bkgd = $('<div id="wkofs_bkgd" refcnt="0"></div>');
- anchor.prepend(bkgd);
- }
- var refcnt = Number(bkgd.attr('refcnt'));
- bkgd.attr('refcnt', refcnt + 1);
- }
-
- function close_background() {
- var bkgd = $('#wkof_ds > #wkofs_bkgd');
- if (bkgd.length === 0) return;
- var refcnt = Number(bkgd.attr('refcnt'));
- if (refcnt <= 0) return;
- bkgd.attr('refcnt', refcnt - 1);
- }
-
- //------------------------------
- // Load jquery UI and the appropriate CSS based on location.
- //------------------------------
- var css_url = wkof.support_files['jqui_wkmain.css'];
-
- wkof.include('Jquery');
- wkof.ready('document, Jquery')
- .then(function(){
- return Promise.all([
- wkof.load_script(wkof.support_files['jquery_ui.js'], true /* cache */),
- wkof.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) {}
-
- // Notify listeners that we are ready.
- // Delay guarantees include() callbacks are called before ready() callbacks.
- setTimeout(function(){wkof.set_state('wkof.Settings', 'ready');},0);
- });
-
- })(this);