GM_config_cnjames

A lightweight, reusable, cross-browser graphical settings framework for inclusion in user scripts.

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/483173/1301961/GM_config_cnjames.js

  1. /*
  2. Copyright 2009+, GM_config Contributors (https://github.com/sizzlemctwizzle/GM_config)
  3.  
  4. GM_config Collaborators/Contributors:
  5. Mike Medley <medleymind@gmail.com>
  6. Joe Simmons
  7. Izzy Soft
  8. Marti Martz
  9. Adam Thompson-Sharpe
  10.  
  11. GM_config is distributed under the terms of the GNU Lesser General Public License.
  12.  
  13. GM_config is free software: you can redistribute it and/or modify
  14. it under the terms of the GNU Lesser General Public License as published by
  15. the Free Software Foundation, either version 3 of the License, or
  16. (at your option) any later version.
  17.  
  18. This program is distributed in the hope that it will be useful,
  19. but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. GNU Lesser General Public License for more details.
  22.  
  23. You should have received a copy of the GNU Lesser General Public License
  24. along with this program. If not, see <https://www.gnu.org/licenses/>.
  25. */
  26.  
  27. // ==UserScript==
  28. // @exclude *
  29. // @author Mike Medley <medleymind@gmail.com> (https://github.com/sizzlemctwizzle/GM_config)
  30. // @icon https://raw.githubusercontent.com/sizzlemctwizzle/GM_config/master/gm_config_icon_large.png
  31.  
  32. // ==UserLibrary==
  33. // @name GM_config_cnjames
  34. // @description A lightweight, reusable, cross-browser graphical settings framework for inclusion in user scripts.
  35. // @copyright 2009+, Mike Medley (https://github.com/sizzlemctwizzle)
  36. // @license LGPL-3.0-or-later; https://raw.githubusercontent.com/sizzlemctwizzle/GM_config/master/LICENSE
  37.  
  38. // @homepageURL https://openuserjs.org/libs/sizzle/GM_config
  39. // @homepageURL https://github.com/sizzlemctwizzle/GM_config
  40. // @supportURL https://github.com/sizzlemctwizzle/GM_config/issues
  41.  
  42. // ==/UserScript==
  43.  
  44. // ==/UserLibrary==
  45.  
  46. /* jshint esversion: 8 */
  47.  
  48. let GM_config = (function (GM) {
  49. // This is the initializer function
  50. function GM_configInit(config, args) {
  51. // Initialize instance variables
  52. if (typeof config.fields == "undefined") {
  53. config.fields = {};
  54. config.onInit = config.onInit || function() {};
  55. config.onOpen = config.onOpen || function() {};
  56. config.onSave = config.onSave || function() {};
  57. config.onClose = config.onClose || function() {};
  58. config.onReset = config.onReset || function() {};
  59. config.isOpen = false;
  60. config.title = 'User Script Settings';
  61. config.css = {
  62. basic: [
  63. "#GM_config * { font-family: arial,tahoma,myriad pro,sans-serif; }",
  64. "#GM_config { background: #FFF; }",
  65. "#GM_config input[type='radio'] { margin-right: 8px; }",
  66. "#GM_config .indent40 { margin-left: 40%; }",
  67. "#GM_config .field_label { font-size: 12px; font-weight: bold; margin-right: 6px; }",
  68. "#GM_config .radio_label { font-size: 12px; }",
  69. "#GM_config .block { display: block; }",
  70. "#GM_config .saveclose_buttons { margin: 16px 10px 10px; padding: 2px 12px; }",
  71. "#GM_config .reset, #GM_config .reset a," +
  72. " #GM_config_buttons_holder { color: #000; text-align: right; }",
  73. "#GM_config .config_header { font-size: 20pt; margin: 0; }",
  74. "#GM_config .config_desc, #GM_config .section_desc, #GM_config .reset { font-size: 9pt; }",
  75. "#GM_config .center { text-align: center; }",
  76. "#GM_config .section_header_holder { margin-top: 8px; }",
  77. "#GM_config .config_var { margin: 0 0 4px; }",
  78. "#GM_config .section_header { background: #414141; border: 1px solid #000; color: #FFF;",
  79. " font-size: 13pt; margin: 0; }",
  80. "#GM_config .section_desc { background: #EFEFEF; border: 1px solid #CCC; color: #575757;" +
  81. " font-size: 9pt; margin: 0 0 6px; }"
  82. ].join('\n') + '\n',
  83. basicPrefix: "GM_config",
  84. stylish: ""
  85. };
  86. }
  87. config.frameStyle = [
  88. 'bottom: auto; border: 1px solid #000; display: none; height: 75%;',
  89. 'left: 0; margin: 0; max-height: 95%; max-width: 95%; opacity: 0;',
  90. 'overflow: auto; padding: 0; position: fixed; right: auto; top: 0;',
  91. 'width: 75%; z-index: 9999;'
  92. ].join(' ');
  93.  
  94. var settings = null;
  95. if (args.length == 1 &&
  96. typeof args[0].id == "string" &&
  97. typeof args[0].appendChild != "function") settings = args[0];
  98. else {
  99. // Provide backwards-compatibility with argument style intialization
  100. settings = {};
  101.  
  102. // loop through GM_config.init() arguments
  103. for (let i = 0, l = args.length, arg; i < l; ++i) {
  104. arg = args[i];
  105.  
  106. // An element to use as the config window
  107. if (typeof arg.appendChild == "function") {
  108. settings.frame = arg;
  109. continue;
  110. }
  111.  
  112. switch (typeof arg) {
  113. case 'object':
  114. for (let j in arg) { // could be a callback functions or settings object
  115. if (typeof arg[j] != "function") { // we are in the settings object
  116. settings.fields = arg; // store settings object
  117. break; // leave the loop
  118. } // otherwise it must be a callback function
  119. if (!settings.events) settings.events = {};
  120. settings.events[j] = arg[j];
  121. }
  122. break;
  123. case 'function': // passing a bare function is set to open callback
  124. settings.events = {onOpen: arg};
  125. break;
  126. case 'string': // could be custom CSS or the title string
  127. if (/\w+\s*\{\s*\w+\s*:\s*\w+[\s|\S]*\}/.test(arg))
  128. settings.css = arg;
  129. else
  130. settings.title = arg;
  131. break;
  132. }
  133. }
  134. }
  135.  
  136. /* Initialize everything using the new settings object */
  137. // Set the id
  138. if (settings.id) config.id = settings.id;
  139. else if (typeof config.id == "undefined") config.id = 'GM_config';
  140.  
  141. // Set the title
  142. if (settings.title) config.title = settings.title;
  143.  
  144. // Set the custom css
  145. if (settings.css) config.css.stylish = settings.css;
  146.  
  147. // Set the frame
  148. if (settings.frame) config.frame = settings.frame;
  149. // Set the style attribute of the frame
  150. if (typeof settings.frameStyle === 'string') config.frameStyle = settings.frameStyle;
  151.  
  152. // Set the event callbacks
  153. if (settings.events) {
  154. let events = settings.events;
  155. for (let e in events) {
  156. config["on" + e.charAt(0).toUpperCase() + e.slice(1)] = events[e];
  157. }
  158. }
  159.  
  160. // If the id has changed we must modify the default style
  161. if (config.id != config.css.basicPrefix) {
  162. config.css.basic = config.css.basic.replace(
  163. new RegExp('#' + config.css.basicPrefix, 'gm'), '#' + config.id);
  164. config.css.basicPrefix = config.id;
  165. }
  166.  
  167. // Create the fields
  168. config.isInit = false;
  169. if (settings.fields) {
  170. config.read(null, (stored) => { // read the stored settings
  171. let fields = settings.fields,
  172. customTypes = settings.types || {},
  173. configId = config.id;
  174.  
  175. for (let id in fields) {
  176. let field = fields[id],
  177. fieldExists = false;
  178.  
  179. if (config.fields[id]) {
  180. fieldExists = true;
  181. }
  182.  
  183. // for each field definition create a field object
  184. if (field) {
  185. if (config.isOpen && fieldExists) {
  186. config.fields[id].remove();
  187. }
  188.  
  189. config.fields[id] = new GM_configField(field, stored[id], id,
  190. customTypes[field.type], configId);
  191.  
  192. // Add field to open frame
  193. if (config.isOpen) {
  194. config.fields[id].wrapper = config.fields[id].toNode();
  195. config.frameSection.appendChild(config.fields[id].wrapper);
  196. }
  197. } else if (!field && fieldExists) {
  198. // Remove field from open frame
  199. if (config.isOpen) {
  200. config.fields[id].remove();
  201. }
  202.  
  203. delete config.fields[id];
  204. }
  205. }
  206.  
  207. config.isInit = true;
  208. config.onInit.call(config);
  209. });
  210. } else {
  211. config.isInit = true;
  212. config.onInit.call(config);
  213. }
  214. }
  215.  
  216. let construct = function () {
  217. // Parsing of input provided via frontends
  218. GM_configInit(this, arguments);
  219. };
  220. construct.prototype = {
  221. // Support re-initalization
  222. init: function() {
  223. GM_configInit(this, arguments);
  224. },
  225.  
  226. // call GM_config.open() from your script to open the menu
  227. open: function () {
  228. // don't open before init is finished
  229. if (!this.isInit) {
  230. setTimeout(() => this.open(), 0);
  231. return;
  232. }
  233. // Die if the menu is already open on this page
  234. // You can have multiple instances but you can't open the same instance twice
  235. let match = document.getElementById(this.id);
  236. if (match && (match.tagName == "IFRAME" || match.childNodes.length > 0)) return;
  237.  
  238. // Sometimes "this" gets overwritten so create an alias
  239. let config = this;
  240.  
  241. // Function to build the mighty config window :)
  242. function buildConfigWin (body, head) {
  243. let create = config.create,
  244. fields = config.fields,
  245. configId = config.id,
  246. bodyWrapper = create('div', {id: configId + '_wrapper'});
  247.  
  248. // Append the style which is our default style plus the user style
  249. head.appendChild(
  250. create('style', {
  251. type: 'text/css',
  252. textContent: config.css.basic + config.css.stylish
  253. }));
  254.  
  255. // Add header and title
  256. bodyWrapper.appendChild(create('div', {
  257. id: configId + '_header',
  258. className: 'config_header block center'
  259. }, config.title));
  260.  
  261. // Append elements
  262. let section = bodyWrapper,
  263. secNum = 0; // Section count
  264.  
  265. // loop through fields
  266. for (let id in fields) {
  267. let field = fields[id],
  268. settings = field.settings;
  269.  
  270. if (settings.section) { // the start of a new section
  271. section = bodyWrapper.appendChild(create('div', {
  272. className: 'section_header_holder',
  273. id: configId + '_section_' + secNum
  274. }));
  275.  
  276. if (!Array.isArray(settings.section))
  277. settings.section = [settings.section];
  278.  
  279. if (settings.section[0])
  280. section.appendChild(create('div', {
  281. className: 'section_header center',
  282. id: configId + '_section_header_' + secNum
  283. }, settings.section[0]));
  284.  
  285. if (settings.section[1])
  286. section.appendChild(create('p', {
  287. className: 'section_desc center',
  288. id: configId + '_section_desc_' + secNum
  289. }, settings.section[1]));
  290. ++secNum;
  291. }
  292. if (secNum === 0) {
  293. section = bodyWrapper.appendChild(create('div', {
  294. className: 'section_header_holder',
  295. id: configId + '_section_' + (secNum++)
  296. }));
  297. }
  298.  
  299. // Create field elements and append to current section
  300. section.appendChild((field.wrapper = field.toNode()));
  301. }
  302. config.frameSection = section;
  303.  
  304. // Add save and close buttons
  305. bodyWrapper.appendChild(create('div',
  306. {id: configId + '_buttons_holder'},
  307.  
  308. create('button', {
  309. id: configId + '_saveBtn',
  310. textContent: 'Save',
  311. title: 'Save settings',
  312. className: 'saveclose_buttons',
  313. onclick: function () { config.save(); }
  314. }),
  315.  
  316. create('button', {
  317. id: configId + '_closeBtn',
  318. textContent: 'Close',
  319. title: 'Close window',
  320. className: 'saveclose_buttons',
  321. onclick: function () { config.close(); }
  322. }),
  323.  
  324. create('div',
  325. {className: 'reset_holder block'},
  326.  
  327. // Reset link
  328. create('a', {
  329. id: configId + '_resetLink',
  330. textContent: 'Reset to defaults',
  331. href: '#',
  332. title: 'Reset fields to default values',
  333. className: 'reset',
  334. onclick: function(e) { e.preventDefault(); config.reset(); }
  335. })
  336. )));
  337.  
  338. body.appendChild(bodyWrapper); // Paint everything to window at once
  339. config.center(); // Show and center iframe
  340. window.addEventListener('resize', config.center, false); // Center frame on resize
  341.  
  342. // Call the open() callback function
  343. config.onOpen(config.frame.contentDocument || config.frame.ownerDocument,
  344. config.frame.contentWindow || window,
  345. config.frame);
  346.  
  347. // Close frame on window close
  348. window.addEventListener('beforeunload', function () {
  349. config.close();
  350. }, false);
  351.  
  352. // Now that everything is loaded, make it visible
  353. config.frame.style.display = "block";
  354. config.isOpen = true;
  355. }
  356.  
  357. // Either use the element passed to init() or create an iframe
  358. if (this.frame) {
  359. this.frame.id = this.id; // Allows for prefixing styles with the config id
  360. if (this.frameStyle) this.frame.setAttribute('style', this.frameStyle);
  361. buildConfigWin(this.frame, this.frame.ownerDocument.getElementsByTagName('head')[0]);
  362. } else {
  363. // Create frame
  364. this.frame = this.create('iframe', { id: this.id });
  365. if (this.frameStyle) this.frame.setAttribute('style', this.frameStyle);
  366. document.body.appendChild(this.frame);
  367.  
  368. // In WebKit src can't be set until it is added to the page
  369. this.frame.src = '';
  370. // we wait for the iframe to load before we can modify it
  371. let that = this;
  372. this.frame.addEventListener('load', function(e) {
  373. let frame = config.frame;
  374. if (!frame.contentDocument) {
  375. that.log("GM_config failed to initialize default settings dialog node!");
  376. } else {
  377. let body = frame.contentDocument.getElementsByTagName('body')[0];
  378. body.id = config.id; // Allows for prefixing styles with the config id
  379. buildConfigWin(body, frame.contentDocument.getElementsByTagName('head')[0]);
  380. }
  381. }, false);
  382. }
  383. },
  384.  
  385. save: function () {
  386. this.write(null, null, (vals) => this.onSave(vals));
  387. },
  388.  
  389. close: function() {
  390. // If frame is an iframe then remove it
  391. if (this.frame && this.frame.contentDocument) {
  392. this.remove(this.frame);
  393. this.frame = null;
  394. } else if (this.frame) { // else wipe its content
  395. this.frame.innerHTML = "";
  396. this.frame.style.display = "none";
  397. }
  398.  
  399. // Null out all the fields so we don't leak memory
  400. let fields = this.fields;
  401. for (let id in fields) {
  402. let field = fields[id];
  403. field.wrapper = null;
  404. field.node = null;
  405. }
  406.  
  407. this.onClose(); // Call the close() callback function
  408. this.isOpen = false;
  409. },
  410.  
  411. set: function (name, val) {
  412. this.fields[name].value = val;
  413.  
  414. if (this.fields[name].node) {
  415. this.fields[name].reload();
  416. }
  417. },
  418.  
  419. get: function (name, getLive) {
  420. /* Migration warning */
  421. if (!this.isInit) {
  422. this.log('GM_config: get called before init, see https://github.com/sizzlemctwizzle/GM_config/issues/113');
  423. }
  424.  
  425. let field = this.fields[name],
  426. fieldVal = null;
  427.  
  428. if (getLive && field.node) {
  429. fieldVal = field.toValue();
  430. }
  431.  
  432. return fieldVal != null ? fieldVal : field.value;
  433. },
  434.  
  435. write: function (store, obj, cb) {
  436. let forgotten = null,
  437. values = null;
  438. if (!obj) {
  439. let fields = this.fields;
  440.  
  441. values = {};
  442. forgotten = {};
  443.  
  444. for (let id in fields) {
  445. let field = fields[id];
  446. let value = field.toValue();
  447.  
  448. if (field.save) {
  449. if (value != null) {
  450. values[id] = value;
  451. field.value = value;
  452. } else
  453. values[id] = field.value;
  454. } else
  455. forgotten[id] = value != null ? value : field.value;
  456. }
  457. }
  458.  
  459. (async () => {
  460. try {
  461. let val = this.stringify(obj || values);
  462. await this.setValue(store || this.id, val);
  463. } catch(e) {
  464. this.log("GM_config failed to save settings!");
  465. }
  466. cb(forgotten);
  467. })();
  468. },
  469.  
  470. read: function (store, cb) {
  471. (async () => {
  472. let val = await this.getValue(store || this.id, '{}');
  473. try {
  474. let rval = this.parser(val);
  475. cb(rval);
  476. } catch(e) {
  477. this.log("GM_config failed to read saved settings!");
  478. cb({});
  479. }
  480. })();
  481. },
  482.  
  483. reset: function () {
  484. let fields = this.fields;
  485.  
  486. // Reset all the fields
  487. for (let id in fields) { fields[id].reset(); }
  488.  
  489. this.onReset(); // Call the reset() callback function
  490. },
  491.  
  492. create: function () {
  493. let A = null,
  494. B = null;
  495. switch(arguments.length) {
  496. case 1:
  497. A = document.createTextNode(arguments[0]);
  498. break;
  499. default:
  500. A = document.createElement(arguments[0]);
  501. B = arguments[1];
  502. for (let b in B) {
  503. if (b.indexOf("on") == 0)
  504. A.addEventListener(b.substring(2), B[b], false);
  505. else if (",style,accesskey,id,name,src,href,which,for".indexOf("," +
  506. b.toLowerCase()) != -1)
  507. A.setAttribute(b, B[b]);
  508. else
  509. A[b] = B[b];
  510. }
  511. if (typeof arguments[2] == "string")
  512. A.innerHTML = arguments[2];
  513. else
  514. for (let i = 2, len = arguments.length; i < len; ++i)
  515. A.appendChild(arguments[i]);
  516. }
  517. return A;
  518. },
  519.  
  520. center: function () {
  521. let node = this.frame;
  522. if (!node) return;
  523. let style = node.style,
  524. beforeOpacity = style.opacity;
  525. if (style.display == 'none') style.opacity = '0';
  526. style.display = '';
  527. style.top = Math.floor((window.innerHeight / 2) - (node.offsetHeight / 2)) + 'px';
  528. style.left = Math.floor((window.innerWidth / 2) - (node.offsetWidth / 2)) + 'px';
  529. style.opacity = '1';
  530. },
  531.  
  532. remove: function (el) {
  533. if (el && el.parentNode) el.parentNode.removeChild(el);
  534. }
  535. };
  536.  
  537. construct.prototype.name = 'GM_config';
  538. construct.prototype.constructor = construct;
  539. let isGM4 = typeof GM.getValue !== 'undefined' &&
  540. typeof GM.setValue !== 'undefined';
  541. let isGM = isGM4 || (typeof GM_getValue !== 'undefined' &&
  542. typeof GM_getValue('a', 'b') !== 'undefined');
  543. construct.prototype.isGM = isGM;
  544.  
  545. if (!isGM4) {
  546. let promisify = (old) => (...args) => {
  547. return new Promise((resolve, reject) => {
  548. try {
  549. resolve(old.apply(this, args));
  550. } catch (e) {
  551. reject(e);
  552. }
  553. });
  554. };
  555.  
  556. let getValue = isGM ? GM_getValue
  557. : (name, def) => {
  558. let s = localStorage.getItem(name);
  559. return s !== null ? s : def;
  560. };
  561. let setValue = isGM ? GM_setValue
  562. : (name, value) => localStorage.setItem(name, value);
  563. let log = typeof GM_log !== 'undefined' ? GM_log : console.log;
  564.  
  565. GM.getValue = promisify(getValue);
  566. GM.setValue = promisify(setValue);
  567. GM.log = promisify(log);
  568. }
  569.  
  570. construct.prototype.stringify = JSON.stringify;
  571. construct.prototype.parser = JSON.parse;
  572. construct.prototype.getValue = GM.getValue;
  573. construct.prototype.setValue = GM.setValue;
  574. construct.prototype.log = GM.log || console.log;
  575.  
  576. // Passthrough frontends for new and old usage
  577. let config = function () {
  578. return new (config.bind.apply(construct,
  579. [null].concat(Array.from(arguments))));
  580. };
  581. config.prototype.constructor = config;
  582.  
  583. // Support old method of initalizing
  584. config.init = function () {
  585. GM_config = config.apply(this, arguments);
  586. GM_config.init = function() {
  587. GM_configInit(this, arguments);
  588. };
  589. };
  590.  
  591. // Reusable functions and properties
  592. // Usable via GM_config.*
  593. config.create = construct.prototype.create;
  594. config.isGM = construct.prototype.isGM;
  595. config.setValue = construct.prototype.setValue;
  596. config.getValue = construct.prototype.getValue;
  597. config.stringify = construct.prototype.stringify;
  598. config.parser = construct.prototype.parser;
  599. config.log = construct.prototype.log;
  600. config.remove = construct.prototype.remove;
  601. config.read = construct.prototype.read.bind(config);
  602. config.write = construct.prototype.write.bind(config);
  603.  
  604. return config;
  605. }(typeof GM === 'object' ? GM : Object.create(null)));
  606. let GM_configStruct = GM_config;
  607.  
  608. function GM_configField(settings, stored, id, customType, configId) {
  609. // Store the field's settings
  610. this.settings = settings;
  611. this.id = id;
  612. this.configId = configId;
  613. this.node = null;
  614. this.wrapper = null;
  615. this.save = typeof settings.save == "undefined" ? true : settings.save;
  616.  
  617. // Buttons are static and don't have a stored value
  618. if (settings.type == "button") this.save = false;
  619.  
  620. // if a default value wasn't passed through init() then
  621. // if the type is custom use its default value
  622. // else use default value for type
  623. // else use the default value passed through init()
  624. this['default'] = typeof settings['default'] == "undefined" ?
  625. customType ?
  626. customType['default']
  627. : this.defaultValue(settings.type, settings.options)
  628. : settings['default'];
  629.  
  630. // Store the field's value
  631. this.value = typeof stored == "undefined" ? this['default'] : stored;
  632.  
  633. // Setup methods for a custom type
  634. if (customType) {
  635. this.toNode = customType.toNode;
  636. this.toValue = customType.toValue;
  637. this.reset = customType.reset;
  638. }
  639. }
  640.  
  641. GM_configField.prototype = {
  642. create: GM_config.create,
  643.  
  644. defaultValue: function(type, options) {
  645. let value;
  646.  
  647. if (type.indexOf('unsigned ') == 0)
  648. type = type.substring(9);
  649.  
  650. switch (type) {
  651. case 'radio': case 'select':
  652. value = options[0];
  653. break;
  654. case 'checkbox':
  655. value = false;
  656. break;
  657. case 'int': case 'integer':
  658. case 'float': case 'number':
  659. value = 0;
  660. break;
  661. default:
  662. value = '';
  663. }
  664.  
  665. return value;
  666. },
  667.  
  668. toNode: function() {
  669. let field = this.settings,
  670. value = this.value,
  671. options = field.options,
  672. type = field.type,
  673. id = this.id,
  674. configId = this.configId,
  675. labelPos = field.labelPos,
  676. create = this.create;
  677.  
  678. function addLabel(pos, labelEl, parentNode, beforeEl) {
  679. if (!beforeEl) beforeEl = parentNode.firstChild;
  680. switch (pos) {
  681. case 'right': case 'below':
  682. if (pos == 'below')
  683. parentNode.appendChild(create('br', {}));
  684. parentNode.appendChild(labelEl);
  685. break;
  686. default:
  687. if (pos == 'above')
  688. parentNode.insertBefore(create('br', {}), beforeEl);
  689. parentNode.insertBefore(labelEl, beforeEl);
  690. }
  691. }
  692.  
  693. let retNode = create('div', { className: 'config_var',
  694. id: configId + '_' + id + '_var',
  695. title: field.title || '' }),
  696. firstProp;
  697.  
  698. // Retrieve the first prop
  699. for (let i in field) { firstProp = i; break; }
  700.  
  701. let label = field.label && type != "button" ?
  702. create('label', {
  703. id: configId + '_' + id + '_field_label',
  704. for: configId + '_field_' + id,
  705. className: 'field_label'
  706. }, field.label) : null;
  707.  
  708. let wrap = null;
  709. switch (type) {
  710. case 'textarea':
  711. retNode.appendChild((this.node = create('textarea', {
  712. innerHTML: value,
  713. id: configId + '_field_' + id,
  714. className: 'block',
  715. cols: (field.cols ? field.cols : 20),
  716. rows: (field.rows ? field.rows : 2)
  717. })));
  718. break;
  719. case 'radio':
  720. wrap = create('div', {
  721. id: configId + '_field_' + id
  722. });
  723. this.node = wrap;
  724.  
  725. for (let i = 0, len = options.length; i < len; ++i) {
  726. let radLabel = create('label', {
  727. className: 'radio_label'
  728. }, options[i]);
  729.  
  730. let rad = wrap.appendChild(create('input', {
  731. value: options[i],
  732. type: 'radio',
  733. name: id,
  734. checked: options[i] == value
  735. }));
  736.  
  737. let radLabelPos = labelPos &&
  738. (labelPos == 'left' || labelPos == 'right') ?
  739. labelPos : firstProp == 'options' ? 'left' : 'right';
  740.  
  741. addLabel(radLabelPos, radLabel, wrap, rad);
  742. }
  743.  
  744. retNode.appendChild(wrap);
  745. break;
  746. case 'select':
  747. wrap = create('select', {
  748. id: configId + '_field_' + id
  749. });
  750. this.node = wrap;
  751.  
  752. for (let i = 0, len = options.length; i < len; ++i) {
  753. let option = options[i];
  754. wrap.appendChild(create('option', {
  755. value: option,
  756. selected: option == value
  757. }, option));
  758. }
  759.  
  760. retNode.appendChild(wrap);
  761. break;
  762. default: // fields using input elements
  763. let props = {
  764. id: configId + '_field_' + id,
  765. type: type,
  766. value: type == 'button' ? field.label : value
  767. };
  768.  
  769. switch (type) {
  770. case 'checkbox':
  771. props.checked = value;
  772. break;
  773. case 'button':
  774. props.size = field.size ? field.size : 25;
  775. if (field.script) field.click = field.script;
  776. if (field.click) props.onclick = field.click;
  777. break;
  778. case 'hidden':
  779. break;
  780. default:
  781. // type = text, int, or float
  782. props.type = 'text';
  783. props.size = field.size ? field.size : 25;
  784. }
  785.  
  786. retNode.appendChild((this.node = create('input', props)));
  787. }
  788.  
  789. if (label) {
  790. // If the label is passed first, insert it before the field
  791. // else insert it after
  792. if (!labelPos)
  793. labelPos = firstProp == "label" || type == "radio" ?
  794. "left" : "right";
  795.  
  796. addLabel(labelPos, label, retNode);
  797. }
  798.  
  799. return retNode;
  800. },
  801.  
  802. toValue: function() {
  803. let node = this.node,
  804. field = this.settings,
  805. type = field.type,
  806. unsigned = false,
  807. rval = null;
  808.  
  809. if (!node) return rval;
  810.  
  811. if (type.indexOf('unsigned ') == 0) {
  812. type = type.substring(9);
  813. unsigned = true;
  814. }
  815.  
  816. switch (type) {
  817. case 'checkbox':
  818. rval = node.checked;
  819. break;
  820. case 'select':
  821. rval = node[node.selectedIndex].value;
  822. break;
  823. case 'radio':
  824. let radios = node.getElementsByTagName('input');
  825. for (let i = 0, len = radios.length; i < len; ++i) {
  826. if (radios[i].checked)
  827. rval = radios[i].value;
  828. }
  829. break;
  830. case 'button':
  831. break;
  832. case 'int': case 'integer':
  833. case 'float': case 'number':
  834. let num = Number(node.value);
  835. let warn = 'Field labeled "' + field.label + '" expects a' +
  836. (unsigned ? ' positive ' : 'n ') + 'integer value';
  837.  
  838. if (isNaN(num) || (type.substr(0, 3) == 'int' &&
  839. Math.ceil(num) != Math.floor(num)) ||
  840. (unsigned && num < 0)) {
  841. alert(warn + '.');
  842. return null;
  843. }
  844.  
  845. if (!this._checkNumberRange(num, warn))
  846. return null;
  847. rval = num;
  848. break;
  849. default:
  850. rval = node.value;
  851. break;
  852. }
  853.  
  854. return rval; // value read successfully
  855. },
  856.  
  857. reset: function() {
  858. let node = this.node,
  859. field = this.settings,
  860. type = field.type;
  861.  
  862. if (!node) return;
  863.  
  864. switch (type) {
  865. case 'checkbox':
  866. node.checked = this['default'];
  867. break;
  868. case 'select':
  869. for (let i = 0, len = node.options.length; i < len; ++i) {
  870. if (node.options[i].textContent == this['default'])
  871. node.selectedIndex = i;
  872. }
  873. break;
  874. case 'radio':
  875. let radios = node.getElementsByTagName('input');
  876. for (let i = 0, len = radios.length; i < len; ++i) {
  877. if (radios[i].value == this['default'])
  878. radios[i].checked = true;
  879. }
  880. break;
  881. case 'button' :
  882. break;
  883. default:
  884. node.value = this['default'];
  885. break;
  886. }
  887. },
  888.  
  889. remove: function() {
  890. GM_config.remove(this.wrapper);
  891. this.wrapper = null;
  892. this.node = null;
  893. },
  894.  
  895. reload: function() {
  896. let wrapper = this.wrapper;
  897. if (wrapper) {
  898. let fieldParent = wrapper.parentNode;
  899. let newWrapper = this.toNode();
  900. fieldParent.insertBefore(newWrapper, wrapper);
  901. GM_config.remove(this.wrapper);
  902. this.wrapper = newWrapper;
  903. }
  904. },
  905.  
  906. _checkNumberRange: function(num, warn) {
  907. let field = this.settings;
  908. if (typeof field.min == "number" && num < field.min) {
  909. alert(warn + ' greater than or equal to ' + field.min + '.');
  910. return null;
  911. }
  912.  
  913. if (typeof field.max == "number" && num > field.max) {
  914. alert(warn + ' less than or equal to ' + field.max + '.');
  915. return null;
  916. }
  917. return true;
  918. }
  919. };