Wanikani Open Framework - Settings module

Settings module for Wanikani Open Framework

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/38576/1091793/Wanikani%20Open%20Framework%20-%20Settings%20module.js

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