format-lib.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971
  1. /**
  2. * 日期格式化
  3. * @param {Object} pattern
  4. */
  5. Date.prototype.format = function (pattern) {
  6. let pad = function (source, length) {
  7. let pre = "",
  8. negative = (source < 0),
  9. string = String(Math.abs(source));
  10. if (string.length < length) {
  11. pre = (new Array(length - string.length + 1)).join('0');
  12. }
  13. return (negative ? "-" : "") + pre + string;
  14. };
  15. if ('string' !== typeof pattern) {
  16. return this.toString();
  17. }
  18. let replacer = function (patternPart, result) {
  19. pattern = pattern.replace(patternPart, result);
  20. };
  21. let year = this.getFullYear(),
  22. month = this.getMonth() + 1,
  23. date2 = this.getDate(),
  24. hours = this.getHours(),
  25. minutes = this.getMinutes(),
  26. seconds = this.getSeconds(),
  27. milliSec = this.getMilliseconds();
  28. replacer(/yyyy/g, pad(year, 4));
  29. replacer(/yy/g, pad(parseInt(year.toString().slice(2), 10), 2));
  30. replacer(/MM/g, pad(month, 2));
  31. replacer(/M/g, month);
  32. replacer(/dd/g, pad(date2, 2));
  33. replacer(/d/g, date2);
  34. replacer(/HH/g, pad(hours, 2));
  35. replacer(/H/g, hours);
  36. replacer(/hh/g, pad(hours % 12, 2));
  37. replacer(/h/g, hours % 12);
  38. replacer(/mm/g, pad(minutes, 2));
  39. replacer(/m/g, minutes);
  40. replacer(/ss/g, pad(seconds, 2));
  41. replacer(/s/g, seconds);
  42. replacer(/SSS/g, pad(milliSec, 3));
  43. replacer(/S/g, milliSec);
  44. return pattern;
  45. };
  46. /**
  47. * 自动消失的Alert弹窗
  48. * @param content
  49. */
  50. window.toast = function (content) {
  51. window.clearTimeout(window.feHelperAlertMsgTid);
  52. let elAlertMsg = document.querySelector("#fehelper_alertmsg");
  53. if (!elAlertMsg) {
  54. let elWrapper = document.createElement('div');
  55. elWrapper.innerHTML = '<div id="fehelper_alertmsg" style="position:fixed;bottom:25px;left:5px;z-index:1000000">' +
  56. '<p style="background:#000;display:inline-block;color:#fff;text-align:center;' +
  57. 'padding:10px 10px;margin:0 auto;font-size:14px;border-radius:4px;">' + content + '</p></div>';
  58. elAlertMsg = elWrapper.childNodes[0];
  59. document.body.appendChild(elAlertMsg);
  60. } else {
  61. elAlertMsg.querySelector('p').innerHTML = content;
  62. elAlertMsg.style.display = 'block';
  63. }
  64. window.feHelperAlertMsgTid = window.setTimeout(function () {
  65. elAlertMsg.style.display = 'none';
  66. }, 1000);
  67. };
  68. /**
  69. * FeHelper Json Format Lib,入口文件
  70. * @example
  71. * Formatter.format(jsonString)
  72. */
  73. window.Formatter = (function () {
  74. "use strict";
  75. let jfContent,
  76. jfPre,
  77. jfStyleEl,
  78. jfStatusBar,
  79. formattingMsg;
  80. let lastItemIdGiven = 0;
  81. let cachedJsonString = '';
  82. // 单例Worker实例
  83. let workerInstance = null;
  84. let _initElements = function () {
  85. jfContent = $('#jfContent');
  86. if (!jfContent[0]) {
  87. jfContent = $('<div id="jfContent" />').appendTo('body');
  88. }
  89. jfPre = $('#jfContent_pre');
  90. if (!jfPre[0]) {
  91. jfPre = $('<pre id="jfContent_pre" />').appendTo('body');
  92. }
  93. jfStyleEl = $('#jfStyleEl');
  94. if (!jfStyleEl[0]) {
  95. jfStyleEl = $('<style id="jfStyleEl" />').appendTo('head');
  96. }
  97. formattingMsg = $('#formattingMsg');
  98. if (!formattingMsg[0]) {
  99. formattingMsg = $('<div id="formattingMsg"><span class="x-loading"></span>格式化中...</div>').appendTo('body');
  100. }
  101. try {
  102. jfContent.html('').show();
  103. jfPre.html('').hide();
  104. jfStatusBar && jfStatusBar.hide();
  105. formattingMsg.hide();
  106. } catch (e) {
  107. }
  108. };
  109. /**
  110. * HTML特殊字符格式化
  111. * @param str
  112. * @returns {*}
  113. */
  114. let htmlspecialchars = function (str) {
  115. str = str.replace(/&/g, '&amp;');
  116. str = str.replace(/</g, '&lt;');
  117. str = str.replace(/>/g, '&gt;');
  118. str = str.replace(/"/g, '&quot;');
  119. str = str.replace(/'/g, '&#039;');
  120. str = str.replace(/\\/g, '&#92;');
  121. return str;
  122. };
  123. /**
  124. * 直接下载,能解决中文乱码
  125. * @param content
  126. * @private
  127. */
  128. let _downloadSupport = function (content) {
  129. // 下载链接
  130. let dt = (new Date()).format('yyyyMMddHHmmss');
  131. let blob = new Blob([content], {type: 'application/octet-stream'});
  132. let button = $('<button class="xjf-btn xjf-btn-right">下载JSON</button>').appendTo('#optionBar');
  133. if (typeof chrome === 'undefined' || !chrome.permissions) {
  134. button.click(function (e) {
  135. let aLink = $('#aLinkDownload');
  136. if (!aLink[0]) {
  137. aLink = $('<a id="aLinkDownload" target="_blank" title="保存到本地">下载JSON数据</a>').appendTo('body');
  138. aLink.attr('download', 'FeHelper-' + dt + '.json');
  139. aLink.attr('href', URL.createObjectURL(blob));
  140. }
  141. aLink[0].click();
  142. });
  143. } else {
  144. button.click(function (e) {
  145. // 请求权限
  146. chrome.permissions.request({
  147. permissions: ['downloads']
  148. }, (granted) => {
  149. if (granted) {
  150. chrome.downloads.download({
  151. url: URL.createObjectURL(blob),
  152. saveAs: true,
  153. conflictAction: 'overwrite',
  154. filename: 'FeHelper-' + dt + '.json'
  155. });
  156. } else {
  157. toast('必须接受授权,才能正常下载!');
  158. }
  159. });
  160. });
  161. }
  162. };
  163. /**
  164. * chrome 下复制到剪贴板
  165. * @param text
  166. */
  167. let _copyToClipboard = function (text) {
  168. let input = document.createElement('textarea');
  169. input.style.position = 'fixed';
  170. input.style.opacity = 0;
  171. input.value = text;
  172. document.body.appendChild(input);
  173. input.select();
  174. document.execCommand('Copy');
  175. document.body.removeChild(input);
  176. toast('Json片段复制成功,随处粘贴可用!')
  177. };
  178. /**
  179. * 从el中获取json文本
  180. * @param el
  181. * @returns {string}
  182. */
  183. let getJsonText = function (el) {
  184. let txt = el.text().replace(/复制\|下载\|删除/gm,'').replace(/":\s/gm, '":').replace(/,$/, '').trim();
  185. if (!(/^{/.test(txt) && /\}$/.test(txt)) && !(/^\[/.test(txt) && /\]$/.test(txt))) {
  186. txt = '{' + txt + '}';
  187. }
  188. try {
  189. txt = JSON.stringify(JSON.parse(txt), null, 4);
  190. } catch (err) {
  191. }
  192. return txt;
  193. };
  194. // 添加json路径
  195. let _showJsonPath = function (curEl) {
  196. let keys = [];
  197. let current = curEl;
  198. // 处理当前节点
  199. if (current.hasClass('item') && !current.hasClass('rootItem')) {
  200. if (current.hasClass('item-array-element')) {
  201. // 这是数组元素,使用data-array-index属性
  202. let index = current.attr('data-array-index');
  203. if (index !== undefined) {
  204. keys.unshift('[' + index + ']');
  205. }
  206. } else {
  207. // 这是对象属性,获取key
  208. let keyText = current.find('>.key').text();
  209. if (keyText) {
  210. keys.unshift(keyText);
  211. }
  212. }
  213. }
  214. // 向上遍历所有祖先节点
  215. current.parents('.item').each(function() {
  216. let $this = $(this);
  217. // 跳过根节点
  218. if ($this.hasClass('rootItem')) {
  219. return false; // 终止遍历
  220. }
  221. if ($this.hasClass('item-array-element')) {
  222. // 这是数组元素,使用data-array-index属性
  223. let index = $this.attr('data-array-index');
  224. if (index !== undefined) {
  225. keys.unshift('[' + index + ']');
  226. }
  227. } else if ($this.hasClass('item-object') || $this.hasClass('item-array')) {
  228. // 这是容器节点,寻找它的key
  229. let $container = $this.parent().parent(); // 跳过 .kv-list
  230. if ($container.length && !$container.hasClass('rootItem')) {
  231. if ($container.hasClass('item-array-element')) {
  232. // 容器本身是数组元素
  233. let index = $container.attr('data-array-index');
  234. if (index !== undefined) {
  235. keys.unshift('[' + index + ']');
  236. }
  237. } else {
  238. // 容器是对象属性
  239. let keyText = $container.find('>.key').text();
  240. if (keyText) {
  241. keys.unshift(keyText);
  242. }
  243. }
  244. }
  245. } else {
  246. // 普通item节点,获取key
  247. let keyText = $this.find('>.key').text();
  248. if (keyText) {
  249. keys.unshift(keyText);
  250. }
  251. }
  252. });
  253. // 过滤掉空值和无效的key
  254. let validKeys = keys.filter(key => key && key.trim() !== '');
  255. // 创建或获取语言选择器和路径显示区域
  256. let jfPathContainer = $('#jsonPathContainer');
  257. if (!jfPathContainer.length) {
  258. jfPathContainer = $('<div id="jsonPathContainer"/>').prependTo(jfStatusBar);
  259. // 创建语言选择下拉框
  260. let langSelector = $('<select id="jsonPathLangSelector" title="选择编程语言格式">' +
  261. '<option value="javascript">JavaScript</option>' +
  262. '<option value="php">PHP</option>' +
  263. '<option value="python">Python</option>' +
  264. '<option value="java">Java</option>' +
  265. '<option value="csharp">C#</option>' +
  266. '<option value="golang">Go</option>' +
  267. '<option value="ruby">Ruby</option>' +
  268. '<option value="swift">Swift</option>' +
  269. '</select>').appendTo(jfPathContainer);
  270. // 创建路径显示区域
  271. let jfPath = $('<span id="jsonPath"/>').appendTo(jfPathContainer);
  272. // 绑定语言切换事件
  273. langSelector.on('change', function() {
  274. // 保存选择的语言到本地存储
  275. localStorage.setItem('fehelper_json_path_lang', $(this).val());
  276. // 从容器中获取当前保存的keys,而不是使用闭包中的validKeys
  277. let currentKeys = jfPathContainer.data('currentKeys') || [];
  278. _updateJsonPath(currentKeys, $(this).val());
  279. });
  280. // 从本地存储恢复语言选择
  281. let savedLang = localStorage.getItem('fehelper_json_path_lang') || 'javascript';
  282. langSelector.val(savedLang);
  283. }
  284. // 保存当前的keys到容器的data属性中,供语言切换时使用
  285. jfPathContainer.data('currentKeys', validKeys);
  286. // 获取当前选择的语言
  287. let selectedLang = $('#jsonPathLangSelector').val() || 'javascript';
  288. _updateJsonPath(validKeys, selectedLang);
  289. };
  290. // 根据不同编程语言格式化JSON路径
  291. let _updateJsonPath = function(keys, language) {
  292. let path = _formatJsonPath(keys, language);
  293. $('#jsonPath').html('当前节点:' + path);
  294. };
  295. // 格式化JSON路径为不同编程语言格式
  296. let _formatJsonPath = function(keys, language) {
  297. if (!keys.length) {
  298. return _getLanguageRoot(language);
  299. }
  300. let path = '';
  301. switch (language) {
  302. case 'javascript':
  303. path = '$';
  304. for (let i = 0; i < keys.length; i++) {
  305. let key = keys[i];
  306. if (key.startsWith('[') && key.endsWith(']')) {
  307. // 数组索引
  308. path += key;
  309. } else {
  310. // 对象属性
  311. if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) {
  312. // 有效的标识符,使用点语法
  313. path += '.' + key;
  314. } else {
  315. // 包含特殊字符,使用方括号语法
  316. path += '["' + key.replace(/"/g, '\\"') + '"]';
  317. }
  318. }
  319. }
  320. break;
  321. case 'php':
  322. path = '$data';
  323. for (let i = 0; i < keys.length; i++) {
  324. let key = keys[i];
  325. if (key.startsWith('[') && key.endsWith(']')) {
  326. // 数组索引
  327. path += key;
  328. } else {
  329. // 对象属性
  330. path += '["' + key.replace(/"/g, '\\"') + '"]';
  331. }
  332. }
  333. break;
  334. case 'python':
  335. path = 'data';
  336. for (let i = 0; i < keys.length; i++) {
  337. let key = keys[i];
  338. if (key.startsWith('[') && key.endsWith(']')) {
  339. // 数组索引
  340. path += key;
  341. } else {
  342. // 对象属性
  343. if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key) && !/^(and|as|assert|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|not|or|pass|print|raise|return|try|while|with|yield)$/.test(key)) {
  344. // 有效的标识符且不是关键字,可以使用点语法
  345. path += '.' + key;
  346. } else {
  347. // 使用方括号语法
  348. path += '["' + key.replace(/"/g, '\\"') + '"]';
  349. }
  350. }
  351. }
  352. break;
  353. case 'java':
  354. path = 'jsonObject';
  355. for (let i = 0; i < keys.length; i++) {
  356. let key = keys[i];
  357. if (key.startsWith('[') && key.endsWith(']')) {
  358. // 数组索引
  359. let index = key.slice(1, -1);
  360. path += '.get(' + index + ')';
  361. } else {
  362. // 对象属性
  363. path += '.get("' + key.replace(/"/g, '\\"') + '")';
  364. }
  365. }
  366. break;
  367. case 'csharp':
  368. path = 'jsonObject';
  369. for (let i = 0; i < keys.length; i++) {
  370. let key = keys[i];
  371. if (key.startsWith('[') && key.endsWith(']')) {
  372. // 数组索引
  373. path += key;
  374. } else {
  375. // 对象属性
  376. path += '["' + key.replace(/"/g, '\\"') + '"]';
  377. }
  378. }
  379. break;
  380. case 'golang':
  381. path = 'data';
  382. for (let i = 0; i < keys.length; i++) {
  383. let key = keys[i];
  384. if (key.startsWith('[') && key.endsWith(']')) {
  385. // 数组索引
  386. let index = key.slice(1, -1);
  387. path += '.(' + index + ')';
  388. } else {
  389. // 对象属性
  390. path += '["' + key.replace(/"/g, '\\"') + '"]';
  391. }
  392. }
  393. break;
  394. case 'ruby':
  395. path = 'data';
  396. for (let i = 0; i < keys.length; i++) {
  397. let key = keys[i];
  398. if (key.startsWith('[') && key.endsWith(']')) {
  399. // 数组索引
  400. path += key;
  401. } else {
  402. // 对象属性
  403. if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
  404. // 可以使用符号访问
  405. path += '[:"' + key + '"]';
  406. } else {
  407. // 字符串键
  408. path += '["' + key.replace(/"/g, '\\"') + '"]';
  409. }
  410. }
  411. }
  412. break;
  413. case 'swift':
  414. path = 'jsonObject';
  415. for (let i = 0; i < keys.length; i++) {
  416. let key = keys[i];
  417. if (key.startsWith('[') && key.endsWith(']')) {
  418. // 数组索引
  419. path += key;
  420. } else {
  421. // 对象属性
  422. path += '["' + key.replace(/"/g, '\\"') + '"]';
  423. }
  424. }
  425. break;
  426. default:
  427. // 默认使用JavaScript格式
  428. return _formatJsonPath(keys, 'javascript');
  429. }
  430. return path;
  431. };
  432. // 获取不同语言的根对象表示
  433. let _getLanguageRoot = function(language) {
  434. switch (language) {
  435. case 'javascript': return '$';
  436. case 'php': return '$data';
  437. case 'python': return 'data';
  438. case 'java': return 'jsonObject';
  439. case 'csharp': return 'jsonObject';
  440. case 'golang': return 'data';
  441. case 'ruby': return 'data';
  442. case 'swift': return 'jsonObject';
  443. default: return '$';
  444. }
  445. };
  446. // 给某个节点增加操作项
  447. let _addOptForItem = function (el, show) {
  448. // 下载json片段
  449. let fnDownload = function (event) {
  450. event.stopPropagation();
  451. let txt = getJsonText(el);
  452. // 下载片段
  453. let dt = (new Date()).format('yyyyMMddHHmmss');
  454. let blob = new Blob([txt], {type: 'application/octet-stream'});
  455. if (typeof chrome === 'undefined' || !chrome.permissions) {
  456. // 下载JSON的简单形式
  457. $(this).attr('download', 'FeHelper-' + dt + '.json').attr('href', URL.createObjectURL(blob));
  458. } else {
  459. // 请求权限
  460. chrome.permissions.request({
  461. permissions: ['downloads']
  462. }, (granted) => {
  463. if (granted) {
  464. chrome.downloads.download({
  465. url: URL.createObjectURL(blob),
  466. saveAs: true,
  467. conflictAction: 'overwrite',
  468. filename: 'FeHelper-' + dt + '.json'
  469. });
  470. } else {
  471. toast('必须接受授权,才能正常下载!');
  472. }
  473. });
  474. }
  475. };
  476. // 复制json片段
  477. let fnCopy = function (event) {
  478. event.stopPropagation();
  479. _copyToClipboard(getJsonText(el));
  480. };
  481. // 删除json片段
  482. let fnDel = function (event) {
  483. event.stopPropagation();
  484. if (el.parent().is('#formattedJson')) {
  485. toast('如果连最外层的Json也删掉的话,就没啥意义了哦!');
  486. return false;
  487. }
  488. toast('节点已删除成功!');
  489. el.remove();
  490. jfStatusBar && jfStatusBar.hide();
  491. };
  492. $('.boxOpt').hide();
  493. if (show) {
  494. let jfOptEl = el.children('.boxOpt');
  495. if (!jfOptEl.length) {
  496. jfOptEl = $('<b class="boxOpt">' +
  497. '<a class="opt-copy" title="复制当前选中节点的JSON数据">复制</a>|' +
  498. '<a class="opt-download" target="_blank" title="下载当前选中节点的JSON数据">下载</a>|' +
  499. '<a class="opt-del" title="删除当前选中节点的JSON数据">删除</a></b>').appendTo(el);
  500. } else {
  501. jfOptEl.show();
  502. }
  503. jfOptEl.find('a.opt-download').unbind('click').bind('click', fnDownload);
  504. jfOptEl.find('a.opt-copy').unbind('click').bind('click', fnCopy);
  505. jfOptEl.find('a.opt-del').unbind('click').bind('click', fnDel);
  506. }
  507. };
  508. // 显示当前节点的Key
  509. let _toogleStatusBar = function (curEl, show) {
  510. if (!jfStatusBar) {
  511. jfStatusBar = $('<div id="statusBar"/>').appendTo('body');
  512. }
  513. if (!show) {
  514. jfStatusBar.hide();
  515. return;
  516. } else {
  517. jfStatusBar.show();
  518. }
  519. _showJsonPath(curEl);
  520. };
  521. /**
  522. * 递归折叠所有层级的对象和数组节点
  523. * @param elements
  524. */
  525. function collapse(elements) {
  526. elements.each(function () {
  527. var el = $(this);
  528. if (el.children('.kv-list').length) {
  529. el.addClass('collapsed');
  530. // 只给没有id的节点分配唯一id,并生成注释
  531. if (!el.attr('id')) {
  532. el.attr('id', 'item' + (++lastItemIdGiven));
  533. let count = el.children('.kv-list').eq(0).children().length;
  534. let comment = count + (count === 1 ? ' item' : ' items');
  535. jfStyleEl[0].insertAdjacentHTML(
  536. 'beforeend',
  537. '\n#item' + lastItemIdGiven + '.collapsed:after{color: #aaa; content:" // ' + comment + '"}'
  538. );
  539. }
  540. // 递归对子节点继续折叠,确保所有嵌套层级都被处理
  541. collapse(el.children('.kv-list').children('.item-object, .item-block'));
  542. }
  543. });
  544. }
  545. /**
  546. * 创建几个全局操作的按钮,置于页面右上角即可
  547. * @private
  548. */
  549. let _buildOptionBar = function () {
  550. let optionBar = $('#optionBar');
  551. if (optionBar.length) {
  552. optionBar.html('');
  553. } else {
  554. optionBar = $('<span id="optionBar" />').appendTo(jfContent.parent());
  555. }
  556. $('<span class="x-split">|</span>').appendTo(optionBar);
  557. let buttonFormatted = $('<button class="xjf-btn xjf-btn-left">元数据</button>').appendTo(optionBar);
  558. let buttonCollapseAll = $('<button class="xjf-btn xjf-btn-mid">折叠所有</button>').appendTo(optionBar);
  559. let plainOn = false;
  560. buttonFormatted.bind('click', function (e) {
  561. if (plainOn) {
  562. plainOn = false;
  563. jfPre.hide();
  564. jfContent.show();
  565. buttonFormatted.text('元数据');
  566. } else {
  567. plainOn = true;
  568. jfPre.show();
  569. jfContent.hide();
  570. buttonFormatted.text('格式化');
  571. }
  572. jfStatusBar && jfStatusBar.hide();
  573. });
  574. buttonCollapseAll.bind('click', function (e) {
  575. // 如果内容还没有格式化过,需要再格式化一下
  576. if (plainOn) {
  577. buttonFormatted.trigger('click');
  578. }
  579. if (buttonCollapseAll.text() === '折叠所有') {
  580. buttonCollapseAll.text('展开所有');
  581. // 递归折叠所有层级的对象和数组,确保所有内容都被折叠
  582. collapse($('#jfContent .item-object, #jfContent .item-block'));
  583. } else {
  584. buttonCollapseAll.text('折叠所有');
  585. // 展开所有内容
  586. $('.item-object,.item-block').removeClass('collapsed');
  587. }
  588. jfStatusBar && jfStatusBar.hide();
  589. });
  590. };
  591. // 附加操作
  592. let _addEvents = function () {
  593. // 折叠、展开
  594. $('#jfContent span.expand').bind('click', function (ev) {
  595. ev.preventDefault();
  596. ev.stopPropagation();
  597. let parentEl = $(this).parent();
  598. parentEl.toggleClass('collapsed');
  599. if (parentEl.hasClass('collapsed')) {
  600. collapse(parentEl);
  601. }
  602. });
  603. // 点击选中:高亮
  604. $('#jfContent .item').bind('click', function (e) {
  605. let el = $(this);
  606. if (el.hasClass('x-selected')) {
  607. _toogleStatusBar(el, false);
  608. _addOptForItem(el, false);
  609. el.removeClass('x-selected');
  610. e.stopPropagation();
  611. return true;
  612. }
  613. $('.x-selected').removeClass('x-selected');
  614. el.addClass('x-selected');
  615. // 显示底部状态栏
  616. _toogleStatusBar(el, true);
  617. _addOptForItem(el, true);
  618. if (!$(e.target).is('.item .expand')) {
  619. e.stopPropagation();
  620. } else {
  621. $(e.target).parent().trigger('click');
  622. }
  623. // 触发钩子
  624. if (typeof window._OnJsonItemClickByFH === 'function') {
  625. window._OnJsonItemClickByFH(getJsonText(el));
  626. }
  627. });
  628. // 行悬停效果:只高亮当前直接悬停的item,避免嵌套冒泡
  629. let currentHoverElement = null;
  630. $('#jfContent .item').bind('mouseenter', function (e) {
  631. // 只处理视觉效果,不触发任何其他逻辑
  632. // 清除之前的悬停样式
  633. if (currentHoverElement) {
  634. currentHoverElement.removeClass('fh-hover');
  635. }
  636. // 添加当前悬停样式
  637. let el = $(this);
  638. el.addClass('fh-hover');
  639. currentHoverElement = el;
  640. // 严格阻止事件冒泡和默认行为
  641. e.stopPropagation();
  642. e.stopImmediatePropagation();
  643. e.preventDefault();
  644. });
  645. $('#jfContent .item').bind('mouseleave', function (e) {
  646. // 只处理视觉效果,不触发任何其他逻辑
  647. let el = $(this);
  648. el.removeClass('fh-hover');
  649. // 如果当前移除的元素是记录的悬停元素,清空记录
  650. if (currentHoverElement && currentHoverElement[0] === el[0]) {
  651. currentHoverElement = null;
  652. }
  653. // 严格阻止事件冒泡和默认行为
  654. e.stopPropagation();
  655. e.stopImmediatePropagation();
  656. });
  657. // 为整个jfContent区域添加鼠标离开事件,确保彻底清除悬停样式
  658. $('#jfContent').bind('mouseleave', function (e) {
  659. if (currentHoverElement) {
  660. currentHoverElement.removeClass('fh-hover');
  661. currentHoverElement = null;
  662. }
  663. });
  664. // 图片预览功能:针对所有data-is-link=1的a标签
  665. let $imgPreview = null;
  666. // 加载缓存
  667. function getImgCache() {
  668. try {
  669. return JSON.parse(sessionStorage.getItem('fehelper-img-preview-cache') || '{}');
  670. } catch (e) { return {}; }
  671. }
  672. function setImgCache(url, isImg) {
  673. let cache = getImgCache();
  674. cache[url] = isImg;
  675. sessionStorage.setItem('fehelper-img-preview-cache', JSON.stringify(cache));
  676. }
  677. $('#jfContent').on('mouseenter', 'a[data-is-link="1"]', function(e) {
  678. const url = $(this).attr('data-link-url');
  679. if (!url) return;
  680. let cache = getImgCache();
  681. if (cache.hasOwnProperty(url)) {
  682. if (cache[url]) {
  683. $imgPreview = getOrCreateImgPreview();
  684. $imgPreview.find('img').attr('src', url);
  685. $imgPreview.show();
  686. $(document).on('mousemove.fhimg', function(ev) {
  687. $imgPreview.css({
  688. left: ev.pageX + 20 + 'px',
  689. top: ev.pageY + 20 + 'px'
  690. });
  691. });
  692. $imgPreview.css({
  693. left: e.pageX + 20 + 'px',
  694. top: e.pageY + 20 + 'px'
  695. });
  696. }
  697. return;
  698. }
  699. // 创建图片对象尝试加载
  700. const img = new window.Image();
  701. img.src = url;
  702. img.onload = function() {
  703. setImgCache(url, true);
  704. $imgPreview = getOrCreateImgPreview();
  705. $imgPreview.find('img').attr('src', url);
  706. $imgPreview.show();
  707. $(document).on('mousemove.fhimg', function(ev) {
  708. $imgPreview.css({
  709. left: ev.pageX + 20 + 'px',
  710. top: ev.pageY + 20 + 'px'
  711. });
  712. });
  713. $imgPreview.css({
  714. left: e.pageX + 20 + 'px',
  715. top: e.pageY + 20 + 'px'
  716. });
  717. };
  718. img.onerror = function() {
  719. setImgCache(url, false);
  720. };
  721. }).on('mouseleave', 'a[data-is-link="1"]', function(e) {
  722. if ($imgPreview) $imgPreview.hide();
  723. $(document).off('mousemove.fhimg');
  724. });
  725. // 新增:全局监听,防止浮窗残留
  726. $(document).on('mousemove.fhimgcheck', function(ev) {
  727. let $target = $(ev.target).closest('a[data-is-link="1"]');
  728. if ($target.length === 0) {
  729. if ($imgPreview) $imgPreview.hide();
  730. $(document).off('mousemove.fhimg');
  731. }
  732. });
  733. };
  734. /**
  735. * 初始化或获取Worker实例(异步,兼容Chrome/Edge/Firefox)
  736. * @returns {Promise<Worker|null>}
  737. */
  738. let _getWorkerInstance = async function() {
  739. if (workerInstance) {
  740. return workerInstance;
  741. }
  742. let workerUrl = chrome.runtime.getURL('json-format/json-worker.js');
  743. // 判断是否为Firefox
  744. const isFirefox = typeof InstallTrigger !== 'undefined' || navigator.userAgent.includes('Firefox');
  745. try {
  746. if (isFirefox) {
  747. workerInstance = new Worker(workerUrl);
  748. return workerInstance;
  749. } else {
  750. // Chrome/Edge用fetch+Blob方式
  751. const resp = await fetch(workerUrl);
  752. const workerScript = await resp.text();
  753. const blob = new Blob([workerScript], { type: 'application/javascript' });
  754. const blobUrl = URL.createObjectURL(blob);
  755. workerInstance = new Worker(blobUrl);
  756. return workerInstance;
  757. }
  758. } catch (e) {
  759. console.error('创建Worker失败:', e);
  760. workerInstance = null;
  761. return null;
  762. }
  763. };
  764. /**
  765. * 执行代码格式化
  766. * 支持异步worker
  767. */
  768. let format = async function (jsonStr, skin) {
  769. cachedJsonString = JSON.stringify(JSON.parse(jsonStr), null, 4);
  770. _initElements();
  771. jfPre.html(htmlspecialchars(cachedJsonString));
  772. try {
  773. // 获取Worker实例(异步)
  774. let worker = await _getWorkerInstance();
  775. if (worker) {
  776. // 设置消息处理程序
  777. worker.onmessage = function (evt) {
  778. let msg = evt.data;
  779. switch (msg[0]) {
  780. case 'FORMATTING':
  781. formattingMsg.show();
  782. break;
  783. case 'FORMATTED':
  784. formattingMsg.hide();
  785. jfContent.html(msg[1]);
  786. _buildOptionBar();
  787. // 事件绑定
  788. _addEvents();
  789. // 支持文件下载
  790. _downloadSupport(cachedJsonString);
  791. break;
  792. }
  793. };
  794. // 发送格式化请求
  795. worker.postMessage({
  796. jsonString: jsonStr,
  797. skin: skin
  798. });
  799. } else {
  800. // Worker创建失败,回退到同步方式
  801. formatSync(jsonStr, skin);
  802. }
  803. } catch (e) {
  804. console.error('Worker处理失败:', e);
  805. // 出现任何错误,回退到同步方式
  806. formatSync(jsonStr, skin);
  807. }
  808. };
  809. // 同步的方式格式化
  810. let formatSync = function (jsonStr, skin) {
  811. cachedJsonString = JSON.stringify(JSON.parse(jsonStr), null, 4);
  812. _initElements();
  813. jfPre.html(htmlspecialchars(cachedJsonString));
  814. // 显示格式化进度
  815. formattingMsg.show();
  816. try {
  817. // 回退方案:使用简单模式直接显示格式化的JSON
  818. let formattedJson = JSON.stringify(JSON.parse(jsonStr), null, 4);
  819. jfContent.html(`<div id="formattedJson"><pre class="rootItem">${htmlspecialchars(formattedJson)}</pre></div>`);
  820. // 隐藏进度提示
  821. formattingMsg.hide();
  822. // 构建操作栏
  823. _buildOptionBar();
  824. // 事件绑定
  825. _addEvents();
  826. // 支持文件下载
  827. _downloadSupport(cachedJsonString);
  828. return;
  829. } catch (e) {
  830. jfContent.html(`<div class="error">JSON格式化失败: ${e.message}</div>`);
  831. // 隐藏进度提示
  832. formattingMsg.hide();
  833. }
  834. };
  835. // 工具函数:获取或创建唯一图片预览浮窗节点
  836. function getOrCreateImgPreview() {
  837. let $img = $('#fh-img-preview');
  838. if (!$img.length) {
  839. $img = $('<div id="fh-img-preview" style="position:absolute;z-index:999999;border:1px solid #ccc;background:#fff;padding:4px;box-shadow:0 2px 8px #0002;pointer-events:none;"><img style="max-width:300px;max-height:200px;display:block;"></div>').appendTo('body');
  840. }
  841. return $img;
  842. }
  843. return {
  844. format: format,
  845. formatSync: formatSync
  846. }
  847. })();