Wanikani Open Framework - Settings module

Settings module for Wanikani Open Framework

当前为 2018-03-06 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/38576/256577/Wanikani%20Open%20Framework%20-%20Settings%20module.js

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Wanikani Open Framework - Settings module
// @namespace   rfindley
// @description Settings module for Wanikani Open Framework
// @version     1.0.1
// @copyright   2018+, 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,
			script_id: config.script_id,
			title: config.title,
			autosave: (config.autosave === true || config.autosave === undefined),
			config: 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.load = load_settings.bind(context, context);
		this.save = save_settings.bind(context, context);
		this.on_save = config.on_save;
		this.on_close = config.on_close;
		this.on_cancel = config.on_cancel;
	};

	global.wkof.Settings = Settings;
	Settings.save = save_settings;
	Settings.load = load_settings;
	//########################################################################

	wkof.settings = {};
	var ready = false;
	var revert_settings = {};

	//------------------------------
	// Convert a config object to html dialog.
	//------------------------------
	function config_to_html(context) {
		context.config_list = {};
		if (wkof.settings[context.script_id] === undefined) wkof.settings[context.script_id] = {};
		var saved = wkof.settings[context.script_id], value;
		var pages = [], depth = 0;

		var html = '';
		for (var name in context.config)
			html += parse_item(name, context.config[name]);
		if (pages.length > 0)
			html = '<div class="wkof_stabs"><ul>'+pages.join('')+'</ul>'+html+'</div>';
		return '<form>'+html+'</form>';

		//============
		function parse_item(name, item) {
			depth++;
			if (typeof item.type !== 'string') return '';
			if (depth == 1 && item.type !== 'page' && pages.length > 0) return '';
			if (typeof item.label !== 'string') item.label = '&lt;untitled&gt;';
			var id = context.script_id+'_'+name;
			var cname,html = '';
			switch (item.type) {
				case 'page':
					if (depth > 1) return;
					if (typeof item.content !== 'object') item.content = {};
					pages.push('<li id="'+id+'_tab"><a href="#'+id+'">'+item.label+'</a></li>');
					html = '<div id="'+id+'">';
					for (cname in item.content) 
						html += parse_item(cname, item.content[cname]);
					html += '</div>';
					break;

				case 'group':
					if (typeof item.content !== 'object') item.content = {};
					html = '<fieldset id="'+id+'" class="wkof_group"><legend>'+item.label+'</legend>';
					for (cname in item.content) 
						html += '<div class="setting_row">'+parse_item(cname, item.content[cname])+'</div>';
					html += '</fieldset>';
					break;

				case 'dropdown':
					context.config_list[name] = item;
					if (typeof item.label === 'string')
						html += '<label for="'+id+'">'+item.label+'</label>';
					html += '<select id="'+id+'" class="setting" name="'+name+'">';
					if (saved[name] === undefined) saved[name] = (item.default || Object.keys(item.content)[0]);
					for (cname in item.content) {
						value = (cname == saved[name] ? ' selected="selected"':'');
						html += '<option name="'+cname+'"'+value+'>'+item.content[cname]+'</option>';
					}
					html += '</select>';
					break;

				case 'checkbox':
					context.config_list[name] = item;
					if (typeof item.label === 'string')
						html += '<label for="'+id+'">'+item.label+'</label>';
					if (saved[name] === undefined) saved[name] = (item.default || false);
					value = (saved[name] === true ? ' checked="checked"':'');
					html += '<input id="'+id+'" class="setting" type="checkbox" name="'+name+'"'+value+'>';
					break;

				case 'number':
				case 'text':
					context.config_list[name] = item;
					if (typeof item.label === 'string')
						html += '<label for="'+id+'">'+item.label+'</label>';
					if (saved[name] === undefined) saved[name] = (item.default || (item.type==='text'?'':'0'));
					html += '<input id="'+id+'" class="setting" type="text" name="'+name+'" value="'+saved[name]+'">'
					break;

				case 'color':
					context.config_list[name] = item;
					if (typeof item.label === 'string')
						html += '<label for="'+id+'">'+item.label+'</label>';
					if (saved[name] === undefined) saved[name] = (item.default || '#000000');
					html += '<input id="'+id+'" class="setting" type="color" name="'+name+'" value="'+saved[name]+'">'
					break;
			}
			depth--;
			return html;
		}
	}

	//------------------------------
	// Open the settings dialog.
	//------------------------------
	function open(context) {
		if (!ready) return;
		if ($('#wkofs_'+context.script_id).length > 0) return;
		var dialog = $('<div id="wkofs_'+context.script_id+'" class="wkof_settings" style="display:none;"></div>');
		dialog.html(config_to_html(context));
		revert_settings = {};

		// We need something to appendTo that our CSS rules can anchor to, so
		// we can avoid overlapping with other instances of Jquery UI.
		if ($('#wkof_ds').length === 0)
			$('body').prepend('<div id="wkof_ds"></div>');

		var width = 500;
		if (window.innerWidth < 510) {
			width = 280;
			dialog.addClass('narrow');
		}
		dialog.dialog({
			title: context.title+' Settings',
			buttons: [
				{text:'Save',click:save_btn.bind(context,context)},
				{text:'Cancel',click:cancel_btn.bind(context,context)}
			],
			width: width,
			maxHeight: window.innerHeight,
			modal: false,
			autoOpen: false,
			appendTo: '#wkof_ds',
			resize: resize.bind(context,context),
			close: close.bind(context,context)
		});

		$('.wkof_stabs').tabs();
		dialog.dialog('open');
		$('#wkofs_'+context.script_id+' .setting').on('change', setting_changed.bind(null,context));

		//============
		function resize(context, event, ui){
			var dialog = $('#wkofs_'+context.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');
		}
	}

	//------------------------------
	// Open the settings dialog.
	//------------------------------
	function save_settings(context) {
		var script_id = (typeof context === 'string' ? context : context.script_id);
		var obj = wkof.settings[script_id];
		if (!obj) return Promise.resolve();
		return wkof.file_cache.save('wkof.settings.'+script_id, obj);
	}

	//------------------------------
	// Open the settings dialog.
	//------------------------------
	function load_settings(context) {
		var script_id = (typeof context === 'string' ? context : context.script_id);
		return wkof.file_cache.load('wkof.settings.'+script_id)
		.then(finish, finish.bind(null,{}));

		function finish(content) {
			wkof.settings[script_id] = content;
		}
	}

	//------------------------------
	// Save button handler.
	//------------------------------
	function save_btn(context) {
		// Make a list of the settings that changed.
		for (var name in revert_settings) {
			if (revert_settings[name] != wkof.settings[context.script_id][name])
				revert_settings[name] = wkof.settings[context.script_id][name];
			else
				delete revert_settings[name];
		}
		if (context.autosave) save_settings(context);
		if (typeof context.self.on_save === 'function') context.self.on_save(revert_settings);
		wkof.trigger('wkof.settings.save');
		var dialog = $('#wkofs_'+context.script_id);
		context.keep_settings = true;
		dialog.dialog('close');
	}

	//------------------------------
	// Cancel button handler.
	//------------------------------
	function cancel_btn(context) {
		var dialog = $('#wkofs_'+context.script_id);
		dialog.dialog('close');
		if (typeof context.self.on_cancel === 'function') context.self.on_cancel();
	}

	//------------------------------
	// Close and destroy the dialog.
	//------------------------------
	function close(context) {
		var dialog = $('#wkofs_'+context.script_id);
		if (!context.keep_settings) {
			// Revert settings
			Object.assign(wkof.settings[context.script_id], revert_settings);
			for (var name in revert_settings) {
				var config = context.config_list[name];
				var elem = document.querySelector('#wkofs_'+context.script_id+' [name="location"]');
				var value = revert_settings[name];
				if (typeof config.on_change === 'function') config.on_change.call(elem, value, config);
			}
		}
		delete context.keep_settings;
		dialog.dialog('destroy');
		if (typeof context.self.on_close === 'function') context.self.on_close();
	}

	//------------------------------
	// Handler for live settings changes.  Handles built-in validation and user callbacks.
	//------------------------------
	function setting_changed(context, event) {
		var elem = $(event.target);
		var name = elem.attr('name');
		var config = context.config_list[name];

		// Extract the value
		var value;
		switch (config.type) {
			case 'dropdown': 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;
		}

		if (typeof config.on_change === 'function') config.on_change.call(event.target, value, config);

		// Validation
		var valid = {valid:true, msg:''};
		if (typeof config.validate === 'function') valid = config.validate.call(event.target, value, config);
		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 (config.type) {
			case 'number':
				if (typeof config.min === 'number' && Number(value) < config.min) {
					valid.valid = false;
					if (valid.msg.length === 0) {
						if (typeof config.max === 'number')
							valid.msg = 'Must be between '+config.min+' and '+config.max;
						else
							valid.msg = 'Must be '+config.min+' or higher';
					}
				} else if (typeof config.max === 'number' && Number(value) > config.max) {
					valid.valid = false;
					if (valid.msg.length === 0) {
						if (typeof config.min === 'number')
							valid.msg = 'Must be between '+config.min+' and '+config.max;
						else
							valid.msg = 'Must be '+config.max+' or lower';
					}
				}
				if (!valid)
				break;

			case 'text':
				if (config.match !== undefined && value.match(config.match) === null) {
					valid.valid = false;
					if (valid.msg.length === 0) valid.msg = 'Invalid value'
				}
				break;
		}

		// Style for valid/invalid
		var parent = elem.closest('.setting_row');
		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');
		}

		if (!(name in revert_settings)) revert_settings[name] = wkof.settings[context.script_id][name];
		wkof.settings[context.script_id][name] = value;
	}

	//------------------------------
	// Load jquery UI and the appropriate CSS based on location.
	//------------------------------
	var css_url;
	if (location.hostname.match(/^(www\.)?wanikani\.com$/) !== null)
		css_url = 'https://raw.githubusercontent.com/rfindley/wanikani-open-framework/master/jqui-wkmain.css';

	Promise.all([
		wkof.load_script('https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js', true /* cache */),
		wkof.load_css(css_url, true /* cache */)
	])
	.then(function(data){
		ready = true;

		// 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);