Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add 'dom-ready' event to WebFrameMain #30801

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 16 additions & 6 deletions docs/api/web-contents.md
Expand Up @@ -114,7 +114,7 @@ Returns:

* `event` Event

Emitted when the document in the given frame is loaded.
Emitted when the document in the top-level frame is loaded.

#### Event: 'page-title-updated'

Expand Down Expand Up @@ -856,6 +856,16 @@ Emitted when the `WebContents` preferred size has changed.
This event will only be emitted when `enablePreferredSizeMode` is set to `true`
in `webPreferences`.

#### Event: 'frame-created'

Returns:

* `event` Event
* `details` Object
* `frame` WebFrameMain

Emitted when the [mainFrame](web-contents.md#contentsmainframe-readonly), an `<iframe>`, or a nested `<iframe>` is loaded within the page.

### Instance Methods

#### `contents.loadURL(url[, options])`
Expand Down Expand Up @@ -1992,11 +2002,6 @@ when the DevTools has been closed.

A [`Debugger`](debugger.md) instance for this webContents.

[keyboardevent]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
[event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter
[SCA]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[`postMessage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

#### `contents.backgroundThrottling`

A `Boolean` property that determines whether or not this WebContents will throttle animations and timers
Expand All @@ -2005,3 +2010,8 @@ when the page becomes backgrounded. This also affects the Page Visibility API.
#### `contents.mainFrame` _Readonly_

A [`WebFrameMain`](web-frame-main.md) property that represents the top frame of the page's frame hierarchy.

[keyboardevent]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
[event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter
[SCA]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[`postMessage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
6 changes: 6 additions & 0 deletions docs/api/web-frame-main.md
Expand Up @@ -71,6 +71,12 @@ or `undefined` if there is no WebFrameMain associated with the given IDs.
Process: [Main](../glossary.md#main-process)<br />
_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._

### Instance Events

#### Event: 'dom-ready'

Emitted when the document is loaded.

### Instance Methods

#### `frame.executeJavaScript(code[, userGesture])`
Expand Down
30 changes: 28 additions & 2 deletions shell/browser/api/electron_api_web_contents.cc
Expand Up @@ -1399,6 +1399,29 @@ void WebContents::HandleNewRenderFrame(
void WebContents::RenderFrameCreated(
content::RenderFrameHost* render_frame_host) {
HandleNewRenderFrame(render_frame_host);

// RenderFrameCreated is called for speculative frames which may not be
// used in certain cross-origin navigations. Invoking
// RenderFrameHost::GetLifecycleState currently crashes when called for
// speculative frames so we need to filter it out for now. Check
// https://crbug.com/1183639 for details on when this can be removed.
auto* rfh_impl =
static_cast<content::RenderFrameHostImpl*>(render_frame_host);
if (rfh_impl->lifecycle_state() ==
content::RenderFrameHostImpl::LifecycleStateImpl::kSpeculative) {
return;
}

content::RenderFrameHost::LifecycleState lifecycle_state =
render_frame_host->GetLifecycleState();
if (lifecycle_state == content::RenderFrameHost::LifecycleState::kActive) {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
gin_helper::Dictionary details =
gin_helper::Dictionary::CreateEmpty(isolate);
details.SetGetter("frame", render_frame_host);
Emit("frame-created", details);
}
}

void WebContents::RenderFrameDeleted(
Expand All @@ -1425,12 +1448,11 @@ void WebContents::RenderFrameHostChanged(content::RenderFrameHost* old_host,
// If an instance of WebFrameMain exists, it will need to have its RFH
// swapped as well.
//
// |old_host| can be a nullptr in so we use |new_host| for looking up the
// |old_host| can be a nullptr so we use |new_host| for looking up the
// WebFrameMain instance.
auto* web_frame =
WebFrameMain::FromFrameTreeNodeId(new_host->GetFrameTreeNodeId());
if (web_frame) {
CHECK_EQ(web_frame->render_frame_host(), old_host);
web_frame->UpdateRenderFrameHost(new_host);
}
}
Expand Down Expand Up @@ -1517,6 +1539,10 @@ void WebContents::DidAcquireFullscreen(content::RenderFrameHost* rfh) {

void WebContents::DOMContentLoaded(
content::RenderFrameHost* render_frame_host) {
auto* web_frame = WebFrameMain::FromRenderFrameHost(render_frame_host);
if (web_frame)
web_frame->DOMContentLoaded();

if (!render_frame_host->GetParent())
Emit("dom-ready");
}
Expand Down
4 changes: 4 additions & 0 deletions shell/browser/api/electron_api_web_frame_main.cc
Expand Up @@ -317,6 +317,10 @@ void WebFrameMain::Connect() {
}
}

void WebFrameMain::DOMContentLoaded() {
Emit("dom-ready");
}

// static
gin::Handle<WebFrameMain> WebFrameMain::New(v8::Isolate* isolate) {
return gin::Handle<WebFrameMain>();
Expand Down
5 changes: 4 additions & 1 deletion shell/browser/api/electron_api_web_frame_main.h
Expand Up @@ -13,6 +13,7 @@
#include "gin/handle.h"
#include "gin/wrappable.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "shell/browser/event_emitter_mixin.h"
#include "shell/common/gin_helper/constructible.h"
#include "shell/common/gin_helper/pinnable.h"
#include "third_party/blink/public/mojom/page/page_visibility_state.mojom-forward.h"
Expand All @@ -35,6 +36,7 @@ class WebContents;

// Bindings for accessing frames from the main process.
class WebFrameMain : public gin::Wrappable<WebFrameMain>,
public gin_helper::EventEmitterMixin<WebFrameMain>,
public gin_helper::Pinnable<WebFrameMain>,
public gin_helper::Constructible<WebFrameMain> {
public:
Expand Down Expand Up @@ -80,7 +82,6 @@ class WebFrameMain : public gin::Wrappable<WebFrameMain>,
// WebFrameMain can outlive its RenderFrameHost pointer so we need to check
// whether its been disposed of prior to accessing it.
bool CheckRenderFrame() const;
void Connect();

v8::Local<v8::Promise> ExecuteJavaScript(gin::Arguments* args,
const std::u16string& code);
Expand Down Expand Up @@ -108,6 +109,8 @@ class WebFrameMain : public gin::Wrappable<WebFrameMain>,
std::vector<content::RenderFrameHost*> FramesInSubtree() const;

void OnRendererConnectionError();
void Connect();
void DOMContentLoaded();

mojo::Remote<mojom::ElectronRenderer> renderer_api_;
mojo::PendingReceiver<mojom::ElectronRenderer> pending_receiver_;
Expand Down
68 changes: 68 additions & 0 deletions shell/common/gin_converters/frame_converter.cc
Expand Up @@ -5,10 +5,18 @@
#include "shell/common/gin_converters/frame_converter.h"

#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "shell/browser/api/electron_api_web_frame_main.h"
#include "shell/common/gin_helper/accessor.h"

namespace gin {

namespace {

v8::Persistent<v8::ObjectTemplate> rfh_templ;

} // namespace

// static
v8::Local<v8::Value> Converter<content::RenderFrameHost*>::ToV8(
v8::Isolate* isolate,
Expand All @@ -18,4 +26,64 @@ v8::Local<v8::Value> Converter<content::RenderFrameHost*>::ToV8(
return electron::api::WebFrameMain::From(isolate, val).ToV8();
}

// static
v8::Local<v8::Value>
Converter<gin_helper::AccessorValue<content::RenderFrameHost*>>::ToV8(
v8::Isolate* isolate,
gin_helper::AccessorValue<content::RenderFrameHost*> val) {
content::RenderFrameHost* rfh = val.Value;
if (!rfh)
return v8::Null(isolate);

const int process_id = rfh->GetProcess()->GetID();
const int routing_id = rfh->GetRoutingID();

if (rfh_templ.IsEmpty()) {
v8::EscapableHandleScope inner(isolate);
v8::Local<v8::ObjectTemplate> local = v8::ObjectTemplate::New(isolate);
local->SetInternalFieldCount(2);
rfh_templ.Reset(isolate, inner.Escape(local));
}

v8::Local<v8::Object> rfh_obj =
v8::Local<v8::ObjectTemplate>::New(isolate, rfh_templ)
->NewInstance(isolate->GetCurrentContext())
.ToLocalChecked();

rfh_obj->SetInternalField(0, v8::Number::New(isolate, process_id));
rfh_obj->SetInternalField(1, v8::Number::New(isolate, routing_id));

return rfh_obj;
}

// static
bool Converter<gin_helper::AccessorValue<content::RenderFrameHost*>>::FromV8(
v8::Isolate* isolate,
v8::Local<v8::Value> val,
gin_helper::AccessorValue<content::RenderFrameHost*>* out) {
v8::Local<v8::Object> rfh_obj;
if (!ConvertFromV8(isolate, val, &rfh_obj))
return false;

if (rfh_obj->InternalFieldCount() != 2)
return false;

v8::Local<v8::Value> process_id_wrapper = rfh_obj->GetInternalField(0);
v8::Local<v8::Value> routing_id_wrapper = rfh_obj->GetInternalField(1);

if (process_id_wrapper.IsEmpty() || !process_id_wrapper->IsNumber() ||
routing_id_wrapper.IsEmpty() || !routing_id_wrapper->IsNumber())
return false;

const int process_id = process_id_wrapper.As<v8::Number>()->Value();
const int routing_id = routing_id_wrapper.As<v8::Number>()->Value();

auto* rfh = content::RenderFrameHost::FromID(process_id, routing_id);
if (!rfh)
return false;

out->Value = rfh;
return true;
}

} // namespace gin
11 changes: 11 additions & 0 deletions shell/common/gin_converters/frame_converter.h
Expand Up @@ -6,6 +6,7 @@
#define SHELL_COMMON_GIN_CONVERTERS_FRAME_CONVERTER_H_

#include "gin/converter.h"
#include "shell/common/gin_helper/accessor.h"

namespace content {
class RenderFrameHost;
Expand All @@ -19,6 +20,16 @@ struct Converter<content::RenderFrameHost*> {
content::RenderFrameHost* val);
};

template <>
struct Converter<gin_helper::AccessorValue<content::RenderFrameHost*>> {
static v8::Local<v8::Value> ToV8(
v8::Isolate* isolate,
gin_helper::AccessorValue<content::RenderFrameHost*> val);
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
gin_helper::AccessorValue<content::RenderFrameHost*>* out);
};

} // namespace gin

#endif // SHELL_COMMON_GIN_CONVERTERS_FRAME_CONVERTER_H_
27 changes: 27 additions & 0 deletions shell/common/gin_helper/accessor.h
@@ -0,0 +1,27 @@
// Copyright (c) 2021 Samuel Maddock <sam@samuelmaddock.com>.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#ifndef SHELL_COMMON_GIN_HELPER_ACCESSOR_H_
#define SHELL_COMMON_GIN_HELPER_ACCESSOR_H_

namespace gin_helper {

// Wrapper for a generic value to be used as an accessor in a
// gin_helper::Dictionary.
template <typename T>
struct AccessorValue {
T Value;
};
template <typename T>
struct AccessorValue<const T&> {
T Value;
};
template <typename T>
struct AccessorValue<const T*> {
T* Value;
};

} // namespace gin_helper

#endif // SHELL_COMMON_GIN_HELPER_ACCESSOR_H_
31 changes: 31 additions & 0 deletions shell/common/gin_helper/dictionary.h
Expand Up @@ -10,6 +10,7 @@

#include "gin/dictionary.h"
#include "shell/common/gin_converters/std_converter.h"
#include "shell/common/gin_helper/accessor.h"
#include "shell/common/gin_helper/function_template.h"
#include "third_party/abseil-cpp/absl/types/optional.h"

Expand Down Expand Up @@ -109,6 +110,36 @@ class Dictionary : public gin::Dictionary {
.ToChecked();
}

template <typename K, typename V>
bool SetGetter(const K& key, const V& val) {
AccessorValue<V> acc_value;
acc_value.Value = val;

v8::Local<v8::Value> v8_value_accessor;
if (!gin::TryConvertToV8(isolate(), acc_value, &v8_value_accessor))
return false;

auto context = isolate()->GetCurrentContext();

return GetHandle()
->SetAccessor(
context, gin::StringToV8(isolate(), key),
[](v8::Local<v8::Name> property_name,
const v8::PropertyCallbackInfo<v8::Value>& info) {
AccessorValue<V> acc_value;
if (!gin::ConvertFromV8(info.GetIsolate(), info.Data(),
&acc_value))
return;

V val = acc_value.Value;
v8::Local<v8::Value> v8_value;
if (gin::TryConvertToV8(info.GetIsolate(), val, &v8_value))
info.GetReturnValue().Set(v8_value);
},
NULL, v8_value_accessor)
.ToChecked();
}

template <typename T>
bool SetReadOnly(base::StringPiece key, const T& val) {
v8::Local<v8::Value> v8_value;
Expand Down