Skip to content

Conversation

@zhravan
Copy link
Owner

@zhravan zhravan commented Feb 11, 2025

Summary by CodeRabbit

  • New Features
    • Revamped the user interface to support three distinct JSON modes: Formatter, Compare, and Code Generation.
    • Enhanced state management now preserves session-specific interactions and settings.
    • Updated keyboard shortcut displays and improved error handling provide a more interactive experience.
    • Overall layout and navigation improvements deliver a more intuitive and versatile tool for JSON data manipulation.

@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2025

Walkthrough

The HTML file has undergone a comprehensive update to expand its functionality from a JSON previewer to a generic JSON tool. The document now supports multiple modes—Formatter, Compare, and Code Generation—through dedicated tab management functions. In addition, the state management has been enhanced to preserve user interactions across sessions, and the keyboard shortcuts modal and error handling have been updated to reflect these new features.

Changes

Files Change Summary
index.html - Renamed document title from "Multitab JSON Previewer" to "Generic JSON Tool".
- Overhauled HTML structure to include separate sections for Formatter, Compare, and Code Generation modes.
- Added new JavaScript functions for tab creation, switching, state management, preview updates, JSON comparison, code generation, keyboard shortcut modal toggling, and dark mode.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI
    participant JSONTool

    User->>UI: Selects tab (Formatter / Compare / CodeGen)
    UI->>JSONTool: Calls addTab() for selected mode
    JSONTool->>UI: Invokes createTab() function for mode
    UI->>JSONTool: Switches to selected tab (switchTab())
    JSONTool->>UI: Refreshes preview / displays comparison / generates code
    User->>UI: Toggles keyboard shortcuts or dark mode
    UI->>JSONTool: Calls toggleShortcutModal() / toggleDarkMode()
Loading

Poem

I'm a little rabbit, hopping with delight,
New tabs are blooming, shining bright.
Formatter, Compare, Code Gen – oh what fun,
JSON magic now for everyone!
With every click, my heart does leap,
A tool so fresh, a code garden to keep.
🐇🌟 Happy changes, in the digital deep!


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@zhravan zhravan merged commit 444b22a into main Feb 11, 2025
@zhravan zhravan deleted the shravan20-patch-1 branch February 11, 2025 17:32
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (6)
index.html (6)

7-275: Consider extracting CSS to a separate file and using CSS variables.

The CSS is well-organized but could benefit from the following improvements:

  1. Move styles to a separate .css file for better maintainability.
  2. Use CSS variables for consistent theming (colors, spacing, etc.).
  3. Add media queries for better responsive design.

Example of using CSS variables:

 :root {
+  /* Colors */
+  --primary-color: #007bff;
+  --primary-hover: #0056b3;
+  --bg-color: #f0f0f0;
+  --text-color: #333;
+  --border-color: #ccc;
+  
+  /* Spacing */
+  --spacing-sm: 5px;
+  --spacing-md: 10px;
+  --spacing-lg: 20px;
 }

 body {
-  background-color: #f0f0f0;
-  color: #333;
+  background-color: var(--bg-color);
+  color: var(--text-color);
 }

 .dark-mode-toggle,
 .shortcut-preview-button {
-  background-color: #007bff;
+  background-color: var(--primary-color);
 }

423-441: Enhance mode switching with event delegation and animations.

The mode switching implementation could be improved with:

  1. Event delegation for better performance.
  2. Smooth transitions between modes.

Apply these changes:

+const MODE_TRANSITION_MS = 300;

 function switchMode(mode) {
+  const sections = {
+    formatter: document.getElementById("formatter-section"),
+    compare: document.getElementById("compare-section"),
+    codegen: document.getElementById("codegen-section")
+  };
+  
+  // Add transition class
+  Object.values(sections).forEach(section => {
+    section.style.transition = `opacity ${MODE_TRANSITION_MS}ms ease-in-out`;
+    section.style.opacity = "0";
+  });
+  
+  // Switch after fade out
+  setTimeout(() => {
     document.getElementById("formatter-section").style.display = "none";
     document.getElementById("compare-section").style.display = "none";
     document.getElementById("codegen-section").style.display = "none";
     
-    if (mode === "formatter") {
-      document.getElementById("formatter-section").style.display = "block";
-    } // ... rest of the mode checks
+    const activeSection = sections[mode];
+    if (activeSection) {
+      activeSection.style.display = "block";
+      // Trigger reflow
+      void activeSection.offsetWidth;
+      activeSection.style.opacity = "1";
+    }
+  }, MODE_TRANSITION_MS);
 }

+// Event delegation for mode buttons
+document.querySelector(".mode-selector").addEventListener("click", (e) => {
+  const btn = e.target.closest("button");
+  if (btn) {
+    const mode = btn.id.replace("mode-", "").replace("-btn", "");
+    switchMode(mode);
+  }
+});

623-780: Enhance JSON comparison with better diff visualization.

The comparison functionality could be improved with:

  1. Advanced diff algorithm for structural changes.
  2. Better visualization of differences.
  3. Copy-to-clipboard functionality.

Apply these enhancements:

+function deepDiff(left, right, path = "") {
+  const changes = [];
+  if (typeof left !== typeof right) {
+    changes.push({ path, type: "type", left, right });
+    return changes;
+  }
+  if (typeof left === "object" && left !== null && right !== null) {
+    const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
+    for (const key of keys) {
+      const newPath = path ? `${path}.${key}` : key;
+      if (!(key in left)) {
+        changes.push({ path: newPath, type: "added", value: right[key] });
+      } else if (!(key in right)) {
+        changes.push({ path: newPath, type: "removed", value: left[key] });
+      } else {
+        changes.push(...deepDiff(left[key], right[key], newPath));
+      }
+    }
+  } else if (left !== right) {
+    changes.push({ path, type: "modified", left, right });
+  }
+  return changes;
+}

 function compareJSONs(tabId) {
   // ... existing parsing code ...
   
+  const differences = deepDiff(leftObj, rightObj);
+  const html = `
+    <div class="diff-summary">
+      <p>Found ${differences.length} differences</p>
+      <button onclick="copyDiffToClipboard('${tabId}')">Copy Diff</button>
+    </div>
+    <div class="diff-details">
+      ${differences.map(diff => `
+        <div class="diff-item diff-${diff.type}">
+          <span class="diff-path">${diff.path}</span>
+          <span class="diff-type">${diff.type}</span>
+          ${diff.type === "modified" ? `
+            <div class="diff-value">
+              <pre>- ${JSON.stringify(diff.left, null, 2)}</pre>
+              <pre>+ ${JSON.stringify(diff.right, null, 2)}</pre>
+            </div>
+          ` : `
+            <pre>${JSON.stringify(diff.value, null, 2)}</pre>
+          `}
+        </div>
+      `).join("")}
+    </div>
+  `;
   resultDiv.innerHTML = html;
 }

+function copyDiffToClipboard(tabId) {
+  const diffDetails = document.querySelector(`#${tabId} .diff-details`).textContent;
+  navigator.clipboard.writeText(diffDetails)
+    .then(() => alert("Diff copied to clipboard"))
+    .catch(err => console.error("Failed to copy:", err));
+}

781-907: Expand code generation capabilities.

The code generation functionality could be improved with:

  1. Support for more languages (Java, C#, etc.).
  2. Code validation and formatting.
  3. Copy-to-clipboard functionality.

Add these enhancements:

+const SUPPORTED_LANGUAGES = {
+  typescript: { name: "TypeScript", extension: ".ts" },
+  python: { name: "Python", extension: ".py" },
+  go: { name: "Go", extension: ".go" },
+  java: { name: "Java", extension: ".java" },
+  csharp: { name: "C#", extension: ".cs" }
+};

 function createCodegenTab() {
   // ... existing code ...
   tabContent.innerHTML = `
     <textarea class="json-input" placeholder="Enter JSON here..."></textarea>
     <div style="margin-top:10px;">
       <label for="lang-select-${tabId}">Select Language:</label>
       <select id="lang-select-${tabId}">
-        <option value="typescript">TypeScript</option>
-        <option value="python">Python</option>
-        <option value="go">Go</option>
+        ${Object.entries(SUPPORTED_LANGUAGES)
+          .map(([value, { name }]) => 
+            `<option value="${value}">${name}</option>`
+          ).join("")}
       </select>
       <button onclick="generateCode('${tabId}')">Generate Code</button>
+      <button onclick="copyGeneratedCode('${tabId}')">Copy Code</button>
+      <button onclick="downloadGeneratedCode('${tabId}')">Download</button>
     </div>
     <pre class="code-output" style="margin-top:10px; overflow:auto;"></pre>
   `;
 }

+function copyGeneratedCode(tabId) {
+  const code = document.querySelector(`#${tabId} .code-output`).textContent;
+  navigator.clipboard.writeText(code)
+    .then(() => alert("Code copied to clipboard"))
+    .catch(err => console.error("Failed to copy:", err));
+}

+function downloadGeneratedCode(tabId) {
+  const code = document.querySelector(`#${tabId} .code-output`).textContent;
+  const lang = document.getElementById(`lang-select-${tabId}`).value;
+  const extension = SUPPORTED_LANGUAGES[lang].extension;
+  
+  const blob = new Blob([code], { type: "text/plain" });
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement("a");
+  a.href = url;
+  a.download = `generated_code${extension}`;
+  document.body.appendChild(a);
+  a.click();
+  document.body.removeChild(a);
+  URL.revokeObjectURL(url);
+}

908-1093: Optimize tree view performance and enhance tab renaming UX.

The utility functions could be improved with:

  1. Virtual scrolling for large tree views.
  2. Better tab renaming experience.

Apply these optimizations:

+class VirtualScroller {
+  constructor(container, items, rowHeight) {
+    this.container = container;
+    this.items = items;
+    this.rowHeight = rowHeight;
+    this.visibleItems = Math.ceil(container.clientHeight / rowHeight);
+    this.scrollTop = 0;
+    this.setupScroller();
+  }
+  
+  setupScroller() {
+    this.container.style.overflow = "auto";
+    this.content = document.createElement("div");
+    this.content.style.height = `${this.items.length * this.rowHeight}px`;
+    this.container.appendChild(this.content);
+    this.container.addEventListener("scroll", () => this.onScroll());
+    this.renderVisible();
+  }
+  
+  onScroll() {
+    this.scrollTop = this.container.scrollTop;
+    this.renderVisible();
+  }
+  
+  renderVisible() {
+    const start = Math.floor(this.scrollTop / this.rowHeight);
+    const end = Math.min(start + this.visibleItems + 1, this.items.length);
+    this.content.innerHTML = "";
+    for (let i = start; i < end; i++) {
+      const item = this.items[i];
+      const div = document.createElement("div");
+      div.style.position = "absolute";
+      div.style.top = `${i * this.rowHeight}px`;
+      div.innerHTML = item;
+      this.content.appendChild(div);
+    }
+  }
+}

 function createTreeView(data, parentElement) {
+  const items = [];
+  function processNode(value, depth = 0) {
+    const indent = "  ".repeat(depth);
+    if (typeof value === "object" && value !== null) {
+      if (Array.isArray(value)) {
+        items.push(`${indent}[`);
+        value.forEach(item => processNode(item, depth + 1));
+        items.push(`${indent}]`);
+      } else {
+        items.push(`${indent}{`);
+        Object.entries(value).forEach(([key, val]) => {
+          items.push(`${indent}  "${key}": ${
+            typeof val === "object" && val !== null ? "" : JSON.stringify(val)
+          }`);
+          if (typeof val === "object" && val !== null) {
+            processNode(val, depth + 1);
+          }
+        });
+        items.push(`${indent}}`);
+      }
+    }
+  }
+  processNode(data);
+  new VirtualScroller(parentElement, items, 20);
 }

 function openTabRenameTooltip(tabId, mode) {
+  const TOOLTIP_DELAY = 300;
+  let tooltipTimer;
+  
   // ... existing code ...
   
   input.addEventListener("keydown", (e) => {
     if (e.key === "Enter") finalizeRename();
     else if (e.key === "Escape") tooltip.remove();
+    else if (e.key === "Tab") {
+      e.preventDefault();
+      const allTabs = document.querySelectorAll(`${containerSelector} .tab-button[data-tab]`);
+      const currentIndex = Array.from(allTabs).findIndex(tab => tab.getAttribute("data-tab") === tabId);
+      const nextTab = allTabs[currentIndex + 1] || allTabs[0];
+      if (nextTab) {
+        finalizeRename();
+        tooltipTimer = setTimeout(() => {
+          openTabRenameTooltip(nextTab.getAttribute("data-tab"), mode);
+        }, TOOLTIP_DELAY);
+      }
+    }
   });
+  
+  return () => {
+    if (tooltipTimer) clearTimeout(tooltipTimer);
+  };
 }

1109-1137: Expand keyboard shortcuts and improve initialization.

The keyboard shortcuts and initialization could be improved with:

  1. More comprehensive shortcuts.
  2. Better error handling.

Apply these improvements:

+const SHORTCUTS = {
+  FORMATTER: {
+    "Ctrl+T": "New Tab",
+    "Ctrl+W": "Close Tab",
+    "Ctrl+S": "Save JSON",
+    "Ctrl+O": "Open JSON",
+    "Ctrl+F": "Search",
+  },
+  COMPARE: {
+    "Ctrl+T": "New Compare Tab",
+    "Ctrl+W": "Close Tab",
+    "Ctrl+R": "Run Comparison",
+  },
+  CODEGEN: {
+    "Ctrl+T": "New Codegen Tab",
+    "Ctrl+W": "Close Tab",
+    "Ctrl+G": "Generate Code",
+  },
+  GLOBAL: {
+    "Ctrl+/": "Toggle Shortcuts",
+    "Ctrl+D": "Toggle Dark Mode",
+    "1": "Switch to Formatter",
+    "2": "Switch to Compare",
+    "3": "Switch to Codegen",
+  },
+};

 document.addEventListener("keydown", (e) => {
+  // Don't handle shortcuts when typing in inputs
+  if (e.target.matches("input, textarea")) return;
+  
+  const activeMode = getActiveMode();
+  const shortcuts = { ...SHORTCUTS.GLOBAL, ...SHORTCUTS[activeMode.toUpperCase()] };
+  
+  // Handle number keys for mode switching
+  if (!e.ctrlKey && !e.altKey && !e.shiftKey) {
+    if (e.key === "1") switchMode("formatter");
+    else if (e.key === "2") switchMode("compare");
+    else if (e.key === "3") switchMode("codegen");
+  }
+  
   // ... existing shortcuts ...
+  
+  // Add new shortcuts
+  if (e.ctrlKey && e.key.toLowerCase() === "s") {
+    e.preventDefault();
+    if (activeMode === "formatter") {
+      const activeTab = document.querySelector("#formatter-tab-contents .json-tab-content.active");
+      if (activeTab) downloadFormatterJSON(activeTab.id);
+    }
+  }
 });

 window.addEventListener("load", () => {
+  try {
     loadGlobalState();
-    if (document.getElementById("formatter-tab-contents").children.length === 0) {
+    const formatterContent = document.getElementById("formatter-tab-contents");
+    if (!formatterContent) {
+      console.error("Required DOM elements not found");
+      return;
+    }
+    
+    if (formatterContent.children.length === 0) {
       addFormatterTab();
     }
+  } catch (e) {
+    console.error("Failed to initialize:", e);
+    // Show user-friendly error message
+    const error = document.createElement("div");
+    error.className = "error";
+    error.textContent = "Failed to initialize the application. Please refresh the page.";
+    document.body.prepend(error);
+  }
 });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 151f7bc and 6fe9b46.

📒 Files selected for processing (1)
  • index.html (1 hunks)

Comment on lines +276 to 329
</head>
<body>
<div class="container">
<div style="display: flex; align-items: center; margin-bottom: 20px;">
<h1>Generic JSON Tool</h1>
<button class="dark-mode-toggle" onclick="toggleDarkMode()">Toggle Dark Mode</button>
<button class="shortcut-preview-button" onclick="toggleShortcutModal()">Shortcuts</button>
</div>
<!-- Mode Selector -->
<div class="mode-selector">
<button id="mode-formatter-btn" onclick="switchMode('formatter')">JSON Formatter</button>
<button id="mode-compare-btn" onclick="switchMode('compare')">JSON Compare</button>
<button id="mode-codegen-btn" onclick="switchMode('codegen')">JSON to Code</button>
</div>
<!-- Formatter Section -->
<div id="formatter-section">
<div id="formatter-tabs-container" class="tabs">
<button class="add-tab-button" onclick="addFormatterTab()">+ Add Tab</button>
</div>
<div id="formatter-tab-contents">
<!-- Formatter tab contents will be dynamically added here -->
</div>
</div>
<!-- Compare Section -->
<div id="compare-section" style="display: none;">
<div id="compare-tabs-container" class="tabs">
<button class="add-tab-button" onclick="addCompareTab()">+ Add Tab</button>
</div>
<div id="compare-tab-contents">
<!-- Compare tab contents will be dynamically added here -->
</div>
</div>
<!-- Code Generator Section -->
<div id="codegen-section" style="display: none;">
<div id="codegen-tabs-container" class="tabs">
<button class="add-tab-button" onclick="addCodegenTab()">+ Add Tab</button>
</div>
<div id="codegen-tab-contents">
<!-- CodeGen tab contents will be dynamically added here -->
</div>
</div>
</div>
<div id="${tabId}-error-preview" class="preview-section">
<div class="error-message"></div>
<!-- Shortcut Modal -->
<div id="shortcut-modal" class="modal">
<div class="modal-content">
<span class="close-modal" onclick="toggleShortcutModal()">&times;</span>
<h2>Keyboard Shortcuts</h2>
<ul>
<li><strong>Ctrl + T</strong>: New Formatter Tab (in Formatter mode)</li>
<li><strong>Ctrl + W</strong>: Close Current Formatter Tab (in Formatter mode)</li>
<li><strong>Ctrl + /</strong>: Show/Hide Shortcut Panel</li>
</ul>
</div>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance accessibility with ARIA attributes and keyboard navigation.

The UI structure is clean but needs accessibility improvements:

  1. Add ARIA attributes for better screen reader support.
  2. Enhance keyboard navigation in the modal.

Apply these changes to improve accessibility:

-<div class="mode-selector">
+<div class="mode-selector" role="tablist" aria-label="JSON Tool Modes">
-  <button id="mode-formatter-btn" onclick="switchMode('formatter')">JSON Formatter</button>
+  <button id="mode-formatter-btn" onclick="switchMode('formatter')" role="tab" aria-selected="true" aria-controls="formatter-section">JSON Formatter</button>

-<div id="formatter-section">
+<div id="formatter-section" role="tabpanel" aria-labelledby="mode-formatter-btn">

-<div id="shortcut-modal" class="modal">
+<div id="shortcut-modal" class="modal" role="dialog" aria-labelledby="shortcut-modal-title" aria-modal="true">
   <div class="modal-content">
-    <h2>Keyboard Shortcuts</h2>
+    <h2 id="shortcut-modal-title">Keyboard Shortcuts</h2>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
</head>
<body>
<div class="container">
<div style="display: flex; align-items: center; margin-bottom: 20px;">
<h1>Generic JSON Tool</h1>
<button class="dark-mode-toggle" onclick="toggleDarkMode()">Toggle Dark Mode</button>
<button class="shortcut-preview-button" onclick="toggleShortcutModal()">Shortcuts</button>
</div>
<!-- Mode Selector -->
<div class="mode-selector">
<button id="mode-formatter-btn" onclick="switchMode('formatter')">JSON Formatter</button>
<button id="mode-compare-btn" onclick="switchMode('compare')">JSON Compare</button>
<button id="mode-codegen-btn" onclick="switchMode('codegen')">JSON to Code</button>
</div>
<!-- Formatter Section -->
<div id="formatter-section">
<div id="formatter-tabs-container" class="tabs">
<button class="add-tab-button" onclick="addFormatterTab()">+ Add Tab</button>
</div>
<div id="formatter-tab-contents">
<!-- Formatter tab contents will be dynamically added here -->
</div>
</div>
<!-- Compare Section -->
<div id="compare-section" style="display: none;">
<div id="compare-tabs-container" class="tabs">
<button class="add-tab-button" onclick="addCompareTab()">+ Add Tab</button>
</div>
<div id="compare-tab-contents">
<!-- Compare tab contents will be dynamically added here -->
</div>
</div>
<!-- Code Generator Section -->
<div id="codegen-section" style="display: none;">
<div id="codegen-tabs-container" class="tabs">
<button class="add-tab-button" onclick="addCodegenTab()">+ Add Tab</button>
</div>
<div id="codegen-tab-contents">
<!-- CodeGen tab contents will be dynamically added here -->
</div>
</div>
</div>
<div id="${tabId}-error-preview" class="preview-section">
<div class="error-message"></div>
<!-- Shortcut Modal -->
<div id="shortcut-modal" class="modal">
<div class="modal-content">
<span class="close-modal" onclick="toggleShortcutModal()">&times;</span>
<h2>Keyboard Shortcuts</h2>
<ul>
<li><strong>Ctrl + T</strong>: New Formatter Tab (in Formatter mode)</li>
<li><strong>Ctrl + W</strong>: Close Current Formatter Tab (in Formatter mode)</li>
<li><strong>Ctrl + /</strong>: Show/Hide Shortcut Panel</li>
</ul>
</div>
</div>
</head>
<body>
<div class="container">
<div style="display: flex; align-items: center; margin-bottom: 20px;">
<h1>Generic JSON Tool</h1>
<button class="dark-mode-toggle" onclick="toggleDarkMode()">Toggle Dark Mode</button>
<button class="shortcut-preview-button" onclick="toggleShortcutModal()">Shortcuts</button>
</div>
<!-- Mode Selector -->
<div class="mode-selector" role="tablist" aria-label="JSON Tool Modes">
<button id="mode-formatter-btn" onclick="switchMode('formatter')" role="tab" aria-selected="true" aria-controls="formatter-section">JSON Formatter</button>
<button id="mode-compare-btn" onclick="switchMode('compare')">JSON Compare</button>
<button id="mode-codegen-btn" onclick="switchMode('codegen')">JSON to Code</button>
</div>
<!-- Formatter Section -->
<div id="formatter-section" role="tabpanel" aria-labelledby="mode-formatter-btn">
<div id="formatter-tabs-container" class="tabs">
<button class="add-tab-button" onclick="addFormatterTab()">+ Add Tab</button>
</div>
<div id="formatter-tab-contents">
<!-- Formatter tab contents will be dynamically added here -->
</div>
</div>
<!-- Compare Section -->
<div id="compare-section" style="display: none;">
<div id="compare-tabs-container" class="tabs">
<button class="add-tab-button" onclick="addCompareTab()">+ Add Tab</button>
</div>
<div id="compare-tab-contents">
<!-- Compare tab contents will be dynamically added here -->
</div>
</div>
<!-- Code Generator Section -->
<div id="codegen-section" style="display: none;">
<div id="codegen-tabs-container" class="tabs">
<button class="add-tab-button" onclick="addCodegenTab()">+ Add Tab</button>
</div>
<div id="codegen-tab-contents">
<!-- CodeGen tab contents will be dynamically added here -->
</div>
</div>
</div>
<!-- Shortcut Modal -->
<div id="shortcut-modal" class="modal" role="dialog" aria-labelledby="shortcut-modal-title" aria-modal="true">
<div class="modal-content">
<span class="close-modal" onclick="toggleShortcutModal()">&times;</span>
<h2 id="shortcut-modal-title">Keyboard Shortcuts</h2>
<ul>
<li><strong>Ctrl + T</strong>: New Formatter Tab (in Formatter mode)</li>
<li><strong>Ctrl + W</strong>: Close Current Formatter Tab (in Formatter mode)</li>
<li><strong>Ctrl + /</strong>: Show/Hide Shortcut Panel</li>
</ul>
</div>
</div>

Comment on lines +1094 to +1108
/* ========== Shortcut Modal & Dark Mode ========== */
function toggleShortcutModal() {
const modal = document.getElementById("shortcut-modal");
modal.style.display = modal.style.display === "block" ? "none" : "block";
}
function toggleDarkMode() {
document.body.classList.toggle("dark-mode");
saveGlobalState();
// If the Compare section is visible, update all diff previews
if (document.getElementById("compare-section").style.display !== "none") {
document.querySelectorAll("#compare-tab-contents .json-tab-content").forEach((tab) => {
compareJSONs(tab.id);
});
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance modal accessibility and dark mode behavior.

The modal and dark mode implementation could be improved with:

  1. Focus trap for modal accessibility.
  2. System dark mode preference support.

Apply these improvements:

+function createFocusTrap(element) {
+  const focusableElements = element.querySelectorAll(
+    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+  );
+  const firstFocusable = focusableElements[0];
+  const lastFocusable = focusableElements[focusableElements.length - 1];
+  
+  function trapFocus(e) {
+    if (e.key === "Tab") {
+      if (e.shiftKey && document.activeElement === firstFocusable) {
+        e.preventDefault();
+        lastFocusable.focus();
+      } else if (!e.shiftKey && document.activeElement === lastFocusable) {
+        e.preventDefault();
+        firstFocusable.focus();
+      }
+    }
+  }
+  
+  element.addEventListener("keydown", trapFocus);
+  firstFocusable.focus();
+  
+  return () => element.removeEventListener("keydown", trapFocus);
+}

 function toggleShortcutModal() {
   const modal = document.getElementById("shortcut-modal");
+  const isVisible = modal.style.display === "block";
+  modal.style.display = isVisible ? "none" : "block";
+  
+  if (!isVisible) {
+    const removeTrap = createFocusTrap(modal);
+    modal.setAttribute("data-focus-trap", "");
+    modal.removeTrap = removeTrap;
+  } else if (modal.removeTrap) {
+    modal.removeTrap();
+    modal.removeAttribute("data-focus-trap");
+    delete modal.removeTrap;
+  }
 }

+function initDarkMode() {
+  // Check system preference
+  const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
+  
+  function updateDarkMode(e) {
+    const shouldBeDark = e.matches;
+    document.body.classList.toggle("dark-mode", shouldBeDark);
+    saveGlobalState();
+  }
+  
+  prefersDark.addEventListener("change", updateDarkMode);
+  updateDarkMode(prefersDark);
+}

+// Call initDarkMode on load
+window.addEventListener("load", initDarkMode);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/* ========== Shortcut Modal & Dark Mode ========== */
function toggleShortcutModal() {
const modal = document.getElementById("shortcut-modal");
modal.style.display = modal.style.display === "block" ? "none" : "block";
}
function toggleDarkMode() {
document.body.classList.toggle("dark-mode");
saveGlobalState();
// If the Compare section is visible, update all diff previews
if (document.getElementById("compare-section").style.display !== "none") {
document.querySelectorAll("#compare-tab-contents .json-tab-content").forEach((tab) => {
compareJSONs(tab.id);
});
}
}
/* ========== Shortcut Modal & Dark Mode ========== */
function createFocusTrap(element) {
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
function trapFocus(e) {
if (e.key === "Tab") {
if (e.shiftKey && document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
element.addEventListener("keydown", trapFocus);
firstFocusable.focus();
return () => element.removeEventListener("keydown", trapFocus);
}
function toggleShortcutModal() {
const modal = document.getElementById("shortcut-modal");
const isVisible = modal.style.display === "block";
modal.style.display = isVisible ? "none" : "block";
if (!isVisible) {
const removeTrap = createFocusTrap(modal);
modal.setAttribute("data-focus-trap", "");
modal.removeTrap = removeTrap;
} else if (modal.removeTrap) {
modal.removeTrap();
modal.removeAttribute("data-focus-trap");
delete modal.removeTrap;
}
}
function toggleDarkMode() {
document.body.classList.toggle("dark-mode");
saveGlobalState();
// If the Compare section is visible, update all diff previews
if (document.getElementById("compare-section").style.display !== "none") {
document.querySelectorAll("#compare-tab-contents .json-tab-content")
.forEach((tab) => {
compareJSONs(tab.id);
});
}
}
function initDarkMode() {
// Check system preference
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
function updateDarkMode(e) {
const shouldBeDark = e.matches;
document.body.classList.toggle("dark-mode", shouldBeDark);
saveGlobalState();
}
prefersDark.addEventListener("change", updateDarkMode);
updateDarkMode(prefersDark);
}
// Call initDarkMode on load
window.addEventListener("load", initDarkMode);

Comment on lines +330 to +422
<script>
/* ========== Global Persistence Functions ========== */
function getActiveMode() {
if (document.getElementById("formatter-section").style.display !== "none") return "formatter";
if (document.getElementById("compare-section").style.display !== "none") return "compare";
if (document.getElementById("codegen-section").style.display !== "none") return "codegen";
return "formatter";
}

// Switch between tabs
function switchTab(tabId) {
document.querySelectorAll('.json-tab-content').forEach(tab => {
tab.classList.remove('active');
});
const selectedTab = document.getElementById(tabId);
if (selectedTab) {
selectedTab.classList.add('active');
}
document.querySelectorAll('.tab-button[data-tab]').forEach(button => {
button.classList.toggle('active', button.getAttribute('data-tab') === tabId);
});
saveState();
}
function saveGlobalState() {
const state = {
darkMode: document.body.classList.contains("dark-mode"),
activeMode: getActiveMode(),
formatter: {
activeTab: document.querySelector("#formatter-tab-contents .json-tab-content.active")?.id || "",
tabs: [],
},
compare: {
activeTab: document.querySelector("#compare-tab-contents .json-tab-content.active")?.id || "",
tabs: [],
},
codegen: {
activeTab: document.querySelector("#codegen-tab-contents .json-tab-content.active")?.id || "",
tabs: [],
},
};
// Formatter tabs
document.querySelectorAll("#formatter-tabs-container .tab-button[data-tab]").forEach((btn) => {
const tabId = btn.getAttribute("data-tab");
const name = btn.querySelector(".tab-name").textContent;
const color = btn.querySelector(".tab-color-picker")?.value || "#e0e0e0";
const content = document.querySelector("#" + tabId + " .json-input")?.value || "";
state.formatter.tabs.push({ id: tabId, name, color, content });
});
// Compare tabs
document.querySelectorAll("#compare-tabs-container .tab-button[data-tab]").forEach((btn) => {
const tabId = btn.getAttribute("data-tab");
const name = btn.querySelector(".tab-name").textContent;
const leftContent = document.querySelector("#" + tabId + " .json-input-left")?.value || "";
const rightContent = document.querySelector("#" + tabId + " .json-input-right")?.value || "";
state.compare.tabs.push({ id: tabId, name, leftContent, rightContent });
});
// Codegen tabs
document.querySelectorAll("#codegen-tabs-container .tab-button[data-tab]").forEach((btn) => {
const tabId = btn.getAttribute("data-tab");
const name = btn.querySelector(".tab-name").textContent;
const input = document.querySelector("#" + tabId + " .json-input")?.value || "";
const lang = document.getElementById("lang-select-" + tabId)?.value || "typescript";
state.codegen.tabs.push({ id: tabId, name, input, lang });
});
localStorage.setItem("jsonToolState", JSON.stringify(state));
}

/* ----------------------- Preview, Search, and JSON Functions ----------------------- */
function showPreviewTab(tabId, previewType) {
const previewSections = document.querySelectorAll(`#${tabId} .preview-section`);
previewSections.forEach(section => {
section.classList.toggle('active', section.id === `${tabId}-${previewType}-preview`);
});
// Update preview tab buttons inside the tab content
document.querySelectorAll(`#${tabId} .tabs .tab-button`).forEach(button => {
button.classList.toggle('active', button.textContent.toLowerCase().includes(previewType));
});
}
function loadGlobalState() {
const stateStr = localStorage.getItem("jsonToolState");
if (!stateStr) return;
const state = JSON.parse(stateStr);
// Dark Mode
if (state.darkMode) document.body.classList.add("dark-mode");
else document.body.classList.remove("dark-mode");
// Active Mode
switchMode(state.activeMode || "formatter");

function createTreeView(data, parentElement) {
parentElement.innerHTML = '';
function processNode(value, parent, key) {
const node = document.createElement('div');
node.className = 'tree-node';
if (typeof value === 'object' && value !== null) {
const keySpan = document.createElement('span');
keySpan.className = 'tree-key collapsed';
keySpan.textContent = key !== undefined ? key : (Array.isArray(value) ? `[${value.length}]` : `{${Object.keys(value).length}}`);
keySpan.onclick = () => {
keySpan.classList.toggle('collapsed');
keySpan.classList.toggle('expanded');
children.classList.toggle('hidden');
};
const children = document.createElement('div');
children.className = 'tree-children';
if (Array.isArray(value)) {
value.forEach((item, index) => {
processNode(item, children, index);
});
} else {
Object.entries(value).forEach(([k, v]) => {
processNode(v, children, k);
});
}
node.appendChild(keySpan);
node.appendChild(children);
parent.appendChild(node);
} else {
node.textContent = `${key}: ${value}`;
parent.appendChild(node);
}
}
processNode(data, parentElement);
}
// Load Formatter tabs
const ftc = document.getElementById("formatter-tabs-container");
ftc.querySelectorAll(".tab-button[data-tab]").forEach((btn) => btn.remove());
document.getElementById("formatter-tab-contents").innerHTML = "";
formatterTabCount = 0;
state.formatter.tabs.forEach((tabData) => {
createFormatterTab(tabData);
});
if (state.formatter.activeTab) switchFormatterTab(state.formatter.activeTab);

function updatePreview(tabId) {
const input = document.querySelector(`#${tabId} .json-input`).value;
const rawPreview = document.querySelector(`#${tabId} .raw-json`);
const errorMessage = document.querySelector(`#${tabId} .error-message`);
try {
const parsed = JSON.parse(input);
rawPreview.textContent = JSON.stringify(parsed, null, 2);
createTreeView(parsed, document.querySelector(`#${tabId} .tree-view`));
errorMessage.textContent = '';
showPreviewTab(tabId, 'raw');
} catch (e) {
errorMessage.textContent = `Error: ${e.message}`;
showPreviewTab(tabId, 'error');
}
saveState();
}
// Load Compare tabs
const ctc = document.getElementById("compare-tabs-container");
ctc.querySelectorAll(".tab-button[data-tab]").forEach((btn) => btn.remove());
document.getElementById("compare-tab-contents").innerHTML = "";
compareTabCount = 0;
state.compare.tabs.forEach((tabData) => {
createCompareTabWithData(tabData);
});
if (state.compare.activeTab) switchCompareTab(state.compare.activeTab);

function searchJSON(tabId) {
const searchInput = document.querySelector(`#${tabId} .search-input`).value.trim().toLowerCase();
const rawPreview = document.querySelector(`#${tabId} .raw-json`);
const treeView = document.querySelector(`#${tabId} .tree-view`);
// Remove previous highlights
document.querySelectorAll(`#${tabId} .highlight`).forEach(highlight => {
const parent = highlight.parentNode;
parent.replaceChild(document.createTextNode(highlight.textContent), highlight);
});
if (!searchInput) return;
const escapedSearch = searchInput.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedSearch})`, 'gi');
if (rawPreview.classList.contains('active')) {
const rawContent = rawPreview.textContent;
rawPreview.innerHTML = rawContent.replace(regex, '<span class="highlight">$1</span>');
}
if (treeView.classList.contains('active')) {
function highlightNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
const matches = node.nodeValue.match(regex);
if (matches) {
const span = document.createElement('span');
span.innerHTML = node.nodeValue.replace(regex, '<span class="highlight">$1</span>');
node.parentNode.replaceChild(span, node);
}
} else if (node.nodeType === Node.ELEMENT_NODE && node.childNodes) {
node.childNodes.forEach(child => highlightNode(child));
}
}
treeView.childNodes.forEach(child => highlightNode(child));
}
}

/* ----------------------- Tab Name & Color Editing ----------------------- */
// New function to open a tooltip for renaming the tab
function openTabRenameTooltip(tabId) {
const tabButton = document.querySelector(`.tab-button[data-tab="${tabId}"]`);
// Remove any existing tooltip
const existingTooltip = document.querySelector('.tab-rename-tooltip');
if (existingTooltip) {
existingTooltip.remove();
}
// Create tooltip element
const tooltip = document.createElement('div');
tooltip.className = 'tab-rename-tooltip';
// Position tooltip relative to the tab button
const rect = tabButton.getBoundingClientRect();
tooltip.style.left = `${rect.left}px`;
tooltip.style.top = `${rect.bottom + window.scrollY + 5}px`;

// Create input element
const input = document.createElement('input');
input.type = 'text';
input.value = tabButton.querySelector('.tab-name').textContent;
input.style.width = '150px';
// When Enter is pressed or the input loses focus, finalize the rename
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
finalizeRename();
} else if (e.key === 'Escape') {
tooltip.remove();
}
});
input.addEventListener('blur', finalizeRename);

tooltip.appendChild(input);
document.body.appendChild(tooltip);
input.focus();
// Load Codegen tabs
const cgtc = document.getElementById("codegen-tabs-container");
cgtc.querySelectorAll(".tab-button[data-tab]").forEach((btn) => btn.remove());
document.getElementById("codegen-tab-contents").innerHTML = "";
codegenTabCount = 0;
state.codegen.tabs.forEach((tabData) => {
createCodegenTabWithData(tabData);
});
if (state.codegen.activeTab) switchCodegenTab(state.codegen.activeTab);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling and versioning to state management.

The state management implementation needs improvements in error handling and future-proofing:

  1. Add try-catch blocks for localStorage operations.
  2. Implement size limit checks to prevent quota exceeded errors.
  3. Add state version for future schema updates.

Apply these changes to improve robustness:

 function saveGlobalState() {
+  try {
     const state = {
+      version: "1.0",
       darkMode: document.body.classList.contains("dark-mode"),
       // ... rest of the state
     };
-    localStorage.setItem("jsonToolState", JSON.stringify(state));
+    const stateStr = JSON.stringify(state);
+    if (stateStr.length > 5242880) { // 5MB limit
+      console.error("State too large to save");
+      return;
+    }
+    localStorage.setItem("jsonToolState", stateStr);
+  } catch (e) {
+    console.error("Failed to save state:", e);
+  }
 }

 function loadGlobalState() {
+  try {
     const stateStr = localStorage.getItem("jsonToolState");
     if (!stateStr) return;
     const state = JSON.parse(stateStr);
+    
+    // Version check for future schema updates
+    if (!state.version || state.version !== "1.0") {
+      console.warn("State version mismatch, resetting to defaults");
+      return;
+    }
     
     // ... rest of the loading logic
+  } catch (e) {
+    console.error("Failed to load state:", e);
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<script>
/* ========== Global Persistence Functions ========== */
function getActiveMode() {
if (document.getElementById("formatter-section").style.display !== "none") return "formatter";
if (document.getElementById("compare-section").style.display !== "none") return "compare";
if (document.getElementById("codegen-section").style.display !== "none") return "codegen";
return "formatter";
}
// Switch between tabs
function switchTab(tabId) {
document.querySelectorAll('.json-tab-content').forEach(tab => {
tab.classList.remove('active');
});
const selectedTab = document.getElementById(tabId);
if (selectedTab) {
selectedTab.classList.add('active');
}
document.querySelectorAll('.tab-button[data-tab]').forEach(button => {
button.classList.toggle('active', button.getAttribute('data-tab') === tabId);
});
saveState();
}
function saveGlobalState() {
const state = {
darkMode: document.body.classList.contains("dark-mode"),
activeMode: getActiveMode(),
formatter: {
activeTab: document.querySelector("#formatter-tab-contents .json-tab-content.active")?.id || "",
tabs: [],
},
compare: {
activeTab: document.querySelector("#compare-tab-contents .json-tab-content.active")?.id || "",
tabs: [],
},
codegen: {
activeTab: document.querySelector("#codegen-tab-contents .json-tab-content.active")?.id || "",
tabs: [],
},
};
// Formatter tabs
document.querySelectorAll("#formatter-tabs-container .tab-button[data-tab]").forEach((btn) => {
const tabId = btn.getAttribute("data-tab");
const name = btn.querySelector(".tab-name").textContent;
const color = btn.querySelector(".tab-color-picker")?.value || "#e0e0e0";
const content = document.querySelector("#" + tabId + " .json-input")?.value || "";
state.formatter.tabs.push({ id: tabId, name, color, content });
});
// Compare tabs
document.querySelectorAll("#compare-tabs-container .tab-button[data-tab]").forEach((btn) => {
const tabId = btn.getAttribute("data-tab");
const name = btn.querySelector(".tab-name").textContent;
const leftContent = document.querySelector("#" + tabId + " .json-input-left")?.value || "";
const rightContent = document.querySelector("#" + tabId + " .json-input-right")?.value || "";
state.compare.tabs.push({ id: tabId, name, leftContent, rightContent });
});
// Codegen tabs
document.querySelectorAll("#codegen-tabs-container .tab-button[data-tab]").forEach((btn) => {
const tabId = btn.getAttribute("data-tab");
const name = btn.querySelector(".tab-name").textContent;
const input = document.querySelector("#" + tabId + " .json-input")?.value || "";
const lang = document.getElementById("lang-select-" + tabId)?.value || "typescript";
state.codegen.tabs.push({ id: tabId, name, input, lang });
});
localStorage.setItem("jsonToolState", JSON.stringify(state));
}
/* ----------------------- Preview, Search, and JSON Functions ----------------------- */
function showPreviewTab(tabId, previewType) {
const previewSections = document.querySelectorAll(`#${tabId} .preview-section`);
previewSections.forEach(section => {
section.classList.toggle('active', section.id === `${tabId}-${previewType}-preview`);
});
// Update preview tab buttons inside the tab content
document.querySelectorAll(`#${tabId} .tabs .tab-button`).forEach(button => {
button.classList.toggle('active', button.textContent.toLowerCase().includes(previewType));
});
}
function loadGlobalState() {
const stateStr = localStorage.getItem("jsonToolState");
if (!stateStr) return;
const state = JSON.parse(stateStr);
// Dark Mode
if (state.darkMode) document.body.classList.add("dark-mode");
else document.body.classList.remove("dark-mode");
// Active Mode
switchMode(state.activeMode || "formatter");
function createTreeView(data, parentElement) {
parentElement.innerHTML = '';
function processNode(value, parent, key) {
const node = document.createElement('div');
node.className = 'tree-node';
if (typeof value === 'object' && value !== null) {
const keySpan = document.createElement('span');
keySpan.className = 'tree-key collapsed';
keySpan.textContent = key !== undefined ? key : (Array.isArray(value) ? `[${value.length}]` : `{${Object.keys(value).length}}`);
keySpan.onclick = () => {
keySpan.classList.toggle('collapsed');
keySpan.classList.toggle('expanded');
children.classList.toggle('hidden');
};
const children = document.createElement('div');
children.className = 'tree-children';
if (Array.isArray(value)) {
value.forEach((item, index) => {
processNode(item, children, index);
});
} else {
Object.entries(value).forEach(([k, v]) => {
processNode(v, children, k);
});
}
node.appendChild(keySpan);
node.appendChild(children);
parent.appendChild(node);
} else {
node.textContent = `${key}: ${value}`;
parent.appendChild(node);
}
}
processNode(data, parentElement);
}
// Load Formatter tabs
const ftc = document.getElementById("formatter-tabs-container");
ftc.querySelectorAll(".tab-button[data-tab]").forEach((btn) => btn.remove());
document.getElementById("formatter-tab-contents").innerHTML = "";
formatterTabCount = 0;
state.formatter.tabs.forEach((tabData) => {
createFormatterTab(tabData);
});
if (state.formatter.activeTab) switchFormatterTab(state.formatter.activeTab);
function updatePreview(tabId) {
const input = document.querySelector(`#${tabId} .json-input`).value;
const rawPreview = document.querySelector(`#${tabId} .raw-json`);
const errorMessage = document.querySelector(`#${tabId} .error-message`);
try {
const parsed = JSON.parse(input);
rawPreview.textContent = JSON.stringify(parsed, null, 2);
createTreeView(parsed, document.querySelector(`#${tabId} .tree-view`));
errorMessage.textContent = '';
showPreviewTab(tabId, 'raw');
} catch (e) {
errorMessage.textContent = `Error: ${e.message}`;
showPreviewTab(tabId, 'error');
}
saveState();
}
// Load Compare tabs
const ctc = document.getElementById("compare-tabs-container");
ctc.querySelectorAll(".tab-button[data-tab]").forEach((btn) => btn.remove());
document.getElementById("compare-tab-contents").innerHTML = "";
compareTabCount = 0;
state.compare.tabs.forEach((tabData) => {
createCompareTabWithData(tabData);
});
if (state.compare.activeTab) switchCompareTab(state.compare.activeTab);
function searchJSON(tabId) {
const searchInput = document.querySelector(`#${tabId} .search-input`).value.trim().toLowerCase();
const rawPreview = document.querySelector(`#${tabId} .raw-json`);
const treeView = document.querySelector(`#${tabId} .tree-view`);
// Remove previous highlights
document.querySelectorAll(`#${tabId} .highlight`).forEach(highlight => {
const parent = highlight.parentNode;
parent.replaceChild(document.createTextNode(highlight.textContent), highlight);
});
if (!searchInput) return;
const escapedSearch = searchInput.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedSearch})`, 'gi');
if (rawPreview.classList.contains('active')) {
const rawContent = rawPreview.textContent;
rawPreview.innerHTML = rawContent.replace(regex, '<span class="highlight">$1</span>');
}
if (treeView.classList.contains('active')) {
function highlightNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
const matches = node.nodeValue.match(regex);
if (matches) {
const span = document.createElement('span');
span.innerHTML = node.nodeValue.replace(regex, '<span class="highlight">$1</span>');
node.parentNode.replaceChild(span, node);
}
} else if (node.nodeType === Node.ELEMENT_NODE && node.childNodes) {
node.childNodes.forEach(child => highlightNode(child));
}
}
treeView.childNodes.forEach(child => highlightNode(child));
}
}
/* ----------------------- Tab Name & Color Editing ----------------------- */
// New function to open a tooltip for renaming the tab
function openTabRenameTooltip(tabId) {
const tabButton = document.querySelector(`.tab-button[data-tab="${tabId}"]`);
// Remove any existing tooltip
const existingTooltip = document.querySelector('.tab-rename-tooltip');
if (existingTooltip) {
existingTooltip.remove();
}
// Create tooltip element
const tooltip = document.createElement('div');
tooltip.className = 'tab-rename-tooltip';
// Position tooltip relative to the tab button
const rect = tabButton.getBoundingClientRect();
tooltip.style.left = `${rect.left}px`;
tooltip.style.top = `${rect.bottom + window.scrollY + 5}px`;
// Create input element
const input = document.createElement('input');
input.type = 'text';
input.value = tabButton.querySelector('.tab-name').textContent;
input.style.width = '150px';
// When Enter is pressed or the input loses focus, finalize the rename
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
finalizeRename();
} else if (e.key === 'Escape') {
tooltip.remove();
}
});
input.addEventListener('blur', finalizeRename);
tooltip.appendChild(input);
document.body.appendChild(tooltip);
input.focus();
// Load Codegen tabs
const cgtc = document.getElementById("codegen-tabs-container");
cgtc.querySelectorAll(".tab-button[data-tab]").forEach((btn) => btn.remove());
document.getElementById("codegen-tab-contents").innerHTML = "";
codegenTabCount = 0;
state.codegen.tabs.forEach((tabData) => {
createCodegenTabWithData(tabData);
});
if (state.codegen.activeTab) switchCodegenTab(state.codegen.activeTab);
}
<script>
/* ========== Global Persistence Functions ========== */
function getActiveMode() {
if (document.getElementById("formatter-section").style.display !== "none") return "formatter";
if (document.getElementById("compare-section").style.display !== "none") return "compare";
if (document.getElementById("codegen-section").style.display !== "none") return "codegen";
return "formatter";
}
function saveGlobalState() {
try {
const state = {
version: "1.0",
darkMode: document.body.classList.contains("dark-mode"),
activeMode: getActiveMode(),
formatter: {
activeTab: document.querySelector("#formatter-tab-contents .json-tab-content.active")?.id || "",
tabs: [],
},
compare: {
activeTab: document.querySelector("#compare-tab-contents .json-tab-content.active")?.id || "",
tabs: [],
},
codegen: {
activeTab: document.querySelector("#codegen-tab-contents .json-tab-content.active")?.id || "",
tabs: [],
},
};
// Formatter tabs
document.querySelectorAll("#formatter-tabs-container .tab-button[data-tab]").forEach((btn) => {
const tabId = btn.getAttribute("data-tab");
const name = btn.querySelector(".tab-name").textContent;
const color = btn.querySelector(".tab-color-picker")?.value || "#e0e0e0";
const content = document.querySelector("#" + tabId + " .json-input")?.value || "";
state.formatter.tabs.push({ id: tabId, name, color, content });
});
// Compare tabs
document.querySelectorAll("#compare-tabs-container .tab-button[data-tab]").forEach((btn) => {
const tabId = btn.getAttribute("data-tab");
const name = btn.querySelector(".tab-name").textContent;
const leftContent = document.querySelector("#" + tabId + " .json-input-left")?.value || "";
const rightContent = document.querySelector("#" + tabId + " .json-input-right")?.value || "";
state.compare.tabs.push({ id: tabId, name, leftContent, rightContent });
});
// Codegen tabs
document.querySelectorAll("#codegen-tabs-container .tab-button[data-tab]").forEach((btn) => {
const tabId = btn.getAttribute("data-tab");
const name = btn.querySelector(".tab-name").textContent;
const input = document.querySelector("#" + tabId + " .json-input")?.value || "";
const lang = document.getElementById("lang-select-" + tabId)?.value || "typescript";
state.codegen.tabs.push({ id: tabId, name, input, lang });
});
const stateStr = JSON.stringify(state);
if (stateStr.length > 5242880) { // 5MB limit
console.error("State too large to save");
return;
}
localStorage.setItem("jsonToolState", stateStr);
} catch (e) {
console.error("Failed to save state:", e);
}
}
function loadGlobalState() {
try {
const stateStr = localStorage.getItem("jsonToolState");
if (!stateStr) return;
const state = JSON.parse(stateStr);
// Version check for future schema updates
if (!state.version || state.version !== "1.0") {
console.warn("State version mismatch, resetting to defaults");
return;
}
// Dark Mode
if (state.darkMode) document.body.classList.add("dark-mode");
else document.body.classList.remove("dark-mode");
// Active Mode
switchMode(state.activeMode || "formatter");
// Load Formatter tabs
const ftc = document.getElementById("formatter-tabs-container");
ftc.querySelectorAll(".tab-button[data-tab]").forEach((btn) => btn.remove());
document.getElementById("formatter-tab-contents").innerHTML = "";
formatterTabCount = 0;
state.formatter.tabs.forEach((tabData) => {
createFormatterTab(tabData);
});
if (state.formatter.activeTab) switchFormatterTab(state.formatter.activeTab);
// Load Compare tabs
const ctc = document.getElementById("compare-tabs-container");
ctc.querySelectorAll(".tab-button[data-tab]").forEach((btn) => btn.remove());
document.getElementById("compare-tab-contents").innerHTML = "";
compareTabCount = 0;
state.compare.tabs.forEach((tabData) => {
createCompareTabWithData(tabData);
});
if (state.compare.activeTab) switchCompareTab(state.compare.activeTab);
// Load Codegen tabs
const cgtc = document.getElementById("codegen-tabs-container");
cgtc.querySelectorAll(".tab-button[data-tab]").forEach((btn) => btn.remove());
document.getElementById("codegen-tab-contents").innerHTML = "";
codegenTabCount = 0;
state.codegen.tabs.forEach((tabData) => {
createCodegenTabWithData(tabData);
});
if (state.codegen.activeTab) switchCodegenTab(state.codegen.activeTab);
} catch (e) {
console.error("Failed to load state:", e);
}
}
</script>

Comment on lines 442 to +622

function updateTabColor(tabId, colorValue) {
const tabButton = document.querySelector(`.tab-button[data-tab="${tabId}"]`);
const colorIndicator = tabButton.querySelector('.tab-color-indicator');
colorIndicator.style.backgroundColor = colorValue;
const colorPicker = tabButton.querySelector('.tab-color-picker');
colorPicker.value = colorValue;
saveState();
}
/* ========== Formatter Functions ========== */
let formatterTabCount = 0;
function addFormatterTab() {
createFormatterTab();
switchFormatterTab("formatterTab" + formatterTabCount);
saveGlobalState();
}
function createFormatterTab(tabData = null) {
formatterTabCount++;
const tabId = "formatterTab" + formatterTabCount;
// Create tab button
const tabButton = document.createElement("button");
tabButton.className = "tab-button";
tabButton.setAttribute("data-tab", tabId);
tabButton.onclick = () => switchFormatterTab(tabId);
tabButton.innerHTML = `<span class="tab-name">${tabData && tabData.name ? tabData.name : "Tab " + formatterTabCount}</span>
<input type="color" class="tab-color-picker" value="${tabData && tabData.color ? tabData.color : "#e0e0e0"}" onchange="updateFormatterTabColor('${tabId}', this.value)">
<span class="close-tab" onclick="closeFormatterTab('${tabId}', event)">×</span>`;
tabButton.addEventListener("dblclick", () => openTabRenameTooltip(tabId, "formatter"));
const tabsContainer = document.getElementById("formatter-tabs-container");
const addButton = tabsContainer.querySelector(".add-tab-button");
tabsContainer.insertBefore(tabButton, addButton);
// Create tab content
const tabContent = document.createElement("div");
tabContent.id = tabId;
tabContent.className = "json-tab-content";
tabContent.innerHTML = `
<textarea class="json-input" placeholder="Enter JSON here..."></textarea>
<div class="search-container">
<input type="text" class="search-input" placeholder="Search keys or values..." />
<button onclick="searchFormatterJSON('${tabId}')">Search</button>
</div>
<div class="upload-download-container">
<input type="file" class="upload-json" style="display:none" onchange="uploadFormatterJSON('${tabId}', this)">
<button onclick="document.querySelector('#${tabId} .upload-json').click()">Upload JSON</button>
<button onclick="downloadFormatterJSON('${tabId}')">Download JSON</button>
</div>
<div class="tabs">
<button class="tab-button active" onclick="showFormatterPreviewTab('${tabId}', 'raw')">Raw JSON</button>
<button class="tab-button" onclick="showFormatterPreviewTab('${tabId}', 'tree')">Tree View</button>
<button class="tab-button" onclick="showFormatterPreviewTab('${tabId}', 'error')">Errors</button>
</div>
<div id="${tabId}-raw-preview" class="preview-section active">
<pre class="raw-json"></pre>
</div>
<div id="${tabId}-tree-preview" class="preview-section">
<div class="tree-view"></div>
</div>
<div id="${tabId}-error-preview" class="preview-section">
<div class="error-message"></div>
</div>
`;
document.getElementById("formatter-tab-contents").appendChild(tabContent);
// Set content if provided
if (tabData && tabData.content) {
tabContent.querySelector(".json-input").value = tabData.content;
}
const textarea = tabContent.querySelector(".json-input");
textarea.addEventListener("paste", () => setTimeout(() => autoFormatTextarea(textarea), 100));
textarea.addEventListener("blur", () => autoFormatTextarea(textarea));
textarea.addEventListener("input", () => updateFormatterPreview(tabId));
updateFormatterPreview(tabId);
}
function switchFormatterTab(tabId) {
document.querySelectorAll("#formatter-tab-contents .json-tab-content").forEach((tab) => tab.classList.remove("active"));
const selectedTab = document.getElementById(tabId);
if (selectedTab) selectedTab.classList.add("active");
document.querySelectorAll("#formatter-tabs-container .tab-button[data-tab]").forEach((btn) => {
btn.classList.toggle("active", btn.getAttribute("data-tab") === tabId);
});
saveGlobalState();
}
function updateFormatterPreview(tabId) {
const tabContent = document.getElementById(tabId);
const textarea = tabContent.querySelector(".json-input");
const rawPreview = tabContent.querySelector(".raw-json");
const errorMessage = tabContent.querySelector(".error-message");
try {
const parsed = JSON.parse(textarea.value);
const formatted = JSON.stringify(parsed, null, 2);
rawPreview.textContent = formatted;
createTreeView(parsed, tabContent.querySelector(".tree-view"));
errorMessage.textContent = "";
showFormatterPreviewTab(tabId, "raw");
textarea.value = formatted;
} catch (e) {
errorMessage.textContent = "Error: " + e.message;
showFormatterPreviewTab(tabId, "error");
}
saveGlobalState();
}
function showFormatterPreviewTab(tabId, previewType) {
const tabContent = document.getElementById(tabId);
const previews = tabContent.querySelectorAll(".preview-section");
previews.forEach((section) => {
section.classList.toggle("active", section.id === `${tabId}-${previewType}-preview`);
});
const buttons = tabContent.querySelectorAll(".tabs .tab-button");
buttons.forEach((btn) => {
btn.classList.toggle("active", btn.textContent.toLowerCase().includes(previewType));
});
}
function searchFormatterJSON(tabId) {
const tabContent = document.getElementById(tabId);
const searchInput = tabContent.querySelector(".search-input").value.trim().toLowerCase();
const rawPreview = tabContent.querySelector(".raw-json");
const treeView = tabContent.querySelector(".tree-view");
tabContent.querySelectorAll(".highlight").forEach((el) => {
const parent = el.parentNode;
parent.replaceChild(document.createTextNode(el.textContent), el);
});
if (!searchInput) return;
const regex = new RegExp(`(${searchInput.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
if (rawPreview.classList.contains("active")) {
const content = rawPreview.textContent;
rawPreview.innerHTML = content.replace(regex, '<span class="highlight">$1</span>');
}
if (treeView.classList.contains("active")) {
function highlightNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
const matches = node.nodeValue.match(regex);
if (matches) {
const span = document.createElement("span");
span.innerHTML = node.nodeValue.replace(regex, '<span class="highlight">$1</span>');
node.parentNode.replaceChild(span, node);
}
} else if (node.nodeType === Node.ELEMENT_NODE && node.childNodes) {
node.childNodes.forEach((child) => highlightNode(child));
}
}
treeView.childNodes.forEach((child) => highlightNode(child));
}
saveGlobalState();
}
function updateFormatterTabColor(tabId, colorValue) {
// If needed, update visual indicators here.
saveGlobalState();
}
function uploadFormatterJSON(tabId, inputElement) {
if (inputElement.files && inputElement.files[0]) {
const file = inputElement.files[0];
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target.result;
const tabContent = document.getElementById(tabId);
const textarea = tabContent.querySelector(".json-input");
textarea.value = content;
updateFormatterPreview(tabId);
};
reader.readAsText(file);
inputElement.value = "";
}
}
function downloadFormatterJSON(tabId) {
const tabContent = document.getElementById(tabId);
const content = tabContent.querySelector(".json-input").value;
const blob = new Blob([content], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = tabId + ".json";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function closeFormatterTab(tabId, event) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
if (!confirm("Are you sure you want to close this tab?")) return;
const tabButton = document.querySelector(`#formatter-tabs-container .tab-button[data-tab="${tabId}"]`);
const tabContent = document.getElementById(tabId);
if (tabButton) tabButton.remove();
if (tabContent) tabContent.remove();
const remaining = document.querySelectorAll("#formatter-tab-contents .json-tab-content");
if (remaining.length > 0) switchFormatterTab(remaining[remaining.length - 1].id);
saveGlobalState();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize performance for large JSON files.

The formatter implementation needs improvements for handling large files:

  1. Add file size checks and chunked processing.
  2. Optimize search with worker threads.
  3. Debounce textarea input events.

Apply these optimizations:

+const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
+const DEBOUNCE_MS = 300;

+function debounce(func, wait) {
+  let timeout;
+  return function executedFunction(...args) {
+    const later = () => {
+      clearTimeout(timeout);
+      func(...args);
+    };
+    clearTimeout(timeout);
+    timeout = setTimeout(later, wait);
+  };
+}

 function uploadFormatterJSON(tabId, inputElement) {
   if (inputElement.files && inputElement.files[0]) {
     const file = inputElement.files[0];
+    if (file.size > MAX_FILE_SIZE) {
+      alert(`File too large. Maximum size is ${MAX_FILE_SIZE / 1024 / 1024}MB`);
+      return;
+    }
     // ... rest of the upload logic
   }
 }

 function createFormatterTab(tabData = null) {
   // ... existing code ...
   const textarea = tabContent.querySelector(".json-input");
-  textarea.addEventListener("input", () => updateFormatterPreview(tabId));
+  textarea.addEventListener("input", debounce(() => updateFormatterPreview(tabId), DEBOUNCE_MS));
   // ... rest of the code
 }

+// Web Worker for search
+const searchWorker = new Worker(URL.createObjectURL(new Blob([`
+  self.onmessage = function(e) {
+    const { text, searchTerm } = e.data;
+    const regex = new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
+    const matches = [];
+    let match;
+    while ((match = regex.exec(text)) !== null) {
+      matches.push({ index: match.index, length: match[0].length });
+    }
+    self.postMessage(matches);
+  };
+`], { type: "text/javascript" })));

 function searchFormatterJSON(tabId) {
   const tabContent = document.getElementById(tabId);
   const searchInput = tabContent.querySelector(".search-input").value.trim().toLowerCase();
   if (!searchInput) return;
   
+  const content = tabContent.querySelector(".json-input").value;
+  searchWorker.postMessage({ text: content, searchTerm: searchInput });
+  searchWorker.onmessage = function(e) {
+    const matches = e.data;
+    // Update UI with matches
+  };
 }

Committable suggestion skipped: line range outside the PR's diff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants