diff --git a/README.md b/README.md
deleted file mode 100644
index 54a0825..0000000
--- a/README.md
+++ /dev/null
@@ -1,117 +0,0 @@
-# JSONP - Multi tab JSON toolkit
-
-
- A versatile web-based toolkit for working with JSON data. Open-source and works offline!
-
-
-
-
-
-
-## ๐ Overview
-
-JSONP is a powerful, browser-based JSON toolkit that helps developers work with JSON data more efficiently. It works completely offline and includes multiple tools for formatting, comparing, converting, and generating JSON data.
-
-## โจ Features
-
-### ๐งน JSON Formatter & Validator
-- Multi-tab support with color coding
-- Tree view with expandable nodes
-- Search functionality
-- Error highlighting
-- Real-time validation
-
-### ๐ JSON Comparison (Diff Checker)
-- Side-by-side diff view
-- Multi-tab support
-- Visual difference highlighting
-
-### ๐ป Code Generation
-- TypeScript interfaces
-- Python dataclasses
-- Go structs
-
-### ๐ Format Conversion
-- Python dictionary โ JSON converter
-- Preserves data types
-- Syntax highlighting
-
-### ๐งช Mock Data Generator
-- Faker.js integration
-- JSON/CSV export
-- Table/JSON view modes
-- Customizable schemas
-- Built-in presets
-
-### ๐ Markdown Editor
-- Real-time preview
-- Syntax highlighting
-- Multi-tab support
-
-## ๐ Getting Started
-
-1. **Quick Start**
- ```bash
- git clone https://github.com/shravan20/jsonp.git
- cd jsonp
- # Open index.html in your browser
- ```
-
-2. **Usage**
- - Open `index.html` in any modern browser
- - Drag & drop JSON files directly
- - Use tabs to organize multiple documents
- - Toggle between different tools using the sidebar
-
-## ๐ก Key Features
-
-- **Offline Support**: Works completely offline - no server needed
-- **Multi-tab Interface**: Work with multiple JSON documents simultaneously
-- **Dark/Light Mode**: Comfortable viewing in any environment
-- **Keyboard Shortcuts**: Efficient workflow with keyboard navigation
-- **Local Storage**: Auto-saves your work
-- **Mobile Responsive**: Works on all devices
-
-## ๐ ๏ธ Development
-
-```bash
-# Install dependencies
-npm install
-
-# Format code
-npm run format
-
-# Lint code
-npm run lint
-
-# Run checks
-npm run check
-```
-
-## ๐ค Contributing
-
-We welcome contributions! Please see:
-
-1. [Code of Conduct](./CODE_OF_CONDUCT.md)
-2. [Contributing Guidelines](./CONTRIBUTING.md)
-3. [Issue Templates](./.github/ISSUE_TEMPLATE)
-
-## ๐ Changelog
-
-See [CHANGELOG.md](./CHANGELOG.md) for a detailed history of changes.
-
-## ๐ License
-
-MIT Licensed - Free for personal and commercial use. See [LICENSE](./LICENSE) for details.
-
-## ๐ Links
-
-- [GitHub Repository](https://github.com/shravan20/jsonp)
-- [Report Issues](https://github.com/shravan20/jsonp/issues)
-- [Feature Requests](https://github.com/shravan20/jsonp/issues/new?template=feature_request.md)
-
----
-
-
- Made with โค๏ธ in India
-
diff --git a/index.css b/index.css
deleted file mode 100644
index f252904..0000000
--- a/index.css
+++ /dev/null
@@ -1,1221 +0,0 @@
-/* Complete index.css with improved alignment, spacing, theme handling, and UI components */
-
-/* Base Styles */
-body {
- font-family: Arial, sans-serif;
- margin: 0;
- padding: 20px;
- background-color: #f0f0f0;
- color: #333;
- transition: background-color 0.3s, color 0.3s;
-}
-
-.container {
- max-width: 1200px;
- margin: 0 auto;
- padding: 0 20px;
-}
-
-h1 {
- margin: 0 20px 0 0;
- font-size: 1.8rem;
-}
-
-.dark-mode-toggle,
-.shortcut-preview-button {
- padding: 8px 16px;
- background-color: #007bff;
- color: white;
- border: none;
- cursor: pointer;
- border-radius: 4px;
- margin-right: 10px;
- font-size: 0.9rem;
-}
-
-/* Mode Selector */
-.mode-selector {
- margin: 20px 0;
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
-}
-
-.mode-selector button {
- padding: 10px 16px;
- background-color: #007bff;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 0.9rem;
-}
-
-.mode-selector button.active {
- background-color: #0056b3;
- font-weight: bold;
-}
-
-/* Tabs */
-.tabs {
- display: flex;
- flex-wrap: wrap;
- gap: 5px;
- margin-bottom: 15px;
-}
-
-.tab-button {
- padding: 8px 12px;
- background-color: #e0e0e0;
- border: none;
- cursor: pointer;
- border-radius: 4px;
- display: flex;
- align-items: center;
- gap: 5px;
- transition: background-color 0.3s;
- font-size: 0.9rem;
-}
-
-.tab-button.active {
- background-color: #ffffff;
- font-weight: bold;
-}
-
-.tab-button .close-tab {
- color: red;
- font-weight: bold;
- cursor: pointer;
-}
-
-.tab-button .tab-color-picker {
- width: 16px;
- height: 16px;
- padding: 0;
- border: none;
- cursor: pointer;
-}
-
-.add-tab-button {
- background-color: #007bff;
- color: white;
- padding: 8px 16px;
- border-radius: 4px;
- cursor: pointer;
- border: none;
- font-size: 0.9rem;
-}
-
-/* Content Panels */
-.json-tab-content {
- display: none;
-}
-
-.json-tab-content.active {
- display: block;
-}
-
-textarea {
- width: 100%;
- height: 200px;
- padding: 10px;
- font-family: monospace;
- font-size: 0.95rem;
- border: 1px solid #ccc;
- border-radius: 4px;
- margin-bottom: 15px;
- box-sizing: border-box;
-}
-
-/* Utility Containers */
-.search-container,
-.upload-download-container {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 15px;
- flex-wrap: wrap;
-}
-/*Change made to make markdown preview text visible in dark mode*/
-body.dark-mode .toastui-editor-contents {
- color: #ffffff;
-}
-body.dark-mode .toastui-editor-contents * {
- color: #ffffff;
-}
-
-.preview-section {
- background-color: white;
- padding: 20px;
- border-radius: 5px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- overflow: auto;
- word-wrap: normal;
- display: none;
- max-height: 600px; /* Add max height */
-}
-
-.preview-section.active {
- display: block;
-}
-
-.copy-button,
-button.copy-button {
- padding: 6px 12px;
- background-color: #28a745;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 0.85rem;
-}
-
-.copy-button:hover {
- background-color: #218838;
-}
-
-/* Tree View */
-.tree-node {
- margin: 4px 0;
- padding-left: 20px;
- position: relative;
-}
-
-.tree-key {
- cursor: pointer;
- padding: 2px 5px;
- border-radius: 3px;
- display: inline-block;
-}
-
-.tree-key:hover {
- background-color: rgba(0, 123, 255, 0.1);
-}
-
-.type-string {
- color: #4caf50;
-}
-
-.type-number {
- color: #2196f3;
-}
-
-.type-boolean {
- color: #9c27b0;
-}
-
-.type-null {
- color: #666;
-}
-
-.type-object {
- color: #ff9800;
-}
-
-.type-array {
- color: #e91e63;
-}
-
-/* Dark Mode */
-body.dark-mode {
- background-color: #121212;
- color: #e0e0e0;
-}
-
-body.dark-mode textarea,
-body.dark-mode .preview-section,
-body.dark-mode .modal-content {
- background-color: #2e2e2e;
- color: #e0e0e0;
- border-color: #555;
-}
-
-body.dark-mode .tab-button {
- background-color: #333;
- color: #e0e0e0;
-}
-
-body.dark-mode .tab-button.active {
- background-color: #555;
-}
-
-/* Reordering Drag Styles */
-.tab-button.dragging {
- opacity: 0.6;
- border: 2px dashed #007bff;
-}
-
-.tab-button.drag-over {
- transform: translateY(2px) scale(1.02);
- box-shadow: 0 2px 6px rgba(0, 123, 255, 0.3);
-}
-
-/* Markdown & Modal */
-.markdown-preview {
- background: #fff;
- padding: 1rem;
- border-radius: 5px;
- border: 1px solid #ddd;
- max-height: 500px;
- overflow-y: auto;
- line-height: 1.6;
-}
-
-.markdown-preview code {
- background: #f0f0f0;
- padding: 2px 4px;
- border-radius: 3px;
-}
-
-.modal {
- display: none;
- position: fixed;
- z-index: 1000;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.5);
-}
-
-.modal-content {
- background: #fff;
- margin: 10% auto;
- padding: 20px;
- width: 400px;
- border-radius: 5px;
- position: relative;
-}
-
-.close-modal {
- position: absolute;
- top: 10px;
- right: 15px;
- font-size: 1.4rem;
- cursor: pointer;
-}
-
-/* Tab Rename Tooltip */
-.tab-rename-tooltip {
- position: absolute;
- z-index: 1000;
- background-color: #fff;
- border: 1px solid #ccc;
- border-radius: 3px;
- padding: 5px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
-}
-
-body.dark-mode .tab-rename-tooltip {
- background-color: #2e2e2e;
- color: #e0e0e0;
- border-color: #555;
-}
-
-/* GitHub buttons */
-.github-links {
- text-align: center;
- margin: 20px 0;
-}
-
-.github-button,
-.contribute-button {
- display: inline-flex;
- align-items: center;
- padding: 10px 15px;
- margin: 0 10px;
- border-radius: 5px;
- text-decoration: none;
- color: white;
- font-size: 0.9rem;
-}
-
-.github-button {
- background-color: #24292e;
-}
-
-.github-button:hover {
- background-color: #444;
-}
-
-.contribute-button {
- background-color: #2ea44f;
-}
-
-.contribute-button:hover {
- background-color: #22863a;
-}
-
-.github-button i,
-.contribute-button i {
- margin-right: 8px;
-}
-
-.tree-children {
- margin-left: 20px;
- overflow: hidden;
- transition: max-height 0.3s ease, opacity 0.3s ease;
- max-height: 1000px;
- opacity: 1;
-}
-
-.tree-children.collapsed {
- max-height: 0;
- opacity: 0;
- pointer-events: none;
-}
-
-.tree-key {
- position: relative;
- cursor: pointer;
- padding: 4px 6px;
- display: inline-block;
- border-radius: 3px;
- transition: background-color 0.2s ease, box-shadow 0.2s ease;
- outline: none;
-}
-
-.tree-key:hover,
-.tree-key:focus {
- background-color: rgba(0, 123, 255, 0.12);
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
-}
-
-.tree-key:active {
- background-color: rgba(0, 123, 255, 0.25);
-}
-
-.tree-key::before {
- content: "\25B6";
- display: inline-block;
- transition: transform 0.2s ease;
- margin-right: 6px;
- font-size: 0.8em;
- color: #555;
-}
-
-.tree-key.expanded::before {
- transform: rotate(90deg);
- color: #007bff;
-}
-
-.tree-key .node-info {
- font-size: 0.8em;
- color: #999;
- margin-left: 6px;
-}
-
-.tree-key[title] {
- position: relative;
-}
-
-.tree-key[title]:hover::after {
- content: attr(title);
- position: absolute;
- top: 100%;
- left: 0;
- background: rgba(0, 0, 0, 0.75);
- color: #fff;
- font-size: 12px;
- padding: 4px 6px;
- border-radius: 4px;
- white-space: nowrap;
- margin-top: 4px;
- z-index: 10;
- pointer-events: none;
-}
-
-body.dark-mode .tree-key[title]:hover::after {
- background: rgba(255, 255, 255, 0.1);
- color: #e0e0e0;
- border: 1px solid #666;
-}
-
-/* Accessibility: keyboard focus outline */
-.tree-key:focus-visible {
- outline: 2px solid #007bff;
- outline-offset: 2px;
-}
-
-/* Shift-click recursive expand/collapse (JS logic must add 'recursive' class to target for styling if needed) */
-.tree-key.shift-toggle {
- background-color: rgba(0, 123, 255, 0.08);
-}
-
-/* Enhanced index.css with animation for tree expand/collapse, dropdown arrow, tooltip support, hover/click effects, keyboard accessibility, child count, and shift-click support */
-
-/* JSON to Code preview themes */
-.code-output {
- background-color: #1e1e1e;
- color: #d4d4d4;
- font-family: "Fira Code", monospace;
- font-size: 14px;
- line-height: 1.6;
- padding: 16px;
- border-radius: 6px;
- overflow-x: auto;
- white-space: pre;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- margin-top: 10px;
-}
-
-.code-output.python {
- background-color: #0b2239;
- color: #dcdcdc;
-}
-
-.code-output.go {
- background-color: #263238;
- color: #c3e88d;
-}
-
-.code-output.typescript {
- background-color: #1e1e1e;
- color: #d4d4d4;
-}
-
-.code-output .keyword {
- color: #569cd6;
- font-weight: bold;
-}
-
-.code-output .string {
- color: #ce9178;
-}
-
-.code-output .number {
- color: #b5cea8;
-}
-
-.code-output .type {
- color: #4ec9b0;
- font-style: italic;
-}
-
-.code-output .comment {
- color: #6a9955;
- font-style: italic;
-}
-
-.tree-children {
- margin-left: 20px;
- overflow: hidden;
- transition: max-height 0.3s ease, opacity 0.3s ease;
- max-height: 1000px;
- opacity: 1;
-}
-
-.tree-children.collapsed {
- max-height: 0;
- opacity: 0;
- pointer-events: none;
-}
-
-.tree-key {
- position: relative;
- cursor: pointer;
- padding: 4px 6px;
- display: inline-block;
- border-radius: 3px;
- transition: background-color 0.2s ease, box-shadow 0.2s ease;
- outline: none;
-}
-
-.tree-key:hover,
-.tree-key:focus {
- background-color: rgba(0, 123, 255, 0.12);
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
-}
-
-.tree-key:active {
- background-color: rgba(0, 123, 255, 0.25);
-}
-
-.tree-key::before {
- content: "\25B6";
- display: inline-block;
- transition: transform 0.2s ease;
- margin-right: 6px;
- font-size: 0.8em;
- color: #555;
-}
-
-.tree-key.expanded::before {
- transform: rotate(90deg);
- color: #007bff;
-}
-
-.tree-key .node-info {
- font-size: 0.8em;
- color: #999;
- margin-left: 6px;
-}
-
-.tree-key[title] {
- position: relative;
-}
-
-.tree-key[title]:hover::after {
- content: attr(title);
- position: absolute;
- top: 100%;
- left: 0;
- background: rgba(0, 0, 0, 0.75);
- color: #fff;
- font-size: 12px;
- padding: 4px 6px;
- border-radius: 4px;
- white-space: nowrap;
- margin-top: 4px;
- z-index: 10;
- pointer-events: none;
-}
-
-body.dark-mode .tree-key[title]:hover::after {
- background: rgba(255, 255, 255, 0.1);
- color: #e0e0e0;
- border: 1px solid #666;
-}
-
-/* Accessibility: keyboard focus outline */
-.tree-key:focus-visible {
- outline: 2px solid #007bff;
- outline-offset: 2px;
-}
-
-/* Shift-click recursive expand/collapse (JS logic must add 'recursive' class to target for styling if needed) */
-.tree-key.shift-toggle {
- background-color: rgba(0, 123, 255, 0.08);
-}
-
-/* Editor Specific */
-#editor-section .tab-button {
- padding: 8px 12px;
- background-color: #f5f5f5;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- margin-right: 5px;
- font-size: 0.9rem;
-}
-
-#editor-section .tab-button.active {
- background-color: #ffffff;
- font-weight: bold;
-}
-
-#editor-section .add-tab-button {
- padding: 8px 16px;
- background-color: #007bff;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 0.9rem;
-}
-
-#editor-tab-contents .json-tab-content {
- display: none;
- margin-top: 10px;
-}
-
-#editor-tab-contents .json-tab-content.active {
- display: block;
-}
-
-.toastui-editor-defaultUI {
- border-radius: 6px;
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
-}
-
-/* Dark Mode */
-/* Dark mode enhancements for toastui Editor */
-body.dark-mode .toastui-editor-defaultUI {
- background-color: #1e1e1e !important;
- color: #eee !important;
- border-color: #333 !important;
-}
-
-body.dark-mode .toastui-editor-defaultUI .ProseMirror {
- background-color: #1e1e1e !important;
- color: #eee !important;
-}
-
-body.dark-mode .toastui-editor-defaultUI .toastui-editor-toolbar {
- background-color: #2a2a2a !important;
- border-bottom: 1px solid #444 !important;
-}
-
-body.dark-mode .toastui-editor-defaultUI .toastui-editor-contents pre,
-body.dark-mode .toastui-editor-defaultUI .toastui-editor-contents code {
- background-color: #2b2b2b !important;
- color: #c5c5c5 !important;
-}
-
-body.dark-mode .toastui-editor-defaultUI .toastui-editor-md-container,
-body.dark-mode .toastui-editor-defaultUI .toastui-editor-ww-container {
- background-color: #1e1e1e !important;
- color: #eee !important;
-}
-
-/* Tooltip/input styling */
-body.dark-mode .tab-rename-tooltip {
- background-color: #2c2c2c;
- border: 1px solid #555;
- color: #fff;
- padding: 4px;
- z-index: 1000;
- position: absolute;
- border-radius: 4px;
-}
-
-body.dark-mode .tab-rename-tooltip input {
- background-color: #1f1f1f;
- color: #eee;
- border: 1px solid #555;
-}
-
-/* Active tab indicator */
-body.dark-mode .tab-button.active {
- background-color: #333;
- color: #fff;
-}
-
-/* Tree View Scrolling Fix */
-.preview-section {
- background-color: white;
- padding: 20px;
- border-radius: 5px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- overflow: auto;
- word-wrap: normal;
- display: none;
- max-height: 600px; /* Add max height */
-}
-
-.preview-section.active {
- display: block;
-}
-
-.tree-view {
- padding: 10px;
- font-family: monospace;
- line-height: 1.5;
- overflow-y: auto; /* Ensure vertical scrolling */
- overflow-x: auto; /* Handle horizontal overflow */
-}
-
-/* Enhance tree node spacing and structure */
-.tree-node {
- margin: 4px 0;
- padding-left: 20px;
- position: relative;
-}
-
-/* Dark mode adjustments */
-body.dark-mode .preview-section {
- background-color: #1e1e1e;
- border: 1px solid #333;
-}
-
-body.dark-mode .tree-view {
- color: #e0e0e0;
-}
-
-/* Scrollbar styling for better visibility */
-.tree-view::-webkit-scrollbar {
- width: 12px;
- height: 12px;
-}
-
-.tree-view::-webkit-scrollbar-track {
- background: #f1f1f1;
- border-radius: 6px;
-}
-
-.tree-view::-webkit-scrollbar-thumb {
- background: #888;
- border-radius: 6px;
- border: 3px solid #f1f1f1;
-}
-
-.tree-view::-webkit-scrollbar-thumb:hover {
- background: #555;
-}
-
-/* Dark mode scrollbar */
-body.dark-mode .tree-view::-webkit-scrollbar-track {
- background: #1e1e1e;
-}
-
-body.dark-mode .tree-view::-webkit-scrollbar-thumb {
- background: #666;
- border: 3px solid #1e1e1e;
-}
-
-body.dark-mode .tree-view::-webkit-scrollbar-thumb:hover {
- background: #888;
-}
-
-/* New App Layout */
-.app-container {
- display: flex;
- min-height: 100vh;
- background-color: #f0f0f0;
-}
-
-/* Sidebar Styles */
-.sidebar {
- width: 250px;
- background-color: #252526;
- color: #ffffff;
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
-}
-
-.sidebar-header {
- padding: 15px;
- border-bottom: 1px solid #3c3c3c;
-}
-
-.sidebar-header h3 {
- margin: 0;
- font-size: 1.1rem;
- color: #cccccc;
-}
-
-.sidebar-content {
- flex: 1;
- overflow-y: auto;
-}
-
-.feature-group {
- padding: 10px 0;
-}
-
-.feature-item {
- padding: 8px 15px;
- display: flex;
- align-items: center;
- gap: 10px;
- cursor: pointer;
- transition: background-color 0.2s;
- color: #cccccc;
-}
-
-.feature-item:hover {
- background-color: #2a2d2e;
-}
-
-.feature-item.active {
- background-color: #37373d;
- color: #ffffff;
-}
-
-.feature-item i {
- width: 16px;
- text-align: center;
-}
-
-/* Main Content Area */
-.main-content {
- flex: 1;
- padding: 20px;
- background-color: #f0f0f0;
- overflow-y: auto;
-}
-
-.header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
-}
-
-.header-actions {
- display: flex;
- gap: 10px;
-}
-
-/* Dark Mode Adjustments */
-body.dark-mode .app-container {
- background-color: #1e1e1e;
-}
-
-body.dark-mode .main-content {
- background-color: #1e1e1e;
-}
-
-body.dark-mode .sidebar {
- background-color: #252526;
- border-right: 1px solid #3c3c3c;
-}
-
-/* Mobile Sidebar Toggle */
-.mobile-sidebar-toggle {
- display: none;
- position: fixed;
- top: 10px;
- left: 10px;
- z-index: 1000;
- background: #252526;
- color: white;
- border: none;
- border-radius: 4px;
- padding: 10px;
- cursor: pointer;
-}
-
-.mobile-sidebar-close {
- display: none;
- background: transparent;
- border: none;
- color: #cccccc;
- cursor: pointer;
- padding: 5px;
-}
-
-/* Responsive Styles */
-@media (max-width: 768px) {
- body {
- padding: 10px;
- }
-
- .app-container {
- flex-direction: column;
- }
-
- .mobile-sidebar-toggle {
- display: block;
- }
-
- .mobile-sidebar-close {
- display: block;
- position: absolute;
- right: 10px;
- top: 50%;
- transform: translateY(-50%);
- }
-
- .sidebar {
- position: fixed;
- left: -250px;
- top: 0;
- bottom: 0;
- z-index: 1000;
- transition: left 0.3s ease;
- }
-
- .sidebar.active {
- left: 0;
- }
-
- .main-content {
- margin-left: 0;
- padding: 15px;
- padding-top: 50px;
- }
-
- .header {
- flex-direction: column;
- align-items: flex-start;
- gap: 15px;
- }
-
- .header h1 {
- font-size: 1.5rem;
- margin-right: 0;
- }
-
- /* Adjust tabs layout */
- .tabs {
- flex-wrap: nowrap;
- overflow-x: auto;
- padding-bottom: 5px;
- -webkit-overflow-scrolling: touch;
- }
-
- .tab-button {
- flex-shrink: 0;
- }
-
- /* Adjust form controls */
- .search-container,
- .upload-download-container {
- flex-direction: column;
- align-items: stretch;
- }
-
- .search-container input,
- .search-container button,
- .upload-download-container button {
- width: 100%;
- margin-bottom: 10px;
- }
-
- /* Adjust JSON compare layout */
- #compare-section [style*="display:flex"] {
- flex-direction: column !important;
- }
-
- #compare-section .json-input-left,
- #compare-section .json-input-right {
- width: 100% !important;
- margin-bottom: 10px;
- }
-
- /* Adjust mock data generator layout */
- #mockgen-section .mock-preview-table {
- display: block;
- overflow-x: auto;
- white-space: nowrap;
- }
-
- /* Adjust markdown editor */
- .toastui-editor-defaultUI {
- flex-direction: column !important;
- }
-
- .toastui-editor-defaultUI .toastui-editor-md-container,
- .toastui-editor-defaultUI .toastui-editor-ww-container {
- width: 100% !important;
- }
-}
-
-/* Small mobile devices */
-@media (max-width: 480px) {
- .header-actions {
- flex-direction: column;
- width: 100%;
- }
-
- .header-actions button {
- width: 100%;
- }
-
- .github-links {
- flex-direction: column;
- gap: 10px;
- }
-
- .github-links a {
- width: 100%;
- justify-content: center;
- }
-}
-
-/* Tablet adjustments */
-@media (min-width: 769px) and (max-width: 1024px) {
- .sidebar {
- width: 200px;
- }
-
- .main-content {
- margin-left: 200px;
- }
-}
-
-/* Touch device optimizations */
-@media (hover: none) {
- .feature-item:hover {
- background-color: transparent;
- }
-
- .feature-item:active {
- background-color: #2a2d2e;
- }
-
- .tab-button:hover {
- background-color: transparent;
- }
-
- .tab-button:active {
- background-color: #e0e0e0;
- }
-}
-
-/* Dark mode adjustments for mobile */
-body.dark-mode .mobile-sidebar-toggle {
- background: #333;
- color: #fff;
-}
-
-body.dark-mode .mobile-sidebar-close {
- color: #fff;
-}
-
-/* Improve scrollbar appearance on mobile */
-@media (max-width: 768px) {
- ::-webkit-scrollbar {
- width: 6px;
- height: 6px;
- }
-
- ::-webkit-scrollbar-thumb {
- background: rgba(0, 0, 0, 0.2);
- border-radius: 3px;
- }
-
- body.dark-mode ::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.2);
- }
-}
-
-/* Mock Data Table Styling */
-.mock-preview-table {
- width: 100%;
- border-collapse: collapse;
- margin: 20px 0;
- background-color: #fff;
- border-radius: 8px;
- overflow: hidden;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-}
-
-.mock-preview-table thead {
- background-color: #f8f9fa;
- border-bottom: 2px solid #dee2e6;
-}
-
-.mock-preview-table th {
- padding: 12px 15px;
- text-align: left;
- font-weight: 600;
- font-size: 0.9rem;
- color: #495057;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- border-bottom: 2px solid #dee2e6;
- position: sticky;
- top: 0;
- background-color: #f8f9fa;
- z-index: 1;
-}
-
-.mock-preview-table td {
- padding: 12px 15px;
- border-bottom: 1px solid #dee2e6;
- font-size: 0.9rem;
- color: #212529;
- max-width: 300px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.mock-preview-table tbody tr:hover {
- background-color: #f8f9fa;
-}
-
-.mock-preview-table tbody tr:last-child td {
- border-bottom: none;
-}
-
-/* Error row styling */
-.mock-preview-table tr.error td {
- background-color: #fff3f3;
- color: #dc3545;
-}
-
-/* Dark mode support */
-body.dark-mode .mock-preview-table {
- background-color: #2d2d2d;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
-}
-
-body.dark-mode .mock-preview-table thead {
- background-color: #333;
- border-bottom-color: #444;
-}
-
-body.dark-mode .mock-preview-table th {
- color: #e0e0e0;
- background-color: #333;
- border-bottom-color: #444;
-}
-
-body.dark-mode .mock-preview-table td {
- color: #e0e0e0;
- border-bottom-color: #444;
-}
-
-body.dark-mode .mock-preview-table tbody tr:hover {
- background-color: #383838;
-}
-
-body.dark-mode .mock-preview-table tr.error td {
- background-color: #442326;
- color: #ff6b6b;
-}
-
-/* Responsive table */
-@media (max-width: 768px) {
- #mockgen-section .mock-preview-table {
- display: block;
- overflow-x: auto;
- -webkit-overflow-scrolling: touch;
- }
-
- .mock-preview-table thead {
- display: block;
- }
-
- .mock-preview-table tbody {
- display: block;
- }
-
- .mock-preview-table th,
- .mock-preview-table td {
- min-width: 120px; /* Ensure minimum column width */
- }
-
- /* Add horizontal scroll indicator */
- .mock-preview-table::after {
- content: "โท";
- position: absolute;
- bottom: 10px;
- right: 10px;
- background: rgba(0, 0, 0, 0.5);
- color: white;
- padding: 5px 10px;
- border-radius: 20px;
- font-size: 14px;
- opacity: 0.7;
- pointer-events: none;
- }
-
- body.dark-mode .mock-preview-table::after {
- background: rgba(255, 255, 255, 0.2);
- }
-}
-
-/* Container for the table */
-#mock-output-container {
- position: relative;
- margin-top: 20px;
- border-radius: 8px;
- overflow: hidden;
-}
-
-.preset-select {
- margin-left: 10px;
- padding: 5px 10px;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- background-color: var(--bg-color);
- color: var(--text-color);
- font-size: 14px;
- cursor: pointer;
- transition: border-color 0.2s;
-}
-
-.preset-select:hover {
- border-color: var(--primary-color);
-}
-
-.preset-select:focus {
- outline: none;
- border-color: var(--primary-color);
- box-shadow: 0 0 0 2px var(--primary-color-light);
-}
diff --git a/index.js b/index.js
deleted file mode 100644
index d70ca1c..0000000
--- a/index.js
+++ /dev/null
@@ -1,1890 +0,0 @@
-/* ========== Global Persistence Functions ========== */
-function getActiveMode() {
- // Find the active feature item in the sidebar
- const activeFeature = document.querySelector(".feature-item.active");
- if (activeFeature) {
- // Extract mode from the onclick attribute
- const onclickAttr = activeFeature.getAttribute("onclick");
- const match = onclickAttr.match(/switchMode\('(.+?)'\)/);
- if (match) return match[1];
- }
-
- // Fallback to checking sections
- 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";
- if (document.getElementById("convert-section").style.display !== "none") return "convert";
- if (document.getElementById("mockgen-section").style.display !== "none") return "mockgen";
- if (document.getElementById("editor-section").style.display !== "none") return "editor";
- return "formatter"; // Default fallback
-}
-
-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: [],
- activeFeatureTab: {}, // Store active feature tab (Raw/Tree/Error) for each formatter tab
- },
- 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));
-}
-
-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 Feature Mode (Formatter/Compare/Codegen/etc.)
- if (state.activeMode) {
- switchMode(state.activeMode);
- } else {
- switchMode("formatter"); // Default fallback
- }
-
- // 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);
-
- enableTabReordering("formatter-tabs-container");
- enableTabReordering("compare-tabs-container");
- enableTabReordering("codegen-tabs-container");
-}
-
-/* ========== COPY FUNCTIONS ========== */
-function copyRawJSON(tabId) {
- const rawPre = document.querySelector(`#${tabId}-raw-preview .raw-json`);
- copyToClipboard(rawPre.textContent, "JSON copied to clipboard");
-}
-
-function copyCompareLeft(tabId) {
- const leftTA = document.querySelector(`#${tabId} .json-input-left`);
- copyToClipboard(leftTA.value, "Left JSON copied");
-}
-
-function copyCompareRight(tabId) {
- const rightTA = document.querySelector(`#${tabId} .json-input-right`);
- copyToClipboard(rightTA.value, "Right JSON copied");
-}
-
-function copyCodeOutput(tabId) {
- const codePre = document.querySelector(`#${tabId} .code-output`);
- copyToClipboard(codePre.textContent, "Code copied");
-}
-
-function copyToClipboard(text, successMessage) {
- navigator.clipboard
- .writeText(text)
- .then(() => {
- Swal.fire({
- icon: "success",
- title: successMessage,
- toast: true,
- timer: 2000,
- position: "top-end",
- showConfirmButton: false,
- });
- })
- .catch((err) => {
- console.error("Failed to copy:", err);
- Swal.fire({
- icon: "error",
- title: "Copy failed",
- toast: true,
- timer: 2000,
- position: "top-end",
- showConfirmButton: false,
- });
- });
-}
-
-/* ========== Mode Selector ========== */
-function switchMode(mode) {
- const sections = ["formatter", "compare", "codegen", "convert", "mockgen", "editor"];
- sections.forEach((s) => {
- const section = document.getElementById(`${s}-section`);
- if (section) {
- section.style.display = "none";
- }
- });
-
- const targetSection = document.getElementById(`${mode}-section`);
- if (targetSection) {
- targetSection.style.display = "block";
- }
-
- // Update sidebar active state
- document.querySelectorAll(".feature-item").forEach((item) => {
- item.classList.remove("active");
- });
- document.querySelector(`.feature-item[onclick*="${mode}"]`)?.classList.add("active");
-
- if (mode === "mockgen") {
- renderMockgenDocs();
- } else if (mode === "editor") {
- loadEditorGlobalState();
- }
-
- applyEditorTabDarkMode();
- saveGlobalState(); // Save state when mode changes
-}
-
-/* ========== 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);
-
- const tabName = tabData?.name || "Tab " + formatterTabCount;
- const tabColor = tabData?.color || "#e0e0e0";
-
- tabButton.innerHTML = `${tabName}
-
- ร`;
-
- tabButton.addEventListener("dblclick", () => openTabRenameTooltip(tabId, "formatter"));
-
- const tabsContainer = document.getElementById("formatter-tabs-container");
- const addButton = tabsContainer.querySelector(".add-tab-button");
- if (tabsContainer && addButton) {
- tabsContainer.insertBefore(tabButton, addButton);
- }
- // Create tab content
- const tabContent = document.createElement("div");
- tabContent.id = tabId;
- tabContent.className = "json-tab-content";
- tabContent.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
- document.getElementById("formatter-tab-contents").appendChild(tabContent);
- // Set content if provided
- if (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);
- enableTabReordering("formatter-tabs-container");
- applyTabButtonTheme();
-}
-
-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");
- }
- document.querySelectorAll(".tree-key").forEach((key) => {
- key.addEventListener("focus", () => {
- key.scrollIntoView({ block: "nearest", behavior: "smooth" });
- });
- });
- 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, '$1');
- }
- 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, '$1');
- 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?.[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);
-}
-
-async function closeFormatterTab(tabId, event) {
- if (event) {
- event.stopPropagation();
- event.preventDefault();
- }
-
- const result = await Swal.fire({
- title: "Close tab?",
- text: "Are you sure you want to close this tab?",
- icon: "warning",
- showCancelButton: true,
- confirmButtonText: "Yes",
- cancelButtonText: "No",
- });
-
- if (!result.isConfirmed) 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();
-}
-
-/* ========== Compare Functions ========== */
-let compareTabCount = 0;
-
-function addCompareTab() {
- createCompareTab();
- switchCompareTab("compareTab" + compareTabCount);
- saveGlobalState();
-}
-
-function createCompareTab() {
- compareTabCount++;
- const tabId = "compareTab" + compareTabCount;
- // Create tab button
- const tabButton = document.createElement("button");
- tabButton.className = "tab-button";
- tabButton.setAttribute("data-tab", tabId);
- tabButton.onclick = () => switchCompareTab(tabId);
- tabButton.innerHTML = `Tab ${compareTabCount}
- ร`;
- tabButton.addEventListener("dblclick", () => openTabRenameTooltip(tabId, "compare"));
- const tabsContainer = document.getElementById("compare-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 = `
-
-
-
-
-
-
- `;
- document.getElementById("compare-tab-contents").appendChild(tabContent);
- const leftTA = tabContent.querySelector(".json-input-left");
- const rightTA = tabContent.querySelector(".json-input-right");
- leftTA.addEventListener("paste", () => setTimeout(() => autoFormatTextarea(leftTA), 100));
- leftTA.addEventListener("blur", () => autoFormatTextarea(leftTA));
- rightTA.addEventListener("paste", () => setTimeout(() => autoFormatTextarea(rightTA), 100));
- rightTA.addEventListener("blur", () => autoFormatTextarea(rightTA));
- saveGlobalState();
- enableTabReordering("compare-tabs-container");
-}
-// Create Compare tab using saved data
-function createCompareTabWithData(tabData) {
- compareTabCount++;
- const tabId = tabData.id;
- const tabButton = document.createElement("button");
- tabButton.className = "tab-button";
- tabButton.setAttribute("data-tab", tabId);
- tabButton.onclick = () => switchCompareTab(tabId);
- tabButton.innerHTML = `${tabData.name}
- ร`;
- tabButton.addEventListener("dblclick", () => openTabRenameTooltip(tabId, "compare"));
- const tabsContainer = document.getElementById("compare-tabs-container");
- const addButton = tabsContainer.querySelector(".add-tab-button");
- tabsContainer.insertBefore(tabButton, addButton);
- const tabContent = document.createElement("div");
- tabContent.id = tabId;
- tabContent.className = "json-tab-content";
- tabContent.innerHTML = `
-
-
-
-
-
-
- `;
- document.getElementById("compare-tab-contents").appendChild(tabContent);
- const leftTA = tabContent.querySelector(".json-input-left");
- const rightTA = tabContent.querySelector(".json-input-right");
- leftTA.value = tabData.leftContent;
- rightTA.value = tabData.rightContent;
- leftTA.addEventListener("paste", () => setTimeout(() => autoFormatTextarea(leftTA), 100));
- leftTA.addEventListener("blur", () => autoFormatTextarea(leftTA));
- rightTA.addEventListener("paste", () => setTimeout(() => autoFormatTextarea(rightTA), 100));
- rightTA.addEventListener("blur", () => autoFormatTextarea(rightTA));
- saveGlobalState();
- enableTabReordering("compare-tabs-container");
-}
-
-function switchCompareTab(tabId) {
- document
- .querySelectorAll("#compare-tab-contents .json-tab-content")
- .forEach((tab) => tab.classList.remove("active"));
- const selectedTab = document.getElementById(tabId);
- if (selectedTab) selectedTab.classList.add("active");
- document.querySelectorAll("#compare-tabs-container .tab-button[data-tab]").forEach((btn) => {
- btn.classList.toggle("active", btn.getAttribute("data-tab") === tabId);
- });
- saveGlobalState();
-}
-
-function compareJSONs(tabId) {
- const tabContent = document.getElementById(tabId);
- const leftTA = tabContent.querySelector(".json-input-left");
- const rightTA = tabContent.querySelector(".json-input-right");
- const resultDiv = tabContent.querySelector(".compare-result");
- const leftText = leftTA.value;
- const rightText = rightTA.value;
- let leftObj;
- let rightObj;
- try {
- leftObj = JSON.parse(leftText);
- } catch (e) {
- resultDiv.textContent = "Left JSON Error: " + e.message;
- return;
- }
- try {
- rightObj = JSON.parse(rightText);
- } catch (e) {
- resultDiv.textContent = "Right JSON Error: " + e.message;
- return;
- }
- const leftFormatted = JSON.stringify(leftObj, null, 2);
- const rightFormatted = JSON.stringify(rightObj, null, 2);
- resultDiv.innerHTML = `
-
-
-
-
- ${diffJSONsPreview(leftFormatted, rightFormatted)}
- `;
- leftTA.value = leftFormatted;
- rightTA.value = rightFormatted;
- saveGlobalState();
-}
-
-function diffJSONsPreview(leftText, rightText) {
- const isDarkMode = document.body.classList.contains("dark-mode");
- // Choose a diff color based on the theme:
- // For light mode, use a light red; for dark mode, use a darker or more muted red.
- const diffStyle = isDarkMode ? "background-color:#662222;" : "background-color:#ddd;";
-
- const leftLines = leftText.split("\n");
- const rightLines = rightText.split("\n");
- const maxLines = Math.max(leftLines.length, rightLines.length);
- let html = "";
- for (let i = 0; i < maxLines; i++) {
- const lLine = leftLines[i] || "";
- const rLine = rightLines[i] || "";
- // Apply diffStyle if the lines differ
- const style = lLine === rLine ? "" : diffStyle;
- html += `
-
- ${lLine}
- |
-
- ${rLine}
- |
-
`;
- }
- html += "
";
- return html;
-}
-
-async function closeCompareTab(tabId, event) {
- if (event) {
- event.stopPropagation();
- event.preventDefault();
- }
-
- const result = await Swal.fire({
- title: "Close tab?",
- text: "Are you sure you want to close this tab?",
- icon: "warning",
- showCancelButton: true,
- confirmButtonText: "Yes",
- cancelButtonText: "No",
- });
-
- if (!result.isConfirmed) return;
-
- const tabButton = document.querySelector(
- `#compare-tabs-container .tab-button[data-tab="${tabId}"]`
- );
- const tabContent = document.getElementById(tabId);
- if (tabButton) tabButton.remove();
- if (tabContent) tabContent.remove();
- const remaining = document.querySelectorAll("#compare-tab-contents .json-tab-content");
- if (remaining.length > 0) switchCompareTab(remaining[remaining.length - 1].id);
- saveGlobalState();
-}
-
-/* ========== CodeGen Functions ========== */
-let codegenTabCount = 0;
-
-function addCodegenTab() {
- createCodegenTab();
- switchCodegenTab("codegenTab" + codegenTabCount);
- saveGlobalState();
-}
-
-function createCodegenTab() {
- codegenTabCount++;
- const tabId = "codegenTab" + codegenTabCount;
- // Create tab button
- const tabButton = document.createElement("button");
- tabButton.className = "tab-button";
- tabButton.setAttribute("data-tab", tabId);
- tabButton.onclick = () => switchCodegenTab(tabId);
- tabButton.innerHTML = `Tab ${codegenTabCount}
- ร`;
- tabButton.addEventListener("dblclick", () => openTabRenameTooltip(tabId, "codegen"));
- const tabsContainer = document.getElementById("codegen-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 = `
-
-
-
-
-
-
-
-
- `;
- document.getElementById("codegen-tab-contents").appendChild(tabContent);
- const textarea = tabContent.querySelector(".json-input");
- textarea.addEventListener("paste", () => setTimeout(() => autoFormatTextarea(textarea), 100));
- textarea.addEventListener("blur", () => autoFormatTextarea(textarea));
- saveGlobalState();
-}
-
-function createCodegenTabWithData(tabData) {
- codegenTabCount++;
- const tabId = tabData.id;
- const tabButton = document.createElement("button");
- tabButton.className = "tab-button";
- tabButton.setAttribute("data-tab", tabId);
- tabButton.onclick = () => switchCodegenTab(tabId);
- tabButton.innerHTML = `${tabData.name}
- ร`;
- tabButton.addEventListener("dblclick", () => openTabRenameTooltip(tabId, "codegen"));
- const tabsContainer = document.getElementById("codegen-tabs-container");
- const addButton = tabsContainer.querySelector(".add-tab-button");
- tabsContainer.insertBefore(tabButton, addButton);
- const tabContent = document.createElement("div");
- tabContent.id = tabId;
- tabContent.className = "json-tab-content";
- tabContent.innerHTML = `
-
-
-
-
-
-
-
- `;
- document.getElementById("codegen-tab-contents").appendChild(tabContent);
- const textarea = tabContent.querySelector(".json-input");
- textarea.value = tabData.input;
- const selectElem = document.getElementById("lang-select-" + tabId);
- selectElem.value = tabData.lang;
- textarea.addEventListener("paste", () => setTimeout(() => autoFormatTextarea(textarea), 100));
- textarea.addEventListener("blur", () => autoFormatTextarea(textarea));
- saveGlobalState();
- enableTabReordering("codegen-tabs-container");
-}
-
-function switchCodegenTab(tabId) {
- document
- .querySelectorAll("#codegen-tab-contents .json-tab-content")
- .forEach((tab) => tab.classList.remove("active"));
- const selectedTab = document.getElementById(tabId);
- if (selectedTab) selectedTab.classList.add("active");
- document.querySelectorAll("#codegen-tabs-container .tab-button[data-tab]").forEach((btn) => {
- btn.classList.toggle("active", btn.getAttribute("data-tab") === tabId);
- });
- saveGlobalState();
-}
-
-function generateCode(tabId) {
- const tabContent = document.getElementById(tabId);
- const textarea = tabContent.querySelector(".json-input");
- const langSelect = document.getElementById("lang-select-" + tabId);
- const outputPre = tabContent.querySelector(".code-output");
- const inputText = textarea.value;
- const lang = langSelect.value;
- let obj;
- try {
- obj = JSON.parse(inputText);
- } catch (e) {
- outputPre.textContent = "Invalid JSON: " + e.message;
- return;
- }
- let code = "";
- if (lang === "typescript") code = generateTypeScript(obj, "Root");
- else if (lang === "python") code = generatePython(obj, "Root");
- else if (lang === "go") code = generateGo(obj, "Root");
- outputPre.innerHTML = code;
- // .replace(/(interface|class|type)\b/g, '$1')
- // .replace(/"([^"]+)"/g, '"$1"')
- // .replace(/\b\d+\b/g, '$&')
- // .replace(/\bstring|number|boolean|any|void\b/g, '$&');
-
- saveGlobalState();
-}
-
-async function closeCodegenTab(tabId, event) {
- if (event) {
- event.stopPropagation();
- event.preventDefault();
- }
-
- const result = await Swal.fire({
- title: "Close tab?",
- text: "Are you sure you want to close this tab?",
- icon: "warning",
- showCancelButton: true,
- confirmButtonText: "Yes",
- cancelButtonText: "No",
- });
-
- if (!result.isConfirmed) return;
-
- const tabButton = document.querySelector(
- `#codegen-tabs-container .tab-button[data-tab="${tabId}"]`
- );
- const tabContent = document.getElementById(tabId);
- if (tabButton) tabButton.remove();
- if (tabContent) tabContent.remove();
- const remaining = document.querySelectorAll("#codegen-tab-contents .json-tab-content");
- if (remaining.length > 0) switchCodegenTab(remaining[remaining.length - 1].id);
- saveGlobalState();
-}
-
-/* ========== Utility Functions ========== */
-function autoFormatTextarea(textarea) {
- try {
- const parsed = JSON.parse(textarea.value);
- textarea.value = JSON.stringify(parsed, null, 2);
- } catch (e) {
- // Do nothing if invalid
- }
-}
-
-function createTreeView(data, parentElement) {
- parentElement.innerHTML = "";
- const focusedNode = null;
-
- function processNode(value, parent, key, path = []) {
- const node = document.createElement("div");
- node.className = "tree-node";
- const currentPath = [...path, key];
- const displayKey = key !== undefined ? key : "";
-
- if (typeof value === "object" && value !== null) {
- const isArray = Array.isArray(value);
- const keySpan = document.createElement("span");
- keySpan.className = `tree-key ${isArray ? "type-array" : "type-object"}`;
- keySpan.tabIndex = 0;
- keySpan.innerHTML = `
- ${displayKey}
- ${
- isArray ? `[${value.length}]` : `{${Object.keys(value).length}}`
- }
- `;
-
- // Keyboard navigation
- keySpan.addEventListener("keydown", (e) => {
- switch (e.key) {
- case "ArrowRight":
- if (keySpan.classList.contains("collapsed")) toggleNode();
- break;
- case "ArrowLeft":
- if (!keySpan.classList.contains("collapsed")) toggleNode();
- break;
- case "ArrowDown":
- focusNextNode(keySpan);
- break;
- case "ArrowUp":
- focusPreviousNode(keySpan);
- break;
- }
- });
-
- // Context menu
- keySpan.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- showContextMenu(e, currentPath, value);
- });
-
- const children = document.createElement("div");
- children.className = "tree-children";
- children.style.display = "none";
-
- const toggleNode = () => {
- keySpan.classList.toggle("expanded");
- keySpan.classList.toggle("collapsed");
- children.style.display = children.style.display === "none" ? "block" : "none";
- };
-
- keySpan.addEventListener("click", toggleNode);
-
- if (isArray) {
- value.forEach((item, index) => processNode(item, children, index, currentPath));
- } else {
- Object.entries(value).forEach(([k, v]) => processNode(v, children, k, currentPath));
- }
-
- node.appendChild(keySpan);
- node.appendChild(children);
- parent.appendChild(node);
- } else {
- const valueSpan = document.createElement("span");
- valueSpan.innerHTML = `
- ${key}:
- ${formatValue(value)}
- `;
-
- // Value context menu
- valueSpan.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- showContextMenu(e, currentPath, value);
- });
-
- node.appendChild(valueSpan);
- parent.appendChild(node);
- }
-
- return node;
- }
-
- function getValueTypeClass(value) {
- if (value === null) return "type-null";
- if (Array.isArray(value)) return "type-array";
- if (typeof value === "object") return "type-object";
- return `type-${typeof value}`;
- }
-
- function formatValue(value) {
- if (value === null) return "null";
- if (typeof value === "string") return `"${value}"`;
- return value.toString();
- }
-
- function showContextMenu(e, path, value) {
- const menu = document.createElement("div");
- menu.className = "tree-context-menu";
- menu.style.left = `${e.pageX}px`;
- menu.style.top = `${e.pageY}px`;
-
- const copyPath = document.createElement("div");
- copyPath.className = "tree-context-menu-item";
- copyPath.textContent = "Copy Path";
- copyPath.onclick = () => navigator.clipboard.writeText(path.join("."));
-
- const copyValue = document.createElement("div");
- copyValue.className = "tree-context-menu-item";
- copyValue.textContent = "Copy Value";
- copyValue.onclick = () => navigator.clipboard.writeText(JSON.stringify(value));
-
- menu.appendChild(copyPath);
- menu.appendChild(copyValue);
- document.body.appendChild(menu);
-
- const closeMenu = () => {
- document.body.removeChild(menu);
- document.removeEventListener("click", closeMenu);
- };
-
- document.addEventListener("click", closeMenu);
- }
-
- function focusNextNode(currentNode) {
- const allNodes = parentElement.querySelectorAll(".tree-key");
- const currentIndex = Array.from(allNodes).indexOf(currentNode);
- if (currentIndex < allNodes.length - 1) {
- allNodes[currentIndex + 1].focus();
- }
- }
-
- function focusPreviousNode(currentNode) {
- const allNodes = parentElement.querySelectorAll(".tree-key");
- const currentIndex = Array.from(allNodes).indexOf(currentNode);
- if (currentIndex > 0) {
- allNodes[currentIndex - 1].focus();
- }
- }
-
- processNode(data, parentElement);
-}
-
-function openTabRenameTooltip(tabId, mode) {
- let containerSelector;
- if (mode === "formatter") containerSelector = "#formatter-tabs-container";
- else if (mode === "compare") containerSelector = "#compare-tabs-container";
- else if (mode === "codegen") containerSelector = "#codegen-tabs-container";
- else containerSelector = "#editor-tabs-container";
-
- const tabButton = document.querySelector(containerSelector + ` .tab-button[data-tab="${tabId}"]`);
- const existingTooltip = document.querySelector(".tab-rename-tooltip");
- if (existingTooltip) existingTooltip.remove();
- const tooltip = document.createElement("div");
- tooltip.className = "tab-rename-tooltip";
- const rect = tabButton.getBoundingClientRect();
- tooltip.style.left = rect.left + "px";
- tooltip.style.top = rect.bottom + window.scrollY + 5 + "px";
- const input = document.createElement("input");
- input.type = "text";
- input.value = tabButton.querySelector(".tab-name").textContent;
- input.style.width = "150px";
- input.addEventListener("keydown", (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();
-
- function finalizeRename() {
- const newName = input.value.trim();
- if (newName) {
- tabButton.querySelector(".tab-name").textContent = newName;
- }
- tooltip.remove();
- saveGlobalState();
- }
-}
-
-function generateTypeScript(obj, interfaceName) {
- let result = `interface ${interfaceName} {\n`;
- for (const key in obj) {
- if (!Object.hasOwn(obj, key)) continue;
- const value = obj[key];
- if (value === null) {
- result += ` ${key}: any;\n`;
- } else if (Array.isArray(value)) {
- if (value.length > 0) {
- const elem = value[0];
- if (typeof elem === "object" && elem !== null) {
- const subInterface = interfaceName + capitalize(key);
- result += ` ${key}: ${subInterface}[];\n`;
- result += generateTypeScript(elem, subInterface);
- } else {
- result += ` ${key}: ${typeof elem}[];\n`;
- }
- } else {
- result += ` ${key}: any[];\n`;
- }
- } else if (typeof value === "object") {
- const subInterface = interfaceName + capitalize(key);
- result += ` ${key}: ${subInterface};\n`;
- result += generateTypeScript(value, subInterface);
- } else {
- result += ` ${key}: ${typeof value};\n`;
- }
- }
- result += "}\n";
- return result;
-}
-
-function generatePython(obj, className) {
- let result = "from dataclasses import dataclass\nfrom typing import Any, List\n\n";
- result += `@dataclass\nclass ${className}:\n`;
- for (const key in obj) {
- if (!Object.hasOwn(obj, key)) continue;
- const value = obj[key];
- let pyType = "Any";
- if (value === null) {
- pyType = "Any";
- } else if (Array.isArray(value)) {
- if (value.length > 0) {
- const elem = value[0];
- if (typeof elem === "object" && elem !== null) {
- const subClass = className + capitalize(key);
- pyType = `List[${subClass}]`;
- result += "\n" + generatePython(elem, subClass);
- } else {
- if (typeof elem === "number") pyType = "List[float]";
- else if (typeof elem === "string") pyType = "List[str]";
- else if (typeof elem === "boolean") pyType = "List[bool]";
- else pyType = "List[Any]";
- }
- } else {
- pyType = "List[Any]";
- }
- } else if (typeof value === "object") {
- const subClass = className + capitalize(key);
- pyType = subClass;
- result += "\n" + generatePython(value, subClass);
- } else {
- if (typeof value === "number") pyType = "float";
- else if (typeof value === "string") pyType = "str";
- else if (typeof value === "boolean") pyType = "bool";
- }
- result += ` ${key}: ${pyType}\n`;
- }
- return result;
-}
-
-function generateGo(obj, structName) {
- let result = `type ${structName} struct {\n`;
- for (const key in obj) {
- if (!Object.hasOwn(obj, key)) continue;
- const value = obj[key];
- let goType = "interface{}";
- if (value === null) {
- goType = "interface{}";
- } else if (Array.isArray(value)) {
- if (value.length > 0) {
- const elem = value[0];
- if (typeof elem === "object" && elem !== null) {
- const subStruct = structName + capitalize(key);
- goType = `[]${subStruct}`;
- result += generateGo(elem, subStruct);
- } else {
- if (typeof elem === "number") goType = "[]float64";
- else if (typeof elem === "string") goType = "[]string";
- else if (typeof elem === "boolean") goType = "[]bool";
- else goType = "[]interface{}";
- }
- } else {
- goType = "[]interface{}";
- }
- } else if (typeof value === "object") {
- const subStruct = structName + capitalize(key);
- goType = subStruct;
- result += generateGo(value, subStruct);
- } else {
- if (typeof value === "number") goType = "float64";
- else if (typeof value === "string") goType = "string";
- else if (typeof value === "boolean") goType = "bool";
- }
- const fieldName = key.charAt(0).toUpperCase() + key.slice(1);
- result += ` ${fieldName} ${goType} \`json:"${key}"\`\n`;
- }
- result += "}\n";
- return result;
-}
-
-function capitalize(str) {
- return str.charAt(0).toUpperCase() + str.slice(1);
-}
-
-/* ========== Convert to Dict Functions ========== */
-let currentConvertMode = "dict-to-json";
-
-function switchConvertDirection(mode) {
- currentConvertMode = mode;
- document.querySelectorAll("#convert-section .tab-button").forEach((btn) => {
- btn.classList.toggle("active", btn.textContent.includes("Dict") === (mode === "dict-to-json"));
- });
- document.getElementById("convert-input").value = "";
- document.getElementById("convert-output").textContent = "";
-}
-
-function convert() {
- const input = document.getElementById("convert-input").value;
- const output = document.getElementById("convert-output");
-
- try {
- if (currentConvertMode === "dict-to-json") {
- // 1) None/True/False โ null/true/false
- let json = input
- .replace(/\bNone\b/g, "null")
- .replace(/\bTrue\b/g, "true")
- .replace(/\bFalse\b/g, "false");
- // 2) remove trailing commas before } or ]
- json = json.replace(/,\s*(?=[}\]])/g, "");
- // 3) convert only quoted 'strings' โ "strings"
- json = json.replace(/'([^']*?)'/g, '"$1"');
-
- const parsed = JSON.parse(json);
- output.textContent = JSON.stringify(parsed, null, 2);
- } else {
- // JSON โ Python dict
- const obj = JSON.parse(input);
- let dictStr = JSON.stringify(obj, null, 2);
- // 1) only replace "quoted" strings โ 'strings'
- dictStr = dictStr.replace(/"([^"]*?)"/g, "'$1'");
- // 2) true/false/null โ True/False/None
- dictStr = dictStr
- .replace(/\btrue\b/g, "True")
- .replace(/\bfalse\b/g, "False")
- .replace(/\bnull\b/g, "None");
-
- output.textContent = dictStr;
- }
- } catch (e) {
- output.textContent = "Error: " + e.message;
- }
-}
-
-function copyConvertOutput() {
- const output = document.getElementById("convert-output").textContent;
- copyToClipboard(output, "Copied!");
-}
-
-/* ========== Mock data generator ========== */
-
-let latestMockData = [];
-
-function loadMockPreset(name) {
- if (!name) return;
-
- const preset = window.PRESETS.mockgen[name];
- if (!preset) return;
-
- const schemaInput = document.getElementById('mock-schema-input');
- if (schemaInput) {
- schemaInput.value = JSON.stringify(preset.schema, null, 2);
- }
-}
-
-function generateMockData() {
- const input = document.getElementById("mock-schema-input").value;
- const count = Number.parseInt(document.getElementById("mockgen-count").value || "1", 10);
- const outputContainer = document.getElementById("mock-output-container");
-
- try {
- const schema = JSON.parse(input);
- latestMockData = Array.from({ length: count }, () => {
- try {
- return mockFromSchema(schema);
- } catch (err) {
- return { error: err.message };
- }
- });
- document.getElementById("mock-stats").textContent = `Showing ${count} record(s)`;
- updateMockView();
- } catch (e) {
- outputContainer.innerHTML = `โ Error: ${e.message}`;
- }
-}
-/* global faker */
-function resolveFakerPath(path) {
- if (!path) throw new Error("Empty faker path");
- const parts = path.split(".");
- let current = faker;
- for (const part of parts) {
- if (!current[part]) throw new Error(`Invalid faker path: "${path}"`);
- current = current[part];
- }
- if (typeof current === "function") return current();
- throw new Error(`Faker path "${path}" does not resolve to a function`);
-}
-
-function mockFromSchema(schema) {
- if (Array.isArray(schema)) {
- return schema.map((item) => mockFromSchema(item));
- }
- if (typeof schema === "object") {
- const result = {};
- for (const [key, value] of Object.entries(schema)) {
- result[key] = mockFromSchema(value);
- }
- return result;
- }
- if (typeof schema === "string") {
- if (schema === "boolean") return Math.random() < 0.5;
- if (schema.startsWith("number|")) {
- const [min, max] = schema.split("|")[1].split("-").map(Number);
- return faker.number.int({ min, max });
- }
-
- return resolveFakerPath(schema);
- }
- return schema;
-}
-
-function updateMockView() {
- const mode = document.querySelector('input[name="mock-view-mode"]:checked').value;
- const container = document.getElementById("mock-output-container");
- container.innerHTML = "";
-
- if (mode === "json") {
- const pre = document.createElement("pre");
- pre.className = "code-output";
- pre.textContent = JSON.stringify(
- latestMockData.length === 1 ? latestMockData[0] : latestMockData,
- null,
- 2
- );
- container.appendChild(pre);
- } else {
- container.appendChild(renderTableFromJson(latestMockData));
- }
-}
-
-function renderTableFromJson(data) {
- const table = document.createElement("table");
- table.className = "mock-preview-table";
-
- if (!Array.isArray(data) || data.length === 0 || typeof data[0] !== "object") {
- table.innerHTML = "| No tabular data available |
";
- return table;
- }
-
- const headers = [...new Set(data.flatMap((obj) => Object.keys(obj)))];
- const thead = document.createElement("thead");
- const headerRow = document.createElement("tr");
- headers.forEach((h) => {
- const th = document.createElement("th");
- th.textContent = h;
- // Add title for full text on hover
- th.title = h;
- headerRow.appendChild(th);
- });
- thead.appendChild(headerRow);
- table.appendChild(thead);
-
- const tbody = document.createElement("tbody");
- data.forEach((row) => {
- const tr = document.createElement("tr");
- if (row.error) {
- tr.classList.add("error");
- }
-
- headers.forEach((key) => {
- const td = document.createElement("td");
- const val = row[key];
- let displayVal;
-
- if (val && typeof val === "object") {
- displayVal = JSON.stringify(val);
- } else {
- displayVal = val ?? "";
- }
-
- td.textContent = displayVal;
- // Add title for full text on hover
- td.title = displayVal;
-
- tr.appendChild(td);
- });
- tbody.appendChild(tr);
- });
- table.appendChild(tbody);
- return table;
-}
-
-function copyMockOutput(format = "json") {
- let text = "";
- if (format === "json") {
- text = JSON.stringify(
- latestMockData.length === 1 ? latestMockData[0] : latestMockData,
- null,
- 2
- );
- } else if (format === "csv") {
- if (!latestMockData.length || typeof latestMockData[0] !== "object") return;
- const keys = Object.keys(latestMockData[0]);
- const rows = latestMockData.map((obj) => keys.map((k) => JSON.stringify(obj[k] ?? "")));
- text = [keys.join(","), ...rows.map((r) => r.join(","))].join("\n");
- }
-
- copyToClipboard(text, `Copied ${format.toUpperCase()}!`);
-}
-
-function downloadFile(content, filename, type) {
- const blob = new Blob([content], { type });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
-}
-
-let userInputValue = ""; // This variable will store filename input from user using the following event listener:
-
-document.getElementById("userInput").addEventListener("input", (e) => {
- userInputValue = e.target.value;
- //console.log("Saved input:", userInputValue); // Optional: see it live
-});
-
-function exportMockOutput(format = "json") {
- if (!Array.isArray(latestMockData) || latestMockData.length === 0) {
- console.error("No data available to export.");
- return;
- }
-
- let content = "";
- let filename = userInputValue !== "" ? userInputValue : "mock_data"; // Use user input if available
-
- if (format === "json") {
- content = JSON.stringify(
- latestMockData.length === 1 ? latestMockData[0] : latestMockData,
- null,
- 2
- );
- filename += ".json";
- downloadFile(content, filename, "application/json");
- } else if (format === "csv") {
- if (!latestMockData.length || typeof latestMockData[0] !== "object") return;
-
- const keys = Object.keys(latestMockData[0]);
- const rows = latestMockData.map((obj) => keys.map((k) => JSON.stringify(obj[k] ?? "")));
- content = [keys.join(","), ...rows.map((r) => r.join(","))].join("\n");
- filename += ".csv";
- downloadFile(content, filename, "text/csv");
- }
-}
-
-const mockgenDocs = `
- # ๐งช Mock Data Schema Format
-
- ## Supported Types
-
- - \`"name.fullName"\`: Full name
- - \`"internet.email"\`: Email address
- - \`"number|10-50"\`: Custom range
- - \`"boolean"\`: true / false
- - Arrays: \`["lorem.word"]\`
- - Nested:
-
- \`\`\`json
- {
- "user": {
- "name": "name.fullName",
- "email": "internet.email"
- }
- }
- \`\`\`
-
- ## Example
-
- \`\`\`json
- {
- "id": "number|1000-9999",
- "name": "name.fullName",
- "email": "internet.email",
- "isActive": "boolean"
- }
- \`\`\`
- `;
-
-function switchMockTab(tab) {
- const schemaPanel = document.getElementById("mockgen-schema-panel");
- const docsPanel = document.getElementById("mockgen-docs-panel");
-
- document.getElementById("mock-tab-schema").classList.remove("active");
- document.getElementById("mock-tab-docs").classList.remove("active");
-
- if (tab === "schema") {
- schemaPanel.style.display = "block";
- docsPanel.style.display = "none";
- document.getElementById("mock-tab-schema").classList.add("active");
- } else {
- schemaPanel.style.display = "none";
- docsPanel.style.display = "block";
- document.getElementById("mock-tab-docs").classList.add("active");
- renderMockgenDocs();
- }
-}
-
-function renderMockgenDocs() {
- document.getElementById("mockgen-docs-preview").innerHTML = marked.parse(mockgenDocs);
-}
-
-// Text Editor
-let editorTabCount = 0;
-const editorInstances = {};
-
-function applyTabButtonTheme() {
- const isDark = document.body.classList.contains("dark-mode");
- const buttons = document.querySelectorAll(".tab-button[data-tab]");
-
- buttons.forEach((button) => {
- button.style.backgroundColor = isDark ? "#2a2a2a" : "#ffffff";
- button.style.color = isDark ? "#ffffff" : "#000000";
- });
-}
-
-function addEditorTab(tabData = null) {
- let tabId;
- if (tabData?.id) {
- tabId = tabData.id;
- const match = tabId.match(/editor-tab-(\d+)/);
- if (match && Number.parseInt(match[1], 10) > editorTabCount) {
- editorTabCount = Number.parseInt(match[1], 10);
- }
- } else {
- editorTabCount++;
- tabId = `editor-tab-${editorTabCount}`;
- }
-
- const tabButton = document.createElement("button");
- tabButton.className = "tab-button";
- tabButton.setAttribute("data-tab", tabId);
- tabButton.innerHTML = `${
- tabData?.title || `Note ${editorTabCount}`
- }ร`;
- tabButton.onclick = () => switchEditorTab(tabId);
- tabButton.addEventListener("dblclick", () => openTabRenameTooltip(tabId, "editor"));
-
- document
- .getElementById("editor-tabs-container")
- .insertBefore(tabButton, document.querySelector("#editor-tabs-container .add-tab-button"));
-
- const tabContent = document.createElement("div");
- tabContent.id = tabId;
- tabContent.className = "json-tab-content";
- tabContent.innerHTML = `
-
-
-
-
- `;
-
- document.getElementById("editor-tab-contents").appendChild(tabContent);
-
- const editor = new toastui.Editor({
- el: document.getElementById(`${tabId}-editor`),
- height: "400px",
- initialEditType: "markdown",
- previewStyle: "vertical",
- });
- editorInstances[tabId] = editor;
-
- const saved = localStorage.getItem(tabId);
- if (saved) editor.setMarkdown(saved);
-
- enableEditorTabReordering();
- applyEditorTabDarkMode();
-}
-
-function switchEditorTab(tabId) {
- document
- .querySelectorAll("#editor-tab-contents .json-tab-content")
- .forEach((el) => el.classList.remove("active"));
- document.getElementById(tabId)?.classList.add("active");
-
- document.querySelectorAll("#editor-tabs-container .tab-button").forEach((btn) => {
- btn.classList.toggle("active", btn.getAttribute("data-tab") === tabId);
- });
- updateEditorGlobalState();
-}
-
-function saveEditorContent(tabId) {
- const content = editorInstances[tabId].getMarkdown();
- localStorage.setItem(tabId, content);
- updateEditorGlobalState();
- Swal.fire({
- toast: true,
- position: "top-end",
- icon: "success",
- title: "Autosaved",
- showConfirmButton: false,
- timer: 1500,
- });
-}
-
-async function deleteEditorTab(tabId, event) {
- if (event) {
- event.stopPropagation();
- event.preventDefault();
- }
-
- const result = await Swal.fire({
- title: "Close tab?",
- text: "Are you sure you want to close this tab?",
- icon: "warning",
- showCancelButton: true,
- confirmButtonText: "Yes",
- cancelButtonText: "No",
- });
-
- if (!result.isConfirmed) return;
-
- localStorage.removeItem(tabId);
- delete editorInstances[tabId];
- document.querySelector(`#editor-tabs-container .tab-button[data-tab="${tabId}"]`)?.remove();
- document.getElementById(tabId)?.remove();
- const remaining = document.querySelectorAll("#editor-tab-contents .json-tab-content");
- if (remaining.length > 0) switchEditorTab(remaining[0].id);
- updateEditorGlobalState();
-}
-
-function updateEditorGlobalState() {
- const state = {
- activeTab: document.querySelector("#editor-tab-contents .json-tab-content.active")?.id || "",
- tabs: [],
- };
- document.querySelectorAll("#editor-tabs-container .tab-button[data-tab]").forEach((btn) => {
- const tabId = btn.getAttribute("data-tab");
- const title = btn.querySelector(".tab-name").textContent;
- state.tabs.push({ id: tabId, title });
- });
- localStorage.setItem("editorState", JSON.stringify(state));
-}
-
-function loadEditorGlobalState() {
- const stateStr = localStorage.getItem("editorState");
- const container = document.getElementById("editor-tabs-container");
- container.querySelectorAll(".tab-button[data-tab]").forEach((btn) => btn.remove());
- document.getElementById("editor-tab-contents").innerHTML = "";
- editorTabCount = 0;
-
- if (!stateStr) {
- addEditorTab();
- return;
- }
-
- const state = JSON.parse(stateStr);
- state.tabs.forEach((tabData) => addEditorTab(tabData));
-
- if (state.activeTab && document.getElementById(state.activeTab)) {
- switchEditorTab(state.activeTab);
- } else if (state.tabs.length > 0) {
- switchEditorTab(state.tabs[0].id);
- }
-}
-
-function enableEditorTabReordering() {
- const container = document.getElementById("editor-tabs-container");
- const buttons = container.querySelectorAll(".tab-button[data-tab]");
- buttons.forEach((btn) => {
- btn.draggable = true;
- btn.addEventListener("dragstart", (e) => {
- e.dataTransfer.setData("text/plain", btn.getAttribute("data-tab"));
- btn.classList.add("dragging");
- });
- btn.addEventListener("dragend", () => {
- btn.classList.remove("dragging");
- container.querySelectorAll(".tab-button").forEach((b) => b.classList.remove("drag-over"));
- });
- btn.addEventListener("dragover", (e) => {
- e.preventDefault();
- btn.classList.add("drag-over");
- });
- btn.addEventListener("dragleave", () => {
- btn.classList.remove("drag-over");
- });
- btn.addEventListener("drop", (e) => {
- e.preventDefault();
- const draggedId = e.dataTransfer.getData("text/plain");
- const draggedBtn = container.querySelector(`[data-tab="${draggedId}"]`);
-
- btn.classList.remove("drag-over");
-
- if (draggedBtn && draggedBtn !== btn) {
- container.insertBefore(draggedBtn, btn);
- updateEditorGlobalState();
- }
- });
- });
-}
-
-function applyEditorTabDarkMode() {
- const container = document.getElementById("editor-tabs-container");
- container.querySelectorAll(".tab-button").forEach((btn) => {
- if (document.body.classList.contains("dark-mode")) {
- btn.style.backgroundColor = "#2c2c2c";
- btn.style.color = "#eee";
- btn.style.borderColor = "#444";
- } else {
- btn.style.backgroundColor = "";
- btn.style.color = "";
- btn.style.borderColor = "";
- }
- });
-}
-
-/* ========== Mobile Sidebar Functions ========== */
-function toggleSidebar() {
- const sidebar = document.querySelector(".sidebar");
- sidebar.classList.toggle("active");
-
- // Close sidebar when clicking outside
- if (sidebar.classList.contains("active")) {
- const closeOnClickOutside = (e) => {
- if (!sidebar.contains(e.target) && !e.target.matches(".mobile-sidebar-toggle")) {
- sidebar.classList.remove("active");
- document.removeEventListener("click", closeOnClickOutside);
- }
- };
- // Add event listener with a slight delay to prevent immediate closing
- setTimeout(() => {
- document.addEventListener("click", closeOnClickOutside);
- }, 100);
- }
-}
-
-// Add touch event handling for better mobile experience
-document.addEventListener("DOMContentLoaded", () => {
- let touchStartX = 0;
- let touchEndX = 0;
- const sidebar = document.querySelector(".sidebar");
-
- document.addEventListener(
- "touchstart",
- (e) => {
- touchStartX = e.changedTouches[0].screenX;
- },
- false
- );
-
- document.addEventListener(
- "touchend",
- (e) => {
- touchEndX = e.changedTouches[0].screenX;
- handleSwipe();
- },
- false
- );
-
- function handleSwipe() {
- const swipeThreshold = 50;
- const swipeLength = touchEndX - touchStartX;
-
- // Swipe right to open sidebar
- if (swipeLength > swipeThreshold && touchStartX < 30) {
- sidebar.classList.add("active");
- }
- // Swipe left to close sidebar
- else if (swipeLength < -swipeThreshold && sidebar.classList.contains("active")) {
- sidebar.classList.remove("active");
- }
- }
-});
-
-/* ========== 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");
- applyTabButtonTheme();
- 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);
- });
- }
-}
-/* ========== Keyboard Shortcuts ========== */
-document.addEventListener("keydown", (e) => {
- if (
- e.ctrlKey &&
- e.key.toLowerCase() === "t" &&
- document.getElementById("formatter-section").style.display !== "none"
- ) {
- e.preventDefault();
- addFormatterTab();
- }
- if (
- e.ctrlKey &&
- e.key.toLowerCase() === "w" &&
- document.getElementById("formatter-section").style.display !== "none"
- ) {
- e.preventDefault();
- const activeTab = document.querySelector("#formatter-tab-contents .json-tab-content.active");
- if (activeTab) closeFormatterTab(activeTab.id);
- }
- if (e.ctrlKey && (e.key === "/" || e.key === "?")) {
- e.preventDefault();
- toggleShortcutModal();
- }
- if (e.key === "Escape") {
- const modal = document.getElementById("shortcut-modal");
- if (modal.style.display === "block") toggleShortcutModal();
- }
- if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "s") {
- e.preventDefault();
- const activeTab = document.querySelector("#editor-tab-contents .json-tab-content.active");
- if (activeTab) saveEditorContent(activeTab.id);
- }
-});
-
-/* ========== Tab reordering ========== */
-function enableTabReordering(containerId) {
- const container = document.getElementById(containerId);
- const tabButtons = container.querySelectorAll(".tab-button[data-tab]");
-
- tabButtons.forEach((button) => {
- button.draggable = true;
-
- button.addEventListener("dragstart", (e) => {
- e.dataTransfer.setData("text/plain", button.getAttribute("data-tab"));
- button.classList.add("dragging");
- });
-
- button.addEventListener("dragend", () => {
- button.classList.remove("dragging");
- container.querySelectorAll(".tab-button").forEach((b) => b.classList.remove("drag-over"));
- });
-
- button.addEventListener("dragover", (e) => {
- e.preventDefault();
- button.classList.add("drag-over");
- });
-
- button.addEventListener("dragleave", () => {
- button.classList.remove("drag-over");
- });
-
- button.addEventListener("drop", (e) => {
- e.preventDefault();
- const draggedId = e.dataTransfer.getData("text/plain");
- const draggedBtn = container.querySelector(`[data-tab="${draggedId}"]`);
-
- btn.classList.remove("drag-over");
-
- if (draggedBtn && draggedBtn !== btn) {
- container.insertBefore(draggedBtn, btn);
- saveGlobalState();
- }
- });
- });
-}
-
-/* ========== Preset Functions ========== */
-function populatePresetSelects() {
- // Populate formatter presets
- const formatterSelect = document.querySelector("#formatter-tabs-container .preset-select");
- Object.keys(window.PRESETS.formatter).forEach((key) => {
- const option = document.createElement("option");
- option.value = key;
- option.textContent = window.PRESETS.formatter[key].name;
- formatterSelect.appendChild(option);
- });
-
- // Populate compare presets
- const compareSelect = document.querySelector("#compare-tabs-container .preset-select");
- Object.keys(window.PRESETS.compare).forEach((key) => {
- const option = document.createElement("option");
- option.value = key;
- option.textContent = window.PRESETS.compare[key].name;
- compareSelect.appendChild(option);
- });
-
- // Populate codegen presets
- const codegenSelect = document.querySelector("#codegen-tabs-container .preset-select");
- Object.keys(window.PRESETS.codegen).forEach((key) => {
- const option = document.createElement("option");
- option.value = key;
- option.textContent = window.PRESETS.codegen[key].name;
- codegenSelect.appendChild(option);
- });
-}
-
-function loadFormatterPreset(presetName) {
- if (!presetName) return;
-
- const preset = window.PRESETS.formatter[presetName];
- if (!preset) return;
-
- const activeTab = document.querySelector("#formatter-tab-contents .json-tab-content.active");
- if (!activeTab) return;
-
- const textarea = activeTab.querySelector(".json-input");
- if (textarea) {
- textarea.value = preset.content;
- updateFormatterPreview(activeTab.id);
- }
-}
-
-function loadComparePreset(presetName) {
- if (!presetName) return;
-
- const preset = window.PRESETS.compare[presetName];
- if (!preset) return;
-
- const activeTab = document.querySelector("#compare-tab-contents .json-tab-content.active");
- if (!activeTab) return;
-
- const leftTextarea = activeTab.querySelector(".json-input-left");
- const rightTextarea = activeTab.querySelector(".json-input-right");
-
- if (leftTextarea && rightTextarea) {
- leftTextarea.value = preset.leftContent;
- rightTextarea.value = preset.rightContent;
- compareJSONs(activeTab.id);
- }
-}
-
-function loadCodegenPreset(presetName) {
- if (!presetName) return;
-
- const preset = window.PRESETS.codegen[presetName];
- if (!preset) return;
-
- const activeTab = document.querySelector("#codegen-tab-contents .json-tab-content.active");
- if (!activeTab) return;
-
- const textarea = activeTab.querySelector(".json-input");
- const langSelect = document.getElementById("lang-select-" + activeTab.id);
-
- if (textarea && langSelect) {
- textarea.value = preset.input;
- langSelect.value = preset.lang;
- generateCode(activeTab.id);
- }
-}
-
-/* ========== Initialization ========== */
-window.addEventListener("load", () => {
- loadGlobalState();
- loadEditorGlobalState();
- // If no saved state, create a default tabs.
- if (document.getElementById("formatter-tab-contents").children.length === 0) {
- addFormatterTab();
- }
-
- if (document.getElementById("compare-tab-contents").children.length === 0) {
- addCompareTab();
- }
-
- if (document.getElementById("codegen-tab-contents").children.length === 0) {
- addCodegenTab();
- }
-
- if (document.getElementById("editor-tab-contents").children.length === 0) {
- addEditorTab();
- switchEditorTab("editor-tab-1");
- }
-
- populatePresetSelects();
- applyTabButtonTheme();
- enableTabReordering("formatter-tabs-container");
- enableTabReordering("compare-tabs-container");
- enableTabReordering("codegen-tabs-container");
- enableEditorTabReordering();
- applyEditorTabDarkMode();
-});
-
-setInterval(() => {
- const editorVisible = document.getElementById("editor-section").style.display !== "none";
- const activeTab = document.querySelector("#editor-tab-contents .json-tab-content.active");
-
- if (!Swal.isVisible() && editorVisible && activeTab) {
- saveEditorContent(activeTab.id);
- }
-}, 5000);
diff --git a/json-tool/README.md b/json-tool/README.md
new file mode 100644
index 0000000..5726f61
--- /dev/null
+++ b/json-tool/README.md
@@ -0,0 +1,146 @@
+# JSONP - Multi tab JSON toolkit
+
+
+ A versatile web-based toolkit for working with JSON data. Open-source and works offline!
+
+
+
+
+
+
+## ๐ Project Structure
+
+```
+json-tool/
+โโโ public/
+โ โโโ index.html # Main entry point
+โ โโโ assets/
+โ โโโ css/
+โ โ โโโ base.css # Reset/normalize
+โ โ โโโ main.css # Core styles
+โ โ โโโ themes/ # Dark/light mode
+โ โโโ js/
+โ โโโ core/ # Reusable utilities
+โ โ โโโ storage.js
+โ โ โโโ dom.js
+โ โ โโโ json-utils.js
+โ โโโ features/ # Feature modules
+โ โ โโโ formatter/
+โ โ โ โโโ formatter.js
+โ โ โ โโโ formatter.html
+โ โ โโโ compare/
+โ โ โโโ codegen/
+โ โ โโโ mockgen/
+โ โ โ โโโ mockgen.js
+โ โ โโโ convert/
+โ โ โ โโโ convert.js
+โ โ โโโ editor/
+โ โโโ app.js # Main app initialization
+โโโ README.md # Project documentation
+```
+
+## โจ Features
+
+- **Core Tools**
+
+ - ๐งน JSON Formatter & Validator
+ - Multi-tab support with color coding
+ - Tree view with expandable nodes
+ - Search functionality
+ - Error highlighting
+ - ๐ JSON Comparison (diff checker)
+ - Side-by-side diff view
+ - Multi-tab support
+ - ๐ป Code Generation
+ - TypeScript interfaces
+ - Python dataclasses
+ - Go structs
+ - ๐ Python - json-to-dict and dict-to-json converter
+ - ๐งช Mock Data Generator
+ - Faker.js integration
+ - JSON/CSV export
+ - Table/JSON view modes
+ - Customizable schemas
+ - Built-in presets
+ - Real-time preview
+ - Custom field types
+ - Data validation
+ - ๐ Data Conversion
+ - JSON to YAML
+ - JSON to XML
+ - JSON to CSV
+ - JSON to SQL
+ - Custom format support
+ - Batch conversion
+ - Template system
+
+- **Workflow**
+
+ - ๐ Multi-tab interface with drag-and-drop reordering
+ - ๐ Dark/Light mode toggle
+ - โจ๏ธ Keyboard shortcuts
+ - ๐พ Automatic local saving
+ - ๐ Real-time previews
+
+- **Convenience**
+ - ๐ค๐ฅ Import/Export JSON files
+ - ๐ Browser-based (no install needed)
+ - ๐ Real-time validation
+ - ๐ฆ Error highlighting
+ - ๐ Copy/paste support
+
+## ๐ Architecture
+
+- **Core Utilities**
+
+ - `storage.js`: Local storage and state management
+ - `dom.js`: DOM manipulation helpers
+ - `json-utils.js`: JSON parsing and validation utilities
+
+- **Feature Modules**
+
+ - Formatter: JSON formatting and validation
+ - Compare: JSON comparison and diffing
+ - Codegen: Code generation from JSON schemas
+ - Mockgen: Mock data generation with Faker.js
+ - Convert: Multi-format data conversion
+
+- **Styling**
+ - Modular CSS architecture
+ - Theme support (dark/light modes)
+ - Responsive design
+
+## ๐ง Planned Features
+
+- ๐ JSON Schema generator
+- ๐ JSON-to-XML conversion
+- ๐ Visual JSON chart view
+- ๐งฉ Plugins/extensions system
+- ๐ Collaborative editing
+- ๐ Smart copy-paste detection
+- โญ Enhanced error handling and validation
+- ๐ Advanced search capabilities
+
+## ๐ Usage
+
+1. Open `index.html` in any modern browser
+2. Drag & drop JSON files directly
+3. Use tabs to organize multiple documents
+4. Toggle modes using the top buttons
+
+> **Note**: All data stays local - nothing is uploaded to servers
+
+## ๐ Changelog
+
+For a detailed list of changes and version history, please see the [CHANGELOG](./CHANGELOG/README.md) directory.
+
+## ๐ค Contributing
+
+We welcome contributions! Please see:
+
+- [Contribution Guidelines](https://github.com/shravan20/jsonp/blob/main/CONTRIBUTING.md)
+- [GitHub Repository](https://github.com/shravan20/jsonp)
+
+## ๐ License
+
+MIT Licensed - Free for personal and commercial use
diff --git a/json.svg b/json-tool/json.svg
similarity index 100%
rename from json.svg
rename to json-tool/json.svg
diff --git a/json-tool/public/assets/css/base.css b/json-tool/public/assets/css/base.css
new file mode 100644
index 0000000..172ffa0
--- /dev/null
+++ b/json-tool/public/assets/css/base.css
@@ -0,0 +1,226 @@
+/* Base Styles - Structure and layout */
+body {
+ font-family: Arial, sans-serif;
+ margin: 0;
+ padding: 20px;
+ transition: background-color 0.3s, color 0.3s;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 20px;
+}
+
+h1 {
+ margin: 0 20px 0 0;
+ font-size: 1.8rem;
+}
+
+/* App Layout */
+.app-container {
+ display: flex;
+ min-height: 100vh;
+}
+
+/* Sidebar Styles */
+.sidebar {
+ width: 250px;
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+}
+
+.sidebar-header {
+ padding: 15px;
+ border-bottom: 1px solid;
+}
+
+.sidebar-header h3 {
+ margin: 0;
+ font-size: 1.1rem;
+}
+
+.sidebar-content {
+ flex: 1;
+ overflow-y: auto;
+}
+
+.feature-group {
+ padding: 10px 0;
+}
+
+.feature-item {
+ padding: 8px 15px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.feature-item i {
+ width: 16px;
+ text-align: center;
+}
+
+/* Main Content Area */
+.main-content {
+ flex: 1;
+ padding: 20px;
+ overflow-y: auto;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.header-actions {
+ display: flex;
+ gap: 10px;
+}
+
+/* Mobile Sidebar Toggle */
+.mobile-sidebar-toggle {
+ display: none;
+ position: fixed;
+ top: 10px;
+ left: 10px;
+ z-index: 1000;
+ border: none;
+ border-radius: 4px;
+ padding: 10px;
+ cursor: pointer;
+}
+
+.mobile-sidebar-close {
+ display: none;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ padding: 5px;
+}
+
+/* Responsive Styles */
+@media (max-width: 768px) {
+ body {
+ padding: 10px;
+ }
+
+ .app-container {
+ flex-direction: column;
+ }
+
+ .mobile-sidebar-toggle {
+ display: block;
+ }
+
+ .mobile-sidebar-close {
+ display: block;
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+
+ .sidebar {
+ position: fixed;
+ left: -250px;
+ top: 0;
+ bottom: 0;
+ z-index: 1000;
+ transition: left 0.3s ease;
+ }
+
+ .sidebar.active {
+ left: 0;
+ }
+
+ .main-content {
+ margin-left: 0;
+ padding: 15px;
+ padding-top: 50px;
+ }
+
+ .header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 15px;
+ }
+
+ .header h1 {
+ font-size: 1.5rem;
+ margin-right: 0;
+ }
+
+ /* Adjust tabs layout */
+ .tabs {
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ padding-bottom: 5px;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .tab-button {
+ flex-shrink: 0;
+ }
+
+ /* Adjust form controls */
+ .search-container,
+ .upload-download-container {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .search-container input,
+ .search-container button,
+ .upload-download-container button {
+ width: 100%;
+ margin-bottom: 10px;
+ }
+}
+
+@media (max-width: 480px) {
+ .header-actions {
+ flex-direction: column;
+ width: 100%;
+ }
+
+ .header-actions button {
+ width: 100%;
+ }
+}
+
+@media (min-width: 769px) and (max-width: 1024px) {
+ .sidebar {
+ width: 200px;
+ }
+}
+
+@media (hover: none) {
+ .feature-item:hover {
+ background-color: transparent;
+ }
+
+ .feature-item:active {
+ background-color: #2a2d2e;
+ }
+
+ .tab-button:hover {
+ background-color: transparent;
+ }
+
+ .tab-button:active {
+ background-color: #e0e0e0;
+ }
+}
+
+@media (max-width: 768px) {
+ ::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+ }
+}
diff --git a/json-tool/public/assets/css/main.css b/json-tool/public/assets/css/main.css
new file mode 100644
index 0000000..cc1908d
--- /dev/null
+++ b/json-tool/public/assets/css/main.css
@@ -0,0 +1,401 @@
+/* Component Styles */
+/* Buttons */
+.dark-mode-toggle,
+.shortcut-preview-button {
+ padding: 8px 16px;
+ border: none;
+ cursor: pointer;
+ border-radius: 4px;
+ margin-right: 10px;
+ font-size: 0.9rem;
+}
+
+/* Mode Selector */
+.mode-selector {
+ margin: 20px 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.mode-selector button {
+ padding: 10px 16px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.9rem;
+}
+
+.mode-selector button.active {
+ font-weight: bold;
+}
+
+/* Tabs */
+.tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 5px;
+ margin-bottom: 15px;
+}
+
+.tab-button {
+ padding: 8px 12px;
+ border: none;
+ cursor: pointer;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ transition: background-color 0.3s;
+ font-size: 0.9rem;
+}
+
+.tab-button.active {
+ background-color: #ffffff;
+ font-weight: bold;
+}
+
+.tab-button .close-tab {
+ color: red;
+ font-weight: bold;
+ cursor: pointer;
+}
+
+.tab-button .tab-color-picker {
+ width: 16px;
+ height: 16px;
+ padding: 0;
+ border: none;
+ cursor: pointer;
+}
+
+.add-tab-button {
+ padding: 8px 16px;
+ border-radius: 4px;
+ cursor: pointer;
+ border: none;
+ font-size: 0.9rem;
+}
+
+/* Content Panels */
+.json-tab-content {
+ display: none;
+}
+
+.json-tab-content.active {
+ display: block;
+}
+
+textarea {
+ width: 100%;
+ height: 200px;
+ padding: 10px;
+ font-family: monospace;
+ font-size: 0.95rem;
+ border: 1px solid;
+ border-radius: 4px;
+ margin-bottom: 15px;
+ box-sizing: border-box;
+}
+
+/* Utility Containers */
+.search-container,
+.upload-download-container {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 15px;
+ flex-wrap: wrap;
+}
+
+.preview-section {
+ padding: 20px;
+ border-radius: 5px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ overflow: auto;
+ word-wrap: normal;
+ display: none;
+ max-height: 600px;
+}
+
+.preview-section.active {
+ display: block;
+}
+.copy-button,
+button.copy-button {
+ padding: 6px 12px;
+ background-color: #28a745;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.85rem;
+}
+
+.copy-button:hover {
+ background-color: #218838;
+}
+
+/* Tree View */
+.tree-node {
+ margin: 4px 0;
+ padding-left: 20px;
+ position: relative;
+}
+
+.tree-key {
+ cursor: pointer;
+ padding: 2px 5px;
+ border-radius: 3px;
+ display: inline-block;
+}
+
+.tree-key:hover {
+ background-color: rgba(0, 123, 255, 0.1);
+}
+
+.type-string {
+ color: #4caf50;
+}
+
+.type-number {
+ color: #2196f3;
+}
+
+.type-boolean {
+ color: #9c27b0;
+}
+
+.type-null {
+ color: #666;
+}
+
+.type-object {
+ color: #ff9800;
+}
+
+.type-array {
+ color: #e91e63;
+}
+
+/* Reordering Drag Styles */
+.tab-button.dragging {
+ opacity: 0.6;
+ border: 2px dashed;
+}
+
+.tab-button.drag-over {
+ transform: translateY(2px) scale(1.02);
+ box-shadow: 0 2px 6px rgba(0, 123, 255, 0.3);
+}
+
+/* Markdown & Modal */
+.markdown-preview {
+ padding: 1rem;
+ border-radius: 5px;
+ border: 1px solid;
+ max-height: 500px;
+ overflow-y: auto;
+ line-height: 1.6;
+}
+
+.markdown-preview code {
+ padding: 2px 4px;
+ border-radius: 3px;
+}
+
+.modal {
+ display: none;
+ position: fixed;
+ z-index: 1000;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+.modal-content {
+ margin: 10% auto;
+ padding: 20px;
+ width: 400px;
+ border-radius: 5px;
+ position: relative;
+}
+
+.close-modal {
+ position: absolute;
+ top: 10px;
+ right: 15px;
+ font-size: 1.4rem;
+ cursor: pointer;
+}
+
+/* Tab Rename Tooltip */
+.tab-rename-tooltip {
+ position: absolute;
+ z-index: 1000;
+ border: 1px solid;
+ border-radius: 3px;
+ padding: 5px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+/* GitHub buttons */
+.github-links {
+ text-align: center;
+ margin: 20px 0;
+}
+
+.github-button,
+.contribute-button {
+ display: inline-flex;
+ align-items: center;
+ padding: 10px 15px;
+ margin: 0 10px;
+ border-radius: 5px;
+ text-decoration: none;
+ color: white;
+ font-size: 0.9rem;
+}
+
+.github-button i,
+.contribute-button i {
+ margin-right: 8px;
+}
+
+.tree-children {
+ margin-left: 20px;
+ overflow: hidden;
+ transition: max-height 0.3s ease, opacity 0.3s ease;
+ max-height: 1000px;
+ opacity: 1;
+}
+
+.tree-children.collapsed {
+ max-height: 0;
+ opacity: 0;
+ pointer-events: none;
+}
+
+/* JSON to Code preview themes */
+.code-output {
+ font-family: "Fira Code", monospace;
+ font-size: 14px;
+ line-height: 1.6;
+ padding: 16px;
+ border-radius: 6px;
+ overflow-x: auto;
+ white-space: pre;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+ margin-top: 10px;
+}
+
+.code-output .keyword {
+ font-weight: bold;
+}
+
+.code-output .comment {
+ font-style: italic;
+}
+
+/* Editor Specific */
+#editor-section .tab-button {
+ padding: 8px 12px;
+ background-color: #f5f5f5;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ margin-right: 5px;
+ font-size: 0.9rem;
+}
+
+#editor-section .tab-button.active {
+ background-color: #ffffff;
+ font-weight: bold;
+}
+
+#editor-section .add-tab-button {
+ padding: 8px 16px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.9rem;
+}
+
+#editor-tab-contents .json-tab-content {
+ display: none;
+ margin-top: 10px;
+}
+
+#editor-tab-contents .json-tab-content.active {
+ display: block;
+}
+
+.toastui-editor-defaultUI {
+ border-radius: 6px;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+}
+
+/* Tree View Scrolling Fix */
+.tree-view {
+ padding: 10px;
+ font-family: monospace;
+ line-height: 1.5;
+ overflow-y: auto;
+ overflow-x: auto;
+}
+
+/* Mock Data Table Styling */
+.mock-preview-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 20px 0;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.mock-preview-table thead {
+ border-bottom: 2px solid;
+}
+
+.mock-preview-table th {
+ padding: 12px 15px;
+ text-align: left;
+ font-weight: 600;
+ font-size: 0.9rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ border-bottom: 2px solid;
+ position: sticky;
+ top: 0;
+ z-index: 1;
+}
+
+.mock-preview-table td {
+ padding: 12px 15px;
+ border-bottom: 1px solid;
+ font-size: 0.9rem;
+ max-width: 300px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.mock-preview-table tbody tr:last-child td {
+ border-bottom: none;
+}
+
+/* Error row styling */
+.mock-preview-table tr.error td {
+ color: #dc3545;
+}
+
+/* Container for the table */
+#mock-output-container {
+ position: relative;
+ margin-top: 20px;
+ border-radius: 8px;
+ overflow: hidden;
+}
diff --git a/json-tool/public/assets/css/themes/dark.css b/json-tool/public/assets/css/themes/dark.css
new file mode 100644
index 0000000..594b6bd
--- /dev/null
+++ b/json-tool/public/assets/css/themes/dark.css
@@ -0,0 +1,352 @@
+/* Light Theme (Default) */
+body {
+ background-color: #f0f0f0;
+ color: #333;
+}
+
+body.dark-mode {
+ background-color: #121212;
+ color: #e0e0e0;
+}
+
+/* Sidebar */
+.sidebar {
+ background-color: #252526;
+ color: #ffffff;
+}
+
+.sidebar-header {
+ border-bottom-color: #3c3c3c;
+}
+
+.sidebar-header h3 {
+ color: #cccccc;
+}
+
+.feature-item {
+ color: #cccccc;
+}
+
+.feature-item:hover {
+ background-color: #2a2d2e;
+}
+
+.feature-item.active {
+ background-color: #37373d;
+ color: #ffffff;
+}
+
+/* Main Content */
+.main-content {
+ background-color: #f0f0f0;
+}
+
+body.dark-mode .main-content {
+ background-color: #1e1e1e;
+}
+
+body.dark-mode .sidebar {
+ background-color: #252526;
+ border-right: 1px solid #3c3c3c;
+}
+
+/* Buttons */
+.dark-mode-toggle,
+.shortcut-preview-button,
+.mode-selector button,
+.add-tab-button,
+.copy-button {
+ background-color: #007bff;
+ color: white;
+}
+
+.mode-selector button.active {
+ background-color: #0056b3;
+}
+
+/* Tabs */
+.tab-button {
+ background-color: #e0e0e0;
+}
+
+.tab-button.active {
+ background-color: #ffffff;
+}
+
+body.dark-mode .tab-button {
+ background-color: #333;
+ color: #e0e0e0;
+}
+
+body.dark-mode .tab-button.active {
+ background-color: #555;
+}
+
+/* Textareas and preview sections */
+textarea,
+.preview-section,
+.modal-content {
+ background-color: white;
+ border-color: #ccc;
+}
+
+body.dark-mode textarea,
+body.dark-mode .preview-section,
+body.dark-mode .modal-content {
+ background-color: #2e2e2e;
+ color: #e0e0e0;
+ border-color: #555;
+}
+
+/* Markdown preview */
+.markdown-preview {
+ background: #fff;
+ border-color: #ddd;
+}
+
+.markdown-preview code {
+ background: #f0f0f0;
+}
+
+/* Modal */
+.modal-content {
+ background: #fff;
+}
+
+/* Tree key colors */
+.type-string {
+ color: #4caf50;
+}
+
+.type-number {
+ color: #2196f3;
+}
+
+.type-boolean {
+ color: #9c27b0;
+}
+
+.type-null {
+ color: #666;
+}
+
+.type-object {
+ color: #ff9800;
+}
+
+.type-array {
+ color: #e91e63;
+}
+
+/* Drag styles */
+.tab-button.dragging {
+ border-color: #007bff;
+}
+
+/* Tooltip */
+.tab-rename-tooltip {
+ background-color: #fff;
+ border-color: #ccc;
+}
+
+body.dark-mode .tab-rename-tooltip {
+ background-color: #2c2c2c;
+ border-color: #555;
+ color: #fff;
+}
+
+body.dark-mode .tab-rename-tooltip input {
+ background-color: #1f1f1f;
+ color: #eee;
+ border-color: #555;
+}
+
+/* GitHub buttons */
+.github-button {
+ background-color: #24292e;
+}
+
+.github-button:hover {
+ background-color: #444;
+}
+
+.contribute-button {
+ background-color: #2ea44f;
+}
+
+.contribute-button:hover {
+ background-color: #22863a;
+}
+
+/* ToastUI Editor Dark Mode */
+body.dark-mode .toastui-editor-defaultUI {
+ background-color: #1e1e1e !important;
+ color: #eee !important;
+ border-color: #333 !important;
+}
+
+body.dark-mode .toastui-editor-defaultUI .ProseMirror {
+ background-color: #1e1e1e !important;
+ color: #eee !important;
+}
+
+body.dark-mode .toastui-editor-defaultUI .toastui-editor-toolbar {
+ background-color: #2a2a2a !important;
+ border-bottom-color: #444 !important;
+}
+
+body.dark-mode .toastui-editor-defaultUI .toastui-editor-contents pre,
+body.dark-mode .toastui-editor-defaultUI .toastui-editor-contents code {
+ background-color: #2b2b2b !important;
+ color: #c5c5c5 !important;
+}
+
+body.dark-mode .toastui-editor-defaultUI .toastui-editor-md-container,
+body.dark-mode .toastui-editor-defaultUI .toastui-editor-ww-container {
+ background-color: #1e1e1e !important;
+ color: #eee !important;
+}
+
+/* Tree View */
+body.dark-mode .preview-section {
+ background-color: #1e1e1e;
+ border-color: #333;
+}
+
+body.dark-mode .tree-view {
+ color: #e0e0e0;
+}
+
+/* Scrollbars */
+.tree-view::-webkit-scrollbar-track {
+ background: #f1f1f1;
+}
+
+.tree-view::-webkit-scrollbar-thumb {
+ background: #888;
+ border-color: #f1f1f1;
+}
+
+.tree-view::-webkit-scrollbar-thumb:hover {
+ background: #555;
+}
+
+body.dark-mode .tree-view::-webkit-scrollbar-track {
+ background: #1e1e1e;
+}
+
+body.dark-mode .tree-view::-webkit-scrollbar-thumb {
+ background: #666;
+ border-color: #1e1e1e;
+}
+
+body.dark-mode .tree-view::-webkit-scrollbar-thumb:hover {
+ background: #888;
+}
+
+/* Mobile sidebar toggle */
+body.dark-mode .mobile-sidebar-toggle {
+ background: #333;
+ color: #fff;
+}
+
+body.dark-mode .mobile-sidebar-close {
+ color: #fff;
+}
+
+/* Mock Data Table */
+.mock-preview-table {
+ background-color: #fff;
+}
+
+.mock-preview-table thead {
+ background-color: #f8f9fa;
+ border-bottom-color: #dee2e6;
+}
+
+.mock-preview-table th {
+ color: #495057;
+ background-color: #f8f9fa;
+ border-bottom-color: #dee2e6;
+}
+
+.mock-preview-table td {
+ color: #212529;
+ border-bottom-color: #dee2e6;
+}
+
+.mock-preview-table tbody tr:hover {
+ background-color: #f8f9fa;
+}
+
+/* Dark mode table */
+body.dark-mode .mock-preview-table {
+ background-color: #2d2d2d;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+}
+
+body.dark-mode .mock-preview-table thead {
+ background-color: #333;
+ border-bottom-color: #444;
+}
+
+body.dark-mode .mock-preview-table th {
+ color: #e0e0e0;
+ background-color: #333;
+ border-bottom-color: #444;
+}
+
+body.dark-mode .mock-preview-table td {
+ color: #e0e0e0;
+ border-bottom-color: #444;
+}
+
+body.dark-mode .mock-preview-table tbody tr:hover {
+ background-color: #383838;
+}
+
+body.dark-mode .mock-preview-table tr.error td {
+ background-color: #442326;
+ color: #ff6b6b;
+}
+
+/* JSON Code Output Themes */
+.code-output {
+ background-color: #1e1e1e;
+ color: #d4d4d4;
+}
+
+.code-output.python {
+ background-color: #0b2239;
+ color: #dcdcdc;
+}
+
+.code-output.go {
+ background-color: #263238;
+ color: #c3e88d;
+}
+
+.code-output.typescript {
+ background-color: #1e1e1e;
+ color: #d4d4d4;
+}
+
+.code-output .keyword {
+ color: #569cd6;
+}
+
+.code-output .string {
+ color: #ce9178;
+}
+
+.code-output .number {
+ color: #b5cea8;
+}
+
+.code-output .type {
+ color: #4ec9b0;
+}
+
+.code-output .comment {
+ color: #6a9955;
+}
diff --git a/json-tool/public/assets/js/app.js b/json-tool/public/assets/js/app.js
new file mode 100644
index 0000000..7105902
--- /dev/null
+++ b/json-tool/public/assets/js/app.js
@@ -0,0 +1,112 @@
+/* ========== Mode Selector ========== */
+function switchMode(mode) {
+ const sections = [
+ "formatter",
+ "compare",
+ "codegen",
+ "convert",
+ "mockgen",
+ "editor",
+ ];
+ sections.forEach((s) => {
+ const section = document.getElementById(`${s}-section`);
+ if (section) {
+ section.style.display = "none";
+ }
+ });
+
+ const targetSection = document.getElementById(`${mode}-section`);
+ if (targetSection) {
+ targetSection.style.display = "block";
+ }
+
+ // Update sidebar active state
+ document.querySelectorAll(".feature-item").forEach((item) => {
+ item.classList.remove("active");
+ });
+ document
+ .querySelector(`.feature-item[onclick*="${mode}"]`)
+ ?.classList.add("active");
+
+ if (mode === "mockgen") {
+ renderMockgenDocs();
+ } else if (mode === "editor") {
+ loadEditorGlobalState();
+ }
+
+ applyEditorTabDarkMode();
+ saveGlobalState(); // Save state when mode changes
+}
+
+/* ========== Keyboard Shortcuts ========== */
+document.addEventListener("keydown", (e) => {
+ if (
+ e.ctrlKey &&
+ e.key.toLowerCase() === "t" &&
+ document.getElementById("formatter-section").style.display !== "none"
+ ) {
+ e.preventDefault();
+ addFormatterTab();
+ }
+ if (
+ e.ctrlKey &&
+ e.key.toLowerCase() === "w" &&
+ document.getElementById("formatter-section").style.display !== "none"
+ ) {
+ e.preventDefault();
+ const activeTab = document.querySelector(
+ "#formatter-tab-contents .json-tab-content.active"
+ );
+ if (activeTab) closeFormatterTab(activeTab.id);
+ }
+ if (e.ctrlKey && (e.key === "/" || e.key === "?")) {
+ e.preventDefault();
+ toggleShortcutModal();
+ }
+ if (e.key === "Escape") {
+ const modal = document.getElementById("shortcut-modal");
+ if (modal.style.display === "block") toggleShortcutModal();
+ }
+ if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "s") {
+ e.preventDefault();
+ const activeTab = document.querySelector(
+ "#editor-tab-contents .json-tab-content.active"
+ );
+ if (activeTab) saveEditorContent(activeTab.id);
+ }
+});
+
+/* ========== Initialization ========== */
+window.addEventListener("load", () => {
+ loadGlobalState();
+ loadEditorGlobalState();
+ // If no saved state, create a default tabs.
+ if (document.getElementById("formatter-tab-contents").children.length === 0) {
+ addFormatterTab();
+ }
+
+ if (document.getElementById("compare-tab-contents").children.length === 0) {
+ addCompareTab();
+ }
+
+ if (document.getElementById("codegen-tab-contents").children.length === 0) {
+ addCodegenTab();
+ }
+
+ if (document.getElementById("editor-tab-contents").children.length === 0) {
+ addEditorTab();
+ switchEditorTab("editor-tab-1");
+ }
+});
+
+setInterval(() => {
+ const editorVisible =
+ document.getElementById("editor-section").style.display !== "none";
+ const activeTab = document.querySelector(
+ "#editor-tab-contents .json-tab-content.active"
+ );
+
+ if (!Swal.isVisible() && editorVisible && activeTab) {
+ saveEditorContent(activeTab.id);
+ }
+}, 5000);
diff --git a/json-tool/public/assets/js/core/dom.js b/json-tool/public/assets/js/core/dom.js
new file mode 100644
index 0000000..194d5cb
--- /dev/null
+++ b/json-tool/public/assets/js/core/dom.js
@@ -0,0 +1,217 @@
+/* ========== COPY FUNCTIONS ========== */
+function copyToClipboard(text, successMessage) {
+ (navigator.clipboard?.writeText
+ ? navigator.clipboard.writeText(text)
+ : new Promise((res, rej) => {
+ try {
+ const ta = Object.assign(document.createElement("textarea"), {
+ value: text,
+ style: "position:fixed;top:-1000px",
+ });
+ document.body.appendChild(ta);
+ ta.select();
+ document.execCommand("copy") ? res() : rej();
+ ta.remove();
+ } catch (e) {
+ rej(e);
+ }
+ })
+ )
+ .then(() => {
+ Swal.fire({
+ icon: "success",
+ title: successMessage,
+ toast: true,
+ timer: 2000,
+ position: "top-end",
+ showConfirmButton: false,
+ });
+ })
+ .catch((err) => {
+ console.error("Failed to copy:", err);
+ Swal.fire({
+ icon: "error",
+ title: "Copy failed",
+ toast: true,
+ timer: 2000,
+ position: "top-end",
+ showConfirmButton: false,
+ });
+ });
+}
+
+function autoFormatTextarea(textarea) {
+ try {
+ const parsed = JSON.parse(textarea.value);
+ textarea.value = JSON.stringify(parsed, null, 2);
+ } catch (e) {
+ // Do nothing if invalid
+ }
+}
+
+function openTabRenameTooltip(tabId, mode) {
+ let containerSelector;
+ if (mode === "formatter") containerSelector = "#formatter-tabs-container";
+ else if (mode === "compare") containerSelector = "#compare-tabs-container";
+ else if (mode === "codegen") containerSelector = "#codegen-tabs-container";
+ else containerSelector = "#editor-tabs-container";
+
+ const tabButton = document.querySelector(
+ containerSelector + ` .tab-button[data-tab="${tabId}"]`
+ );
+ const existingTooltip = document.querySelector(".tab-rename-tooltip");
+ if (existingTooltip) existingTooltip.remove();
+ const tooltip = document.createElement("div");
+ tooltip.className = "tab-rename-tooltip";
+ const rect = tabButton.getBoundingClientRect();
+ tooltip.style.left = rect.left + "px";
+ tooltip.style.top = rect.bottom + window.scrollY + 5 + "px";
+ const input = document.createElement("input");
+ input.type = "text";
+ input.value = tabButton.querySelector(".tab-name").textContent;
+ input.style.width = "150px";
+ input.addEventListener("keydown", (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();
+
+ function finalizeRename() {
+ const newName = input.value.trim();
+ if (newName) {
+ tabButton.querySelector(".tab-name").textContent = newName;
+ }
+ tooltip.remove();
+ saveGlobalState();
+ }
+}
+
+/* ========== Mobile Sidebar Functions ========== */
+function toggleSidebar() {
+ const sidebar = document.querySelector(".sidebar");
+ sidebar.classList.toggle("active");
+
+ // Close sidebar when clicking outside
+ if (sidebar.classList.contains("active")) {
+ const closeOnClickOutside = (e) => {
+ if (
+ !sidebar.contains(e.target) &&
+ !e.target.matches(".mobile-sidebar-toggle")
+ ) {
+ sidebar.classList.remove("active");
+ document.removeEventListener("click", closeOnClickOutside);
+ }
+ };
+ // Add event listener with a slight delay to prevent immediate closing
+ setTimeout(() => {
+ document.addEventListener("click", closeOnClickOutside);
+ }, 100);
+ }
+}
+
+// Add touch event handling for better mobile experience
+document.addEventListener("DOMContentLoaded", () => {
+ let touchStartX = 0;
+ let touchEndX = 0;
+ const sidebar = document.querySelector(".sidebar");
+
+ document.addEventListener(
+ "touchstart",
+ (e) => {
+ touchStartX = e.changedTouches[0].screenX;
+ },
+ false
+ );
+
+ document.addEventListener(
+ "touchend",
+ (e) => {
+ touchEndX = e.changedTouches[0].screenX;
+ handleSwipe();
+ },
+ false
+ );
+
+ function handleSwipe() {
+ const swipeThreshold = 50;
+ const swipeLength = touchEndX - touchStartX;
+
+ // Swipe right to open sidebar
+ if (swipeLength > swipeThreshold && touchStartX < 30) {
+ sidebar.classList.add("active");
+ }
+ // Swipe left to close sidebar
+ else if (
+ swipeLength < -swipeThreshold &&
+ sidebar.classList.contains("active")
+ ) {
+ sidebar.classList.remove("active");
+ }
+ }
+});
+
+/* ========== 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);
+ });
+ }
+}
+
+/* ========== Tab reordering ========== */
+function enableTabReordering(containerId) {
+ const container = document.getElementById(containerId);
+ const tabButtons = container.querySelectorAll(".tab-button[data-tab]");
+
+ tabButtons.forEach((button) => {
+ button.draggable = true;
+
+ button.addEventListener("dragstart", (e) => {
+ e.dataTransfer.setData("text/plain", button.getAttribute("data-tab"));
+ button.classList.add("dragging");
+ });
+
+ button.addEventListener("dragend", () => {
+ button.classList.remove("dragging");
+ container
+ .querySelectorAll(".tab-button")
+ .forEach((b) => b.classList.remove("drag-over"));
+ });
+
+ button.addEventListener("dragover", (e) => {
+ e.preventDefault();
+ button.classList.add("drag-over");
+ });
+
+ button.addEventListener("dragleave", () => {
+ button.classList.remove("drag-over");
+ });
+
+ button.addEventListener("drop", (e) => {
+ e.preventDefault();
+ const draggedId = e.dataTransfer.getData("text/plain");
+ const draggedBtn = container.querySelector(`[data-tab="${draggedId}"]`);
+
+ button.classList.remove("drag-over");
+
+ if (draggedBtn && draggedBtn !== button) {
+ container.insertBefore(draggedBtn, button);
+ saveGlobalState();
+ }
+ });
+ });
+}
diff --git a/json-tool/public/assets/js/core/json-utils.js b/json-tool/public/assets/js/core/json-utils.js
new file mode 100644
index 0000000..4d3dfd1
--- /dev/null
+++ b/json-tool/public/assets/js/core/json-utils.js
@@ -0,0 +1,47 @@
+function capitalize(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}
+
+function formatValue(value) {
+ if (value === null) return "null";
+ if (typeof value === "string") return `"${value}"`;
+ return value.toString();
+}
+
+function getValueTypeClass(value) {
+ if (value === null) return "type-null";
+ if (Array.isArray(value)) return "type-array";
+ if (typeof value === "object") return "type-object";
+ return `type-${typeof value}`;
+}
+
+function diffJSONsPreview(leftText, rightText) {
+ const isDarkMode = document.body.classList.contains("dark-mode");
+ // Choose a diff color based on the theme:
+ // For light mode, use a light red; for dark mode, use a darker or more muted red.
+ const diffStyle = isDarkMode
+ ? "background-color:#662222;"
+ : "background-color:#ddd;";
+
+ const leftLines = leftText.split("\n");
+ const rightLines = rightText.split("\n");
+ const maxLines = Math.max(leftLines.length, rightLines.length);
+ let html = "";
+ for (let i = 0; i < maxLines; i++) {
+ const lLine = leftLines[i] || "";
+ const rLine = rightLines[i] || "";
+ // Apply diffStyle if the lines differ
+ const style = lLine === rLine ? "" : diffStyle;
+ html += `
+
+ ${lLine}
+ |
+
+ ${rLine}
+ |
+
`;
+ }
+ html += "
";
+ return html;
+}
+
diff --git a/json-tool/public/assets/js/core/storage.js b/json-tool/public/assets/js/core/storage.js
new file mode 100644
index 0000000..dda2589
--- /dev/null
+++ b/json-tool/public/assets/js/core/storage.js
@@ -0,0 +1,160 @@
+/* ========== Global Persistence Functions ========== */
+function getActiveMode() {
+ // Find the active feature item in the sidebar
+ const activeFeature = document.querySelector(".feature-item.active");
+ if (activeFeature) {
+ // Extract mode from the onclick attribute
+ const onclickAttr = activeFeature.getAttribute("onclick");
+ const match = onclickAttr.match(/switchMode\('(.+?)'\)/);
+ if (match) return match[1];
+ }
+
+ // Fallback to checking sections
+ 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";
+ if (document.getElementById("convert-section").style.display !== "none")
+ return "convert";
+ if (document.getElementById("mockgen-section").style.display !== "none")
+ return "mockgen";
+ if (document.getElementById("editor-section").style.display !== "none")
+ return "editor";
+ return "formatter"; // Default fallback
+}
+
+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: [],
+ activeFeatureTab: {}, // Store active feature tab (Raw/Tree/Error) for each formatter tab
+ },
+ 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));
+}
+
+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 Feature Mode (Formatter/Compare/Codegen/etc.)
+ if (state.activeMode) {
+ switchMode(state.activeMode);
+ } else {
+ switchMode("formatter"); // Default fallback
+ }
+
+ // 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);
+
+ enableTabReordering("formatter-tabs-container");
+ enableTabReordering("compare-tabs-container");
+ enableTabReordering("codegen-tabs-container");
+}
+
diff --git a/json-tool/public/assets/js/features/codegen/codegen.js b/json-tool/public/assets/js/features/codegen/codegen.js
new file mode 100644
index 0000000..6a81919
--- /dev/null
+++ b/json-tool/public/assets/js/features/codegen/codegen.js
@@ -0,0 +1,328 @@
+let codegenTabCount = 0;
+
+function addCodegenTab() {
+ createCodegenTab();
+ switchCodegenTab("codegenTab" + codegenTabCount);
+ saveGlobalState();
+}
+
+function createCodegenTab() {
+ codegenTabCount++;
+ const tabId = "codegenTab" + codegenTabCount;
+ // Create tab button
+ const tabButton = document.createElement("button");
+ tabButton.className = "tab-button";
+ tabButton.setAttribute("data-tab", tabId);
+ tabButton.onclick = () => switchCodegenTab(tabId);
+
+ const nameSpan = document.createElement("span");
+ nameSpan.className = "tab-name";
+ nameSpan.textContent = `Tab ${codegenTabCount}`;
+
+ const closeSpan = document.createElement("span");
+ closeSpan.className = "close-tab";
+ closeSpan.textContent = "ร";
+ closeSpan.onclick = (e) => closeCodegenTab(tabId, e);
+
+ tabButton.append(nameSpan, closeSpan);
+
+ tabButton.addEventListener("dblclick", () =>
+ openTabRenameTooltip(tabId, "codegen")
+ );
+ const tabsContainer = document.getElementById("codegen-tabs-container");
+ const addButton = tabsContainer.querySelector(".add-tab-button");
+ tabsContainer.insertBefore(tabButton, addButton);
+ enableTabReordering("codegen-tabs-container");
+
+ // Create tab content
+ const tabContent = document.createElement("div");
+ tabContent.id = tabId;
+ tabContent.className = "json-tab-content";
+ tabContent.innerHTML = `
+
+
+
+
+
+
+
+
+ `;
+ document.getElementById("codegen-tab-contents").appendChild(tabContent);
+ const textarea = tabContent.querySelector(".json-input");
+ textarea.addEventListener("paste", () =>
+ setTimeout(() => autoFormatTextarea(textarea), 100)
+ );
+ textarea.addEventListener("blur", () => autoFormatTextarea(textarea));
+ saveGlobalState();
+}
+
+function createCodegenTabWithData(tabData) {
+ codegenTabCount++;
+ const tabId = tabData.id;
+ const tabButton = document.createElement("button");
+ tabButton.className = "tab-button";
+ tabButton.setAttribute("data-tab", tabId);
+ tabButton.onclick = () => switchCodegenTab(tabId);
+
+ const nameSpan = document.createElement("span");
+ nameSpan.className = "tab-name";
+ nameSpan.textContent = tabData.name;
+
+ const closeSpan = document.createElement("span");
+ closeSpan.className = "close-tab";
+ closeSpan.textContent = "ร";
+ closeSpan.onclick = (e) => closeCodegenTab(tabId, e);
+
+ tabButton.append(nameSpan, closeSpan);
+
+ tabButton.addEventListener("dblclick", () =>
+ openTabRenameTooltip(tabId, "codegen")
+ );
+ const tabsContainer = document.getElementById("codegen-tabs-container");
+ const addButton = tabsContainer.querySelector(".add-tab-button");
+ tabsContainer.insertBefore(tabButton, addButton);
+ const tabContent = document.createElement("div");
+ tabContent.id = tabId;
+ tabContent.className = "json-tab-content";
+ tabContent.innerHTML = `
+
+
+
+
+
+
+
+ `;
+ document.getElementById("codegen-tab-contents").appendChild(tabContent);
+ const textarea = tabContent.querySelector(".json-input");
+ textarea.value = tabData.input;
+ const selectElem = document.getElementById("lang-select-" + tabId);
+ selectElem.value = tabData.lang;
+ textarea.addEventListener("paste", () =>
+ setTimeout(() => autoFormatTextarea(textarea), 100)
+ );
+ textarea.addEventListener("blur", () => autoFormatTextarea(textarea));
+ saveGlobalState();
+ enableTabReordering("codegen-tabs-container");
+}
+
+function switchCodegenTab(tabId) {
+ document
+ .querySelectorAll("#codegen-tab-contents .json-tab-content")
+ .forEach((tab) => tab.classList.remove("active"));
+ const selectedTab = document.getElementById(tabId);
+ if (selectedTab) selectedTab.classList.add("active");
+ document
+ .querySelectorAll("#codegen-tabs-container .tab-button[data-tab]")
+ .forEach((btn) => {
+ btn.classList.toggle("active", btn.getAttribute("data-tab") === tabId);
+ });
+ saveGlobalState();
+}
+
+function generateCode(tabId) {
+ const tabContent = document.getElementById(tabId);
+ const textarea = tabContent.querySelector(".json-input");
+ const langSelect = document.getElementById("lang-select-" + tabId);
+ const outputPre = tabContent.querySelector(".code-output");
+ const inputText = textarea.value;
+ const lang = langSelect.value;
+ let obj;
+ try {
+ obj = JSON.parse(inputText);
+ } catch (e) {
+ outputPre.textContent = "Invalid JSON: " + e.message;
+ return;
+ }
+ let code = "";
+ if (lang === "typescript") code = generateTypeScript(obj, "Root");
+ else if (lang === "python") code = generatePython(obj, "Root");
+ else if (lang === "go") code = generateGo(obj, "Root");
+ outputPre.textContent = code;
+ saveGlobalState();
+}
+
+async function closeCodegenTab(tabId, event) {
+ if (event) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ const result = await Swal.fire({
+ title: "Close tab?",
+ text: "Are you sure you want to close this tab?",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonText: "Yes",
+ cancelButtonText: "No",
+ });
+
+ if (!result.isConfirmed) return;
+
+ const tabButton = document.querySelector(
+ `#codegen-tabs-container .tab-button[data-tab="${tabId}"]`
+ );
+ const tabContent = document.getElementById(tabId);
+ if (tabButton) tabButton.remove();
+ if (tabContent) tabContent.remove();
+ const remaining = document.querySelectorAll(
+ "#codegen-tab-contents .json-tab-content"
+ );
+ if (remaining.length > 0)
+ switchCodegenTab(remaining[remaining.length - 1].id);
+ saveGlobalState();
+}
+
+function copyCodeOutput(tabId) {
+ const codePre = document.querySelector(`#${tabId} .code-output`);
+ copyToClipboard(codePre.textContent, "Code copied");
+}
+
+function generateTypeScript(obj, interfaceName) {
+ let result = `interface ${interfaceName} {\n`;
+ for (const key in obj) {
+ if (!Object.hasOwn(obj, key)) continue;
+ const value = obj[key];
+ if (value === null) {
+ result += ` ${key}: any;\n`;
+ } else if (Array.isArray(value)) {
+ if (value.length > 0) {
+ const elem = value[0];
+ if (typeof elem === "object" && elem !== null) {
+ const subInterface = interfaceName + capitalize(key);
+ result += ` ${key}: ${subInterface}[];\n`;
+ result += generateTypeScript(elem, subInterface);
+ } else {
+ result += ` ${key}: ${typeof elem}[];\n`;
+ }
+ } else {
+ result += ` ${key}: any[];\n`;
+ }
+ } else if (typeof value === "object") {
+ const subInterface = interfaceName + capitalize(key);
+ result += ` ${key}: ${subInterface};\n`;
+ result += generateTypeScript(value, subInterface);
+ } else {
+ result += ` ${key}: ${typeof value};\n`;
+ }
+ }
+ result += "}\n";
+ return result;
+}
+
+function generatePython(obj, className, isRoot = true) {
+ let result = "";
+ if (isRoot) {
+ result +=
+ "from dataclasses import dataclass\nfrom typing import Any, List\n\n";
+ }
+ result += `@dataclass\\nclass ${className}:\\n`;
+ for (const key in obj) {
+ if (!Object.hasOwn(obj, key)) continue;
+ const value = obj[key];
+ let pyType = "Any";
+ if (value === null) {
+ pyType = "Any";
+ } else if (Array.isArray(value)) {
+ if (value.length > 0) {
+ const elem = value[0];
+ if (typeof elem === "object" && elem !== null) {
+ const subClass = className + capitalize(key);
+ pyType = `List[${subClass}]`;
+ result += "\\n" + generatePython(elem, subClass, false);
+ } else {
+ if (typeof elem === "number") pyType = "List[float]";
+ else if (typeof elem === "string") pyType = "List[str]";
+ else if (typeof elem === "boolean") pyType = "List[bool]";
+ else pyType = "List[Any]";
+ }
+ } else {
+ pyType = "List[Any]";
+ }
+ } else if (typeof value === "object") {
+ const subClass = className + capitalize(key);
+ pyType = subClass;
+ result += "\\n" + generatePython(value, subClass, false);
+ } else {
+ if (typeof value === "number") pyType = "float";
+ else if (typeof value === "string") pyType = "str";
+ else if (typeof value === "boolean") pyType = "bool";
+ }
+ result += ` ${key}: ${pyType}\n`;
+ }
+ return result;
+}
+
+function generateGo(obj, structName) {
+ let result = `type ${structName} struct {\n`;
+ for (const key in obj) {
+ if (!Object.hasOwn(obj, key)) continue;
+ const value = obj[key];
+ let goType = "interface{}";
+ if (value === null) {
+ goType = "interface{}";
+ } else if (Array.isArray(value)) {
+ if (value.length > 0) {
+ // Scan all elements for type consistency
+ let firstType = null;
+ let uniform = true;
+ let subStruct = structName + capitalize(key);
+ for (let i = 0; i < value.length; i++) {
+ const elem = value[i];
+ let elemType;
+ if (elem === null) {
+ elemType = "interface{}";
+ } else if (typeof elem === "object") {
+ elemType = subStruct;
+ } else if (typeof elem === "number") {
+ elemType = "float64";
+ } else if (typeof elem === "string") {
+ elemType = "string";
+ } else if (typeof elem === "boolean") {
+ elemType = "bool";
+ } else {
+ elemType = "interface{}";
+ }
+ if (firstType === null) {
+ firstType = elemType;
+ } else if (firstType !== elemType) {
+ uniform = false;
+ break;
+ }
+ }
+ if (uniform) {
+ if (firstType === subStruct) {
+ // If array of objects, generate sub-struct
+ result += generateGo(value[0], subStruct);
+ }
+ goType = `[]${firstType}`;
+ } else {
+ goType = "[]interface{}";
+ }
+ } else {
+ goType = "[]interface{}";
+ }
+ } else if (typeof value === "object") {
+ const subStruct = structName + capitalize(key);
+ goType = subStruct;
+ result += generateGo(value, subStruct);
+ } else {
+ if (typeof value === "number") goType = "float64";
+ else if (typeof value === "string") goType = "string";
+ else if (typeof value === "boolean") goType = "bool";
+ }
+ const fieldName = key.charAt(0).toUpperCase() + key.slice(1);
+ result += ` ${fieldName} ${goType} \`json:"${key}"\`\n`;
+ }
+ result += "}\n";
+ return result;
+}
diff --git a/json-tool/public/assets/js/features/compare/compare.js b/json-tool/public/assets/js/features/compare/compare.js
new file mode 100644
index 0000000..fb85c3e
--- /dev/null
+++ b/json-tool/public/assets/js/features/compare/compare.js
@@ -0,0 +1,187 @@
+let compareTabCount = 0;
+
+function addCompareTab() {
+ createCompareTab();
+ switchCompareTab("compareTab" + compareTabCount);
+ saveGlobalState();
+}
+
+function createCompareTab() {
+ compareTabCount++;
+ const tabId = "compareTab" + compareTabCount;
+ // Create tab button
+ const tabButton = document.createElement("button");
+ tabButton.className = "tab-button";
+ tabButton.setAttribute("data-tab", tabId);
+ tabButton.onclick = () => switchCompareTab(tabId);
+ tabButton.innerHTML = `Tab ${compareTabCount}
+ ร`;
+ tabButton.addEventListener("dblclick", () =>
+ openTabRenameTooltip(tabId, "compare")
+ );
+ const tabsContainer = document.getElementById("compare-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 = `
+
+
+
+
+
+
+ `;
+ document.getElementById("compare-tab-contents").appendChild(tabContent);
+ const leftTA = tabContent.querySelector(".json-input-left");
+ const rightTA = tabContent.querySelector(".json-input-right");
+ leftTA.addEventListener("paste", () =>
+ setTimeout(() => autoFormatTextarea(leftTA), 100)
+ );
+ leftTA.addEventListener("blur", () => autoFormatTextarea(leftTA));
+ rightTA.addEventListener("paste", () =>
+ setTimeout(() => autoFormatTextarea(rightTA), 100)
+ );
+ rightTA.addEventListener("blur", () => autoFormatTextarea(rightTA));
+ saveGlobalState();
+ enableTabReordering("compare-tabs-container");
+}
+
+// Create Compare tab using saved data
+function createCompareTabWithData(tabData) {
+ compareTabCount++;
+ const tabId = tabData.id;
+ const tabButton = document.createElement("button");
+ tabButton.className = "tab-button";
+ tabButton.setAttribute("data-tab", tabId);
+ tabButton.onclick = () => switchCompareTab(tabId);
+ tabButton.innerHTML = `${tabData.name}
+ ร`;
+ tabButton.addEventListener("dblclick", () =>
+ openTabRenameTooltip(tabId, "compare")
+ );
+ const tabsContainer = document.getElementById("compare-tabs-container");
+ const addButton = tabsContainer.querySelector(".add-tab-button");
+ tabsContainer.insertBefore(tabButton, addButton);
+ const tabContent = document.createElement("div");
+ tabContent.id = tabId;
+ tabContent.className = "json-tab-content";
+ tabContent.innerHTML = `
+
+
+
+
+
+
+ `;
+ document.getElementById("compare-tab-contents").appendChild(tabContent);
+ const leftTA = tabContent.querySelector(".json-input-left");
+ const rightTA = tabContent.querySelector(".json-input-right");
+ leftTA.value = tabData.leftContent;
+ rightTA.value = tabData.rightContent;
+ leftTA.addEventListener("paste", () =>
+ setTimeout(() => autoFormatTextarea(leftTA), 100)
+ );
+ leftTA.addEventListener("blur", () => autoFormatTextarea(leftTA));
+ rightTA.addEventListener("paste", () =>
+ setTimeout(() => autoFormatTextarea(rightTA), 100)
+ );
+ rightTA.addEventListener("blur", () => autoFormatTextarea(rightTA));
+ saveGlobalState();
+ enableTabReordering("compare-tabs-container");
+}
+
+function switchCompareTab(tabId) {
+ document
+ .querySelectorAll("#compare-tab-contents .json-tab-content")
+ .forEach((tab) => tab.classList.remove("active"));
+ const selectedTab = document.getElementById(tabId);
+ if (selectedTab) selectedTab.classList.add("active");
+ document
+ .querySelectorAll("#compare-tabs-container .tab-button[data-tab]")
+ .forEach((btn) => {
+ btn.classList.toggle("active", btn.getAttribute("data-tab") === tabId);
+ });
+ saveGlobalState();
+}
+
+function compareJSONs(tabId) {
+ const tabContent = document.getElementById(tabId);
+ const leftTA = tabContent.querySelector(".json-input-left");
+ const rightTA = tabContent.querySelector(".json-input-right");
+ const resultDiv = tabContent.querySelector(".compare-result");
+ const leftText = leftTA.value;
+ const rightText = rightTA.value;
+ let leftObj;
+ let rightObj;
+ try {
+ leftObj = JSON.parse(leftText);
+ } catch (e) {
+ resultDiv.textContent = "Left JSON Error: " + e.message;
+ return;
+ }
+ try {
+ rightObj = JSON.parse(rightText);
+ } catch (e) {
+ resultDiv.textContent = "Right JSON Error: " + e.message;
+ return;
+ }
+ const leftFormatted = JSON.stringify(leftObj, null, 2);
+ const rightFormatted = JSON.stringify(rightObj, null, 2);
+ const diffPreview = diffJSONsPreview(leftFormatted, rightFormatted);
+ const sanitizedDiffPreview = DOMPurify.sanitize(diffPreview);
+
+ resultDiv.innerHTML = `
+
+
+
+
+ ${sanitizedDiffPreview}
+ `;
+ leftTA.value = leftFormatted;
+ rightTA.value = rightFormatted;
+ saveGlobalState();
+}
+
+async function closeCompareTab(tabId, event) {
+ if (event) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ const result = await Swal.fire({
+ title: "Close tab?",
+ text: "Are you sure you want to close this tab?",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonText: "Yes",
+ cancelButtonText: "No",
+ });
+
+ if (!result.isConfirmed) return;
+
+ const tabButton = document.querySelector(
+ `#compare-tabs-container .tab-button[data-tab="${tabId}"]`
+ );
+ const tabContent = document.getElementById(tabId);
+ if (tabButton) tabButton.remove();
+ if (tabContent) tabContent.remove();
+ const remaining = document.querySelectorAll(
+ "#compare-tab-contents .json-tab-content"
+ );
+ if (remaining.length > 0)
+ switchCompareTab(remaining[remaining.length - 1].id);
+ saveGlobalState();
+}
+
+function copyCompareLeft(tabId) {
+ const leftTA = document.querySelector(`#${tabId} .json-input-left`);
+ copyToClipboard(leftTA.value, "Left JSON copied");
+}
+
+function copyCompareRight(tabId) {
+ const rightTA = document.querySelector(`#${tabId} .json-input-right`);
+ copyToClipboard(rightTA.value, "Right JSON copied");
+}
diff --git a/json-tool/public/assets/js/features/convert/convert.js b/json-tool/public/assets/js/features/convert/convert.js
new file mode 100644
index 0000000..eeb1ae8
--- /dev/null
+++ b/json-tool/public/assets/js/features/convert/convert.js
@@ -0,0 +1,55 @@
+let currentConvertMode = "dict-to-json";
+
+function switchConvertDirection(mode) {
+ currentConvertMode = mode;
+ document.querySelectorAll("#convert-section .tab-button").forEach((btn) => {
+ btn.classList.toggle(
+ "active",
+ btn.textContent.includes("Dict") === (mode === "dict-to-json")
+ );
+ });
+ document.getElementById("convert-input").value = "";
+ document.getElementById("convert-output").textContent = "";
+}
+
+function convert() {
+ const input = document.getElementById("convert-input").value;
+ const output = document.getElementById("convert-output");
+
+ try {
+ if (currentConvertMode === "dict-to-json") {
+ // 1) None/True/False โ null/true/false
+ let json = input
+ .replace(/\bNone\b/g, "null")
+ .replace(/\bTrue\b/g, "true")
+ .replace(/\bFalse\b/g, "false");
+ // 2) remove trailing commas before } or ]
+ json = json.replace(/,\s*(?=[}\]])/g, "");
+ // 3) convert only quoted 'strings' โ "strings"
+ json = json.replace(/'([^']*?)'/g, '"$1"');
+
+ const parsed = JSON.parse(json);
+ output.textContent = JSON.stringify(parsed, null, 2);
+ } else {
+ // JSON โ Python dict
+ const obj = JSON.parse(input);
+ let dictStr = JSON.stringify(obj, null, 2);
+ // 1) only replace "quoted" strings โ 'strings'
+ dictStr = dictStr.replace(/"([^"]*?)"/g, "'$1'");
+ // 2) true/false/null โ True/False/None
+ dictStr = dictStr
+ .replace(/\btrue\b/g, "True")
+ .replace(/\bfalse\b/g, "False")
+ .replace(/\bnull\b/g, "None");
+
+ output.textContent = dictStr;
+ }
+ } catch (e) {
+ output.textContent = "Error: " + e.message;
+ }
+}
+
+function copyConvertOutput() {
+ const output = document.getElementById("convert-output").textContent;
+ copyToClipboard(output, "Copied!");
+}
diff --git a/json-tool/public/assets/js/features/editor/editor.js b/json-tool/public/assets/js/features/editor/editor.js
new file mode 100644
index 0000000..cf2063c
--- /dev/null
+++ b/json-tool/public/assets/js/features/editor/editor.js
@@ -0,0 +1,177 @@
+let editorTabCount = 0;
+const editorInstances = {};
+
+function addEditorTab(tabData = null) {
+ let tabId;
+ if (tabData?.id) {
+ tabId = tabData.id;
+ const match = tabId.match(/editor-tab-(\d+)/);
+ if (match && Number.parseInt(match[1], 10) > editorTabCount) {
+ editorTabCount = Number.parseInt(match[1], 10);
+ }
+ } else {
+ editorTabCount++;
+ tabId = `editor-tab-${editorTabCount}`;
+ }
+
+ const tabButton = document.createElement("button");
+ tabButton.className = "tab-button";
+ tabButton.setAttribute("data-tab", tabId);
+ tabButton.innerHTML = `${
+ tabData?.title || `Note ${editorTabCount}`
+ }ร`;
+ tabButton.onclick = () => switchEditorTab(tabId);
+ tabButton.addEventListener("dblclick", () =>
+ openTabRenameTooltip(tabId, "editor")
+ );
+
+ document
+ .getElementById("editor-tabs-container")
+ .insertBefore(
+ tabButton,
+ document.querySelector("#editor-tabs-container .add-tab-button")
+ );
+
+ const tabContent = document.createElement("div");
+ tabContent.id = tabId;
+ tabContent.className = "json-tab-content";
+ tabContent.innerHTML = `
+
+
+
+
+ `;
+
+ document.getElementById("editor-tab-contents").appendChild(tabContent);
+
+ const editor = new toastui.Editor({
+ el: document.getElementById(`${tabId}-editor`),
+ height: "400px",
+ initialEditType: "markdown",
+ previewStyle: "vertical",
+ });
+ editorInstances[tabId] = editor;
+
+ const saved = localStorage.getItem(tabId);
+ if (saved) editor.setMarkdown(saved);
+
+ enableTabReordering("editor-tabs-container");
+ applyEditorTabDarkMode();
+}
+
+function switchEditorTab(tabId) {
+ document
+ .querySelectorAll("#editor-tab-contents .json-tab-content")
+ .forEach((el) => el.classList.remove("active"));
+ document.getElementById(tabId)?.classList.add("active");
+
+ document
+ .querySelectorAll("#editor-tabs-container .tab-button")
+ .forEach((btn) => {
+ btn.classList.toggle("active", btn.getAttribute("data-tab") === tabId);
+ });
+ updateEditorGlobalState();
+}
+
+function saveEditorContent(tabId, silent = false) {
+ const content = editorInstances[tabId].getMarkdown();
+ localStorage.setItem(tabId, content);
+ updateEditorGlobalState();
+ if (!silent) {
+ Swal.fire({
+ toast: true,
+ position: "top-end",
+ icon: "success",
+ title: "Saved!", // Changed from Autosaved to Saved!
+ showConfirmButton: false,
+ timer: 1500,
+ });
+ }
+}
+
+async function deleteEditorTab(tabId, event) {
+ if (event) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ const result = await Swal.fire({
+ title: "Close tab?",
+ text: "Are you sure you want to close this tab?",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonText: "Yes",
+ cancelButtonText: "No",
+ });
+
+ if (!result.isConfirmed) return;
+
+ localStorage.removeItem(tabId);
+ editorInstances[tabId]?.destroy();
+ delete editorInstances[tabId];
+ document
+ .querySelector(`#editor-tabs-container .tab-button[data-tab="${tabId}"]`)
+ ?.remove();
+ document.getElementById(tabId)?.remove();
+ const remaining = document.querySelectorAll(
+ "#editor-tab-contents .json-tab-content"
+ );
+ if (remaining.length > 0) switchEditorTab(remaining[0].id);
+ updateEditorGlobalState();
+}
+
+function updateEditorGlobalState() {
+ const state = {
+ activeTab:
+ document.querySelector("#editor-tab-contents .json-tab-content.active")
+ ?.id || "",
+ tabs: [],
+ };
+ document
+ .querySelectorAll("#editor-tabs-container .tab-button[data-tab]")
+ .forEach((btn) => {
+ const tabId = btn.getAttribute("data-tab");
+ const title = btn.querySelector(".tab-name").textContent;
+ state.tabs.push({ id: tabId, title });
+ });
+ localStorage.setItem("editorState", JSON.stringify(state));
+}
+
+function loadEditorGlobalState() {
+ const stateStr = localStorage.getItem("editorState");
+ const container = document.getElementById("editor-tabs-container");
+ container
+ .querySelectorAll(".tab-button[data-tab]")
+ .forEach((btn) => btn.remove());
+ document.getElementById("editor-tab-contents").innerHTML = "";
+ editorTabCount = 0;
+
+ if (!stateStr) {
+ addEditorTab();
+ return;
+ }
+
+ const state = JSON.parse(stateStr);
+ state.tabs.forEach((tabData) => addEditorTab(tabData));
+
+ if (state.activeTab && document.getElementById(state.activeTab)) {
+ switchEditorTab(state.activeTab);
+ } else if (state.tabs.length > 0) {
+ switchEditorTab(state.tabs[0].id);
+ }
+}
+
+function applyEditorTabDarkMode() {
+ const container = document.getElementById("editor-tabs-container");
+ container.querySelectorAll(".tab-button").forEach((btn) => {
+ if (document.body.classList.contains("dark-mode")) {
+ btn.style.backgroundColor = "#2c2c2c";
+ btn.style.color = "#eee";
+ btn.style.borderColor = "#444";
+ } else {
+ btn.style.backgroundColor = "";
+ btn.style.color = "";
+ btn.style.borderColor = "";
+ }
+ });
+}
diff --git a/json-tool/public/assets/js/features/formatter/formatter.html b/json-tool/public/assets/js/features/formatter/formatter.html
new file mode 100644
index 0000000..5a5af39
--- /dev/null
+++ b/json-tool/public/assets/js/features/formatter/formatter.html
@@ -0,0 +1,11 @@
+
+
diff --git a/json-tool/public/assets/js/features/formatter/formatter.js b/json-tool/public/assets/js/features/formatter/formatter.js
new file mode 100644
index 0000000..65c94b4
--- /dev/null
+++ b/json-tool/public/assets/js/features/formatter/formatter.js
@@ -0,0 +1,428 @@
+async function loadFormatterHTML() {
+ try {
+ const response = await fetch("assets/js/features/formatter/formatter.html");
+ const html = await response.text();
+ document.getElementById("formatter-section-container").innerHTML = html;
+ } catch (error) {
+ console.error("Error loading formatter HTML:", error);
+ }
+}
+document.addEventListener("DOMContentLoaded", loadFormatterHTML);
+
+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);
+
+ const tabName = tabData?.name || "Tab " + formatterTabCount;
+ const tabColor = tabData?.color || "#e0e0e0";
+
+ tabButton.innerHTML = `${tabName}
+
+ ร`;
+
+ tabButton.addEventListener("dblclick", () =>
+ openTabRenameTooltip(tabId, "formatter")
+ );
+
+ const tabsContainer = document.getElementById("formatter-tabs-container");
+ const addButton = tabsContainer.querySelector(".add-tab-button");
+ if (tabsContainer && addButton) {
+ tabsContainer.insertBefore(tabButton, addButton);
+ }
+
+ // Create tab content
+ const tabContent = document.createElement("div");
+ tabContent.id = tabId;
+ tabContent.className = "json-tab-content";
+ tabContent.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ document.getElementById("formatter-tab-contents").appendChild(tabContent);
+
+ // Set content if provided
+ if (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);
+ enableTabReordering("formatter-tabs-container");
+}
+
+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");
+ // Avoid resetting user cursor while typing
+ if (document.activeElement !== textarea) {
+ textarea.value = formatted;
+ }
+ } catch (e) {
+ errorMessage.textContent = "Error: " + e.message;
+ showFormatterPreviewTab(tabId, "error");
+ }
+ document.querySelectorAll(".tree-key").forEach((key) => {
+ key.addEventListener("focus", () => {
+ key.scrollIntoView({ block: "nearest", behavior: "smooth" });
+ });
+ });
+ 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.parentElement.classList.contains("active")) {
+ const content = rawPreview.textContent;
+ rawPreview.innerHTML = content.replace(
+ regex,
+ '$1'
+ );
+ }
+ if (treeView.parentElement.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,
+ '$1'
+ );
+ 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) {
+ saveGlobalState();
+}
+
+function uploadFormatterJSON(tabId, inputElement) {
+ if (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);
+}
+
+async function closeFormatterTab(tabId, event) {
+ if (event) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ const result = await Swal.fire({
+ title: "Close tab?",
+ text: "Are you sure you want to close this tab?",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonText: "Yes",
+ cancelButtonText: "No",
+ });
+
+ if (!result.isConfirmed) 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();
+}
+
+function copyRawJSON(tabId) {
+ const rawPre = document.querySelector(`#${tabId}-raw-preview .raw-json`);
+ copyToClipboard(rawPre.textContent, "JSON copied to clipboard");
+}
+
+function createTreeView(data, parentElement) {
+ parentElement.innerHTML = "";
+ const focusedNode = null;
+
+ function processNode(value, parent, key, path = []) {
+ const node = document.createElement("div");
+ node.className = "tree-node";
+ const currentPath = [...path, key];
+ const displayKey = key !== undefined ? key : "";
+
+ if (typeof value === "object" && value !== null) {
+ const isArray = Array.isArray(value);
+ const keySpan = document.createElement("span");
+ keySpan.className = `tree-key ${isArray ? "type-array" : "type-object"}`;
+ keySpan.tabIndex = 0;
+
+ const nameEl = document.createElement("span");
+ nameEl.textContent = displayKey;
+ const infoEl = document.createElement("span");
+ infoEl.className = "node-info";
+ infoEl.textContent = isArray
+ ? `[${value.length}]`
+ : `{${Object.keys(value).length}}`;
+ keySpan.append(nameEl, infoEl);
+
+ // Keyboard navigation
+ keySpan.addEventListener("keydown", (e) => {
+ switch (e.key) {
+ case "ArrowRight":
+ if (keySpan.classList.contains("collapsed")) toggleNode();
+ break;
+ case "ArrowLeft":
+ if (!keySpan.classList.contains("collapsed")) toggleNode();
+ break;
+ case "ArrowDown":
+ focusNextNode(keySpan);
+ break;
+ case "ArrowUp":
+ focusPreviousNode(keySpan);
+ break;
+ }
+ });
+
+ // Context menu
+ keySpan.addEventListener("contextmenu", (e) => {
+ e.preventDefault();
+ showContextMenu(e, currentPath, value);
+ });
+
+ const children = document.createElement("div");
+ children.className = "tree-children";
+ children.style.display = "none";
+
+ const toggleNode = () => {
+ keySpan.classList.toggle("expanded");
+ keySpan.classList.toggle("collapsed");
+ children.style.display =
+ children.style.display === "none" ? "block" : "none";
+ };
+
+ keySpan.addEventListener("click", toggleNode);
+
+ if (isArray) {
+ value.forEach((item, index) =>
+ processNode(item, children, index, currentPath)
+ );
+ } else {
+ Object.entries(value).forEach(([k, v]) =>
+ processNode(v, children, k, currentPath)
+ );
+ }
+
+ node.appendChild(keySpan);
+ node.appendChild(children);
+ parent.appendChild(node);
+ } else {
+ const valueSpan = document.createElement("span");
+
+ const keyEl = document.createElement("span");
+ keyEl.className = "tree-key";
+ keyEl.textContent = `${key}: `;
+
+ const valEl = document.createElement("span");
+ valEl.className = getValueTypeClass(value);
+ valEl.textContent = formatValue(value);
+
+ valueSpan.append(keyEl, valEl);
+
+ // Value context menu
+ valueSpan.addEventListener("contextmenu", (e) => {
+ e.preventDefault();
+ showContextMenu(e, currentPath, value);
+ });
+
+ node.appendChild(valueSpan);
+ parent.appendChild(node);
+ }
+
+ return node;
+ }
+
+ function showContextMenu(e, path, value) {
+ const menu = document.createElement("div");
+ menu.className = "tree-context-menu";
+ menu.style.left = `${e.pageX}px`;
+ menu.style.top = `${e.pageY}px`;
+
+ const copyPath = document.createElement("div");
+ copyPath.className = "tree-context-menu-item";
+ copyPath.textContent = "Copy Path";
+ copyPath.onclick = () => navigator.clipboard.writeText(path.join("."));
+
+ const copyValue = document.createElement("div");
+ copyValue.className = "tree-context-menu-item";
+ copyValue.textContent = "Copy Value";
+ copyValue.onclick = () =>
+ navigator.clipboard.writeText(JSON.stringify(value));
+
+ menu.appendChild(copyPath);
+ menu.appendChild(copyValue);
+ document.body.appendChild(menu);
+
+ const closeMenu = () => {
+ document.body.removeChild(menu);
+ document.removeEventListener("click", closeMenu);
+ };
+
+ document.addEventListener("click", closeMenu);
+ }
+
+ function focusNextNode(currentNode) {
+ const allNodes = parentElement.querySelectorAll(".tree-key");
+ const currentIndex = Array.from(allNodes).indexOf(currentNode);
+ if (currentIndex < allNodes.length - 1) {
+ allNodes[currentIndex + 1].focus();
+ }
+ }
+
+ function focusPreviousNode(currentNode) {
+ const allNodes = parentElement.querySelectorAll(".tree-key");
+ const currentIndex = Array.from(allNodes).indexOf(currentNode);
+ if (currentIndex > 0) {
+ allNodes[currentIndex - 1].focus();
+ }
+ }
+
+ processNode(data, parentElement);
+}
diff --git a/json-tool/public/assets/js/features/mockgen/mockgen.js b/json-tool/public/assets/js/features/mockgen/mockgen.js
new file mode 100644
index 0000000..733eaa1
--- /dev/null
+++ b/json-tool/public/assets/js/features/mockgen/mockgen.js
@@ -0,0 +1,282 @@
+let latestMockData = [];
+const presets = {
+ User: {
+ id: "number|1000-9999",
+ name: "name.fullName",
+ email: "internet.email",
+ isActive: "boolean",
+ },
+ Product: {
+ id: "number|1-1000",
+ title: "commerce.productName",
+ price: "number|10-500",
+ available: "boolean",
+ },
+};
+
+function loadMockPreset(name) {
+ if (presets[name]) {
+ document.getElementById("mock-schema-input").value = JSON.stringify(
+ presets[name],
+ null,
+ 2
+ );
+ }
+}
+
+function generateMockData() {
+ const input = document.getElementById("mock-schema-input").value;
+ const count = Number.parseInt(
+ document.getElementById("mockgen-count").value || "1",
+ 10
+ );
+ const outputContainer = document.getElementById("mock-output-container");
+
+ try {
+ const schema = JSON.parse(input);
+ latestMockData = Array.from({ length: count }, () => {
+ try {
+ return mockFromSchema(schema);
+ } catch (err) {
+ return { error: err.message };
+ }
+ });
+ document.getElementById(
+ "mock-stats"
+ ).textContent = `Showing ${count} record(s)`;
+ updateMockView();
+ } catch (e) {
+ outputContainer.innerHTML = `โ Error: ${e.message}`;
+ }
+}
+
+function mockFromSchema(schema) {
+ if (Array.isArray(schema)) {
+ return schema.map((item) => mockFromSchema(item));
+ }
+ if (typeof schema === "object") {
+ const result = {};
+ for (const [key, value] of Object.entries(schema)) {
+ result[key] = mockFromSchema(value);
+ }
+ return result;
+ }
+ if (typeof schema === "string") {
+ if (schema === "boolean") return Math.random() < 0.5;
+ if (schema.startsWith("number|")) {
+ const [min, max] = schema.split("|")[1].split("-").map(Number);
+ return faker.number.int({ min, max });
+ }
+
+ const fakerFn = schema.split(".");
+ let val = faker;
+ for (const part of fakerFn) {
+ val = val?.[part];
+ }
+ if (typeof val === "function") return val();
+ throw new Error(`Invalid faker path: "${schema}"`);
+ }
+ return schema;
+}
+
+function updateMockView() {
+ const mode = document.querySelector(
+ 'input[name="mock-view-mode"]:checked'
+ ).value;
+ const container = document.getElementById("mock-output-container");
+ container.innerHTML = "";
+
+ if (mode === "json") {
+ const pre = document.createElement("pre");
+ pre.className = "code-output";
+ pre.textContent = JSON.stringify(
+ latestMockData.length === 1 ? latestMockData[0] : latestMockData,
+ null,
+ 2
+ );
+ container.appendChild(pre);
+ } else {
+ container.appendChild(renderTableFromJson(latestMockData));
+ }
+}
+
+function renderTableFromJson(data) {
+ const table = document.createElement("table");
+ table.className = "mock-preview-table";
+
+ if (
+ !Array.isArray(data) ||
+ data.length === 0 ||
+ typeof data[0] !== "object"
+ ) {
+ table.innerHTML = "| No tabular data available |
";
+ return table;
+ }
+
+ const headers = [...new Set(data.flatMap((obj) => Object.keys(obj)))];
+ const thead = document.createElement("thead");
+ const headerRow = document.createElement("tr");
+ headers.forEach((h) => {
+ const th = document.createElement("th");
+ th.textContent = h;
+ // Add title for full text on hover
+ th.title = h;
+ headerRow.appendChild(th);
+ });
+ thead.appendChild(headerRow);
+ table.appendChild(thead);
+
+ const tbody = document.createElement("tbody");
+ data.forEach((row) => {
+ const tr = document.createElement("tr");
+ if (row.error) {
+ tr.classList.add("error");
+ }
+
+ headers.forEach((key) => {
+ const td = document.createElement("td");
+ const val = row[key];
+ let displayVal;
+
+ if (val && typeof val === "object") {
+ displayVal = JSON.stringify(val);
+ } else {
+ displayVal = val ?? "";
+ }
+
+ td.textContent = displayVal;
+ // Add title for full text on hover
+ td.title = displayVal;
+
+ tr.appendChild(td);
+ });
+ tbody.appendChild(tr);
+ });
+ table.appendChild(tbody);
+ return table;
+}
+
+function copyMockOutput(format = "json") {
+ let text = "";
+ if (format === "json") {
+ text = JSON.stringify(
+ latestMockData.length === 1 ? latestMockData[0] : latestMockData,
+ null,
+ 2
+ );
+ } else if (format === "csv") {
+ if (!latestMockData.length || typeof latestMockData[0] !== "object") return;
+ const keys = Object.keys(latestMockData[0]);
+ const rows = latestMockData.map((obj) =>
+ keys.map((k) => JSON.stringify(obj[k] ?? ""))
+ );
+ text = [keys.join(","), ...rows.map((r) => r.join(","))].join("\n");
+ }
+
+ copyToClipboard(text, `Copied ${format.toUpperCase()}!`);
+}
+
+function downloadFile(content, filename, type) {
+ const blob = new Blob([content], { type });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
+
+let userInputValue = "";
+
+document.getElementById("userInput").addEventListener("input", (e) => {
+ userInputValue = e.target.value;
+});
+
+function exportMockOutput(format = "json") {
+ if (!Array.isArray(latestMockData) || latestMockData.length === 0) {
+ console.error("No data available to export.");
+ return;
+ }
+
+ let content = "";
+ let filename = userInputValue !== "" ? userInputValue : "mock_data"; // Use user input if available
+
+ if (format === "json") {
+ content = JSON.stringify(
+ latestMockData.length === 1 ? latestMockData[0] : latestMockData,
+ null,
+ 2
+ );
+ filename += ".json";
+ downloadFile(content, filename, "application/json");
+ } else if (format === "csv") {
+ if (!latestMockData.length || typeof latestMockData[0] !== "object") return;
+
+ const keys = Object.keys(latestMockData[0]);
+ const rows = latestMockData.map((obj) =>
+ keys.map((k) => JSON.stringify(obj[k] ?? ""))
+ );
+ content = [keys.join(","), ...rows.map((r) => r.join(","))].join("\n");
+ filename += ".csv";
+ downloadFile(content, filename, "text/csv");
+ }
+}
+
+const mockgenDocs = `
+ # ๐งช Mock Data Schema Format
+
+ ## Supported Types
+
+ - \`"name.fullName"\`: Full name
+ - \`"internet.email"\`: Email address
+ - \`"number|10-50"\`: Custom range
+ - \`"boolean"\`: true / false
+ - Arrays: \`["lorem.word"]\`
+ - Nested:
+
+ \`\`\`json
+ {
+ "user": {
+ "name": "name.fullName",
+ "email": "internet.email"
+ }
+ }
+ \`\`\`
+
+ ## Example
+
+ \`\`\`json
+ {
+ "id": "number|1000-9999",
+ "name": "name.fullName",
+ "email": "internet.email",
+ "isActive": "boolean"
+ }
+ \`\`\`
+ `;
+
+function switchMockTab(tab) {
+ const schemaPanel = document.getElementById("mockgen-schema-panel");
+ const docsPanel = document.getElementById("mockgen-docs-panel");
+
+ document.getElementById("mock-tab-schema").classList.remove("active");
+ document.getElementById("mock-tab-docs").classList.remove("active");
+
+ if (tab === "schema") {
+ schemaPanel.style.display = "block";
+ docsPanel.style.display = "none";
+ document.getElementById("mock-tab-schema").classList.add("active");
+ } else {
+ schemaPanel.style.display = "none";
+ docsPanel.style.display = "block";
+ document.getElementById("mock-tab-docs").classList.add("active");
+ renderMockgenDocs();
+ }
+}
+
+function renderMockgenDocs() {
+ document.getElementById("mockgen-docs-preview").innerHTML =
+ marked.parse(mockgenDocs);
+}
diff --git a/index.html b/json-tool/public/index.html
similarity index 88%
rename from index.html
rename to json-tool/public/index.html
index 00ce211..c9aca31 100644
--- a/index.html
+++ b/json-tool/public/index.html
@@ -4,15 +4,20 @@
JSONP - Dev toolkit
-
-
+
+
+
+
+
@@ -26,7 +31,7 @@
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
-
+
@@ -95,19 +100,7 @@
-
+
@@ -115,9 +108,6 @@
-
@@ -130,9 +120,6 @@
-
@@ -315,6 +302,20 @@
Keyboard Shortcuts
alt="Made with love with Open Source"
/>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+