Spaces:
Sleeping
Sleeping
| <html><head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Nested Object Editor & Viewer</title> | |
| <style> | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| margin: 0; | |
| padding: 20px; | |
| background-color: #f0f8ff; | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 100vh; | |
| } | |
| .tree-container { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| overflow-x: auto; | |
| padding-bottom: 20px; | |
| flex-grow: 1; | |
| } | |
| .tree { | |
| display: flex; | |
| flex-direction: column; | |
| min-width: fit-content; | |
| } | |
| .tree-item { | |
| margin: 5px 0; | |
| padding: 10px; | |
| border-radius: 5px; | |
| transition: all 0.3s ease; | |
| background-color: white; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.1); | |
| min-width: 300px; | |
| } | |
| .tree-item:hover { | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.15); | |
| } | |
| .tree-content { | |
| display: flex; | |
| align-items: center; | |
| flex-wrap: nowrap; | |
| min-width: 100%; | |
| } | |
| .tree-actions { | |
| display: none; | |
| margin-left: auto; | |
| white-space: nowrap; | |
| } | |
| .tree-content:hover > .tree-actions { | |
| display: flex; | |
| } | |
| .tree-content:hover > .tree-actions.hidden { | |
| display: none; | |
| } | |
| .tree-actions.hidden { | |
| display: none; | |
| } | |
| .icon-button { | |
| background: #2ecc71; | |
| border: none; | |
| cursor: pointer; | |
| padding: 6px; | |
| margin: 0 2px; | |
| transition: transform 0.2s ease, background-color 0.2s ease; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 28px; | |
| height: 28px; | |
| flex-shrink: 0; | |
| } | |
| .icon-button:hover { | |
| transform: scale(1.1); | |
| background-color: #27ae60; | |
| } | |
| .icon-button svg { | |
| width: 16px; | |
| height: 16px; | |
| fill: white; | |
| } | |
| input[type="text"], select { | |
| padding: 5px; | |
| margin: 5px; | |
| border: 1px solid #ccc; | |
| border-radius: 3px; | |
| } | |
| .expand-collapse { | |
| cursor: pointer; | |
| margin-right: 10px; | |
| width: 20px; | |
| text-align: center; | |
| font-weight: bold; | |
| flex-shrink: 0; | |
| } | |
| .collapsed > .child-tree { | |
| display: none; | |
| } | |
| .child-tree { | |
| border-left: 2px solid #e0e0e0; | |
| padding-left: 10px; | |
| transition: padding-left 0.3s ease; | |
| } | |
| .color-dot { | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 50%; | |
| margin-right: 10px; | |
| flex-shrink: 0; | |
| } | |
| #add-sibling-tree { | |
| background-color: #2ecc71; | |
| border: none; | |
| color: white; | |
| width: 40px; | |
| height: 40px; | |
| text-align: center; | |
| text-decoration: none; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 24px; | |
| margin: 20px 0; | |
| transition-duration: 0.4s; | |
| cursor: pointer; | |
| border-radius: 50%; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.2); | |
| } | |
| #add-sibling-tree:hover { | |
| background-color: #27ae60; | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.3); | |
| } | |
| .fields-container { | |
| margin-top: 5px; | |
| display: flex; | |
| flex-direction: column; | |
| width: calc(100% - 26px); | |
| padding-left: 26px; | |
| } | |
| .field { | |
| display: flex; | |
| align-items: center; | |
| margin: 2px 0; | |
| padding: 3px; | |
| border-radius: 3px; | |
| transition: background-color 0.3s ease; | |
| font-size: 0.85em; | |
| background-color: #f5f5f5; | |
| max-width: 100%; | |
| box-sizing: border-box; | |
| } | |
| .field:hover { | |
| background-color: #e8e8e8; | |
| } | |
| .field-name { | |
| font-weight: bold; | |
| margin-right: 5px; | |
| min-width: 100px; | |
| color: #555; | |
| flex-shrink: 0; | |
| } | |
| .field-value { | |
| flex-grow: 1; | |
| color: #333; | |
| word-break: break-word; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .field-value a { | |
| color: #4a90e2; | |
| text-decoration: none; | |
| } | |
| .field-value a:hover { | |
| text-decoration: underline; | |
| } | |
| .field-actions { | |
| display: none; | |
| margin-left: 5px; | |
| flex-shrink: 0; | |
| } | |
| .field:hover .field-actions { | |
| display: flex; | |
| } | |
| .alert-icon-container { | |
| display: none; | |
| flex-grow: 1; | |
| line-height: 2.5rem; | |
| text-align: right; | |
| } | |
| .alert-icon-container.show { | |
| display:block; | |
| } | |
| .alert-icon { | |
| width: 2rem; | |
| vertical-align: middle; | |
| } | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| z-index: 1; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100%; | |
| overflow: auto; | |
| background-color: rgba(0,0,0,0.4); | |
| } | |
| .modal-content { | |
| background-color: #fefefe; | |
| margin: 15% auto; | |
| padding: 20px; | |
| border: 1px solid #888; | |
| width: 300px; | |
| border-radius: 5px; | |
| } | |
| .close { | |
| color: #aaa; | |
| float: right; | |
| font-size: 28px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| } | |
| .close:hover, | |
| .close:focus { | |
| color: black; | |
| text-decoration: none; | |
| cursor: pointer; | |
| } | |
| #customFieldName { | |
| display: none; | |
| } | |
| .edit-mode input { | |
| border: none; | |
| background: transparent; | |
| font-size: inherit; | |
| font-family: inherit; | |
| padding: 0; | |
| margin: 0; | |
| width: 100%; | |
| outline: none; | |
| } | |
| .edit-actions { | |
| display: none; | |
| } | |
| .edit-mode .edit-actions { | |
| display: flex; | |
| } | |
| .edit-mode .tree-actions { | |
| display: none; | |
| } | |
| .edit-actions .icon-button { | |
| background: transparent; | |
| padding: 0; | |
| width: auto; | |
| height: auto; | |
| } | |
| .edit-actions .icon-button:hover { | |
| background: transparent; | |
| } | |
| .edit-actions .icon-button svg { | |
| width: 20px; | |
| height: 20px; | |
| } | |
| .save-button svg { | |
| fill: none; | |
| stroke: #2ecc71; | |
| stroke-width: 2; | |
| } | |
| .cancel-button svg { | |
| fill: none; | |
| stroke: #e74c3c; | |
| stroke-width: 2; | |
| } | |
| .tree-label { | |
| flex-grow: 1; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| #output-container { | |
| margin-top: 20px; | |
| padding: 10px; | |
| background-color: #fff; | |
| border-radius: 5px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.1); | |
| } | |
| #output-text-field, #output-json-field, #output-markdown-field { | |
| width: calc(100% - 5px); | |
| box-sizing: border-box; | |
| margin: 0; | |
| white-space: pre; | |
| overflow-x: auto; | |
| height: 150px; | |
| resize: vertical; | |
| padding: 10px; | |
| border: 1px solid #ccc; | |
| border-radius: 5px; | |
| font-family: 'Courier New', Courier, monospace; | |
| display: none; | |
| font-size: 14px; | |
| } | |
| #output-text-field.active, #output-json-field.active, #output-markdown-field.active { | |
| display: block; | |
| } | |
| .output-tabs { | |
| display: flex; | |
| border-bottom: 1px solid #ccc; | |
| margin-bottom: 10px; | |
| } | |
| .output-tab.active { | |
| background-color: #fff; | |
| border-bottom: 1px solid #fff; | |
| margin-bottom: -1px; | |
| } | |
| .output-tab { | |
| padding: 10px 20px; | |
| cursor: pointer; | |
| background-color: #f0f0f0; | |
| border: 1px solid #ccc; | |
| border-bottom: none; | |
| border-radius: 5px 5px 0 0; | |
| margin-right: 5px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="tree-container" id="main-container"> | |
| <div class="tree"> | |
| <div class="tree-item"> | |
| <div class="tree-content"> | |
| <span class="expand-collapse">▼</span> | |
| <div class="color-dot" style="background-color: #ff4757;"></div> | |
| <span class="tree-label">Object 1</span> | |
| <input type="text" class="edit-input" style="display: none;"> | |
| <div class="tree-actions"> | |
| <button class="icon-button" onclick="addChildFromButton(this)" title="Add Child"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="12" y1="5" x2="12" y2="19"></line> | |
| <line x1="5" y1="12" x2="19" y2="12"></line> | |
| </svg> | |
| </button> | |
| <button class="icon-button" onclick="editItem(this)" title="Edit"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> | |
| <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> | |
| </svg> | |
| </button> | |
| <button class="icon-button" onclick="deleteItem(this)" title="Delete"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="3 6 5 6 21 6"></polyline> | |
| <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> | |
| </svg> | |
| </button> | |
| <button class="icon-button" onclick="showAddFieldModal(this)" title="Add Field"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="edit-actions" style="display: none;"> | |
| <button class="icon-button save-button" onclick="saveEdit(this)" title="Save"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="#2ecc71" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="20 6 9 17 4 12"></polyline> | |
| </svg> | |
| </button> | |
| <button class="icon-button cancel-button" onclick="cancelEdit(this)" title="Cancel"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="#e74c3c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="fields-container"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <button id="add-sibling-tree" onclick="addTreeFromBtn()" title="Add Sibling Tree"> | |
| <svg viewBox="0 0 24 24" fill="white" width="24" height="24"> | |
| <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"></path> | |
| </svg> | |
| </button> | |
| <div id="output-container"> | |
| <div class="output-tabs"> | |
| <div id="output-text-tab" class="output-tab active" onclick="onSwitchTab('text')" >Text Output</div> | |
| <div id="output-json-tab" class="output-tab" onclick="onSwitchTab('json')" >JSON Output</div> | |
| <div id="output-markdown-tab" class="output-tab" onclick="onSwitchTab('markdown')" >MD Output</div> | |
| <div id="output-export-btn" style="color:#fff;border-radius:15%;width:auto;line-height:2.5em; margin:0.3em 0.2em" class="icon-button" > | |
| Export | |
| <button style="background: transparent;padding: 4; border:0;width: auto;height: auto;display:inline; vertical-align: middle;"> | |
| <svg fill="#ffffff" version="1.1" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve"> | |
| <path fill="#ffffff" d="M20,24H0V0h14.41L20,5.59v4.38h-2V8h-6V2H2v20h18V24z M14,6h3.59L14,2.41V6z M18.71,20.71l-1.41-1.41L19.59,17H11v-2h8.59 | |
| l-2.29-2.29l1.41-1.41L23.41,16L18.71,20.71z"/> | |
| </svg> | |
| </button> | |
| </div> | |
| <div id='parse-error' class="alert-icon-container"> | |
| Invalid Input! | |
| <svg fill="#f00" viewBox="0 0 24 24" class="alert-icon"> | |
| <path d="M12.205 3.839a0.239 0.239 0 0 0 -0.08 -0.08c-0.113 -0.069 -0.261 -0.033 -0.33 0.08L1.881 20.083a0.24 0.24 0 0 0 -0.035 | |
| 0.125c0 0.133 0.107 0.24 0.24 0.24h19.828c0.044 0 0.087 -0.012 0.125 -0.035 0.113 -0.069 0.149 -0.217 0.08 -0.33L12.205 3.839zm1.024 | |
| -0.625L23.143 19.458c0.414 0.679 0.2 1.565 -0.479 1.979a1.44 1.44 0 0 1 -0.75 0.211H2.086c-0.795 0 -1.44 -0.645 -1.44 -1.44a1.44 1.44 0 0 1 0.211 | |
| -0.75l9.914 -16.244c0.414 -0.679 1.3 -0.893 1.979 -0.479a1.44 1.44 0 0 1 0.479 0.479M12 18.24c0.53 0 0.96 -0.43 0.96 -0.96s-0.43 -0.96 -0.96 -0.96 | |
| -0.96 0.43 -0.96 0.96 0.43 0.96 0.96 0.96m0 -10.32c-0.53 0 -0.96 0.43 -0.96 0.96v5.28c0 0.53 0.43 0.96 0.96 0.96s0.96 -0.43 0.96 -0.96V8.88c0 -0.53 | |
| -0.43 -0.96 -0.96 -0.96"/> | |
| </svg> | |
| </div> | |
| </div> | |
| <textarea id="output-text-field" class="active" placeholder="Objects output will appear here..."></textarea> | |
| <textarea id="output-json-field" readonly="" placeholder="Objects output will appear here..."></textarea> | |
| <textarea id="output-markdown-field" readonly="" placeholder="Objects output will appear here..."></textarea> | |
| </div> | |
| <div id="addFieldModal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close">×</span> | |
| <h2>Add Field</h2> | |
| <select id="fieldCategory" onchange="toggleCustomField()"> | |
| <option value="">Select a category</option> | |
| <option value="state of the art">State of the Art</option> | |
| <option value="product examples">Product Examples</option> | |
| <option value="cost range">Cost Range</option> | |
| <option value="links">Links</option> | |
| <option value="application">Application</option> | |
| <option value="maturity">Maturity</option> | |
| <option value="custom">Custom</option> | |
| </select> | |
| <input type="text" id="customFieldName" placeholder="Enter custom field name"> | |
| <input type="text" id="fieldValue" placeholder="Enter field value"> | |
| <button onclick="addFieldFromModal()">Add Field</button> | |
| </div> | |
| </div> | |
| <script> | |
| const colors = [ | |
| '#ff4757', '#ffa502', '#2ed573', '#1e90ff', '#5352ed', '#8e44ad' | |
| ]; | |
| let currentTreeItem; | |
| let objectCounter = 1; | |
| const MAX_FIRST_LEVEL_INDENT = 40; | |
| const MIN_INDENT = 10; | |
| const MIN_TREE_ITEM_WIDTH = 300; | |
| function createTreeItem(content, level) { | |
| const item = document.createElement('div'); | |
| item.className = 'tree-item'; | |
| const color = colors[level % colors.length]; | |
| item.innerHTML = ` | |
| <div class="tree-content"> | |
| <span class="expand-collapse">▼</span> | |
| <div class="color-dot" style="background-color: ${color};"></div> | |
| <span class="tree-label">${content}</span> | |
| <input type="text" class="edit-input" style="display: none;"> | |
| <div class="tree-actions"> | |
| <button class="icon-button" onclick="addChildFromButton(this)" title="Add Child"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="12" y1="5" x2="12" y2="19"></line> | |
| <line x1="5" y1="12" x2="19" y2="12"></line> | |
| </svg> | |
| </button> | |
| <button class="icon-button" onclick="editItem(this)" title="Edit"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> | |
| <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> | |
| </svg> | |
| </button> | |
| <button class="icon-button" onclick="deleteItem(this)" title="Delete"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="3 6 5 6 21 6"></polyline> | |
| <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> | |
| </svg> | |
| </button> | |
| <button class="icon-button" onclick="showAddFieldModal(this)" title="Add Field"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="edit-actions" style="display: none;"> | |
| <button class="icon-button save-button" onclick="saveEdit(this)" title="Save"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="#2ecc71" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="20 6 9 17 4 12"></polyline> | |
| </svg> | |
| </button> | |
| <button class="icon-button cancel-button" onclick="cancelEdit(this)" title="Cancel"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="#e74c3c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="fields-container"></div> | |
| `; | |
| return item; | |
| } | |
| function addChildFromButton(button) { | |
| const parentItem = button.closest('.tree-item'); | |
| objectCounter++; | |
| const newContent = `Object ${objectCounter}`; | |
| addChild(parentItem, newContent); | |
| updateOutput(); | |
| } | |
| function addChild(parentItem, childName) { | |
| let childTree = parentItem.querySelector('.child-tree'); | |
| if (!childTree) { | |
| childTree = document.createElement('div'); | |
| childTree.className = 'child-tree'; | |
| parentItem.appendChild(childTree); | |
| } | |
| const parentLevel = getItemLevel(parentItem); | |
| const newLevel = parentLevel + 1; | |
| const newItem = createTreeItem(childName, newLevel); | |
| childTree.appendChild(newItem); | |
| updateExpandCollapse(parentItem); | |
| updateExpandCollapse(newItem); | |
| updateIndents(); | |
| return newItem; | |
| } | |
| // Add field name and value into a tree member | |
| function addFieldProperty(currentTreeItem,fieldCategory,fieldValue) { | |
| const fieldsContainer = currentTreeItem.querySelector('.fields-container'); | |
| const fieldDiv = document.createElement('div'); | |
| fieldDiv.className = 'field'; | |
| fieldDiv.innerHTML = ` | |
| <span class="field-name">${fieldCategory}:</span> | |
| <span class="field-value">${formatFieldValue(fieldValue)}</span> | |
| <div class="field-actions"> | |
| <button class="icon-button" onclick="editField(this)" title="Edit Field"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> | |
| <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> | |
| </svg> | |
| </button> | |
| <button class="icon-button" onclick="deleteField(this)" title="Delete Field"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="3 6 5 6 21 6"></polyline> | |
| <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| `; | |
| fieldsContainer.appendChild(fieldDiv); | |
| } | |
| function editItem(button) { | |
| const treeContent = button.closest('.tree-content'); | |
| const label = treeContent.querySelector('.tree-label'); | |
| const input = treeContent.querySelector('.edit-input'); | |
| const treeActions = treeContent.querySelector('.tree-actions'); | |
| const editActions = treeContent.querySelector('.edit-actions'); | |
| label.style.display = 'none'; | |
| input.style.display = 'inline-block'; | |
| input.value = label.textContent; | |
| treeActions.classList.add('hidden'); | |
| editActions.style.display = 'flex'; | |
| input.focus(); | |
| treeContent.classList.add('edit-mode'); | |
| } | |
| function saveEdit(button) { | |
| const treeContent = button.closest('.tree-content'); | |
| const label = treeContent.querySelector('.tree-label'); | |
| const input = treeContent.querySelector('.edit-input'); | |
| const treeActions = treeContent.querySelector('.tree-actions'); | |
| const editActions = treeContent.querySelector('.edit-actions'); | |
| label.textContent = input.value; | |
| label.style.display = 'inline'; | |
| input.style.display = 'none'; | |
| treeActions.classList.remove('hidden'); | |
| editActions.style.display = 'none'; | |
| treeContent.classList.remove('edit-mode'); | |
| updateOutput(); | |
| } | |
| function cancelEdit(button) { | |
| const treeContent = button.closest('.tree-content'); | |
| const label = treeContent.querySelector('.tree-label'); | |
| const input = treeContent.querySelector('.edit-input'); | |
| const treeActions = treeContent.querySelector('.tree-actions'); | |
| const editActions = treeContent.querySelector('.edit-actions'); | |
| label.style.display = 'inline'; | |
| input.style.display = 'none'; | |
| treeActions.classList.remove('hidden'); | |
| editActions.style.display = 'none'; | |
| treeContent.classList.remove('edit-mode'); | |
| } | |
| function deleteItem(button) { | |
| const item = button.closest('.tree-item'); | |
| if (confirm('Are you sure you want to delete this item and its children?')) { | |
| item.remove(); | |
| updateIndents(); | |
| updateOutput(); | |
| } | |
| } | |
| function showAddFieldModal(button) { | |
| currentTreeItem = button.closest('.tree-item'); | |
| const modal = document.getElementById('addFieldModal'); | |
| modal.style.display = 'block'; | |
| } | |
| function toggleCustomField() { | |
| const fieldCategory = document.getElementById('fieldCategory'); | |
| const customFieldName = document.getElementById('customFieldName'); | |
| customFieldName.style.display = fieldCategory.value === 'custom' ? 'block' : 'none'; | |
| } | |
| function addFieldFromModal() { | |
| let fieldCategory = document.getElementById('fieldCategory').value; | |
| const customFieldName = document.getElementById('customFieldName'); | |
| const fieldValue = document.getElementById('fieldValue').value; | |
| if (fieldCategory === 'custom') { | |
| fieldCategory = customFieldName.value; | |
| } | |
| if (fieldCategory && fieldValue) { | |
| addFieldProperty(currentTreeItem, fieldCategory, fieldValue); | |
| closeModal(); | |
| updateOutput(); | |
| } else { | |
| alert('Please select a category (or enter a custom name) and enter a value.'); | |
| } | |
| } | |
| function formatFieldValue(value) { | |
| // if field value is ends with a image or vector file , png, jpg, svg, bmp, try to test and display it too | |
| const imageRegex = /(\.png|\.jpg|\.svg|\.bmp)$/; | |
| if (imageRegex.test(value)) { | |
| const imageDiv = document.createElement('div'); | |
| const image = document.createElement('img'); | |
| const imageLink = document.createElement('div'); | |
| image.src = value; | |
| image.style = 'width: 100%; max-width: 300px;'; | |
| imageDiv.appendChild(image); | |
| // add hidden text content when returning the image; | |
| imageLink.innerHTML = `<a href="${value}" target="_blank">${value}</a>`; | |
| imageDiv.appendChild(imageLink); | |
| return imageDiv.outerHTML; | |
| } | |
| // if field value is a video streaming link, i.e. youtube link or is a media file, loads it too | |
| const videoRegex = /^(https?:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|v\/|user\/\S+|[^/]+\?v=)|youtu\.be\/|vimeo\.com\/\d+))|(?:\.mp4|\.webm|\.ogg|\.mov|\.avi|\.wmv|\.flv|\.3gp|\.m4v|\.mkv|\.m3u8|\.ts|\.3g2|\.3gp2|\.m2ts|\.mts|\.m2t|\.ts|\.mxf|\.mks|\.webm|\.mpd|\.m3u8|\.m4s|\.f4m|\.ism|\.ismc|\.isma)$/; | |
| if (videoRegex.test(value)) { | |
| function getVideoID(url) { | |
| var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; | |
| var match = url.match(regExp); | |
| if (match && match[2].length == 11) { | |
| return match[2]; | |
| } else { | |
| return ''; | |
| } | |
| } | |
| const videoDiv = document.createElement('div'); | |
| let videoElement; | |
| const videoLink = document.createElement('div'); | |
| // Check if the value is a YouTube or other video streaming link | |
| if (/^(https?:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|v\/|user\/\S+|[^/]+\?v=)|youtu\.be\/|vimeo\.com\/\d+))/.test(value)) { | |
| videoElement = document.createElement('iframe'); | |
| videoID = getVideoID(value); | |
| videoElement.src = videoID ?'https://www.youtube.com/embed/' + videoID : value; | |
| videoElement.width = '100%'; | |
| videoElement.height = '300'; | |
| videoElement.frameborder = '0'; | |
| videoElement.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'; | |
| videoElement.allowfullscreen = true; | |
| } else { | |
| videoElement = document.createElement('video'); | |
| videoElement.src = value; | |
| videoElement.controls = true; | |
| videoElement.style = 'width: 100%; max-width: 300px;'; | |
| } | |
| videoDiv.appendChild(videoElement); | |
| videoLink.innerHTML = `<a href="${value}" target="_blank">${value}</a>`; | |
| videoDiv.appendChild(videoLink); | |
| return videoDiv.outerHTML; | |
| } | |
| const urlRegex = /^(https?:\/\/[^\s]+)$/; | |
| if (urlRegex.test(value)) { | |
| return `<a href="${value}" target="_blank">${value}</a>`; | |
| } | |
| return value; | |
| } | |
| function editField(button) { | |
| const fieldDiv = button.closest('.field'); | |
| const fieldName = fieldDiv.querySelector('.field-name').textContent.slice(0, -1); | |
| const fieldValueSpan = fieldDiv.querySelector('.field-value'); | |
| const currentValue = fieldValueSpan.textContent || fieldValueSpan.querySelector('a').href; | |
| const newValue = prompt(`Edit ${fieldName}:`, currentValue); | |
| if (newValue !== null) { | |
| fieldValueSpan.innerHTML = formatFieldValue(newValue); | |
| updateOutput(); | |
| } | |
| } | |
| function deleteField(button) { | |
| const fieldDiv = button.closest('.field'); | |
| if (confirm('Are you sure you want to delete this field?')) { | |
| fieldDiv.remove(); | |
| updateOutput(); | |
| } | |
| } | |
| function updateExpandCollapse(item) { | |
| const expandCollapseSpan = item.querySelector('.expand-collapse'); | |
| const childTree = item.querySelector('.child-tree'); | |
| if (childTree && childTree.children.length > 0) { | |
| expandCollapseSpan.style.visibility = 'visible'; | |
| expandCollapseSpan.textContent = item.classList.contains('collapsed') ? '▶' : '▼'; | |
| } else { | |
| expandCollapseSpan.style.visibility = 'hidden'; | |
| } | |
| } | |
| function toggleCollapse(event) { | |
| if (event.target.classList.contains('expand-collapse')) { | |
| const item = event.target.closest('.tree-item'); | |
| item.classList.toggle('collapsed'); | |
| updateExpandCollapse(item); | |
| updateOutput(); | |
| } | |
| } | |
| function addTreeFromBtn() { | |
| addTree(); | |
| updateOutput(); | |
| } | |
| function addTree(itemName='') { | |
| const container = document.getElementById('main-container'); | |
| const newTree = document.createElement('div'); | |
| newTree.className = 'tree'; | |
| itemName = itemName? itemName : `Object ${++objectCounter}`; | |
| const newTreeItem=createTreeItem(itemName, 0); | |
| newTree.appendChild(newTreeItem); | |
| container.appendChild(newTree); | |
| updateExpandCollapse(newTree); | |
| updateIndents(); | |
| return newTreeItem; | |
| } | |
| function getItemLevel(item) { | |
| return item.closest('.tree').querySelectorAll('.child-tree').length; | |
| } | |
| function closeModal() { | |
| const modal = document.getElementById('addFieldModal'); | |
| modal.style.display = 'none'; | |
| document.getElementById('fieldCategory').value = ''; | |
| document.getElementById('customFieldName').value = ''; | |
| document.getElementById('customFieldName').style.display = 'none'; | |
| document.getElementById('fieldValue').value = ''; | |
| } | |
| function updateIndents() { | |
| const trees = document.querySelectorAll('.tree'); | |
| let maxDepth = 0; | |
| trees.forEach(tree => { | |
| const depth = getTreeDepth(tree); | |
| maxDepth = Math.max(maxDepth, depth); | |
| }); | |
| const availableWidth = Math.max(window.innerWidth - 40, MIN_TREE_ITEM_WIDTH * (maxDepth + 1)); | |
| let baseIndent = Math.min(MAX_FIRST_LEVEL_INDENT, (availableWidth - MIN_TREE_ITEM_WIDTH) / maxDepth); | |
| document.querySelectorAll('.child-tree').forEach(childTree => { | |
| const level = getItemLevel(childTree); | |
| const indentWidth = Math.max(MIN_INDENT, baseIndent * (maxDepth - level + 1) / maxDepth); | |
| childTree.style.paddingLeft = `${indentWidth}px`; | |
| }); | |
| document.querySelectorAll('.tree-item').forEach(item => { | |
| item.style.minWidth = `${MIN_TREE_ITEM_WIDTH}px`; | |
| }); | |
| const mainContainer = document.getElementById('main-container'); | |
| mainContainer.style.overflowX = availableWidth > window.innerWidth ? 'scroll' : 'hidden'; | |
| } | |
| function getTreeDepth(element, depth = 0) { | |
| const childTree = element.querySelector(':scope > .tree-item > .child-tree'); | |
| if (!childTree) return depth; | |
| return getTreeDepth(childTree, depth + 1); | |
| } | |
| //Update Tree function | |
| function updateTree() { | |
| const textAreaFormat = document.querySelector('.output-tab.active').id.split('-')[1]; | |
| const changedTextArea = document.getElementById(`output-${textAreaFormat}-field`); | |
| const change = changedTextArea.value; | |
| const mainContainer = document.getElementById('main-container'); | |
| const backup = mainContainer.innerHTML; | |
| try { | |
| mainContainer.innerHTML = ''; | |
| if (textAreaFormat === 'json') { | |
| createTreesFromJson(change); | |
| } | |
| if (textAreaFormat ==='markdown') { | |
| createTreesFromMarkdown(change); | |
| } | |
| if (textAreaFormat === 'text') | |
| { | |
| createTreesFromIndentText(change); | |
| } | |
| } catch (error) { | |
| console.log(error); | |
| document.getElementById('parse-error').classList.add('show'); | |
| mainContainer.innerHTML = backup; | |
| return; | |
| } | |
| document.getElementById('parse-error').classList.remove('show'); | |
| updateIndents(); | |
| updateCollapseExpandState(); | |
| return; | |
| } | |
| function createTreesFromIndentText(text) { | |
| const lines = text.split('\n'); | |
| const rootNode = document.getElementById('main-container'); | |
| let currentIndent = -1; | |
| let currentNode = rootNode; | |
| for (let i = 0; i < lines.length; i++) { | |
| let line = lines[i]; | |
| if (line.trim() === '') { | |
| continue; | |
| } | |
| const lineIndent = getLineIndent(line); | |
| let lineTr = line.trim(); | |
| const itemName = lineTr.indexOf('-')==0 ? lineTr.slice(lineTr.indexOf('-')+1).trim() : '' | |
| const fieldName = lineTr.indexOf(':')>0 ? lineTr.split(':',1)[0].trim() : ''; | |
| const fieldValue = lineTr.slice(lineTr.indexOf(':')+1).trim(); | |
| //main container - > tree -> treeItem -> childTree -> ChildTreeItem | |
| if (itemName) { | |
| if (currentIndent < lineIndent) { | |
| if (currentNode.isSameNode(rootNode)) { | |
| currentNode=addTree(itemName); | |
| } else if (currentNode) { | |
| currentNode=addChild(currentNode,itemName); | |
| } | |
| currentIndent = lineIndent; | |
| } else if (currentIndent >= lineIndent) { | |
| // Move up the tree | |
| while (currentIndent >= lineIndent) { | |
| currentNode = currentNode.parentNode.parentNode ; | |
| currentIndent--; | |
| } | |
| if (currentNode.isSameNode(rootNode)) { | |
| currentNode=addTree(itemName); | |
| } else if (currentNode) { | |
| currentNode=addChild(currentNode,itemName); | |
| } | |
| currentIndent = lineIndent; | |
| } | |
| } else if (fieldName) { | |
| addFieldProperty(currentNode, fieldName, fieldValue); | |
| } | |
| } | |
| return; | |
| } | |
| function getLineIndent(line) { | |
| const match = line.match(/^\t*/); | |
| console.log(match); | |
| return match ? (match[0].length) : 0; | |
| } | |
| //Update collapse expand state of the whole tree | |
| function updateCollapseExpandState() { | |
| // Updates the expand/collapse state of all tree items | |
| document.querySelectorAll('.tree-item').forEach(updateExpandCollapse); | |
| } | |
| // Update output function | |
| function updateOutput(outputFormat='') { | |
| outputFormat=outputFormat?outputFormat:document.querySelector('.output-tab.active').id.split('-')[1]; | |
| const outputField = document.getElementById(`output-${outputFormat}-field`); | |
| const trees = document.querySelectorAll('.tree'); | |
| let output = ''; | |
| if (outputFormat === 'json') { | |
| output += '{\n'; | |
| } | |
| trees.forEach((tree) => { | |
| if (outputFormat === 'text') { | |
| output += generateTreeOutputIndent(tree, 0); | |
| output += '\n'; | |
| } | |
| else if (outputFormat === 'json') | |
| { | |
| output += generateTreeOutputJson(tree, 0); | |
| output += '\n'; | |
| } | |
| else if (outputFormat === "markdown") | |
| { | |
| output += generateTreeOutputMarkdown(tree, 0); | |
| output += '\n'; | |
| } | |
| }); | |
| if (outputFormat === 'json') { | |
| output += '}\n'; | |
| } | |
| outputField.value = output; | |
| document.getElementById('parse-error').classList.remove('show'); | |
| } | |
| // Generate the output for the tree in pure indented text format. | |
| function generateTreeOutputIndent(element, level) { | |
| let output = ''; | |
| const items = element.children; | |
| for (let item of items) { | |
| if (item.classList.contains('tree-item')) { | |
| const label = item.querySelector('.tree-label').textContent; | |
| const indent = '\t'.repeat(level); | |
| output += `${indent}- ${label}\n`; | |
| const fields = item.querySelector('.fields-container').querySelectorAll('.field'); | |
| fields.forEach(field => { | |
| const fieldName = field.querySelector('.field-name').textContent.slice(0, -1); | |
| const fieldValue = field.querySelector('.field-value').textContent; | |
| output += `${indent} ${fieldName}: ${fieldValue}\n`; | |
| }); | |
| const childTree = item.querySelector('.child-tree'); | |
| if (childTree) { | |
| output += generateTreeOutputIndent(childTree, level + 1); | |
| } | |
| } | |
| } | |
| return output; | |
| } | |
| // Generate a JSON string from the tree | |
| function generateTreeOutputJson(element, level) { | |
| let output = ''; | |
| const items = element.children; | |
| const indent = level * 2; | |
| for (let item of items) { | |
| if (item.classList.contains('tree-item')) { | |
| const label = item.querySelector('.tree-label').textContent; | |
| output += `${' '.repeat(indent+2)}\"${label}\": {\n`; | |
| const fields = item.querySelector('.fields-container').querySelectorAll('.field'); | |
| fields.forEach(field => { | |
| const fieldName = field.querySelector('.field-name').textContent.slice(0, -1); | |
| const fieldValue = field.querySelector('.field-value').textContent; | |
| output += `${' '.repeat(indent + 4)}\"${fieldName}\": \"${fieldValue}\",\n`;}) | |
| } | |
| const childTree = item.querySelector('.child-tree'); | |
| if (childTree) { | |
| output += generateTreeOutputJson(childTree, level + 1); | |
| } | |
| output += `${' '.repeat(indent+2)}},\n`; | |
| } | |
| return output; | |
| } | |
| // Generate the output in markdown format | |
| function generateTreeOutputMarkdown(element, level){ | |
| let output = ''; | |
| const items = element.children; | |
| const indent = level * 2; | |
| for (let item of items) { | |
| if (item.classList.contains('tree-item')) { | |
| const label = item.querySelector('.tree-label').textContent; | |
| output += `${' '.repeat(indent+2)}<details open>\n${' '.repeat(indent+4)}<summary>${label}</summary>\n${' '.repeat(indent+4)}<blockquote>\n`; | |
| const fields = item.querySelector('.fields-container').querySelectorAll('.field'); | |
| fields.forEach(field => { | |
| const fieldName = field.querySelector('.field-name').textContent.slice(0, -1); | |
| const fieldValue = field.querySelector('.field-value').textContent; | |
| output += `\n${fieldName}: \`\`\`${fieldValue}\`\`\`\n`;}) | |
| const childTree = item.querySelector('.child-tree'); | |
| if (childTree) { | |
| output += generateTreeOutputMarkdown(childTree, level + 1); | |
| } | |
| output += `${' '.repeat(indent+4)}</blockquote>\n${' '.repeat(indent+2)}</details>\n`; | |
| } | |
| } | |
| return output; | |
| } | |
| // update display and output when tabs are selected or switched | |
| function onSwitchTab(tab) { | |
| const textOutput = document.getElementById('output-text-field'); | |
| const jsonOutput = document.getElementById('output-json-field'); | |
| const markdownOutput = document.getElementById('output-markdown-field'); | |
| const textOutputTab = document.getElementById('output-text-tab'); | |
| const jsonOutputTab = document.getElementById('output-json-tab'); | |
| const markdownOutputTab = document.getElementById('output-markdown-tab'); | |
| if (tab === 'text') { | |
| textOutput.classList.add('active'); | |
| jsonOutput.classList.remove('active'); | |
| markdownOutput.classList.remove('active'); | |
| textOutputTab.classList.add('active'); | |
| jsonOutputTab.classList.remove('active'); | |
| markdownOutputTab.classList.remove('active'); | |
| updateOutput(); | |
| } else if (tab === 'json') { | |
| textOutput.classList.remove('active'); | |
| jsonOutput.classList.add('active'); | |
| markdownOutput.classList.remove('active'); | |
| textOutputTab.classList.remove('active'); | |
| jsonOutputTab.classList.add('active'); | |
| markdownOutputTab.classList.remove('active'); | |
| updateOutput(); | |
| } else if (tab === 'markdown') { | |
| textOutput.classList.remove('active'); | |
| jsonOutput.classList.remove('active'); | |
| markdownOutput.classList.add('active'); | |
| textOutputTab.classList.remove('active'); | |
| jsonOutputTab.classList.remove('active'); | |
| markdownOutputTab.classList.add('active'); | |
| updateOutput(); | |
| } | |
| } | |
| async function exportData(fileHandle) { | |
| const fileData = await fileHandle.getFile(); | |
| const format = fileData.name.split('.').pop(); | |
| let exportContent; | |
| let fileExtension; | |
| switch (format) { | |
| case 'json': | |
| updateOutput('json'); | |
| exportContent = document.getElementById('output-json-field').value; | |
| fileExtension = '.json'; | |
| break; | |
| case 'md': | |
| updateOutput('markdown'); | |
| exportContent = document.getElementById('output-markdown-field').value; | |
| fileExtension = '.md'; | |
| break; | |
| case 'txt': | |
| updateOutput('text'); | |
| exportContent = document.getElementById('output-text-field').value; | |
| fileExtension = '.txt'; | |
| break; | |
| case 'html': | |
| exportContent = `<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Exported Data</title> | |
| <style> | |
| /* Add any necessary styles here */ | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Exported Data</h1> | |
| </body> | |
| </html>`; | |
| fileExtension = '.html'; | |
| break; | |
| default: | |
| return; | |
| } | |
| const blob = new Blob([exportContent], { type: 'text/plain' }); | |
| const writable = await fileHandle.createWritable(); | |
| await writable.write(blob); | |
| await writable.close(); | |
| return; | |
| } | |
| // Add the export button | |
| const exportButton = document.getElementById('output-export-btn'); | |
| exportButton.addEventListener('click', async () => { | |
| async function getNewFileHandle() { | |
| const opts = { | |
| types: [ | |
| { | |
| description: "Text, Json, Markdown or Html", | |
| accept: { "text/plain": [".txt",".json",".md",".html"] }, | |
| }, | |
| ], | |
| }; | |
| return await window.showSaveFilePicker(opts); | |
| } | |
| const filehandler = await getNewFileHandle(); | |
| await exportData(filehandler); | |
| }); | |
| document.querySelectorAll(`textarea`).forEach((textArea) => { | |
| textArea.addEventListener(`keydown`, (e) => { | |
| if (e.keyCode === 9) { | |
| let selectionStart=textArea.selectionStart; | |
| textArea.value = `${textArea.value.substring(0, textArea.selectionStart)}\t${textArea.value.substring(textArea.selectionEnd)}`; | |
| textArea.selectionEnd = selectionStart + 1; | |
| textArea.selectionStart = textArea.selectionEnd; | |
| updateTree(); | |
| e.preventDefault(); | |
| } | |
| }, false); | |
| }); | |
| // Toggles the collapse/expand state of a tree item | |
| document.addEventListener('click', toggleCollapse); | |
| // Closes the modal when the close button is clicked | |
| document.querySelector('.close').onclick = closeModal; | |
| // Closes the modal when the background is clicked | |
| window.onclick = function(event) { | |
| const modal = document.getElementById('addFieldModal'); | |
| if (event.target == modal) { | |
| closeModal(); | |
| } | |
| } | |
| // Updates the indents of all tree items | |
| window.addEventListener('resize', updateIndents); | |
| // Updates the tree when the input is changed | |
| document.getElementById('output-text-field').addEventListener('input', updateTree); | |
| // Update the tree when DOM content is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| updateIndents(); | |
| updateOutput(); | |
| document.querySelectorAll('.tree-item').forEach(updateExpandCollapse); | |
| }); | |
| </script> | |
| </body></html> |