KameSame Open Framework - Settings module

Settings module for KameSame Open Framework

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

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

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