-
Notifications
You must be signed in to change notification settings - Fork 11
feat: add codegen and jsoncomp #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughThe 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
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()
Poem
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? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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:
- Move styles to a separate
.cssfile for better maintainability.- Use CSS variables for consistent theming (colors, spacing, etc.).
- 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:
- Event delegation for better performance.
- 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:
- Advanced diff algorithm for structural changes.
- Better visualization of differences.
- 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:
- Support for more languages (Java, C#, etc.).
- Code validation and formatting.
- 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:
- Virtual scrolling for large tree views.
- 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:
- More comprehensive shortcuts.
- 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); + } });
| </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()">×</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> |
There was a problem hiding this comment.
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:
- Add ARIA attributes for better screen reader support.
- 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.
| </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()">×</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()">×</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> |
| /* ========== 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); | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
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:
- Focus trap for modal accessibility.
- 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.
| /* ========== 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); |
| <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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling and versioning to state management.
The state management implementation needs improvements in error handling and future-proofing:
- Add try-catch blocks for localStorage operations.
- Implement size limit checks to prevent quota exceeded errors.
- 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.
| <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> |
|
|
||
| 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(); | ||
| } |
There was a problem hiding this comment.
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:
- Add file size checks and chunked processing.
- Optimize search with worker threads.
- 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.
Summary by CodeRabbit