@zjy4fun/json-open 0.1.9 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/json.js +210 -0
  2. package/package.json +1 -1
package/bin/json.js CHANGED
@@ -224,12 +224,71 @@ function toHtml(jsonObj, deepParsedObj) {
224
224
  .null { color: #cbd5e1; }
225
225
  .symbol { color: #c4b5fd; }
226
226
  .meta { color: #64748b; }
227
+ /* 搜索框 */
228
+ .search-wrap {
229
+ display: flex;
230
+ align-items: center;
231
+ gap: 6px;
232
+ }
233
+ .search-wrap input {
234
+ border: 1px solid #475569;
235
+ background: #1e293b;
236
+ color: #e2e8f0;
237
+ border-radius: 8px;
238
+ padding: 7px 12px;
239
+ font-size: 13px;
240
+ font-family: inherit;
241
+ width: 200px;
242
+ outline: none;
243
+ transition: border-color 0.2s;
244
+ }
245
+ .search-wrap input:focus {
246
+ border-color: #58a6ff;
247
+ }
248
+ .search-wrap input::placeholder {
249
+ color: #64748b;
250
+ }
251
+ .search-count {
252
+ font-size: 12px;
253
+ color: #64748b;
254
+ min-width: 60px;
255
+ }
256
+ .search-nav button {
257
+ padding: 4px 8px;
258
+ font-size: 12px;
259
+ border-radius: 6px;
260
+ }
261
+ /* 搜索高亮 */
262
+ mark.highlight {
263
+ background: #f0883e;
264
+ color: #0d1117;
265
+ border-radius: 2px;
266
+ padding: 0 1px;
267
+ }
268
+ mark.highlight.current {
269
+ background: #58a6ff;
270
+ color: #fff;
271
+ box-shadow: 0 0 0 2px rgba(88,166,255,0.4);
272
+ }
273
+ /* 搜索时隐藏不匹配的行 */
274
+ .search-active .line.hidden-by-search,
275
+ .search-active li.hidden-by-search {
276
+ display: none;
277
+ }
227
278
  </style>
228
279
  </head>
229
280
  <body>
230
281
  <div class=\"toolbar\">
231
282
  <button id=\"expand-all\">Expand all</button>
232
283
  <button id=\"collapse-all\">Collapse all</button>
284
+ <div class=\"search-wrap\">
285
+ <input type=\"text\" id=\"search-input\" placeholder=\"Search...\" autocomplete=\"off\" />
286
+ <span class=\"search-count\" id=\"search-count\"></span>
287
+ <span class=\"search-nav\">
288
+ <button id=\"search-prev\" title=\"Previous (Shift+Enter)\">▲</button>
289
+ <button id=\"search-next\" title=\"Next (Enter)\">▼</button>
290
+ </span>
291
+ </div>
233
292
  <div class=\"toggle-wrap${hasDiff ? '' : ' hidden'}\" title=\"Parse embedded JSON strings inside values\">
234
293
  <span>Parse JSON strings</span>
235
294
  <label class=\"toggle\">
@@ -257,8 +316,159 @@ function toHtml(jsonObj, deepParsedObj) {
257
316
  rawView.style.display = ''
258
317
  parsedView.style.display = 'none'
259
318
  }
319
+ // 切换视图后重新搜索
320
+ if (searchInput.value.trim()) doSearch()
321
+ })
322
+ }
323
+
324
+ // ===== 搜索功能 =====
325
+ const searchInput = document.getElementById('search-input')
326
+ const searchCount = document.getElementById('search-count')
327
+ const searchPrev = document.getElementById('search-prev')
328
+ const searchNext = document.getElementById('search-next')
329
+ let highlights = []
330
+ let currentIdx = -1
331
+
332
+ function getActiveView() {
333
+ return parsedView.style.display === 'none' ? rawView : parsedView
334
+ }
335
+
336
+ function clearSearch() {
337
+ // 移除所有高亮
338
+ document.querySelectorAll('mark.highlight').forEach(mark => {
339
+ const parent = mark.parentNode
340
+ parent.replaceChild(document.createTextNode(mark.textContent), mark)
341
+ parent.normalize()
342
+ })
343
+ highlights = []
344
+ currentIdx = -1
345
+ searchCount.textContent = ''
346
+ document.body.classList.remove('search-active')
347
+ }
348
+
349
+ function doSearch() {
350
+ clearSearch()
351
+ const query = searchInput.value.trim()
352
+ if (!query) return
353
+
354
+ const view = getActiveView()
355
+ const regex = new RegExp(query.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&'), 'gi')
356
+
357
+ // 遍历所有文本节点进行高亮
358
+ const walker = document.createTreeWalker(view, NodeFilter.SHOW_TEXT, null)
359
+ const textNodes = []
360
+ while (walker.nextNode()) textNodes.push(walker.currentNode)
361
+
362
+ textNodes.forEach(node => {
363
+ const text = node.textContent
364
+ if (!regex.test(text)) return
365
+ regex.lastIndex = 0
366
+
367
+ const frag = document.createDocumentFragment()
368
+ let lastIdx = 0
369
+ let match
370
+ while ((match = regex.exec(text)) !== null) {
371
+ if (match.index > lastIdx) {
372
+ frag.appendChild(document.createTextNode(text.slice(lastIdx, match.index)))
373
+ }
374
+ const mark = document.createElement('mark')
375
+ mark.className = 'highlight'
376
+ mark.textContent = match[0]
377
+ frag.appendChild(mark)
378
+ lastIdx = regex.lastIndex
379
+ }
380
+ if (lastIdx < text.length) {
381
+ frag.appendChild(document.createTextNode(text.slice(lastIdx)))
382
+ }
383
+ node.parentNode.replaceChild(frag, node)
260
384
  })
385
+
386
+ highlights = Array.from(view.querySelectorAll('mark.highlight'))
387
+ if (highlights.length > 0) {
388
+ // 展开所有包含匹配项的 <details>(从内到外逐层展开)
389
+ highlights.forEach(h => {
390
+ let el = h.closest('details')
391
+ while (el) {
392
+ el.open = true
393
+ el = el.parentElement ? el.parentElement.closest('details') : null
394
+ }
395
+ })
396
+ currentIdx = 0
397
+ // 等 DOM 重新布局后再滚动,确保展开动画完成
398
+ requestAnimationFrame(() => scrollToCurrent())
399
+ }
400
+ updateCount()
261
401
  }
402
+
403
+ function updateCount() {
404
+ if (highlights.length === 0 && searchInput.value.trim()) {
405
+ searchCount.textContent = 'No match'
406
+ } else if (highlights.length > 0) {
407
+ searchCount.textContent = (currentIdx + 1) + ' / ' + highlights.length
408
+ } else {
409
+ searchCount.textContent = ''
410
+ }
411
+ }
412
+
413
+ function scrollToCurrent() {
414
+ highlights.forEach((h, i) => {
415
+ h.classList.toggle('current', i === currentIdx)
416
+ })
417
+ if (highlights[currentIdx]) {
418
+ // 确保当前高亮项所在的所有 <details> 都是展开的
419
+ let el = highlights[currentIdx].closest('details')
420
+ while (el) {
421
+ el.open = true
422
+ el = el.parentElement ? el.parentElement.closest('details') : null
423
+ }
424
+ highlights[currentIdx].scrollIntoView({ behavior: 'smooth', block: 'center' })
425
+ }
426
+ updateCount()
427
+ }
428
+
429
+ function goNext() {
430
+ if (highlights.length === 0) return
431
+ currentIdx = (currentIdx + 1) % highlights.length
432
+ scrollToCurrent()
433
+ }
434
+
435
+ function goPrev() {
436
+ if (highlights.length === 0) return
437
+ currentIdx = (currentIdx - 1 + highlights.length) % highlights.length
438
+ scrollToCurrent()
439
+ }
440
+
441
+ // 输入时实时搜索(防抖 200ms)
442
+ let debounceTimer
443
+ searchInput.addEventListener('input', () => {
444
+ clearTimeout(debounceTimer)
445
+ debounceTimer = setTimeout(doSearch, 200)
446
+ })
447
+
448
+ // Enter = 下一个,Shift+Enter = 上一个
449
+ searchInput.addEventListener('keydown', (e) => {
450
+ if (e.key === 'Enter') {
451
+ e.preventDefault()
452
+ e.shiftKey ? goPrev() : goNext()
453
+ }
454
+ if (e.key === 'Escape') {
455
+ searchInput.value = ''
456
+ clearSearch()
457
+ searchInput.blur()
458
+ }
459
+ })
460
+
461
+ searchNext.addEventListener('click', goNext)
462
+ searchPrev.addEventListener('click', goPrev)
463
+
464
+ // Ctrl+F / Cmd+F 聚焦搜索框
465
+ document.addEventListener('keydown', (e) => {
466
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
467
+ e.preventDefault()
468
+ searchInput.focus()
469
+ searchInput.select()
470
+ }
471
+ })
262
472
  </script>
263
473
  </body>
264
474
  </html>`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zjy4fun/json-open",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "Open JSON (stdin or inline text) in a browser with collapsible tree view",
5
5
  "type": "module",
6
6
  "bin": {