Wanikani Open Framework - Settings module

Settings module for Wanikani Open Framework

当前为 2022-03-17 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/38576/1029391/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.15
  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. if (!config.content) config.content = config.settings;
  24.  
  25. if (publish_context) this.context = context;
  26.  
  27. // Create public methods bound to context.
  28. this.cancel = cancel_btn.bind(context, context);
  29. this.open = open.bind(context, context);
  30. this.close = close.bind(context, context);
  31. this.load = load_settings.bind(context, context);
  32. this.save = save_settings.bind(context, context);
  33. this.refresh = refresh.bind(context, context);
  34. this.background = Settings.background;
  35. };
  36.  
  37. global.wkof.Settings = Settings;
  38. Settings.save = save_settings;
  39. Settings.load = load_settings;
  40. Settings.background = {
  41. open: open_background,
  42. close: close_background,
  43. }
  44. //########################################################################
  45.  
  46. wkof.settings = {};
  47. var ready = false;
  48. var hotkey_elem;
  49.  
  50. //------------------------------
  51. // Convert a config object to html dialog.
  52. //------------------------------
  53. function config_to_html(context) {
  54. context.config_list = {};
  55. var base = wkof.settings[context.cfg.script_id];
  56. if (base === undefined) wkof.settings[context.cfg.script_id] = base = {};
  57.  
  58. var html = '', item, child_passback = {};
  59. var id = context.cfg.script_id+'_dialog';
  60. for (var name in context.cfg.content) {
  61. var item = context.cfg.content[name];
  62. html += parse_item(name, context.cfg.content[name], child_passback);
  63. }
  64. if (child_passback.tabs)
  65. html = assemble_pages(id, child_passback.tabs, child_passback.pages) + html;
  66. return '<form>'+html+'</form>';
  67.  
  68. //============
  69. function parse_item(name, item, passback) {
  70. if (typeof item.type !== 'string') return '';
  71. var id = context.cfg.script_id+'_'+name;
  72. var cname, html = '', value, child_passback, non_page = '';
  73. switch (item.type) {
  74. case 'tabset':
  75. child_passback = {};
  76. for (cname in item.content)
  77. non_page += parse_item(cname, item.content[cname], child_passback);
  78. if (child_passback.tabs)
  79. html = assemble_pages(id, child_passback.tabs, child_passback.pages);
  80. break;
  81.  
  82. case 'page':
  83. if (typeof item.content !== 'object') item.content = {};
  84. if (!passback.tabs) {
  85. passback.tabs = [];
  86. passback.pages = [];
  87. }
  88. passback.tabs.push('<li id="'+id+'_tab"'+to_title(item.hover_tip)+'><a href="#'+id+'">'+item.label+'</a></li>');
  89. child_passback = {};
  90. for (cname in item.content)
  91. non_page += parse_item(cname, item.content[cname], child_passback);
  92. if (child_passback.tabs)
  93. html = assemble_pages(id, child_passback.tabs, child_passback.pages);
  94. passback.pages.push('<div id="'+id+'">'+html+non_page+'</div>');
  95. passback.is_page = true;
  96. html = '';
  97. break;
  98.  
  99. case 'group':
  100. if (typeof item.content !== 'object') item.content = {};
  101. child_passback = {};
  102. for (cname in item.content)
  103. non_page += parse_item(cname, item.content[cname], child_passback);
  104. if (child_passback.tabs)
  105. html = assemble_pages(id, child_passback.tabs, child_passback.pages);
  106. html = '<fieldset id="'+id+'" class="wkof_group"><legend>'+item.label+'</legend>'+html+non_page+'</fieldset>';
  107. break;
  108.  
  109. case 'dropdown':
  110. case 'list':
  111. var classes = 'setting', attribs = '';
  112. context.config_list[name] = item;
  113. value = get_value(context, base, name);
  114. if (value === undefined) {
  115. if (item.default !== undefined) {
  116. value = item.default;
  117. } else {
  118. if (item.multi === true) {
  119. value = {};
  120. Object.keys(item.content).forEach(function(key){
  121. value[key] = false;
  122. });
  123. } else {
  124. value = Object.keys(item.content)[0];
  125. }
  126. }
  127. set_value(context, base, name, value);
  128. }
  129. if (item.type === 'list') {
  130. classes += ' list';
  131. attribs += ' size="'+(item.size || Object.keys(item.content).length || 4)+'"';
  132. if (item.multi === true) attribs += ' multiple';
  133. }
  134. html = '<select id="'+id+'" name="'+name+'" class="'+classes+'"'+attribs+to_title(item.hover_tip)+'>';
  135. for (cname in item.content)
  136. html += '<option name="'+cname+'">'+escape_text(item.content[cname])+'</option>';
  137. html += '</select>';
  138. html = make_label(item) + wrap_right(html);
  139. html = wrap_row(html, item.full_width, item.hover_tip);
  140. break;
  141.  
  142. case 'checkbox':
  143. context.config_list[name] = item;
  144. html = make_label(item);
  145. value = get_value(context, base, name);
  146. if (value === undefined) {
  147. value = (item.default || false);
  148. set_value(context, base, name, value);
  149. }
  150. html += wrap_right('<input id="'+id+'" class="setting" type="checkbox" name="'+name+'">');
  151. html = wrap_row(html, item.full_width, item.hover_tip);
  152. break;
  153.  
  154. case 'input':
  155. case 'number':
  156. case 'text':
  157. var itype = item.type;
  158. if (itype === 'input') itype = item.subtype || 'text';
  159. context.config_list[name] = item;
  160. html += make_label(item);
  161. value = get_value(context, base, name);
  162. if (value === undefined) {
  163. var is_number = (item.type==='number' || item.subtype==='number');
  164. value = (item.default || (is_number==='number'?0:''));
  165. set_value(context, base, name, value);
  166. }
  167. html += wrap_right('<input id="'+id+'" class="setting" type="'+itype+'" name="'+name+'"'+(item.placeholder?' placeholder="'+escape_attr(item.placeholder)+'"':'')+'>');
  168. html = wrap_row(html, item.full_width, item.hover_tip);
  169. break;
  170.  
  171. case 'color':
  172. context.config_list[name] = item;
  173. html += make_label(item);
  174. value = get_value(context, base, name);
  175. if (value === undefined) {
  176. value = (item.default || '#000000');
  177. set_value(context, base, name, value);
  178. }
  179. html += wrap_right('<input id="'+id+'" class="setting" type="color" name="'+name+'">');
  180. html = wrap_row(html, item.full_width, item.hover_tip);
  181. break;
  182.  
  183. case 'button':
  184. context.config_list[name] = item;
  185. html += make_label(item);
  186. var text = escape_text(item.text || 'Click');
  187. html += wrap_right('<button type="button" class="setting" name="'+name+'">'+text+'</button>');
  188. html = wrap_row(html, item.full_width, item.hover_tip);
  189. break;
  190.  
  191. case 'divider':
  192. html += '<hr>';
  193. break;
  194.  
  195. case 'section':
  196. html += '<section>'+(item.label || '')+'</section>';
  197. break;
  198.  
  199. case 'html':
  200. html += make_label(item);
  201. html += item.html;
  202. switch (item.wrapper) {
  203. case 'row': html = wrap_row(html, null, item.hover_tip); break;
  204. case 'left': html = wrap_left(html); break;
  205. case 'right': html = wrap_right(html); break;
  206. }
  207. break;
  208. }
  209. return html;
  210.  
  211. function make_label(item) {
  212. if (typeof item.label !== 'string') return '';
  213. return wrap_left('<label for="'+id+'">'+item.label+'</label>');
  214. }
  215. }
  216.  
  217. //============
  218. function assemble_pages(id, tabs, pages) {return '<div id="'+id+'" class="wkof_stabs"><ul>'+tabs.join('')+'</ul>'+pages.join('')+'</div>';}
  219. function wrap_row(html,full,hover_tip) {return '<div class="row'+(full?' full':'')+'"'+to_title(hover_tip)+'>'+html+'</div>';}
  220. function wrap_left(html) {return '<div class="left">'+html+'</div>';}
  221. function wrap_right(html) {return '<div class="right">'+html+'</div>';}
  222. function escape_text(text) {return text.replace(/[<>]/g, function(ch) {var map={'<':'&lt','>':'&gt;'}; return map[ch];});}
  223. function escape_attr(text) {return text.replace(/"/g, '&quot;');}
  224. function to_title(tip) {if (!tip) return ''; return ' title="'+tip.replace(/"/g,'&quot;')+'"';}
  225. }
  226.  
  227. //------------------------------
  228. // Open the settings dialog.
  229. //------------------------------
  230. function open(context) {
  231. if (!ready) return;
  232. if ($('#wkofs_'+context.cfg.script_id).length > 0) return;
  233. install_anchor();
  234. if (context.cfg.background !== false) open_background();
  235. var dialog = $('<div id="wkofs_'+context.cfg.script_id+'" class="wkof_settings" style="display:none;"></div>');
  236. dialog.html(config_to_html(context));
  237.  
  238. var width = 500;
  239. if (window.innerWidth < 510) {
  240. width = 280;
  241. dialog.addClass('narrow');
  242. }
  243. dialog.dialog({
  244. title: context.cfg.title,
  245. buttons: [
  246. {text:'Save',click:save_btn.bind(context,context)},
  247. {text:'Cancel',click:cancel_btn.bind(context,context)}
  248. ],
  249. width: width,
  250. maxHeight: document.body.clientHeight,
  251. modal: false,
  252. autoOpen: false,
  253. appendTo: '#wkof_ds',
  254. resize: resize.bind(context,context),
  255. close: close.bind(context,context)
  256. });
  257. $(dialog.dialog('widget')).css('position','fixed');
  258.  
  259. $('.wkof_stabs').tabs({activate:tab_activated.bind(null,context)});
  260. var settings = wkof.settings[context.cfg.script_id];
  261. if (settings && settings.wkofs_active_tabs instanceof Array) {
  262. var active_tabs = settings.wkofs_active_tabs;
  263. for (var tab_idx = 0; tab_idx < active_tabs.length; tab_idx++) {
  264. var tab = $(active_tabs[tab_idx]);
  265. tab.closest('.ui-tabs').tabs({active:tab.index()});
  266. }
  267. }
  268.  
  269. dialog.dialog('open');
  270. var dialog_elem = $('#wkofs_'+context.cfg.script_id);
  271. dialog_elem.find('.setting[multiple]').on('mousedown', toggle_multi.bind(null,context));
  272. dialog_elem.find('.setting').on('change', setting_changed.bind(null,context));
  273. dialog_elem.find('form').on('submit', function(){return false;});
  274. dialog_elem.find('button.setting').on('click', setting_button_clicked.bind(null,context));
  275.  
  276. if (typeof context.cfg.pre_open === 'function') context.cfg.pre_open(dialog);
  277. context.reversions = $.extend(true,{},wkof.settings[context.cfg.script_id]);
  278. refresh(context);
  279.  
  280. // Enable workaround that blocks WK's hotkey handler.
  281. hotkey_elem = $('#reviews, #quiz').prop('id');
  282. if (hotkey_elem) $('#'+hotkey_elem).prop('id',hotkey_elem+'_backup');
  283.  
  284. //============
  285. function tab_activated(context, event, ui) {
  286. var dialog = $('#wkofs_'+context.cfg.script_id);
  287. var wrapper = $(dialog.dialog('widget'));
  288. if (wrapper.outerHeight() + wrapper.position().top > document.body.clientHeight) {
  289. dialog.dialog('option', 'maxHeight', document.body.clientHeight);
  290. }
  291. }
  292.  
  293. function resize(context, event, ui){
  294. var dialog = $('#wkofs_'+context.cfg.script_id);
  295. var is_narrow = dialog.hasClass('narrow');
  296. if (is_narrow && ui.size.width >= 510)
  297. dialog.removeClass('narrow');
  298. else if (!is_narrow && ui.size.width < 490)
  299. dialog.addClass('narrow');
  300. }
  301.  
  302. function toggle_multi(context, e) {
  303. if (e.button != 0) return true;
  304. var multi = $(e.currentTarget);
  305. var scroll = e.currentTarget.scrollTop;
  306. e.target.selected = !e.target.selected;
  307. setTimeout(function(){
  308. e.currentTarget.scrollTop = scroll;
  309. multi.focus();
  310. },0);
  311. return setting_changed(context, e);
  312. }
  313.  
  314. function setting_button_clicked(context, e) {
  315. var name = e.target.attributes.name.value;
  316. var item = context.config_list[name];
  317. if (typeof item.on_click === 'function')
  318. item.on_click.call(e, name, item, setting_changed.bind(context, context, e));
  319. }
  320. }
  321.  
  322. //------------------------------
  323. // Open the settings dialog.
  324. //------------------------------
  325. function save_settings(context) {
  326. var script_id = (typeof context === 'string' ? context : context.cfg.script_id);
  327. var settings = wkof.settings[script_id];
  328. if (!settings) return Promise.resolve();
  329. return wkof.file_cache.save('wkof.settings.'+script_id, settings);
  330. }
  331.  
  332. //------------------------------
  333. // Open the settings dialog.
  334. //------------------------------
  335. function load_settings(context, defaults) {
  336. var script_id = (typeof context === 'string' ? context : context.cfg.script_id);
  337. return wkof.file_cache.load('wkof.settings.'+script_id)
  338. .then(finish, finish.bind(null,{}));
  339.  
  340. function finish(settings) {
  341. if (defaults)
  342. wkof.settings[script_id] = $.extend(true, {}, defaults, settings);
  343. else
  344. wkof.settings[script_id] = settings;
  345. return wkof.settings[script_id];
  346. }
  347. }
  348.  
  349. //------------------------------
  350. // Save button handler.
  351. //------------------------------
  352. function save_btn(context, e) {
  353. var script_id = context.cfg.script_id;
  354. var dialog = $('#wkofs_'+script_id);
  355. var settings = wkof.settings[script_id];
  356. if (settings) {
  357. var active_tabs = dialog.find('.ui-tabs-active').toArray().map(function(tab){return '#'+tab.attributes.id.value});
  358. if (active_tabs.length > 0) settings.wkofs_active_tabs = active_tabs;
  359. }
  360. if (context.cfg.autosave === undefined || context.cfg.autosave === true) save_settings(context);
  361. if (typeof context.cfg.on_save === 'function') context.cfg.on_save(wkof.settings[context.cfg.script_id]);
  362. wkof.trigger('wkof.settings.save');
  363. context.keep_settings = true;
  364. dialog.dialog('close');
  365. }
  366.  
  367. //------------------------------
  368. // Cancel button handler.
  369. //------------------------------
  370. function cancel_btn(context) {
  371. var dialog = $('#wkofs_'+context.cfg.script_id);
  372. dialog.dialog('close');
  373. if (typeof context.cfg.on_cancel === 'function') context.cfg.on_cancel(wkof.settings[context.cfg.script_id]);
  374. }
  375.  
  376. //------------------------------
  377. // Close and destroy the dialog.
  378. //------------------------------
  379. function close(context, keep_settings) {
  380. var dialog = $('#wkofs_'+context.cfg.script_id);
  381. if (!context.keep_settings && keep_settings !== true) {
  382. // Revert settings
  383. wkof.settings[context.cfg.script_id] = $.extend(true,{},context.reversions);
  384. delete context.reversions;
  385. }
  386. delete context.keep_settings;
  387. dialog.dialog('destroy');
  388. if (context.cfg.background !== false) close_background();
  389. if (typeof context.cfg.on_close === 'function') context.cfg.on_close(wkof.settings[context.cfg.script_id]);
  390.  
  391. // Disable workaround that blocks WK's hotkey handler.
  392. if (hotkey_elem) $('#'+hotkey_elem+'_backup').prop('id',hotkey_elem);
  393. hotkey_elem = undefined;
  394. }
  395.  
  396. //------------------------------
  397. // Update the dialog to reflect changed settings.
  398. //------------------------------
  399. function refresh(context) {
  400. var script_id = context.cfg.script_id;
  401. var settings = wkof.settings[script_id];
  402. var dialog = $('#wkofs_'+script_id);
  403. for (var name in context.config_list) {
  404. var elem = dialog.find('#'+script_id+'_'+name);
  405. var config = context.config_list[name];
  406. var value = get_value(context, settings, name);
  407. switch (config.type) {
  408. case 'dropdown':
  409. case 'list':
  410. if (config.multi === true) {
  411. elem.find('option').each(function(i,e){
  412. var opt_name = e.getAttribute('name') || '#'+e.index;
  413. e.selected = value[opt_name];
  414. });
  415. } else {
  416. elem.find('option[name="'+value+'"]').prop('selected', true);
  417. }
  418. break;
  419.  
  420. case 'checkbox':
  421. elem.prop('checked', value);
  422. break;
  423.  
  424. default:
  425. elem.val(value);
  426. break;
  427. }
  428. }
  429. if (typeof context.cfg.on_refresh === 'function') context.cfg.on_refresh(wkof.settings[context.cfg.script_id]);
  430. }
  431.  
  432. //------------------------------
  433. // Handler for live settings changes. Handles built-in validation and user callbacks.
  434. //------------------------------
  435. function setting_changed(context, event) {
  436. var elem = $(event.currentTarget);
  437. var name = elem.attr('name');
  438. var item = context.config_list[name];
  439. var config;
  440.  
  441. // Extract the value
  442. var value;
  443. var itype = ((item.type==='input' && item.subtype==='number') ? 'number' : item.type);
  444. switch (itype) {
  445. case 'dropdown':
  446. case 'list':
  447. if (item.multi === true) {
  448. value = {};
  449. elem.find('option').each(function(i,e){
  450. var opt_name = e.getAttribute('name') || '#'+e.index;
  451. value[opt_name] = e.selected;
  452. });
  453. } else {
  454. value = elem.find(':checked').attr('name');
  455. }
  456. break;
  457. case 'checkbox': value = elem.is(':checked'); break;
  458. case 'number': value = Number(elem.val()); break;
  459. default: value = elem.val(); break;
  460. }
  461.  
  462. // Validation
  463. var valid = {valid:true, msg:''};
  464. if (typeof item.validate === 'function') valid = item.validate.call(event.target, value, item);
  465. if (typeof valid === 'boolean')
  466. valid = {valid:valid, msg:''};
  467. else if (typeof valid === 'string')
  468. valid = {valid:false, msg:valid};
  469. else if (valid === undefined)
  470. valid = {valid:true, msg:''};
  471. switch (itype) {
  472. case 'number':
  473. if (typeof item.min === 'number' && Number(value) < item.min) {
  474. valid.valid = false;
  475. if (valid.msg.length === 0) {
  476. if (typeof item.max === 'number')
  477. valid.msg = 'Must be between '+item.min+' and '+item.max;
  478. else
  479. valid.msg = 'Must be '+item.min+' or higher';
  480. }
  481. } else if (typeof item.max === 'number' && Number(value) > item.max) {
  482. valid.valid = false;
  483. if (valid.msg.length === 0) {
  484. if (typeof item.min === 'number')
  485. valid.msg = 'Must be between '+item.min+' and '+item.max;
  486. else
  487. valid.msg = 'Must be '+item.max+' or lower';
  488. }
  489. }
  490. if (!valid)
  491. break;
  492.  
  493. case 'text':
  494. if (item.match !== undefined && value.match(item.match) === null) {
  495. valid.valid = false;
  496. if (valid.msg.length === 0)
  497. valid.msg = item.error_msg || 'Invalid value';
  498. }
  499. break;
  500. }
  501.  
  502. // Style for valid/invalid
  503. var parent = elem.closest('.right');
  504. parent.find('.note').remove();
  505. if (typeof valid.msg === 'string' && valid.msg.length > 0)
  506. parent.append('<div class="note'+(valid.valid?'':' error')+'">'+valid.msg+'</div>');
  507. if (!valid.valid) {
  508. elem.addClass('invalid');
  509. } else {
  510. elem.removeClass('invalid');
  511. }
  512.  
  513. var script_id = context.cfg.script_id;
  514. var settings = wkof.settings[script_id];
  515. if (valid.valid) {
  516. if (item.no_save !== true) set_value(context, settings, name, value);
  517. if (typeof item.on_change === 'function') item.on_change.call(event.target, name, value, item);
  518. if (typeof context.cfg.on_change === 'function') context.cfg.on_change.call(event.target, name, value, item);
  519. if (item.refresh_on_change === true) refresh(context);
  520. }
  521.  
  522. return false;
  523. }
  524.  
  525. function get_value(context, base, name){
  526. var item = context.config_list[name];
  527. var evaluate = (item.path !== undefined);
  528. var path = (item.path || name);
  529. try {
  530. if (!evaluate) return base[path];
  531. return eval(path.replace(/@/g,'base.'));
  532. } catch(e) {return;}
  533. }
  534.  
  535. function set_value(context, base, name, value) {
  536. var item = context.config_list[name];
  537. var evaluate = (item.path !== undefined);
  538. var path = (item.path || name);
  539. try {
  540. if (!evaluate) return base[path] = value;
  541. var depth=0, new_path='', param, c;
  542. for (var idx = 0; idx < path.length; idx++) {
  543. c = path[idx];
  544. if (c === '[') {
  545. if (depth++ === 0) {
  546. new_path += '[';
  547. param = '';
  548. } else {
  549. param += '[';
  550. }
  551. } else if (c === ']') {
  552. if (--depth === 0) {
  553. new_path += JSON.stringify(eval(param)) + ']';
  554. } else {
  555. param += ']';
  556. }
  557. } else {
  558. if (c === '@') c = 'base.';
  559. if (depth === 0)
  560. new_path += c;
  561. else
  562. param += c;
  563. }
  564. }
  565. eval(new_path + '=value');
  566. } catch(e) {return;}
  567. }
  568.  
  569. function install_anchor() {
  570. var anchor = $('#wkof_ds');
  571. if (anchor.length === 0) {
  572. anchor = $('<div id="wkof_ds"></div></div>');
  573. $('body').prepend(anchor);
  574. }
  575. return anchor;
  576. }
  577.  
  578. function open_background() {
  579. var anchor = install_anchor();
  580. var bkgd = anchor.find('> #wkofs_bkgd');
  581. if (bkgd.length === 0) {
  582. bkgd = $('<div id="wkofs_bkgd" refcnt="0"></div>');
  583. anchor.prepend(bkgd);
  584. }
  585. var refcnt = Number(bkgd.attr('refcnt'));
  586. bkgd.attr('refcnt', refcnt + 1);
  587. }
  588.  
  589. function close_background() {
  590. var bkgd = $('#wkof_ds > #wkofs_bkgd');
  591. if (bkgd.length === 0) return;
  592. var refcnt = Number(bkgd.attr('refcnt'));
  593. if (refcnt <= 0) return;
  594. bkgd.attr('refcnt', refcnt - 1);
  595. }
  596.  
  597. //------------------------------
  598. // Load jquery UI and the appropriate CSS based on location.
  599. //------------------------------
  600. var css_url = wkof.support_files['jqui_wkmain.css'];
  601.  
  602. wkof.ready('document')
  603. .then(function(){
  604. return Promise.all([
  605. wkof.load_script(wkof.support_files['jquery_ui.js'], true /* cache */),
  606. wkof.load_css(css_url, true /* cache */)
  607. ]);
  608. })
  609. .then(function(data){
  610. ready = true;
  611.  
  612. // Workaround... https://community.wanikani.com/t/19984/55
  613. delete $.fn.autocomplete;
  614.  
  615. // Notify listeners that we are ready.
  616. // Delay guarantees include() callbacks are called before ready() callbacks.
  617. setTimeout(function(){wkof.set_state('wkof.Settings', 'ready');},0);
  618. });
  619.  
  620. })(this);