Wanikani Open Framework - Settings module

Settings module for Wanikani Open Framework

Version vom 06.03.2018. Aktuellste Version

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/38576/256577/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.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);