Wanikani Self-Study Quiz

Quiz yourself on Wanikani items

目前為 2018-06-24 提交的版本,檢視 最新版本

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

QingJ © 2025

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