Class: Dommy::Js::ScriptBooter

Inherits:
Object
  • Object
show all
Defined in:
lib/dommy/js/script_boot.rb

Overview

One document's script-boot run. Instantiated per boot by ScriptBoot; the collaborators are ivars so the per-script steps take only what varies.

Constant Summary collapse

HANDLER_ATTRIBUTES =

Compile each element's event-handler content attributes (onclick="…", oninput="…") into an event handler, reusing the working el.onclick = fn IDL path. Runs once at boot, after parsing and before scripts (matching the spec, where content attributes are set as the document is parsed), so a listener is live for post-load interaction. new Function parses the code as a function body with an event parameter; a syntactically bad handler is skipped, not fatal.

Two subtleties: (1) a handler on / for a window-reflected event (onload, onunload, …) belongs on the WINDOW, not the element — so <body onload> fires; it is wired via window.addEventListener since the element's own load never fires. (2) The scan targets only elements carrying a known handler attribute (via a selector) rather than every element, then wires all on* attributes on each. Handlers on elements inserted after boot (innerHTML / setAttribute) are not wired — frameworks use addEventListener; this covers server-rendered inline handlers.

%w[
  onabort onauxclick onbeforeinput onbeforetoggle onblur oncancel oncanplay oncanplaythrough
  onchange onclick onclose oncontextmenu 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 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
].freeze
WINDOW_REFLECTED_HANDLERS =

Body/frameset handlers for these events reflect onto the Window.

%w[
  onafterprint onbeforeprint onbeforeunload onhashchange onlanguagechange onmessage onmessageerror
  onoffline ononline onpagehide onpageshow onpopstate onrejectionhandled onstorage
  onunhandledrejection onunload onload onresize onscroll onerror onblur onfocus
].freeze
WIRE_INLINE_HANDLERS_JS =
<<~JS
  (() => {
    const HANDLERS = new Set(#{HANDLER_ATTRIBUTES.to_json});
    const REFLECTED = new Set(#{WINDOW_REFLECTED_HANDLERS.to_json});
    const selector = [...HANDLERS].map((name) => `[${name}]`).join(",");
    const body = document.body;
    // An inline handler runs with a scope chain of [element, form owner,
    // document] inside the global, per the HTML "compile" algorithm — so
    // `onclick="getElementById(...)"` (document) or a form-associated
    // control's bare member resolve. Reflected (body/window) handlers keep
    // the plain global scope (their `this` is the window, not the element).
    const scoped = (el, code) => {
      let src = `with(this){\n${code}\n}`;
      if (el.form) src = `with(this.form){\n${src}\n}`;
      return `with(document){\n${src}\n}`;
    };
    for (const el of document.querySelectorAll(selector)) {
      const onBody = el === body || el.tagName === "FRAMESET";
      for (const name of el.getAttributeNames()) {
        if (!HANDLERS.has(name)) continue;
        const code = el.getAttribute(name);
        try {
          if (onBody && REFLECTED.has(name)) {
            window.addEventListener(name.slice(2), new Function("event", code));
          } else if (typeof el[name] !== "function") {
            let fn;
            try { fn = new Function("event", scoped(el, code)); }
            catch { fn = new Function("event", code); } // fall back to plain scope
            el[name] = fn;
          }
        } catch {
          // A syntactically invalid handler is skipped, not fatal.
        }
      }
    }
  })();
JS

Instance Method Summary collapse

Constructor Details

#initialize(runtime, document, resources: nil, on_error: nil, on_script: nil) ⇒ ScriptBooter

Returns a new instance of ScriptBooter.



46
47
48
49
50
51
52
53
# File 'lib/dommy/js/script_boot.rb', line 46

def initialize(runtime, document, resources: nil, on_error: nil, on_script: nil)
  @runtime = runtime
  @document = document
  @resources = resources
  @on_error = on_error
  @on_script = on_script
  @loader = nil
end

Instance Method Details

#runObject



128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/dommy/js/script_boot.rb', line 128

def run
  @runtime.set_document_ready_state("loading")
  @loader = install_module_loader
  wire_inline_event_handlers
  scripts = @document.scripts.to_a
  # Pass 1: parser-blocking classic scripts, in document order.
  scripts.each { |element| run_one(element) unless deferred?(element) }
  # Pass 2: deferred scripts (modules + classic `defer`), in document order.
  scripts.each { |element| run_one(element) if deferred?(element) }
  @runtime.set_document_ready_state("interactive")
  @runtime.set_document_ready_state("complete")
end

#run_inserted_external(element, src) ⇒ Object

Fetch + run a dynamically-inserted external script, then fire load (or error if the fetch failed / it threw) so a loader awaiting the script element's onload resolves. The src was already taken from the element by the mutation coordinator, so this does not re-consume pending state.



151
152
153
154
155
156
157
158
159
160
161
# File 'lib/dommy/js/script_boot.rb', line 151

def run_inserted_external(element, src)
  ran = false
  if @resources && (url = resolve_url(src)) && (response = @resources.get(url)) && response.success?
    with_current_script(element) { @runtime.load_script_cached(response.body, cache_key: url) }
    ran = true
  end
  dispatch_script_event(element, ran ? "load" : "error")
rescue StandardError => e
  @on_error&.call(e)
  dispatch_script_event(element, "error")
end

#wire_inline_event_handlersObject



141
142
143
144
145
# File 'lib/dommy/js/script_boot.rb', line 141

def wire_inline_event_handlers
  @runtime.execute(WIRE_INLINE_HANDLERS_JS)
rescue StandardError => e
  @on_error&.call(e)
end