1use std::cell::RefCell;
5use std::collections::VecDeque;
6use std::path::Path;
7use std::rc::Rc;
8use std::sync::LazyLock;
9
10use blitz_dom::BaseDocument;
11use blitz_dom::NodeId;
12use blitz_traits::events::{BlitzPointerId, DomEvent, DomEventData, EventState};
13use boa_engine::builtins::promise::PromiseState;
14use boa_engine::job::SimpleJobExecutor;
15use boa_engine::object::builtins::JsPromise;
16use boa_engine::object::{JsObject, ObjectInitializer};
17use boa_engine::property::Attribute;
18use boa_engine::value::JsValue;
19use boa_engine::{
20 Context, JsError, JsNativeError, JsResult, JsString, Module, NativeFunction, Source, js_string,
21};
22use boa_gc::{Finalize, Trace};
23use boa_runtime::Console;
24use boa_runtime::console::{ConsoleState, DefaultLogger, Logger};
25use url::Url;
26use web_time::{Duration, Instant};
27
28use crate::dom::event::{EventRef, create_event, create_event_for_dom_event, set_event_path};
29use crate::dom::{define_accessor, dom_ctx, node_wrapper};
30use crate::job_budget::{JobBudget, JobSlice};
31use crate::module::{BlitzModuleLoader, SharedFetcher};
32use crate::state::{DomCtx, Listener};
33
34const DIAGNOSTIC_CAPACITY: usize = 1_000;
35
36#[derive(Debug, Clone)]
37#[cfg_attr(not(feature = "debug-control"), allow(dead_code))]
38pub struct DiagnosticEntry {
39 pub sequence: u64,
40 pub order: u64,
41 pub level: String,
42 pub message: String,
43 pub stack: String,
44}
45
46#[derive(Debug, Clone)]
47pub struct DiagnosticStreamBatch {
48 pub entries: Vec<DiagnosticEntry>,
49 pub lost: u64,
50 pub next_sequence: u64,
51}
52
53#[derive(Debug, Clone)]
54pub struct DiagnosticBatch {
55 pub console: DiagnosticStreamBatch,
56 pub errors: DiagnosticStreamBatch,
57}
58
59#[cfg(feature = "debug-control")]
60fn diagnostic_stream(
61 queue: &VecDeque<DiagnosticEntry>,
62 after: u64,
63 limit: usize,
64) -> DiagnosticStreamBatch {
65 let lost = queue.front().map_or(0, |entry| {
66 entry.sequence.saturating_sub(after.saturating_add(1))
67 });
68 let entries: Vec<_> = queue
69 .iter()
70 .filter(|entry| entry.sequence > after)
71 .take(limit.clamp(1, DIAGNOSTIC_CAPACITY))
72 .cloned()
73 .collect();
74 let next_sequence = entries.last().map_or(after, |entry| entry.sequence);
75 DiagnosticStreamBatch {
76 entries,
77 lost,
78 next_sequence,
79 }
80}
81
82#[derive(Debug, Default)]
83struct RuntimeDiagnostics {
84 next_order: u64,
85 next_console_sequence: u64,
86 next_error_sequence: u64,
87 console: VecDeque<DiagnosticEntry>,
88 errors: VecDeque<DiagnosticEntry>,
89}
90
91impl RuntimeDiagnostics {
92 fn push_console(&mut self, level: &str, message: String) {
93 self.next_console_sequence += 1;
94 self.next_order += 1;
95 push_bounded(
96 &mut self.console,
97 DiagnosticEntry {
98 sequence: self.next_console_sequence,
99 order: self.next_order,
100 level: level.into(),
101 message,
102 stack: String::new(),
103 },
104 );
105 }
106
107 fn push_error(&mut self, what: &str, error: &boa_engine::JsError) {
108 self.next_error_sequence += 1;
109 self.next_order += 1;
110 push_bounded(
111 &mut self.errors,
112 DiagnosticEntry {
113 sequence: self.next_error_sequence,
114 order: self.next_order,
115 level: "error".into(),
116 message: format!("Uncaught JS error in {what}: {error}"),
117 stack: format!("{error:?}"),
118 },
119 );
120 }
121}
122
123fn push_bounded(queue: &mut VecDeque<DiagnosticEntry>, entry: DiagnosticEntry) {
124 if queue.len() == DIAGNOSTIC_CAPACITY {
125 queue.pop_front();
126 }
127 queue.push_back(entry);
128}
129
130#[derive(Debug, Trace, Finalize)]
131struct CapturingLogger {
132 #[unsafe_ignore_trace]
133 diagnostics: Rc<RefCell<RuntimeDiagnostics>>,
134}
135
136impl Logger for CapturingLogger {
137 fn log(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
138 self.diagnostics
139 .borrow_mut()
140 .push_console("log", msg.clone());
141 DefaultLogger.log(msg, state, context)
142 }
143
144 fn info(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
145 self.diagnostics
146 .borrow_mut()
147 .push_console("info", msg.clone());
148 DefaultLogger.info(msg, state, context)
149 }
150
151 fn warn(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
152 self.diagnostics
153 .borrow_mut()
154 .push_console("warn", msg.clone());
155 DefaultLogger.warn(msg, state, context)
156 }
157
158 fn error(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
159 self.diagnostics
160 .borrow_mut()
161 .push_console("error", msg.clone());
162 DefaultLogger.error(msg, state, context)
163 }
164}
165
166fn report_js_error(
168 diagnostics: &Rc<RefCell<RuntimeDiagnostics>>,
169 what: &str,
170 error: &boa_engine::JsError,
171) {
172 diagnostics.borrow_mut().push_error(what, error);
173 #[cfg(feature = "tracing")]
174 tracing::error!("Uncaught JS error in {what}: {error}");
175 eprintln!("Uncaught JS error in {what}: {error}");
176}
177
178#[derive(Clone, Trace, Finalize, boa_engine::JsData)]
179struct HandlerErrorReporter {
180 #[unsafe_ignore_trace]
181 diagnostics: Rc<RefCell<RuntimeDiagnostics>>,
182}
183
184pub(crate) fn report_handler_error(context: &Context, what: &str, error: &JsError) {
185 if let Some(reporter) = context.get_data::<HandlerErrorReporter>() {
186 report_js_error(&reporter.diagnostics, what, error);
187 } else {
188 eprintln!("Uncaught JS error in {what}: {error}");
189 }
190}
191
192fn listener_error_context(
193 event_name: &str,
194 target: &str,
195 callback: &JsObject,
196 context: &mut Context,
197) -> String {
198 let source = JsValue::from(callback.clone())
199 .to_string(context)
200 .map(|source| source.to_std_string_escaped())
201 .unwrap_or_else(|_| "<source unavailable>".to_owned());
202 let source = source.chars().take(500).collect::<String>();
203 format!("{event_name} event listener on {target}: {source}")
204}
205
206pub(crate) struct ScriptRuntime {
207 pub context: Context,
208 pub ctx: DomCtx,
209 diagnostics: Rc<RefCell<RuntimeDiagnostics>>,
210 module_loader: Rc<BlitzModuleLoader>,
211 job_executor: Rc<SimpleJobExecutor>,
212 job_budget: Rc<JobBudget>,
213 pending_modules: Vec<(JsPromise, String)>,
221 focussed_text_value: Option<(NodeId, String)>,
225 domc_polls: Option<[Option<JsObject>; 2]>,
227 domc_generation: Option<u64>,
229}
230
231impl ScriptRuntime {
232 pub fn new(
233 doc: Rc<RefCell<BaseDocument>>,
234 base_url: Option<&Url>,
235 fetcher: SharedFetcher,
236 ) -> Self {
237 let module_loader = Rc::new(BlitzModuleLoader::new(fetcher, base_url.cloned()));
243 let job_executor = Rc::new(SimpleJobExecutor::new());
244 let mut context = Context::builder()
245 .module_loader(Rc::clone(&module_loader))
246 .job_executor(Rc::clone(&job_executor))
247 .build()
248 .expect("building the script context should not fail");
249 let limits = context.runtime_limits_mut();
284 limits.set_recursion_limit(16384);
285 limits.set_host_recursion_limit(4096);
286 limits.set_stack_size_limit(16384 * 32);
287
288 let ctx = DomCtx::new(doc);
289 context.insert_data(ctx.clone());
290 let diagnostics = Rc::new(RefCell::new(RuntimeDiagnostics::default()));
291 context.insert_data(HandlerErrorReporter {
292 diagnostics: Rc::clone(&diagnostics),
293 });
294
295 Console::register_with_logger(
296 CapturingLogger {
297 diagnostics: Rc::clone(&diagnostics),
298 },
299 &mut context,
300 )
301 .expect("failed to register console");
302
303 boa_runtime::register_extensions(
315 (
316 boa_runtime::extensions::UrlExtension,
317 boa_runtime::extensions::EncodingExtension,
318 ),
319 None,
320 &mut context,
321 )
322 .expect("failed to register URL and text-encoding globals");
323
324 crate::dom::init_protos(&ctx, &mut context);
325
326 let root_id = ctx.doc.borrow().root_node().id;
328 let document_wrapper = node_wrapper(&ctx, root_id, &mut context);
329 register_global(&mut context, "document", document_wrapper.into());
330
331 let global: JsValue = context.global_object().into();
333 register_global(&mut context, "window", global.clone());
334 register_global(&mut context, "self", global);
335 let global_object = context.global_object().clone();
336 define_accessor(
337 &global_object,
338 "innerWidth",
339 Some(window_inner_width),
340 None,
341 &mut context,
342 );
343 define_accessor(
344 &global_object,
345 "innerHeight",
346 Some(window_inner_height),
347 None,
348 &mut context,
349 );
350 define_accessor(
351 &global_object,
352 "outerWidth",
353 Some(window_inner_width),
354 None,
355 &mut context,
356 );
357 define_accessor(
358 &global_object,
359 "outerHeight",
360 Some(window_inner_height),
361 None,
362 &mut context,
363 );
364 define_accessor(
365 &global_object,
366 "devicePixelRatio",
367 Some(window_device_pixel_ratio),
368 None,
369 &mut context,
370 );
371
372 let ipc = ObjectInitializer::new(&mut context)
375 .function(
376 NativeFunction::from_fn_ptr(ipc_post_message),
377 js_string!("postMessage"),
378 1,
379 )
380 .build();
381 register_global(&mut context, "ipc", ipc.into());
382
383 let location = build_location(base_url, &mut context);
385 register_global(&mut context, "location", location);
386
387 let computed_style =
390 NativeFunction::from_fn_ptr(get_computed_style).to_js_function(context.realm());
391 register_global(&mut context, "getComputedStyle", computed_style.into());
392
393 let clipboard = ObjectInitializer::new(&mut context)
402 .function(
403 NativeFunction::from_fn_ptr(clipboard_write_text),
404 js_string!("writeText"),
405 1,
406 )
407 .function(
408 NativeFunction::from_fn_ptr(clipboard_read_text),
409 js_string!("readText"),
410 0,
411 )
412 .build();
413 let navigator = ObjectInitializer::new(&mut context)
414 .property(
415 js_string!("userAgent"),
416 js_string!("Mozilla/5.0 (compatible; Blitz)"),
417 Attribute::all(),
418 )
419 .property(js_string!("clipboard"), clipboard, Attribute::all())
420 .build();
421 register_global(&mut context, "navigator", navigator.into());
422
423 crate::dom::custom_elements::install(&ctx, &mut context);
424
425 let performance = ObjectInitializer::new(&mut context)
427 .function(
428 NativeFunction::from_fn_ptr(performance_now),
429 js_string!("now"),
430 0,
431 )
432 .build();
433 register_global(&mut context, "performance", performance.into());
434 register_global_fn(&mut context, "__blitzViewportScroll", 0, viewport_scroll);
435 register_global_fn(&mut context, "__blitzScrollViewport", 2, scroll_viewport);
436
437 register_global_fn(
440 &mut context,
441 "__blitzObserveMutations",
442 1,
443 observe_mutations,
444 );
445
446 register_global_fn(&mut context, "setTimeout", 2, set_timeout);
448 register_global_fn(&mut context, "clearTimeout", 1, clear_timer);
449 register_global_fn(&mut context, "setInterval", 2, set_interval);
450 register_global_fn(&mut context, "clearInterval", 1, clear_timer);
451 register_global_fn(
452 &mut context,
453 "requestAnimationFrame",
454 1,
455 request_animation_frame,
456 );
457 register_global_fn(&mut context, "cancelAnimationFrame", 1, clear_timer);
458 register_global_fn(
459 &mut context,
460 "addEventListener",
461 2,
462 window_add_event_listener,
463 );
464 register_global_fn(
465 &mut context,
466 "removeEventListener",
467 2,
468 window_remove_event_listener,
469 );
470 register_global_fn(&mut context, "dispatchEvent", 1, window_dispatch_event);
471 register_global_fn(
472 &mut context,
473 "__blitzDispatchHistoryEvent",
474 1,
475 window_dispatch_history_event,
476 );
477 register_global_fn(
478 &mut context,
479 "__blitzDispatchMessageEvent",
480 1,
481 window_dispatch_history_event,
482 );
483 register_global_fn(&mut context, "__blitzRandomU32", 0, random_u32);
484
485 let mut runtime = Self {
486 context,
487 ctx,
488 diagnostics,
489 module_loader,
490 job_executor,
491 job_budget: Rc::new(JobBudget::default()),
492 pending_modules: Vec::new(),
493 focussed_text_value: None,
494 domc_polls: None,
495 domc_generation: None,
496 };
497
498 runtime.eval_internal(
500 r##"
501 if (typeof globalThis.queueMicrotask !== "function") {
502 globalThis.queueMicrotask = function (callback) {
503 Promise.resolve().then(callback);
504 };
505 }
506 // `MutationObserver`, fed by the document's change log. The log
507 // records only while a registration exists, and `run_jobs` hands
508 // it to `__blitzDeliverMutations` at the end of each script turn.
509 // Records list plain arrays for `addedNodes`/`removedNodes`, which
510 // is what code iterating them with `for`, `length` or `forEach`
511 // needs; a live `NodeList` is not modelled.
512 if (typeof globalThis.MutationObserver !== "function") {
513 (function () {
514 const registrations = [];
515 const syncRecording = function () {
516 globalThis.__blitzObserveMutations(registrations.length > 0);
517 };
518 const normalize = function (options) {
519 const o = Object.assign({}, options || {});
520 if ((o.attributeOldValue || o.attributeFilter) && o.attributes === undefined) {
521 o.attributes = true;
522 }
523 if (o.characterDataOldValue && o.characterData === undefined) {
524 o.characterData = true;
525 }
526 if (!o.childList && !o.attributes && !o.characterData) {
527 throw new TypeError(
528 "Failed to execute 'observe' on 'MutationObserver': The options object must set at least one of 'attributes', 'characterData', or 'childList' to true."
529 );
530 }
531 return o;
532 };
533 const within = function (ancestor, node) {
534 try {
535 return ancestor === node
536 || (typeof ancestor.contains === "function" && ancestor.contains(node));
537 } catch (error) {
538 return false;
539 }
540 };
541 const MutationObserver = function (callback) {
542 if (typeof callback !== "function") {
543 throw new TypeError(
544 "Failed to construct 'MutationObserver': parameter 1 is not of type 'Function'."
545 );
546 }
547 this._callback = callback;
548 this._records = [];
549 };
550 MutationObserver.prototype.observe = function (target, options) {
551 if (!target || typeof target !== "object") {
552 throw new TypeError(
553 "Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'."
554 );
555 }
556 const normalized = normalize(options);
557 const existing = registrations.find(
558 (r) => r.observer === this && r.target === target
559 );
560 if (existing) {
561 existing.options = normalized;
562 } else {
563 registrations.push({ observer: this, target: target, options: normalized });
564 }
565 syncRecording();
566 };
567 MutationObserver.prototype.disconnect = function () {
568 for (let i = registrations.length - 1; i >= 0; i--) {
569 if (registrations[i].observer === this) registrations.splice(i, 1);
570 }
571 this._records = [];
572 syncRecording();
573 };
574 MutationObserver.prototype.takeRecords = function () {
575 const records = this._records;
576 this._records = [];
577 return records;
578 };
579 globalThis.__blitzDeliverMutations = function (raw) {
580 const notified = [];
581 for (const entry of raw) {
582 const type = entry[0];
583 const target = entry[1];
584 const name = entry[6];
585 // One record per observer per change, however many
586 // of its registrations match.
587 const seen = [];
588 for (const r of registrations) {
589 if (seen.indexOf(r.observer) !== -1) continue;
590 const o = r.options;
591 if (r.target !== target && !(o.subtree && within(r.target, target))) continue;
592 if (type === "childList" && !o.childList) continue;
593 if (type === "attributes") {
594 if (!o.attributes) continue;
595 if (o.attributeFilter && o.attributeFilter.indexOf(name) === -1) continue;
596 }
597 if (type === "characterData" && !o.characterData) continue;
598 const keepOld = type === "attributes"
599 ? o.attributeOldValue
600 : type === "characterData" && o.characterDataOldValue;
601 seen.push(r.observer);
602 r.observer._records.push({
603 type: type,
604 target: target,
605 addedNodes: entry[2],
606 removedNodes: entry[3],
607 previousSibling: entry[4],
608 nextSibling: entry[5],
609 attributeName: type === "attributes" ? name : null,
610 attributeNamespace: null,
611 oldValue: keepOld ? entry[7] : null
612 });
613 if (notified.indexOf(r.observer) === -1) notified.push(r.observer);
614 }
615 }
616 for (const observer of notified) {
617 const records = observer.takeRecords();
618 if (!records.length) continue;
619 try {
620 observer._callback.call(observer, records, observer);
621 } catch (error) {
622 setTimeout(function () { throw error; }, 0);
623 }
624 }
625 };
626 globalThis.MutationObserver = MutationObserver;
627 })();
628 }
629 if (typeof globalThis.structuredClone !== "function") {
630 const Exception = globalThis.DOMException;
631 const exceptionName = Object.getOwnPropertyDescriptor(Exception.prototype, "name").get;
632 const exceptionMessage = Object.getOwnPropertyDescriptor(Exception.prototype, "message").get;
633 globalThis.structuredClone = function (input, options) {
634 if (options && options.transfer && options.transfer.length) {
635 throw new Exception("structuredClone transfer is not supported", "DataCloneError");
636 }
637
638 const seen = new Map();
639 const clone = function (value) {
640 if (value === null || typeof value !== "object") {
641 if (typeof value === "function" || typeof value === "symbol") {
642 throw new Exception("value cannot be structured-cloned", "DataCloneError");
643 }
644 return value;
645 }
646 if (value === globalThis || typeof value.nodeType === "number") {
647 throw new Exception("value cannot be structured-cloned", "DataCloneError");
648 }
649 if (seen.has(value)) return seen.get(value);
650
651 let copy;
652 if (value instanceof Exception) {
653 copy = new Exception(
654 exceptionMessage.call(value),
655 exceptionName.call(value),
656 );
657 seen.set(value, copy);
658 return copy;
659 } else if (Array.isArray(value)) {
660 copy = [];
661 } else if (value instanceof Date) {
662 return new Date(value.getTime());
663 } else if (value instanceof RegExp) {
664 return new RegExp(value.source, value.flags);
665 } else if (value instanceof Map) {
666 copy = new Map();
667 seen.set(value, copy);
668 for (const [key, entry] of value) copy.set(clone(key), clone(entry));
669 return copy;
670 } else if (value instanceof Set) {
671 copy = new Set();
672 seen.set(value, copy);
673 for (const entry of value) copy.add(clone(entry));
674 return copy;
675 } else if (typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer) {
676 return value.slice(0);
677 } else if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(value)) {
678 const buffer = clone(value.buffer);
679 if (typeof DataView !== "undefined" && value instanceof DataView) {
680 return new DataView(buffer, value.byteOffset, value.byteLength);
681 }
682 return new value.constructor(buffer, value.byteOffset, value.length);
683 } else if (value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise ||
684 (typeof MessagePort === "function" && value instanceof MessagePort)) {
685 throw new Exception("value cannot be structured-cloned", "DataCloneError");
686 } else {
687 copy = Object.create(Object.getPrototypeOf(value) === null ? null : Object.prototype);
688 }
689
690 seen.set(value, copy);
691 for (const key of Object.keys(value)) copy[key] = clone(value[key]);
692 return copy;
693 };
694 return clone(input);
695 };
696 }
697 if (typeof globalThis.crypto !== "object" || globalThis.crypto === null) {
698 Object.defineProperty(globalThis, "crypto", {
699 value: {},
700 writable: false,
701 enumerable: true,
702 configurable: true,
703 });
704 }
705 if (typeof globalThis.crypto.getRandomValues !== "function") {
706 const randomU32 = globalThis.__blitzRandomU32;
707 Object.defineProperty(globalThis.crypto, "getRandomValues", {
708 value: function (array) {
709 if (typeof ArrayBuffer === "undefined" || !ArrayBuffer.isView(array)) {
710 throw new TypeError("getRandomValues requires an integer TypedArray");
711 }
712 const supported = [
713 Int8Array,
714 Uint8Array,
715 Uint8ClampedArray,
716 Int16Array,
717 Uint16Array,
718 Int32Array,
719 Uint32Array,
720 ];
721 if (!supported.some((Type) => array instanceof Type)) {
722 throw new TypeError("getRandomValues requires an integer TypedArray");
723 }
724 if (array.byteLength > 65536) {
725 throw new DOMException("getRandomValues quota exceeded", "QuotaExceededError");
726 }
727 for (let index = 0; index < array.length; index += 1) {
728 array[index] = randomU32();
729 }
730 return array;
731 },
732 writable: true,
733 enumerable: true,
734 configurable: true,
735 });
736 }
737 if (typeof globalThis.history !== "object" || globalThis.history === null) {
738 const entries = [{ state: null, url: globalThis.location.href }];
739 let index = 0;
740 const dispatchHistoryEvent = globalThis.__blitzDispatchHistoryEvent;
741 let applyingUrl = false;
742 const resolveHistoryUrl = function (url) {
743 if (url === undefined || url === null) return globalThis.location.href;
744 let resolved;
745 try {
746 resolved = new URL(String(url), globalThis.location.href);
747 } catch (error) {
748 throw new DOMException("Invalid history URL", "SecurityError");
749 }
750 const current = new URL(globalThis.location.href);
751 if (resolved.origin !== current.origin) {
752 throw new DOMException("History URL must have the document's origin", "SecurityError");
753 }
754 return resolved.href;
755 };
756 const applyUrl = function (url) {
757 if (url === undefined || url === null) return;
758 const value = String(url);
759 let pathname = globalThis.location.pathname;
760 let search = globalThis.location.search;
761 let hash = globalThis.location.hash;
762
763 if (value.startsWith("#")) {
764 hash = value;
765 } else if (value.startsWith("?")) {
766 const hashAt = value.indexOf("#");
767 search = hashAt < 0 ? value : value.slice(0, hashAt);
768 hash = hashAt < 0 ? "" : value.slice(hashAt);
769 } else {
770 let relative = value;
771 const schemeAt = relative.indexOf("://");
772 if (schemeAt >= 0) {
773 const pathAt = relative.indexOf("/", schemeAt + 3);
774 relative = pathAt < 0 ? "/" : relative.slice(pathAt);
775 }
776 if (!relative.startsWith("/")) {
777 const slashAt = pathname.lastIndexOf("/");
778 relative = pathname.slice(0, slashAt + 1) + relative;
779 }
780 const hashAt = relative.indexOf("#");
781 hash = hashAt < 0 ? "" : relative.slice(hashAt);
782 const withoutHash = hashAt < 0 ? relative : relative.slice(0, hashAt);
783 const searchAt = withoutHash.indexOf("?");
784 search = searchAt < 0 ? "" : withoutHash.slice(searchAt);
785 pathname = searchAt < 0 ? withoutHash : withoutHash.slice(0, searchAt);
786 }
787
788 globalThis.location.pathname = pathname || "/";
789 globalThis.location.search = search;
790 globalThis.location.hash = hash;
791 applyingUrl = true;
792 try {
793 globalThis.location.href = globalThis.location.protocol + "//" +
794 globalThis.location.host + globalThis.location.pathname + search + hash;
795 } finally {
796 applyingUrl = false;
797 }
798 };
799 const history = {
800 scrollRestoration: "auto",
801 get length() { return entries.length; },
802 get state() { return entries[index].state; },
803 pushState(state, _unused, url) {
804 const entry = { state: structuredClone(state), url: resolveHistoryUrl(url) };
805 entries.splice(index + 1, entries.length, entry);
806 index += 1;
807 applyUrl(entry.url);
808 },
809 replaceState(state, _unused, url) {
810 const entry = { state: structuredClone(state), url: resolveHistoryUrl(url) };
811 entries[index] = entry;
812 applyUrl(entry.url);
813 },
814 go(delta) {
815 const number = Number(delta === undefined ? 0 : delta);
816 const integer = Number.isFinite(number) ? Math.trunc(number) : 0;
817 const distance = (integer >>> 0) | 0;
818 if (distance === 0) {
819 globalThis.location.reload();
820 return;
821 }
822 setTimeout(function () {
823 const next = index + distance;
824 if (next < 0 || next >= entries.length) return;
825 const oldURL = globalThis.location.href;
826 const oldHash = globalThis.location.hash;
827 index = next;
828 applyUrl(entries[index].url);
829 const newURL = globalThis.location.href;
830 dispatchHistoryEvent(new PopStateEvent("popstate", {
831 state: entries[index].state,
832 }));
833 if (oldHash !== globalThis.location.hash) {
834 dispatchHistoryEvent(new HashChangeEvent("hashchange", {
835 oldURL: oldURL,
836 newURL: newURL,
837 }));
838 }
839 }, 0);
840 },
841 back() { this.go(-1); },
842 forward() { this.go(1); },
843 };
844 Object.defineProperty(globalThis, "history", {
845 value: history,
846 writable: false,
847 enumerable: true,
848 configurable: true,
849 });
850 // `location.href = url` navigates. A plain data property only
851 // stored the string, so pages that redirect by assignment never
852 // left. A change of fragment alone stays in this document, as a
853 // history entry with a hashchange; applyUrl keeps the value in sync.
854 let currentHref = globalThis.location.href;
855 Object.defineProperty(globalThis.location, "href", {
856 get() { return currentHref; },
857 set(value) {
858 if (applyingUrl) {
859 currentHref = String(value);
860 return;
861 }
862 const target = new URL(String(value), currentHref);
863 const current = new URL(currentHref);
864 const withoutHash = function (href) {
865 const hashAt = href.indexOf("#");
866 return hashAt < 0 ? href : href.slice(0, hashAt);
867 };
868 if (target.hash && withoutHash(target.href) === withoutHash(current.href)) {
869 const oldURL = currentHref;
870 history.pushState(null, "", target.href);
871 if (current.hash !== target.hash) {
872 dispatchHistoryEvent(new HashChangeEvent("hashchange", {
873 oldURL: oldURL,
874 newURL: currentHref,
875 }));
876 }
877 return;
878 }
879 globalThis.location.assign(target.href);
880 },
881 enumerable: true,
882 configurable: true,
883 });
884 }
885 const htmlConstructor = globalThis.__blitzHTMLConstructor;
886 const domPrototype = globalThis.__blitzDOMPrototype;
887 const sequenceFrom = Array.from;
888 const sequenceString = String;
889 globalThis.__blitzCEInitialize(function (value) {
890 if (value === null || value === undefined) {
891 throw new TypeError("cannot convert 'null' or 'undefined' to object");
892 }
893 const iterator = value[Symbol.iterator];
894 if (typeof iterator !== "function") {
895 throw new TypeError("not a callable function");
896 }
897 return sequenceFrom(value, sequenceString);
898 });
899 delete globalThis.__blitzCEInitialize;
900 const defineDomInterface = function (name, matches) {
901 if (typeof globalThis[name] === "function") return;
902 const html = name === "HTMLElement"
903 || (name.startsWith("HTML") && name.endsWith("Element"));
904 const Interface = html ? function () {
905 return htmlConstructor(new.target, name);
906 } : function () {
907 throw new TypeError("Illegal constructor");
908 };
909 Object.defineProperty(Interface, "name", { value: name, configurable: true });
910 Interface.prototype = domPrototype(name);
911 Object.defineProperty(Interface.prototype, "constructor", {
912 value: Interface,
913 writable: true,
914 configurable: true,
915 });
916 if (html) {
917 Object.setPrototypeOf(Interface,
918 name === "HTMLElement" ? globalThis.Element : globalThis.HTMLElement);
919 }
920 Object.defineProperty(Interface, Symbol.hasInstance, {
921 value: html ? Function.prototype[Symbol.hasInstance] : matches,
922 configurable: true,
923 });
924 Object.defineProperty(globalThis, name, {
925 value: Interface,
926 writable: true,
927 enumerable: false,
928 configurable: true,
929 });
930 };
931 const isNode = function (value) {
932 return value !== null && typeof value === "object" && typeof value.nodeType === "number";
933 };
934 const isElement = function (value) { return isNode(value) && value.nodeType === 1; };
935 const isTag = function (tagName) {
936 return function (value) { return isElement(value) && value.tagName === tagName; };
937 };
938 defineDomInterface("Node", isNode);
939 Object.defineProperties(globalThis.Node, {
940 ELEMENT_NODE: { value: 1 },
941 ATTRIBUTE_NODE: { value: 2 },
942 TEXT_NODE: { value: 3 },
943 CDATA_SECTION_NODE: { value: 4 },
944 PROCESSING_INSTRUCTION_NODE: { value: 7 },
945 COMMENT_NODE: { value: 8 },
946 DOCUMENT_NODE: { value: 9 },
947 DOCUMENT_TYPE_NODE: { value: 10 },
948 DOCUMENT_FRAGMENT_NODE: { value: 11 },
949 DOCUMENT_POSITION_DISCONNECTED: { value: 0x01 },
950 DOCUMENT_POSITION_PRECEDING: { value: 0x02 },
951 DOCUMENT_POSITION_FOLLOWING: { value: 0x04 },
952 DOCUMENT_POSITION_CONTAINS: { value: 0x08 },
953 DOCUMENT_POSITION_CONTAINED_BY: { value: 0x10 },
954 DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: { value: 0x20 },
955 });
956 defineDomInterface("Element", isElement);
957 defineDomInterface("HTMLElement", isElement);
958 defineDomInterface("Document", function (value) { return isNode(value) && value.nodeType === 9; });
959 defineDomInterface("HTMLDocument", function (value) { return isNode(value) && value.nodeType === 9; });
960 defineDomInterface("HTMLHeadElement", isTag("HEAD"));
961 defineDomInterface("HTMLBodyElement", isTag("BODY"));
962 defineDomInterface("HTMLAnchorElement", isTag("A"));
963 defineDomInterface("HTMLButtonElement", isTag("BUTTON"));
964 defineDomInterface("HTMLFormElement", isTag("FORM"));
965 defineDomInterface("HTMLImageElement", isTag("IMG"));
966 defineDomInterface("HTMLInputElement", isTag("INPUT"));
967 defineDomInterface("HTMLOptionElement", isTag("OPTION"));
968 defineDomInterface("HTMLScriptElement", isTag("SCRIPT"));
969 defineDomInterface("HTMLSelectElement", isTag("SELECT"));
970 defineDomInterface("HTMLStyleElement", isTag("STYLE"));
971 defineDomInterface("HTMLTemplateElement", isTag("TEMPLATE"));
972 defineDomInterface("HTMLTextAreaElement", isTag("TEXTAREA"));
973 delete globalThis.__blitzDispatchHistoryEvent;
974 defineDomInterface("HTMLDivElement", isTag("DIV"));
975 defineDomInterface("HTMLSpanElement", isTag("SPAN"));
976 defineDomInterface("HTMLParagraphElement", isTag("P"));
977 defineDomInterface("HTMLLIElement", isTag("LI"));
978 defineDomInterface("HTMLUListElement", isTag("UL"));
979 defineDomInterface("HTMLOListElement", isTag("OL"));
980 delete globalThis.__blitzHTMLConstructor;
981 delete globalThis.__blitzDOMPrototype;
982 delete globalThis.__blitzRandomU32;
983 "##,
984 "<blitz-bootstrap>",
985 );
986
987 crate::domc::install(&mut runtime.context);
988 runtime.eval_internal(include_str!("domc.js"), "<blitz-domc>");
989 runtime.eval_internal(include_str!("domexc.js"), "<blitz-domexc>");
990 let loader = Rc::clone(&runtime.module_loader);
991 crate::docwrite::install(&mut runtime.context, loader);
992 crate::fonts::install(&mut runtime.context);
993 runtime.eval_internal(include_str!("docwrite.js"), "<blitz-docwrite>");
994
995 runtime
996 }
997
998 pub fn eval(&mut self, code: &str, description: &str) {
1001 self.eval_at(code, None, description);
1002 }
1003
1004 pub fn eval_at(&mut self, code: &str, url: Option<&Url>, description: &str) {
1011 let path = url.map(|url| url.as_str().to_owned());
1012 let source = Source::from_bytes(code.as_bytes());
1013 let source = match &path {
1014 Some(path) => source.with_path(Path::new(path)),
1015 None => source,
1016 };
1017
1018 if let Err(error) = self.context.eval(source) {
1019 report_js_error(&self.diagnostics, description, &error);
1020 }
1021 crate::domc::set_current_script(&self.context, None);
1022 self.run_jobs(description);
1023 }
1024
1025 pub fn set_import_map(&self, json: &str, base_url: Option<&Url>) {
1031 self.module_loader
1032 .set_import_map(crate::module::ImportMap::parse(json, base_url));
1033 }
1034
1035 pub fn eval_module(&mut self, code: &str, url: Option<&Url>, description: &str) {
1042 let path = url.map(|url| url.as_str().to_owned());
1043 let source = Source::from_bytes(code.as_bytes());
1044 let source = match &path {
1045 Some(path) => source.with_path(Path::new(path)),
1046 None => source,
1047 };
1048
1049 let module = match Module::parse(source, None, &mut self.context) {
1050 Ok(module) => module,
1051 Err(error) => {
1052 report_js_error(&self.diagnostics, description, &error);
1053 return;
1054 }
1055 };
1056
1057 if let Some(url) = url {
1061 self.module_loader.register(url, module.clone());
1062 }
1063
1064 let promise = module.load_link_evaluate(&mut self.context);
1069 self.run_jobs(description);
1070
1071 match promise.state() {
1072 PromiseState::Fulfilled(_) => {}
1073 PromiseState::Rejected(error) => {
1074 report_js_error(&self.diagnostics, description, &JsError::from_opaque(error));
1075 }
1076 PromiseState::Pending => self.pending_modules.push((promise, description.to_owned())),
1081 }
1082 }
1083
1084 pub fn poll_module_evaluations(&mut self) {
1090 if self.pending_modules.is_empty() {
1091 return;
1092 }
1093
1094 let mut still_pending = Vec::new();
1095 for (promise, description) in std::mem::take(&mut self.pending_modules) {
1096 match promise.state() {
1097 PromiseState::Pending => still_pending.push((promise, description)),
1098 PromiseState::Fulfilled(_) => {}
1099 PromiseState::Rejected(error) => {
1100 report_js_error(
1101 &self.diagnostics,
1102 &description,
1103 &JsError::from_opaque(error),
1104 );
1105 }
1106 }
1107 }
1108 self.pending_modules = still_pending;
1109 }
1110
1111 pub fn eval_json(
1112 &mut self,
1113 code: &str,
1114 description: &str,
1115 ) -> Result<serde_json::Value, String> {
1116 match self.context.eval(Source::from_bytes(code)) {
1117 Ok(value) => {
1118 self.run_jobs(description);
1119 value
1120 .to_json(&mut self.context)
1121 .map(|value| value.unwrap_or(serde_json::Value::Null))
1122 .map_err(|error| error.to_string())
1123 }
1124 Err(error) => {
1125 report_js_error(&self.diagnostics, description, &error);
1126 Err(error.to_string())
1127 }
1128 }
1129 }
1130
1131 #[cfg(feature = "debug-control")]
1132 pub fn console_entries_after(&self, sequence: u64) -> Vec<DiagnosticEntry> {
1133 self.diagnostics
1134 .borrow()
1135 .console
1136 .iter()
1137 .filter(|entry| entry.sequence > sequence)
1138 .cloned()
1139 .collect()
1140 }
1141
1142 #[cfg(feature = "debug-control")]
1143 pub fn runtime_errors_after(&self, sequence: u64) -> Vec<DiagnosticEntry> {
1144 self.diagnostics
1145 .borrow()
1146 .errors
1147 .iter()
1148 .filter(|entry| entry.sequence > sequence)
1149 .cloned()
1150 .collect()
1151 }
1152
1153 #[cfg(feature = "debug-control")]
1154 pub fn diagnostic_batch(
1155 &self,
1156 console_after: u64,
1157 errors_after: u64,
1158 limit: usize,
1159 ) -> DiagnosticBatch {
1160 let diagnostics = self.diagnostics.borrow();
1161 DiagnosticBatch {
1162 console: diagnostic_stream(&diagnostics.console, console_after, limit),
1163 errors: diagnostic_stream(&diagnostics.errors, errors_after, limit),
1164 }
1165 }
1166
1167 pub(crate) fn eval_internal(&mut self, code: &str, description: &str) {
1168 let source = Source::from_bytes(code).with_path(Path::new(description));
1171 if let Err(error) = self.context.eval(source) {
1172 report_js_error(&self.diagnostics, description, &error);
1173 }
1174 }
1175
1176 pub(crate) fn begin_job_slice(&self) -> JobSlice {
1177 self.job_budget.begin()
1178 }
1179
1180 pub(crate) fn set_document_ready_state(&mut self, next: &'static str) -> bool {
1181 if !crate::domc::set_ready_state(&self.context, next) {
1182 return false;
1183 }
1184 let root_id = self.ctx.doc.borrow().root_node().id;
1185 self.dispatch_node_event(root_id, "readystatechange");
1186 true
1187 }
1188
1189 pub(crate) fn has_pending_module_evaluations(&self) -> bool {
1190 self.pending_modules
1191 .iter()
1192 .any(|(promise, _)| matches!(promise.state(), PromiseState::Pending))
1193 }
1194
1195 pub(crate) fn has_pending_module_loads(&self) -> bool {
1196 self.module_loader.has_pending_loads()
1197 }
1198
1199 pub(crate) fn poll_domc(&mut self) -> bool {
1200 let polls = match &self.domc_polls {
1201 Some(polls) => polls.clone(),
1202 None => {
1203 let global = self.context.global_object().clone();
1204 let polls = ["__blitzDOMCPoll", "__blitzDocwritePoll"].map(|name| {
1205 global
1206 .get(boa_engine::JsString::from(name), &mut self.context)
1207 .ok()
1208 .and_then(|value| value.as_callable())
1209 });
1210 self.domc_polls = Some(polls.clone());
1211 polls
1212 }
1213 };
1214 let generation = self.ctx.doc.borrow().viewport_generation();
1218 let device_changed = self.domc_generation != Some(generation);
1219 self.domc_generation = Some(generation);
1220 let mut changed = false;
1221 for (index, function) in polls.into_iter().enumerate() {
1222 let Some(function) = function else {
1223 continue;
1224 };
1225 if index == 0 && !device_changed {
1226 continue;
1227 }
1228 let result = function.call(&JsValue::undefined(), &[], &mut self.context);
1229 match result {
1230 Ok(value) => changed |= value.to_boolean(),
1231 Err(error) => {
1232 report_js_error(&self.diagnostics, "document platform changes", &error);
1233 }
1234 }
1235 }
1236 changed
1237 }
1238
1239 pub(crate) fn job_budget(&self) -> Duration {
1240 self.job_budget.duration()
1241 }
1242
1243 pub(crate) fn set_job_budget(&self, duration: Duration) {
1244 self.job_budget.set_duration(duration);
1245 }
1246
1247 pub(crate) fn pending_task_count(&self) -> u64 {
1251 let due = self.ctx.state.borrow().timers.due_count(Instant::now()) as u64;
1252 due + u64::from(self.jobs_remain())
1253 }
1254
1255 pub(crate) fn job_slice_exhausted(&self) -> bool {
1256 self.job_budget.exhausted()
1257 }
1258
1259 pub(crate) fn jobs_remain(&self) -> bool {
1260 self.job_executor.has_pending_jobs(&self.context)
1261 || self.ctx.doc.borrow().has_pending_mutations()
1262 }
1263
1264 pub fn run_jobs(&mut self, description: &str) {
1266 self.run_jobs_with_progress(description);
1267 }
1268
1269 pub(crate) fn run_jobs_with_progress(&mut self, description: &str) -> bool {
1276 let _slice = self.begin_job_slice();
1277 let before = self.job_executor.executed_job_count();
1278 const MUTATION_DELIVERY_ROUNDS: usize = 32;
1279 let mut delivered = false;
1280 for _ in 0..MUTATION_DELIVERY_ROUNDS {
1281 crate::dom::inline_handlers::sync(&self.ctx, &mut self.context);
1282 crate::dom::custom_elements::checkpoint(&self.ctx, &mut self.context);
1283 if let Err(error) = Rc::clone(&self.job_executor)
1284 .run_jobs_with_budget(&mut self.context, self.job_budget.remaining())
1285 {
1286 report_js_error(&self.diagnostics, description, &error);
1287 }
1288 let mutations_delivered = self.deliver_mutations();
1289 let toggles_delivered =
1290 crate::dom::element::deliver_details_toggles(&self.ctx, &mut self.context);
1291 #[cfg(feature = "shadow-dom")]
1292 let slots_delivered =
1293 crate::dom::shadow::deliver_slot_changes(&self.ctx, &mut self.context);
1294 #[cfg(not(feature = "shadow-dom"))]
1295 let slots_delivered = false;
1296 if !mutations_delivered && !slots_delivered && !toggles_delivered {
1297 return delivered
1298 || self.jobs_remain()
1299 || self.job_executor.executed_job_count() != before;
1300 }
1301 delivered = true;
1302 }
1303 delivered || self.jobs_remain() || self.job_executor.executed_job_count() != before
1304 }
1305
1306 fn deliver_mutations(&mut self) -> bool {
1309 let records = {
1310 let mut doc = self.ctx.doc.borrow_mut();
1311 if !doc.is_recording_mutations() {
1312 return false;
1313 }
1314 doc.take_mutations()
1315 };
1316 if records.is_empty() {
1317 return false;
1318 }
1319 let deliver = match self
1320 .context
1321 .global_object()
1322 .get(js_string!("__blitzDeliverMutations"), &mut self.context)
1323 {
1324 Ok(value) => value,
1325 Err(_) => return false,
1326 };
1327 let Some(deliver) = deliver.as_callable() else {
1328 return false;
1329 };
1330 let values: Vec<JsValue> = records
1331 .into_iter()
1332 .filter_map(|record| mutation_record_value(&self.ctx, record, &mut self.context))
1333 .collect();
1334 let list = boa_engine::object::builtins::JsArray::from_iter(values, &mut self.context);
1335 if let Err(error) = deliver.call(&JsValue::undefined(), &[list.into()], &mut self.context) {
1336 report_js_error(&self.diagnostics, "MutationObserver delivery", &error);
1337 }
1338 true
1339 }
1340
1341 pub fn next_timer_deadline(&self) -> Option<Instant> {
1343 let dom = self.ctx.state.borrow().timers.next_deadline();
1344 let boa = self
1345 .job_executor
1346 .next_job_timeout(&self.context)
1347 .map(|delay| Instant::now() + delay);
1348 match (dom, boa) {
1349 (Some(dom), Some(boa)) => Some(dom.min(boa)),
1350 (dom, boa) => dom.or(boa),
1351 }
1352 }
1353
1354 pub fn sweep_detached_nodes(&mut self) {
1361 crate::dom::sweep_detached_nodes(&self.ctx);
1362 }
1363
1364 pub fn run_due_timers(&mut self, profiling: bool) -> bool {
1370 if self.job_budget.exhausted() || self.jobs_remain() {
1371 return false;
1372 }
1373 let timers_started = profiling.then(std::time::Instant::now);
1374 let due = self.ctx.state.borrow_mut().timers.take_due(Instant::now());
1375 if due.is_empty() {
1376 return false;
1377 }
1378 let frame_args = [JsValue::new(TIME_ORIGIN.elapsed().as_secs_f64() * 1000.0)];
1381 for timer in due {
1382 let args = if timer.animation_frame {
1383 if !self
1384 .ctx
1385 .state
1386 .borrow_mut()
1387 .timers
1388 .begin_animation_frame_callback(timer.id)
1389 {
1390 continue;
1391 }
1392 &frame_args[..]
1393 } else {
1394 &timer.args[..]
1395 };
1396 if let Err(error) = timer
1397 .callback
1398 .call(&JsValue::undefined(), args, &mut self.context)
1399 {
1400 report_js_error(&self.diagnostics, "timer callback", &error);
1401 }
1402 }
1403 self.run_jobs("timer microtasks");
1404 if let Some(started) = timers_started {
1405 crate::script_stats::record_work("timers", started.elapsed());
1406 }
1407 true
1408 }
1409
1410 pub fn dispatch_dom_event(
1415 &mut self,
1416 chain: &[NodeId],
1417 event: &DomEvent,
1418 event_state: &mut EventState,
1419 profiling: bool,
1420 ) -> bool {
1421 let dispatch_started = profiling.then(std::time::Instant::now);
1424 let ran = self.dispatch_dom_event_timed(chain, event, event_state);
1425 if let Some(started) = dispatch_started {
1426 crate::script_stats::record_work(
1427 &format!("event:{}", event.data.name()),
1428 started.elapsed(),
1429 );
1430 }
1431 ran
1432 }
1433
1434 fn dispatch_dom_event_timed(
1435 &mut self,
1436 chain: &[NodeId],
1437 event: &DomEvent,
1438 event_state: &mut EventState,
1439 ) -> bool {
1440 let pointer_id = match &event.data {
1441 DomEventData::PointerMove(pointer)
1442 | DomEventData::PointerDown(pointer)
1443 | DomEventData::PointerUp(pointer)
1444 | DomEventData::PointerCancel(pointer) => Some(match pointer.id {
1445 BlitzPointerId::Mouse => 1,
1446 BlitzPointerId::Pen => 2,
1447 BlitzPointerId::Finger(id) => id.saturating_add(3),
1448 }),
1449 _ => None,
1450 };
1451 let captured_node =
1452 pointer_id.and_then(|id| self.ctx.state.borrow().pointer_capture.get(&id).copied());
1453 let captured_chain = captured_node.map(|target| {
1454 let doc = self.ctx.doc.borrow();
1455 let mut chain = Vec::new();
1456 let mut current = Some(target);
1457 while let Some(node_id) = current {
1458 chain.push(node_id);
1459 current = doc.get_node(node_id).and_then(|node| node.parent);
1460 }
1461 chain
1462 });
1463 let chain = captured_chain.as_deref().unwrap_or(chain);
1464
1465 if chain.is_empty() {
1466 return false;
1467 }
1468 let name = event.name().to_string();
1469 let target: JsValue = node_wrapper(&self.ctx, chain[0], &mut self.context).into();
1471 let event_object = create_event_for_dom_event(
1472 &self.ctx,
1473 &event.data,
1474 event.bubbles,
1475 event.cancelable,
1476 &target,
1477 &mut self.context,
1478 );
1479 let mut any_called = self.dispatch_event_inner(
1480 chain,
1481 &name,
1482 event.bubbles,
1483 |_, _, _| event_object,
1484 event_state,
1485 );
1486
1487 let synthesise_change = match &event.data {
1496 DomEventData::Input(_) => {
1497 self.target_is_checkbox_or_radio(event.target)
1500 || self.target_is_select(event.target)
1501 }
1502 DomEventData::Focus(_) => {
1503 self.focussed_text_value = self
1504 .text_control_value(event.target)
1505 .map(|value| (event.target, value));
1506 false
1507 }
1508 DomEventData::Blur(_) => match self.focussed_text_value.take() {
1509 Some((node_id, focussed_value)) if node_id == event.target => self
1510 .text_control_value(event.target)
1511 .is_some_and(|current| current != focussed_value),
1512 _ => false,
1513 },
1514 _ => false,
1515 };
1516 if synthesise_change {
1517 let mut change_state = EventState::default();
1518 any_called |= self.dispatch_event_inner(
1519 chain,
1520 "change",
1521 true,
1522 |ctx, target, context| create_event(ctx, "change", true, false, target, context),
1523 &mut change_state,
1524 );
1525 if change_state.redraw_is_requested() {
1526 event_state.request_redraw();
1527 }
1528 }
1529
1530 if any_called {
1531 self.run_jobs("event microtasks");
1532 }
1533
1534 if matches!(
1535 event.data,
1536 DomEventData::PointerUp(_) | DomEventData::PointerCancel(_)
1537 ) && let Some(pointer_id) = pointer_id
1538 {
1539 self.ctx
1540 .state
1541 .borrow_mut()
1542 .pointer_capture
1543 .remove(&pointer_id);
1544 }
1545
1546 any_called
1547 }
1548
1549 fn text_control_value(&self, node_id: NodeId) -> Option<String> {
1552 let doc = self.ctx.doc.borrow();
1553 doc.get_node(node_id)?
1554 .element_data()?
1555 .text_input_data()
1556 .map(|input| input.editor.text().to_string())
1557 }
1558
1559 fn target_is_select(&self, node_id: NodeId) -> bool {
1560 let doc = self.ctx.doc.borrow();
1561 doc.get_node(node_id).is_some_and(|node| {
1562 node.data
1563 .is_element_with_tag_name(&blitz_dom::local_name!("select"))
1564 })
1565 }
1566
1567 fn target_is_checkbox_or_radio(&self, node_id: NodeId) -> bool {
1568 let doc = self.ctx.doc.borrow();
1569 doc.get_node(node_id)
1570 .and_then(|node| node.element_data())
1571 .is_some_and(|element| {
1572 element.name.local == blitz_dom::local_name!("input")
1573 && matches!(
1574 element.attr(blitz_dom::local_name!("type")),
1575 Some("checkbox") | Some("radio")
1576 )
1577 })
1578 }
1579
1580 fn dispatch_event_inner(
1583 &mut self,
1584 chain: &[NodeId],
1585 name: &str,
1586 bubbles: bool,
1587 make_event: impl FnOnce(&DomCtx, &JsValue, &mut Context) -> JsObject,
1588 event_state: &mut EventState,
1589 ) -> bool {
1590 let ctx = self.ctx.clone();
1591 let context = &mut self.context;
1592 crate::dom::inline_handlers::sync(&ctx, context);
1593 let on_name = JsString::from(format!("on{name}"));
1594
1595 #[cfg(feature = "shadow-dom")]
1596 if ctx.doc.borrow().has_shadow_roots() {
1597 let Some(&target_id) = chain.first() else {
1598 return false;
1599 };
1600 let target: JsValue = node_wrapper(&ctx, target_id, context).into();
1601 let event_obj = make_event(&ctx, &target, context);
1602 let composed = name.starts_with("pointer")
1603 || name.starts_with("mouse")
1604 || name.starts_with("touch")
1605 || name.starts_with("key")
1606 || matches!(
1607 name,
1608 "click"
1609 | "dblclick"
1610 | "contextmenu"
1611 | "wheel"
1612 | "input"
1613 | "focus"
1614 | "blur"
1615 | "focusin"
1616 | "focusout"
1617 );
1618 crate::dom::define_value(&event_obj, "composed", JsValue::from(composed), context);
1619 match crate::dom::shadow_event::dispatch(&ctx, target_id, &event_obj, false, context) {
1620 Ok(result) => {
1621 if result.prevented {
1622 event_state.prevent_default();
1623 }
1624 if result.stopped {
1625 event_state.stop_propagation();
1626 }
1627 if result.called {
1628 event_state.request_redraw();
1629 }
1630 return result.called;
1631 }
1632 Err(error) => {
1633 report_js_error(&self.diagnostics, name, &error);
1634 return false;
1635 }
1636 }
1637 }
1638
1639 let window_handler = bubbles
1641 && context
1642 .global_object()
1643 .get(on_name, context)
1644 .is_ok_and(|handler| handler.is_callable());
1645 let may_have_listeners = {
1646 let state = ctx.state.borrow();
1647 let registry_hit = chain.iter().any(|node_id| {
1648 state
1649 .node_listeners
1650 .get(node_id)
1651 .and_then(|map| map.get(name))
1652 .is_some_and(|listeners| !listeners.is_empty())
1653 }) || state
1654 .window_listeners
1655 .get(name)
1656 .is_some_and(|listeners| !listeners.is_empty());
1657 let wrapper_hit = chain
1659 .iter()
1660 .any(|node_id| state.node_wrappers.contains_key(node_id));
1661 registry_hit || wrapper_hit || window_handler
1662 };
1663 if !may_have_listeners {
1664 return false;
1665 }
1666
1667 let target: JsValue = node_wrapper(&ctx, chain[0], context).into();
1668 let event_obj = make_event(&ctx, &target, context);
1669 let _dispatch = crate::dom::event::begin_dispatch(&event_obj, false, context)
1670 .expect("fresh native event");
1671 crate::dom::define_value(&event_obj, "eventPhase", JsValue::from(2), context);
1672 let mut event_path: Vec<JsObject> = chain
1673 .iter()
1674 .map(|&node_id| node_wrapper(&ctx, node_id, context))
1675 .collect();
1676 event_path.push(context.global_object().clone());
1677 set_event_path(&event_obj, event_path);
1678 let event_ref = |event_obj: &JsObject, f: &dyn Fn(&EventRef) -> bool| -> bool {
1679 event_obj
1680 .downcast_ref::<EventRef>()
1681 .map(|event| f(&event))
1682 .unwrap_or(false)
1683 };
1684
1685 let mut any_called = false;
1686
1687 'chain: for &node_id in chain {
1688 let mut callbacks: Vec<(JsObject, bool)> = Vec::new();
1691 {
1692 let mut state = ctx.state.borrow_mut();
1693 if let Some(listeners) = state
1694 .node_listeners
1695 .get_mut(&node_id)
1696 .and_then(|map| map.get_mut(name))
1697 {
1698 callbacks.extend(listeners.iter().filter_map(|listener| {
1699 listener
1700 .callback
1701 .upgrade()
1702 .map(|callback| (callback, listener.passive))
1703 }));
1704 listeners.retain(|l| !l.once);
1706 }
1707 }
1708 crate::dom::node::sync_node_listener_callbacks(&ctx, node_id, context);
1709
1710 let current_target: JsValue = node_wrapper(&ctx, node_id, context).into();
1711 let phase = if node_id == chain[0] { 2 } else { 3 };
1712 crate::dom::define_value(&event_obj, "eventPhase", JsValue::from(phase), context);
1713 crate::dom::define_value(&event_obj, "currentTarget", current_target.clone(), context);
1714
1715 for (callback, passive) in callbacks {
1716 any_called = true;
1717 if let Err(error) = crate::dom::passive::invoke(
1718 &callback,
1719 ¤t_target,
1720 &event_obj,
1721 passive,
1722 context,
1723 ) {
1724 let what = listener_error_context(
1725 name,
1726 &format!("node {node_id}"),
1727 &callback,
1728 context,
1729 );
1730 report_js_error(&self.diagnostics, &what, &error);
1731 }
1732 if event_ref(&event_obj, &|event| event.stopped_immediate.get()) {
1733 break 'chain;
1734 }
1735 }
1736
1737 let wrapper = node_wrapper(&ctx, node_id, context);
1738 any_called |=
1739 crate::dom::inline_handlers::invoke(&ctx, &wrapper, &event_obj, name, context);
1740 if event_ref(&event_obj, &|event| event.stopped_immediate.get()) {
1741 break 'chain;
1742 }
1743 if !bubbles || event_ref(&event_obj, &|event| event.stopped.get()) {
1744 break;
1745 }
1746 }
1747
1748 if bubbles && !event_ref(&event_obj, &|event| event.stopped.get()) {
1750 let listeners: Vec<Listener> = {
1751 let mut state = ctx.state.borrow_mut();
1752 match state.window_listeners.get_mut(name) {
1753 Some(listeners) => {
1754 let cloned = listeners.clone();
1755 listeners.retain(|l| !l.once);
1756 cloned
1757 }
1758 None => Vec::new(),
1759 }
1760 };
1761 let window = context.global_object().clone();
1762 let global: JsValue = window.clone().into();
1763 crate::dom::define_value(&event_obj, "eventPhase", JsValue::from(3), context);
1764 crate::dom::define_value(&event_obj, "currentTarget", global.clone(), context);
1765 for listener in listeners {
1766 any_called = true;
1767 if let Err(error) = crate::dom::passive::invoke(
1768 &listener.callback,
1769 &global,
1770 &event_obj,
1771 listener.passive,
1772 context,
1773 ) {
1774 let what = listener_error_context(name, "window", &listener.callback, context);
1775 report_js_error(&self.diagnostics, &what, &error);
1776 }
1777 if event_ref(&event_obj, &|event| event.stopped_immediate.get()) {
1778 break;
1779 }
1780 }
1781 if !event_ref(&event_obj, &|event| event.stopped_immediate.get()) {
1782 any_called |=
1783 crate::dom::inline_handlers::invoke(&ctx, &window, &event_obj, name, context);
1784 }
1785 }
1786
1787 crate::dom::define_value(&event_obj, "currentTarget", JsValue::null(), context);
1788
1789 if event_ref(&event_obj, &|event| event.prevented.get()) {
1791 event_state.prevent_default();
1792 }
1793 if event_ref(&event_obj, &|event| event.stopped.get()) {
1794 event_state.stop_propagation();
1795 }
1796 if any_called {
1797 event_state.request_redraw();
1798 }
1799
1800 any_called
1801 }
1802
1803 pub fn dispatch_document_event(&mut self, name: &str) -> bool {
1805 let root_id = self.ctx.doc.borrow().root_node().id;
1806 let mut event_state = EventState::default();
1807 let ran = self.dispatch_event_inner(
1808 &[root_id],
1809 name,
1810 true,
1811 |ctx, target, context| create_event(ctx, name, true, false, target, context),
1812 &mut event_state,
1813 );
1814 if ran {
1815 self.run_jobs("event microtasks");
1816 }
1817 ran
1818 }
1819
1820 pub fn dispatch_node_event(&mut self, node_id: NodeId, name: &str) -> bool {
1827 let mut event_state = EventState::default();
1828 let ran = self.dispatch_event_inner(
1829 &[node_id],
1830 name,
1831 false,
1832 |ctx, target, context| create_event(ctx, name, false, false, target, context),
1833 &mut event_state,
1834 );
1835 if ran {
1836 self.run_jobs("event microtasks");
1837 }
1838 ran
1839 }
1840
1841 pub fn dispatch_window_event(&mut self, name: &str) -> bool {
1843 let ctx = self.ctx.clone();
1844 let context = &mut self.context;
1845
1846 let listeners: Vec<Listener> = {
1847 let mut state = ctx.state.borrow_mut();
1848 match state.window_listeners.get_mut(name) {
1849 Some(listeners) => {
1850 let cloned = listeners.clone();
1851 listeners.retain(|l| !l.once);
1852 cloned
1853 }
1854 None => Vec::new(),
1855 }
1856 };
1857
1858 let global: JsValue = context.global_object().into();
1859 let event_obj = create_event(&ctx, name, false, false, &global, context);
1860 crate::dom::define_value(&event_obj, "currentTarget", global.clone(), context);
1861
1862 let mut any_called = false;
1863 for listener in listeners {
1864 any_called = true;
1865 if let Err(error) = crate::dom::passive::invoke(
1866 &listener.callback,
1867 &global,
1868 &event_obj,
1869 listener.passive,
1870 context,
1871 ) {
1872 let what = listener_error_context(name, "window", &listener.callback, context);
1873 report_js_error(&self.diagnostics, &what, &error);
1874 }
1875 }
1876
1877 if !event_obj
1878 .downcast_ref::<EventRef>()
1879 .is_some_and(|event| event.stopped_immediate.get())
1880 {
1881 let window = context.global_object().clone();
1882 any_called |=
1883 crate::dom::inline_handlers::invoke(&ctx, &window, &event_obj, name, context);
1884 }
1885 crate::dom::define_value(&event_obj, "currentTarget", JsValue::null(), context);
1886
1887 if any_called {
1888 self.run_jobs("event microtasks");
1889 }
1890 any_called
1891 }
1892}
1893
1894fn get_computed_style(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
1906 crate::domc::get_computed_style(args, context)
1907}
1908
1909fn observe_mutations(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
1913 let on = args.first().is_some_and(JsValue::to_boolean);
1914 dom_ctx(context)?
1915 .doc
1916 .borrow_mut()
1917 .set_recording_mutations(on);
1918 Ok(JsValue::undefined())
1919}
1920
1921fn mutation_record_value(
1925 ctx: &DomCtx,
1926 record: blitz_dom::DomMutation,
1927 context: &mut Context,
1928) -> Option<JsValue> {
1929 use blitz_dom::DomMutation;
1930 use boa_engine::object::builtins::JsArray;
1931
1932 let exists = |id: NodeId| ctx.doc.borrow().get_node(id).is_some();
1933 let node = |id: NodeId, context: &mut Context| -> JsValue {
1934 if exists(id) {
1935 node_wrapper(ctx, id, context).into()
1936 } else {
1937 JsValue::null()
1938 }
1939 };
1940 let nodes = |ids: Vec<NodeId>, context: &mut Context| -> JsValue {
1941 let values: Vec<JsValue> = ids
1942 .into_iter()
1943 .filter(|id| exists(*id))
1944 .map(|id| node_wrapper(ctx, id, context).into())
1945 .collect();
1946 JsArray::from_iter(values, context).into()
1947 };
1948 let text = |value: Option<String>| -> JsValue {
1949 value.map_or(JsValue::null(), |value| {
1950 JsString::from(value.as_str()).into()
1951 })
1952 };
1953
1954 let fields: Vec<JsValue> = match record {
1955 DomMutation::ChildList {
1956 target,
1957 added,
1958 removed,
1959 previous_sibling,
1960 next_sibling,
1961 } => {
1962 if !exists(target) {
1963 return None;
1964 }
1965 vec![
1966 js_string!("childList").into(),
1967 node(target, context),
1968 nodes(added, context),
1969 nodes(removed, context),
1970 previous_sibling.map_or(JsValue::null(), |id| node(id, context)),
1971 next_sibling.map_or(JsValue::null(), |id| node(id, context)),
1972 JsValue::null(),
1973 JsValue::null(),
1974 ]
1975 }
1976 DomMutation::Attributes {
1977 target,
1978 name,
1979 old_value,
1980 } => {
1981 if !exists(target) {
1982 return None;
1983 }
1984 vec![
1985 js_string!("attributes").into(),
1986 node(target, context),
1987 nodes(Vec::new(), context),
1988 nodes(Vec::new(), context),
1989 JsValue::null(),
1990 JsValue::null(),
1991 text(Some(name)),
1992 text(old_value),
1993 ]
1994 }
1995 DomMutation::CharacterData { target, old_value } => {
1996 if !exists(target) {
1997 return None;
1998 }
1999 vec![
2000 js_string!("characterData").into(),
2001 node(target, context),
2002 nodes(Vec::new(), context),
2003 nodes(Vec::new(), context),
2004 JsValue::null(),
2005 JsValue::null(),
2006 JsValue::null(),
2007 text(Some(old_value)),
2008 ]
2009 }
2010 };
2011 Some(JsArray::from_iter(fields, context).into())
2012}
2013
2014fn ipc_post_message(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2015 let body = args
2016 .first()
2017 .unwrap_or(&JsValue::undefined())
2018 .to_string(context)?
2019 .to_std_string_lossy();
2020 let handler = dom_ctx(context)?.state.borrow().ipc_handler.clone();
2021 if let Some(handler) = handler {
2022 handler(body);
2023 }
2024 Ok(JsValue::undefined())
2025}
2026
2027fn clipboard_write_text(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2034 let text = args
2035 .first()
2036 .unwrap_or(&JsValue::undefined())
2037 .to_string(context)?
2038 .to_std_string_lossy();
2039 let wrote = dom_ctx(context)?
2040 .doc
2041 .borrow()
2042 .shell_provider
2043 .set_clipboard_text(text)
2044 .is_ok();
2045 let promise = if wrote {
2046 JsPromise::resolve(JsValue::undefined(), context)
2047 } else {
2048 let error =
2049 crate::dom_exception::error("NotAllowedError", "the clipboard is unavailable", context);
2050 JsPromise::reject(error, context)
2051 };
2052 Ok(JsValue::from(promise?))
2053}
2054
2055fn clipboard_read_text(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2059 let read = dom_ctx(context)?
2060 .doc
2061 .borrow()
2062 .shell_provider
2063 .get_clipboard_text();
2064 let promise = match read {
2065 Ok(text) => JsPromise::resolve(JsValue::from(JsString::from(text)), context),
2066 Err(_) => {
2067 let error = crate::dom_exception::error(
2068 "NotAllowedError",
2069 "the clipboard is unavailable",
2070 context,
2071 );
2072 JsPromise::reject(error, context)
2073 }
2074 };
2075 Ok(JsValue::from(promise?))
2076}
2077
2078static TIME_ORIGIN: LazyLock<Instant> = LazyLock::new(Instant::now);
2082
2083fn performance_now(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
2084 Ok(JsValue::new(TIME_ORIGIN.elapsed().as_secs_f64() * 1000.0))
2085}
2086
2087fn viewport_scroll(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2088 let ctx = dom_ctx(context)?;
2089 let scroll = ctx.doc.borrow().viewport_scroll();
2090 Ok(boa_engine::object::builtins::JsArray::from_iter(
2091 [JsValue::new(scroll.x), JsValue::new(scroll.y)],
2092 context,
2093 )
2094 .into())
2095}
2096
2097fn scroll_viewport(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2098 let ctx = dom_ctx(context)?;
2099 let x = args
2100 .first()
2101 .unwrap_or(&JsValue::undefined())
2102 .to_number(context)?;
2103 let y = args
2104 .get(1)
2105 .unwrap_or(&JsValue::undefined())
2106 .to_number(context)?;
2107 let x = if x.is_finite() { x } else { 0.0 };
2108 let y = if y.is_finite() { y } else { 0.0 };
2109 ctx.flush_layout();
2110 let mut document = ctx.doc.borrow_mut();
2111 let current = document.viewport_scroll();
2112 let changed = document.scroll_viewport_by_has_changed(current.x - x, current.y - y);
2113 if changed {
2114 document.shell_provider.request_redraw();
2115 }
2116 Ok(JsValue::new(changed))
2117}
2118
2119fn random_u32(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2120 getrandom::u32().map(JsValue::new).map_err(|error| {
2121 crate::dom_exception::error(
2122 "OperationError",
2123 &format!("secure random generation failed: {error}"),
2124 context,
2125 )
2126 })
2127}
2128
2129fn register_global(context: &mut Context, name: &str, value: JsValue) {
2130 context
2131 .register_global_property(
2132 JsString::from(name),
2133 value,
2134 Attribute::WRITABLE.union(Attribute::CONFIGURABLE),
2135 )
2136 .expect("failed to register global");
2137}
2138
2139fn register_global_fn(
2140 context: &mut Context,
2141 name: &str,
2142 length: usize,
2143 body: fn(&JsValue, &[JsValue], &mut Context) -> JsResult<JsValue>,
2144) {
2145 context
2146 .register_global_callable(
2147 JsString::from(name),
2148 length,
2149 NativeFunction::from_fn_ptr(body),
2150 )
2151 .expect("failed to register global function");
2152}
2153
2154fn build_location(base_url: Option<&Url>, context: &mut Context) -> JsValue {
2155 let (href, protocol, host, pathname, search, hash) = match base_url {
2156 Some(url) => (
2157 url.to_string(),
2158 format!("{}:", url.scheme()),
2159 url.host_str().unwrap_or_default().to_string(),
2160 url.path().to_string(),
2161 url.query().map(|q| format!("?{q}")).unwrap_or_default(),
2162 url.fragment().map(|f| format!("#{f}")).unwrap_or_default(),
2163 ),
2164 None => (
2165 "about:blank".to_string(),
2166 "about:".to_string(),
2167 String::new(),
2168 "blank".to_string(),
2169 String::new(),
2170 String::new(),
2171 ),
2172 };
2173 ObjectInitializer::new(context)
2174 .property(js_string!("href"), JsString::from(href), Attribute::all())
2175 .property(
2176 js_string!("protocol"),
2177 JsString::from(protocol),
2178 Attribute::all(),
2179 )
2180 .property(
2181 js_string!("host"),
2182 JsString::from(host.clone()),
2183 Attribute::all(),
2184 )
2185 .property(
2186 js_string!("hostname"),
2187 JsString::from(host),
2188 Attribute::all(),
2189 )
2190 .property(
2191 js_string!("pathname"),
2192 JsString::from(pathname),
2193 Attribute::all(),
2194 )
2195 .property(
2196 js_string!("search"),
2197 JsString::from(search),
2198 Attribute::all(),
2199 )
2200 .property(js_string!("hash"), JsString::from(hash), Attribute::all())
2201 .function(
2206 NativeFunction::from_fn_ptr(location_assign),
2207 js_string!("assign"),
2208 1,
2209 )
2210 .function(
2211 NativeFunction::from_fn_ptr(location_assign),
2212 js_string!("replace"),
2213 1,
2214 )
2215 .function(
2216 NativeFunction::from_fn_ptr(location_reload),
2217 js_string!("reload"),
2218 0,
2219 )
2220 .function(
2221 NativeFunction::from_fn_ptr(location_to_string),
2222 js_string!("toString"),
2223 0,
2224 )
2225 .build()
2226 .into()
2227}
2228
2229fn location_assign(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2233 let ctx = dom_ctx(context)?;
2234 let url = crate::dom::to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
2235 if !ctx.doc.borrow().navigate_to_url(&url) {
2236 return Err(crate::dom_exception::error(
2237 "SyntaxError",
2238 &format!("{url} is not a valid URL"),
2239 context,
2240 ));
2241 }
2242 Ok(JsValue::undefined())
2243}
2244
2245fn location_reload(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2246 let ctx = dom_ctx(context)?;
2247 let doc = ctx.doc.borrow();
2248 let current = doc.current_url();
2249 doc.navigate_to_url(¤t);
2250 Ok(JsValue::undefined())
2251}
2252
2253fn location_to_string(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2258 match this.as_object() {
2259 Some(object) => object.get(js_string!("href"), context),
2260 None => Ok(js_string!("").into()),
2261 }
2262}
2263
2264fn window_inner_width(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2265 let ctx = dom_ctx(context)?;
2266 let viewport = ctx.doc.borrow().get_viewport();
2267 Ok(JsValue::from(
2268 viewport.window_size.0 as f64 / viewport.scale_f64(),
2269 ))
2270}
2271
2272fn window_inner_height(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2273 let ctx = dom_ctx(context)?;
2274 let viewport = ctx.doc.borrow().get_viewport();
2275 Ok(JsValue::from(
2276 viewport.window_size.1 as f64 / viewport.scale_f64(),
2277 ))
2278}
2279
2280fn window_device_pixel_ratio(
2281 _: &JsValue,
2282 _: &[JsValue],
2283 context: &mut Context,
2284) -> JsResult<JsValue> {
2285 let ctx = dom_ctx(context)?;
2286 Ok(JsValue::from(ctx.doc.borrow().get_viewport().scale_f64()))
2287}
2288
2289fn timer_args(
2292 args: &[JsValue],
2293 context: &mut Context,
2294) -> JsResult<Option<(JsObject, Duration, Vec<JsValue>)>> {
2295 let Some(callback) = args
2296 .first()
2297 .and_then(|value| value.as_object())
2298 .filter(|obj| obj.is_callable())
2299 else {
2300 return Ok(None);
2301 };
2302 let delay_ms = match args.get(1) {
2303 Some(value) => value.to_number(context)?,
2304 None => 0.0,
2305 };
2306 let delay_ms = if delay_ms.is_finite() && delay_ms > 0.0 {
2307 delay_ms
2308 } else {
2309 0.0
2310 };
2311 let rest: Vec<JsValue> = args.iter().skip(2).cloned().collect();
2312 Ok(Some((
2313 callback,
2314 Duration::from_secs_f64(delay_ms / 1000.0),
2315 rest,
2316 )))
2317}
2318
2319fn set_timeout(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2320 let ctx = dom_ctx(context)?;
2321 let Some((callback, delay, rest)) = timer_args(args, context)? else {
2322 return Ok(JsValue::from(0));
2323 };
2324 let id = ctx
2325 .state
2326 .borrow_mut()
2327 .timers
2328 .add(delay, None, callback, rest);
2329 Ok(JsValue::from(id as f64))
2330}
2331
2332fn set_interval(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2333 let ctx = dom_ctx(context)?;
2334 let Some((callback, delay, rest)) = timer_args(args, context)? else {
2335 return Ok(JsValue::from(0));
2336 };
2337 let id = ctx
2338 .state
2339 .borrow_mut()
2340 .timers
2341 .add(delay, Some(delay), callback, rest);
2342 Ok(JsValue::from(id as f64))
2343}
2344
2345fn request_animation_frame(
2346 _: &JsValue,
2347 args: &[JsValue],
2348 context: &mut Context,
2349) -> JsResult<JsValue> {
2350 let ctx = dom_ctx(context)?;
2351 let Some(callback) = args
2352 .first()
2353 .and_then(|value| value.as_object())
2354 .filter(|obj| obj.is_callable())
2355 else {
2356 return Ok(JsValue::from(0));
2357 };
2358 let id = ctx.state.borrow_mut().timers.add_animation_frame(callback);
2359 Ok(JsValue::from(id as f64))
2360}
2361
2362fn clear_timer(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2363 let ctx = dom_ctx(context)?;
2364 let id = match args.first() {
2365 Some(value) => value.to_number(context)?,
2366 None => return Ok(JsValue::undefined()),
2367 };
2368 if id.is_finite() && id >= 0.0 {
2369 ctx.state.borrow_mut().timers.remove(id as u64);
2370 }
2371 Ok(JsValue::undefined())
2372}
2373
2374pub(crate) fn window_dispatch_event(
2380 _: &JsValue,
2381 args: &[JsValue],
2382 context: &mut Context,
2383) -> JsResult<JsValue> {
2384 dispatch_window_supplied_event(args, true, context)
2385}
2386
2387fn window_dispatch_history_event(
2388 _: &JsValue,
2389 args: &[JsValue],
2390 context: &mut Context,
2391) -> JsResult<JsValue> {
2392 let event = args
2393 .first()
2394 .and_then(JsValue::as_object)
2395 .filter(|event| event.downcast_ref::<EventRef>().is_some())
2396 .ok_or_else(|| JsNativeError::typ().with_message("History dispatch requires an Event"))?;
2397 crate::dom::define_value(&event, "isTrusted", JsValue::from(true), context);
2398 dispatch_window_supplied_event(args, false, context)
2399}
2400
2401fn dispatch_window_supplied_event(
2402 args: &[JsValue],
2403 scripted: bool,
2404 context: &mut Context,
2405) -> JsResult<JsValue> {
2406 let ctx = dom_ctx(context)?;
2407 let event = args
2408 .first()
2409 .and_then(JsValue::as_object)
2410 .filter(|event| event.downcast_ref::<EventRef>().is_some())
2411 .ok_or_else(|| JsNativeError::typ().with_message("dispatchEvent requires an Event"))?;
2412 let _dispatch = crate::dom::event::begin_dispatch(&event, scripted, context)?;
2413 let event_type = crate::dom::to_rust_string(&event.get(js_string!("type"), context)?, context)?;
2414
2415 set_event_path(&event, vec![context.global_object().clone()]);
2416 let global: JsValue = context.global_object().into();
2417 crate::dom::define_value(&event, "target", global.clone(), context);
2418 crate::dom::define_value(&event, "srcElement", global.clone(), context);
2419 crate::dom::define_value(&event, "currentTarget", global.clone(), context);
2420 crate::dom::define_value(&event, "eventPhase", JsValue::from(2), context);
2421
2422 let listeners: Vec<Listener> = {
2423 let mut state = ctx.state.borrow_mut();
2424 match state.window_listeners.get_mut(&event_type) {
2425 Some(listeners) => {
2426 let cloned = listeners.clone();
2427 listeners.retain(|listener| !listener.once);
2428 cloned
2429 }
2430 None => Vec::new(),
2431 }
2432 };
2433 for listener in listeners {
2434 crate::dom::passive::invoke(
2435 &listener.callback,
2436 &global,
2437 &event,
2438 listener.passive,
2439 context,
2440 )?;
2441 if event
2442 .downcast_ref::<EventRef>()
2443 .is_some_and(|event| event.stopped_immediate.get())
2444 {
2445 break;
2446 }
2447 }
2448
2449 if !event
2451 .downcast_ref::<EventRef>()
2452 .is_some_and(|event| event.stopped_immediate.get())
2453 {
2454 let window = context.global_object().clone();
2455 crate::dom::inline_handlers::invoke(&ctx, &window, &event, &event_type, context);
2456 }
2457
2458 crate::dom::define_value(&event, "currentTarget", JsValue::null(), context);
2459 let prevented = event
2460 .downcast_ref::<EventRef>()
2461 .is_some_and(|event| event.prevented.get());
2462 Ok(JsValue::from(!prevented))
2463}
2464
2465pub(crate) fn window_add_event_listener(
2466 _: &JsValue,
2467 args: &[JsValue],
2468 context: &mut Context,
2469) -> JsResult<JsValue> {
2470 let ctx = dom_ctx(context)?;
2471 let event_type =
2472 crate::dom::to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
2473 let Some(callback) = args
2474 .get(1)
2475 .and_then(|value| value.as_object())
2476 .filter(|obj| obj.is_callable())
2477 else {
2478 return Ok(JsValue::undefined());
2479 };
2480
2481 let window: JsValue = context.global_object().into();
2482 let passive = crate::dom::passive::resolve(&window, args, &event_type, context)?;
2483 let mut state = ctx.state.borrow_mut();
2484 let listeners = state.window_listeners.entry(event_type).or_default();
2485 if !listeners
2486 .iter()
2487 .any(|l| JsObject::equals(&l.callback, &callback))
2488 {
2489 listeners.push(Listener {
2490 callback,
2491 once: false,
2492 passive,
2493 });
2494 }
2495 Ok(JsValue::undefined())
2496}
2497
2498pub(crate) fn window_remove_event_listener(
2499 _: &JsValue,
2500 args: &[JsValue],
2501 context: &mut Context,
2502) -> JsResult<JsValue> {
2503 let ctx = dom_ctx(context)?;
2504 let event_type =
2505 crate::dom::to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
2506 let Some(callback) = args.get(1).and_then(|value| value.as_object()) else {
2507 return Ok(JsValue::undefined());
2508 };
2509
2510 let mut state = ctx.state.borrow_mut();
2511 if let Some(listeners) = state.window_listeners.get_mut(&event_type) {
2512 listeners.retain(|l| !JsObject::equals(&l.callback, &callback));
2513 }
2514 Ok(JsValue::undefined())
2515}