Wanikani Open Framework

Framework for writing scripts for Wanikani

  1. // ==UserScript==
  2. // @name Wanikani Open Framework
  3. // @namespace rfindley
  4. // @description Framework for writing scripts for Wanikani
  5. // @version 1.2.10
  6. // @match https://www.wanikani.com/*
  7. // @match https://preview.wanikani.com/*
  8. // @copyright 2018-2024, Robin Findley
  9. // @license MIT; http://opensource.org/licenses/MIT
  10. // @run-at document-start
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function(global) {
  15. 'use strict';
  16.  
  17. /* eslint no-multi-spaces: off */
  18. /* globals wkof */
  19.  
  20. const version = '1.2.10';
  21. let ignore_missing_indexeddb = false;
  22.  
  23. //########################################################################
  24. //------------------------------
  25. // Supported Modules
  26. //------------------------------
  27. const supported_modules = {
  28. Apiv2: { url: 'https://update.greasyfork.org/scripts/38581/1402158/Wanikani%20Open%20Framework%20-%20Apiv2%20module.js'},
  29. ItemData: { url: 'https://update.greasyfork.org/scripts/38580/1187212/Wanikani%20Open%20Framework%20-%20ItemData%20module.js'},
  30. Jquery: { url: 'https://update.greasyfork.org/scripts/451078/1091794/Wanikani%20Open%20Framework%20-%20Jquery%20module.js'},
  31. Menu: { url: 'https://update.greasyfork.org/scripts/38578/1489081/Wanikani%20Open%20Framework%20-%20Menu%20module.js'},
  32. Progress: { url: 'https://update.greasyfork.org/scripts/38577/1091792/Wanikani%20Open%20Framework%20-%20Progress%20module.js'},
  33. Settings: { url: 'https://update.greasyfork.org/scripts/38576/1091793/Wanikani%20Open%20Framework%20-%20Settings%20module.js'},
  34. };
  35.  
  36. //########################################################################
  37. //------------------------------
  38. // Published interface
  39. //------------------------------
  40. const published_interface = {
  41. on_pageload: on_pageload, // on_pageload(urls, load_handler [, unload_handler])
  42.  
  43. include: include, // include(module_list) => Promise
  44. ready: ready, // ready(module_list) => Promise
  45.  
  46. load_file: load_file, // load_file(url, use_cache) => Promise
  47. load_css: load_css, // load_css(url, use_cache) => Promise
  48. load_script: load_script, // load_script(url, use_cache) => Promise
  49.  
  50. file_cache: {
  51. dir: {}, // Object containing directory of files.
  52. ls: file_cache_list, // ls()
  53. clear: file_cache_clear, // clear() => Promise
  54. delete: file_cache_delete, // delete(name) => Promise
  55. flush: file_cache_flush, // flush() => Promise
  56. load: file_cache_load, // load(name) => Promise
  57. save: file_cache_save, // save(name, content) => Promise
  58. no_cache:file_nocache, // no_cache(modules)
  59. },
  60.  
  61. on: wait_event, // on(event, callback)
  62. trigger: trigger_event, // trigger(event[, data1[, data2[, ...]]])
  63.  
  64. get_state: get_state, // get(state_var)
  65. set_state: set_state, // set(state_var, value)
  66. wait_state: wait_state, // wait(state_var, value[, callback[, persistent]]) => if no callback, return one-shot Promise
  67.  
  68. version: {
  69. value: version,
  70. compare_to: compare_to, // compare_version(version)
  71. }
  72. };
  73.  
  74. published_interface.support_files = {
  75. 'jquery.js': 'https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js',
  76. 'jquery_ui.js': 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js',
  77. 'jqui_wkmain.css': 'https://raw.githubusercontent.com/rfindley/wanikani-open-framework/1550af8383ec28ad406cf401aee2de4c52446f6c/jqui-wkmain.css',
  78. };
  79.  
  80. //########################################################################
  81.  
  82. function split_list(str) {return str.replace(/、/g,',').replace(/[\s ]+/g,' ').trim().replace(/ *, */g, ',').split(',').filter(function(name) {return (name.length > 0);});}
  83. function promise(){let a,b,c=new Promise(function(d,e){a=d;b=e;});c.resolve=a;c.reject=b;return c;}
  84.  
  85. //########################################################################
  86.  
  87. //------------------------------
  88. // Compare the framework version against a specific version.
  89. //------------------------------
  90. function compare_to(client_version) {
  91. let client_ver = client_version.split('.').map(d => Number(d));
  92. let wkof_ver = version.split('.').map(d => Number(d));
  93. let len = Math.max(client_ver.length, wkof_ver.length);
  94. for (let idx = 0; idx < len; idx++) {
  95. let a = client_ver[idx] || 0;
  96. let b = wkof_ver[idx] || 0;
  97. if (a === b) continue;
  98. if (a < b) return 'newer';
  99. return 'older';
  100. }
  101. return 'same';
  102. }
  103.  
  104. //------------------------------
  105. // Include a list of modules.
  106. //------------------------------
  107. let include_promises = {};
  108.  
  109. function include(module_list) {
  110. if (wkof.get_state('wkof.wkof') !== 'ready') {
  111. return wkof.ready('wkof').then(function(){return wkof.include(module_list);});
  112. }
  113. let include_promise = promise();
  114. let module_names = split_list(module_list);
  115. let script_cnt = module_names.length;
  116. if (script_cnt === 0) {
  117. include_promise.resolve({loaded:[], failed:[]});
  118. return include_promise;
  119. }
  120.  
  121. let done_cnt = 0;
  122. let loaded = [], failed = [];
  123. let no_cache = split_list(localStorage.getItem('wkof.include.nocache') || '');
  124. for (let idx = 0; idx < module_names.length; idx++) {
  125. let module_name = module_names[idx];
  126. let module = supported_modules[module_name];
  127. if (!module) {
  128. failed.push({name:module_name, url:undefined});
  129. check_done();
  130. continue;
  131. }
  132. let await_load = include_promises[module_name];
  133. let use_cache = (no_cache.indexOf(module_name) < 0) && (no_cache.indexOf('*') < 0);
  134. if (!use_cache) file_cache_delete(module.url);
  135. if (await_load === undefined) include_promises[module_name] = await_load = load_script(module.url, use_cache);
  136. await_load.then(push_loaded, push_failed);
  137. }
  138.  
  139. return include_promise;
  140.  
  141. function push_loaded(url) {
  142. loaded.push(url);
  143. check_done();
  144. }
  145.  
  146. function push_failed(url) {
  147. failed.push(url);
  148. check_done();
  149. }
  150.  
  151. function check_done() {
  152. if (++done_cnt < script_cnt) return;
  153. if (failed.length === 0) include_promise.resolve({loaded:loaded, failed:failed});
  154. else include_promise.reject({error:'Failure loading module', loaded:loaded, failed:failed});
  155. }
  156. }
  157.  
  158. //------------------------------
  159. // Wait for all modules to report that they are ready
  160. //------------------------------
  161. function ready(module_list) {
  162. let module_names = split_list(module_list);
  163.  
  164. let ready_promises = [ ];
  165. for (let idx in module_names) {
  166. let module_name = module_names[idx];
  167. ready_promises.push(wait_state('wkof.' + module_name, 'ready'));
  168. }
  169.  
  170. if (ready_promises.length === 0) {
  171. return Promise.resolve();
  172. } else if (ready_promises.length === 1) {
  173. return ready_promises[0];
  174. } else {
  175. return Promise.all(ready_promises);
  176. }
  177. }
  178. //########################################################################
  179.  
  180. //------------------------------
  181. // Load a file asynchronously, and pass the file as resolved Promise data.
  182. //------------------------------
  183. function load_file(url, use_cache) {
  184. let fetch_promise = promise();
  185. let no_cache = split_list(localStorage.getItem('wkof.load_file.nocache') || '');
  186. if (no_cache.indexOf(url) >= 0 || no_cache.indexOf('*') >= 0) use_cache = false;
  187. if (use_cache === true) {
  188. return file_cache_load(url, use_cache).catch(fetch_url);
  189. } else {
  190. return fetch_url();
  191. }
  192.  
  193. // Retrieve file from server
  194. function fetch_url(){
  195. let request = new XMLHttpRequest();
  196. request.onreadystatechange = process_result;
  197. request.open('GET', url, true);
  198. request.send();
  199. return fetch_promise;
  200. }
  201.  
  202. function process_result(event){
  203. if (event.target.readyState !== 4) return;
  204. if (event.target.status >= 400 || event.target.status === 0) return fetch_promise.reject(event.target.status);
  205. if (use_cache) {
  206. file_cache_save(url, event.target.response)
  207. .then(fetch_promise.resolve.bind(null,event.target.response));
  208. } else {
  209. fetch_promise.resolve(event.target.response);
  210. }
  211. }
  212. }
  213.  
  214. //------------------------------
  215. // Load and install a specific file type into the DOM.
  216. //------------------------------
  217. function load_and_append(url, tag_name, location, use_cache) {
  218. url = url.replace(/"/g,'\'');
  219. if (document.querySelector(tag_name+'[uid="'+url+'"]') !== null) return Promise.resolve();
  220. return load_file(url, use_cache).then(append_to_tag);
  221.  
  222. function append_to_tag(content) {
  223. let tag = document.createElement(tag_name);
  224. tag.innerHTML = content;
  225. tag.setAttribute('uid', url);
  226. document.querySelector(location).appendChild(tag);
  227. return url;
  228. }
  229. }
  230.  
  231. //------------------------------
  232. // Load and install a CSS file.
  233. //------------------------------
  234. function load_css(url, use_cache) {
  235. return load_and_append(url, 'style', 'head', use_cache);
  236. }
  237.  
  238. //------------------------------
  239. // Load and install Javascript.
  240. //------------------------------
  241. function load_script(url, use_cache) {
  242. return load_and_append(url, 'script', 'head', use_cache);
  243. }
  244. //########################################################################
  245.  
  246. let state_listeners = {};
  247. let state_values = {};
  248.  
  249. //------------------------------
  250. // Get the value of a state variable, and notify listeners.
  251. //------------------------------
  252. function get_state(state_var) {
  253. return state_values[state_var];
  254. }
  255.  
  256. //------------------------------
  257. // Set the value of a state variable, and notify listeners.
  258. //------------------------------
  259. function set_state(state_var, value) {
  260. let old_value = state_values[state_var];
  261. if (old_value === value) return;
  262. state_values[state_var] = value;
  263.  
  264. // Do listener callbacks, and remove non-persistent listeners
  265. let listeners = state_listeners[state_var];
  266. let persistent_listeners = [ ];
  267. for (let idx in listeners) {
  268. let listener = listeners[idx];
  269. let keep = true;
  270. if (listener.value === value || listener.value === '*') {
  271. keep = listener.persistent;
  272. try {
  273. listener.callback(value, old_value);
  274. } catch (e) {}
  275. }
  276. if (keep) persistent_listeners.push(listener);
  277. }
  278. state_listeners[state_var] = persistent_listeners;
  279. }
  280.  
  281. //------------------------------
  282. // When state of state_var changes to value, call callback.
  283. // If persistent === true, continue listening for additional state changes
  284. // If value is '*', callback will be called for all state changes.
  285. //------------------------------
  286. function wait_state(state_var, value, callback, persistent) {
  287. let promise;
  288. if (callback === undefined) {
  289. promise = new Promise(function(resolve, reject) {
  290. callback = resolve;
  291. });
  292. }
  293. if (state_listeners[state_var] === undefined) state_listeners[state_var] = [ ];
  294. persistent = (persistent === true);
  295. let current_value = state_values[state_var];
  296. if (persistent || value !== current_value) state_listeners[state_var].push({callback:callback, persistent:persistent, value:value});
  297.  
  298. // If it's already at the desired state, call the callback immediately.
  299. if (value === current_value) {
  300. try {
  301. callback(value, current_value);
  302. } catch (err) {}
  303. }
  304. return promise;
  305. }
  306. //########################################################################
  307.  
  308. let event_listeners = {};
  309.  
  310. //------------------------------
  311. // Fire an event, which then calls callbacks for any listeners.
  312. //------------------------------
  313. function trigger_event(event) {
  314. let listeners = event_listeners[event];
  315. if (listeners === undefined) return;
  316. let args = [];
  317. Array.prototype.push.apply(args,arguments);
  318. args.shift();
  319. for (let idx in listeners) { try {
  320. listeners[idx].apply(null,args);
  321. } catch (err) {} }
  322. return global.wkof;
  323. }
  324.  
  325. //------------------------------
  326. // Add a listener for an event.
  327. //------------------------------
  328. function wait_event(event, callback) {
  329. if (event_listeners[event] === undefined) event_listeners[event] = [];
  330. event_listeners[event].push(callback);
  331. return global.wkof;
  332. }
  333.  
  334. //------------------------------
  335. // Add handlers for page load events for a list of URLs.
  336. //------------------------------
  337. let regex_store = {};
  338. let pgld_req_store = [];
  339. let current_page = '!';
  340. function on_pageload(url_patterns, load_handler, unload_handler) {
  341. if (!Array.isArray(url_patterns)) url_patterns = [url_patterns];
  342.  
  343. let pgld_req = {url_patterns, load_handler, unload_handler, pattern_idx:-1, next_pattern_idx:-1};
  344. pgld_req_store.push(pgld_req);
  345.  
  346. url_patterns.forEach((pattern, pattern_idx) => {
  347. let regex;
  348. if (typeof pattern === 'string') {
  349. regex = new RegExp('^'+pattern.replace(/[.+?^${}()|[\]\\]/g,'\\$&').replaceAll('*','.*')+'$');
  350. } else if (pattern instanceof RegExp) {
  351. regex = pattern;
  352. } else {
  353. return;
  354. }
  355.  
  356. let regex_str = regex.toString();
  357. let regex_entry;
  358. if (!regex_store[regex_str]) {
  359. regex_store[regex_str] = regex_entry = {regex, pgld_reqs:[]};
  360. } else {
  361. regex_entry = regex_store[regex_str];
  362. regex = regex_entry.regex;
  363. }
  364. regex_entry.pgld_reqs.push({pgld_req, pattern_idx});
  365.  
  366. // Call 'load' callback now if the current URL matches.
  367. if (pgld_req.pattern_idx !== -1) return;
  368. if (regex.test(current_page)) {
  369. pgld_req.pattern_idx = pattern_idx;
  370. try {
  371. load_handler(current_page, pattern_idx);
  372. } catch(e) {}
  373. }
  374. });
  375. }
  376.  
  377. //------------------------------
  378. // Call pageload handlers.
  379. //------------------------------
  380. function handle_pageload(event) {
  381. let last_page = current_page;
  382. if (event) {
  383. current_page = (new URL(event.detail.url)).pathname;
  384. } else {
  385. current_page = window.location.pathname;
  386. }
  387.  
  388. // Update the active status of all monitored URL patterns.
  389. Object.keys(regex_store).forEach((key, key_idx) => {
  390. let regex_entry = regex_store[key];
  391. let is_active = regex_entry.regex.test(current_page);
  392. regex_entry.pgld_reqs.forEach((regex_pgld_entry, regex_pgld_entry_idx) => {
  393. if (regex_pgld_entry.pgld_req.next_pattern_idx === -1 && is_active) {
  394. regex_pgld_entry.pgld_req.next_pattern_idx = regex_pgld_entry.pattern_idx;
  395. }
  396. });
  397. });
  398.  
  399. // Call all 'unload' handlers.
  400. pgld_req_store.forEach(pgld_req => {
  401. // If page was active, but not anymore, call the unload handler
  402. if ((pgld_req.pattern_idx !== -1) && (pgld_req.next_pattern_idx === -1)) {
  403. try {
  404. pgld_req.unload_handler(last_page, pgld_req.pattern_idx);
  405. } catch(e) {}
  406. }
  407. pgld_req.pattern_idx = pgld_req.next_pattern_idx;
  408. pgld_req.next_pattern_idx = -1;
  409. });
  410.  
  411. // Call all 'load' handlers.
  412. pgld_req_store.forEach(pgld_req => {
  413. if (pgld_req.pattern_idx !== -1) {
  414. try {
  415. pgld_req.load_handler(current_page, pgld_req.pattern_idx);
  416. } catch(e) {}
  417. }
  418. });
  419. }
  420.  
  421. let first_pageload = true;
  422. let skip_turbo_load = null;
  423. function delayed_pageload(event) {
  424. if (!event) { // If 'doc ready'
  425. if (!first_pageload) return; // Shouldn't happen, but just in case...
  426. skip_turbo_load = location.href.replace(/#.*/g,'');
  427. } else { // If 'turbo:load'
  428. if (skip_turbo_load === event.detail.url) {
  429. skip_turbo_load = null;
  430. return;
  431. }
  432. skip_turbo_load = null;
  433. }
  434. first_pageload = false;
  435. setTimeout(handle_pageload.bind(null, event), 10);
  436. }
  437.  
  438. //########################################################################
  439.  
  440. let file_cache_open_promise;
  441.  
  442. //------------------------------
  443. // Open the file_cache database (or return handle if open).
  444. //------------------------------
  445. function file_cache_open() {
  446. if (file_cache_open_promise) return file_cache_open_promise;
  447. let open_promise = promise();
  448. file_cache_open_promise = open_promise;
  449. let request;
  450. request = indexedDB.open('wkof.file_cache');
  451. request.onupgradeneeded = upgrade_db;
  452. request.onsuccess = get_dir;
  453. request.onerror = error;
  454. return open_promise;
  455.  
  456. function error() {
  457. console.log('indexedDB could not open!');
  458. wkof.file_cache.dir = {};
  459. if (ignore_missing_indexeddb) {
  460. open_promise.resolve(null);
  461. } else {
  462. open_promise.reject();
  463. }
  464. }
  465.  
  466. function upgrade_db(event){
  467. let db = event.target.result;
  468. let store = db.createObjectStore('files', {keyPath:'name'});
  469. }
  470.  
  471. function get_dir(event){
  472. let db = event.target.result;
  473. let transaction = db.transaction('files', 'readonly');
  474. let store = transaction.objectStore('files');
  475. let request = store.get('[dir]');
  476. request.onsuccess = process_dir;
  477. transaction.oncomplete = open_promise.resolve.bind(null, db);
  478. open_promise.then(setTimeout.bind(null, file_cache_cleanup, 10000));
  479. }
  480.  
  481. function process_dir(event){
  482. if (event.target.result === undefined) {
  483. wkof.file_cache.dir = {};
  484. } else {
  485. wkof.file_cache.dir = JSON.parse(event.target.result.content);
  486. }
  487. }
  488. }
  489.  
  490. //------------------------------
  491. // Lists the content of the file_cache.
  492. //------------------------------
  493. function file_cache_list() {
  494. console.log(Object.keys(wkof.file_cache.dir).sort().join('\n'));
  495. }
  496.  
  497. //------------------------------
  498. // Clear the file_cache database.
  499. //------------------------------
  500. function file_cache_clear() {
  501. return file_cache_open().then(clear);
  502.  
  503. function clear(db) {
  504. let clear_promise = promise();
  505. wkof.file_cache.dir = {};
  506. if (db === null) return clear_promise.resolve();
  507. let transaction = db.transaction('files', 'readwrite');
  508. let store = transaction.objectStore('files');
  509. store.clear();
  510. transaction.oncomplete = clear_promise.resolve;
  511. }
  512. }
  513.  
  514. //------------------------------
  515. // Delete a file from the file_cache database.
  516. //------------------------------
  517. function file_cache_delete(pattern) {
  518. return file_cache_open().then(del);
  519.  
  520. function del(db) {
  521. let del_promise = promise();
  522. if (db === null) return del_promise.resolve();
  523. let transaction = db.transaction('files', 'readwrite');
  524. let store = transaction.objectStore('files');
  525. let files = Object.keys(wkof.file_cache.dir).filter(function(file){
  526. if (pattern instanceof RegExp) {
  527. return file.match(pattern) !== null;
  528. } else {
  529. return (file === pattern);
  530. }
  531. });
  532. files.forEach(function(file){
  533. store.delete(file);
  534. delete wkof.file_cache.dir[file];
  535. });
  536. file_cache_dir_save();
  537. transaction.oncomplete = del_promise.resolve.bind(null, files);
  538. return del_promise;
  539. }
  540. }
  541.  
  542. //------------------------------
  543. // Force immediate save of file_cache directory.
  544. //------------------------------
  545. function file_cache_flush() {
  546. file_cache_dir_save(true /* immediately */);
  547. }
  548.  
  549. //------------------------------
  550. // Load a file from the file_cache database.
  551. //------------------------------
  552. function file_cache_load(name) {
  553. let load_promise = promise();
  554. return file_cache_open().then(load);
  555.  
  556. function load(db) {
  557. if (wkof.file_cache.dir[name] === undefined) {
  558. load_promise.reject(name);
  559. return load_promise;
  560. }
  561. let transaction = db.transaction('files', 'readonly');
  562. let store = transaction.objectStore('files');
  563. let request = store.get(name);
  564. wkof.file_cache.dir[name].last_loaded = new Date().toISOString();
  565. file_cache_dir_save();
  566. request.onsuccess = finish;
  567. request.onerror = error;
  568. return load_promise;
  569.  
  570. function finish(event){
  571. if (event.target.result === undefined || event.target.result === null) {
  572. load_promise.reject(name);
  573. } else {
  574. load_promise.resolve(event.target.result.content);
  575. }
  576. }
  577.  
  578. function error(event){
  579. load_promise.reject(name);
  580. }
  581. }
  582. }
  583.  
  584. //------------------------------
  585. // Save a file into the file_cache database.
  586. //------------------------------
  587. function file_cache_save(name, content, extra_attribs) {
  588. return file_cache_open().then(save);
  589.  
  590. function save(db) {
  591. let save_promise = promise();
  592. if (db === null) return save_promise.resolve(name);
  593. let transaction = db.transaction('files', 'readwrite');
  594. let store = transaction.objectStore('files');
  595. store.put({name:name,content:content});
  596. let now = new Date().toISOString();
  597. wkof.file_cache.dir[name] = Object.assign({added:now, last_loaded:now}, extra_attribs);
  598. file_cache_dir_save(true /* immediately */);
  599. transaction.oncomplete = save_promise.resolve.bind(null, name);
  600. }
  601. }
  602.  
  603. //------------------------------
  604. // Save a the file_cache directory contents.
  605. //------------------------------
  606. let fc_sync_timer;
  607. function file_cache_dir_save(immediately) {
  608. if (fc_sync_timer !== undefined) clearTimeout(fc_sync_timer);
  609. let delay = (immediately ? 0 : 2000);
  610. fc_sync_timer = setTimeout(save, delay);
  611.  
  612. function save(){
  613. file_cache_open().then(save2);
  614. }
  615.  
  616. function save2(db){
  617. fc_sync_timer = undefined;
  618. let transaction = db.transaction('files', 'readwrite');
  619. let store = transaction.objectStore('files');
  620. store.put({name:'[dir]',content:JSON.stringify(wkof.file_cache.dir)});
  621. }
  622. }
  623.  
  624. //------------------------------
  625. // Remove files that haven't been accessed in a while.
  626. //------------------------------
  627. function file_cache_cleanup() {
  628. let threshold = new Date() - 14*86400000; // 14 days
  629. let old_files = [];
  630. for (var fname in wkof.file_cache.dir) {
  631. if (fname.match(/^wkof\.settings\./)) continue; // Don't flush settings files.
  632. let fdate = new Date(wkof.file_cache.dir[fname].last_loaded);
  633. if (fdate < threshold) old_files.push(fname);
  634. }
  635. if (old_files.length === 0) return;
  636. console.log('Cleaning out '+old_files.length+' old file(s) from "wkof.file_cache":');
  637. for (let fnum in old_files) {
  638. console.log(' '+(Number(fnum)+1)+': '+old_files[fnum]);
  639. wkof.file_cache.delete(old_files[fnum]);
  640. }
  641. }
  642.  
  643. //------------------------------
  644. // Process no-cache requests.
  645. //------------------------------
  646. function file_nocache(list) {
  647. if (list === undefined) {
  648. list = split_list(localStorage.getItem('wkof.include.nocache') || '');
  649. list = list.concat(split_list(localStorage.getItem('wkof.load_file.nocache') || ''));
  650. console.log(list.join(','));
  651. } else if (typeof list === 'string') {
  652. let no_cache = split_list(list);
  653. let idx, modules = [], urls = [];
  654. for (idx = 0; idx < no_cache.length; idx++) {
  655. let item = no_cache[idx];
  656. if (supported_modules[item] !== undefined) {
  657. modules.push(item);
  658. } else {
  659. urls.push(item);
  660. }
  661. }
  662. console.log('Modules: '+modules.join(','));
  663. console.log(' URLs: '+urls.join(','));
  664. localStorage.setItem('wkof.include.nocache', modules.join(','));
  665. localStorage.setItem('wkof.load_file.nocache', urls.join(','));
  666. }
  667. }
  668.  
  669. function doc_ready() {
  670. wkof.set_state('wkof.document', 'ready');
  671. }
  672.  
  673. //########################################################################
  674. // Bootloader Startup
  675. //------------------------------
  676. function startup() {
  677. global.wkof = published_interface;
  678.  
  679. // Handle page-loading/unloading events.
  680. function install_load_listener() {
  681. if (!document.documentElement) {
  682. setTimeout(install_load_listener, 10);
  683. return;
  684. }
  685. document.documentElement.addEventListener('turbo:load', delayed_pageload);
  686. }
  687. install_load_listener();
  688.  
  689. ready('document').then((e) => {
  690. if (first_pageload) delayed_pageload();
  691. });
  692.  
  693. // Mark document state as 'ready'.
  694. if (document.readyState === 'complete') {
  695. doc_ready();
  696. } else {
  697. window.addEventListener("load", doc_ready, false); // Notify listeners that we are ready.
  698. }
  699.  
  700. // Open cache, so wkof.file_cache.dir is available to console immediately.
  701. file_cache_open();
  702. wkof.set_state('wkof.wkof', 'ready');
  703. }
  704. startup();
  705.  
  706. })(window);
  707.