Wanikani Open Framework - Settings module

Settings module for Wanikani Open Framework

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

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