JSON formatter

Format JSON data in a beautiful way.

目前为 2020-12-21 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name JSON formatter
  3. // @namespace http://gerald.top
  4. // @author Gerald <i@gerald.top>
  5. // @icon http://cn.gravatar.com/avatar/a0ad718d86d21262ccd6ff271ece08a3?s=80
  6. // @description Format JSON data in a beautiful way.
  7. // @description:zh-CN 更加漂亮地显示JSON数据。
  8. // @version 2.0.9
  9. // @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@1
  10. // @match *://*/*
  11. // @match file:///*
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_addStyle
  15. // @grant GM_registerMenuCommand
  16. // @grant GM_setClipboard
  17. // ==/UserScript==
  18.  
  19. (function () {
  20. 'use strict';
  21.  
  22. var css_248z = "/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:initial;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:initial}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:initial}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:initial;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::placeholder,textarea::placeholder{color:#a0aec0}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}body{font-family:Menlo,Microsoft YaHei,Tahoma;background:#343434}#json-formatter{position:relative;height:100vh;display:flex;flex-direction:column;padding:.5rem;font-size:.875rem}#json-formatter>pre{flex:1 1 0%;min-height:0;overflow:auto;padding-left:1rem;padding-right:1rem;white-space:pre-wrap}#json-formatter>pre:not(.show-commas) .comma,#json-formatter>pre:not(.show-quotes) .quote{display:none}.quote{color:#878787}.color{display:inline-block;width:.75rem;height:.75rem;margin-left:.25rem;margin-right:.25rem;border-width:1px;--border-opacity:1;border-color:#cbd5e0;border-color:rgba(203,213,224,var(--border-opacity))}.item{cursor:pointer}.content{padding-left:1rem}.collapse>span>.content{padding-left:0;display:inline}.collapse>span>.content>*{display:none}.collapse>span>.content:before{content:\"...\"}.complex{position:relative}.complex:before{position:absolute;width:.25rem;border-left-width:1px;border-bottom-width:1px;--border-opacity:1;border-color:#cbd5e0;border-color:rgba(203,213,224,var(--border-opacity));content:\"\";top:1.5em;left:-.5em;bottom:.7em;margin-left:-1px}.complex.collapse:before{display:none}.folder{position:absolute;--text-opacity:1;color:#a0aec0;color:rgba(160,174,192,var(--text-opacity));top:0;width:.5rem;text-align:center;transition-property:transform;transition-duration:.3s;--transform-rotate:90deg;cursor:pointer;left:-1em}.collapse>.folder{--transform-rotate:0}.summary{margin-left:1rem;--text-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--text-opacity))}:not(.collapse)>.summary{display:none}.tips{position:absolute;border-radius:.25rem;padding:.5rem;box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06);--bg-opacity:1;background-color:#4a5568;background-color:rgba(74,85,104,var(--bg-opacity));z-index:10;white-space:nowrap;--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.tips-key{font-weight:600}.tips-val{--text-opacity:1;color:#d69e2e;color:rgba(214,158,46,var(--text-opacity))}.tips-link{--text-opacity:1;color:#f56565;color:rgba(245,101,101,var(--text-opacity));cursor:pointer}.tips-link:hover{--text-opacity:1;color:#fc8181;color:rgba(252,129,129,var(--text-opacity))}.menu{padding:.5rem;text-align:right;--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity));user-select:none}.menu>span{display:inline-block;padding:.25rem .5rem;margin-right:.25rem;border-radius:.25rem;border-width:1px;cursor:pointer}.menu>span.toggle.active,.menu>span:not(.toggle){--bg-opacity:1;background-color:#4a5568;background-color:rgba(74,85,104,var(--bg-opacity))}";
  23.  
  24. var css_248z$1 = ".cm-s-material-darker.CodeMirror{background-color:#212121;color:#eff}.cm-s-material-darker .CodeMirror-gutters{background:#212121;color:#545454;border:none}.cm-s-material-darker .CodeMirror-guttermarker,.cm-s-material-darker .CodeMirror-guttermarker-subtle,.cm-s-material-darker .CodeMirror-linenumber{color:#545454}.cm-s-material-darker .CodeMirror-cursor{border-left:1px solid #fc0}.cm-s-material-darker.CodeMirror-focused div.CodeMirror-selected,.cm-s-material-darker div.CodeMirror-selected{background:rgba(97,97,97,.2)}.cm-s-material-darker .CodeMirror-line::selection,.cm-s-material-darker .CodeMirror-line>span::selection,.cm-s-material-darker .CodeMirror-line>span>span::selection{background:rgba(128,203,196,.2)}.cm-s-material-darker .CodeMirror-line::-moz-selection,.cm-s-material-darker .CodeMirror-line>span::-moz-selection,.cm-s-material-darker .CodeMirror-line>span>span::-moz-selection{background:rgba(128,203,196,.2)}.cm-s-material-darker .CodeMirror-activeline-background{background:rgba(0,0,0,.5)}.cm-s-material-darker .cm-keyword{color:#c792ea}.cm-s-material-darker .cm-operator{color:#89ddff}.cm-s-material-darker .cm-variable-2{color:#eff}.cm-s-material-darker .cm-type,.cm-s-material-darker .cm-variable-3{color:#f07178}.cm-s-material-darker .cm-builtin{color:#ffcb6b}.cm-s-material-darker .cm-atom{color:#f78c6c}.cm-s-material-darker .cm-number{color:#ff5370}.cm-s-material-darker .cm-def{color:#82aaff}.cm-s-material-darker .cm-string{color:#c3e88d}.cm-s-material-darker .cm-string-2{color:#f07178}.cm-s-material-darker .cm-comment{color:#545454}.cm-s-material-darker .cm-variable{color:#f07178}.cm-s-material-darker .cm-tag{color:#ff5370}.cm-s-material-darker .cm-meta{color:#ffcb6b}.cm-s-material-darker .cm-attribute,.cm-s-material-darker .cm-property{color:#c792ea}.cm-s-material-darker .cm-qualifier,.cm-s-material-darker .cm-type,.cm-s-material-darker .cm-variable-3{color:#decb6b}.cm-s-material-darker .cm-error{color:#fff;background-color:#ff5370}.cm-s-material-darker .CodeMirror-matchingbracket{text-decoration:underline;color:#fff!important}";
  25.  
  26. const React = VM;
  27. const gap = 5;
  28. const formatter = {
  29. options: [{
  30. key: 'show-quotes',
  31. title: '"',
  32. def: true
  33. }, {
  34. key: 'show-commas',
  35. title: ',',
  36. def: true
  37. }]
  38. };
  39. const classMap = {
  40. boolean: 'cm-atom',
  41. null: 'cm-atom',
  42. number: 'cm-number',
  43. string: 'cm-string'
  44. };
  45. const config = { ...formatter.options.reduce((res, item) => {
  46. res[item.key] = item.def;
  47. return res;
  48. }, {}),
  49. ...GM_getValue('config')
  50. };
  51. if (testRules([// text/javascript - file:///foo/bar.js
  52. /^(?:text|application)\/(?:.*?\+)?(?:plain|json|javascript)$/], document.contentType)) formatJSON();
  53. GM_registerMenuCommand('Toggle JSON format', formatJSON);
  54.  
  55. function testRules(rules, contentType) {
  56. for (const rule of rules) {
  57. if (typeof rule === 'string') {
  58. if (rule === contentType) return true;
  59. } else if (typeof rule?.test === 'function') {
  60. if (rule.test(contentType)) return true;
  61. }
  62. }
  63.  
  64. return false;
  65. }
  66.  
  67. function createQuote() {
  68. return /*#__PURE__*/React.createElement("span", {
  69. className: "subtle quote"
  70. }, "\"");
  71. }
  72.  
  73. function createComma() {
  74. return /*#__PURE__*/React.createElement("span", {
  75. className: "subtle comma"
  76. }, ",");
  77. }
  78.  
  79. function isColor(str) {
  80. return /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(str);
  81. }
  82.  
  83. function tokenize(raw) {
  84. const skipWhitespace = index => {
  85. while (index < raw.length && ' \t\r\n'.includes(raw[index])) index += 1;
  86.  
  87. return index;
  88. };
  89.  
  90. const expectIndex = index => {
  91. if (index < raw.length) return index;
  92. throw new Error('Unexpected end of input');
  93. };
  94.  
  95. const expectChar = (index, white, black) => {
  96. const ch = raw[index];
  97.  
  98. if (white && !white.includes(ch) || black && black.includes(ch)) {
  99. throw new Error(`Unexpected token "${ch}" at ${index}`);
  100. }
  101.  
  102. return ch;
  103. };
  104.  
  105. const findWord = (index, words) => {
  106. for (const word of words) {
  107. if (raw.slice(index, index + word.length) === word) {
  108. return word;
  109. }
  110. }
  111. };
  112.  
  113. const expectSpaceAndCharIndex = (index, white, black) => {
  114. const i = expectIndex(skipWhitespace(index));
  115. expectChar(i, white, black);
  116. return i;
  117. };
  118.  
  119. const parseString = start => {
  120. let j;
  121.  
  122. for (j = start + 1; true; j = expectIndex(j + 1)) {
  123. const ch = raw[j];
  124. if (ch === '"') break;
  125.  
  126. if (ch === '\\') {
  127. j = expectIndex(j + 1);
  128. const ch2 = raw[j];
  129.  
  130. if (ch2 === 'x') {
  131. j = expectIndex(j + 2);
  132. } else if (ch2 === 'u') {
  133. j = expectIndex(j + 4);
  134. }
  135. }
  136. }
  137.  
  138. const source = raw.slice(start + 1, j);
  139. return {
  140. type: 'string',
  141. source,
  142. data: JSON.parse(raw.slice(start, j + 1)),
  143. color: isColor(source),
  144. start,
  145. end: j + 1
  146. };
  147. };
  148.  
  149. const parseKeyword = start => {
  150. const nullWord = findWord(start, ['null']);
  151.  
  152. if (nullWord) {
  153. return {
  154. type: 'null',
  155. source: 'null',
  156. data: null,
  157. start,
  158. end: start + 4
  159. };
  160. }
  161.  
  162. const bool = findWord(start, ['true', 'false']);
  163.  
  164. if (bool) {
  165. return {
  166. type: 'boolean',
  167. source: bool,
  168. data: bool === 'true',
  169. start,
  170. end: start + bool.length
  171. };
  172. }
  173.  
  174. expectChar(start, '0');
  175. };
  176.  
  177. const DIGITS = '0123456789';
  178.  
  179. const findDecimal = (start, fractional) => {
  180. let i = start;
  181. if ('+-'.includes(raw[i])) i += 1;
  182. let j;
  183. let dot = -1;
  184.  
  185. for (j = i; true; j = expectIndex(j + 1)) {
  186. const ch = expectChar(j, // there must be at least one digit
  187. // dot must not be the last character of a number, expecting a digit
  188. j === i || dot >= 0 && dot === j - 1 ? DIGITS : null, // there can be at most one dot
  189. !fractional || dot >= 0 ? '.' : null);
  190. if (ch === '.') dot = j;else if (!DIGITS.includes(ch)) break;
  191. }
  192.  
  193. return j;
  194. };
  195.  
  196. const parseNumber = start => {
  197. let i = findDecimal(start, true);
  198. const ch = raw[i];
  199.  
  200. if (ch && ch.toLowerCase() === 'e') {
  201. i = findDecimal(i + 1);
  202. }
  203.  
  204. const source = raw.slice(start, i);
  205. return {
  206. type: 'number',
  207. source,
  208. data: +source,
  209. start,
  210. end: i
  211. };
  212. };
  213.  
  214. let parseItem;
  215.  
  216. const parseArray = start => {
  217. const result = {
  218. type: 'array',
  219. data: [],
  220. start
  221. };
  222. let i = start + 1;
  223.  
  224. while (true) {
  225. i = expectIndex(skipWhitespace(i));
  226. if (raw[i] === ']') break;
  227. if (result.data.length) i = expectSpaceAndCharIndex(i, ',') + 1;
  228. const item = parseItem(i);
  229. result.data.push(item);
  230. i = item.end;
  231. }
  232.  
  233. result.end = i + 1;
  234. return result;
  235. };
  236.  
  237. const parseObject = start => {
  238. const result = {
  239. type: 'object',
  240. data: [],
  241. start
  242. };
  243. let i = start + 1;
  244.  
  245. while (true) {
  246. i = expectIndex(skipWhitespace(i));
  247. if (raw[i] === '}') break;
  248. if (result.data.length) i = expectSpaceAndCharIndex(i, ',') + 1;
  249. i = expectSpaceAndCharIndex(i, '"');
  250. const key = parseString(i);
  251. i = expectSpaceAndCharIndex(key.end, ':') + 1;
  252. const value = parseItem(i);
  253. result.data.push({
  254. key,
  255. value
  256. });
  257. i = value.end;
  258. }
  259.  
  260. result.end = i + 1;
  261. return result;
  262. };
  263.  
  264. parseItem = start => {
  265. const i = expectIndex(skipWhitespace(start));
  266. const ch = raw[i];
  267. if (ch === '"') return parseString(i);
  268. if (ch === '[') return parseArray(i);
  269. if (ch === '{') return parseObject(i);
  270. if ('-0123456789'.includes(ch)) return parseNumber(i);
  271. return parseKeyword(i);
  272. };
  273.  
  274. const result = parseItem(0);
  275. const end = skipWhitespace(result.end);
  276. if (end < raw.length) expectChar(end, []);
  277. return result;
  278. }
  279.  
  280. function loadJSON() {
  281. const raw = document.body.innerText;
  282.  
  283. try {
  284. // JSON
  285. const content = tokenize(raw);
  286. return {
  287. raw,
  288. content
  289. };
  290. } catch (e) {
  291. // not JSON
  292. console.error('Not JSON', e);
  293. }
  294.  
  295. try {
  296. // JSONP
  297. const parts = raw.match(/^(.*?\w\s*\()(.+)(\)[;\s]*)$/);
  298. const content = tokenize(parts[2]);
  299. return {
  300. raw,
  301. content,
  302. prefix: /*#__PURE__*/React.createElement("span", {
  303. className: "subtle"
  304. }, parts[1].trim()),
  305. suffix: /*#__PURE__*/React.createElement("span", {
  306. className: "subtle"
  307. }, parts[3].trim())
  308. };
  309. } catch (e) {
  310. // not JSONP
  311. console.error('Not JSONP', e);
  312. }
  313. }
  314.  
  315. function formatJSON() {
  316. if (formatter.formatted) return;
  317. formatter.formatted = true;
  318. formatter.data = loadJSON();
  319. if (!formatter.data) return;
  320. document.head.innerHTML = '';
  321. document.body.innerHTML = '';
  322. formatter.style = GM_addStyle(css_248z + css_248z$1);
  323. formatter.root = /*#__PURE__*/React.createElement("div", {
  324. id: "json-formatter"
  325. });
  326. document.body.append(formatter.root);
  327. initTips();
  328. initMenu();
  329. bindEvents();
  330. generateNodes(formatter.data, formatter.root);
  331. }
  332.  
  333. function generateNodes(data, container) {
  334. const rootSpan = /*#__PURE__*/React.createElement("span", null);
  335. const root = /*#__PURE__*/React.createElement("div", null, rootSpan);
  336. const pre = /*#__PURE__*/React.createElement("pre", {
  337. className: "CodeMirror cm-s-material-darker"
  338. }, root);
  339. formatter.pre = pre;
  340. const queue = [{
  341. el: rootSpan,
  342. elBlock: root,
  343. ...data
  344. }];
  345.  
  346. while (queue.length) {
  347. const item = queue.shift();
  348. const {
  349. el,
  350. content,
  351. prefix,
  352. suffix
  353. } = item;
  354. if (prefix) el.append(prefix);
  355.  
  356. if (content.type === 'array') {
  357. queue.push(...generateArray(item));
  358. } else if (content.type === 'object') {
  359. queue.push(...generateObject(item));
  360. } else {
  361. const {
  362. type,
  363. color
  364. } = content;
  365. const children = [];
  366. if (type === 'string') children.push(createQuote());
  367. if (color) children.push( /*#__PURE__*/React.createElement("span", {
  368. className: "color",
  369. style: `background-color: ${content.data}`
  370. }));
  371. children.push(toString(content));
  372. if (type === 'string') children.push(createQuote());
  373. const className = [classMap[type], 'item'].filter(Boolean).join(' ');
  374. el.append( /*#__PURE__*/React.createElement("span", {
  375. className: className,
  376. "data-type": type,
  377. "data-value": content.data
  378. }, children));
  379. }
  380.  
  381. if (suffix) el.append(suffix);
  382. }
  383.  
  384. container.append(pre);
  385. updateView();
  386. }
  387.  
  388. function toString(content) {
  389. return `${content.source}`;
  390. }
  391.  
  392. function setFolder(el, length) {
  393. if (length) {
  394. el.classList.add('complex');
  395. el.append( /*#__PURE__*/React.createElement("div", {
  396. className: "folder"
  397. }, '\u25b8'), /*#__PURE__*/React.createElement("span", {
  398. className: "summary"
  399. }, `// ${length} items`));
  400. }
  401. }
  402.  
  403. function generateArray({
  404. el,
  405. elBlock,
  406. content
  407. }) {
  408. const elContent = content.data.length && /*#__PURE__*/React.createElement("div", {
  409. className: "content"
  410. });
  411. setFolder(elBlock, content.data.length);
  412. el.append( /*#__PURE__*/React.createElement("span", {
  413. className: "bracket"
  414. }, "["), elContent || ' ', /*#__PURE__*/React.createElement("span", {
  415. className: "bracket"
  416. }, "]"));
  417. return content.data.map((item, i) => {
  418. const elValue = /*#__PURE__*/React.createElement("span", null);
  419. const elChild = /*#__PURE__*/React.createElement("div", null, elValue);
  420. elContent.append(elChild);
  421. if (i < content.data.length - 1) elChild.append(createComma());
  422. return {
  423. el: elValue,
  424. elBlock: elChild,
  425. content: item
  426. };
  427. });
  428. }
  429.  
  430. function generateObject({
  431. el,
  432. elBlock,
  433. content
  434. }) {
  435. const elContent = content.data.length && /*#__PURE__*/React.createElement("div", {
  436. className: "content"
  437. });
  438. setFolder(elBlock, content.data.length);
  439. el.append( /*#__PURE__*/React.createElement("span", {
  440. className: "bracket"
  441. }, '{'), elContent || ' ', /*#__PURE__*/React.createElement("span", {
  442. className: "bracket"
  443. }, '}'));
  444. return content.data.map(({
  445. key,
  446. value
  447. }, i) => {
  448. const elValue = /*#__PURE__*/React.createElement("span", null);
  449. const elChild = /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
  450. className: "cm-property item",
  451. "data-type": key.type
  452. }, createQuote(), key.data, createQuote()), ': ', elValue);
  453. if (i < content.data.length - 1) elChild.append(createComma());
  454. elContent.append(elChild);
  455. return {
  456. el: elValue,
  457. content: value,
  458. elBlock: elChild
  459. };
  460. });
  461. }
  462.  
  463. function updateView() {
  464. formatter.options.forEach(({
  465. key
  466. }) => {
  467. formatter.pre.classList[config[key] ? 'add' : 'remove'](key);
  468. });
  469. }
  470.  
  471. function removeEl(el) {
  472. el.remove();
  473. }
  474.  
  475. function initMenu() {
  476. const handleCopy = () => {
  477. GM_setClipboard(formatter.data.raw);
  478. };
  479.  
  480. const handleMenuClick = e => {
  481. const el = e.target;
  482. const {
  483. key
  484. } = el.dataset;
  485.  
  486. if (key) {
  487. config[key] = !config[key];
  488. GM_setValue('config', config);
  489. el.classList.toggle('active');
  490. updateView();
  491. }
  492. };
  493.  
  494. formatter.root.append( /*#__PURE__*/React.createElement("div", {
  495. className: "menu",
  496. onClick: handleMenuClick
  497. }, /*#__PURE__*/React.createElement("span", {
  498. onClick: handleCopy
  499. }, "Copy"), formatter.options.map(item => /*#__PURE__*/React.createElement("span", {
  500. className: `toggle${config[item.key] ? ' active' : ''}`,
  501. dangerouslySetInnerHTML: {
  502. __html: item.title
  503. },
  504. "data-key": item.key
  505. }))));
  506. }
  507.  
  508. function initTips() {
  509. const tips = /*#__PURE__*/React.createElement("div", {
  510. className: "tips",
  511. onClick: e => {
  512. e.stopPropagation();
  513. }
  514. });
  515.  
  516. const hide = () => removeEl(tips);
  517.  
  518. document.addEventListener('click', hide, false);
  519. formatter.tips = {
  520. node: tips,
  521. hide,
  522.  
  523. show(range) {
  524. const {
  525. scrollTop
  526. } = document.body;
  527. const rects = range.getClientRects();
  528. let rect;
  529.  
  530. if (rects[0].top < 100) {
  531. rect = rects[rects.length - 1];
  532. tips.style.top = `${rect.bottom + scrollTop + gap}px`;
  533. tips.style.bottom = '';
  534. } else {
  535. [rect] = rects;
  536. tips.style.top = '';
  537. tips.style.bottom = `${formatter.root.offsetHeight - rect.top - scrollTop + gap}px`;
  538. }
  539.  
  540. tips.style.left = `${rect.left}px`;
  541. const {
  542. type,
  543. value
  544. } = range.startContainer.dataset;
  545. tips.innerHTML = '';
  546. tips.append( /*#__PURE__*/React.createElement("span", {
  547. className: "tips-key"
  548. }, "type"), ': ', /*#__PURE__*/React.createElement("span", {
  549. className: "tips-val",
  550. dangerouslySetInnerHTML: {
  551. __html: type
  552. }
  553. }));
  554.  
  555. if (type === 'string') {
  556. const handleCopyParsed = () => {
  557. GM_setClipboard(value);
  558. };
  559.  
  560. tips.append( /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement("span", {
  561. className: "tips-link",
  562. onClick: handleCopyParsed
  563. }, "Copy parsed"));
  564.  
  565. if (/^(https?|ftps?):\/\/\S+/.test(value)) {
  566. tips.append( /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement("a", {
  567. className: "tips-link",
  568. href: value,
  569. target: "_blank",
  570. rel: "noopener noreferrer"
  571. }, "Open link"));
  572. }
  573. }
  574.  
  575. formatter.root.append(tips);
  576. }
  577.  
  578. };
  579. }
  580.  
  581. function selectNode(node) {
  582. const selection = window.getSelection();
  583. selection.removeAllRanges();
  584. const range = document.createRange();
  585. range.setStartBefore(node.firstChild);
  586. range.setEndAfter(node.lastChild);
  587. selection.addRange(range);
  588. return range;
  589. }
  590.  
  591. function bindEvents() {
  592. formatter.root.addEventListener('click', e => {
  593. e.stopPropagation();
  594. const {
  595. target
  596. } = e;
  597.  
  598. if (target.classList.contains('item')) {
  599. formatter.tips.show(selectNode(target));
  600. } else {
  601. formatter.tips.hide();
  602. }
  603.  
  604. if (target.classList.contains('folder')) {
  605. target.parentNode.classList.toggle('collapse');
  606. }
  607. }, false);
  608. }
  609.  
  610. }());