dom.lua (3871B)
1 -- DOM (Document Object Model) 2 -- Handles nested elements with positioning, dimensions, and click handling 3 4 local DOM = {} 5 DOM.__index = DOM 6 7 -- Element class 8 local Element = {} 9 Element.__index = Element 10 11 function Element:new(tag, attrs) 12 attrs = attrs or {} 13 14 return setmetatable({ 15 tag = tag or "div", 16 id = attrs.id, 17 classes = attrs.class and self:parse_classes(attrs.class) or {}, 18 attributes = attrs, 19 children = {}, 20 parent = nil, 21 content = "", 22 23 -- Layout properties (computed during layout) 24 x = 0, 25 y = 0, 26 width = 0, 27 height = 0, 28 29 -- Event handlers 30 handlers = {} 31 }, self) 32 end 33 34 function Element:parse_classes(class_string) 35 local classes = {} 36 for class in class_string:gmatch("%S+") do 37 table.insert(classes, class) 38 end 39 return classes 40 end 41 42 function Element:has_class(class_name) 43 for _, cls in ipairs(self.classes) do 44 if cls == class_name then 45 return true 46 end 47 end 48 return false 49 end 50 51 function Element:add_child(child) 52 table.insert(self.children, child) 53 child.parent = self 54 return child 55 end 56 57 function Element:set_content(content) 58 self.content = content 59 end 60 61 function Element:set_layout(x, y, width, height) 62 self.x = x 63 self.y = y 64 self.width = width 65 self.height = height 66 end 67 68 function Element:contains_point(px, py) 69 return px >= self.x and px < self.x + self.width and 70 py >= self.y and py < self.y + self.height 71 end 72 73 function Element:on(event, handler) 74 self.handlers[event] = handler 75 end 76 77 function Element:trigger(event, ...) 78 local handler = self.handlers[event] 79 if handler then 80 return handler(self, ...) 81 end 82 return false 83 end 84 85 -- Walk the tree in paint order (depth-first, reverse order so last child is on top) 86 function Element:find_element_at(px, py) 87 -- Check children in reverse order (last child painted on top) 88 for i = #self.children, 1, -1 do 89 local child = self.children[i] 90 local found = child:find_element_at(px, py) 91 if found then 92 return found 93 end 94 end 95 96 -- Check if this element contains the point 97 if self:contains_point(px, py) then 98 return self 99 end 100 101 return nil 102 end 103 104 function Element:debug_print(indent) 105 indent = indent or 0 106 local spaces = string.rep(" ", indent) 107 local class_str = #self.classes > 0 and (" ." .. table.concat(self.classes, ".")) or "" 108 local id_str = self.id and ("#" .. self.id) or "" 109 print(string.format("%s<%s%s%s> [%d,%d %dx%d]", 110 spaces, self.tag, id_str, class_str, self.x, self.y, self.width, self.height)) 111 112 for _, child in ipairs(self.children) do 113 child:debug_print(indent + 1) 114 end 115 end 116 117 -- DOM tree manager 118 function DOM:new() 119 return setmetatable({ 120 root = nil 121 }, self) 122 end 123 124 function DOM:create_element(tag, attrs) 125 return Element:new(tag, attrs) 126 end 127 128 function DOM:set_root(element) 129 self.root = element 130 end 131 132 function DOM:click(x, y) 133 if not self.root then 134 return false 135 end 136 137 -- Find the topmost element at this position 138 local element = self.root:find_element_at(x, y) 139 140 if not element then 141 return false 142 end 143 144 -- Bubble up from the clicked element to root 145 local current = element 146 while current do 147 -- Trigger click event 148 local blocked = current:trigger("click", x, y) 149 if blocked then 150 -- Event was handled and blocked from propagating 151 return true 152 end 153 154 current = current.parent 155 end 156 157 return false 158 end 159 160 function DOM:debug_print() 161 if self.root then 162 print("=== DOM Tree ===") 163 self.root:debug_print() 164 else 165 print("=== DOM Tree (empty) ===") 166 end 167 end 168 169 return { 170 DOM = DOM, 171 Element = Element 172 }