GMCommonAPI.js

GM Common API (GMC) is a simple library designed for easily adding Greasemonkey 4 compatibilty to existing userscripts

Tento skript by neměl být instalován přímo. Jedná se o knihovnu, kterou by měly jiné skripty využívat pomocí meta příkazu // @require https://update.greatest.deepsurf.us/scripts/34527/751210/GMCommonAPIjs.js

  1. /*
  2. * GM Common API is a library for userscripts. A "Synchronious API" compatible
  3. * with both Greasemonkey 4 WebExtension and all the other commonly used
  4. * userscript managers. Using GM Common API can be a fast and easy way to add
  5. * Greasemonkey 4 WebExtension compatibility to (some) existing userscripts.
  6. *
  7. * https://greatest.deepsurf.us/scripts/34527-gmcommonapi-js
  8. * https://github.com/StigNygaard/GMCommonAPI.js
  9. */
  10.  
  11.  
  12. var GMC = GMC || (function api() {
  13.  
  14. // CHANGELOG - The most important updates/versions:
  15. let changelog = [
  16. {version: '2017.12.16', description: 'GM4 actually supports GM.openInTab.'},
  17. {version: '2017.12.07', description: 'Fixing an error seen in Chrome/Tampermonkey.'},
  18. {version: '2017.12.06', description: 'Prefetch @resource objects in GM4+ for potential performance improvements (Requires @grant GM.getResourceUrl).'},
  19. {version: '2017.12.03', description: 'Some refactoring. Fix: getResourceUrl would sometimes only return one resource in GM4.'},
  20. {version: '2017.11.18', description: 'Adding GMC.xmlHttpRequest().'},
  21. {version: '2017.11.11', description: 'Advanced options for menus (Via GMC.registerMenuCommand() using new options parameter).'},
  22. {version: '2017.10.29', description: 'Adding GMC.listValues(), GMC.listLocalStorageValues() and GMC.listSessionStorageValues().'},
  23. {version: '2017.10.28', description: '@grant not needed for use of GM.info/GM_info.'},
  24. {version: '2017.10.25', description: 'Initial release.'}
  25. ];
  26.  
  27.  
  28. /*
  29. * GMC.info
  30. *
  31. * Maps to GM_info or GM.info object.
  32. * Grants: none needed.
  33. */
  34. let info = (GM_info ? GM_info : (typeof GM === 'object' && GM !== null && typeof GM.info === 'object' ? GM.info : null) );
  35.  
  36.  
  37. /*
  38. * GMC.registerMenuCommand(caption, commandFunc, accessKey)
  39. * GMC.registerMenuCommand(caption, commandFunc, options)
  40. *
  41. * Currently the GM4 API is missing GM.registerMenuCommand, but luckily Firefox supports HTML5
  42. * context menus, which are created by this method when supported (Currently only supported by
  43. * the Firefox family). AccessKey is currently ignored for context menus.
  44. * Instead of the accessKey string parameter, there's an option to pass an options object
  45. * adding multiple configuration options for fine-tuning menus. Example:
  46. * GMC.registerMenuCommand( 'Hide the top', toggleTop , {accessKey: 'T', type: 'checkbox', checked: isHiddenTop()} );
  47. * Currently supported properties in the options object are:
  48. * accessKey, topLevel, id, name, type, checked, disabled & icon.
  49. * Todo: Document the properties in the options object.
  50. *
  51. * Grants:
  52. * GM_registerMenuCommand
  53. * GM.registerMenuCommand (Optional for possible future support. Currently not available with any userscript manager)
  54. */
  55. function registerMenuCommand(caption, commandFunc, options) {
  56. if (typeof options === 'string') {
  57. options = {'accessKey': options};
  58. } else if (typeof options === 'undefined') {
  59. options = {};
  60. }
  61. // "Legacy" menu:
  62. if (!options['disabled']) {
  63. let prefix = '';
  64. if (options['type'] === 'radio') {
  65. prefix = options['checked'] ? '\u26AB ' : '\u26AA '; // ⚫/⚪
  66. } else if (options['type'] === 'checkbox') {
  67. prefix = options['checked'] ? '\u2B1B ' : '\u2B1C '; // ⬛/⬜
  68. }
  69. if (typeof GM_registerMenuCommand === 'function') {
  70. // Supported by most userscript managers, but NOT with Greasemonkey 4 WebExtension
  71. GM_registerMenuCommand(prefix + caption, commandFunc, options['accessKey']);
  72. } else if (typeof GM === 'object' && GM !== null && typeof GM.registerMenuCommand === 'function') {
  73. // NOT implemented in Greasemonkey 4.0 WebExtension, but if later?...
  74. GM.registerMenuCommand(prefix + caption, commandFunc, options['accessKey']);
  75. }
  76. }
  77. // HTML5 context menu (currently only supported by the Firefox family):
  78. if (contextMenuSupported()) {
  79. if (!document.body) {
  80. alert('GMC Error: Body element for context menu not found. If running userscript at "document-start" you might need to delay initialization of menus.');
  81. return;
  82. }
  83. let topMenu = null;
  84. if (document.body.getAttribute('contextmenu')) {
  85. // If existing context menu on body, don't replace but use/extend it...
  86. topMenu = document.querySelector('menu#' + document.body.getAttribute('contextmenu'));
  87. }
  88. if (!topMenu) {
  89. // if not already defined, create the "top menu container"
  90. topMenu = document.createElement('menu');
  91. topMenu.setAttribute('type', 'context');
  92. topMenu.setAttribute('id', 'gm-registered-menu');
  93. document.body.appendChild(topMenu);
  94. document.body.setAttribute('contextmenu', topMenu.getAttribute('id'));
  95. }
  96. // Create menu item
  97. let menuItem = document.createElement('menuitem');
  98. menuItem.setAttribute('type', options['type'] ? options['type'] : 'command'); // command, checkbox or radio
  99. menuItem.setAttribute('label', caption);
  100. if (options['id']) menuItem.setAttribute('id', options['id']);
  101. if (options['name']) menuItem.setAttribute('name', options['name']);
  102. if (options['checked']) menuItem.setAttribute('checked', 'checked');
  103. if (options['disabled']) menuItem.setAttribute('disabled', 'disabled');
  104. if (options['icon']) menuItem.setAttribute('icon', options['icon']); // does icon work on radio/checkbox or only command?
  105. // Append menuitem
  106. if (options['topLevel']) {
  107. topMenu.appendChild(menuItem);
  108. } else { // script menu
  109. let scriptMenu = topMenu.querySelector('menu[label="' + getScriptName() + '"]');
  110. if (!scriptMenu) {
  111. // if not already defined, create a "sub-menu" for current userscript
  112. scriptMenu = document.createElement('menu');
  113. scriptMenu.setAttribute('label', getScriptName());
  114. // icon = @icon from metadata??? NO, icon not working for menu elements :-(
  115. topMenu.appendChild(scriptMenu);
  116. }
  117. scriptMenu.appendChild(menuItem);
  118. }
  119. menuItem.addEventListener('click', commandFunc, false);
  120. }
  121. }
  122.  
  123.  
  124. /*
  125. * GMC.getResourceURL(resourceName)
  126. * GMC.getResourceUrl(resourceName)
  127. *
  128. * This will use GM_getResourceURL if available, and otherwise try to find an url directly via
  129. * the GMC.info object properties.
  130. * Grants:
  131. * GM_getResourceURL
  132. * GM.getResourceUrl (Optional, but add this for potential performance improvement in GM4+)
  133. */
  134. function getResourceUrl(resourceName) {
  135. if (typeof GM_getResourceURL === 'function') {
  136. return GM_getResourceURL(resourceName);
  137. } else if (info) {
  138. if (typeof info.script === 'object' && typeof info.script.resources === 'object' && typeof info.script.resources[resourceName] === 'object' && info.script.resources[resourceName].url) {
  139. return info.script.resources[resourceName].url;
  140. } else if (info.scriptMetaStr) {
  141. // Parse metadata block to find the original "remote url" instead:
  142. let ptrn = new RegExp('^\\s*\\/\\/\\s*@resource\\s+([^\\s]+)\\s+([^\\s]+)\\s*$','igm');
  143. let result;
  144. while((result = ptrn.exec(info.scriptMetaStr)) !== null) {
  145. if (result[1] === resourceName) return result[2];
  146. // and do a GM4 "prefetch"?
  147. }
  148. }
  149. alert('GMC Error: Cannot find url of resource=' + resourceName + ' in GMC.info object');
  150. } else {
  151. alert('GMC Error: Cannot lookup resourceURL (Missing @grant for GM_getResourceURL?)');
  152. }
  153. }
  154.  
  155.  
  156. /*
  157. * GMC.setValue(name, value)
  158. *
  159. * When supported, this points to GM_setValue which stores values in a userscript specific
  160. * database. Otherwise the HTML5 Web Storage is used, which is a domain(+protocol) specific
  161. * database in the browser.
  162. * IMPORTANT: If your userscript is a "single-domain type", the difference in storage type is
  163. * probably not a problem, but for "multiple-domain userscripts" GMC.setValue() might not be a
  164. * satisfying solution.
  165. * To prevent mistakenly overwriting or reading other clientscript's values when using Web
  166. * Storage, a prefix based on userscript namespace and scriptname is added to name used in Web
  167. * Storage.
  168. * Grants:
  169. * GM_setValue
  170. */
  171. function setValue(name, value) {
  172. if (typeof GM_setValue === 'function') {
  173. GM_setValue(name, value);
  174. } else {
  175. setLocalStorageValue(name, value);
  176. }
  177. }
  178.  
  179.  
  180. /*
  181. * GMC.getValue(name, defvalue)
  182. *
  183. * Get the values stored using GMC.setValue(). When supported via GM_getValue and otherwise from
  184. * HTML5 Web Storage.
  185. * Grants:
  186. * GM_getValue
  187. */
  188. function getValue(name, defvalue) { // getLocalStorageValue: function(name, defvalue) {
  189. if (typeof GM_getValue === 'function') {
  190. return GM_getValue(name, defvalue);
  191. } else {
  192. return getLocalStorageValue(name, defvalue);
  193. }
  194. }
  195.  
  196.  
  197. /*
  198. * GMC.deleteValue(name)
  199. *
  200. * Deletes a value stored using GMC.setValue(). When supported via GM_deleteValue and otherwise
  201. * from HTML5 Web Storage.
  202. * Grants:
  203. * GM_deleteValue
  204. */
  205. function deleteValue(name) {
  206. if (typeof GM_deleteValue === 'function') {
  207. GM_deleteValue(name);
  208. } else {
  209. deleteLocalStorageValue(name);
  210. }
  211. }
  212.  
  213.  
  214. /*
  215. * GMC.listValues()
  216. *
  217. * Returns the values (key-names) stored using GMC.setValue(). When supported via GM_listValues
  218. * and otherwise from HTML5 Web Storage.
  219. * Grants:
  220. * GM_listValues
  221. */
  222. function listValues() {
  223. if (typeof GM_listValues === 'function') {
  224. return GM_listValues();
  225. } else {
  226. return listLocalStorageValues();
  227. }
  228. }
  229.  
  230.  
  231. /*
  232. * GMC.setLocalStorageValue(name, value)
  233. *
  234. * Save value in HTML5 Web Storage (window.localStorage), which is a domain(+protocol) specific
  235. * database in the browser. To prevent mistakenly overwriting or reading other clientscript's
  236. * values when using Web Storage, a prefix based on userscript namespace and scriptname is added
  237. * to the name used in Web Storage.
  238. * Grants: none needed.
  239. */
  240. function setLocalStorageValue(name, value) {
  241. localStorage.setItem(getScriptIdentifier() + '_' + name, value);
  242. }
  243.  
  244.  
  245. /*
  246. * GMC.getLocalStorageValue(name, defvalue)
  247. *
  248. * Get a value that was stored using GMC.setLocalStorageValue().
  249. * Grants: none needed.
  250. */
  251. function getLocalStorageValue(name, defvalue) {
  252. if ((getScriptIdentifier()+'_'+name) in localStorage) {
  253. return localStorage.getItem(getScriptIdentifier()+'_'+name);
  254. } else {
  255. return defvalue;
  256. }
  257. }
  258.  
  259.  
  260. /*
  261. * GMC.deleteLocalStorageValue(name)
  262. *
  263. * Deletes a value that was stored using GMC.setLocalStorageValue().
  264. * Grants: none needed.
  265. */
  266. function deleteLocalStorageValue(name) {
  267. localStorage.removeItem(getScriptIdentifier() + '_' + name);
  268. }
  269.  
  270.  
  271. /*
  272. * GMC.listLocalStorageValues()
  273. *
  274. * Returns the values (key-names) stored using GMC.setLocalStorageValue().
  275. * Grants: none needed.
  276. */
  277. function listLocalStorageValues() {
  278. let values = [];
  279. let prefix = getScriptIdentifier();
  280. let prelen = getScriptIdentifier().length;
  281. for (let i = 0; i < localStorage.length; i++) {
  282. if (localStorage.key(i).substr(0, prelen) === prefix) {
  283. values.push(localStorage.key(i).substr(prelen+1));
  284. }
  285. }
  286. return values;
  287. }
  288.  
  289.  
  290. /*
  291. * GMC.setSessionStorageValue(name, value)
  292. *
  293. * Similar to setLocalStorageValue(), but setSessionStorageValue() only stores for the current
  294. * session.
  295. * Grants: none needed.
  296. */
  297. function setSessionStorageValue(name, value) {
  298. sessionStorage.setItem(getScriptIdentifier() + '_' + name, value);
  299. }
  300.  
  301.  
  302. /*
  303. * GMC.getSessionStorageValue(name, defvalue)
  304. *
  305. * Get a value that was stored using GMC.setSessionStorageValue().
  306. * Grants: none needed.
  307. */
  308. function getSessionStorageValue(name, defvalue) {
  309. if ((getScriptIdentifier()+'_'+name) in localStorage) {
  310. return sessionStorage.getItem(getScriptIdentifier()+'_'+name);
  311. } else {
  312. return defvalue;
  313. }
  314. }
  315.  
  316.  
  317. /*
  318. * GMC.deleteSessionStorageValue(name)
  319. *
  320. * Deletes a value that was stored using GMC.setSessionStorageValue().
  321. * Grants: none needed.
  322. */
  323. function deleteSessionStorageValue(name) {
  324. sessionStorage.removeItem(getScriptIdentifier() + '_' + name);
  325. }
  326.  
  327.  
  328. /*
  329. * GMC.listSessionStorageValues()
  330. *
  331. * Returns the values (key-names) stored using GMC.setSessionStorageValue().
  332. * Grants: none needed.
  333. */
  334. function listSessionStorageValues() {
  335. let values = [];
  336. let prefix = getScriptIdentifier();
  337. let prelen = getScriptIdentifier().length;
  338. for (let i = 0; i < sessionStorage.length; i++) {
  339. if (sessionStorage.key(i).substr(0, prelen) === prefix) {
  340. values.push(sessionStorage.key(i).substr(prelen+1));
  341. }
  342. }
  343. return values;
  344. }
  345.  
  346.  
  347. /*
  348. * GMC.log(message)
  349. *
  350. * Writes a log-line to the console. It will use GM_log if supported/granted, otherwise
  351. * it will do it using window.console.log().
  352. * Grants:
  353. * GM_log
  354. */
  355. function log(message) {
  356. if(typeof GM_log === 'function') {
  357. GM_log(message);
  358. } else if (window.console) {
  359. if (info) {
  360. window.console.log(getScriptName() + ' : ' + message);
  361. } else {
  362. window.console.log('GMC : ' + message);
  363. }
  364. }
  365. }
  366.  
  367.  
  368. /*
  369. * GMC.setClipboard(text)
  370. *
  371. * Sets content of the clipboard by using either GM.setClipboard or GM_setClipboard.
  372. * Grants:
  373. * GM.setClipboard
  374. * GM_setClipboard
  375. */
  376. let setClipboard = (typeof GM_setClipboard === 'function' ? GM_setClipboard : (typeof GM === 'object' && GM !== null && typeof GM.setClipboard === 'function' ? GM.setClipboard : null) );
  377.  
  378.  
  379. /*
  380. * GMC.addStyle(style)
  381. *
  382. * Adds style in a an element in html header.
  383. * Grants:
  384. * GM_addStyle (Optional. Will be used when available, but this method should normally work fine without)
  385. * GM.addStyle (Optional for possible future support. Currently not available with any userscript manager)
  386. */
  387. function addStyle(style) {
  388. if (typeof GM_addStyle === 'function') {
  389. return GM_addStyle(style);
  390. } else if (typeof GM === 'object' && GM !== null && typeof GM.addStyle === 'function') {
  391. return GM.addStyle(style); // For possible future support. Will Probably return undefined.
  392. } else {
  393. let head = document.getElementsByTagName('head')[0];
  394. if (head) {
  395. let styleElem = document.createElement('style');
  396. styleElem.setAttribute('type', 'text/css');
  397. styleElem.textContent = style;
  398. head.appendChild(styleElem);
  399. return styleElem;
  400. }
  401. alert('GMC Error: Unable to add style element to head element. If running userscript at "document-start" you might need to delay initialization of styles.');
  402. }
  403. }
  404.  
  405.  
  406. /*
  407. * GMC.openInTab(url, open_in_background)
  408. *
  409. * Opens url in a new tab.
  410. * Grants:
  411. * GM.openInTab
  412. * GM_openInTab
  413. */
  414. function openInTab(url, open_in_background) {
  415. if (typeof GM_openInTab === 'function') {
  416. return GM_openInTab(url, open_in_background);
  417. } else if (typeof GM === 'object' && GM !== null && typeof GM.openInTab === 'function') {
  418. return GM.openInTab(url, open_in_background);
  419. }
  420. return window.open(url);
  421. }
  422.  
  423.  
  424. /*
  425. * GMC.xmlHttpRequest(details)
  426. * GMC.xmlhttpRequest(details)
  427. *
  428. * Forwards to either GM_xmlhttpRequest or GM.xmlHttpRequest.
  429. * When adding @grant declarations, make sure to take notice of the case differences between
  430. * the APIs. GMC supports both case-variants (GMC.xmlHttpRequest and GMC.xmlhttpRequest).
  431. * Also remember to add needed @connect declarations for Tampermonkey:
  432. * https://tampermonkey.net/documentation.php#_connect
  433. *
  434. * Grants:
  435. * GM.xmlHttpRequest
  436. * GM_xmlhttpRequest
  437. */
  438. function xmlHttpRequest(details) {
  439. if (typeof GM_xmlhttpRequest === 'function') {
  440. return GM_xmlhttpRequest(details);
  441. } else if (typeof GM === 'object' && GM !== null && typeof GM.xmlHttpRequest === 'function') {
  442. return GM.xmlHttpRequest(details); // probably undefined return value!
  443. }
  444. alert('GMC Error: xmlHttpRequest not found! Missing or misspelled @grant declaration? (Be aware of case differences in the APIs!)');
  445. }
  446.  
  447.  
  448. /*
  449. * GMC.getScriptName()
  450. *
  451. * Simply returns script name as defined in meta data. If no name was defined, returns "Userscript".
  452. * Grants: none needed.
  453. */
  454. function getScriptName() {
  455. if (typeof info.script.name === 'string' && info.script.name.trim().length > 0) {
  456. return info.script.name.trim();
  457. } else {
  458. return 'Userscript';
  459. }
  460. }
  461.  
  462.  
  463. /*
  464. * GMC.getScriptNamespace()
  465. *
  466. * Simply returns the script's namespace as defined in meta data.
  467. * Grants: none needed.
  468. */
  469. function getScriptNamespace() {
  470. if (typeof info.script.namespace === 'string') {
  471. return info.script.namespace.trim();
  472. } else {
  473. return '';
  474. }
  475. }
  476.  
  477.  
  478.  
  479. // Internal, temporary and experimental stuff:
  480.  
  481. function isGreasemonkey4up() {
  482. if (typeof info.scriptHandler === 'string' && typeof info.version === 'string') {
  483. return info.scriptHandler === 'Greasemonkey' && parseInt(info.version,10)>=4;
  484. }
  485. return false;
  486. }
  487. function contextMenuSupported() { // Argh, it's a bit ugly, not 100% accurate (and probably not really necessary)
  488. let oMenu = document.createElement('menu');
  489. return (oMenu.type !== 'undefined'); // type="list|context|toolbar" if supported ?
  490. }
  491. function getScriptIdentifier() { // A "safe" identifier without any special characters (but doesn't work well for non-latin :-/ )
  492. if (info && typeof info.script === 'object') {
  493. return 'gmc' + getScriptNamespace().replace(/[^\w]+/g,'x') + getScriptName().replace(/[^\w]+/g,'x');
  494. } else {
  495. alert('GMC Error: Script Namespace or Name not found.');
  496. }
  497. }
  498. function inspect(obj) { // for some debugging
  499. let output='';
  500. Object.keys(obj).forEach(function(key, idx) {
  501. output+=key+': ' + typeof obj[key] + ((typeof obj[key] === 'string' || typeof obj[key] === 'boolean' || typeof obj[key] === 'number') ? ' = ' + obj[key] : '') + '\n';
  502. });
  503. alert(output);
  504. }
  505.  
  506. if (typeof GM === 'object' && GM !== null && typeof GM.getResourceUrl === 'function' && typeof info === 'object' && typeof info.script === 'object' && typeof info.script.resources === 'object' ) {
  507. // Prefetch @resource objects and update info.script.resources[resourcename].url with local address, using GM.getResourceUrl().
  508. prefetchResource = function(name) {
  509. if (typeof info === 'object' && typeof info.script === 'object' && typeof info.script.resources === 'object') {
  510. let obj = info.script.resources[name];
  511. if (typeof GM === 'object' && GM !== null && typeof GM.getResourceUrl === 'function' && obj) {
  512. GM.getResourceUrl(obj.name).then(function (url) {
  513. obj.url = url;
  514. }, function (err) {
  515. log('Error fetching resource ' + obj.name + ': ' + err);
  516. });
  517. } else {
  518. log('Error, info.script.resources[' + name + '] not found in resources object.');
  519. }
  520. }
  521. };
  522. Object.keys(info.script.resources).forEach(function(key, idx) {
  523. prefetchResource(key);
  524. });
  525. }
  526.  
  527. return {
  528. info: info,
  529. registerMenuCommand: registerMenuCommand,
  530. getResourceUrl: getResourceUrl,
  531. getResourceURL: getResourceUrl,
  532. setValue: setValue,
  533. getValue: getValue,
  534. deleteValue: deleteValue,
  535. listValues: listValues,
  536. setLocalStorageValue: setLocalStorageValue,
  537. getLocalStorageValue: getLocalStorageValue,
  538. deleteLocalStorageValue: deleteLocalStorageValue,
  539. listLocalStorageValues: listLocalStorageValues,
  540. setSessionStorageValue: setSessionStorageValue,
  541. getSessionStorageValue: getSessionStorageValue,
  542. deleteSessionStorageValue: deleteSessionStorageValue,
  543. listSessionStorageValues: listSessionStorageValues,
  544. log: log,
  545. setClipboard: setClipboard,
  546. addStyle: addStyle,
  547. openInTab: openInTab,
  548. xmlHttpRequest: xmlHttpRequest,
  549. xmlhttpRequest: xmlHttpRequest,
  550. getScriptName: getScriptName,
  551. getScriptNamespace: getScriptNamespace,
  552.  
  553. // Temporary and experimental stuff:
  554. isGreasemonkey4up: isGreasemonkey4up,
  555. contextMenuSupported: contextMenuSupported,
  556. inspect: inspect
  557. };
  558.  
  559. })();