agentscript
Version:
AgentScript Model in Model/View architecture
678 lines (571 loc) • 25.3 kB
JavaScript
// import * as util from 'https://code.agentscript.org/src/utils.js'
import * as util from '../src/utils.js'
// =================== initialization ===================
// loading links
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = 'uielements.css'
link.type = 'text/css'
document.head.appendChild(link)
// loading divs.html
// await fetch('./divs.html')
// .then(response => response.text())
// .then(html => {
// // Insert after <body> tag starts
// document.body.insertAdjacentHTML('afterbegin', html)
// // console.log('fetch divs.html')
// })
await fetch('./divs.html')
.then(response => response.text())
.then(html => {
// Insert the divs.html content
document.body.insertAdjacentHTML('afterbegin', html)
console.log('divs.html loaded and parsed')
// Attach the click handlers for the menu options globally
document.getElementById('editOption').onclick = function (e) {
e.stopPropagation() // Prevent event bubbling
console.log('Edit option clicked') // Log for debugging
const elementId = window.selectedElementId
const jsonElement = window.ui.json.find(el => el.id == elementId)
if (jsonElement) {
showPopup(jsonElement.type, jsonElement) // Show the form for editing
}
document.getElementById('popupMenu').style.display = 'none' // Hide the popup menu
}
document.getElementById('deleteOption').onclick = function (e) {
e.stopPropagation() // Prevent event bubbling
console.log('Delete option clicked') // Log for debugging
const elementId = window.selectedElementId
const wrapper = document.querySelector(`[data-id="${elementId}"]`)
if (wrapper) {
wrapper.remove() // Remove the element from the UI
window.ui.json = window.ui.json.filter(
el => el.id !== elementId
) // Remove from JSON array
jsonToStorage() // Save the updated state
}
document.getElementById('popupMenu').style.display = 'none' // Hide the popup menu
}
document.getElementById('cancelOption').onclick = function (e) {
e.stopPropagation() // Prevent event bubbling
console.log('Cancel option clicked') // Log for debugging
document.getElementById('popupMenu').style.display = 'none' // Hide the popup menu
}
// Temporarily disable the popup hiding when clicking outside
// document.onclick = function (e) {
// const popupMenu = document.getElementById('popupMenu')
// if (!popupMenu.contains(e.target)) {
// popupMenu.style.display = 'none' // Hide the popup menu if clicking outside
// }
// }
// document.onclick = function (e) {
// const popupMenu = document.getElementById('popupMenu')
// // Check if the click was outside the popup menu
// if (
// !popupMenu.contains(e.target) &&
// popupMenu.style.display === 'block'
// ) {
// popupMenu.style.display = 'none' // Hide the popup menu
// }
// }
})
// Initialize the window.ui object if not already defined
window.ui = window.ui || {} // Ensure 'ui' exists
window.ui.json = [] // Initialize the json array
document.getElementById('uiContainer').addEventListener('contextmenu', e => {
e.preventDefault()
})
// =================== create ui forms for popups ===================
let elementType = ''
let currentDragElement = null
let offsetX = 0
let offsetY = 0
let editingElementId = null // Global variable to track editing
// Show popup modal to create new elements
export function showPopup(type, jsonElement = {}) {
elementType = type
const formContainer = document.getElementById('formContainer')
formContainer.innerHTML = '' // Clear previous form inputs
// Default or existing values from jsonElement
const name = jsonElement.name || ''
const command = jsonElement.command || ''
const min = jsonElement.min || 0
const max = jsonElement.max || 100
const step = jsonElement.step || 1
const value = jsonElement.value || ''
const selected = jsonElement.selected || ''
let formContent = `
<label for="name">Name:</label>
<input type="text" id="elementName" value="${name}" required><br>
`
if (type !== 'output') {
formContent += `
<label for="command">Command:</label>
<input type="text" id="elementCommand" value="${command}" required><br>
`
}
// Handle specific element types
let modelTitle = 'Button'
if (type === 'checkbox') {
modelTitle = 'Checkbox'
formContent += `
<label for="checked">Checked:</label>
<input type="checkbox" id="elementChecked" ${
jsonElement.checked ? 'checked' : ''
}><br>
`
} else if (type === 'dropdown') {
modelTitle = 'Dropdown'
formContent += `
<label for="values">Dropdown Options:</label>
<input type="text" id="elementOptions" value="${
jsonElement.options ? jsonElement.options.join(', ') : ''
}" required><br>
<label for="selected">Selected Value:</label>
<input type="text" id="elementSelected" value="${selected}" required><br>
`
} else if (type === 'range') {
modelTitle = 'Slider'
formContent += `
<label for="min">Min:</label>
<input type="number" id="elementMin" value="${min}" required><br>
<label for="max">Max:</label>
<input type="number" id="elementMax" value="${max}" required><br>
<label for="step">Step:</label>
<input type="number" id="elementStep" value="${step}"><br>
<label for="value">Current Value:</label>
<input type="number" id="elementValue" value="${value}" required><br>
`
} else if (type === 'output') {
modelTitle = 'Monitor'
formContent += `
<label for="monitor">Value/Function to Monitor:</label>
<input type="text" id="elementMonitor" value="${
jsonElement.monitor || ''
}" required><br>
<label for="fps">Frames per Second (FPS):</label>
<input type="number" id="elementFps" value="${jsonElement.fps || 10}"><br>
`
}
// document.getElementById('modal-header').innerText = 'Edit ' + type
document.getElementById('modal-header').innerText = modelTitle
formContainer.innerHTML = formContent
document.getElementById('popupModal').style.display = 'flex'
}
// Cancel the popup
export function cancel() {
document.getElementById('popupModal').style.display = 'none'
}
// =================== create a wrapper for a ui form ===================
function createElementWrapper(element, id) {
const wrapper = document.createElement('div')
wrapper.className = 'ui-element'
wrapper.dataset.id = id // Store the id in the wrapper for reference
// Set initial pixel-based position for the new element
const containerRect = document
.getElementById('uiContainer')
.getBoundingClientRect()
wrapper.style.left = containerRect.width / 2 - 50 + 'px' // Centered horizontally
wrapper.style.top = containerRect.height / 2 - 25 + 'px' // Centered vertically
wrapper.appendChild(element)
// Make the element draggable
wrapper.onmousedown = function (e) {
if (e.ctrlKey && e.detail === 1) {
dragMouseDown(e)
} else if (e.shiftKey) {
// Prevent normal click behavior
e.preventDefault()
const elementId = wrapper.dataset.id
// Show the popup menu near the clicked element
const popupMenu = document.getElementById('popupMenu')
popupMenu.style.display = 'block'
popupMenu.style.left = `${e.pageX}px`
popupMenu.style.top = `${e.pageY}px`
// Store the element being edited or deleted in a global variable
window.selectedElementId = elementId
// Stop further event propagation
e.stopPropagation()
}
}
return wrapper
}
// // Attach the click handlers for the menu options globally
// document.addEventListener('DOMContentLoaded', function () {
// console.log('DOM fully loaded and parsed')
// // Attach the click handlers for the menu options globally
// document.getElementById('editOption').onclick = function () {
// const elementId = window.selectedElementId
// const jsonElement = window.ui.json.find(el => el.id == elementId)
// if (jsonElement) {
// showPopup(jsonElement.type, jsonElement) // Show the form for editing
// }
// document.getElementById('popupMenu').style.display = 'none' // Hide the popup menu
// }
// document.getElementById('deleteOption').onclick = function () {
// const elementId = window.selectedElementId
// const wrapper = document.querySelector(`[data-id="${elementId}"]`)
// if (wrapper) {
// wrapper.remove() // Remove the element from the UI
// window.ui.json = window.ui.json.filter(el => el.id !== elementId) // Remove from JSON array
// jsonToStorage() // Save the updated state
// }
// document.getElementById('popupMenu').style.display = 'none' // Hide the popup menu
// }
// document.getElementById('cancelOption').onclick = function () {
// document.getElementById('popupMenu').style.display = 'none' // Hide the popup menu
// }
// // Hide the popup when clicking outside of it
// document.onclick = function (e) {
// const popupMenu = document.getElementById('popupMenu')
// if (!popupMenu.contains(e.target)) {
// popupMenu.style.display = 'none' // Hide the popup menu if clicking outside
// }
// }
// })
// function handleDeletion() {
// if (window.ui.json && window.ui.json.length === 0) {
// document.getElementById('uiContainer').innerHTML = '' // Clear the container if empty
// }
// }
// =================== create ui form & json from popups ===================
// Submit the form and add the element. called by divs
// new
export function submitForm() {
const name = document.getElementById('elementName').value
const id = editingElementId || Date.now() // Use existing id if editing, otherwise generate a new one
let command
if (elementType !== 'output') {
command = document.getElementById('elementCommand').value
}
let jsonElement = window.ui.json.find(el => el.id === id)
// Capture the old position if editing an element
let oldPosition = jsonElement ? jsonElement.position : { x: 100, y: 100 }
if (jsonElement) {
// Delete the old element from the DOM and JSON array
const wrapper = document.querySelector(`[data-id="${id}"]`)
if (wrapper) {
wrapper.remove() // Remove the old element from the UI
}
window.ui.json = window.ui.json.filter(el => el.id !== id) // Remove from JSON array
}
// Create a new JSON object with updated values and retain old position
jsonElement = {
id: id,
type: elementType,
name: name,
command: command,
position: oldPosition, // Retain old position
}
// Populate additional properties based on element type
if (elementType === 'checkbox') {
jsonElement.checked = document.getElementById('elementChecked').checked
} else if (elementType === 'dropdown') {
jsonElement.options = document
.getElementById('elementOptions')
.value.split(/,\s*/)
jsonElement.selected = document.getElementById('elementSelected').value
} else if (elementType === 'range') {
jsonElement.min = document.getElementById('elementMin').value
jsonElement.max = document.getElementById('elementMax').value
jsonElement.step = document.getElementById('elementStep').value
jsonElement.value = document.getElementById('elementValue').value
} else if (elementType === 'output') {
jsonElement.monitor = document.getElementById('elementMonitor').value
jsonElement.fps = document.getElementById('elementFps').value || 10
}
// Add the new element to the JSON array
window.ui.json.push(jsonElement)
// Create the new UI element
createElementFromJSON(jsonElement)
// Save the updated state
jsonToStorage()
// Close the modal
document.getElementById('popupModal').style.display = 'none'
editingElementId = null // Reset after submission
}
// old
// export function submitForm() {
// const name = document.getElementById('elementName').value
// const id = editingElementId || Date.now() // Use existing id if editing, otherwise generate a new one
// let command
// if (elementType !== 'output') {
// command = document.getElementById('elementCommand').value
// }
// let jsonElement = window.ui.json.find(el => el.id === id)
// // Track the old position if editing
// let oldPosition = jsonElement ? jsonElement.position : { x: 100, y: 100 }
// if (jsonElement) {
// // Delete the old element from the DOM and JSON array
// const wrapper = document.querySelector(`[data-id="${id}"]`)
// if (wrapper) {
// wrapper.remove() // Remove the old element from the UI
// }
// window.ui.json = window.ui.json.filter(el => el.id !== id) // Remove from JSON array
// }
// // Create a new JSON object with updated values and retain old position
// jsonElement = {
// id: id,
// type: elementType,
// name: name,
// command: command,
// position: {
// x: oldPosition.x, // Retain old position
// y: oldPosition.y, // Retain old position
// },
// }
// // Populate additional properties based on element type
// if (elementType === 'checkbox') {
// jsonElement.checked = document.getElementById('elementChecked').checked
// } else if (elementType === 'dropdown') {
// jsonElement.options = document
// .getElementById('elementOptions')
// .value.split(/,\s*/)
// jsonElement.selected = document.getElementById('elementSelected').value
// } else if (elementType === 'range') {
// jsonElement.min = document.getElementById('elementMin').value
// jsonElement.max = document.getElementById('elementMax').value
// jsonElement.step = document.getElementById('elementStep').value
// jsonElement.value = document.getElementById('elementValue').value
// } else if (elementType === 'output') {
// jsonElement.monitor = document.getElementById('elementMonitor').value
// jsonElement.fps = document.getElementById('elementFps').value || 10
// }
// // Add the new element to the JSON array
// window.ui.json.push(jsonElement)
// // Create the new UI element
// createElementFromJSON(jsonElement)
// // Save the updated state
// jsonToStorage()
// // Close the modal
// document.getElementById('popupModal').style.display = 'none'
// editingElementId = null // Reset after submission
// }
// =================== json to element ===================
function createElementFromJSON(jsonElement) {
let elementWrapper
// Handle element creation based on type
if (jsonElement.type === 'button') {
const button = document.createElement('button')
button.innerText = jsonElement.name
button.addEventListener('click', () => eval(jsonElement.command))
elementWrapper = createElementWrapper(button, jsonElement.id)
} else if (jsonElement.type === 'checkbox') {
const checkboxWrapper = document.createElement('div')
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.name = jsonElement.name
checkbox.checked = jsonElement.checked
const label = document.createElement('label')
label.innerText = jsonElement.name
label.classList.add('checkbox-label')
// checkbox.addEventListener('change', () => eval(jsonElement.command))
checkbox.addEventListener('change', function () {
const value = checkbox.checked
const checked = checkbox.checked
eval(jsonElement.command)
})
checkboxWrapper.appendChild(checkbox)
checkboxWrapper.appendChild(label)
elementWrapper = createElementWrapper(checkboxWrapper, jsonElement.id)
} else if (jsonElement.type === 'dropdown') {
const selectWrapper = document.createElement('div')
selectWrapper.classList.add('dropdown-wrapper')
const select = document.createElement('select')
select.name = jsonElement.name
jsonElement.options.forEach(optionText => {
const option = document.createElement('option')
option.value = optionText
option.text = optionText
if (option.value === jsonElement.selected) {
option.selected = true
}
select.appendChild(option)
})
const label = document.createElement('label')
label.innerText = jsonElement.name
selectWrapper.appendChild(label)
selectWrapper.appendChild(select)
// select.addEventListener('change', () => eval(jsonElement.command))
select.addEventListener('change', function () {
const value = select.value
eval(jsonElement.command)
})
elementWrapper = createElementWrapper(selectWrapper, jsonElement.id)
} else if (jsonElement.type === 'range') {
const rangeWrapper = document.createElement('div')
const range = document.createElement('input')
range.type = 'range'
range.name = jsonElement.name
range.min = jsonElement.min
range.max = jsonElement.max
range.step = jsonElement.step
range.value = jsonElement.value
const rangeLabel = document.createElement('div')
rangeLabel.classList.add('range-wrapper')
const nameLabel = document.createElement('span')
nameLabel.innerText = jsonElement.name
const valueLabel = document.createElement('span')
valueLabel.innerText = range.value
// range.addEventListener('input', () => eval(jsonElement.command))
range.addEventListener('input', function () {
valueLabel.innerText = range.value
const value = range.value
eval(jsonElement.command)
})
rangeLabel.appendChild(nameLabel)
rangeLabel.appendChild(valueLabel)
rangeWrapper.appendChild(rangeLabel)
rangeWrapper.appendChild(range)
elementWrapper = createElementWrapper(rangeWrapper, jsonElement.id)
} else if (jsonElement.type === 'output') {
const monitorWrapper = document.createElement('div')
monitorWrapper.classList.add('output-wrapper')
const label = document.createElement('label')
label.innerText = jsonElement.name
const output = document.createElement('output')
output.name = jsonElement.name
let previousValue = null
function checkValue() {
let currentValue
try {
currentValue = eval(jsonElement.monitor)
} catch (error) {
console.error('Monitor command execution failed: ', error)
}
if (currentValue !== previousValue) {
output.value = currentValue
previousValue = currentValue
}
}
setInterval(checkValue, 1000 / jsonElement.fps)
monitorWrapper.appendChild(label)
monitorWrapper.appendChild(output)
elementWrapper = createElementWrapper(monitorWrapper, jsonElement.id)
}
// Apply the element's position from JSON
elementWrapper.style.left = jsonElement.position.x + 'px'
elementWrapper.style.top = jsonElement.position.y + 'px'
// Append the recreated element to the UI container
// document.getElementById('uiContainer').appendChild(elementWrapper)
document.getElementById('uiContainer').appendChild(elementWrapper)
// }
}
// =================== ui element utilities ===================
// Make an element draggable when Ctrl is pressed
function dragMouseDown(e) {
// Only start dragging if Ctrl key is held down
// if (!e.ctrlKey) return
e.preventDefault()
currentDragElement = e.target.closest('.ui-element')
// Get initial mouse position and element position
const uiContainerTop = document
.getElementById('uiContainer')
.getBoundingClientRect().top
offsetX = e.clientX - currentDragElement.getBoundingClientRect().left
offsetY =
e.clientY -
currentDragElement.getBoundingClientRect().top +
uiContainerTop
console.log('Initial Mouse Position:', e.clientX, e.clientY)
console.log(
'Initial Element Position:',
currentDragElement.style.left,
currentDragElement.style.top
)
console.log('Calculated Offsets:', offsetX, offsetY)
document.onmousemove = elementDrag
document.onmouseup = closeDragElement
}
// Drag the element
function elementDrag(e) {
e.preventDefault()
const newX = e.clientX - offsetX
const newY = e.clientY - offsetY
// Update the UI element's position in the DOM
currentDragElement.style.left = newX + 'px'
currentDragElement.style.top = newY + 'px'
}
function closeDragElement() {
document.onmousemove = null
document.onmouseup = null
// Update the associated JSON's position after dragging stops
const elementId = currentDragElement.dataset.id
const jsonElement = window.ui.json.find(el => el.id == elementId)
if (jsonElement) {
// Get the current position directly (relative to the parent container)
jsonElement.position.x = parseInt(currentDragElement.style.left, 10)
jsonElement.position.y = parseInt(currentDragElement.style.top, 10)
}
// save the updated state to localStorage
jsonToStorage()
}
// Functions to recreate all elements from stored JSON
function loadElementsFromJSON() {
const uiContainer = document.getElementById('uiContainer')
// Clear existing elements to avoid duplication
uiContainer.innerHTML = ''
// Loop through each element in the window.ui.json array
window.ui.json.forEach(jsonElement => {
createElementFromJSON(jsonElement) // Create each element from JSON
})
}
// =================== localStorage utilities ===================
// let containerDiv = 'uiContainer' // div for dynamically added UI elements
let localStorageName // default localStorage name for caching current elements
export function setStorageName(name) {
localStorageName = name
}
export function clearElements() {
localStorage.removeItem(localStorageName)
window.ui.json = []
}
const minJsonString =
'[{"command":"ui.anim.start()","id":1728678676436,"name":"start","position":{"x":29,"y":33},"type":"button"},{"command":"ui.anim.stop()","id":1728927362120,"name":"stop","position":{"x":92,"y":34},"type":"button"},{"command":"ui.reset()","id":1728927569824,"name":"reset","position":{"x":59,"y":68},"type":"button"},{"command":"ui.anim.setFps(value)","id":1728682054456,"max":"60","min":"0","name":"fps","position":{"x":165,"y":35},"step":"1","type":"range","value":"30"},{"id":1729270887157,"type":"output","name":"ticks","position":{"x":330,"y":37},"monitor":"ui.model.ticks","fps":"10"}]'
const minJson = JSON.parse(minJsonString)
// ui.json to localStorage. was saveUIState
function jsonToStorage() {
const jsonString = JSON.stringify(window.ui.json) // Convert to string
localStorage.setItem(localStorageName, jsonString) // Save it to localStorage
}
// =================== functions called by users html file ===================
// let AppName
export function setAppState(
model,
view,
anim,
storageName = model.constructor.name
) {
Object.assign(window.ui, { model, view, anim })
setStorageName(storageName)
anim.stop() // stop the animation, use uielements to control
view.draw() // draw once to see the model before running animator
console.log('localStorageName', localStorageName)
}
// was loadUIState, now includes minJson for new html file
export function createElements(useMinElements = true) {
const savedState = localStorage.getItem(localStorageName)
if (savedState) {
window.ui.json = JSON.parse(savedState) // Convert back to array
} else if (useMinElements) {
window.ui.json = minJson
} else {
return
}
loadElementsFromJSON()
}
// a bit risky: depends on model, view, anim stored in ui by app
export function reset() {
window.ui.model.reset()
// window.ui.view.reset()
window.ui.anim.reset()
window.ui.view.draw()
}
Object.assign(window.ui, {
showPopup,
submitForm,
cancel,
reset,
util,
createElements,
jsonToStorage,
})