Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello from script in the shadow root of the just inserted div!
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Received a message: 'Hello from iframe in the shadow root of the just inserted div!'
Was it from the shadow root iframe? true
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
test(() => {
const div = document.createElement("div");
const shadowRoot = div.attachShadow({ mode: "closed" });

const script = document.createElement("script");
script.innerText = "println('Hello from script in the shadow root of the just inserted div!')";
shadowRoot.appendChild(script);

document.body.appendChild(div);
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
asyncTest((done) => {
const div = document.createElement("div");
const shadowRoot = div.attachShadow({ mode: "closed" });

const iframe = document.createElement("iframe");

window.addEventListener("message", (messageEvent) => {
println(`Received a message: '${messageEvent.data}'`);
println(`Was it from the shadow root iframe? ${messageEvent.source === iframe.contentWindow}`);
done();
});

iframe.srcdoc = `
\u003cscript\u003e
window.parent.postMessage("Hello from iframe in the shadow root of the just inserted div!");
\u003c/script\u003e
`;
shadowRoot.appendChild(iframe);

document.body.appendChild(div);
});
</script>
7 changes: 4 additions & 3 deletions Userland/Libraries/LibWeb/CSS/MediaQueryList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ bool MediaQueryList::evaluate()
}

// https://www.w3.org/TR/cssom-view/#dom-mediaquerylist-addlistener
void MediaQueryList::add_listener(DOM::IDLEventListener* listener)
void MediaQueryList::add_listener(JS::GCPtr<DOM::IDLEventListener> listener)
{
// 1. If listener is null, terminate these steps.
if (!listener)
Expand All @@ -87,12 +87,13 @@ void MediaQueryList::add_listener(DOM::IDLEventListener* listener)
}

// https://www.w3.org/TR/cssom-view/#dom-mediaquerylist-removelistener
void MediaQueryList::remove_listener(DOM::IDLEventListener* listener)
void MediaQueryList::remove_listener(JS::GCPtr<DOM::IDLEventListener> listener)
{
// 1. Remove an event listener from the associated list of event listeners, whose type is change, callback is listener, and capture is false.
// NOTE: While the spec doesn't technically use remove_event_listener and instead manipulates the list directly, every major engine uses remove_event_listener.
// This means if an event listener removes another event listener that comes after it, the removed event listener will not be invoked.
remove_event_listener_without_options(HTML::EventNames::change, *listener);
if (listener)
remove_event_listener_without_options(HTML::EventNames::change, *listener);
}

void MediaQueryList::set_onchange(WebIDL::CallbackType* event_handler)
Expand Down
4 changes: 2 additions & 2 deletions Userland/Libraries/LibWeb/CSS/MediaQueryList.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ class MediaQueryList final : public DOM::EventTarget {
bool matches() const;
bool evaluate();

void add_listener(DOM::IDLEventListener*);
void remove_listener(DOM::IDLEventListener*);
void add_listener(JS::GCPtr<DOM::IDLEventListener>);
void remove_listener(JS::GCPtr<DOM::IDLEventListener>);

void set_onchange(WebIDL::CallbackType*);
WebIDL::CallbackType* onchange();
Expand Down
34 changes: 22 additions & 12 deletions Userland/Libraries/LibWeb/DOM/ShadowRoot.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,34 +97,44 @@ class ShadowRoot final : public DocumentFragment {
template<>
inline bool Node::fast_is<ShadowRoot>() const { return node_type() == to_underlying(NodeType::DOCUMENT_FRAGMENT_NODE) && is_shadow_root(); }

// https://dom.spec.whatwg.org/#concept-shadow-including-tree-order
// In shadow-including tree order is shadow-including preorder, depth-first traversal of a node tree.
// Shadow-including preorder, depth-first traversal of a node tree tree is preorder, depth-first traversal
// of tree, with for each shadow host encountered in tree, shadow-including preorder, depth-first traversal
// of that element’s shadow root’s node tree just after it is encountered.

// https://dom.spec.whatwg.org/#concept-shadow-including-descendant
// An object A is a shadow-including descendant of an object B, if A is a descendant of B, or A’s root is a
// shadow root and A’s root’s host is a shadow-including inclusive descendant of B.

// https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-descendant
// A shadow-including inclusive descendant is an object or one of its shadow-including descendants.

template<typename Callback>
inline TraversalDecision Node::for_each_shadow_including_inclusive_descendant(Callback callback)
{
if (callback(*this) == TraversalDecision::Break)
return TraversalDecision::Break;
for (auto* child = first_child(); child; child = child->next_sibling()) {
if (child->is_element()) {
if (auto shadow_root = static_cast<Element*>(child)->shadow_root()) {
if (shadow_root->for_each_shadow_including_inclusive_descendant(callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}

if (is_element()) {
if (auto shadow_root = static_cast<Element*>(this)->shadow_root()) {
if (shadow_root->for_each_shadow_including_inclusive_descendant(callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}
}

for (auto* child = first_child(); child; child = child->next_sibling()) {
if (child->for_each_shadow_including_inclusive_descendant(callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}

return TraversalDecision::Continue;
}

template<typename Callback>
inline TraversalDecision Node::for_each_shadow_including_descendant(Callback callback)
{
for (auto* child = first_child(); child; child = child->next_sibling()) {
if (child->is_element()) {
if (JS::GCPtr<ShadowRoot> shadow_root = static_cast<Element*>(child)->shadow_root()) {
if (shadow_root->for_each_shadow_including_inclusive_descendant(callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}
}
if (child->for_each_shadow_including_inclusive_descendant(callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}
Expand Down
31 changes: 20 additions & 11 deletions Userland/Libraries/LibWeb/HTML/HTMLIFrameElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,26 @@ void HTMLIFrameElement::inserted()
{
HTMLElement::inserted();

// When an iframe element element is inserted into a document whose browsing context is non-null, the user agent must run these steps:
if (in_a_document_tree() && document().browsing_context() && document().is_fully_active()) {
// 1. Create a new child navigable for element.
MUST(create_new_child_navigable(JS::create_heap_function(realm().heap(), [this] {
// 3. Process the iframe attributes for element, with initialInsertion set to true.
process_the_iframe_attributes(true);
set_content_navigable_initialized();
})));

// FIXME: 2. If element has a sandbox attribute, then parse the sandboxing directive given the attribute's value and element's iframe sandboxing flag set.
}
// The iframe HTML element insertion steps, given insertedNode, are:
// 1. If insertedNode's shadow-including root's browsing context is null, then return.
if (!is<DOM::Document>(shadow_including_root()))
return;

DOM::Document& document = verify_cast<DOM::Document>(shadow_including_root());

// NOTE: The check for "not fully active" is to prevent a crash on the dom/nodes/node-appendchild-crash.html WPT test.
if (!document.browsing_context() || !document.is_fully_active())
return;

// 2. Create a new child navigable for insertedNode.
MUST(create_new_child_navigable(JS::create_heap_function(realm().heap(), [this] {
// FIXME: 3. If insertedNode has a sandbox attribute, then parse the sandboxing directive given the attribute's
// value and insertedNode's iframe sandboxing flag set.

// 4. Process the iframe attributes for insertedNode, with initialInsertion set to true.
process_the_iframe_attributes(true);
set_content_navigable_initialized();
})));
}

// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#process-the-iframe-attributes
Expand Down
34 changes: 22 additions & 12 deletions Userland/Libraries/LibWeb/WebAudio/AudioContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/HTML/HTMLMediaElement.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/WebAudio/AudioContext.h>
#include <LibWeb/WebIDL/Promise.h>
Expand Down Expand Up @@ -92,7 +93,6 @@ void AudioContext::initialize(JS::Realm& realm)
void AudioContext::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_pending_promises);
visitor.visit(m_pending_resume_promises);
}

Expand All @@ -107,7 +107,6 @@ AudioTimestamp AudioContext::get_output_timestamp()
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::resume()
{
auto& realm = this->realm();
auto& vm = realm.vm();

// 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException.
auto const& associated_document = verify_cast<HTML::Window>(HTML::relevant_global_object(*this)).associated_document();
Expand All @@ -128,8 +127,8 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::resume()

// 5. If the context is not allowed to start, append promise to [[pending promises]] and [[pending resume promises]] and abort these steps, returning promise.
if (m_allowed_to_start) {
TRY_OR_THROW_OOM(vm, m_pending_promises.try_append(promise));
TRY_OR_THROW_OOM(vm, m_pending_resume_promises.try_append(promise));
m_pending_promises.append(promise);
m_pending_resume_promises.append(promise);
}

// 6. Set the [[control thread state]] on the AudioContext to running.
Expand All @@ -147,28 +146,36 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::resume()
if (!start_rendering_audio_graph()) {
// 7.4: In case of failure, queue a media element task to execute the following steps:
queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, this]() {
HTML::TemporaryExecutionContext context(Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);

// 7.4.1: Reject all promises from [[pending resume promises]] in order, then clear [[pending resume promises]].
for (auto const& promise : m_pending_resume_promises) {
WebIDL::reject_promise(realm, promise, JS::js_null());

// 7.4.2: Additionally, remove those promises from [[pending promises]].
m_pending_promises.remove_first_matching([&promise](auto& pending_promise) {
return pending_promise == promise;
});
}
m_pending_resume_promises.clear();

// FIXME: 7.4.2: Additionally, remove those promises from [[pending promises]].
}));
}

// 7.5: queue a media element task to execute the following steps:
queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, promise, this]() {
HTML::TemporaryExecutionContext context(Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);

// 7.5.1: Resolve all promises from [[pending resume promises]] in order.
// 7.5.2: Clear [[pending resume promises]]. Additionally, remove those promises from
// [[pending promises]].
for (auto const& pending_resume_promise : m_pending_resume_promises) {
*pending_resume_promise->resolve();
m_pending_promises.remove_first_matching([&pending_resume_promise](auto& pending_promise) {
return pending_promise == pending_resume_promise;
});
}

// 7.5.2: Clear [[pending resume promises]].
m_pending_resume_promises.clear();

// FIXME: Additionally, remove those promises from [[pending promises]].

// 7.5.3: Resolve promise.
*promise->resolve();

Expand All @@ -192,7 +199,6 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::resume()
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::suspend()
{
auto& realm = this->realm();
auto& vm = realm.vm();

// 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException.
auto const& associated_document = verify_cast<HTML::Window>(HTML::relevant_global_object(*this)).associated_document();
Expand All @@ -209,7 +215,7 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::suspend()
}

// 4. Append promise to [[pending promises]].
TRY_OR_THROW_OOM(vm, m_pending_promises.try_append(promise));
m_pending_promises.append(promise);

// 5. Set [[suspended by user]] to true.
m_suspended_by_user = true;
Expand All @@ -227,6 +233,8 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::suspend()

// 7.3: queue a media element task to execute the following steps:
queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, promise, this]() {
HTML::TemporaryExecutionContext context(Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);

// 7.3.1: Resolve promise.
*promise->resolve();

Expand Down Expand Up @@ -280,6 +288,8 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::close()

// 5.4: queue a media element task to execute the following steps:
queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, promise, this]() {
HTML::TemporaryExecutionContext context(Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);

// 5.4.1: Resolve promise.
*promise->resolve();

Expand Down
1 change: 0 additions & 1 deletion Userland/Libraries/LibWeb/WebAudio/AudioContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ class AudioContext final : public BaseAudioContext {
double m_output_latency { 0 };

bool m_allowed_to_start = true;
Vector<JS::NonnullGCPtr<WebIDL::Promise>> m_pending_promises;
Vector<JS::NonnullGCPtr<WebIDL::Promise>> m_pending_resume_promises;
bool m_suspended_by_user = false;

Expand Down
15 changes: 11 additions & 4 deletions Userland/Libraries/LibWeb/WebAudio/BaseAudioContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ void BaseAudioContext::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_destination);
visitor.visit(m_pending_promises);
}

void BaseAudioContext::set_onstatechange(WebIDL::CallbackType* event_handler)
Expand Down Expand Up @@ -144,7 +145,8 @@ JS::NonnullGCPtr<JS::Promise> BaseAudioContext::decode_audio_data(JS::Handle<Web

// FIXME: 3. If audioData is detached, execute the following steps:
if (true) {
// FIXME: 3.1. Append promise to [[pending promises]].
// 3.1. Append promise to [[pending promises]].
m_pending_promises.append(promise);

// FIXME: 3.2. Detach the audioData ArrayBuffer. If this operations throws, jump to the step 3.

Expand All @@ -159,6 +161,9 @@ JS::NonnullGCPtr<JS::Promise> BaseAudioContext::decode_audio_data(JS::Handle<Web

// 4.2. Reject promise with error, and remove it from [[pending promises]].
WebIDL::reject_promise(realm, promise, error);
m_pending_promises.remove_first_matching([&promise](auto& pending_promise) {
return pending_promise == promise;
});

// 4.3. Queue a media element task to invoke errorCallback with error.
if (error_callback) {
Expand Down Expand Up @@ -201,13 +206,15 @@ void BaseAudioContext::queue_a_decoding_operation(JS::NonnullGCPtr<JS::PromiseCa
// 4. If can decode is false,
if (!can_decode) {
// queue a media element task to execute the following steps:
queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, promise, error_callback] {
queue_a_media_element_task(JS::create_heap_function(heap(), [this, &realm, promise, error_callback] {
// 4.1. Let error be a DOMException whose name is EncodingError.
auto error = WebIDL::EncodingError::create(realm, "Unable to decode."_string);

// 4.1.2. Reject promise with error,
// 4.1.2. Reject promise with error, and remove it from [[pending promises]].
WebIDL::reject_promise(realm, promise, error);
// FIXME: and remove it from [[pending promises]].
m_pending_promises.remove_first_matching([&promise](auto& pending_promise) {
return pending_promise == promise;
});

// 4.2. If errorCallback is not missing, invoke errorCallback with error.
if (error_callback) {
Expand Down
1 change: 1 addition & 0 deletions Userland/Libraries/LibWeb/WebAudio/BaseAudioContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class BaseAudioContext : public DOM::EventTarget {
virtual void visit_edges(Cell::Visitor&) override;

JS::NonnullGCPtr<AudioDestinationNode> m_destination;
Vector<JS::NonnullGCPtr<WebIDL::Promise>> m_pending_promises;

private:
void queue_a_decoding_operation(JS::NonnullGCPtr<JS::PromiseCapability>, JS::Handle<WebIDL::BufferSource>, JS::GCPtr<WebIDL::CallbackType>, JS::GCPtr<WebIDL::CallbackType>);
Expand Down