Sam Lai
added application file
b4466be
<!DOCTYPE html><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>