Wanikani Self-Study Quiz

Quiz yourself on Wanikani items

目前为 2023-09-15 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Wanikani Self-Study Quiz
  3. // @namespace rfindley
  4. // @description Quiz yourself on Wanikani items
  5. // @version 3.1.5
  6. // @match https://www.wanikani.com/*
  7. // @exclude https://www.wanikani.com/extra_study/*
  8. // @exclude https://www.wanikani.com/lesson/*
  9. // @exclude https://www.wanikani.com/review/*
  10. // @match https://preview.wanikani.com/*
  11. // @exclude https://preview.wanikani.com/extra_study/*
  12. // @exclude https://preview.wanikani.com/lesson/*
  13. // @exclude https://preview.wanikani.com/review/*
  14. // @require https://unpkg.com/wanakana@4.0.2/umd/wanakana.min.js
  15. // @copyright 2022+, Robin Findley
  16. // @license MIT; http://opensource.org/licenses/MIT
  17. // @run-at document-end
  18. // @grant none
  19. // ==/UserScript==
  20.  
  21. window.ss_quiz = {};
  22.  
  23. (function(gobj) {
  24.  
  25. /* global $, wkof, wanakana */
  26. /* eslint no-multi-spaces: "off" */
  27.  
  28. //===================================================================
  29. // Initialization of the Wanikani Open Framework.
  30. //-------------------------------------------------------------------
  31. var script_name = 'Self-Study Quiz';
  32. var wkof_version_needed = '1.1.3';
  33. if (!window.wkof) {
  34. if (confirm(script_name+' requires Wanikani Open Framework.\nDo you want to be forwarded to the installation instructions?')) {
  35. window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
  36. }
  37. return;
  38. }
  39. if (wkof.version.compare_to(wkof_version_needed) === 'older') {
  40. if (confirm(script_name+' requires Wanikani Open Framework version '+wkof_version_needed+'.\nDo you want to be forwarded to the update page?')) {
  41. window.location.href = 'https://gf.qytechs.cn/en/scripts/38582-wanikani-open-framework';
  42. }
  43. return;
  44. }
  45.  
  46. wkof.include('Jquery,Menu');
  47. wkof.ready('Jquery,Menu').then(install_menu);
  48.  
  49. function install_menu() {
  50. wkof.Menu.insert_script_link({
  51. name: 'selfstudyquiz',
  52. submenu: 'Open',
  53. title: 'Self-Study Quiz',
  54. on_click: open_quiz
  55. });
  56. }
  57.  
  58. //########################################################################
  59. // QUIZ SETTINGS DIALOG
  60. //########################################################################
  61.  
  62. //========================================================================
  63. // setup_quiz_settings()
  64. //------------------------------------------------------------------------
  65. var quiz_settings_state = 'init';
  66. function setup_quiz_settings() {
  67. if (quiz_settings_state === 'init') {
  68. quiz_settings_state = 'loading';
  69. return wkof.ready('Settings')
  70. .then(function(){
  71. quiz_settings_state = 'setup';
  72. setup_quiz_settings();
  73. });
  74. }
  75. if (quiz_settings_state !== 'setup') return;
  76.  
  77. var config = {
  78. script_id: 'ss_quiz',
  79. title: 'Self-Study Quiz',
  80. pre_open: preopen_quiz_settings,
  81. on_save: save_quiz_settings,
  82. on_close: close_quiz_settings,
  83. on_refresh: refresh_quiz_settings,
  84. no_bkgd: true,
  85. settings: {
  86. pg_questions: {type:'page',label:'Questions',hover_tip:'Choose what quiz questions you want to be asked',content:{
  87. grp_qpre_list: {type:'group',label:'Presets List',content:{
  88. active_qpreset: {type:'list',refresh_on_change:true,hover_tip:'Question Presets',content:{}},
  89. }},
  90. grp_qpre: {type:'group',label:'Selected Preset',content:{
  91. sect_qpre_name: {type:'section',label:'Preset Name'},
  92. qpre_name: {type:'text',label:'Edit Preset Name',on_change:refresh_qpresets,path:'@qpresets[@active_qpreset].name',hover_tip:'Enter a name for the selected preset'},
  93.  
  94. sect_qpre_question: {type:'section',label:'Questions <span class="fa fa-circle-arrow-right icon-circle-arrow-right"></span> Answers'},
  95. char2mean: {type:'checkbox',label:'Rad/Kan/Voc <span class="fa fa-circle-arrow-right icon-circle-arrow-right"></span> Meaning',path:'@qpresets[@active_qpreset].content.char2mean',hover_tip:'Question: A radical or kanji character, or vocab word drawn with kanji\nAnswer: The meaning in English'},
  96. char2read: {type:'checkbox',label:'Kan/Voc <span class="fa fa-circle-arrow-right icon-circle-arrow-right"></span> Reading',path:'@qpresets[@active_qpreset].content.char2read',hover_tip:'Question: A kanji character, or vocab word drawn with kanji\nAnswer: The Japanese reading, in hiragana or katakana'},
  97. read2mean: {type:'checkbox',label:'Voc Reading <span class="fa fa-circle-arrow-right icon-circle-arrow-right"></span> Meaning',path:'@qpresets[@active_qpreset].content.read2mean',hover_tip:'Question: A kanji or vocab reading, in hiragana or katakana\nAnswer: The meaning in English'},
  98. mean2read: {type:'checkbox',label:'Voc Meaning <span class="fa fa-circle-arrow-right icon-circle-arrow-right"></span> Reading',path:'@qpresets[@active_qpreset].content.mean2read',hover_tip:'Question: A vocab word in English\nAnswer: The Japanese reading, in hiragana or katakana'},
  99. aud2mean: {type:'checkbox',label:'Voc Audio <span class="fa fa-circle-arrow-right icon-circle-arrow-right"></span> Meaning',path:'@qpresets[@active_qpreset].content.aud2mean',hover_tip:'Question: A vocab word, in spoken audio\nAnswer: The meaning in English'},
  100. aud2read: {type:'checkbox',label:'Voc Audio <span class="fa fa-circle-arrow-right icon-circle-arrow-right"></span> Reading',path:'@qpresets[@active_qpreset].content.aud2read',hover_tip:'Question: A vocab word, in spoken audio\nAnswer: The Japanese reading, in hiragana or katakana'},
  101. }},
  102. }},
  103. pg_items: {type:'page',label:'Items',hover_tip:'Choose what items you want to be quizzed on',content:{
  104. grp_ipre_list: {type:'group',label:'Presets List',content:{
  105. active_ipreset: {type:'list',refresh_on_change:true,hover_tip:'Item Presets',content:{}},
  106. }},
  107. grp_ipre: {type:'group',label:'Selected Preset',content:{
  108. sect_ipre_name: {type:'section',label:'Preset Name'},
  109. ipre_name: {type:'text',label:'Edit Preset Name',on_change:refresh_ipresets,path:'@ipresets[@active_ipreset].name',hover_tip:'Enter a name for the selected preset'},
  110.  
  111. sect_ipre_srcs: {type:'section',label:'Item Sources'},
  112. ipre_srcs: {type:'tabset',content:{}},
  113. }},
  114. }},
  115. pg_opts: {type:'page',label:'Settings',hover_tip:'Configure the user interface settings',content:{
  116. grp_quiz_size: {type:'group',label:'Quiz Size',content:{
  117. max_quiz_size: {type:'number',label:'Maximum Quiz Size',hover_tip:'Set the approximate maximum quiz size. (0 for unlimited)',default:0},
  118. }},
  119. grp_synonyms: {type:'group',label:'Synonyms',content:{
  120. synonyms_order: {type:'dropdown',label:'Synonym order in Help',hover_tip:'Set the order that synonyms appear in Help hints. (default: First)',default:'first',content:{first:'First',last:'Last'}},
  121. }},
  122. grp_typos: {type:'group',label:'Typo Tolerance',content:{
  123. allow_typos: {type:'checkbox',label:'Allow typos',hover_tip:'When enabled, English answers with minor typos will be accepted.',default:true},
  124. }},
  125. grp_help: {type:'group',label:'Wrong Answers',content:{
  126. autoshow_correct: {type:'checkbox',label:'Auto-show Correct Answer',hover_tip:'Automatically show the correct answer\nwhen you answer incorrectly.',default:false},
  127. }},
  128. grp_msgs: {type:'group',label:'Warning Messages',content:{
  129. show_slightly_off: {type:'checkbox',label:'Answer is slightly off',path:'@messages.show_slightly_off',hover_tip:'Tells you when your answer is slightly off',default:true},
  130. show_multi_reading: {type:'checkbox',label:'Has multiple readings',path:'@messages.show_multi_reading',hover_tip:'Tells you when an item has multiple readings',default:false},
  131. }},
  132. grp_halt: {type:'group',label:'Override Lightning',content:{
  133. halt_slightly_off: {type:'checkbox',label:'Halt if slightly off',path:'@messages.halt_slightly_off',hover_tip:'Override lightning mode when your answer is slightly off',default:true},
  134. halt_multi_reading: {type:'checkbox',label:'Halt if multiple readings',path:'@messages.halt_multi_reading',hover_tip:'Override lightning mode when an item has multiple readings',default:false},
  135. }},
  136. grp_audio: {type:'group',label:'Audio',content:{
  137. audio_type: {type:'dropdown',label:'Audio file type',hover_tip:'Audio file type (default=mp3)',default:'mp3',content:{mp3:'mp3',ogg:'ogg'}},
  138. audio_gender: {type:'dropdown',label:'Speaker',hover_tip:'',default:'rotate',content:{rotate:'Rotate',random:'Random',male:'Male',female:'Female'}},
  139. }},
  140. }},
  141. },
  142. };
  143.  
  144. populate_items_config(config);
  145.  
  146. quiz.settings_dialog = new wkof.Settings(config);
  147. quiz_settings_state = 'ready';
  148. open_quiz_settings();
  149. }
  150.  
  151. //========================================================================
  152. // preopen_quiz_settings()
  153. //------------------------------------------------------------------------
  154. function preopen_quiz_settings(dialog) {
  155. var btn_grp =
  156. '<div class="pre_list_btn_grp">'+
  157. '<button type="button" ref="###" action="new" class="ui-button ui-corner-all ui-widget" title="Create a new preset">New</button>'+
  158. '<button type="button" ref="###" action="up" class="ui-button ui-corner-all ui-widget" title="Move the selected preset up in the list"><span class="fa fa-arrow-up icon-style"></span></button>'+
  159. '<button type="button" ref="###" action="down" class="ui-button ui-corner-all ui-widget" title="Move the selected preset down in the list"><span class="fa fa-arrow-down icon-style"></span></button>'+
  160. '<button type="button" ref="###" action="delete" class="ui-button ui-corner-all ui-widget" title="Delete the selected preset">Delete</button>'+
  161. '</div>';
  162.  
  163. var wrap = dialog.find('#ss_quiz_active_qpreset').closest('.row');
  164. wrap.addClass('pre_list_wrap');
  165. wrap.prepend(btn_grp.replace(/###/g, 'qpreset'));
  166. wrap.find('.pre_list_btn_grp').on('click', 'button', preset_button_pressed);
  167.  
  168. wrap = dialog.find('#ss_quiz_active_ipreset').closest('.row');
  169. wrap.addClass('pre_list_wrap');
  170. wrap.prepend(btn_grp.replace(/###/g, 'ipreset'));
  171. wrap.find('.pre_list_btn_grp').on('click', 'button', preset_button_pressed);
  172.  
  173. $('#ss_quiz_ipre_srcs .row:first-child').each(function(i,e){
  174. var row = $(e);
  175. var right = row.find('>.right');
  176. row.prepend(right);
  177. row.addClass('src_enable');
  178. });
  179.  
  180. // Customize the item source filters.
  181. var srcs = $('#ss_quiz_ipre_srcs');
  182. var flt_grps = srcs.find('.wkof_group');
  183. flt_grps.addClass('filters');
  184. var filters = flt_grps.find('.row');
  185. filters.prepend('<div class="enable"><input type="checkbox"></div>');
  186. filters.on('change', '.enable input[type="checkbox"]', toggle_filter);
  187.  
  188. init_settings();
  189. refresh_qpresets();
  190. refresh_ipresets();
  191. }
  192.  
  193. //========================================================================
  194. // open_quiz_settings()
  195. //------------------------------------------------------------------------
  196. function open_quiz_settings() {
  197. if (quiz_settings_state !== 'ready') return setup_quiz_settings();
  198. quiz_settings_state = 'open';
  199. var backup = {};
  200. quiz.backup = backup;
  201. backup.max_quiz_size = quiz.settings.max_quiz_size;
  202. backup.qpre = JSON.stringify(quiz.settings.qpresets[quiz.settings.active_qpreset].content);
  203. backup.ipre = JSON.stringify(quiz.settings.ipresets[quiz.settings.active_ipreset].content);
  204. quiz.settings_dialog.open();
  205. }
  206.  
  207. //========================================================================
  208. // save_quiz_settings()
  209. //------------------------------------------------------------------------
  210. function save_quiz_settings(settings) {
  211. quiz.settings = settings;
  212. populate_presets($('#ss_quiz_qna'), settings.qpresets, settings.active_qpreset);
  213. populate_presets($('#ss_quiz_source'), settings.ipresets, settings.active_ipreset);
  214. var qpre = JSON.stringify(quiz.settings.qpresets[quiz.settings.active_qpreset].content);
  215. var ipre = JSON.stringify(quiz.settings.ipresets[quiz.settings.active_ipreset].content);
  216. var reshuffle = (qpre !== quiz.backup.qpre) || (quiz.settings.max_quiz_size !== quiz.backup.max_quiz_size);
  217. var refetch = (ipre !== quiz.backup.ipre);
  218. var redraw = (quiz.settings.synonyms_order !== quiz.backup.synonyms_order);
  219. delete quiz.backup;
  220. if (refetch) {
  221. fetch_items().then(quiz.start);
  222. } else if (reshuffle) {
  223. quiz.start();
  224. } else if (redraw) {
  225. quiz.qinfo.cache = {};
  226. quiz.ask();
  227. }
  228. }
  229.  
  230. //========================================================================
  231. // close_quiz_settings()
  232. //------------------------------------------------------------------------
  233. function close_quiz_settings(settings) {
  234. quiz_settings_state = 'setup';
  235. }
  236.  
  237. //========================================================================
  238. // refresh_quiz_settings()
  239. //------------------------------------------------------------------------
  240. function refresh_quiz_settings(settings) {
  241. $('#ss_quiz_ipre_srcs .wkof_group .row').each(function(i,e){
  242. var row = $(e);
  243. var panel = row.closest('[role="tabpanel"]');
  244. var source = panel.attr('id').match(/^ss_quiz_pg_(.*)$/)[1];
  245. var filter_name = row.find('.setting').attr('name').slice((source+'_flt_').length);
  246. var preset = quiz.settings.ipresets[quiz.settings.active_ipreset].content;
  247. var enabled = false;
  248. try {
  249. enabled = preset[source].filters[filter_name].enabled;
  250. } catch(e) {}
  251.  
  252. if (enabled) {
  253. row.addClass('checked');
  254. } else {
  255. row.removeClass('checked');
  256. }
  257. row.find('.enable input[type="checkbox"]').prop('checked', enabled);
  258. });
  259. }
  260.  
  261. //========================================================================
  262. // refresh_qpresets()
  263. //------------------------------------------------------------------------
  264. function refresh_qpresets() {
  265. var settings = quiz.settings;
  266. populate_presets($('#ss_quiz_active_qpreset'), settings.qpresets, settings.active_qpreset);
  267. }
  268.  
  269. //========================================================================
  270. // refresh_ipresets()
  271. //------------------------------------------------------------------------
  272. function refresh_ipresets() {
  273. var settings = quiz.settings;
  274. populate_presets($('#ss_quiz_active_ipreset'), settings.ipresets, settings.active_ipreset);
  275. }
  276.  
  277. //========================================================================
  278. // preset_button_pressed()
  279. //------------------------------------------------------------------------
  280. function preset_button_pressed(e) {
  281. var settings = quiz.settings;
  282. var ref = e.currentTarget.attributes.ref.value;
  283. var action = e.currentTarget.attributes.action.value;
  284. var selected = Number(settings['active_'+ref]);
  285. var presets = settings[ref+'s'];
  286. var elem = $('#ss_quiz_active_'+ref);
  287.  
  288. var dflt;
  289. if (ref === 'qpreset') {
  290. dflt = {name:'<untitled>', content:$.extend(true, {}, qpre_defaults)};
  291. } else {
  292. dflt = {name:'<untitled>', content:$.extend(true, {}, ipre_defaults)};
  293. }
  294.  
  295. switch (action) {
  296. case 'new':
  297. presets.push(dflt);
  298. selected = presets.length - 1;
  299. settings[ref+'s'] = presets;
  300. settings['active_'+ref] = selected;
  301. populate_presets(elem, presets, selected);
  302. quiz.settings_dialog.refresh();
  303. $('#ss_quiz_'+ref.slice(0,4)+'_name').focus().select();
  304. break;
  305.  
  306. case 'up':
  307. if (selected <= 0) break;
  308. presets = [].concat(presets.slice(0, selected-1), presets[selected], presets[selected-1], presets.slice(selected+1));
  309. selected--;
  310. settings[ref+'s'] = presets;
  311. settings['active_'+ref] = selected;
  312. populate_presets(elem, presets, selected);
  313. break;
  314.  
  315. case 'down':
  316. if (selected >= presets.length-1) break;
  317. presets = [].concat(presets.slice(0, selected), presets[selected+1], presets[selected], presets.slice(selected+2));
  318. selected++;
  319. settings[ref+'s'] = presets;
  320. settings['active_'+ref] = selected;
  321. populate_presets(elem, presets, selected);
  322. break;
  323.  
  324. case 'delete':
  325. presets = presets.slice(0, selected).concat(presets.slice(selected+1));
  326. if (presets.length === 0) presets = [dflt];
  327. selected = Math.max(0, selected-1);
  328. settings[ref+'s'] = presets;
  329. settings['active_'+ref] = selected;
  330. populate_presets(elem, presets, selected);
  331. quiz.settings_dialog.refresh();
  332. break;
  333. }
  334. }
  335.  
  336. //========================================================================
  337. // init_settings()
  338. //------------------------------------------------------------------------
  339. var qpre_defaults = {char2mean:false, char2read:false, read2mean:false, mean2read:false, aud2mean:false, aud2read:false};
  340. function init_settings() {
  341. var idx;
  342. // Merge some defaults
  343. var defaults = {
  344. pairing: 'reading_first',
  345. allow_typos: true,
  346. play_audio: true,
  347. mute_audio: false,
  348. autoshow_correct: false,
  349. max_quiz_size: 0, // 0 = unlimited
  350. messages: {
  351. show_slightly_off: true,
  352. show_multi_reading: false,
  353. halt_slightly_off: true,
  354. halt_multi_reading: false,
  355. }
  356. };
  357. var settings = $.extend(true, {}, defaults, wkof.settings.ss_quiz);
  358. wkof.settings.ss_quiz = quiz.settings = settings;
  359. if (settings.qpresets === undefined) {
  360. settings.qpresets = [
  361. {name:'All Questions', content:{char2mean:true, char2read:true, read2mean:true, mean2read:true, aud2mean:true, aud2read:true}},
  362. {name:'Japanese to English', content:{char2mean:true, char2read:true, read2mean:false, mean2read:false, aud2mean:false, aud2read:false}},
  363. {name:'English to Japanese', content:{char2mean:false, char2read:false, read2mean:false, mean2read:true, aud2mean:false, aud2read:false}},
  364. {name:'Audio Quiz', content:{char2mean:false, char2read:false, read2mean:false, mean2read:false, aud2mean:true, aud2read:true}},
  365. ];
  366. settings.active_qpreset = 0;
  367. }
  368. for (idx in settings.qpresets) {
  369. settings.qpresets[idx].content = $.extend(true, {}, qpre_defaults, settings.qpresets[idx].content);
  370. }
  371. if (settings.messages === undefined) {
  372. settings.messages = {show_slightly_off:true, show_multi_reading:false, halt_slightly_off:true, halt_multi_reading:false}
  373. }
  374. if (settings.ipresets === undefined) {
  375. settings.ipresets = [
  376. {name:'All Learned Items', content:{wk_items:{enabled:true,filters:{srs:{enabled:true,value:{appr1:true,appr2:true,appr3:true,appr4:true,guru1:true,guru2:true,mast:true,enli:true,burn:true}}}}}},
  377. {name:'Apprentice Items', content:{wk_items:{enabled:true,filters:{srs:{enabled:true,value:{appr1:true,appr2:true,appr3:true,appr4:true}}}}}},
  378. {name:'Burned Items', content:{wk_items:{enabled:true,filters:{srs:{enabled:true,value:{burn:true}}}}}},
  379. {name:'Resurrected Items', content:{wk_items:{enabled:true,filters:{have_burned:{enabled:true,value:true},srs:{enabled:true,value:{appr1:true,appr2:true,appr3:true,appr4:true,guru1:true,guru2:true,mast:true,enli:true}}}}}},
  380. ];
  381. settings.active_ipreset = 0;
  382. }
  383. if (ipre_defaults) {
  384. for (idx in settings.ipresets) {
  385. settings.ipresets[idx].content = $.extend(true, {}, ipre_defaults, settings.ipresets[idx].content);
  386. }
  387. }
  388. }
  389.  
  390. //========================================================================
  391. // populate_items_config()
  392. //------------------------------------------------------------------------
  393. var ipre_defaults;
  394. function populate_items_config(config) {
  395. var ipre_srcs = config.settings.pg_items.content.grp_ipre.content.ipre_srcs.content;
  396. var srcs = wkof.ItemData.registry.sources;
  397. ipre_defaults = {};
  398. for (var src_name in srcs) {
  399. var src = srcs[src_name];
  400. var pg_content = {};
  401. ipre_srcs['pg_'+src_name] = {type:'page',label:src.description,content:pg_content};
  402. var settings = {};
  403. ipre_defaults[src_name] = settings;
  404. pg_content[src_name+'_enable'] = {
  405. type:'checkbox',
  406. label:'Include this source',
  407. path:'@ipresets[@active_ipreset].content["'+src_name+'"].enabled',
  408. hover_tip:'Check to include this data source in the quiz'
  409. };
  410. // Enable Wanikani source by default.
  411. settings.enabled = (src_name === 'wk_items');
  412.  
  413. // Add 'Options' section. 'wk_items' is handled automatically.
  414. if (src_name !== 'wk_items') {
  415. if (src.options && Object.keys(src.options).length > 0) {
  416. settings.options = {};
  417. var opt_content = {};
  418. pg_content['grp_'+src_name+'_options'] = {type:'group',label:'Options',content:opt_content};
  419. for (var opt_name in src.options) {
  420. var opt = src.options[opt_name];
  421. switch (opt.type) {
  422. case 'checkbox':
  423. opt_content[src_name+'_opt_'+opt_name] = {
  424. type:'checkbox',
  425. label:opt.label,
  426. default:opt.default,
  427. hover_tip:opt.hover_tip
  428. }
  429. break;
  430. }
  431. }
  432. }
  433. }
  434.  
  435. // Add 'Filters' section.
  436. if (src.filters && Object.keys(src.filters).length > 0) {
  437. settings.filters = {};
  438. var flt_content = {};
  439. pg_content['grp_'+src_name+'_filters'] = {type:'group',label:'Filters',content:flt_content};
  440. for (var flt_name in src.filters) {
  441. var flt = src.filters[flt_name];
  442. if (flt.no_ui) continue;
  443. settings.filters[flt_name] = {enabled:false, value:flt.default};
  444. switch (flt.type) {
  445. case 'checkbox':
  446. flt_content[src_name+'_flt_'+flt_name] = {
  447. type:'checkbox',
  448. label:flt.label,
  449. default:flt.default,
  450. path:'@ipresets[@active_ipreset].content["'+src_name+'"].filters["'+flt_name+'"].value',
  451. validate: flt.validate,
  452. hover_tip:flt.hover_tip
  453. }
  454. break;
  455. case 'multi':
  456. var dflt = flt.default;
  457. if (typeof flt.filter_value_map === 'function') dflt = flt.filter_value_map(dflt);
  458. flt_content[src_name+'_flt_'+flt_name] = {
  459. type:'list',
  460. multi:true,
  461. size:Math.min(4,Object.keys(flt.content).length),
  462. label:flt.label,
  463. content:flt.content,
  464. default:dflt,
  465. path:'@ipresets[@active_ipreset].content["'+src_name+'"].filters["'+flt_name+'"].value',
  466. validate: flt.validate,
  467. hover_tip:flt.hover_tip
  468. }
  469. settings.filters[flt_name].value = dflt;
  470. break;
  471. case 'text':
  472. case 'number':
  473. case 'input':
  474. flt_content[src_name+'_flt_'+flt_name] = {
  475. type:flt.type,
  476. label:flt.label,
  477. placeholder:flt.placeholder,
  478. default:flt.default,
  479. path:'@ipresets[@active_ipreset].content["'+src_name+'"].filters["'+flt_name+'"].value',
  480. validate: flt.validate,
  481. hover_tip:flt.hover_tip
  482. }
  483. break;
  484. case 'button':
  485. flt_content[src_name+'_flt_'+flt_name] = {
  486. type:flt.type,
  487. label:flt.label,
  488. on_click:flt.on_click,
  489. validate: flt.validate,
  490. hover_tip:flt.hover_tip
  491. }
  492. break;
  493. }
  494. }
  495. }
  496. }
  497. }
  498.  
  499. //========================================================================
  500. // toggle_filter()
  501. //------------------------------------------------------------------------
  502. function toggle_filter(e) {
  503. var row = $(e.delegateTarget);
  504. var panel = row.closest('[role="tabpanel"]');
  505. var source = panel.attr('id').match(/^ss_quiz_pg_(.*)$/)[1];
  506. var enabled = row.find('.enable input[type="checkbox"]').prop('checked');
  507. var preset = quiz.settings.ipresets[quiz.settings.active_ipreset].content;
  508. var filter_name = row.find('.setting').attr('name').slice((source+'_flt_').length);
  509.  
  510. if (enabled) {
  511. row.addClass('checked');
  512. } else {
  513. row.removeClass('checked');
  514. }
  515. try {
  516. preset[source].filters[filter_name].enabled = enabled;
  517. } catch(e) {}
  518. }
  519.  
  520. //########################################################################
  521. // QUIZ DIALOG
  522. //########################################################################
  523.  
  524. //========================================================================
  525. // install_css()
  526. //------------------------------------------------------------------------
  527. function install_css() {
  528. $('head').append(`
  529. <style id="ss_quiz_css" type="text/css">
  530. .noselect {-webkit-touch-callout:none; -webkit-user-select:none; -khtml-user-select:none; -moz-user-select: none; -ms-user-select:none; user-select: none;}
  531.  
  532. #ss_quiz [lang="ja"] {font-family: "Meiryo","Yu Gothic","Hiragino Kaku Gothic Pro","TakaoPGothic","Yu Gothic","ヒラギノ角ゴ Pro W3","メイリオ","Osaka","MS PGothic","MS Pゴシック",sans-serif;}
  533. #ss_quiz {position:fixed; z-index:12001; width:573px; background-color:#000; border-radius:8px; border:8px solid rgba(0,0,0,0.85); font-size:16px; line-height:16px; font-family:"Helvetica Neue", Helvetica, Arial, sans-serif;}
  534. #ss_quiz * {text-align:center;}
  535.  
  536. #ss_quiz .titlebar {cursor:move; text-align:left; padding-bottom:4px; font-size:1.125em; font-weight:bold; line-height:1.125em; background-color:rgba(0,0,0,0.85); color:#ddd;}
  537. #ss_quiz .titlebar .button {display:inline-block; float:right; height:20px; width:20px; line-height:1em; cursor:pointer; border:1px solid rgba(255,255,255,0.2); border-radius:4px;}
  538.  
  539. #ss_quiz .prev, #ss_quiz .next {display:inline-block; width:80px; color:#fff; line-height:8em; cursor:pointer;}
  540. #ss_quiz .prev:hover {background-image:linear-gradient(to left, rgba(0,0,0,0), rgba(0,0,0,0.2));}
  541. #ss_quiz .next:hover {background-image:linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.2));}
  542. #ss_quiz .prev {float:left;}
  543. #ss_quiz .next {float:right;}
  544.  
  545. #ss_quiz .cfgbar {background-color:rgba(32,32,32,0.85); padding:4px 0; border-bottom:1px solid #444;white-space:nowrap;}
  546. #ss_quiz .cfgbar select {margin:0; background:transparent; color:rgba(255,255,255,0.5); border:1px solid #777; width:248px; height:2em; text-align:left; font-size:0.875em; border-radius:4px; padding:4px 6px;}
  547. #ss_quiz .cfgbar option {color:#000;}
  548. #ss_quiz .cfgbar .button {display:inline-block; width:24px; height:24px; cursor:pointer; color:#777; font-size:24px; vertical-align:middle;}
  549. #ss_quiz .cfgbar .button:hover {color:#ccc;}
  550.  
  551. #ss_quiz .statusbar {line-height:1em; color:rgba(255,255,255,0.5); background-color:rgba(32,32,32,0.85);}
  552.  
  553. #ss_quiz .settings {float:left; padding:6px 8px; text-align:left; line-height:1.5em; font-size:0.875em;}
  554. #ss_quiz .settings span[class*="icon-"] {font-size:1.3em; padding:0 2px;}
  555. #ss_quiz .settings .ss_audio {padding-left:0; padding-right:4px;}
  556. #ss_quiz .settings .ss_done {font-size:1.25em;}
  557. #ss_quiz .settings .ss_pair {font-weight:bold;}
  558. #ss_quiz .settings span {cursor:pointer;}
  559. #ss_quiz .settings > span:hover {color:rgba(255,255,204,0.8);}
  560. #ss_quiz .settings span.active {color:#ffc;}
  561.  
  562. #ss_quiz .stats_labels {text-align:right; font-family:monospace; font-size:14px; line-height:14px; white-space:pre;}
  563. #ss_quiz .stats {float:right; text-align:right; color:rgba(255,255,255,0.8); font-family:monospace; padding:0 5px;}
  564.  
  565. #ss_quiz .fa-audio:before {content:"\\f028";}
  566.  
  567. #ss_quiz .ss_audio {display:inline-block;box-sizing:border-box;width:22px;text-align:left}
  568. #ss_quiz .ss_audio:before {content:"\\f026";}
  569. #ss_quiz .ss_audio.active:before {content:"\\f028";}
  570. #ss_quiz .ss_audio.mute {color:rgba(255,0,0,0.8);}
  571. #ss_quiz .ss_audio.mute:hover {color:rgba(255,127,127,0.8);}
  572.  
  573. #ss_quiz[data-qtype="characters"] .question {font-size:2em;}
  574. #ss_quiz .question svg.radical {width:1em;height:1em;stroke:#fff;stroke-width:68;stroke-linecap:square;stroke-miterlimit:2;fill:none;}
  575. #ss_quiz .question svg.radical * {stroke:#fff;}
  576.  
  577. #ss_quiz .atype {font-size:1.75em; line-height:2em; cursor:default; color:#fff; border-top:1px solid #000; border-bottom:1px solid #000;}
  578. #ss_quiz[data-atype="reading"] .atype {color:#fff; text-shadow:-1px -1px 0 #000; border-top:1px solid #555; border-bottom:1px solid #000; background-color:#2e2e2e; background-image:linear-gradient(to bottom, #3c3c3c, #1a1a1a); background-repeat:repeat-x;}
  579. #ss_quiz[data-atype="meaning"] .atype {color:#555; text-shadow:-1px -1px 0 rgba(255,255,255,0.1); border-top:1px solid #d5d5d5; border-bottom:1px solid #c8c8c8; background-color:#e9e9e9; background-image:linear-gradient(to bottom, #eee, #e1e1e1); background-repeat:repeat-x;}
  580.  
  581. #ss_quiz .help {display:none;
  582. position:absolute; top:3%; left:13%; width:74%; box-sizing:border-box; border:2px solid #000; border-radius:15px; padding:4px;
  583. color:#555; text-shadow:2px 2px 0 rgba(0,0,0,0.13); background-color:rgba(255,255,255,0.9); font-size:0.8em; line-height:1.2em;
  584. }
  585. #ss_quiz.help .help {display:inherit;}
  586.  
  587. #ss_quiz .message {visibility:hidden;
  588. position:absolute; bottom:3%; left:13%; width:74%; box-sizing:border-box; border:2px solid #000; border-radius:15px; padding:4px;
  589. color:#555; text-shadow:2px 2px 0 rgba(0,0,0,0.13); background-color:rgba(255,255,255,0.9); font-size:0.6em; line-height:1.2em; opacity:0; transition:visibility 0.25s, opacity 0.25s linear;
  590. }
  591. #ss_quiz.message .message {visibility:visible; opacity:1; transition:visibility 0s, opacity 0.25s linear;}
  592.  
  593. #wkof_ds #ss_quiz .answer {font-size:1.75em; background-color:#ddd; padding:8px;}
  594. #wkof_ds #ss_quiz .answer input {
  595. width:100%; background-color:#fff; height:2em; margin:0; border:2px solid #000; padding:0;
  596. box-sizing:border-box; border-radius:0; font-size:1em;
  597. }
  598. #wkof_ds #ss_quiz[data-result="correct"] .answer input {color:#fff; background-color:#8c8; text-shadow:2px 2px 0 rgba(0,0,0,0.2);}
  599. #wkof_ds #ss_quiz[data-result="incorrect"] .answer input {color:#fff; background-color:#f03; text-shadow:2px 2px 0 rgba(0,0,0,0.2);}
  600.  
  601. #ss_quiz .btn.requiz {position:absolute; top:6px; right:6px; padding-left:6px; padding-right:6px;}
  602.  
  603. #ss_quiz .qwrap {height:8em; position:relative; clear:both; font-size:1.75em}
  604. #ss_quiz[data-itype="radical"] .qwrap, #ss_quiz .summary .que[title~="Radical"] {background-color:var(--color-radical);}
  605. #ss_quiz[data-itype="kanji"] .qwrap, #ss_quiz .summary .que[title~="Kanji"] {background-color:var(--color-kanji);}
  606. #ss_quiz[data-itype="vocabulary"] .qwrap, #ss_quiz .summary .que[title~="Vocabulary"] {background-color:var(--color-vocabulary);}
  607. #ss_quiz[data-itype="kana_vocabulary"] .qwrap, #ss_quiz .summary .que[title~="Vocabulary"] {background-color:var(--color-vocabulary);}
  608.  
  609. #ss_quiz .qwrap > .center {display:none; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);}
  610.  
  611. #ss_quiz[data-mode="loading"] .qwrap {background-color:#ccc; opacity:0.5;}
  612. #wkof_ds #ss_quiz[data-mode="loading"] .answer {opacity:0.5;}
  613.  
  614. #ss_quiz[data-mode="question"] .question {display:block;}
  615. #ss_quiz .question {overflow-x:auto; overflow-y:hidden; color:#fff; text-align:center; line-height:1.1em; font-size:1em; cursor:default;}
  616. #ss_quiz .question .fa-audio {font-size:2.5em; cursor:pointer;}
  617.  
  618. #ss_quiz[data-mode="summary"] .summary {display:block;}
  619. #ss_quiz .summary {display:none; position:absolute; width:74%; height:100%; background-color:rgba(0,0,0,0.7); color:#fff; font-weight:bold;}
  620. #ss_quiz .summary h3 {
  621. background-image:linear-gradient(to bottom, #3c3c3c, #1a1a1a); background-repeat:repeat-x;
  622. border-top:1px solid #777; border-bottom:1px solid #000; margin:0; box-sizing:border-box;
  623. text-shadow:2px 2px 0 rgba(0,0,0,0.5); color:#fff; font-size:0.8em; font-weight:bold; line-height:40px;
  624. }
  625. #ss_quiz .summary .errors {position:absolute; top:40px; bottom:0px; width:100%; margin:0; overflow-y:auto; list-style-type:none;}
  626. #ss_quiz .summary li {margin:4px 0 0 0; font-size:0.6em; font-weight:bold; line-height:1.4em;}
  627.  
  628. #ss_quiz .summary .errors span {display:inline-block; padding:2px 4px 0px 4px; border-radius:4px; line-height:1.1em; max-width:50%; vertical-align:middle; cursor:pointer;}
  629. #ss_quiz .errors .fa-long-arrow-right {padding:0px 8px;}
  630. #ss_quiz .summary .ans {background-color:#fff; color:#000;}
  631. #ss_quiz .summary .wrong {color:#f22;}`+
  632.  
  633.  
  634. //--[ Settings dialog ]-------------------------------------------
  635. `#wkof_ds div[role="dialog"][aria-describedby="wkofs_ss_quiz"] {z-index:12002;}
  636.  
  637. #wkofs_ss_quiz.wkof_settings .pre_list_btn_grp {width:60px;float:left;margin-right:2px;}
  638. #wkofs_ss_quiz.wkof_settings .pre_list_btn_grp button {width:100%; padding:2px 0;}
  639. #wkofs_ss_quiz.wkof_settings .pre_list_btn_grp button:not(:last-child) {margin-bottom:2px;}
  640. #wkofs_ss_quiz.wkof_settings .pre_list_wrap {display:flex;}
  641. #wkofs_ss_quiz.wkof_settings .pre_list_wrap .right {flex:1;}
  642. #wkofs_ss_quiz.wkof_settings .pre_list_wrap .list {overflow:auto;height:100%;}
  643.  
  644. #wkofs_ss_quiz.wkof_settings .filters .row {border-top:1px solid #ccc; padding:6px 4px; margin-bottom:0;}
  645. #wkofs_ss_quiz.wkof_settings .filters .row:not(.checked) {padding-top:0px;padding-bottom:0px;}
  646. #wkofs_ss_quiz .filters .row .enable input[type="checkbox"] {margin:0;}
  647. #wkofs_ss_quiz.narrow .filters .row.checked .right input[type="checkbox"]:after {content:"⇐yes?";margin-left:28px;line-height:30px;}
  648. #wkofs_ss_quiz .filters .row.checked {background-color:#f7f7f7;}
  649. #wkofs_ss_quiz .filters .row:not(.checked) {opacity:0.5;}
  650. #wkofs_ss_quiz .filters .row .enable {display:inline; margin:0; float:left;}
  651. #wkofs_ss_quiz:not(.narrow) .filters .left {width:170px;}
  652.  
  653. #wkofs_ss_quiz .filters .row .enable input[type="checkbox"] {margin:0 4px 0 0;}
  654. #wkofs_ss_quiz .filters .row:not(.checked) .right {display:none;}
  655. #wkofs_ss_quiz .filters .row:not(.checked) .left label {text-align:left;}
  656. #wkofs_ss_quiz.narrow .filters .row .left {width:initial;}
  657. #wkofs_ss_quiz.narrow .filters .row .left label {line-height:30px;}
  658. #wkofs_ss_quiz #ss_quiz_ipre_srcs .src_enable .left {width:initial;}
  659. #wkofs_ss_quiz #ss_quiz_ipre_srcs .src_enable .left label {text-align:left;width:initial;line-height:30px;}
  660. #wkofs_ss_quiz #ss_quiz_ipre_srcs .src_enable .right {float:left; margin:0 4px;width:initial;}`+
  661. //----------------------------------------------------------------
  662.  
  663. '</style>'
  664. );
  665. }
  666.  
  667. //========================================================================
  668. // open_quiz()
  669. //------------------------------------------------------------------------
  670. var quiz_setup_state = 'init';
  671. function open_quiz(custom_options) {
  672. if (quiz_setup_state === 'init') {
  673. quiz_setup_state = 'loading';
  674. install_css();
  675. wkof.include('ItemData, Settings');
  676. wkof.ready('ItemData, Settings').then(function(){
  677. return wkof.Settings.load('ss_quiz');
  678. }).then(function(){
  679. quiz_setup_state = 'ready';
  680. init_settings();
  681. open_quiz(custom_options);
  682. });
  683. }
  684. if (quiz_setup_state !== 'ready') return;
  685.  
  686. var quiz_html =
  687. '<div id="ss_quiz" class="dialog" data-itype="loading" data-atype="meaning" data-mode="question">'+
  688. ' <div class="titlebar noselect">Self-Study Quiz<span class="button" title="Close the quiz.\nHotkey: Rapid triple-tap [Esc]">x</span></div>'+
  689. ' <div class="cfgbar">'+
  690. ' <select id="ss_quiz_qna" title="Choose what quiz questions you want to be asked"></select>'+
  691. ' <select id="ss_quiz_source" title="Choose what items you want to be quizzed on"></select>'+
  692. ' <span class="fa fa-repeat icon-style shuffle button" title="Shuffle Quiz (Ctrl-S)\nDouble-click to reset Round counter"></span>'+
  693. ' <span class="fa fa-cog icon-style config button" title="Configure presets"></span>'+
  694. ' </div>'+
  695. ' <div class="statusbar">'+
  696. ' <div class="settings noselect">'+
  697. ' <span class="fa fa-bolt icon-style ss_lightning" title="Lightning Mode: Skip <enter> on correct answers (Ctrl-L)"></span>'+
  698. ' <span class="fa fa-audio icon-style ss_audio" title="Toggle when to play audio (Ctrl-Shift-A)\n* Red = Never play audio\n* Gray = Audio questions only\n* Yellow = Audio questions, After correct reading, Opening help for reading\n\nTo play audio immediately, press (Ctrl-A)"></span>'+
  699. ' <span class="fa fa-question icon-style ss_help" title="Help: Peek at item info (F1, Ctrl-H, or ?)"></span>'+
  700. ' <span class="fa fa-step-forward icon-style ss_done" title="End the quiz and show summary (Esc or Ctrl-E)"></span><br />'+
  701. ' <span class="ss_pair" title="Pairing mode: Group reading and meaning together (Ctrl-P)">Pairing: <span class="data">Disabled</span></span>'+
  702. ' </div>'+
  703. ' <div class="stats"></div>'+
  704. ' <div class="stats_labels">Round:<br>Remaining:<br>Correct:<br>Incorrect:</div>'+
  705. ' </div>'+
  706. ' <div class="qwrap">'+
  707. ' <div class="prev" title="Previous question (Ctrl-Left)"><i class="fa fa-chevron-left icon-style"></i></div>'+
  708. ' <div class="next" title="Next question (Ctrl-Right)"><i class="fa fa-chevron-right icon-style"></i></div>'+
  709. ' <div class="question center"></div>'+
  710. ' <div class="help"></div>'+
  711. ' <div class="message"></div>'+
  712. ' <div class="summary center">'+
  713. ' <h3>Summary - <span class="percent">100%</span> Correct <button class="btn requiz" title="Re-quiz wrong items">Re-quiz</button></h3>'+
  714. ' <ul class="errors"></ul>'+
  715. ' </div>'+
  716. ' </div>'+
  717. ' <div class="atype">Loading...</div>'+
  718. ' <div class="answer"><input type="text" lang="en" value=""></div>'+
  719. '</div>';
  720.  
  721. if (quiz.dialog) quiz.close();
  722. var dialog = (quiz.dialog = $(quiz_html));
  723.  
  724. var settings = quiz.settings;
  725. init_custom_options(custom_options);
  726. populate_presets(dialog.find('#ss_quiz_qna'), settings.qpresets, settings.active_qpreset);
  727. populate_presets(dialog.find('#ss_quiz_source'), settings.ipresets, settings.active_ipreset);
  728.  
  729. wkof.Settings.background.open();
  730. $('#wkof_ds').append(dialog);
  731.  
  732. dialog.css('top', Math.max(0,Math.floor((window.innerHeight - dialog.outerHeight()) / 2)));
  733. dialog.css('left', Math.floor((window.innerWidth - dialog.outerWidth()) / 2));
  734.  
  735. // Initialize settings
  736. var settings_bar = dialog.find('.statusbar .settings');
  737. if (settings.lightning_mode === true) settings_bar.find('.ss_lightning').addClass('active');
  738. if (settings.repeat_quiz === true) settings_bar.find('.ss_repeat').addClass('active');
  739. if (settings.shuffle_on_repeat === true) settings_bar.find('.ss_shuffle').addClass('active');
  740. if (settings.play_audio === true) settings_bar.find('.ss_audio').addClass('active');
  741. if (settings.mute_audio === true) settings_bar.find('.ss_audio').addClass('mute');
  742. toggle_pairing(null, true /* initialize */);
  743.  
  744. // Events
  745. dialog.find('.settings .ss_lightning').on('click', toggle_lightning);
  746. dialog.find('.settings .ss_audio').on('click', toggle_audio);
  747. dialog.find('.settings .ss_help').on('click', toggle_help);
  748. dialog.find('.settings .ss_pair').on('click', toggle_pairing);
  749. dialog.find('.settings .ss_done').on('click', process_escape);
  750. dialog.find('.prev').on('click', quiz.prev);
  751. dialog.find('.next').on('click', quiz.next);
  752. dialog.find('.titlebar').on('mousedown touchstart', drag);
  753. dialog.find('.cfgbar .button.shuffle').on('click', manual_shuffle);
  754. dialog.find('.cfgbar .button.config').on('click', open_quiz_settings);
  755. dialog.find('.titlebar .button').on('click', close_quiz);
  756. dialog.find('.summary .requiz').on('click', quiz.requiz);
  757. dialog.find('.question').on('click', '.fa-audio', play_audio.bind(null,true,null));
  758. $('#ss_quiz_qna').on('change', qpreset_changed);
  759. $('#ss_quiz_source').on('change', ipreset_changed);
  760. $('body').on('keydown.ss_quiz_key keypress.ss_quiz_key', quiz_key_handler);
  761. freeze_body();
  762.  
  763. set_mode('loading');
  764. fetch_items().then(quiz.start);
  765. }
  766.  
  767. //========================================================================
  768. // init_custom_options()
  769. //------------------------------------------------------------------------
  770. function init_custom_options(custom) {
  771. if (!custom) {
  772. quiz.custom = {
  773. has_ipreset: false,
  774. using_ipreset: false,
  775. has_qpreset: false,
  776. using_qpreset: false,
  777. }
  778. return;
  779. }
  780. quiz.custom = custom;
  781. if (custom.qpreset) {
  782. quiz.custom.has_qpreset = true;
  783. quiz.custom.using_qpreset = true;
  784. }
  785. if (custom.ipreset) {
  786. quiz.custom.has_ipreset = true;
  787. quiz.custom.using_ipreset = true;
  788. }
  789. }
  790.  
  791. //========================================================================
  792. // close_quiz()
  793. //------------------------------------------------------------------------
  794. function close_quiz(e) {
  795. unfreeze_body();
  796. $('body').off('.ss_quiz_key');
  797. quiz.dialog.remove();
  798. wkof.Settings.background.close();
  799. if (quiz.custom && typeof quiz.custom.on_close === 'function') quiz.custom.on_close();
  800. }
  801.  
  802. var body_scroll_y;
  803. function freeze_body() {
  804. body_scroll_y = window.scrollY;
  805. $('body').css('overflow', 'hidden').scrollTop(body_scroll_y);
  806. }
  807. function unfreeze_body() {
  808. $('body').css('overflow','unset');
  809. window.scroll({top:body_scroll_y});
  810. }
  811.  
  812. //========================================================================
  813. // qpreset_changed()
  814. //------------------------------------------------------------------------
  815. function qpreset_changed(e) {
  816. var settings = quiz.settings;
  817. var selected = e.target.selectedOptions[0].attributes.name.value;
  818. if (selected === 'custom') {
  819. quiz.custom.using_qpreset = true;
  820. } else {
  821. quiz.custom.using_qpreset = false;
  822. settings.active_qpreset = selected;
  823. wkof.Settings.save('ss_quiz');
  824. }
  825. quiz.start();
  826. }
  827.  
  828. //========================================================================
  829. // ipreset_changed()
  830. //------------------------------------------------------------------------
  831. function ipreset_changed(e) {
  832. var settings = quiz.settings;
  833. var selected = e.target.selectedOptions[0].attributes.name.value;
  834. if (selected === 'custom') {
  835. quiz.custom.using_ipreset = true;
  836. } else {
  837. quiz.custom.using_ipreset = false;
  838. settings.active_ipreset = selected;
  839. wkof.Settings.save('ss_quiz');
  840. }
  841. fetch_items().then(quiz.start);
  842. }
  843.  
  844. //========================================================================
  845. // populate_presets()
  846. //------------------------------------------------------------------------
  847. function populate_presets(elem, presets, active_preset) {
  848. var html = '';
  849. for (var idx in presets) {
  850. var preset = presets[idx];
  851. var name = preset.name.replace(/</g,'&lt;').replace(/>/g,'&gt;');
  852. html += '<option name="'+idx+'">'+name+'</option>';
  853. }
  854. var elem_name = elem.attr('id')
  855. if (elem_name === 'ss_quiz_qna' && quiz.custom.has_qpreset) {
  856. html += '<option name="custom">('+quiz.custom.qpreset.name+')</option>';
  857. if (quiz.custom.using_qpreset) active_preset = presets.length;
  858. } else if (elem_name === 'ss_quiz_source' && quiz.custom.has_ipreset) {
  859. html += '<option name="custom">('+quiz.custom.ipreset.name+')</option>';
  860. if (quiz.custom.using_ipreset) active_preset = presets.length;
  861. }
  862. elem.html(html);
  863. elem.children().eq(active_preset).prop('selected', true);
  864. }
  865.  
  866. //########################################################################
  867. // QUIZ DATA
  868. //########################################################################
  869.  
  870. var quiz = {
  871. // Dialogs
  872. dialog: null,
  873. settings_dialog: null,
  874.  
  875. // Item Lists
  876. items: [],
  877. group_list: [],
  878. serial_list: [],
  879. index: null,
  880.  
  881. // Status
  882. showing_help: false,
  883. mode: 'loading',
  884.  
  885. // Question Info
  886. qinfo: {
  887. load: load_qinfo,
  888. prep: prep_qinfo,
  889. cache: {},
  890. },
  891.  
  892. // Stats
  893. stats: {
  894. round: 1,
  895. total: 0,
  896. correct: 0,
  897. incorrect: 0,
  898. },
  899.  
  900. // Functions
  901. start: start_quiz,
  902. shuffle: shuffle_quiz,
  903. requiz: requiz,
  904. ask: ask_question,
  905. submit: submit_answer,
  906. prev: prev_question,
  907. next: next_question,
  908. close: close_quiz,
  909. };
  910. gobj.open = open_quiz;
  911.  
  912. //========================================================================
  913. // fetch_items()
  914. //------------------------------------------------------------------------
  915. function fetch_items() {
  916. var settings = quiz.settings;
  917. var ipreset = (quiz.custom.using_ipreset ? quiz.custom.ipreset.content : settings.ipresets[settings.active_ipreset].content);
  918.  
  919. set_mode('loading');
  920. var config = {};
  921. for (var src_name in ipreset) {
  922. var src_preset = ipreset[src_name];
  923. if (!src_preset.enabled) continue;
  924. if (!wkof.ItemData.registry.sources[src_name]) continue;
  925. var src_cfg = {};
  926. config[src_name] = src_cfg;
  927. src_cfg.filters = {};
  928. if (src_name === 'wk_items') src_cfg.options = {study_materials: true};
  929. var ipre_filters = src_preset.filters;
  930. for (var flt_name in ipre_filters) {
  931. var ipre_flt = ipre_filters[flt_name];
  932. if (!ipre_flt.enabled) continue;
  933. if (!wkof.ItemData.registry.sources[src_name].filters[flt_name]) continue;
  934. src_cfg.filters[flt_name] = {value: ipre_flt.value};
  935. if (ipre_flt.invert === true) src_cfg.filters[flt_name].invert = true;
  936. }
  937. }
  938. return wkof.ItemData.get_items(config)
  939. .then(function(items){
  940. quiz.items = items;
  941. });
  942. }
  943.  
  944. //========================================================================
  945. // shuffle_quiz()
  946. //------------------------------------------------------------------------
  947. function shuffle_quiz() {
  948. var settings = quiz.settings;
  949. var qpreset = (quiz.custom.using_qpreset ? quiz.custom.qpreset.content : settings.qpresets[settings.active_qpreset].content);
  950. var pairing = settings.pairing || 'disabled';
  951.  
  952. var valid_question_types = {
  953. char2read: {radical:false, kanji:true, vocabulary:true, kana_vocabulary:false},
  954. char2mean: {radical:true, kanji:true, vocabulary:true, kana_vocabulary:true },
  955. read2mean: {radical:false, kanji:false, vocabulary:true, kana_vocabulary:false},
  956. mean2read: {radical:false, kanji:false, vocabulary:true, kana_vocabulary:true },
  957. aud2read: {radical:false, kanji:false, vocabulary:true, kana_vocabulary:true },
  958. aud2mean: {radical:false, kanji:false, vocabulary:true, kana_vocabulary:true },
  959. };
  960.  
  961. var id, idx, item, qset;
  962. var grp_list = quiz.group_list = [];
  963. quiz.stats.total = 0;
  964. switch (pairing) {
  965. case 'disabled':
  966. var qna = ['char2mean','char2read','read2mean','mean2read','aud2mean','aud2read'];
  967. for (id in quiz.items) {
  968. item = quiz.items[id];
  969. for (idx in qna) {
  970. var qtype = qna[idx];
  971. if (valid(qtype)) {
  972. grp_list.push({item:item, qna:[qtype], order:Math.random()});
  973. }
  974. }
  975. }
  976. break;
  977.  
  978. case 'reading_first':
  979. for (id in quiz.items) {
  980. item = quiz.items[id];
  981. qset = [];
  982. if (valid('char2read')) qset.push('char2read');
  983. if (valid('char2mean')) qset.push('char2mean');
  984. if (qset.length > 0) grp_list.push({item:item, qna:qset, order:Math.random()});
  985. if (valid('mean2read')) grp_list.push({item:item, qna:['mean2read'], order:Math.random()});
  986. if (valid('read2mean')) grp_list.push({item:item, qna:['read2mean'], order:Math.random()});
  987. qset = [];
  988. if (valid('aud2read')) qset.push('aud2read');
  989. if (valid('aud2mean')) qset.push('aud2mean');
  990. if (qset.length > 0) grp_list.push({item:item, qna:qset, order:Math.random()});
  991. }
  992. break;
  993.  
  994. case 'meaning_first':
  995. for (id in quiz.items) {
  996. item = quiz.items[id];
  997. qset = [];
  998. if (valid('char2mean')) qset.push('char2mean');
  999. if (valid('char2read')) qset.push('char2read');
  1000. if (qset.length > 0) grp_list.push({item:item, qna:qset, order:Math.random()});
  1001. if (valid('read2mean')) grp_list.push({item:item, qna:['read2mean'], order:Math.random()});
  1002. if (valid('mean2read')) grp_list.push({item:item, qna:['mean2read'], order:Math.random()});
  1003. qset = [];
  1004. if (valid('aud2mean')) qset.push('aud2mean');
  1005. if (valid('aud2read')) qset.push('aud2read');
  1006. if (qset.length > 0) grp_list.push({item:item, qna:qset, order:Math.random()});
  1007. }
  1008. break;
  1009.  
  1010. case 'random_order':
  1011. for (id in quiz.items) {
  1012. item = quiz.items[id];
  1013. qset = [];
  1014. if (Math.random() < 0.5) {
  1015. if (valid('char2read')) qset.push('char2read');
  1016. if (valid('char2mean')) qset.push('char2mean');
  1017. } else {
  1018. if (valid('char2mean')) qset.push('char2mean');
  1019. if (valid('char2read')) qset.push('char2read');
  1020. }
  1021. if (qset.length > 0) grp_list.push({item:item, qna:qset, order:Math.random()});
  1022. if (valid('mean2read')) grp_list.push({item:item, qna:['mean2read'], order:Math.random()});
  1023. if (valid('read2mean')) grp_list.push({item:item, qna:['read2mean'], order:Math.random()});
  1024. qset = [];
  1025. if (Math.random() < 0.5) {
  1026. if (valid('aud2read')) qset.push('aud2read');
  1027. if (valid('aud2mean')) qset.push('aud2mean');
  1028. } else {
  1029. if (valid('aud2mean')) qset.push('aud2mean');
  1030. if (valid('aud2read')) qset.push('aud2read');
  1031. }
  1032. if (qset.length > 0) grp_list.push({item:item, qna:qset, order:Math.random()});
  1033. }
  1034. break;
  1035. }
  1036.  
  1037. grp_list.sort(function(a,b){return a.order - b.order;});
  1038. var serial_list = quiz.serial_list = [];
  1039. for (var idx1 in grp_list) {
  1040. for (var idx2 in grp_list[idx1].qna) {
  1041. serial_list.push([idx1, idx2]);
  1042. }
  1043. }
  1044. quiz.qinfo.cache = {};
  1045. quiz.stats.real_total = quiz.stats.total;
  1046. if (settings.max_quiz_size > 0) quiz.stats.total = Math.min(quiz.stats.total, settings.max_quiz_size);
  1047.  
  1048. function valid(qtype) {
  1049. var valid = ((qpreset[qtype] === true) && (valid_question_types[qtype][item.object] === true));
  1050. if (valid) quiz.stats.total++;
  1051. return valid;
  1052. }
  1053. }
  1054.  
  1055. //########################################################################
  1056. // QUIZ
  1057. //########################################################################
  1058.  
  1059. //========================================================================
  1060. // jw_distance() - Jaro-Winkler Distance
  1061. //------------------------------------------------------------------------
  1062. function jw_distance(a, c) {var h,b,d,k,e,g,f,l,n,m,p;if(a.length>c.length) {c=[c,a];a=c[0];c=c[1];}k=~~Math.max(0,c.length/2-1);e=[];g=[];b=n=0;for(p=a.length;n<p;b=++n){for(h=a[b],l=Math.max(0,b-k),f=Math.min(b+k+1,c.length),d=m=l;l<=f?m<f:m>f;d=l<=f?++m:--m){if(g[d]===undefined&&h===c[d]){e[b]=h;g[d]=c[d];break;}}}e=e.join("");g=g.join("");d=e.length;if(d){b=f=k=0;for(l=e.length;f<l;b=++f){h=e[b];if(h!==g[b])k++;}b=g=e=0;for(f=a.length;g<f;b=++g){h=a[b];if(h===c[b])e++;else break;}a=(d/a.length+d/c.length+(d-~~(k/2))/d)/3;a+=0.1*Math.min(e,4)*(1-a);}else{a=0;}return a;}
  1063.  
  1064. //========================================================================
  1065. // to_title_case() - Make first letter of each word upper-case.
  1066. //------------------------------------------------------------------------
  1067. function to_title_case(str) {return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});}
  1068.  
  1069. //========================================================================
  1070. // start_quiz()
  1071. //------------------------------------------------------------------------
  1072. function start_quiz(options) {
  1073. if (!options) options = {};
  1074. if (options.keep_round_count !== true) options.keep_round_count = false; // Default 'false'
  1075. if (!options.keep_round_count) quiz.stats.round = 1;
  1076. quiz.stats.correct = 0;
  1077. quiz.stats.incorrect = 0;
  1078. quiz.index = 0;
  1079. quiz.shuffle();
  1080. if (quiz.stats.total === 0) return set_mode('no_items');
  1081. set_mode('question');
  1082. }
  1083.  
  1084. //========================================================================
  1085. // requiz()
  1086. //------------------------------------------------------------------------
  1087. function requiz() {
  1088. quiz.do_requiz = true;
  1089. quiz.next();
  1090. }
  1091.  
  1092. //========================================================================
  1093. // set_mode()
  1094. //------------------------------------------------------------------------
  1095. function set_mode(mode) {
  1096. var dialog = quiz.dialog;
  1097. if (mode === 'previous') mode = quiz.last_mode;
  1098. dialog.attr('data-mode', mode);
  1099. switch (mode) {
  1100. case 'loading':
  1101. dialog.attr('data-itype', 'loading');
  1102. dialog.attr('data-atype', 'loading');
  1103. dialog.find('.atype').html('Loading...');
  1104. break;
  1105.  
  1106. case 'no_items':
  1107. dialog.attr('data-itype', 'loading');
  1108. dialog.attr('data-atype', 'reading');
  1109. dialog.find('.atype').html('No questions found!');
  1110. break;
  1111.  
  1112. case 'question':
  1113. ask_question();
  1114. break;
  1115.  
  1116. case 'summary':
  1117. dialog.attr('data-atype', 'reading');
  1118. dialog.find('.atype').html('[Enter] for new quiz, [Esc] to return');
  1119. dialog.find('.answer input').val('').prop('readonly', true);
  1120. dialog.attr('data-result', '');
  1121. populate_errors();
  1122. break;
  1123. }
  1124. if (quiz.mode !== quiz.last_mode) quiz.last_mode = quiz.mode;
  1125. quiz.mode = mode;
  1126. }
  1127.  
  1128. function is_svg(img) {return (img.content_type === 'image/svg+xml') && (img.metadata.inline_styles === true);}
  1129.  
  1130. //========================================================================
  1131. // ask_question()
  1132. //------------------------------------------------------------------------
  1133. function ask_question(erase_old_answer) {
  1134. var dialog = quiz.dialog;
  1135. var qinfo = quiz.qinfo.load(quiz.index);
  1136.  
  1137. toggle_help('off');
  1138. dialog.attr('data-itype', qinfo.item.type);
  1139. dialog.attr('data-qtype', qinfo.question.type);
  1140. dialog.attr('data-atype', qinfo.answer.type);
  1141. if (quiz.message_timer) {
  1142. clearTimeout(quiz.message_timer);
  1143. delete quiz.message_timer;
  1144. }
  1145. dialog.removeClass('message');
  1146.  
  1147. // Draw the question
  1148. var question = dialog.find('.question');
  1149. question.attr('lang', qinfo.question.lang);
  1150. if (qinfo.question.html || qinfo.item.type !== 'radical') {
  1151. question.html(qinfo.question.html);
  1152. } else {
  1153. qinfo.question.svg_promise.then(function(qinfo){
  1154. if (quiz.index === qinfo.index) question.html(qinfo.question.html);
  1155. });
  1156. }
  1157.  
  1158. // Initialize the answer
  1159. var input = $('#ss_quiz .answer input');
  1160. var old_answer = get_user_answer(quiz.index);
  1161. if (erase_old_answer) {
  1162. if (old_answer[0] !== '') quiz.stats[old_answer[0]]--;
  1163. update_quiz_stats();
  1164. set_user_answer(quiz.index, '', '');
  1165. old_answer = ['', ''];
  1166. }
  1167. if (old_answer[0] !== '') {
  1168. dialog.attr('data-result', old_answer[0]);
  1169. input.val(old_answer[1]).prop('readonly', true);
  1170. } else {
  1171. dialog.attr('data-result', '');
  1172. input.val('').prop('readonly', false);
  1173. }
  1174.  
  1175. if (qinfo.answer.lang === 'ja') {
  1176. if (input.attr('lang') !== 'ja') {
  1177. input.attr('lang', 'ja');
  1178. wanakana.bind(input[0], {IMEMode:true});
  1179. }
  1180. } else {
  1181. if (input.attr('lang') === 'ja') {
  1182. input.attr('lang', 'en');
  1183. wanakana.unbind(input[0]);
  1184. }
  1185. }
  1186.  
  1187. // Populate the help window
  1188. if (qinfo.question.type !== 'characters' && qinfo.answer.type === 'reading') {
  1189. dialog.find('.help').attr('lang',qinfo.answer.lang).html(to_title_case(qinfo.answer.good.join(', '))+(' ('+qinfo.item.object.data.characters+')')+(qinfo.answer.help_suffix || ''));
  1190. } else {
  1191. dialog.find('.help').attr('lang',qinfo.answer.lang).html(to_title_case(qinfo.answer.good.join(', '))+(qinfo.answer.help_suffix || ''));
  1192. }
  1193. dialog.find('.atype').html(qinfo.answer.html);
  1194.  
  1195. // Update progress stats
  1196. update_quiz_stats();
  1197.  
  1198. // If question is audio, play audio now.
  1199. if (!erase_old_answer && qinfo.question.type === 'audio') {
  1200. play_audio(true /* force_play */, qinfo);
  1201. }
  1202.  
  1203. input.focus();
  1204.  
  1205. quiz.qinfo.prep(quiz.index);
  1206. }
  1207.  
  1208. //========================================================================
  1209. // play_audio()
  1210. //------------------------------------------------------------------------
  1211. function play_audio(force_play, qinfo) {
  1212. if (quiz.settings.mute_audio) return;
  1213. if (!force_play && !quiz.settings.play_audio) return;
  1214. if (!qinfo) qinfo = quiz.qinfo.load(quiz.index);
  1215. if (!qinfo) return;
  1216. if (!qinfo.question.audio_promise) return;
  1217. qinfo.question.audio_promise.then(function(qinfo){
  1218. if (!((quiz.index === qinfo.index) ||
  1219. (quiz.settings.lightning_mode && quiz.index === (qinfo.index + 1)))) return;
  1220. qinfo.question.audio.currentTime = 0;
  1221. qinfo.question.audio.play();
  1222. });
  1223. }
  1224.  
  1225. //========================================================================
  1226. // load_qinfo()
  1227. //------------------------------------------------------------------------
  1228. function load_qinfo(index) {
  1229. if (index < 0 || index >= quiz.stats.total) return null;
  1230. if (!quiz.qinfo.cache[index]) populate_qinfo(index);
  1231. return quiz.qinfo.cache[index];
  1232. }
  1233.  
  1234. //========================================================================
  1235. // prep_qinfo()
  1236. //------------------------------------------------------------------------
  1237. function prep_qinfo(index) {
  1238. Object.keys(quiz.qinfo.cache).forEach(function(cache_idx){
  1239. if (cache_idx < index-2 || cache_idx > index+2) {
  1240. delete quiz.qinfo.cache[cache_idx];
  1241. }
  1242. });
  1243. for (var ofs = 1; ofs <= 2; ofs++) {
  1244. populate_qinfo(index+ofs);
  1245. }
  1246. }
  1247.  
  1248. //========================================================================
  1249. // populate_qinfo()
  1250. //------------------------------------------------------------------------
  1251. function populate_qinfo(index) {
  1252. if (index < 0 || index >= quiz.stats.total) return;
  1253. if (quiz.qinfo.cache[index]) return;
  1254. var qinfo = {index:index, item:{}, question:{}, answer:{}};
  1255. quiz.qinfo.cache[index] = qinfo;
  1256.  
  1257. var grp_idx = quiz.serial_list[index];
  1258. var group = quiz.group_list[grp_idx[0]];
  1259. var item = group.item;
  1260. var qnatype = group.qna[grp_idx[1]];
  1261. qinfo.first_in_group = (grp_idx[1] == 0);
  1262. qinfo.item.type = item.object;
  1263. qinfo.item.object = item;
  1264. qinfo.question.type = {
  1265. char2read:'characters', char2mean:'characters', mean2read:'meaning',
  1266. read2mean:'reading', aud2read:'audio', aud2mean:'audio'
  1267. }[qnatype];
  1268. qinfo.answer.type = {
  1269. char2read:'reading', char2mean:'meaning', mean2read:'reading',
  1270. read2mean:'meaning', aud2read:'reading', aud2mean:'meaning'
  1271. }[qnatype];
  1272. qinfo.answer.html = to_title_case((qinfo.item.type==='kana_vocabulary'?'Kana Vocabulary':qinfo.item.type)+' '+qinfo.answer.type);
  1273.  
  1274. var synonyms = [];
  1275. try {synonyms = item.study_materials.meaning_synonyms || [];} catch(e) {}
  1276. var meanings = item.data.meanings.map(meaning => meaning.meaning);
  1277. if (typeof item.data.auxiliary_meanings !== 'undefined') {
  1278. meanings = meanings.concat(item.data.auxiliary_meanings.filter(m=>m.type==='whitelist').map(m=>m.meaning))
  1279. }
  1280. if (quiz.settings.synonyms_order === 'first') {
  1281. meanings = synonyms.concat(meanings).map(meaning => meaning.toLowerCase());
  1282. } else {
  1283. meanings = meanings.concat(synonyms).map(meaning => meaning.toLowerCase());
  1284. }
  1285.  
  1286. if (qinfo.item.type === 'vocabulary' || qinfo.item.type === 'kana_vocabulary') {
  1287. qinfo.question.audio_promise = new Promise(function(resolve, reject){
  1288. qinfo.question.audio = new Audio();
  1289. qinfo.question.audio.oncanplaythrough = function(){
  1290. resolve(qinfo);
  1291. }
  1292. if (item.id !== undefined) {
  1293. var audio_sources = item.data.pronunciation_audios;
  1294. var filtered_sources;
  1295. switch (quiz.settings.audio_type) {
  1296. case 'mp3': filtered_sources = audio_sources.filter(a => a.content_type == 'audio/mpeg'); break;
  1297. case 'ogg': filtered_sources = audio_sources.filter(a => a.content_type == 'audio/ogg'); break;
  1298. default: filtered_sources = audio_sources;
  1299. }
  1300. if (filtered_sources.length !== 0) audio_sources = filtered_sources;
  1301. switch (quiz.settings.audio_gender) {
  1302. case 'male': filtered_sources = audio_sources.filter(a => a.metadata.gender == 'male'); break;
  1303. case 'female': filtered_sources = audio_sources.filter(a => a.metadata.gender == 'female'); break;
  1304. case 'rotate':
  1305. quiz.gender = quiz.gender || 'female';
  1306. quiz.gender = (quiz.gender === 'female' ? 'male' : 'female');
  1307. filtered_sources = audio_sources.filter(a => a.metadata.gender == quiz.gender);
  1308. break;
  1309. default: filtered_sources = audio_sources;
  1310. }
  1311. if (filtered_sources.length !== 0) audio_sources = filtered_sources;
  1312. if (audio_sources.length === 0) {
  1313. qinfo.question.audio.src = null;
  1314. } else {
  1315. qinfo.question.audio.src = audio_sources[Math.floor(Math.random() * audio_sources.length)].url;
  1316. }
  1317. }
  1318. });
  1319. }
  1320.  
  1321. switch (qinfo.question.type) {
  1322. case 'characters':
  1323. qinfo.question.lang = 'ja';
  1324. if (qinfo.item.type === 'radical' && !item.data.characters) {
  1325. var svg_url = item.data.character_images.filter(is_svg)[0].url;
  1326. qinfo.question.svg_promise = wkof.load_file(svg_url).then(function(svg){
  1327. qinfo.question.html = svg.replace(/svg /, 'svg class="radical"');
  1328. return qinfo;
  1329. });
  1330. } else {
  1331. qinfo.question.html = item.data.characters;
  1332. }
  1333. break;
  1334.  
  1335. case 'reading':
  1336. qinfo.question.lang = 'ja';
  1337. qinfo.question.html = item.data.readings.map(reading => reading.reading).join(', ');
  1338. break;
  1339.  
  1340. case 'meaning':
  1341. qinfo.question.lang = 'en';
  1342. qinfo.question.html = to_title_case(meanings.join(', '));
  1343. break;
  1344.  
  1345. case 'audio':
  1346. qinfo.question.lang = 'ja';
  1347. qinfo.question.html = '<span class="fa fa-audio"></span>';
  1348. qinfo.answer.help_suffix = '<br><span lang="ja">('+item.data.characters+')</span>';
  1349. break;
  1350. }
  1351.  
  1352. var idx, idx2, reading;
  1353. qinfo.answer.other = [];
  1354. qinfo.answer.bad = [];
  1355. qinfo.answer.reading_type = '';
  1356. switch (qinfo.answer.type) {
  1357. case 'reading':
  1358. qinfo.answer.good = [];
  1359. qinfo.answer.lang = 'ja';
  1360. if (qinfo.item.type === 'kana_vocabulary') {
  1361. qinfo.answer.good.push(item.data.characters);
  1362. } else {
  1363. for (idx in item.data.readings) {
  1364. reading = item.data.readings[idx];
  1365. if (qinfo.item.type === 'vocabulary' || reading.accepted_answer) {
  1366. qinfo.answer.good.push(reading.reading);
  1367. if (qinfo.item.type === 'kanji') {
  1368. qinfo.answer.reading_type = reading.type.replace('yomi','\'yomi');
  1369. }
  1370. } else {
  1371. qinfo.answer.other.push(reading.reading);
  1372. }
  1373. }
  1374. }
  1375. qinfo.answer.bad = meanings;
  1376. break;
  1377.  
  1378. case 'meaning':
  1379. qinfo.answer.good = meanings;
  1380. qinfo.answer.lang = 'en';
  1381. if (!item.data.readings) break;
  1382. for (idx in item.data.readings) {
  1383. reading = item.data.readings[idx];
  1384.  
  1385. if (qinfo.item.type === 'vocabulary' || reading.accepted_answer) {
  1386. qinfo.answer.bad.push(reading.reading);
  1387. }
  1388. }
  1389. break;
  1390. }
  1391. }
  1392.  
  1393. //========================================================================
  1394. // get_user_answer()
  1395. //------------------------------------------------------------------------
  1396. function get_user_answer(index) {
  1397. var grp_idx = quiz.serial_list[index];
  1398. var group = quiz.group_list[grp_idx[0]];
  1399. if (!group.answer || !group.answer[grp_idx[1]]) return ['', ''];
  1400. return group.answer[grp_idx[1]];
  1401. }
  1402.  
  1403. //========================================================================
  1404. // set_user_answer()
  1405. //------------------------------------------------------------------------
  1406. function set_user_answer(index, status, answer) {
  1407. var grp_idx = quiz.serial_list[index];
  1408. var group = quiz.group_list[grp_idx[0]];
  1409. if (!group.answer) group.answer = [];
  1410. group.answer[grp_idx[1]] = [status, answer];
  1411. }
  1412.  
  1413. //========================================================================
  1414. // submit_answer()
  1415. //------------------------------------------------------------------------
  1416. function submit_answer() {
  1417. var dialog = quiz.dialog;
  1418. var input = $('#ss_quiz .answer input');
  1419.  
  1420. var qinfo = quiz.qinfo.load(quiz.index);
  1421. var item = qinfo.item.object;
  1422. var itype = qinfo.item.type;
  1423. var atype = qinfo.answer.type;
  1424. var raw_answer = input.val().trim();
  1425. var answer = raw_answer;
  1426. var action = 'fail';
  1427. var msgcfg = quiz.settings.messages;
  1428. var is_exact = true;
  1429. var is_multi = false;
  1430. var message;
  1431.  
  1432. if (answer === '') {
  1433. atype = 'ignore';
  1434. action = 'shake';
  1435. }
  1436.  
  1437. switch (atype) {
  1438. case 'reading':
  1439. answer = wanakana.toHiragana(answer);
  1440. if (qinfo.answer.good.indexOf(answer) >= 0 || qinfo.answer.good.indexOf(raw_answer) >= 0) {
  1441. action = 'correct';
  1442. if (qinfo.answer.good.length > 1) is_multi = true;
  1443. if (is_multi && msgcfg.show_multi_reading) message = 'This item has multiple readings';
  1444. } else if (itype === 'kanji' && qinfo.answer.other.indexOf(answer) >= 0) {
  1445. action = 'shake';
  1446. message = 'We\'re looking for the '+to_title_case(qinfo.answer.reading_type)+' reading';
  1447. } else {
  1448. var bad = qinfo.answer.bad.map(function(english){
  1449. return wanakana.toHiragana(english.toLowerCase());
  1450. });
  1451. if (bad.indexOf(answer) >= 0) {
  1452. action = 'shake';
  1453. message = 'We\'re looking for the reading, not the meaning';
  1454. } else if (!wanakana.isKana(answer)) {
  1455. action = 'shake';
  1456. message = 'Your answer contains invalid characters';
  1457. } else {
  1458. action = 'incorrect';
  1459. }
  1460. }
  1461. break;
  1462.  
  1463. case 'meaning':
  1464. var is_correct = false;
  1465. is_exact = false;
  1466. answer = answer.toLowerCase();
  1467. var allow_typos = (quiz.settings.allow_typos === true);
  1468. for (var idx in qinfo.answer.good) {
  1469. var good_answer = qinfo.answer.good[idx];
  1470. if (answer === good_answer) {
  1471. is_correct = true;
  1472. is_exact = true;
  1473. break;
  1474. } else if (allow_typos && (good_answer.match(/[0-9]/) == null) && jw_distance(good_answer, answer) > 0.9) {
  1475. is_correct = true;
  1476. }
  1477. }
  1478. if (is_correct) {
  1479. action = 'correct';
  1480. if (!is_exact && msgcfg.show_slightly_off === true) message = "Your answer was slightly off";
  1481. } else {
  1482. var alt_answer = wanakana.toHiragana(answer,{IMEMode:true});
  1483. if (qinfo.answer.bad.indexOf(alt_answer) >= 0) {
  1484. action = 'shake';
  1485. message = 'We\'re looking for the meaning, not the reading';
  1486. } else {
  1487. action = 'incorrect';
  1488. }
  1489. }
  1490. break;
  1491. }
  1492.  
  1493. if (action !== 'shake') set_user_answer(quiz.index, action, answer);
  1494. switch (action) {
  1495. case 'correct':
  1496. quiz.stats.correct++;
  1497.  
  1498. // If question is reading, play audio now.
  1499. if (qinfo.answer.type === 'reading' && qinfo.question.type !== 'audio') {
  1500. play_audio(false /* force_play */, qinfo);
  1501. }
  1502.  
  1503. if ((quiz.settings.lightning_mode === true) &&
  1504. (!is_multi || !msgcfg.show_multi_reading || !msgcfg.halt_multi_reading) &&
  1505. (is_exact || !msgcfg.show_slightly_off || !msgcfg.halt_slightly_off )) {
  1506.  
  1507. return quiz.next();
  1508. } else {
  1509. update_quiz_stats();
  1510. input.prop('readonly', true);
  1511. }
  1512. dialog.attr('data-result', 'correct');
  1513. break;
  1514.  
  1515. case 'shake':
  1516. shake(input);
  1517. input.focus();
  1518. break;
  1519.  
  1520. case 'incorrect':
  1521. quiz.stats.incorrect++;
  1522. update_quiz_stats();
  1523. input.prop('readonly', true);
  1524. dialog.attr('data-result', 'incorrect');
  1525.  
  1526. if (quiz.settings.autoshow_correct && !quiz.showing_help) {
  1527. toggle_help('on');
  1528. }
  1529. break;
  1530. }
  1531.  
  1532. if (message) {
  1533. dialog.find('.message').text(message);
  1534. dialog.addClass('message');
  1535. if (quiz.message_timer) {
  1536. clearTimeout(quiz.message_timer);
  1537. delete quiz.message_timer;
  1538. }
  1539. quiz.message_timer = setTimeout(function(){
  1540. dialog.removeClass('message');
  1541. quiz.message_timer = undefined;
  1542. },2750);
  1543. }
  1544. }
  1545.  
  1546. //========================================================================
  1547. // shake()
  1548. //------------------------------------------------------------------------
  1549. function shake(elem) {
  1550. var dist = '15px';
  1551. var speed = 75;
  1552. var right = {padding:'0 '+dist+' 0 0'}, left = {padding:'0 0 0 '+dist}, center = {padding:"0 0 0 0"};
  1553.  
  1554. elem.animate(left,speed/2).animate(right,speed)
  1555. .animate(left,speed).animate(right,speed)
  1556. .animate(left,speed).animate(center,speed/2);
  1557. }
  1558.  
  1559. //========================================================================
  1560. // prev_question()
  1561. //------------------------------------------------------------------------
  1562. function prev_question() {
  1563. switch (quiz.mode) {
  1564. case 'question':
  1565. if (quiz.index > 0) quiz.index--;
  1566. quiz.ask();
  1567. break;
  1568.  
  1569. case 'summary':
  1570. if (quiz.index === quiz.stats.total) {
  1571. quiz.index = quiz.stats.total - 1;
  1572. update_quiz_stats();
  1573. }
  1574. set_mode('question');
  1575. break;
  1576. }
  1577. quiz.ask();
  1578. }
  1579.  
  1580. //========================================================================
  1581. // next_question()
  1582. //------------------------------------------------------------------------
  1583. function next_question() {
  1584. switch (quiz.mode) {
  1585. case 'question':
  1586. if (quiz.index < quiz.stats.total-1) {
  1587. quiz.index++;
  1588. quiz.ask();
  1589. } else {
  1590. quiz.index = quiz.stats.total;
  1591. update_quiz_stats();
  1592. set_mode('summary');
  1593. }
  1594. break;
  1595.  
  1596. case 'summary':
  1597. if (quiz.do_requiz) {
  1598. delete quiz.do_requiz;
  1599. if (!quiz.original_items) {
  1600. quiz.original_items = quiz.items;
  1601. }
  1602. quiz.items = quiz.requiz_items;
  1603. delete quiz.requiz_items;
  1604. } else {
  1605. delete quiz.requiz_items;
  1606. if (quiz.original_items) {
  1607. quiz.items = quiz.original_items;
  1608. delete quiz.original_items;
  1609. }
  1610. quiz.stats.round++;
  1611. }
  1612. quiz.start({keep_round_count:true});
  1613. break;
  1614. }
  1615. }
  1616.  
  1617. //========================================================================
  1618. // populate_errors()
  1619. //------------------------------------------------------------------------
  1620. function populate_errors() {
  1621. var dialog = quiz.dialog;
  1622. var percent_elem = dialog.find('.summary .percent');
  1623. var errors_elem = dialog.find('.summary .errors');
  1624.  
  1625. var total = quiz.stats.correct + quiz.stats.incorrect;
  1626. var percent = (total === 0 ? 100 : 100 * quiz.stats.correct / total);
  1627. percent_elem.text((Math.round(percent*100)/100).toString()+'%');
  1628. if (total === quiz.stats.correct) {
  1629. $('#ss_quiz .summary .requiz').addClass('hidden');
  1630. } else {
  1631. $('#ss_quiz .summary .requiz').removeClass('hidden');
  1632. }
  1633.  
  1634. var idx;
  1635. var err_list = dialog.find('.summary .errors');
  1636. err_list.html('');
  1637. var requiz_items = {};
  1638. quiz.requiz_items = [];
  1639. for (idx = 0; idx < quiz.stats.total; idx++) {
  1640. var grp_idx = quiz.serial_list[idx];
  1641. var group = quiz.group_list[grp_idx[0]];
  1642. if (!group.answer) continue;
  1643. var answer = group.answer[grp_idx[1]];
  1644. if (!answer || answer[0] !== 'incorrect') continue;
  1645. var item = group.item;
  1646. if (!requiz_items[item.id]) {
  1647. requiz_items[item.id] = 1;
  1648. quiz.requiz_items.push(item);
  1649. }
  1650. var itype = (item.object === 'kana_vocabulary' ? 'vocabulary' : item.object);
  1651. var qnatype = group.qna[grp_idx[1]];
  1652. answer = answer[1];
  1653. var qtype = {
  1654. char2read:'characters', char2mean:'characters', mean2read:'meaning',
  1655. read2mean:'reading', aud2read:'audio', aud2mean:'audio'
  1656. }[qnatype];
  1657. var atype = {
  1658. char2read:'reading', char2mean:'meaning', mean2read:'reading',
  1659. read2mean:'meaning', aud2read:'reading', aud2mean:'meaning'
  1660. }[qnatype];
  1661. var qlang = (qtype === 'meaning' ? 'en' : 'ja');
  1662. var alang = (atype === 'meaning' ? 'en' : 'ja');
  1663. var qtitle = to_title_case(itype+' '+atype);
  1664. var atitle;
  1665. switch (atype) {
  1666. case 'meaning':
  1667. var synonyms = [];
  1668. try {synonyms = item.study_materials.meaning_synonyms || [];} catch(e) {}
  1669. var meanings = item.data.meanings.map(meaning => meaning.meaning);
  1670. if (quiz.settings.synonyms_order === 'first') {
  1671. meanings = synonyms.concat(meanings).map(meaning => meaning.toLowerCase());
  1672. } else {
  1673. meanings = meanings.concat(synonyms).map(meaning => meaning.toLowerCase());
  1674. }
  1675. atitle = meanings.join(', ');
  1676. break;
  1677. case 'reading':
  1678. atitle = to_title_case(item.data.readings.map(reading => reading.reading).join(', '));
  1679. if (qtype !== 'characters') atitle += ' ('+item.data.characters+')';
  1680. break;
  1681. }
  1682. var qtext = item.data.slug;
  1683. if (qtype === 'audio') {
  1684. qtext += ' <i class="fa fa-audio icon-style"></i>';
  1685. } else if (qtype === 'meaning') {
  1686. synonyms = [];
  1687. try {synonyms = item.study_materials.meaning_synonyms || [];} catch(e) {}
  1688. meanings = item.data.meanings.map(meaning => meaning.meaning);
  1689. if (quiz.settings.synonyms_order === 'first') {
  1690. meanings = synonyms.concat(meanings).map(meaning => meaning.toLowerCase());
  1691. } else {
  1692. meanings = meanings.concat(synonyms).map(meaning => meaning.toLowerCase());
  1693. }
  1694. qtext = meanings.join(', ');
  1695. }
  1696. var atext = answer + ' <i class="fa fa-times-circle wrong"></i>';
  1697. err_list.append(
  1698. '<li><span class="que" lang="'+qlang+'" title="'+qtitle+'">'+qtext+'</span>'+
  1699. '<i class="fa fa-long-arrow-right icon-style"></i>'+
  1700. '<span class="ans" lang="'+alang+'" title="'+atitle+'">'+atext+'</span></li>'
  1701. );
  1702. }
  1703. }
  1704.  
  1705. //========================================================================
  1706. // update_quiz_stats()
  1707. //------------------------------------------------------------------------
  1708. function update_quiz_stats() {
  1709. var stats = $('#ss_quiz .stats_labels');
  1710. var stats_width = quiz.stats.total.toString().length; // Number of digits in quiz counter
  1711. var remaining = quiz.stats.total - quiz.index;
  1712. stats.html(
  1713. 'Round: '+(' '+quiz.stats.round).slice(-1*stats_width)+'<br>'+
  1714. 'Remaining: '+(' '+remaining).slice(-1*stats_width)+'<br>'+
  1715. 'Correct: '+(' '+quiz.stats.correct).slice(-1*stats_width)+'<br>'+
  1716. 'Incorrect: '+(' '+quiz.stats.incorrect).slice(-1*stats_width)
  1717. );
  1718. }
  1719.  
  1720. //========================================================================
  1721. // quiz_key_handler()
  1722. //------------------------------------------------------------------------
  1723. var keycode_xlat = {
  1724. '8':'Backspace', '13':'Enter', '27':'Escape', '37':'ArrowLeft', '39':'ArrowRight', '65':'KeyA',
  1725. '69':'KeyE', '72':'KeyH', '76':'KeyL', '80':'KeyP', '82':'KeyR', '83':'KeyS', '112':'F1',
  1726. };
  1727. function quiz_key_handler(e) {
  1728. if (quiz_settings_state === 'open') return true;
  1729. var input = quiz.dialog.find('.answer input');
  1730. var input_readonly = input.prop('readonly');
  1731. var code;
  1732. if (e.type === 'keydown') {
  1733. if (e.originalEvent.keyCode) {
  1734. code = keycode_xlat[e.originalEvent.keyCode] || 'Unknown';
  1735. } else {
  1736. code = e.originalEvent.code;
  1737. }
  1738. } else {
  1739. code = String.fromCharCode(e.charCode);
  1740. }
  1741.  
  1742. if (code === 'Enter') {
  1743. if (quiz.mode === 'question' && !input_readonly) {
  1744. quiz.submit(e);
  1745. } else {
  1746. quiz.next();
  1747. }
  1748. } else if (code === 'Escape') {
  1749. process_escape();
  1750. } else if (code === 'F1' || code === '?') {
  1751. toggle_help();
  1752. } else if (code === 'Backspace') {
  1753. // Prevent backspace from navigating away from the page.
  1754. if (quiz.mode !== 'question') return false;
  1755. if (input_readonly) quiz.ask(true /* erase_old_answer */);
  1756. return true;
  1757. } else if (e.ctrlKey || e.metaKey) {
  1758. switch (code) {
  1759. case 'KeyA':
  1760. if (e.shiftKey) {
  1761. toggle_audio();
  1762. } else {
  1763. play_audio(true);
  1764. }
  1765. break;
  1766. case 'KeyE': process_escape(); break; // End
  1767. case 'KeyH': toggle_help(); break; // Help
  1768. case 'KeyL': toggle_lightning(); break; // Lightning
  1769. case 'KeyP': toggle_pairing(); break; // Pairing
  1770. case 'KeyR': // Re-quiz
  1771. if (quiz.mode !== 'summary' || quiz.dialog.find('.summary .requiz').hasClass('hidden')) break;
  1772. quiz.requiz();
  1773. break;
  1774. case 'KeyS': manual_shuffle(); break;
  1775. case 'ArrowLeft': quiz.prev(); break;
  1776. case 'ArrowRight': quiz.next(); break;
  1777. default: return true;
  1778. }
  1779. } else {
  1780. var is_special = (e.key.length !== 1);
  1781. if (is_special) return true;
  1782.  
  1783. // Let the browser handle regular keys in the input box
  1784. if (e.target === input[0]) return true;
  1785.  
  1786. // Let the browser handle all other keys while not in question mode.
  1787. if (quiz.mode !== 'question') return true;
  1788. }
  1789. return false;
  1790. }
  1791.  
  1792. //========================================================================
  1793. // manual_shuffle()
  1794. //------------------------------------------------------------------------
  1795. function manual_shuffle() {
  1796. var keep_round_count = true;
  1797. if (quiz.shuffle_timer === undefined) {
  1798. quiz.shuffle_timer = setTimeout(function(){
  1799. delete quiz.shuffle_timer;
  1800. }, 1000);
  1801. } else {
  1802. clearTimeout(quiz.shuffle_timer);
  1803. delete quiz.shuffle_timer;
  1804. keep_round_count = false;
  1805. }
  1806. quiz.start({keep_round_count:keep_round_count});
  1807. }
  1808.  
  1809. //========================================================================
  1810. // process_escape()
  1811. //------------------------------------------------------------------------
  1812. function process_escape() {
  1813. if (quiz.escape_timer === undefined) {
  1814. quiz.escape_counter = 1;
  1815. quiz.escape_timer = setTimeout(function(){
  1816. delete quiz.escape_counter;
  1817. delete quiz.escape_timer;
  1818. }, 750);
  1819. } else {
  1820. quiz.escape_counter++;
  1821. if (quiz.escape_counter === 3) {
  1822. clearTimeout(quiz.escape_timer);
  1823. delete quiz.escape_timer;
  1824. quiz.close();
  1825. return;
  1826. }
  1827. }
  1828. switch (quiz.mode) {
  1829. case 'question':
  1830. set_mode('summary');
  1831. break;
  1832.  
  1833. case 'summary':
  1834. if (quiz.index === quiz.stats.total) quiz.index = quiz.stats.total-1;
  1835. set_mode('previous');
  1836. break;
  1837. }
  1838. }
  1839.  
  1840. //========================================================================
  1841. // toggle_audio()
  1842. //------------------------------------------------------------------------
  1843. function toggle_audio() {
  1844. var elem = $('#ss_quiz .settings .ss_audio');
  1845. if (quiz.settings.mute_audio) {
  1846. quiz.settings.mute_audio = false;
  1847. quiz.settings.play_audio = false;
  1848. elem.removeClass('mute');
  1849. elem.removeClass('active');
  1850. } else if (quiz.settings.play_audio) {
  1851. quiz.settings.mute_audio = true;
  1852. quiz.settings.play_audio = false;
  1853. elem.addClass('mute');
  1854. elem.removeClass('active');
  1855. } else {
  1856. quiz.settings.mute_audio = false;
  1857. quiz.settings.play_audio = true;
  1858. elem.removeClass('mute');
  1859. elem.addClass('active');
  1860. }
  1861. wkof.Settings.save('ss_quiz');
  1862. }
  1863.  
  1864. //========================================================================
  1865. // toggle_help()
  1866. //------------------------------------------------------------------------
  1867. function toggle_help(value) {
  1868. if (quiz.mode !== 'question') return;
  1869. var elem = $('#ss_quiz .settings .ss_help');
  1870. switch (value) {
  1871. case 'on':
  1872. elem.addClass('active');
  1873. quiz.dialog.addClass('help');
  1874. quiz.showing_help = true;
  1875. break;
  1876. case 'off':
  1877. elem.removeClass('active');
  1878. quiz.dialog.removeClass('help');
  1879. quiz.showing_help = false;
  1880. break;
  1881. default:
  1882. elem.toggleClass('active');
  1883. quiz.dialog.toggleClass('help');
  1884. quiz.showing_help = !quiz.showing_help;
  1885. break;
  1886. }
  1887. var qinfo = quiz.qinfo.load(quiz.index);
  1888. if (quiz.showing_help && qinfo.answer.type === 'reading') play_audio(false /* force_play */);
  1889. }
  1890.  
  1891. //========================================================================
  1892. // toggle_lightning()
  1893. //------------------------------------------------------------------------
  1894. function toggle_lightning() {
  1895. var elem = $('#ss_quiz .settings .ss_lightning');
  1896. elem.toggleClass('active');
  1897. quiz.settings.lightning_mode = elem.hasClass('active');
  1898. wkof.Settings.save('ss_quiz');
  1899. }
  1900.  
  1901. //========================================================================
  1902. // toggle_pairing()
  1903. //------------------------------------------------------------------------
  1904. function toggle_pairing(e, initialize) {
  1905. var elem_pair = $('#ss_quiz .settings .ss_pair');
  1906. var elem_data = elem_pair.find('.data');
  1907. var values = ['disabled', 'reading_first', 'meaning_first', 'random_order'];
  1908. var value = Math.max(0, values.indexOf(quiz.settings.pairing));
  1909.  
  1910. if (!initialize) value = (value + 1) % values.length;
  1911. quiz.settings.pairing = value = values[value];
  1912. wkof.Settings.save('ss_quiz')
  1913.  
  1914. switch (value) {
  1915. case 'disabled': elem_data.text('Disabled'); elem_pair.removeClass('active'); break;
  1916. case 'reading_first': elem_data.text('Reading First'); elem_pair.addClass('active'); break;
  1917. case 'meaning_first': elem_data.text('Meaning First'); elem_pair.addClass('active'); break;
  1918. case 'random_order': elem_data.text('Random Order'); elem_pair.addClass('active'); break;
  1919. }
  1920. if (!initialize) quiz.start({keep_round_count:true});
  1921. }
  1922.  
  1923. //========================================================================
  1924. // drag()
  1925. //------------------------------------------------------------------------
  1926. function drag(e) {
  1927. var dlg = $(e.currentTarget).closest('.dialog');
  1928. var pos = dlg.position();
  1929. var ofs = {x: e.pageX-pos.left, y: e.pageY-pos.top};
  1930. $('body')
  1931. .on('mousemove.ss_quiz_drag touchmove.ss_quiz_drag', function(e){
  1932. dlg.css({left: Math.max(0,e.pageX-ofs.x), top: Math.max(0,e.pageY-ofs.y)});
  1933. })
  1934. .on('mouseup.ss_quiz_drag touchend.ss_quiz_drag', function(e){
  1935. $('body').off('.ss_quiz_drag');
  1936. });
  1937. }
  1938.  
  1939. wkof.set_state('ss_quiz', 'ready');
  1940.  
  1941. })(window.ss_quiz);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址