Wanikani Open Framework - Settings module

Settings module for Wanikani Open Framework

目前為 2018-03-06 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/38576/256577/Wanikani%20Open%20Framework%20-%20Settings%20module.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);