Wanikani Open Framework - Settings module

Settings module for Wanikani Open Framework

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

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

  1. // ==UserScript==
  2. // @name Wanikani Open Framework - Settings module
  3. // @namespace rfindley
  4. // @description Settings module for Wanikani Open Framework
  5. // @version 1.0.2
  6. // @copyright 2018+, Robin Findley
  7. // @license MIT; http://opensource.org/licenses/MIT
  8. // ==/UserScript==
  9.  
  10. (function(global) {
  11.  
  12. const publish_context = false; // Set to 'true' to make context public.
  13.  
  14. //########################################################################
  15. //------------------------------
  16. // Constructor
  17. //------------------------------
  18. function Settings(config) {
  19. var context = {
  20. self: this,
  21. cfg: config,
  22. }
  23.  
  24. if (publish_context) this.context = context;
  25.  
  26. // Create public methods bound to context.
  27. this.cancel = cancel_btn.bind(context, context);
  28. this.open = open.bind(context, context);
  29. this.load = load_settings.bind(context, context);
  30. this.save = save_settings.bind(context, context);
  31. this.refresh = refresh.bind(context, context);
  32. this.background = Settings.background;
  33. };
  34.  
  35. global.wkof.Settings = Settings;
  36. Settings.save = save_settings;
  37. Settings.load = load_settings;
  38. Settings.background = {
  39. open: open_background,
  40. close: close_background,
  41. }
  42. //########################################################################
  43.  
  44. wkof.settings = {};
  45. var ready = false;
  46.  
  47. //------------------------------
  48. // Convert a config object to html dialog.
  49. //------------------------------
  50. function config_to_html(context) {
  51. context.config_list = {};
  52. var base = wkof.settings[context.cfg.script_id];
  53. if (base === undefined) wkof.settings[context.cfg.script_id] = base = {};
  54.  
  55. var html = '', item, child_passback = {};
  56. for (var name in context.cfg.settings) {
  57. var item = context.cfg.settings[name];
  58. html += parse_item(name, context.cfg.settings[name], child_passback);
  59. }
  60. if (child_passback.tabs)
  61. html = assemble_pages(child_passback.tabs, child_passback.pages) + html;
  62. return '<form>'+html+'</form>';
  63.  
  64. //============
  65. function parse_item(name, item, passback) {
  66. if (typeof item.type !== 'string') return '';
  67. var id = context.cfg.script_id+'_'+name;
  68. var cname, html = '', value, child_passback;
  69. switch (item.type) {
  70. case 'page':
  71. if (typeof item.content !== 'object') item.content = {};
  72. if (!passback.tabs) {
  73. passback.tabs = [];
  74. passback.pages = [];
  75. }
  76. passback.tabs.push('<li id="'+id+'_tab"><a href="#'+id+'">'+item.label+'</a></li>');
  77. child_passback = {};
  78. var non_page = '';
  79. for (cname in item.content)
  80. non_page += parse_item(cname, item.content[cname], child_passback);
  81. if (child_passback.tabs)
  82. html = assemble_pages(child_passback.tabs, child_passback.pages);
  83. passback.pages.push('<div id="'+id+'">'+html+non_page+'</div>');
  84. passback.is_page = true;
  85. html = '';
  86. break;
  87.  
  88. case 'group':
  89. if (typeof item.content !== 'object') item.content = {};
  90. for (cname in item.content)
  91. html += parse_item(cname, item.content[cname]);
  92. html = '<fieldset id="'+id+'" class="wkof_group"><legend>'+item.label+'</legend>'+html+'</fieldset>';
  93. break;
  94.  
  95. case 'dropdown':
  96. case 'list':
  97. var classes = 'setting', attribs = '';
  98. context.config_list[name] = item;
  99. value = get_value(context, base, name);
  100. if (value === undefined) {
  101. if (item.default !== undefined) {
  102. value = item.default;
  103. } else {
  104. if (item.multi === true) {
  105. value = {};
  106. Object.keys(item.content).forEach(function(key){
  107. value[key] = false;
  108. });
  109. } else {
  110. value = Object.keys(item.content)[0];
  111. }
  112. }
  113. set_value(context, base, name, value);
  114. }
  115. if (item.type === 'list') {
  116. classes += ' list';
  117. attribs += ' size="'+(item.size || Object.keys(item.content).length || 4)+'"';
  118. if (item.multi === true) attribs += ' multiple';
  119. }
  120. html = '<select id="'+id+'" name="'+name+'" class="'+classes+'"'+attribs+'>';
  121. for (cname in item.content)
  122. html += '<option name="'+cname+'">'+escape(item.content[cname])+'</option>';
  123. html += '</select>';
  124. html = make_label(item) + wrap_right(html);
  125. html = wrap_row(html, item.full_width);
  126. break;
  127.  
  128. case 'checkbox':
  129. context.config_list[name] = item;
  130. html = make_label(item);
  131. value = get_value(context, base, name);
  132. if (value === undefined) {
  133. value = (item.default || false);
  134. set_value(context, base, name, value);
  135. }
  136. html += wrap_right('<input id="'+id+'" class="setting" type="checkbox" name="'+name+'">');
  137. html = wrap_row(html, item.full_width);
  138. break;
  139.  
  140. case 'input':
  141. case 'number':
  142. case 'text':
  143. var itype = item.type;
  144. if (itype === 'input') itype = item.subtype || 'text';
  145. context.config_list[name] = item;
  146. html += make_label(item);
  147. value = get_value(context, base, name);
  148. if (value === undefined) {
  149. var is_number = (item.type==='number' || item.subtype==='number');
  150. value = (item.default || (is_number==='number'?'0':''));
  151. set_value(context, base, name, value);
  152. }
  153. html += wrap_right('<input id="'+id+'" class="setting" type="'+itype+'" name="'+name+'">');
  154. html = wrap_row(html, item.full_width);
  155. break;
  156.  
  157. case 'color':
  158. context.config_list[name] = item;
  159. html += make_label(item);
  160. value = get_value(context, base, name);
  161. if (value === undefined) {
  162. value = (item.default || '#000000');
  163. set_value(context, base, name, value);
  164. }
  165. html += wrap_right('<input id="'+id+'" class="setting" type="color" name="'+name+'">');
  166. html = wrap_row(html, item.full_width);
  167. break;
  168.  
  169. case 'divider':
  170. html += '<hr>';
  171. break;
  172.  
  173. case 'section':
  174. html += '<section>'+(item.label || '')+'</section>';
  175. break;
  176.  
  177. case 'html':
  178. html += make_label(item);
  179. html += item.html;
  180. switch (item.wrapper) {
  181. case 'row': html = wrap_row(html); break;
  182. case 'left': html = wrap_left(html); break;
  183. case 'right': html = wrap_right(html); break;
  184. }
  185. break;
  186. }
  187. return html;
  188.  
  189. function make_label(item) {
  190. if (typeof item.label !== 'string') return '';
  191. return wrap_left('<label for="'+id+'">'+item.label+'</label>');
  192. }
  193. }
  194.  
  195. //============
  196. function assemble_pages(tabs, pages) {return '<div class="wkof_stabs"><ul>'+tabs.join('')+'</ul>'+pages.join('')+'</div>';}
  197. function wrap_row(html,full) {return '<div class="row'+(full?' full':'')+'">'+html+'</div>';}
  198. function wrap_left(html) {return '<div class="left">'+html+'</div>';}
  199. function wrap_right(html) {return '<div class="right">'+html+'</div>';}
  200. function escape(text) {return text.replace('<','&lt;').replace('>','&gt;');}
  201. }
  202.  
  203. //------------------------------
  204. // Open the settings dialog.
  205. //------------------------------
  206. function open(context) {
  207. if (!ready) return;
  208. if ($('#wkofs_'+context.cfg.script_id).length > 0) return;
  209. install_anchor();
  210. if (context.cfg.background !== false) open_background();
  211. var dialog = $('<div id="wkofs_'+context.cfg.script_id+'" class="wkof_settings" style="display:none;"></div>');
  212. dialog.html(config_to_html(context));
  213.  
  214. var width = 500;
  215. if (window.innerWidth < 510) {
  216. width = 280;
  217. dialog.addClass('narrow');
  218. }
  219. dialog.dialog({
  220. title: context.cfg.title+' Settings',
  221. buttons: [
  222. {text:'Save',click:save_btn.bind(context,context)},
  223. {text:'Cancel',click:cancel_btn.bind(context,context)}
  224. ],
  225. width: width,
  226. maxHeight: window.innerHeight,
  227. modal: false,
  228. autoOpen: false,
  229. appendTo: '#wkof_ds',
  230. resize: resize.bind(context,context),
  231. close: close.bind(context,context)
  232. });
  233. dialog.closest('[role="dialog"]').css('position','fixed');
  234.  
  235. $('.wkof_stabs').tabs();
  236. dialog.dialog('open');
  237. $('#wkofs_'+context.cfg.script_id+' .setting[multiple]').on('mousedown', toggle_multi.bind(null,context));
  238. $('#wkofs_'+context.cfg.script_id+' .setting').on('change', setting_changed.bind(null,context));
  239. $('#wkofs_'+context.cfg.script_id+' form').on('submit', function(){return false;});
  240.  
  241. if (typeof context.cfg.pre_open === 'function') context.cfg.pre_open(dialog);
  242. context.reversions = $.extend(true,{},wkof.settings[context.cfg.script_id]);
  243. refresh(context);
  244.  
  245. //============
  246. function resize(context, event, ui){
  247. var dialog = $('#wkofs_'+context.cfg.script_id);
  248. var is_narrow = dialog.hasClass('narrow');
  249. if (is_narrow && ui.size.width >= 510)
  250. dialog.removeClass('narrow');
  251. else if (!is_narrow && ui.size.width < 490)
  252. dialog.addClass('narrow');
  253. }
  254.  
  255. function toggle_multi(context, e) {
  256. if (e.button != 0) return true;
  257. var multi = $(e.currentTarget);
  258. var scroll = e.currentTarget.scrollTop;
  259. e.target.selected = !e.target.selected;
  260. setTimeout(function(){
  261. e.currentTarget.scrollTop = scroll;
  262. multi.focus();
  263. },0);
  264. return setting_changed(context, e);
  265. }
  266. }
  267.  
  268. //------------------------------
  269. // Open the settings dialog.
  270. //------------------------------
  271. function save_settings(context) {
  272. var script_id = (typeof context === 'string' ? context : context.cfg.script_id);
  273. var settings = wkof.settings[script_id];
  274. if (!settings) return Promise.resolve();
  275. return wkof.file_cache.save('wkof.settings.'+script_id, settings);
  276. }
  277.  
  278. //------------------------------
  279. // Open the settings dialog.
  280. //------------------------------
  281. function load_settings(context) {
  282. var script_id = (typeof context === 'string' ? context : context.cfg.script_id);
  283. return wkof.file_cache.load('wkof.settings.'+script_id)
  284. .then(finish, finish.bind(null,{}));
  285.  
  286. function finish(settings) {
  287. wkof.settings[script_id] = settings;
  288. }
  289. }
  290.  
  291. //------------------------------
  292. // Save button handler.
  293. //------------------------------
  294. function save_btn(context, e) {
  295. if (context.cfg.autosave === undefined || context.cfg.autosave === true) save_settings(context);
  296. if (typeof context.cfg.on_save === 'function') context.cfg.on_save(wkof.settings[context.cfg.script_id]);
  297. wkof.trigger('wkof.settings.save');
  298. var dialog = $('#wkofs_'+context.cfg.script_id);
  299. context.keep_settings = true;
  300. dialog.dialog('close');
  301. }
  302.  
  303. //------------------------------
  304. // Cancel button handler.
  305. //------------------------------
  306. function cancel_btn(context) {
  307. var dialog = $('#wkofs_'+context.cfg.script_id);
  308. dialog.dialog('close');
  309. if (typeof context.cfg.on_cancel === 'function') context.cfg.on_cancel(wkof.settings[context.cfg.script_id]);
  310. }
  311.  
  312. //------------------------------
  313. // Close and destroy the dialog.
  314. //------------------------------
  315. function close(context) {
  316. var dialog = $('#wkofs_'+context.cfg.script_id);
  317. if (!context.keep_settings) {
  318. // Revert settings
  319. wkof.settings[context.cfg.script_id] = $.extend(true,{},context.reversions);
  320. delete context.reversions;
  321. }
  322. delete context.keep_settings;
  323. dialog.dialog('destroy');
  324. if (context.cfg.background !== false) close_background();
  325. if (typeof context.cfg.on_close === 'function') context.cfg.on_close(wkof.settings[context.cfg.script_id]);
  326. }
  327.  
  328. //------------------------------
  329. // Update the dialog to reflect changed settings.
  330. //------------------------------
  331. function refresh(context) {
  332. var script_id = context.cfg.script_id;
  333. var settings = wkof.settings[script_id];
  334. var dialog = $('#wkofs_'+script_id);
  335. for (var name in context.config_list) {
  336. var elem = dialog.find('#'+script_id+'_'+name);
  337. var config = context.config_list[name];
  338. var value = get_value(context, settings, name);
  339. switch (config.type) {
  340. case 'dropdown':
  341. case 'list':
  342. if (config.multi === true) {
  343. elem.find('option').each(function(i,e){
  344. var opt_name = e.getAttribute('name') || '#'+e.index;
  345. e.selected = value[opt_name];
  346. });
  347. } else {
  348. elem.find('option[name="'+value+'"]').prop('selected', true);
  349. }
  350. break;
  351.  
  352. case 'checkbox':
  353. elem.prop('checked', value);
  354. break;
  355.  
  356. default:
  357. elem.val(value);
  358. break;
  359. }
  360. }
  361. }
  362.  
  363. //------------------------------
  364. // Handler for live settings changes. Handles built-in validation and user callbacks.
  365. //------------------------------
  366. function setting_changed(context, event) {
  367. var elem = $(event.currentTarget);
  368. var name = elem.attr('name');
  369. var item = context.config_list[name];
  370. var config;
  371.  
  372. // Extract the value
  373. var value;
  374. var itype = ((item.type==='input' && item.subtype==='number') ? 'number' : item.type);
  375. switch (itype) {
  376. case 'dropdown':
  377. case 'list':
  378. if (item.multi === true) {
  379. value = {};
  380. elem.find('option').each(function(i,e){
  381. var opt_name = e.getAttribute('name') || '#'+e.index;
  382. value[opt_name] = e.selected;
  383. });
  384. } else {
  385. value = elem.find(':checked').attr('name');
  386. }
  387. break;
  388. case 'checkbox': value = elem.is(':checked'); break;
  389. case 'number': value = Number(elem.val()); break;
  390. default: value = elem.val(); break;
  391. }
  392.  
  393. // Validation
  394. var valid = {valid:true, msg:''};
  395. if (typeof item.validate === 'function') valid = item.validate.call(event.target, value, item);
  396. if (typeof valid === 'boolean')
  397. valid = {valid:valid, msg:''};
  398. else if (typeof valid === 'string')
  399. valid = {valid:false, msg:valid};
  400. else if (valid === undefined)
  401. valid = {valid:true, msg:''};
  402. switch (itype) {
  403. case 'number':
  404. if (typeof item.min === 'number' && Number(value) < item.min) {
  405. valid.valid = false;
  406. if (valid.msg.length === 0) {
  407. if (typeof item.max === 'number')
  408. valid.msg = 'Must be between '+item.min+' and '+item.max;
  409. else
  410. valid.msg = 'Must be '+item.min+' or higher';
  411. }
  412. } else if (typeof item.max === 'number' && Number(value) > item.max) {
  413. valid.valid = false;
  414. if (valid.msg.length === 0) {
  415. if (typeof item.min === 'number')
  416. valid.msg = 'Must be between '+item.min+' and '+item.max;
  417. else
  418. valid.msg = 'Must be '+item.max+' or lower';
  419. }
  420. }
  421. if (!valid)
  422. break;
  423.  
  424. case 'text':
  425. if (item.match !== undefined && value.match(item.match) === null) {
  426. valid.valid = false;
  427. if (valid.msg.length === 0)
  428. valid.msg = item.error_msg || 'Invalid value';
  429. }
  430. break;
  431. }
  432.  
  433. // Style for valid/invalid
  434. var parent = elem.closest('.row');
  435. parent.find('.note').remove();
  436. if (typeof valid.msg === 'string' && valid.msg.length > 0)
  437. parent.append('<div class="note'+(valid.valid?'':' error')+'">'+valid.msg+'</div>');
  438. if (!valid.valid) {
  439. elem.addClass('invalid');
  440. } else {
  441. elem.removeClass('invalid');
  442. }
  443.  
  444. var script_id = context.cfg.script_id;
  445. var settings = wkof.settings[script_id];
  446. if (valid.valid) {
  447. if (item.no_save !== true) set_value(context, settings, name, value);
  448. if (typeof item.on_change === 'function') item.on_change.call(event.target, name, value, item);
  449. if (item.refresh_on_change === true) refresh(context);
  450. }
  451.  
  452. return false;
  453. }
  454.  
  455. function get_value(context, base, name){
  456. var item = context.config_list[name];
  457. var evaluate = (item.path !== undefined);
  458. var path = (item.path || name);
  459. try {
  460. if (!evaluate) return base[path];
  461. return eval(path.replace(/@/g,'base.'));
  462. } catch(e) {return;}
  463. }
  464.  
  465. function set_value(context, base, name, value) {
  466. var item = context.config_list[name];
  467. var evaluate = (item.path !== undefined);
  468. var path = (item.path || name);
  469. try {
  470. if (!evaluate) return base[path] = value;
  471. var depth=0, new_path='', param, c;
  472. for (var idx = 0; idx < path.length; idx++) {
  473. c = path[idx];
  474. if (c === '[') {
  475. if (depth++ === 0) {
  476. new_path += '[';
  477. param = '';
  478. } else {
  479. param += '[';
  480. }
  481. } else if (c === ']') {
  482. if (--depth === 0) {
  483. new_path += JSON.stringify(eval(param)) + ']';
  484. } else {
  485. param += ']';
  486. }
  487. } else {
  488. if (c === '@') c = 'base.';
  489. if (depth === 0)
  490. new_path += c;
  491. else
  492. param += c;
  493. }
  494. }
  495. eval(new_path + '=value');
  496. } catch(e) {return;}
  497. }
  498.  
  499. function install_anchor() {
  500. var anchor = $('#wkof_ds');
  501. if (anchor.length === 0) {
  502. anchor = $('<div id="wkof_ds"></div></div>');
  503. $('body').prepend(anchor);
  504. }
  505. return anchor;
  506. }
  507.  
  508. function open_background() {
  509. var anchor = install_anchor();
  510. var bkgd = anchor.find('> #wkofs_bkgd');
  511. if (bkgd.length === 0) {
  512. bkgd = $('<div id="wkofs_bkgd" refcnt="0"></div>');
  513. anchor.prepend(bkgd);
  514. }
  515. var refcnt = Number(bkgd.attr('refcnt'));
  516. bkgd.attr('refcnt', refcnt + 1);
  517. }
  518.  
  519. function close_background() {
  520. var bkgd = $('#wkof_ds > #wkofs_bkgd');
  521. if (bkgd.length === 0) return;
  522. var refcnt = Number(bkgd.attr('refcnt'));
  523. if (refcnt <= 0) return;
  524. bkgd.attr('refcnt', refcnt - 1);
  525. }
  526.  
  527. //------------------------------
  528. // Load jquery UI and the appropriate CSS based on location.
  529. //------------------------------
  530. var css_url;
  531. if (location.hostname.match(/^(www\.)?wanikani\.com$/) !== null)
  532. css_url = 'https://raw.githubusercontent.com/rfindley/wanikani-open-framework/4ee8084163ffcf61d7a1009a655f7905ed145324/jqui-wkmain.css';
  533.  
  534. Promise.all([
  535. wkof.load_script('https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js', true /* cache */),
  536. wkof.load_css(css_url, true /* cache */)
  537. ])
  538. .then(function(data){
  539. ready = true;
  540.  
  541. // Notify listeners that we are ready.
  542. // Delay guarantees include() callbacks are called before ready() callbacks.
  543. setTimeout(function(){wkof.set_state('wkof.Settings', 'ready');},0);
  544. });
  545.  
  546. })(this);