input, change, submit on text/number/range/color/date/file/checkbox/radio/select/form/textarea and form API

This commit is contained in:
Face
2025-08-07 14:05:41 +03:00
parent c3e72093ea
commit d4aa741452
24 changed files with 745 additions and 67 deletions

View File

@@ -34,7 +34,7 @@ class HTMLElement:
func get_preserved_text() -> String:
return text_content
func get_bbcode_formatted_text(parser: HTMLParser = null) -> String:
func get_bbcode_formatted_text(parser: HTMLParser) -> String:
var styles = {}
if parser != null:
styles = parser.get_element_styles_with_inheritance(self, "", [])
@@ -142,8 +142,7 @@ func get_element_styles_with_inheritance(element: HTMLElement, event: String = "
var styles = {}
var class_names = extract_class_names(element)
styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(element.tag_name, event, class_names, element))
styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(event, element))
# Apply inline styles (higher priority) - force override CSS rules
var inline_style = element.get_attribute("style")
if inline_style.length() > 0:
@@ -169,8 +168,7 @@ func get_element_styles_internal(element: HTMLElement, event: String = "") -> Di
# Apply CSS rules
if parse_result.css_parser:
var class_names = extract_class_names(element)
styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(element.tag_name, event, class_names, element))
styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(event, element))
# Apply inline styles (higher priority) - force override CSS rules
var inline_style = element.get_attribute("style")
@@ -203,7 +201,7 @@ func parse_inline_style_with_event(style_string: String, event: String = "") ->
else:
# Check if this is a CSS class that might have pseudo-class rules
if parse_result.css_parser and parse_result.css_parser.stylesheet:
var pseudo_styles = parse_result.css_parser.stylesheet.get_styles_for_element("", event, [utility_name], null)
var pseudo_styles = parse_result.css_parser.stylesheet.get_styles_for_element(event, null)
if not pseudo_styles.is_empty():
for property in pseudo_styles:
properties[property] = pseudo_styles[property]
@@ -287,7 +285,7 @@ func find_by_id(element_id: String) -> HTMLElement:
return null
func register_dom_node(element: HTMLElement, node: Control) -> void:
func register_dom_node(element: HTMLElement, node) -> void:
var element_id = element.get_id()
if element_id.length() > 0:
parse_result.dom_nodes[element_id] = node

View File

@@ -10,6 +10,7 @@ class EventSubscription:
var lua_api: LuaAPI
var connected_signal: String = ""
var connected_node: Node = null
var callback_func: Callable
var dom_parser: HTMLParser
var event_subscriptions: Dictionary = {}
@@ -489,6 +490,8 @@ func _element_on_event_handler(vm: LuauVM) -> int:
var signal_node = get_dom_node(dom_node, "signal")
var success = LuaEventUtils.connect_element_event(signal_node, event_name, subscription)
if not success:
print("ERROR: Failed to connect ", event_name, " event for ", element_id)
return _handle_subscription_result(vm, subscription, success)
@@ -641,6 +644,11 @@ func _execute_lua_callback(subscription: EventSubscription, args: Array = []) ->
else:
subscription.vm.lua_pop(1)
func _execute_input_event_callback(subscription: EventSubscription, event_data: Dictionary) -> void:
if not event_subscriptions.has(subscription.id):
return
_execute_lua_callback(subscription, [event_data])
# Global input processing
func _input(event: InputEvent) -> void:
if event is InputEventKey:
@@ -699,6 +707,108 @@ func _handle_mousemove_event(mouse_event: InputEventMouseMotion, subscription: E
}
_execute_lua_callback(subscription, [mouse_info])
# Input event handlers
func _on_input_text_changed(new_text: String, subscription: EventSubscription) -> void:
_execute_input_event_callback(subscription, {"value": new_text})
func _on_input_focus_lost(subscription: EventSubscription) -> void:
if not event_subscriptions.has(subscription.id):
return
# Get the current text value from the input node
var dom_node = dom_parser.parse_result.dom_nodes.get(subscription.element_id, null)
if dom_node:
var current_text = ""
if dom_node.has_method("get_text"):
current_text = dom_node.get_text()
elif "text" in dom_node:
current_text = dom_node.text
var event_info = {"value": current_text}
_execute_lua_callback(subscription, [event_info])
func _on_input_value_changed(new_value, subscription: EventSubscription) -> void:
_execute_input_event_callback(subscription, {"value": new_value})
func _on_input_color_changed(new_color: Color, subscription: EventSubscription) -> void:
_execute_input_event_callback(subscription, {"value": "#" + new_color.to_html(false)})
func _on_input_toggled(pressed: bool, subscription: EventSubscription) -> void:
_execute_input_event_callback(subscription, {"value": pressed})
func _on_input_item_selected(index: int, subscription: EventSubscription) -> void:
if not event_subscriptions.has(subscription.id):
return
# Get value from OptionButton
var dom_node = dom_parser.parse_result.dom_nodes.get(subscription.element_id, null)
var value = ""
var text = ""
if dom_node and dom_node is OptionButton:
var option_button = dom_node as OptionButton
text = option_button.get_item_text(index)
# Get actual value attribute (stored as metadata)
var metadata = option_button.get_item_metadata(index)
value = str(metadata) if metadata != null else text
var event_info = {"index": index, "value": value, "text": text}
_execute_lua_callback(subscription, [event_info])
func _on_file_selected(file_path: String, subscription: EventSubscription) -> void:
if not event_subscriptions.has(subscription.id):
return
var dom_node = dom_parser.parse_result.dom_nodes.get(subscription.element_id, null)
if dom_node:
var file_container = dom_node.get_parent() # FileContainer (HBoxContainer)
if file_container:
var input_element = file_container.get_parent() # Input Control
if input_element and input_element.has_method("get_file_info"):
var file_info = input_element.get_file_info()
if not file_info.is_empty():
_execute_lua_callback(subscription, [file_info])
return
# Fallback
var file_name = file_path.get_file()
_execute_lua_callback(subscription, [{"fileName": file_name}])
func _on_date_selected_text(date_text: String, subscription: EventSubscription) -> void:
if not event_subscriptions.has(subscription.id):
return
var event_info = {"value": date_text}
_execute_lua_callback(subscription, [event_info])
func _on_form_submit(subscription: EventSubscription) -> void:
if not event_subscriptions.has(subscription.id):
return
# Find parent form
var form_data = {}
var element = dom_parser.find_by_id(subscription.element_id)
if element:
var form_element = element.parent
while form_element and form_element.tag_name != "form":
form_element = form_element.parent
if form_element:
var form_dom_node = dom_parser.parse_result.dom_nodes.get(form_element.get_attribute("id"), null)
if form_dom_node and form_dom_node.has_method("submit_form"):
form_data = form_dom_node.submit_form()
var event_info = {"data": form_data}
_execute_lua_callback(subscription, [event_info])
func _on_text_submit(text: String, subscription: EventSubscription) -> void:
if not event_subscriptions.has(subscription.id):
return
var event_info = {"value": text}
_execute_lua_callback(subscription, [event_info])
# DOM node utilities
func get_dom_node(node: Node, purpose: String = "general") -> Node:
if not node:
@@ -717,6 +827,10 @@ func get_dom_node(node: Node, purpose: String = "general") -> Node:
return node.get("rich_text_label")
elif node.get_node_or_null("RichTextLabel"):
return node.get_node_or_null("RichTextLabel")
elif node is LineEdit or node is TextEdit or node is SpinBox or node is HSlider:
return node
elif node is CheckBox or node is ColorPickerButton or node is OptionButton:
return node
else:
return node
"text":

View File

@@ -871,6 +871,281 @@ var HTML_CONTENT_DOM_MANIPULATION = """
""".to_utf8_buffer()
var HTML_CONTENT = """<head>
<title>Input Events API Demo</title>
<icon src="https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Lua-Logo.svg/256px-Lua-Logo.svg.png">
<meta name="theme-color" content="#059669">
<meta name="description" content="Demonstrating input element events with GURT Lua API">
<style>
body { bg-[#f0fdf4] p-6 }
h1 { text-[#059669] text-3xl font-bold text-center }
h2 { text-[#047857] text-xl font-semibold }
.container { bg-[#ffffff] p-6 rounded-lg shadow-lg max-w-4xl mx-auto }
.form-group { flex flex-col gap-2 mb-4 }
.input-demo { bg-[#f8fafc] p-4 rounded-lg border }
.event-log { bg-[#1f2937] text-white p-4 rounded-lg font-mono text-sm max-h-64 overflow-auto }
.form-button { bg-[#10b981] text-white px-4 py-2 rounded hover:bg-[#059669] cursor-pointer }
.clear-btn { bg-[#ef4444] text-white px-4 py-2 rounded hover:bg-[#dc2626] cursor-pointer }
</style>
<script>
-- Get the event log element
local eventLog = gurt.select('#event-log')
local function logEvent(elementType, eventType, data)
local timestamp = Time.format(Time.now(), '%H:%M:%S')
local message = '[' .. timestamp .. '] ' .. elementType .. ' -> ' .. eventType
if data then
message = message .. ': ' .. data
end
print(message)
end
-- Text Input Events
local textInput = gurt.select('#text-input')
print('Text input found:', textInput)
if textInput then
textInput:on('input', function(e)
logEvent('Text Input', 'input', e.value)
end)
textInput:on('change', function(e)
logEvent('Text Input', 'change', e.value)
end)
textInput:on('focusin', function()
logEvent('Text Input', 'focusin', nil)
end)
textInput:on('focusout', function()
logEvent('Text Input', 'focusout', nil)
end)
else
print('Text input not found!')
end
-- Email Input Events
local emailInput = gurt.select('#email-input')
emailInput:on('input', function(e)
logEvent('Email Input', 'input', e.value)
end)
emailInput:on('change', function(e)
logEvent('Email Input', 'change', e.value)
end)
-- Password Input Events
local passwordInput = gurt.select('#password-input')
passwordInput:on('input', function(e)
logEvent('Password Input', 'input', table.tostring(e))
end)
passwordInput:on('change', function(e)
logEvent('Password Input', 'change', table.tostring(e))
end)
-- Number Input Events
local numberInput = gurt.select('#number-input')
numberInput:on('change', function(e)
logEvent('Number Input', 'change', e.value)
end)
-- Range Input Events
local rangeInput = gurt.select('#range-input')
rangeInput:on('change', function(e)
logEvent('Range Input', 'change', e.value)
end)
-- Color Input Events
local colorInput = gurt.select('#color-input')
colorInput:on('change', function(e)
logEvent('Color Input', 'change', e.value)
end)
-- Date Input Events
local dateInput = gurt.select('#date-input')
dateInput:on('change', function(e)
logEvent('Date Input', 'change', e.value)
end)
-- File Input Events
local fileInput = gurt.select('#file-input')
fileInput:on('change', function(e)
if e.dataURL then
e.dataURL = e.dataURL:sub(1, 100)
end
if e.text then
e.text = e.text:sub(1, 100)
end
logEvent('File Input', 'change', table.tostring(e))
end)
-- Checkbox Events
local checkbox = gurt.select('#checkbox')
checkbox:on('change', function(e)
logEvent('Checkbox', 'change', table.tostring(e))
end)
-- Radio Button Events
local radio1 = gurt.select('#radio1')
local radio2 = gurt.select('#radio2')
local radio3 = gurt.select('#radio3')
radio1:on('change', function(e)
logEvent('Radio Button 1', 'change', table.tostring(e))
end)
radio2:on('change', function(e)
logEvent('Radio Button 2', 'change', table.tostring(e))
end)
radio3:on('change', function(e)
logEvent('Radio Button 3', 'change', table.tostring(e))
end)
-- Textarea Events
local textarea = gurt.select('#textarea')
textarea:on('input', function(e)
logEvent('Textarea', 'input', e.value:sub(1, 20) .. '...')
end)
textarea:on('change', function(e)
logEvent('Textarea', 'change', 'Length: ' .. #e.value)
end)
-- Select Events
local selectElement = gurt.select('#select-element')
selectElement:on('change', function(e)
logEvent('Select', 'change', 'Index: ' .. table.tostring(e))
end)
-- Button Events
local submitBtn = gurt.select('#submit-btn')
submitBtn:on('click', function()
logEvent('Submit Button', 'click', nil)
end)
submitBtn:on('submit', function(e)
logEvent('Form', 'submit', table.tostring(e))
end)
-- Clear log button
local clearBtn = gurt.select('#clear-btn')
clearBtn:on('click', function()
eventLog.text = '--- Event Log Cleared ---\\n'
eventCount = 0
end)
-- Initial log message
logEvent('System', 'initialized', 'Input events demo ready')
</script>
</head>
<body>
<h1>🎛️ Input Events API Demo</h1>
<div style="container mt-6">
<div style="flex flex-row gap-6">
<!-- Input Forms Column -->
<div style="flex-1">
<h2>Input Elements</h2>
<form id="demo-form">
<div style="form-group">
<label>Text Input:</label>
<input id="text-input" key="username" type="text" placeholder="Type something..." />
</div>
<div style="form-group">
<label>Email Input:</label>
<input id="email-input" key="email" type="email" placeholder="Enter email..." />
</div>
<div style="form-group">
<label>Password Input:</label>
<input id="password-input" key="password" type="password" placeholder="Enter password..." />
</div>
<div style="form-group">
<label>Number Input:</label>
<input id="number-input" key="score" type="number" min="0" max="100" value="50" />
</div>
<div style="form-group">
<label>Range Input:</label>
<input id="range-input" key="volume" type="range" min="0" max="100" value="25" />
</div>
<div style="form-group">
<label>Color Input:</label>
<input id="color-input" key="favoriteColor" type="color" value="#ff0000" />
</div>
<div style="form-group">
<label>Date Input:</label>
<input id="date-input" key="birthDate" type="date" value="2024-01-01" />
</div>
<div style="form-group">
<label>File Input:</label>
<input id="file-input" key="attachment" type="file" accept=".txt,.pdf,.png" />
</div>
<div style="form-group">
<input id="checkbox" key="newsletter" type="checkbox" />
<p>Checkbox Option</p>
</div>
<div style="form-group">
<input id="radio1" key="option" type="radio" group="options" value="option1" /><span>Option 1</span>
<input id="radio2" key="option" type="radio" group="options" value="option2" /><span>Option 2</span>
<input id="radio3" key="option" type="radio" group="options" value="option3" /><span>Option 3</span>
</div>
<div style="form-group">
<label>Textarea:</label>
<textarea id="textarea" key="message" placeholder="Write something longer..." rows="3"></textarea>
</div>
<div style="form-group">
<label>Select Dropdown:</label>
<select id="select-element" key="fruit">
<option value="apple">Apple</option>
<option value="banana" selected="true">Banana</option>
<option value="orange">Orange</option>
<option value="grape">Grape</option>
</select>
</div>
<div style="flex gap-2">
<button id="submit-btn" type="submit" style="form-button">Submit Form</button>
<button id="clear-btn" type="button" style="clear-btn">Clear Log</button>
</div>
</form>
</div>
<!-- Event Log Column -->
<div style="flex-1">
<h2>Event Log</h2>
<div style="event-log">
<pre id="event-log">Waiting for events...
</pre>
</div>
<div style="bg-[#e0f2fe] p-4 rounded-lg mt-4">
<h3 style="text-[#0277bd] font-semibold mb-2">Available Events:</h3>
<ul style="text-[#01579b] space-y-1 text-sm">
<li><strong>input:</strong> Fires as you type (real-time)</li>
<li><strong>change:</strong> Fires when value changes and element loses focus</li>
<li><strong>focusin:</strong> Fires when element gains focus</li>
<li><strong>focusout:</strong> Fires when element loses focus</li>
<li><strong>click:</strong> Fires when button is clicked</li>
<li><strong>submit:</strong> Fires when form is submitted (includes form data)</li>
</ul>
</div>
</div>
</div>
</div>
</body>
""".to_utf8_buffer()
var HTML_CONTENT_CLIPBOARD = """<head>
<title>Network & Clipboard API Demo</title>
<icon src="https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Lua-Logo.svg/256px-Lua-Logo.svg.png">
<meta name="theme-color" content="#059669">

View File

@@ -39,7 +39,7 @@ static func load_web_font(font_info: Dictionary) -> void:
http_request.timeout = 30.0
http_request.request_completed.connect(func(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray):
http_request.request_completed.connect(func(_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray):
if response_code == 200:
if body.size() > 0:

View File

@@ -12,7 +12,7 @@ func _ready():
child_entered_tree.connect(_on_child_added)
child_exiting_tree.connect(_on_child_removed)
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
list_type = element.get_attribute("type").to_lower()
if list_type == "":
list_type = "disc" if not is_ordered else "decimal"
@@ -67,7 +67,7 @@ func calculate_marker_width(element: HTMLParser.HTMLElement) -> float:
return max(width, 20.0 if not is_ordered else 30.0)
func create_li_node(element: HTMLParser.HTMLElement, index: int, parser: HTMLParser = null) -> Control:
func create_li_node(element: HTMLParser.HTMLElement, index: int, parser: HTMLParser) -> Control:
var li_container = HBoxContainer.new()
# Create marker

View File

@@ -1,4 +1,4 @@
extends Control
func init(_element: HTMLParser.HTMLElement, _parser: HTMLParser = null) -> void:
func init(_element: HTMLParser.HTMLElement, _parser: HTMLParser) -> void:
pass

View File

@@ -4,7 +4,7 @@ extends HBoxContainer
var current_element: HTMLParser.HTMLElement
var current_parser: HTMLParser
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
current_element = element
current_parser = parser
var button_node: Button = $ButtonNode
@@ -26,6 +26,8 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
button_node.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
apply_button_styles(element, parser)
parser.register_dom_node(element, button_node)
func apply_button_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
if not element or not parser:
@@ -150,12 +152,6 @@ func apply_button_color_with_states(button: Button, normal_color: Color, hover_c
button.add_theme_stylebox_override("normal", style_normal)
button.add_theme_stylebox_override("hover", style_hover)
button.add_theme_stylebox_override("pressed", style_pressed)
func apply_button_radius(button: Button, radius: int) -> void:
# Radius is now handled in create_button_stylebox
# This method is kept for backward compatibility but is deprecated
pass
func apply_padding_to_stylebox(style_box: StyleBoxFlat, styles: Dictionary) -> void:
# Apply general padding first

View File

@@ -1,5 +1,5 @@
class_name HTMLDiv
extends VBoxContainer
func init(_element: HTMLParser.HTMLElement, _parser: HTMLParser = null):
func init(_element: HTMLParser.HTMLElement, _parser: HTMLParser):
pass

View File

@@ -1,4 +1,75 @@
extends VBoxContainer
func init(_element: HTMLParser.HTMLElement, _parser: HTMLParser = null) -> void:
pass
var form_element: HTMLParser.HTMLElement
var form_parser: HTMLParser
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
form_element = element
form_parser = parser
parser.register_dom_node(element, self)
func submit_form() -> Dictionary:
var form_data = {}
if not form_element:
return form_data
var form_inputs = collect_form_elements(form_element)
for input_element in form_inputs:
# Use 'key' attribute as primary identifier for form field mapping
var key_attr = input_element.get_attribute("key")
var name_attr = input_element.get_attribute("name")
var id_attr = input_element.get_attribute("id")
# Priority: key > name > id > tag_name
var key = key_attr if not key_attr.is_empty() else name_attr if not name_attr.is_empty() else id_attr if not id_attr.is_empty() else input_element.tag_name
# Get the DOM node for this element
if form_parser:
var element_id = input_element.get_attribute("id")
if element_id.is_empty():
element_id = input_element.tag_name
var dom_node = form_parser.parse_result.dom_nodes.get(element_id, null)
if dom_node:
var value = get_input_value(input_element.tag_name, dom_node)
if value != null:
form_data[key] = value
return form_data
func collect_form_elements(element: HTMLParser.HTMLElement) -> Array:
var form_inputs = []
# Check if current element is an input element
if element.tag_name in ["input", "textarea", "select"]:
form_inputs.append(element)
# Recursively check children
for child in element.children:
form_inputs.append_array(collect_form_elements(child))
return form_inputs
func get_input_value(tag_name: String, dom_node: Node):
match tag_name:
"input":
if dom_node.has_method("get_text"):
return dom_node.get_text()
elif dom_node.has_method("is_pressed"):
return dom_node.is_pressed()
elif dom_node is ColorPickerButton:
return "#" + dom_node.color.to_html()
elif dom_node is SpinBox:
return dom_node.value
elif dom_node is HSlider:
return dom_node.value
"textarea":
if dom_node is TextEdit:
return dom_node.text
"select":
if dom_node is OptionButton:
return dom_node.get_item_metadata(dom_node.selected)
return null

View File

@@ -1,6 +1,6 @@
extends TextureRect
func init(element: HTMLParser.HTMLElement, _parser: HTMLParser = null) -> void:
func init(element: HTMLParser.HTMLElement, _parser: HTMLParser) -> void:
var src = element.get_attribute("src")
if !src: return print("Ignoring <img/> tag without \"src\" attribute.")

View File

@@ -6,8 +6,9 @@ const BROWSER_TEXT: Theme = preload("res://Scenes/Styles/BrowserText.tres")
var custom_hex_input: LineEdit
var _file_text_content: String = ""
var _file_binary_content: PackedByteArray = PackedByteArray()
var _file_info: Dictionary = {}
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
var color_picker_button: ColorPickerButton = $ColorPickerButton
var picker: ColorPicker = color_picker_button.get_picker()
@@ -60,7 +61,7 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
# Define which child should be active for each input type
var active_child_map = {
"checkbox": "CheckBox",
"radio": "RadioButton",
"radio": "CheckBox",
"color": "ColorPickerButton",
"password": "LineEdit",
"date": "DateButton",
@@ -72,6 +73,9 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
var active_child_name = active_child_map.get(input_type, "LineEdit")
remove_unused_children(active_child_name)
if not has_node(active_child_name):
return
var active_child = get_node(active_child_name)
active_child.visible = true
@@ -134,6 +138,18 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
active_child.set("disabled", true)
if element.has_attribute("readonly") and active_child.has_method("set_editable"):
active_child.set_editable(false)
# Enable focus mode for text inputs to support change events on focus lost
if active_child is LineEdit:
active_child.focus_mode = Control.FOCUS_ALL
if input_type == "file":
var file_dialog = active_child.get_node("FileDialog")
parser.register_dom_node(element, file_dialog)
elif input_type == "date":
parser.register_dom_node(element, active_child)
else:
parser.register_dom_node(element, active_child)
func remove_unused_children(keep_child_name: String) -> void:
for child in get_children():
@@ -289,16 +305,72 @@ func _on_file_selected(path: String) -> void:
var file_name = path.get_file()
file_label.text = file_name
var file = FileAccess.open(path, FileAccess.READ)
_process_file_data(path)
func _process_file_data(file_path: String) -> void:
var file_name = file_path.get_file()
var file_extension = file_path.get_extension().to_lower()
var file_size = 0
var mime_type = _get_mime_type(file_extension)
var is_image = _is_image_file(file_extension)
var is_text = _is_text_file(file_extension)
# Read file contents
var file = FileAccess.open(file_path, FileAccess.READ)
if file:
_file_text_content = file.get_as_text()
file.close()
file = FileAccess.open(path, FileAccess.READ)
file_size = file.get_length()
_file_binary_content = file.get_buffer(file.get_length())
file.close()
# TODO: when adding Lua, make these actually usable
_file_info = {
"fileName": file_name,
"size": file_size,
"type": mime_type,
"binary": _file_binary_content,
"isImage": is_image,
"isText": is_text
}
# Add text content only for text files
if is_text:
_file_text_content = _file_binary_content.get_string_from_utf8()
_file_info["text"] = _file_text_content
# Add base64 data URL for images
if is_image:
var base64_data = Marshalls.raw_to_base64(_file_binary_content)
_file_info["dataURL"] = "data:" + mime_type + ";base64," + base64_data
func get_file_info() -> Dictionary:
return _file_info
func _get_mime_type(extension: String) -> String:
match extension:
"png": return "image/png"
"jpg", "jpeg": return "image/jpeg"
"gif": return "image/gif"
"webp": return "image/webp"
"svg": return "image/svg+xml"
"bmp": return "image/bmp"
"txt": return "text/plain"
"html", "htm": return "text/html"
"css": return "text/css"
"js": return "application/javascript"
"json": return "application/json"
"pdf": return "application/pdf"
"mp3": return "audio/mpeg"
"wav": return "audio/wav"
"ogg": return "audio/ogg"
"mp4": return "video/mp4"
"avi": return "video/x-msvideo"
"mov": return "video/quicktime"
_: return "application/octet-stream"
func _is_image_file(extension: String) -> bool:
return extension in ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"]
func _is_text_file(extension: String) -> bool:
return extension in ["txt", "html", "htm", "css", "js", "json", "xml", "csv", "md", "gd"]
func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
if not element or not parser:

View File

@@ -1,6 +1,6 @@
extends Control
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
# This is mainly for cases where <option> appears outside of <select>
var label = RichTextLabel.new()
label.bbcode_enabled = true

View File

@@ -1,7 +1,7 @@
class_name HTMLP
extends RichTextLabel
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser)
# Allow mouse events to pass through to parent containers for hover effects while keeping text selection

View File

@@ -2,7 +2,7 @@ extends Control
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
func init(element: HTMLParser.HTMLElement, _parser: HTMLParser = null) -> void:
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
var option_button: OptionButton = $OptionButton
var selected_index = -1
@@ -14,7 +14,9 @@ func init(element: HTMLParser.HTMLElement, _parser: HTMLParser = null) -> void:
var option_text = child_element.text_content.strip_edges()
var option_value = child_element.get_attribute("value")
option_value = option_text
# If no value attribute is specified, use the text content as the value
if option_value.is_empty():
option_value = option_text
option_button.add_item(option_text, option_index)
option_button.set_item_metadata(option_index, option_value)
@@ -33,3 +35,5 @@ func init(element: HTMLParser.HTMLElement, _parser: HTMLParser = null) -> void:
option_button.selected = selected_index
custom_minimum_size = option_button.size
parser.register_dom_node(element, option_button)

View File

@@ -2,7 +2,7 @@ extends Control
var separator_node: Separator
func init(element: HTMLParser.HTMLElement, _parser: HTMLParser = null) -> void:
func init(element: HTMLParser.HTMLElement, _parser: HTMLParser) -> void:
var direction = element.get_attribute("direction")
if direction == "vertical":

View File

@@ -4,7 +4,7 @@ extends RichTextLabel
@onready var rich_text_label: RichTextLabel = self
@onready var background_rect: ColorRect = $BackgroundRect
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser)
func _ready():

View File

@@ -2,7 +2,7 @@ extends Control
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
func init(element: HTMLParser.HTMLElement, _parser: HTMLParser = null) -> void:
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
var text_edit: TextEdit = $TextEdit
var placeholder = element.get_attribute("placeholder")
@@ -10,8 +10,6 @@ func init(element: HTMLParser.HTMLElement, _parser: HTMLParser = null) -> void:
var rows = element.get_attribute("rows")
var cols = element.get_attribute("cols")
var maxlength = element.get_attribute("maxlength")
var readonly = element.get_attribute("readonly")
var disabled = element.get_attribute("disabled")
# Set placeholder text
text_edit.placeholder_text = placeholder
@@ -70,6 +68,11 @@ func init(element: HTMLParser.HTMLElement, _parser: HTMLParser = null) -> void:
if text_edit.text_changed.is_connected(_on_text_changed):
text_edit.text_changed.disconnect(_on_text_changed)
text_edit.text_changed.connect(_on_text_changed.bind(max_len))
# Enable focus mode for textarea to support change events on focus lost
text_edit.focus_mode = Control.FOCUS_ALL
parser.register_dom_node(element, text_edit)
func _on_text_changed(max_length: int) -> void:
var text_edit = $TextEdit as TextEdit

View File

@@ -361,7 +361,7 @@ static func handle_visual_replacement(old_child_element_id: String, new_child_el
if old_position >= 0:
var parent_dom_node: Node = null
if parent_element_id == "body":
var main_scene = lua_api.get_node("/root/Main")
var main_scene = lua_api.get_main_scene()
if main_scene:
parent_dom_node = main_scene.website_container
else:

View File

@@ -1,8 +1,15 @@
class_name LuaEventUtils
extends RefCounted
static func is_date_button(node: Node) -> bool:
if node is DateButton:
return true
return node.has_method("init_with_date") and node.has_method("parse_date_string")
static func connect_element_event(signal_node: Node, event_name: String, subscription) -> bool:
if not signal_node:
print("ERROR: Signal node is null for event: ", event_name)
return false
match event_name:
@@ -70,6 +77,95 @@ static func connect_element_event(signal_node: Node, event_name: String, subscri
subscription.connected_signal = "focus_exited"
subscription.connected_node = signal_node
return true
"change":
# Check for DateButton first before generic button signals
if is_date_button(signal_node):
if signal_node.has_signal("date_selected"):
signal_node.date_selected.connect(subscription.lua_api._on_date_selected_text.bind(subscription))
subscription.connected_signal = "date_selected"
subscription.connected_node = signal_node
return true
else:
# Try to initialize if it has the init method
if signal_node.has_method("init"):
signal_node.init()
if signal_node.has_signal("date_selected"):
signal_node.date_selected.connect(subscription.lua_api._on_date_selected_text.bind(subscription))
subscription.connected_signal = "date_selected"
subscription.connected_node = signal_node
return true
return false
elif signal_node.has_signal("item_selected"):
signal_node.item_selected.connect(subscription.lua_api._on_input_item_selected.bind(subscription))
subscription.connected_signal = "item_selected"
subscription.connected_node = signal_node
return true
elif signal_node.has_signal("focus_exited") and (signal_node is LineEdit or signal_node is TextEdit):
# For text inputs and textareas, change event fires only on focus lost
signal_node.focus_exited.connect(subscription.lua_api._on_input_focus_lost.bind(subscription))
subscription.connected_signal = "focus_exited_change"
subscription.connected_node = signal_node
return true
elif signal_node.has_signal("value_changed"):
signal_node.value_changed.connect(subscription.lua_api._on_input_value_changed.bind(subscription))
subscription.connected_signal = "value_changed"
subscription.connected_node = signal_node
return true
elif signal_node.has_signal("color_changed"):
signal_node.color_changed.connect(subscription.lua_api._on_input_color_changed.bind(subscription))
subscription.connected_signal = "color_changed"
subscription.connected_node = signal_node
return true
elif signal_node.has_signal("toggled"):
signal_node.toggled.connect(subscription.lua_api._on_input_toggled.bind(subscription))
subscription.connected_signal = "toggled"
subscription.connected_node = signal_node
return true
elif signal_node.has_signal("file_selected"):
signal_node.file_selected.connect(subscription.lua_api._on_file_selected.bind(subscription))
subscription.connected_signal = "file_selected"
subscription.connected_node = signal_node
return true
elif signal_node.has_signal("pressed"):
signal_node.pressed.connect(subscription.lua_api._on_event_triggered.bind(subscription))
subscription.connected_signal = "pressed"
subscription.connected_node = signal_node
return true
else:
return false
"input":
# Input event fires on every keystroke for text inputs and textareas
if signal_node.has_signal("text_changed"):
# Handle different signal signatures: LineEdit passes text, TextEdit doesn't
var callback: Callable
if signal_node is LineEdit:
# LineEdit passes the new text as argument
callback = func(text: String): subscription.lua_api._on_input_text_changed(text, subscription)
else:
# TextEdit doesn't pass arguments, get text manually
callback = func(): subscription.lua_api._on_input_text_changed(signal_node.text, subscription)
signal_node.text_changed.connect(callback)
subscription.connected_signal = "text_changed"
subscription.connected_node = signal_node
subscription.callback_func = callback # Store for later disconnect
return true
else:
print("ERROR: No text_changed signal found for input event on ", signal_node.get_class())
return false
"submit":
# For form elements - look for a submit button or form container
if signal_node.has_signal("pressed"):
signal_node.pressed.connect(subscription.lua_api._on_form_submit.bind(subscription))
subscription.connected_signal = "form_submit"
subscription.connected_node = signal_node
return true
elif signal_node.has_signal("text_submitted"):
# LineEdit Enter key support
signal_node.text_submitted.connect(subscription.lua_api._on_text_submit.bind(subscription))
subscription.connected_signal = "text_submitted"
subscription.connected_node = signal_node
return true
return false
@@ -151,6 +247,41 @@ static func disconnect_subscription(subscription, lua_api) -> void:
"focus_exited":
if target_node.has_signal("focus_exited"):
target_node.focus_exited.disconnect(lua_api._on_event_triggered.bind(subscription))
"text_changed":
if target_node.has_signal("text_changed"):
# Use the stored callback function for proper disconnect
if subscription.callback_func:
target_node.text_changed.disconnect(subscription.callback_func)
else:
# Fallback for old connections
target_node.text_changed.disconnect(lua_api._on_input_text_changed.bind(subscription))
"focus_exited_change":
if target_node.has_signal("focus_exited"):
target_node.focus_exited.disconnect(lua_api._on_input_focus_lost.bind(subscription))
"value_changed":
if target_node.has_signal("value_changed"):
target_node.value_changed.disconnect(lua_api._on_input_value_changed.bind(subscription))
"color_changed":
if target_node.has_signal("color_changed"):
target_node.color_changed.disconnect(lua_api._on_input_color_changed.bind(subscription))
"toggled":
if target_node.has_signal("toggled"):
target_node.toggled.disconnect(lua_api._on_input_toggled.bind(subscription))
"item_selected":
if target_node.has_signal("item_selected"):
target_node.item_selected.disconnect(lua_api._on_input_item_selected.bind(subscription))
"file_selected":
if target_node.has_signal("file_selected"):
target_node.file_selected.disconnect(lua_api._on_file_selected.bind(subscription))
"date_selected":
if target_node.has_signal("date_selected"):
target_node.date_selected.disconnect(lua_api._on_date_selected_text.bind(subscription))
"form_submit":
if target_node.has_signal("pressed"):
target_node.pressed.disconnect(lua_api._on_form_submit.bind(subscription))
"text_submitted":
if target_node.has_signal("text_submitted"):
target_node.text_submitted.disconnect(lua_api._on_text_submit.bind(subscription))
"input":
# Only disable input processing if no other input subscriptions remain
if _count_active_input_subscriptions(lua_api) <= 1:

View File

@@ -46,7 +46,7 @@ class LuaSignal:
vm.lua_pop(1) # Pop callbacks table
connections.clear()
func fire_signal(args: Array, signal_table_ref: int = -1) -> void:
func fire_signal(args: Array) -> void:
for connection in connections:
var vm = connection.vm as LuauVM
# Get the callback function from our custom storage
@@ -173,7 +173,7 @@ static func signal_fire_handler(vm: LuauVM) -> int:
args.append(vm.lua_tovariant(i))
# Fire the signal with the signal table reference
lua_signal.fire_signal(args, lua_signal.signal_table_ref)
lua_signal.fire_signal(args)
return 0

View File

@@ -64,7 +64,7 @@ static func time_date_handler(vm: LuauVM) -> int:
static func time_sleep_handler(vm: LuauVM) -> int:
vm.luaL_checknumber(1)
var seconds = vm.lua_tonumber(1)
var milliseconds = int(seconds * 1000)
var _milliseconds = int(seconds * 1000)
# TODO: implement a proper sleep function
@@ -81,10 +81,10 @@ static func time_benchmark_handler(vm: LuauVM) -> int:
var error_msg = vm.lua_tostring(-1)
vm.lua_pop(1)
var end_time = Time.get_ticks_msec()
var elapsed_ms = end_time - start_time
var end = Time.get_ticks_msec()
var elapsed = end - start_time
vm.lua_pushnumber(elapsed_ms / 1000.0)
vm.lua_pushnumber(elapsed / 1000.0)
vm.lua_pushstring("Error: " + error_msg)
return 2

View File

@@ -8,10 +8,8 @@ static func match_element(selector: String, element: HTMLParser.HTMLElement) ->
var rule = CSSParser.CSSRule.new()
rule.init(selector)
var class_names = HTMLParser.extract_class_names(element)
var stylesheet = CSSParser.CSSStylesheet.new()
return stylesheet.selector_matches(rule, element.tag_name, "", class_names, element)
return stylesheet.selector_matches(rule, "", element)
static func find_all_matching(selector: String, elements: Array[HTMLParser.HTMLElement]) -> Array[HTMLParser.HTMLElement]:
var matches: Array[HTMLParser.HTMLElement] = []

View File

@@ -128,7 +128,9 @@ func render() -> void:
for inline_element in inline_elements:
var inline_node = await create_element_node(inline_element, parser)
if inline_node:
parser.register_dom_node(inline_element, inline_node)
# Input elements register their own DOM nodes in their init() function
if inline_element.tag_name not in ["input", "textarea", "select", "button"]:
parser.register_dom_node(inline_element, inline_node)
safe_add_child(hbox, inline_node)
# Handle hyperlinks for all inline elements
@@ -142,7 +144,9 @@ func render() -> void:
var element_node = await create_element_node(element, parser)
if element_node:
parser.register_dom_node(element, element_node)
# Input elements register their own DOM nodes in their init() function
if element.tag_name not in ["input", "textarea", "select", "button"]:
parser.register_dom_node(element, element_node)
# ul/ol handle their own adding
if element.tag_name != "ul" and element.tag_name != "ol":
@@ -277,21 +281,23 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
if not child_element.is_inline_element() or is_flex_container:
var child_node = await create_element_node(child_element, parser)
if child_node and is_instance_valid(container_for_children):
parser.register_dom_node(child_element, child_node)
# Input elements register their own DOM nodes in their init() function
if child_element.tag_name not in ["input", "textarea", "select", "button"]:
parser.register_dom_node(child_element, child_node)
safe_add_child(container_for_children, child_node)
return final_node
func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> Control:
func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLParser) -> Control:
var node: Control = null
match element.tag_name:
"p":
node = P.instantiate()
node.init(element)
node.init(element, parser)
"pre":
node = PRE.instantiate()
node.init(element)
node.init(element, parser)
"h1", "h2", "h3", "h4", "h5", "h6":
match element.tag_name:
"h1": node = H1.instantiate()
@@ -300,16 +306,16 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
"h4": node = H4.instantiate()
"h5": node = H5.instantiate()
"h6": node = H6.instantiate()
node.init(element)
node.init(element, parser)
"br":
node = BR.instantiate()
node.init(element)
node.init(element, parser)
"img":
node = IMG.instantiate()
node.init(element)
node.init(element, parser)
"separator":
node = SEPARATOR.instantiate()
node.init(element)
node.init(element, parser)
"form":
var form_styles = parser.get_element_styles_with_inheritance(element, "", [])
var is_flex_form = form_styles.has("display") and ("flex" in form_styles["display"])
@@ -319,7 +325,7 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
return null
else:
node = FORM.instantiate()
node.init(element)
node.init(element, parser)
# Manually process children for non-flex forms
for child_element in element.children:
@@ -350,13 +356,13 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
node.init(element)
"select":
node = SELECT.instantiate()
node.init(element)
node.init(element, parser)
"option":
node = OPTION.instantiate()
node.init(element, parser)
"textarea":
node = TEXTAREA.instantiate()
node.init(element)
node.init(element, parser)
"div":
var styles = parser.get_element_styles_with_inheritance(element, "", [])
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
@@ -366,13 +372,13 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
node = BackgroundUtils.create_panel_container_with_background(styles, hover_styles)
else:
node = DIV.instantiate()
node.init(element)
node.init(element, parser)
var has_only_text = is_text_only_element(element)
if has_only_text:
var p_node = P.instantiate()
p_node.init(element)
p_node.init(element, parser)
var container_for_children = node
if node is PanelContainer and node.get_child_count() > 0:

View File

@@ -1,6 +1,8 @@
class_name DateButton
extends Button
signal date_selected(date_text: String)
var calendar: Calendar
var calendar_control: Control
@@ -54,11 +56,19 @@ func parse_date_string(date_string: String) -> Dictionary:
func update_button_text() -> void:
var date = calendar.selected
text = "%d/%d/%d" % [date.month, date.day, date.year]
if date and date.has("month") and date.has("day") and date.has("year"):
text = "%02d/%02d/%04d" % [date.month, date.day, date.year]
date_selected.emit(text)
else:
text = "mm/dd/yyyy"
func _on_date_selected():
var date = calendar.selected
text = "%02d/%02d/%04d" % [date.month, date.day, date.year]
if date and date.has("month") and date.has("day") and date.has("year"):
text = "%02d/%02d/%04d" % [date.month, date.day, date.year]
date_selected.emit(text)
else:
text = "mm/dd/yyyy"
func _on_button_pressed():
if calendar.is_visible():