Greasy Fork is available in English.

Wanikani Open Framework - Settings module

Settings module for Wanikani Open Framework

20.04.2018 itibariyledir. En son verisyonu görün.

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