KameSame Open Framework - Settings module

Settings module for KameSame Open Framework

当前为 2022-10-06 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/451521/1101524/KameSame%20Open%20Framework%20-%20Settings%20module.js

  1. // ==UserScript==
  2. // @name KameSame Open Framework - Settings module
  3. // @namespace timberpile
  4. // @description Settings module for KameSame Open Framework
  5. // @version 0.2
  6. // @copyright 2022+, Robin Findley, Timberpile
  7. // @license MIT; http://opensource.org/licenses/MIT
  8. // ==/UserScript==
  9. (function (global) {
  10. const publish_context = false; // Set to 'true' to make context public.
  11. const ksof = global.ksof;
  12. //########################################################################
  13. //------------------------------
  14. // Constructor
  15. //------------------------------
  16. class Settings {
  17. constructor(config) {
  18. const context = {
  19. self: this,
  20. cfg: config,
  21. };
  22. if (!config.content)
  23. config.content = config.settings;
  24. if (publish_context)
  25. this.context = context;
  26. // Create public methods bound to context.
  27. this.cancel = cancel_btn.bind(context, context);
  28. this.open = open.bind(context, context);
  29. this.close = close.bind(context, context);
  30. this.load = load_settings.bind(context, context);
  31. this.save = save_settings.bind(context, context);
  32. this.refresh = refresh.bind(context, context);
  33. this.background = Settings.background;
  34. }
  35. //------------------------------
  36. // Open the settings dialog.
  37. //------------------------------
  38. save_settings(context) {
  39. const script_id = (typeof context === 'string' ? context : context.cfg.script_id);
  40. const settings = ksof.settings[script_id];
  41. if (!settings)
  42. return Promise.resolve();
  43. return ksof.file_cache.save('ksof.settings.' + script_id, settings);
  44. }
  45. }
  46. global.ksof.Settings = Settings;
  47. global.ksof.settings = {};
  48. // Settings.save = save_settings;
  49. Settings.load = load_settings;
  50. Settings.background = {
  51. open: open_background,
  52. close: close_background,
  53. };
  54. //########################################################################
  55. let ready = false;
  56. //========================================================================
  57. function deep_merge(...objects) {
  58. const merged = {};
  59. function recursive_merge(dest, src) {
  60. for (const prop in src) {
  61. if (typeof src[prop] === 'object' && src[prop] !== null) {
  62. if (Array.isArray(src[prop])) {
  63. dest[prop] = src[prop].slice();
  64. }
  65. else {
  66. dest[prop] = dest[prop] || {};
  67. recursive_merge(dest[prop], src[prop]);
  68. }
  69. }
  70. else {
  71. dest[prop] = src[prop];
  72. }
  73. }
  74. return dest;
  75. }
  76. for (const obj in objects) {
  77. recursive_merge(merged, objects[obj]);
  78. }
  79. return merged;
  80. }
  81. //------------------------------
  82. // Convert a config object to html dialog.
  83. //------------------------------
  84. /* eslint-disable no-case-declarations */
  85. function config_to_html(context) {
  86. context.config_list = {};
  87. let base = ksof.settings[context.cfg.script_id];
  88. if (base === undefined)
  89. ksof.settings[context.cfg.script_id] = base = {};
  90. let html = '';
  91. const child_passback = {};
  92. const id = context.cfg.script_id + '_dialog';
  93. for (const name in context.cfg.content) {
  94. html += parse_item(name, context.cfg.content[name], child_passback);
  95. }
  96. if (child_passback.tabs)
  97. html = assemble_pages(id, child_passback.tabs, child_passback.pages) + html;
  98. return '<form>' + html + '</form>';
  99. //============
  100. function parse_item(name, item, passback) {
  101. if (typeof item.type !== 'string')
  102. return '';
  103. const id = context.cfg.script_id + '_' + name;
  104. let cname, html = '', value, child_passback, non_page = '';
  105. switch (item.type) {
  106. case 'tabset':
  107. child_passback = {};
  108. for (cname in item.content)
  109. non_page += parse_item(cname, item.content[cname], child_passback);
  110. if (child_passback.tabs)
  111. html = assemble_pages(id, child_passback.tabs, child_passback.pages);
  112. break;
  113. case 'page':
  114. if (typeof item.content !== 'object')
  115. item.content = {};
  116. if (!passback.tabs) {
  117. passback.tabs = [];
  118. passback.pages = [];
  119. }
  120. passback.tabs.push('<li id="' + id + '_tab"' + to_title(item.hover_tip) + '><a href="#' + id + '">' + item.label + '</a></li>');
  121. child_passback = {};
  122. for (cname in item.content)
  123. non_page += parse_item(cname, item.content[cname], child_passback);
  124. if (child_passback.tabs)
  125. html = assemble_pages(id, child_passback.tabs, child_passback.pages);
  126. passback.pages.push('<div id="' + id + '">' + html + non_page + '</div>');
  127. passback.is_page = true;
  128. html = '';
  129. break;
  130. case 'group':
  131. if (typeof item.content !== 'object')
  132. item.content = {};
  133. child_passback = {};
  134. for (cname in item.content)
  135. non_page += parse_item(cname, item.content[cname], child_passback);
  136. if (child_passback.tabs)
  137. html = assemble_pages(id, child_passback.tabs, child_passback.pages);
  138. html = '<fieldset id="' + id + '" class="ksof_group"><legend>' + item.label + '</legend>' + html + non_page + '</fieldset>';
  139. break;
  140. case 'dropdown':
  141. case 'list':
  142. let classes = 'setting';
  143. let attribs = '';
  144. context.config_list[name] = item;
  145. value = get_value(context, base, name);
  146. if (value === undefined) {
  147. if (item.default !== undefined) {
  148. value = item.default;
  149. }
  150. else {
  151. if (item.multi === true) {
  152. value = {};
  153. Object.keys(item.content).forEach(function (key) {
  154. value[key] = false;
  155. });
  156. }
  157. else {
  158. value = Object.keys(item.content)[0];
  159. }
  160. }
  161. set_value(context, base, name, value);
  162. }
  163. if (item.type === 'list') {
  164. classes += ' list';
  165. attribs += ' size="' + (item.size || Object.keys(item.content).length || 4) + '"';
  166. if (item.multi === true)
  167. attribs += ' multiple';
  168. }
  169. html = '<select id="' + id + '" name="' + name + '" class="' + classes + '"' + attribs + to_title(item.hover_tip) + '>';
  170. for (cname in item.content)
  171. html += '<option name="' + cname + '">' + escape_text(item.content[cname]) + '</option>';
  172. html += '</select>';
  173. html = make_label(item) + wrap_right(html);
  174. html = wrap_row(html, item.full_width, item.hover_tip);
  175. break;
  176. case 'checkbox':
  177. context.config_list[name] = item;
  178. html = make_label(item);
  179. value = get_value(context, base, name);
  180. if (value === undefined) {
  181. value = (item.default || false);
  182. set_value(context, base, name, value);
  183. }
  184. html += wrap_right('<input id="' + id + '" class="setting" type="checkbox" name="' + name + '">');
  185. html = wrap_row(html, item.full_width, item.hover_tip);
  186. break;
  187. case 'input':
  188. case 'number':
  189. case 'text':
  190. const itype = item.type;
  191. if (itype === 'input')
  192. itype = item.subtype || 'text';
  193. context.config_list[name] = item;
  194. html += make_label(item);
  195. value = get_value(context, base, name);
  196. if (value === undefined) {
  197. const is_number = (item.type === 'number' || item.subtype === 'number');
  198. value = (item.default || (is_number === 'number' ? 0 : ''));
  199. set_value(context, base, name, value);
  200. }
  201. html += wrap_right('<input id="' + id + '" class="setting" type="' + itype + '" name="' + name + '"' + (item.placeholder ? ' placeholder="' + escape_attr(item.placeholder) + '"' : '') + '>');
  202. html = wrap_row(html, item.full_width, item.hover_tip);
  203. break;
  204. case 'color':
  205. context.config_list[name] = item;
  206. html += make_label(item);
  207. value = get_value(context, base, name);
  208. if (value === undefined) {
  209. value = (item.default || '#000000');
  210. set_value(context, base, name, value);
  211. }
  212. html += wrap_right('<input id="' + id + '" class="setting" type="color" name="' + name + '">');
  213. html = wrap_row(html, item.full_width, item.hover_tip);
  214. break;
  215. case 'button':
  216. context.config_list[name] = item;
  217. html += make_label(item);
  218. const text = escape_text(item.text || 'Click');
  219. html += wrap_right('<button type="button" class="setting" name="' + name + '">' + text + '</button>');
  220. html = wrap_row(html, item.full_width, item.hover_tip);
  221. break;
  222. case 'divider':
  223. html += '<hr>';
  224. break;
  225. case 'section':
  226. html += '<section>' + (item.label || '') + '</section>';
  227. break;
  228. case 'html':
  229. html += make_label(item);
  230. html += item.html;
  231. switch (item.wrapper) {
  232. case 'row':
  233. html = wrap_row(html, null, item.hover_tip);
  234. break;
  235. case 'left':
  236. html = wrap_left(html);
  237. break;
  238. case 'right':
  239. html = wrap_right(html);
  240. break;
  241. }
  242. break;
  243. }
  244. return html;
  245. function make_label(item) {
  246. if (typeof item.label !== 'string')
  247. return '';
  248. return wrap_left('<label for="' + id + '">' + item.label + '</label>');
  249. }
  250. }
  251. /* eslint-enable no-case-declarations */
  252. //============
  253. function assemble_pages(id, tabs, pages) { return '<div id="' + id + '" class="ksof_stabs"><ul>' + tabs.join('') + '</ul>' + pages.join('') + '</div>'; }
  254. function wrap_row(html, full, hover_tip) { return '<div class="row' + (full ? ' full' : '') + '"' + to_title(hover_tip) + '>' + html + '</div>'; }
  255. function wrap_left(html) { return '<div class="left">' + html + '</div>'; }
  256. function wrap_right(html) { return '<div class="right">' + html + '</div>'; }
  257. function escape_text(text) { return text.replace(/[<>]/g, function (ch) { const map = { '<': '&lt', '>': '&gt;' }; return map[ch]; }); }
  258. function escape_attr(text) { return text.replace(/"/g, '&quot;'); }
  259. function to_title(tip) { if (!tip)
  260. return ''; return ' title="' + tip.replace(/"/g, '&quot;') + '"'; }
  261. }
  262. //------------------------------
  263. // Open the settings dialog.
  264. //------------------------------
  265. function open(context) {
  266. // todo investigate why the dialog doesn't render properly
  267. if (!ready)
  268. return;
  269. if ($('#ksofs_' + context.cfg.script_id).length > 0)
  270. return;
  271. install_anchor();
  272. if (context.cfg.background !== false)
  273. open_background();
  274. const dialog = $('<div id="ksofs_' + context.cfg.script_id + '" class="ksof_settings" style="display:none;"></div>');
  275. dialog.html(config_to_html(context));
  276. let width = 500;
  277. if (window.innerWidth < 510) {
  278. width = 280;
  279. dialog.addClass('narrow');
  280. }
  281. dialog.dialog({
  282. title: context.cfg.title,
  283. buttons: [
  284. { text: 'Save', click: save_btn.bind(context, context) },
  285. { text: 'Cancel', click: cancel_btn.bind(context, context) }
  286. ],
  287. width: width,
  288. maxHeight: document.body.clientHeight,
  289. modal: false,
  290. autoOpen: false,
  291. appendTo: '#ksof_ds',
  292. resize: resize.bind(context, context),
  293. close: close.bind(context, context)
  294. });
  295. $(dialog.dialog('widget')).css('position', 'fixed');
  296. dialog.parent().addClass('ksof_settings_dialog');
  297. $('.ksof_stabs').tabs({ activate: tab_activated.bind(null, context) });
  298. const settings = ksof.settings[context.cfg.script_id];
  299. if (settings && settings.ksofs_active_tabs instanceof Array) {
  300. const active_tabs = settings.ksofs_active_tabs;
  301. for (let tab_idx = 0; tab_idx < active_tabs.length; tab_idx++) {
  302. const tab = $(active_tabs[tab_idx]);
  303. tab.closest('.ui-tabs').tabs({ active: tab.index() });
  304. }
  305. }
  306. dialog.dialog('open');
  307. const dialog_elem = $('#ksofs_' + context.cfg.script_id);
  308. dialog_elem.find('.setting[multiple]').on('mousedown', toggle_multi.bind(null, context));
  309. dialog_elem.find('.setting').on('change', setting_changed.bind(null, context));
  310. dialog_elem.find('form').on('submit', function () { return false; });
  311. dialog_elem.find('button.setting').on('click', setting_button_clicked.bind(null, context));
  312. if (typeof context.cfg.pre_open === 'function')
  313. context.cfg.pre_open(dialog);
  314. context.reversions = deep_merge({}, ksof.settings[context.cfg.script_id]);
  315. refresh(context);
  316. //============
  317. function tab_activated(context, event, ui) {
  318. const dialog = $('#ksofs_' + context.cfg.script_id);
  319. const wrapper = $(dialog.dialog('widget'));
  320. if (wrapper.outerHeight() + wrapper.position().top > document.body.clientHeight) {
  321. dialog.dialog('option', 'maxHeight', document.body.clientHeight);
  322. }
  323. }
  324. function resize(context, event, ui) {
  325. const dialog = $('#ksofs_' + context.cfg.script_id);
  326. const is_narrow = dialog.hasClass('narrow');
  327. if (is_narrow && ui.size.width >= 510)
  328. dialog.removeClass('narrow');
  329. else if (!is_narrow && ui.size.width < 490)
  330. dialog.addClass('narrow');
  331. }
  332. function toggle_multi(context, e) {
  333. if (e.button != 0)
  334. return true;
  335. const multi = $(e.currentTarget);
  336. const scroll = e.currentTarget.scrollTop;
  337. e.target.selected = !e.target.selected;
  338. setTimeout(function () {
  339. e.currentTarget.scrollTop = scroll;
  340. multi.focus();
  341. }, 0);
  342. return setting_changed(context, e);
  343. }
  344. function setting_button_clicked(context, e) {
  345. const name = e.target.attributes.name.value;
  346. const item = context.config_list[name];
  347. if (typeof item.on_click === 'function')
  348. item.on_click.call(e, name, item, setting_changed.bind(context, context, e));
  349. }
  350. }
  351. //------------------------------
  352. // Open the settings dialog.
  353. //------------------------------
  354. function load_settings(context, defaults) {
  355. const script_id = (typeof context === 'string' ? context : context.cfg.script_id);
  356. return ksof.file_cache.load('ksof.settings.' + script_id)
  357. .then(finish, finish.bind(null, {}));
  358. function finish(settings) {
  359. if (defaults)
  360. ksof.settings[script_id] = deep_merge(defaults, settings);
  361. else
  362. ksof.settings[script_id] = settings;
  363. return ksof.settings[script_id];
  364. }
  365. }
  366. //------------------------------
  367. // Save button handler.
  368. //------------------------------
  369. function save_btn(context, e) {
  370. const script_id = context.cfg.script_id;
  371. const dialog = $('#ksofs_' + script_id);
  372. const settings = ksof.settings[script_id];
  373. if (settings) {
  374. const active_tabs = dialog.find('.ui-tabs-active').toArray().map(function (tab) { return '#' + tab.attributes.id.value; });
  375. if (active_tabs.length > 0)
  376. settings.ksofs_active_tabs = active_tabs;
  377. }
  378. if (context.cfg.autosave === undefined || context.cfg.autosave === true)
  379. save_settings(context);
  380. if (typeof context.cfg.on_save === 'function')
  381. context.cfg.on_save(ksof.settings[context.cfg.script_id]);
  382. ksof.trigger('ksof.settings.save');
  383. context.keep_settings = true;
  384. dialog.dialog('close');
  385. }
  386. //------------------------------
  387. // Cancel button handler.
  388. //------------------------------
  389. function cancel_btn(context) {
  390. const dialog = $('#ksofs_' + context.cfg.script_id);
  391. dialog.dialog('close');
  392. if (typeof context.cfg.on_cancel === 'function')
  393. context.cfg.on_cancel(ksof.settings[context.cfg.script_id]);
  394. }
  395. //------------------------------
  396. // Close and destroy the dialog.
  397. //------------------------------
  398. function close(context, keep_settings) {
  399. const dialog = $('#ksofs_' + context.cfg.script_id);
  400. if (!context.keep_settings && keep_settings !== true) {
  401. // Revert settings
  402. ksof.settings[context.cfg.script_id] = deep_merge({}, context.reversions);
  403. delete context.reversions;
  404. }
  405. delete context.keep_settings;
  406. dialog.dialog('destroy');
  407. if (context.cfg.background !== false)
  408. close_background();
  409. if (typeof context.cfg.on_close === 'function')
  410. context.cfg.on_close(ksof.settings[context.cfg.script_id]);
  411. }
  412. //------------------------------
  413. // Update the dialog to reflect changed settings.
  414. //------------------------------
  415. function refresh(context) {
  416. const script_id = context.cfg.script_id;
  417. const settings = ksof.settings[script_id];
  418. const dialog = $('#ksofs_' + script_id);
  419. for (const name in context.config_list) {
  420. const elem = dialog.find('#' + script_id + '_' + name);
  421. const config = context.config_list[name];
  422. const value = get_value(context, settings, name);
  423. switch (config.type) {
  424. case 'dropdown':
  425. case 'list':
  426. if (config.multi === true) {
  427. elem.find('option').each(function (i, e) {
  428. const opt_name = e.getAttribute('name') || '#' + e.index;
  429. e.selected = value[opt_name];
  430. });
  431. }
  432. else {
  433. elem.find('option[name="' + value + '"]').prop('selected', true);
  434. }
  435. break;
  436. case 'checkbox':
  437. elem.prop('checked', value);
  438. break;
  439. default:
  440. elem.val(value);
  441. break;
  442. }
  443. }
  444. if (typeof context.cfg.on_refresh === 'function')
  445. context.cfg.on_refresh(ksof.settings[context.cfg.script_id]);
  446. }
  447. //------------------------------
  448. // Handler for live settings changes. Handles built-in validation and user callbacks.
  449. //------------------------------
  450. function setting_changed(context, event) {
  451. const elem = $(event.currentTarget);
  452. const name = elem.attr('name');
  453. const item = context.config_list[name];
  454. // Extract the value
  455. let value;
  456. const itype = ((item.type === 'input' && item.subtype === 'number') ? 'number' : item.type);
  457. switch (itype) {
  458. case 'dropdown':
  459. case 'list':
  460. if (item.multi === true) {
  461. value = {};
  462. elem.find('option').each(function (i, e) {
  463. const opt_name = e.getAttribute('name') || '#' + e.index;
  464. value[opt_name] = e.selected;
  465. });
  466. }
  467. else {
  468. value = elem.find(':checked').attr('name');
  469. }
  470. break;
  471. case 'checkbox':
  472. value = elem.is(':checked');
  473. break;
  474. case 'number':
  475. value = Number(elem.val());
  476. break;
  477. default:
  478. value = elem.val();
  479. break;
  480. }
  481. // Validation
  482. let valid = { valid: true, msg: '' };
  483. if (typeof item.validate === 'function')
  484. valid = item.validate.call(event.target, value, item);
  485. if (typeof valid === 'boolean')
  486. valid = { valid: valid, msg: '' };
  487. else if (typeof valid === 'string')
  488. valid = { valid: false, msg: valid };
  489. else if (valid === undefined)
  490. valid = { valid: true, msg: '' };
  491. switch (itype) {
  492. case 'number':
  493. if (typeof item.min === 'number' && Number(value) < item.min) {
  494. valid.valid = false;
  495. if (valid.msg.length === 0) {
  496. if (typeof item.max === 'number')
  497. valid.msg = 'Must be between ' + item.min + ' and ' + item.max;
  498. else
  499. valid.msg = 'Must be ' + item.min + ' or higher';
  500. }
  501. }
  502. else if (typeof item.max === 'number' && Number(value) > item.max) {
  503. valid.valid = false;
  504. if (valid.msg.length === 0) {
  505. if (typeof item.min === 'number')
  506. valid.msg = 'Must be between ' + item.min + ' and ' + item.max;
  507. else
  508. valid.msg = 'Must be ' + item.max + ' or lower';
  509. }
  510. }
  511. if (!valid) // TODO intended?
  512. break;
  513. // eslint-disable-next-line no-fallthrough
  514. case 'text':
  515. if (item.match !== undefined && value.match(item.match) === null) {
  516. valid.valid = false;
  517. if (valid.msg.length === 0)
  518. valid.msg = item.error_msg || 'Invalid value';
  519. }
  520. break;
  521. }
  522. // Style for valid/invalid
  523. const parent = elem.closest('.right');
  524. parent.find('.note').remove();
  525. if (typeof valid.msg === 'string' && valid.msg.length > 0)
  526. parent.append('<div class="note' + (valid.valid ? '' : ' error') + '">' + valid.msg + '</div>');
  527. if (!valid.valid) {
  528. elem.addClass('invalid');
  529. }
  530. else {
  531. elem.removeClass('invalid');
  532. }
  533. const script_id = context.cfg.script_id;
  534. const settings = ksof.settings[script_id];
  535. if (valid.valid) {
  536. if (item.no_save !== true)
  537. set_value(context, settings, name, value);
  538. if (typeof item.on_change === 'function')
  539. item.on_change.call(event.target, name, value, item);
  540. if (typeof context.cfg.on_change === 'function')
  541. context.cfg.on_change.call(event.target, name, value, item);
  542. if (item.refresh_on_change === true)
  543. refresh(context);
  544. }
  545. return false;
  546. }
  547. function get_value(context, base, name) {
  548. const item = context.config_list[name];
  549. const evaluate = (item.path !== undefined);
  550. const path = (item.path || name);
  551. try {
  552. if (!evaluate)
  553. return base[path];
  554. return eval(path.replace(/@/g, 'base.'));
  555. }
  556. catch (e) {
  557. return;
  558. }
  559. }
  560. function set_value(context, base, name, value) {
  561. const item = context.config_list[name];
  562. const evaluate = (item.path !== undefined);
  563. const path = (item.path || name);
  564. try {
  565. if (!evaluate)
  566. return base[path] = value;
  567. var depth = 0, new_path = '', param, c;
  568. for (var idx = 0; idx < path.length; idx++) {
  569. c = path[idx];
  570. if (c === '[') {
  571. if (depth++ === 0) {
  572. new_path += '[';
  573. param = '';
  574. }
  575. else {
  576. param += '[';
  577. }
  578. }
  579. else if (c === ']') {
  580. if (--depth === 0) {
  581. new_path += JSON.stringify(eval(param)) + ']';
  582. }
  583. else {
  584. param += ']';
  585. }
  586. }
  587. else {
  588. if (c === '@')
  589. c = 'base.';
  590. if (depth === 0)
  591. new_path += c;
  592. else
  593. param += c;
  594. }
  595. }
  596. eval(new_path + '=value');
  597. }
  598. catch (e) {
  599. return;
  600. }
  601. }
  602. function install_anchor() {
  603. const anchor = $('#ksof_ds');
  604. if (anchor.length === 0) {
  605. anchor = $('<div id="ksof_ds"></div></div>');
  606. $('body').prepend(anchor);
  607. $('#ksof_ds').on('keydown keyup keypress', '.ksof_settings_dialog', function (e) {
  608. // Stop keys from bubbling beyond the background overlay.
  609. e.stopPropagation();
  610. });
  611. }
  612. return anchor;
  613. }
  614. function open_background() {
  615. const anchor = install_anchor();
  616. let bkgd = anchor.find('> #ksofs_bkgd');
  617. if (bkgd.length === 0) {
  618. bkgd = $('<div id="ksofs_bkgd" refcnt="0"></div>');
  619. anchor.prepend(bkgd);
  620. }
  621. const refcnt = Number(bkgd.attr('refcnt'));
  622. bkgd.attr('refcnt', refcnt + 1);
  623. }
  624. function close_background() {
  625. const bkgd = $('#ksof_ds > #ksofs_bkgd');
  626. if (bkgd.length === 0)
  627. return;
  628. const refcnt = Number(bkgd.attr('refcnt'));
  629. if (refcnt <= 0)
  630. return;
  631. bkgd.attr('refcnt', refcnt - 1);
  632. }
  633. //------------------------------
  634. // Load jquery UI and the appropriate CSS based on location.
  635. //------------------------------
  636. const css_url = ksof.support_files['jqui_ksmain.css'];
  637. ksof.include('Jquery');
  638. ksof.ready('document, Jquery')
  639. .then(function () {
  640. return Promise.all([
  641. ksof.load_script(ksof.support_files['jquery_ui.js'], true /* cache */),
  642. ksof.load_css(css_url, true /* cache */)
  643. ]);
  644. })
  645. .then(function (data) {
  646. ready = true;
  647. // Workaround... https://community.wanikani.com/t/19984/55
  648. try {
  649. delete $.fn.autocomplete;
  650. }
  651. catch (e) {
  652. // do nothing
  653. }
  654. // Notify listeners that we are ready.
  655. // Delay guarantees include() callbacks are called before ready() callbacks.
  656. setTimeout(function () { ksof.set_state('ksof.Settings', 'ready'); }, 0);
  657. });
  658. })(this);