Class: Dommy::Window
- Inherits:
-
Object
- Object
- Dommy::Window
- Includes:
- Bridge::Methods, EventTarget
- Defined in:
- lib/dommy/window.rb
Overview
The browser global. JS.global from inside wasm resolves to this.
Property access (JS.global[:document], JS.global[:console]) is
routed through #__js_get__. Method calls (JS.global.call(:foo))
are routed through #__js_call__.
Constant Summary collapse
- WINDOW_EVENT_HANDLER_NAMES =
Event handler IDL attributes the Window exposes (GlobalEventHandlers + WindowEventHandlers). Setting one (
window.onload = fn) registers a listener; only these known names are intercepted so an arbitrary on-prefixed global (window.onboarding = {...}) still stays a plain expando rather than being mistaken for an event handler. %w[ onabort onauxclick onbeforeinput onbeforematch onbeforetoggle onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextlost oncontextmenu oncontextrestored oncopy oncuechange oncut ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended onerror onfocus onformdata oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart onmousedown onmouseenter onmouseleave onmousemove onmouseout onmouseover onmouseup onpaste onpause onplay onplaying onprogress onratechange onreset onresize onscroll onscrollend onsecuritypolicyviolation onseeked onseeking onselect onslotchange onstalled onsubmit onsuspend ontimeupdate ontoggle onvolumechange onwaiting onwheel onafterprint onbeforeprint onbeforeunload onhashchange onlanguagechange onmessage onmessageerror onoffline ononline onpagehide onpageshow onpopstate onrejectionhandled onstorage onunhandledrejection onunload ].to_set.freeze
Instance Attribute Summary collapse
-
#approximate_layout ⇒ Object
Opt into best-effort geometry: when true, getBoundingClientRect / client* / offset* return non-zero estimates from a cheap pseudo-layout (viewport width + text content) instead of all-zero.
-
#custom_elements ⇒ Object
readonly
Returns the value of attribute custom_elements.
-
#document ⇒ Object
readonly
Returns the value of attribute document.
-
#frame_element ⇒ Object
The
<iframe>/frame element hosting this window's browsing context (nil for a top-level window). -
#globals ⇒ Object
readonly
Returns the value of attribute globals.
-
#history ⇒ Object
readonly
Returns the value of attribute history.
-
#location ⇒ Object
readonly
Returns the value of attribute location.
-
#navigation_delegate ⇒ Object
Navigation host seam (see Dommy::Navigation).
-
#navigator ⇒ Object
readonly
Returns the value of attribute navigator.
-
#scheduler ⇒ Object
readonly
Returns the value of attribute scheduler.
-
#websocket_connector ⇒ Object
Optional WebSocket transport factory (a host seam, like the document's external_script_runner):
->(ws, url, protocols) -> transport | nil.
Instance Method Summary collapse
- #__internal_event_parent__ ⇒ Object
- #__internal_media_environment_changed__ ⇒ Object
-
#__internal_navigate__(url:, source:, method: "GET", body: nil, params: nil, enctype: nil, headers: {}, replace: false) ⇒ Object
Single firing point for cross-document navigation intents.
- #__internal_register_media_query_list__(mql) ⇒ Object
-
#__internal_resolve_url__(url) ⇒ Object
Resolve a (possibly relative) URL against the document base URL — the API base URL of this window's environment, as fetch/XHR use when constructing a request.
-
#__internal_url_path__(url) ⇒ Object
The path (with query) of a URL — lets a stub keyed by a path ("/api") match its resolved absolute form ("http://host/api").
- #__js_call__(method, args) ⇒ Object
-
#__js_get__(key) ⇒ Object
Bridge protocol: respond to a JS-style property read by name.
- #__js_set__(key, value) ⇒ Object
- #fire_hashchange(old_url, new_url) ⇒ Object
-
#fire_popstate(state) ⇒ Object
Called by History#go and Location.href= to fire popstate / hashchange events.
-
#frame_windows ⇒ Object
The child browsing contexts' windows, in document order — one per
<iframe>(nil for a frame whose content document isn't wired). -
#get_computed_style(element, pseudo_element = nil) ⇒ Object
CSSOM getComputedStyle.
-
#initialize(host = nil, backend_doc: nil) ⇒ Window
constructor
A new instance of Window.
- #inner_height ⇒ Object
- #inner_width ⇒ Object
-
#media_environment ⇒ Object
The media-feature environment matchMedia and @media evaluate against (viewport 1280x720, light scheme, dpr 1 by default).
-
#resize_to(width, height) ⇒ Object
Resize the virtual viewport: updates the environment, invalidates computed styles (@media), re-evaluates handed-out MediaQueryLists (firing their
changeevents), and fires the windowresizeevent.
Methods included from Bridge::Methods
Methods included from EventTarget
#__dommy_dump_event_failure__, #__internal_deliver_event__, #__internal_process_event_handler_return__, #add_event_listener, capture_flag, #deliver_at, #dispatch_event, #event_name_from_on, #invoke_listener_isolated, js_truthy?, #on_handler, #remove_event_listener, #set_on_handler
Constructor Details
#initialize(host = nil, backend_doc: nil) ⇒ Window
Returns a new instance of Window.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/dommy/window.rb', line 82 def initialize(host = nil, backend_doc: nil) @host = host @navigation_delegate = Navigation::NullDelegate.new @scheduler = Scheduler.new @crypto = Crypto.new(self) @css_namespace = CSSNamespace.new @cookie_store = CookieStore.new(self) @local_storage = Storage.new @session_storage = Storage.new @location = Location.new(self) @history = History.new(self, @location) # `JS.global[:__some_key__] = ...` from user code lands here. Test code # uses this for stub installation (e.g. a custom `__fetch_stub__`); # production code stays on the typed accessors. Kept last in the read # fallback so it can't shadow intentional getters. @globals = {} @document = Document.new(host, backend_doc: backend_doc) @document.default_view = self # Per the HTML parsing algorithm, a <template>'s contents are parsed into a # separate "template contents" DocumentFragment, not as children of the # element. Backends (libxml2) leave them as direct children, so migrate # eagerly at page-load time — before any framework walks the tree. Without # this, a tree-walk (Alpine's x-for/x-if scan, etc.) descends into the # template's inert content and evaluates directives there out of scope. @document.migrate_template_descendants(@document.backend_doc) @custom_elements = CustomElementRegistry.new(self) @navigator = Navigator.new(self) # All JS global constructors (`new Event()`, `new URL()`, ...) live in a # single name→Constructor registry rather than one ivar + one __js_get__ # arm each. @constructors = Bridge::ConstructorRegistry.new(build_constructors) end |
Instance Attribute Details
#approximate_layout ⇒ Object
Opt into best-effort geometry: when true, getBoundingClientRect / client* / offset* return non-zero estimates from a cheap pseudo-layout (viewport width
- text content) instead of all-zero. Off by default so the no-layout contract (and the tests asserting 0) is unchanged; a browser front end (dommynx) turns it on so sites that bail on all-zero rects can proceed.
52 53 54 |
# File 'lib/dommy/window.rb', line 52 def approximate_layout @approximate_layout end |
#custom_elements ⇒ Object (readonly)
Returns the value of attribute custom_elements.
45 46 47 |
# File 'lib/dommy/window.rb', line 45 def custom_elements @custom_elements end |
#document ⇒ Object (readonly)
Returns the value of attribute document.
45 46 47 |
# File 'lib/dommy/window.rb', line 45 def document @document end |
#frame_element ⇒ Object
The <iframe>/frame element hosting this window's browsing context (nil for
a top-level window). Lets rendering-dependent code (getComputedStyle) tell
whether this document is inside a non-rendered frame.
56 57 58 |
# File 'lib/dommy/window.rb', line 56 def frame_element @frame_element end |
#globals ⇒ Object (readonly)
Returns the value of attribute globals.
45 46 47 |
# File 'lib/dommy/window.rb', line 45 def globals @globals end |
#history ⇒ Object (readonly)
Returns the value of attribute history.
45 46 47 |
# File 'lib/dommy/window.rb', line 45 def history @history end |
#location ⇒ Object (readonly)
Returns the value of attribute location.
45 46 47 |
# File 'lib/dommy/window.rb', line 45 def location @location end |
#navigation_delegate ⇒ Object
Navigation host seam (see Dommy::Navigation). Cross-document navigation intents (link activation, location.assign/replace/reload, history traversal across a document boundary) are routed to this delegate. The default NullDelegate records attempts without navigating, so behaviour is unchanged until an embedder installs a real delegate.
80 81 82 |
# File 'lib/dommy/window.rb', line 80 def @navigation_delegate end |
#navigator ⇒ Object (readonly)
Returns the value of attribute navigator.
45 46 47 |
# File 'lib/dommy/window.rb', line 45 def navigator @navigator end |
#scheduler ⇒ Object (readonly)
Returns the value of attribute scheduler.
45 46 47 |
# File 'lib/dommy/window.rb', line 45 def scheduler @scheduler end |
#websocket_connector ⇒ Object
Optional WebSocket transport factory (a host seam, like the document's
external_script_runner): ->(ws, url, protocols) -> transport | nil.
A returned transport owns the connection — WebSocket#send / #close
delegate to it, and it reports lifecycle back through the
transport*_ callbacks (on the page thread). nil falls back to the
in-memory stub (auto-open + test_simulate*_ seams).
73 74 75 |
# File 'lib/dommy/window.rb', line 73 def websocket_connector @websocket_connector end |
Instance Method Details
#__internal_event_parent__ ⇒ Object
306 307 308 |
# File 'lib/dommy/window.rb', line 306 def __internal_event_parent__ nil end |
#__internal_media_environment_changed__ ⇒ Object
382 383 384 385 386 |
# File 'lib/dommy/window.rb', line 382 def __internal_media_environment_changed__ @document&.__internal_bump_style_generation__ (@media_query_lists || []).each(&:__internal_environment_changed__) nil end |
#__internal_navigate__(url:, source:, method: "GET", body: nil, params: nil, enctype: nil, headers: {}, replace: false) ⇒ Object
Single firing point for cross-document navigation intents. Link activation, location.assign/replace/reload and cross-boundary history traversal all route here; the attached delegate (NullDelegate by default) decides what happens. Same-document navigation never reaches this — it is handled directly by Location/History (hashchange / popstate).
330 331 332 333 334 335 |
# File 'lib/dommy/window.rb', line 330 def __internal_navigate__(url:, source:, method: "GET", body: nil, params: nil, enctype: nil, headers: {}, replace: false) @navigation_delegate&.navigate( url: url, method: method, body: body, params: params, enctype: enctype, headers: headers, replace: replace, source: source ) end |
#__internal_register_media_query_list__(mql) ⇒ Object
388 389 390 391 |
# File 'lib/dommy/window.rb', line 388 def __internal_register_media_query_list__(mql) (@media_query_lists ||= []) << mql nil end |
#__internal_resolve_url__(url) ⇒ Object
Resolve a (possibly relative) URL against the document base URL — the API base URL of this window's environment, as fetch/XHR use when constructing a request. Returns the input unchanged if it can't resolve.
353 354 355 356 357 358 359 360 |
# File 'lib/dommy/window.rb', line 353 def __internal_resolve_url__(url) base = @document&.base_uri return url.to_s if base.to_s.empty? URI.join(base.to_s, url.to_s).to_s rescue URI::Error url.to_s end |
#__internal_url_path__(url) ⇒ Object
The path (with query) of a URL — lets a stub keyed by a path ("/api") match its resolved absolute form ("http://host/api").
364 365 366 367 368 369 |
# File 'lib/dommy/window.rb', line 364 def __internal_url_path__(url) uri = URI.parse(url.to_s) uri.query ? "#{uri.path}?#{uri.query}" : uri.path rescue URI::Error url.to_s end |
#__js_call__(method, args) ⇒ Object
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 |
# File 'lib/dommy/window.rb', line 237 def __js_call__(method, args) case method when "fetch" FetchFn.new(self).__js_call__("call", args) when "encodeURIComponent" Internal::GlobalFunctions.encode_uri_component(args[0]) when "decodeURIComponent" Internal::GlobalFunctions.decode_uri_component(args[0]) when "btoa" Internal::GlobalFunctions.btoa(args[0]) when "atob" Internal::GlobalFunctions.atob(args[0]) when "addEventListener" add_event_listener(args[0], args[1], args[2]) when "removeEventListener" remove_event_listener(args[0], args[1], args[2]) when "dispatchEvent" dispatch_event(args[0]) when "setTimeout" @scheduler.set_timeout(args[0], timer_delay(args[1])) when "clearTimeout" @scheduler.clear_timeout(args[0]) when "setInterval" @scheduler.set_interval(args[0], timer_delay(args[1])) when "clearInterval" @scheduler.clear_interval(args[0]) when "requestAnimationFrame" @scheduler.request_animation_frame(args[0]) when "cancelAnimationFrame" @scheduler.cancel_animation_frame(args[0]) when "queueMicrotask" @scheduler.queue_microtask(args[0]) when "requestIdleCallback" @scheduler.request_idle_callback(args[0], (args[1].is_a?(Hash) && args[1]["timeout"]) || 0) when "cancelIdleCallback" @scheduler.cancel_idle_callback(args[0]) when "structuredClone" Dommy.structured_clone(args[0]) when "matchMedia" MediaQueryList.new(self, args[0].to_s) when "getComputedStyle" get_computed_style(args[0], args[1]) when "resizeTo" resize_to(args[0], args[1]) when "scroll", "scrollTo" scroll_to(*args) when "scrollBy" scroll_by(*args) when "alert" nil # headless: no dialog (happy-dom semantics) when "confirm" false # no user -> treated as "Cancel" when "prompt" nil # no user input when "open" nil # cannot open a new browsing context headlessly when "reportError" nil # swallow programmatic error reports (no uncaught surfacing here) when "getSelection" document&.get_selection when "postMessage" (args[0]) else # Additional window-level methods (fetch, location, history, # Promise, MutationObserver, etc.) arrive in later sessions. nil end end |
#__js_get__(key) ⇒ Object
Bridge protocol: respond to a JS-style property read by name. Returns either a Ruby primitive (Integer / String / true / false / nil), a Hash/Array (for JS object/array literals), or a Dom::* instance for live DOM/BOM objects.
Anything outside the surface we've explicitly polyfilled returns nil (= JS undefined). Spec failures here are the signal to widen the surface in a future session.
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/dommy/window.rb', line 123 def __js_get__(key) ctor = @constructors[key] return ctor if ctor case key when "document" @document when "window", "self", "parent", "top", "frames" # A top-level browsing context refers to itself for these. Returning the # window (not nil) lets `window === window.parent` and frame-walking # loops (e.g. testharness.js's `while (w != w.parent)`) terminate. self when "crypto" @crypto when "cookieStore" @cookie_store when "console" :console when "Object" :object_ctor when "Array" :array_ctor when "JSON" :json_ctor when "performance" @performance ||= Performance.new(self) when "localStorage" @local_storage when "sessionStorage" @session_storage when "location" @location when "history" @history when "CSS" @css_namespace when "fetch" FetchFn.new(self) when "customElements" @custom_elements when "navigator" @navigator when "screen" @screen ||= Screen.new(self) when "innerWidth", "outerWidth" media_environment. when "innerHeight", "outerHeight" media_environment. when "devicePixelRatio" media_environment.device_pixel_ratio when "scrollX", "pageXOffset" @scroll_x || 0 when "scrollY", "pageYOffset" @scroll_y || 0 when "scrollMaxX", "scrollMaxY" # No real content box to scroll past, so the max offset is 0. 0 when "event" # The legacy global current-event is *absent* (undefined, not null) when # no event is being dispatched, so feature detection like # `window.event === undefined` (React's getCurrentEventPriority) takes the # not-supported path instead of dereferencing null. An explicitly-set # value still wins. @globals.key?("event") ? @globals["event"] : Bridge::UNDEFINED when /\A\d+\z/ # `window[i]` / `window.frames[i]` — the i-th child browsing context's # window (the i-th `<iframe>`'s contentWindow), or ABSENT past the end. frame = frame_windows[key.to_i] frame.nil? ? Bridge::ABSENT : frame when ->(k) { k.is_a?(String) && WINDOW_EVENT_HANDLER_NAMES.include?(k) } # An event handler IDL attribute: the registered handler, or null (not # undefined) when unset — matching the spec and Element's on* getter. on_handler(event_name_from_on(key)) else # A stashed global wins (even if its value is nil/null); a key never set # is genuinely absent → ABSENT so JS sees `undefined` and `"x" in window` # is false (feature detection like `isUndefined(window.Vue)` works). @globals.key?(key) ? @globals[key] : Bridge::ABSENT end end |
#__js_set__(key, value) ⇒ Object
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/dommy/window.rb', line 204 def __js_set__(key, value) # `window.location = url` forwards to the Location object (WebIDL # [PutForwards=href]): equivalent to `location.href = url`, i.e. navigate. if key == "location" && @location @location.__js_set__("href", value.to_s) return nil end # `window.onload = fn` (and the other window event handlers) registers a # listener rather than stashing an expando, so the handler actually fires. if key.is_a?(String) && WINDOW_EVENT_HANDLER_NAMES.include?(key) set_on_handler(event_name_from_on(key), value) return nil end # Stash arbitrary keys for later reads (e.g. # `JS.global[:__fetchy_stub__] = map`). @globals[key] = value # The Fetchy spec's `install_fetch_stub` resets `__fetch_count__` # to 0 inside its JS installer (`globalThis.__fetch_count__ = 0; # globalThis.fetch = ...`). Our polyfill ignores raw JS, so we # piggy-back on the stub assignment to perform the same reset # — without it the count accumulates across tests in one VM run. @globals["__fetch_count__"] = 0 if %w[__fetchy_stub__ __resource_fetch_stub__ __inject_fetch_stub__].include?(key) nil end |
#fire_hashchange(old_url, new_url) ⇒ Object
320 321 322 323 |
# File 'lib/dommy/window.rb', line 320 def fire_hashchange(old_url, new_url) event = HashChangeEvent.new("hashchange", "oldURL" => old_url.to_s, "newURL" => new_url.to_s) dispatch_event(event) end |
#fire_popstate(state) ⇒ Object
Called by History#go and Location.href= to fire popstate /
hashchange events. Listeners registered on the Window via
addEventListener("popstate"|"hashchange", cb) receive them.
313 314 315 316 317 318 |
# File 'lib/dommy/window.rb', line 313 def fire_popstate(state) # PopStateEvent exposes the entry's state as `event.state` (the spec # property). Routers (Turbo) branch on `event.state`. event = PopStateEvent.new("popstate", "state" => state) dispatch_event(event) end |
#frame_windows ⇒ Object
The child browsing contexts' windows, in document order — one per <iframe>
(nil for a frame whose content document isn't wired). Backs window[i] /
window.frames[i].
61 62 63 64 65 |
# File 'lib/dommy/window.rb', line 61 def frame_windows @document.query_selector_all("iframe").map do |frame| frame.respond_to?(:content_window) ? frame.content_window : nil end end |
#get_computed_style(element, pseudo_element = nil) ⇒ Object
CSSOM getComputedStyle. With the makiri-backed CSS parser available this resolves the full cascade (UA sheet +