Greasy Fork is available in English.

Greasyfork 快捷编辑收藏

在GF脚本页添加快速打开收藏集编辑页面功能

Fra 06.02.2024. Se den seneste versjonen.

  1. /* eslint-disable no-multi-spaces */
  2.  
  3. // ==UserScript==
  4. // @name Greasyfork 快捷编辑收藏
  5. // @name:zh-CN Greasyfork 快捷编辑收藏
  6. // @name:zh-TW Greasyfork 快捷編輯收藏
  7. // @name:en Greasyfork script-set-edit button
  8. // @name:en-US Greasyfork script-set-edit button
  9. // @name:fr Greasyfork Set Edit+
  10. // @namespace Greasyfork-Favorite
  11. // @version 0.2.3
  12. // @description 在GF脚本页添加快速打开收藏集编辑页面功能
  13. // @description:zh-CN 在GF脚本页添加快速打开收藏集编辑页面功能
  14. // @description:zh-TW 在GF腳本頁添加快速打開收藏集編輯頁面功能
  15. // @description:en Add / Remove script into / from script set directly in GF script info page
  16. // @description:en-US Add / Remove script into / from script set directly in GF script info page
  17. // @description:fr Ajouter un script à un jeu de scripts / supprimer un script d'un jeu de scripts directement sur la page d'informations sur les scripts GF
  18. // @author PY-DNG
  19. // @license GPL-3
  20. // @match http*://*.greatest.deepsurf.us/*
  21. // @match http*://*.sleazyfork.org/*
  22. // @match http*://greatest.deepsurf.us/*
  23. // @match http*://sleazyfork.org/*
  24. // @require https://update.greatest.deepsurf.us/scripts/456034/1303041/Basic%20Functions%20%28For%20userscripts%29.js
  25. // @require https://greatest.deepsurf.us/scripts/460385-gm-web-hooks/code/script.js?version=1221394
  26. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAbBJREFUOE+Vk7GKGlEUhr8pAiKKDlqpCDpLUCzWBxCENBa+hBsL9wHsLWxXG4tNtcGH0MIiWopY7JSGEUWsbESwUDMw4Z7siLsZDbnlPff/7n/+e67G38sA6sAXIPVWXgA/gCdgfinRPuhfCoXCw3Q65XA4eLBl6zvw1S2eAZqmvTqOc5/NZhkMBqRSKWzbvgYxgbwquoAX4MGyLHK5HIlEgtFo9C+IOFEAo1gsWsvlUmyPx2MymYxAhsMh6XT6lpM7BXjWdf1xNpuRz+fl8GQywTAMGo0G1WpVnJxOJ692vinADPgcDAaZz+cCOR6PmKZJPB4XUb/fp1wuewF+KoBCf1JVBVE5dDodms3mWdDtdqlUKl6AX+8ALmS9XgtM0/5kvNlspKX9fv8RIgBp4bISCoXo9XqsVitKpRK6rrPb7STQ7XZ7eVRaeAYerz14OBxGOfL7/eIgmUwKzHEcJZEQ1eha1wBqPxqNihufzyeQWCzmtiPPqJYM0jWIyiISibBYLAgEAtTrdVqt1nmQXN0rcH/LicqmVqvRbrdN27bfjbKru+nk7ZD3Z7q4+b++82/YPKIrXsKZ3AAAAABJRU5ErkJggg==
  27. // @grant GM_xmlhttpRequest
  28. // @grant GM_setValue
  29. // @grant GM_getValue
  30. // ==/UserScript==
  31.  
  32. /* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager */
  33. /* global GMXHRHook GMDLHook */
  34.  
  35. (function __MAIN__() {
  36. 'use strict';
  37.  
  38. const CONST = {
  39. Text: {
  40. 'zh-CN': {
  41. FavEdit: '收藏集:',
  42. Add: '加入此集',
  43. Remove: '移出此集',
  44. Edit: '手动编辑',
  45. EditIframe: '页内编辑',
  46. CloseIframe: '关闭编辑',
  47. CopySID: '复制脚本ID',
  48. Working: ['工作中...', '就快好了...'],
  49. InSetStatus: ['[ ]', '[✔]'],
  50. Error: {
  51. AlreadyExist: '脚本已经在此收藏集中了',
  52. NotExist: '脚本不在此收藏集中',
  53. Unknown: '未知错误'
  54. }
  55. },
  56. 'zh-TW': {
  57. FavEdit: '收藏集:',
  58. Add: '加入此集',
  59. Remove: '移出此集',
  60. Edit: '手動編輯',
  61. EditIframe: '頁內編輯',
  62. CloseIframe: '關閉編輯',
  63. CopySID: '複製腳本ID',
  64. Working: ['工作中...', '就快好了...'],
  65. InSetStatus: ['[ ]', '[✔]'],
  66. Error: {
  67. AlreadyExist: '腳本已經在此收藏集中了',
  68. NotExist: '腳本不在此收藏集中',
  69. Unknown: '未知錯誤'
  70. }
  71. },
  72. 'en': {
  73. FavEdit: 'Add to/Remove from favorite list: ',
  74. Add: 'Add',
  75. Remove: 'Remove',
  76. Edit: 'Edit Manually',
  77. EditIframe: 'In-Page Edit',
  78. CloseIframe: 'Close Editor',
  79. CopySID: 'Copy Script-ID',
  80. Working: ['Working...', 'Just a moment...'],
  81. InSetStatus: ['[ ]', '[✔]'],
  82. Error: {
  83. AlreadyExist: 'Script is already in set',
  84. NotExist: 'Script is not in set yet',
  85. Unknown: 'Unknown Error'
  86. }
  87. },
  88. 'default': {
  89. FavEdit: 'Add to/Remove from favorite list: ',
  90. Add: 'Add',
  91. Remove: 'Remove',
  92. Edit: 'Edit Manually',
  93. EditIframe: 'In-Page Edit',
  94. CloseIframe: 'Close Editor',
  95. CopySID: 'Copy Script-ID',
  96. Working: ['Working...', 'Just a moment...'],
  97. InSetStatus: ['[ ]', '[✔]'],
  98. Error: {
  99. AlreadyExist: 'Script is already in set',
  100. NotExist: 'Script is not in set yet',
  101. Unknown: 'Unknown Error'
  102. }
  103. },
  104. }
  105. }
  106.  
  107. // Get i18n code
  108. let i18n = $('#language-selector-locale') ? $('#language-selector-locale').value : navigator.language;
  109. if (!Object.keys(CONST.Text).includes(i18n)) {i18n = 'default';}
  110.  
  111. main()
  112. function main() {
  113. const HOST = getHost();
  114. const API = getAPI();
  115.  
  116. // Common actions
  117. commons();
  118.  
  119. // API-based actions
  120. switch(API[1]) {
  121. case "scripts":
  122. API[2] && centerScript(API);
  123. break;
  124. default:
  125. DoLog('API is {}'.replace('{}', API));
  126. }
  127. }
  128.  
  129. function centerScript(API) {
  130. switch(API[3]) {
  131. case undefined:
  132. pageScript();
  133. break;
  134. case 'code':
  135. pageCode();
  136. break;
  137. case 'feedback':
  138. pageFeedback();
  139. break;
  140. }
  141. }
  142.  
  143. function commons() {
  144. // Your common actions here...
  145. GMXHRHook(5);
  146. }
  147.  
  148. function pageScript() {
  149. addFavPanel();
  150. }
  151.  
  152. function pageCode() {
  153. addFavPanel();
  154. }
  155.  
  156. function pageFeedback() {
  157. addFavPanel();
  158. }
  159.  
  160. function addFavPanel() {
  161. if (!getUserpage()) {return false;}
  162. GUI();
  163.  
  164. function GUI() {
  165. // Get elements
  166. const script_after = $('#script-feedback-suggestion+*') || $('#new-script-discussion');
  167. const script_parent = script_after.parentElement;
  168.  
  169. // My elements
  170. const script_favorite = $CrE('div');
  171. script_favorite.id = 'script-favorite';
  172. script_favorite.style.margin = '0.75em 0';
  173. script_favorite.innerHTML = CONST.Text[i18n].FavEdit;
  174.  
  175. const favorite_groups = $CrE('select');
  176. favorite_groups.id = 'favorite-groups';
  177.  
  178. const stored_sets = GM_getValue('script-sets', {sets: []}).sets;
  179. for (const set of stored_sets) {
  180. // Make <option>
  181. const option = $CrE('option');
  182. option.innerText = set.name;
  183. option.value = set.linkedit;
  184. $APD(favorite_groups, option);
  185. }
  186. adjustWidth();
  187.  
  188. refresh();
  189. $AEL(favorite_groups, 'change', function(e) {
  190. favorite_edit.href = favorite_groups.value;
  191. refreshButtonDisplay();
  192. });
  193.  
  194. const makeBtn = (id, innerHTML, onClick, isLink=false) => $$CrE({
  195. tagName: 'a',
  196. props: {
  197. id, innerHTML,
  198. [isLink ? 'target' : 'href']: isLink ? '_blank' : 'javascript:void(0);'
  199. },
  200. styles: { margin: '0px 0.5em' },
  201. listeners: [['click', onClick]]
  202. });
  203.  
  204. const favorite_add = makeBtn('favorite-add', CONST.Text[i18n].Add, e => addFav());
  205. const favorite_remove = makeBtn('favorite-remove', CONST.Text[i18n].Remove, e => removeFav());
  206. const favorite_edit = makeBtn('favorite-edit', CONST.Text[i18n].Edit, e => addFav(), true);
  207. const favorite_iframe = makeBtn('favorite-edit-in-page', CONST.Text[i18n].EditIframe, editInPage);
  208. const favorite_copy = makeBtn('favorite-add', CONST.Text[i18n].CopySID, e => copyText(getStrSID()));
  209.  
  210. // Append to document
  211. $APD(script_favorite, favorite_groups);
  212. script_after.before(script_favorite);
  213. [favorite_add, favorite_remove, favorite_edit, favorite_iframe, favorite_copy].forEach(button => $APD(script_favorite, button));
  214.  
  215. async function refresh() {
  216. const sets = await getScriptSets();
  217. const old_value = favorite_groups.value;
  218. clearChildnodes(favorite_groups);
  219.  
  220. for (const set of sets) {
  221. // Make <option>
  222. const option = set.elmOption = $CrE('option');
  223. option.innerText = set.name;
  224. option.value = set.linkedit;
  225. $APD(favorite_groups, option);
  226. }
  227. adjustWidth();
  228.  
  229. // Recover selected <option>
  230. const selected = [...favorite_groups.children].find(option => option.value === old_value);
  231. selected && (selected.selected = true);
  232.  
  233. // Set edit-button.href
  234. favorite_edit.href = favorite_groups.value;
  235.  
  236. // Check script in-set status
  237. const inSets = await getInSets(sets, getStrSID());
  238. sets.forEach(set => {
  239. const inSet = inSets.includes(set);
  240. set.elmOption.innerText = `${CONST.Text[i18n].InSetStatus[inSet+0]} ${set.name}`;
  241. set.elmOption.inSet = inSet;
  242. });
  243. adjustWidth();
  244. refreshButtonDisplay();
  245. }
  246.  
  247. function adjustWidth() {
  248. favorite_groups.style.width = Math.max.apply(null, Array.from(favorite_groups.children).map((o) => (o.innerText.length))).toString() + 'em';
  249. favorite_groups.style.maxWidth = '40vw';
  250. }
  251.  
  252. function refreshButtonDisplay() {
  253. if (favorite_groups.selectedOptions[0].inSet === true) {
  254. favorite_add.style.setProperty('display', 'none');
  255. favorite_remove.style.removeProperty('display');
  256. } else if (favorite_groups.selectedOptions[0].inSet === false) {
  257. favorite_remove.style.setProperty('display', 'none');
  258. favorite_add.style.removeProperty('display');
  259. }
  260. }
  261.  
  262. function addFav() {
  263. const option = favorite_groups.selectedOptions[0];
  264. const set = GM_getValue('script-sets').sets.find(set => set.linkedit === option.value);
  265. const url = favorite_groups.value;
  266.  
  267. displayNotice(CONST.Text[i18n].Working[0]);
  268. modifyFav(favorite_groups.value, oDom => {
  269. const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === getStrSID());
  270. if (existingInput) {
  271. displayNotice(CONST.Text[i18n].Error.AlreadyExist);
  272. option.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;
  273. return false;
  274. }
  275.  
  276. const input = $CrE('input');
  277. input.value = getStrSID();
  278. input.name = 'scripts-included[]';
  279. input.type = 'hidden';
  280. $APD($(oDom, '#script-set-scripts'), input);
  281. displayNotice(CONST.Text[i18n].Working[1]);
  282. }, oDom => {
  283. const status = $(oDom, 'p.notice');
  284. const status_text = status ? status.innerText : CONST.Text[i18n].Error.Unknown;
  285. displayNotice(status_text);
  286. option.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;
  287. option.inSet = true;
  288. refreshButtonDisplay();
  289. }, onerror);
  290. }
  291.  
  292. function removeFav() {
  293. const option = favorite_groups.selectedOptions[0];
  294. const set = GM_getValue('script-sets').sets.find(set => set.linkedit === option.value);
  295. const url = favorite_groups.value;
  296.  
  297. displayNotice(CONST.Text[i18n].Working[0]);
  298. modifyFav(favorite_groups.value, oDom => {
  299. const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === getStrSID());
  300. if (!existingInput) {
  301. displayNotice(CONST.Text[i18n].Error.NotExist);
  302. option.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;
  303. return false;
  304. }
  305.  
  306. existingInput.remove();
  307. displayNotice(CONST.Text[i18n].Working[1]);
  308. }, oDom => {
  309. const status = $(oDom, 'p.notice');
  310. const status_text = status ? status.innerText : CONST.Text[i18n].Error.Unknown;
  311. displayNotice(status_text);
  312. option.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;
  313. option.inSet = false;
  314. refreshButtonDisplay();
  315. }, onerror);
  316. }
  317.  
  318. async function modifyFav(url, editCallback, finishCallback, onerror) {
  319. const oDom = await getDocument(url);
  320. if (editCallback(oDom) === false) { return false; }
  321.  
  322. const form = $(oDom, '.change-script-set');
  323. const data = new FormData(form);
  324. data.append('save', '1');
  325.  
  326. // Use XMLHttpRequest insteadof GM_xmlhttpRequest before Tampermonkey 5.0.0 because of FormData posting issues
  327. if (GM_info.scriptHandler === 'Tampermonkey' && !GM_hasVersion('5.0')) {
  328. const xhr = new XMLHttpRequest();
  329. xhr.open('POST', toAbsoluteURL(form.getAttribute('action')));
  330. xhr.responseType = 'blob';
  331. xhr.onload = async e => finishCallback(await parseDocument(xhr.response));
  332. xhr.onerror = onerror;
  333. xhr.send(data);
  334. } else {
  335. GM_xmlhttpRequest({
  336. method: 'POST',
  337. url: toAbsoluteURL(form.getAttribute('action')),
  338. data,
  339. responseType: 'blob',
  340. onload: async response => finishCallback(await parseDocument(response.response)),
  341. onerror
  342. });
  343. }
  344. }
  345.  
  346. function onerror() {
  347. displayNotice(CONST.Text[i18n].Error.Unknown);
  348. }
  349.  
  350. function editInPage(e) {
  351. e.preventDefault();
  352.  
  353. const _iframes = [...$All(script_favorite, '.script-edit-page')];
  354. if (_iframes.length) {
  355. // Iframe exists, close iframe
  356. favorite_iframe.innerText = CONST.Text[i18n].EditIframe;
  357. _iframes.forEach(ifr => ifr.remove());
  358. } else {
  359. // Iframe not exist, make iframe
  360. favorite_iframe.innerText = CONST.Text[i18n].CloseIframe;
  361.  
  362. const iframe = $$CrE({
  363. tagName: 'iframe',
  364. props: {
  365. src: favorite_groups.value
  366. },
  367. styles: {
  368. width: '100%',
  369. height: '60vh'
  370. },
  371. classes: ['script-edit-page'],
  372. listeners: [['load', e => {
  373. refresh();
  374. //iframe.style.height = iframe.contentDocument.body.parentElement.offsetHeight + 'px';
  375. }]]
  376. });
  377. script_favorite.appendChild(iframe);
  378. }
  379. }
  380.  
  381. function displayNotice(text) {
  382. const notice = $CrE('p');
  383. notice.classList.add('notice');
  384. notice.id = 'fav-notice';
  385. notice.innerText = text;
  386. const old_notice = $('#fav-notice');
  387. old_notice && old_notice.parentElement.removeChild(old_notice);
  388. $('#script-content').insertAdjacentElement('afterbegin', notice);
  389. }
  390. }
  391. }
  392.  
  393. async function getScriptSets() {
  394. const userpage = getUserpage();
  395. const oDom = await getDocument(userpage);
  396.  
  397. /*
  398. const user_script_sets = oDom.querySelector('#user-script-sets');
  399. const script_sets = [];
  400.  
  401. for (const li of user_script_sets.querySelectorAll('li')) {
  402. // Get fav info
  403. const name = li.childNodes[0].nodeValue.trimRight();
  404. const link = li.children[0].href;
  405. const linkedit = li.children[1] ? li.children[1].href : 'https://greatest.deepsurf.us/' + $('#language-selector-locale').value + '/users/' + $('#nav-user-info>.user-profile-link>a').href.match(/[a-zA-Z\-]+\/users\/([^\/]*)/)[1] + '/sets/' + li.children[0].href.match(/[\?&]set=(\d+)/)[1] + '/edit';
  406.  
  407. // Append to script_sets
  408. script_sets.push({
  409. name: name,
  410. link: link,
  411. linkedit: linkedit
  412. });
  413. }
  414. */
  415. const script_sets = Array.from($(oDom, 'ul#user-script-sets').children).map(li => {
  416. try {
  417. return {
  418. name: li.children[0].innerText,
  419. link: li.children[0].href,
  420. linkedit: li.children[1].href
  421. }
  422. } catch(err) {
  423. DoLog(LogLevel.Error, [li, err, li.children.length, li.children[0]?.innerHTML, li.children[1]?.innerHTML], 'error');
  424. Err(err);
  425. }
  426. });
  427.  
  428. // Save to GM_storage
  429. GM_setValue('script-sets', {
  430. sets: script_sets,
  431. time: (new Date()).getTime(),
  432. version: '0.2'
  433. });
  434.  
  435. return script_sets;
  436. }
  437.  
  438. function getUserpage() {
  439. const a = $('#nav-user-info>.user-profile-link>a');
  440. return a ? a.href : null;
  441. }
  442.  
  443. async function getInSet(set, sid) {
  444. sid = sid.toString();
  445. const oDom = await getDocument(set.linkedit);
  446. const inSet = [...$(oDom, '#script-set-scripts').children].some(input => input.value === sid);
  447. return inSet;
  448. }
  449.  
  450. async function getInSets(sets, sid) {
  451. const inSets = await Promise.all(sets.map(set => getInSet(set, sid)));
  452. return sets.filter((set, i) => inSets[i]);
  453. /*
  454. const AM = new AsyncManager();
  455. const inSets = [];
  456. for (const set of sets) {
  457. AM.add();
  458. getInSet(set, sid, inSet => {
  459. inSet && inSets.push(set);
  460. AM.finish();
  461. });
  462. }
  463. AM.onfinish = e => {
  464. callback(inSets);
  465. };
  466. AM.finishEvent = true;
  467. */
  468. }
  469.  
  470. function getStrSID(url=location.href) {
  471. const API = getAPI(url);
  472. const strSID = API[2].match(/\d+/)[0];
  473. return strSID;
  474. }
  475.  
  476. function getSID(url=location.href) {
  477. return Number(getStrSID(url));
  478. }
  479.  
  480. function GM_hasVersion(version) {
  481. return hasVersion(GM_info?.version || '0', version);
  482.  
  483. function hasVersion(ver1, ver2) {
  484. return compareVersions(ver1.toString(), ver2.toString()) >= 0;
  485.  
  486. // https://greatest.deepsurf.us/app/javascript/versioncheck.js
  487. // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version/format
  488. function compareVersions(a, b) {
  489. if (a == b) {
  490. return 0;
  491. }
  492. let aParts = a.split('.');
  493. let bParts = b.split('.');
  494. for (let i = 0; i < aParts.length; i++) {
  495. let result = compareVersionPart(aParts[i], bParts[i]);
  496. if (result != 0) {
  497. return result;
  498. }
  499. }
  500. // If all of a's parts are the same as b's parts, but b has additional parts, b is greater.
  501. if (bParts.length > aParts.length) {
  502. return -1;
  503. }
  504. return 0;
  505. }
  506.  
  507. function compareVersionPart(partA, partB) {
  508. let partAParts = parseVersionPart(partA);
  509. let partBParts = parseVersionPart(partB);
  510. for (let i = 0; i < partAParts.length; i++) {
  511. // "A string-part that exists is always less than a string-part that doesn't exist"
  512. if (partAParts[i].length > 0 && partBParts[i].length == 0) {
  513. return -1;
  514. }
  515. if (partAParts[i].length == 0 && partBParts[i].length > 0) {
  516. return 1;
  517. }
  518. if (partAParts[i] > partBParts[i]) {
  519. return 1;
  520. }
  521. if (partAParts[i] < partBParts[i]) {
  522. return -1;
  523. }
  524. }
  525. return 0;
  526. }
  527.  
  528. // It goes number, string, number, string. If it doesn't exist, then
  529. // 0 for numbers, empty string for strings.
  530. function parseVersionPart(part) {
  531. if (!part) {
  532. return [0, "", 0, ""];
  533. }
  534. let partParts = /([0-9]*)([^0-9]*)([0-9]*)([^0-9]*)/.exec(part)
  535. return [
  536. partParts[1] ? parseInt(partParts[1]) : 0,
  537. partParts[2],
  538. partParts[3] ? parseInt(partParts[3]) : 0,
  539. partParts[4]
  540. ];
  541. }
  542. }
  543. }
  544.  
  545. // Basic functions
  546. function $APD(a,b) {return a.appendChild(b);}
  547.  
  548. // Remove all childnodes from an element
  549. function clearChildnodes(element) {
  550. const cns = []
  551. for (const cn of element.childNodes) {
  552. cns.push(cn);
  553. }
  554. for (const cn of cns) {
  555. element.removeChild(cn);
  556. }
  557. }
  558.  
  559. function getDocumentXHR(url) {
  560. return new Promise((resolve, reject) => {
  561. const xhr = new XMLHttpRequest();
  562. xhr.open('GET', url);
  563. xhr.responseType = 'blob';
  564. xhr.onload = e => {
  565. const htmlblob = xhr.response;
  566. parseDocument(htmlblob).then(resolve).catch(reject);
  567. };
  568. xhr.send();
  569. });
  570. }
  571.  
  572. // Download and parse a url page into a html document(dom).
  573. // Returns a promise fulfills with dom
  574. function getDocument(url, retry=5) {
  575. return new Promise((resolve, reject) => {
  576. GM_xmlhttpRequest({
  577. method : 'GET',
  578. url : url,
  579. responseType : 'blob',
  580. onload : function(response) {
  581. if (response.status === 200) {
  582. const htmlblob = response.response;
  583. parseDocument(htmlblob).then(resolve).catch(reject);
  584. } else {
  585. re(response);
  586. }
  587. },
  588. onerror: err => re(err)
  589. });
  590.  
  591. function re(err) {
  592. DoLog(`Get document failed, retrying: (${retry}) ${url}`);
  593. --retry > 0 ? getDocument(url, retry).then(resolve).catch(reject) : reject(err);
  594. }
  595. });
  596. }
  597.  
  598. // Returns a promise fulfills with dom
  599. function parseDocument(htmlblob) {
  600. return new Promise((resolve, reject) => {
  601. const reader = new FileReader();
  602. reader.onload = function(e) {
  603. const htmlText = reader.result;
  604. const dom = new DOMParser().parseFromString(htmlText, 'text/html');
  605. resolve(dom);
  606. }
  607. reader.onerror = err => reject(err);
  608. reader.readAsText(htmlblob, document.characterSet);
  609. });
  610. }
  611.  
  612. // Copy text to clipboard (needs to be called in an user event)
  613. function copyText(text) {
  614. // Create a new textarea for copying
  615. const newInput = document.createElement('textarea');
  616. document.body.appendChild(newInput);
  617. newInput.value = text;
  618. newInput.select();
  619. document.execCommand('copy');
  620. document.body.removeChild(newInput);
  621. }
  622.  
  623. // get '/' splited API array from a url
  624. function getAPI(url=location.href) {
  625. return url.replace(/https?:\/\/(.*?\.){1,2}.*?\//, '').replace(/\?.*/, '').match(/[^\/]+?(?=(\/|$))/g);
  626. }
  627.  
  628. // get host part from a url(includes '^https://', '/$')
  629. function getHost(url=location.href) {
  630. const match = location.href.match(/https?:\/\/[^\/]+\//);
  631. return match ? match[0] : match;
  632. }
  633.  
  634. function toAbsoluteURL(relativeURL, base=`${location.protocol}//${location.host}/`) {
  635. return new URL(relativeURL, base).href;
  636. }
  637.  
  638. function randint(min, max) {
  639. return Math.floor(Math.random() * (max - min + 1)) + min;
  640. }
  641. })();