/src/serenity/Userland/Libraries/LibWeb/HTML/NavigateEvent.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <LibJS/Console.h> |
8 | | #include <LibJS/Heap/Heap.h> |
9 | | #include <LibJS/Runtime/ConsoleObject.h> |
10 | | #include <LibJS/Runtime/Promise.h> |
11 | | #include <LibJS/Runtime/Realm.h> |
12 | | #include <LibWeb/Bindings/Intrinsics.h> |
13 | | #include <LibWeb/Bindings/NavigateEventPrototype.h> |
14 | | #include <LibWeb/DOM/AbortController.h> |
15 | | #include <LibWeb/DOM/AbortSignal.h> |
16 | | #include <LibWeb/DOM/Document.h> |
17 | | #include <LibWeb/HTML/Focus.h> |
18 | | #include <LibWeb/HTML/NavigateEvent.h> |
19 | | #include <LibWeb/HTML/Navigation.h> |
20 | | #include <LibWeb/HTML/NavigationDestination.h> |
21 | | #include <LibWeb/HTML/Window.h> |
22 | | #include <LibWeb/XHR/FormData.h> |
23 | | |
24 | | namespace Web::HTML { |
25 | | |
26 | | JS_DEFINE_ALLOCATOR(NavigateEvent); |
27 | | |
28 | | JS::NonnullGCPtr<NavigateEvent> NavigateEvent::construct_impl(JS::Realm& realm, FlyString const& event_name, NavigateEventInit const& event_init) |
29 | 0 | { |
30 | 0 | return realm.heap().allocate<NavigateEvent>(realm, realm, event_name, event_init); |
31 | 0 | } |
32 | | |
33 | | NavigateEvent::NavigateEvent(JS::Realm& realm, FlyString const& event_name, NavigateEventInit const& event_init) |
34 | 0 | : DOM::Event(realm, event_name, event_init) |
35 | 0 | , m_navigation_type(event_init.navigation_type) |
36 | 0 | , m_destination(*event_init.destination) |
37 | 0 | , m_can_intercept(event_init.can_intercept) |
38 | 0 | , m_user_initiated(event_init.user_initiated) |
39 | 0 | , m_hash_change(event_init.hash_change) |
40 | 0 | , m_signal(*event_init.signal) |
41 | 0 | , m_form_data(event_init.form_data) |
42 | 0 | , m_download_request(event_init.download_request) |
43 | 0 | , m_info(event_init.info.value_or(JS::js_undefined())) |
44 | 0 | , m_has_ua_visual_transition(event_init.has_ua_visual_transition) |
45 | 0 | { |
46 | 0 | } |
47 | | |
48 | 0 | NavigateEvent::~NavigateEvent() = default; |
49 | | |
50 | | void NavigateEvent::initialize(JS::Realm& realm) |
51 | 0 | { |
52 | 0 | Base::initialize(realm); |
53 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(NavigateEvent); |
54 | 0 | } |
55 | | |
56 | | void NavigateEvent::visit_edges(JS::Cell::Visitor& visitor) |
57 | 0 | { |
58 | 0 | Base::visit_edges(visitor); |
59 | 0 | visitor.visit(m_navigation_handler_list); |
60 | 0 | visitor.visit(m_abort_controller); |
61 | 0 | visitor.visit(m_destination); |
62 | 0 | visitor.visit(m_signal); |
63 | 0 | visitor.visit(m_form_data); |
64 | 0 | visitor.visit(m_info); |
65 | 0 | } |
66 | | |
67 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-intercept |
68 | | WebIDL::ExceptionOr<void> NavigateEvent::intercept(NavigationInterceptOptions const& options) |
69 | 0 | { |
70 | 0 | auto& realm = this->realm(); |
71 | 0 | auto& vm = this->vm(); |
72 | | // The intercept(options) method steps are: |
73 | | |
74 | | // 1. Perform shared checks given this. |
75 | 0 | TRY(perform_shared_checks()); |
76 | | |
77 | | // 2. If this's canIntercept attribute was initialized to false, then throw a "SecurityError" DOMException. |
78 | 0 | if (!m_can_intercept) |
79 | 0 | return WebIDL::SecurityError::create(realm, "NavigateEvent cannot be intercepted"_string); |
80 | | |
81 | | // 3. If this's dispatch flag is unset, then throw an "InvalidStateError" DOMException. |
82 | 0 | if (!this->dispatched()) |
83 | 0 | return WebIDL::InvalidStateError::create(realm, "NavigationEvent is not dispatched yet"_string); |
84 | | |
85 | | // 4. Assert: this's interception state is either "none" or "intercepted". |
86 | 0 | VERIFY(m_interception_state == InterceptionState::None || m_interception_state == InterceptionState::Intercepted); |
87 | | |
88 | | // 5. Set this's interception state to "intercepted". |
89 | 0 | m_interception_state = InterceptionState::Intercepted; |
90 | | |
91 | | // 6. If options["handler"] exists, then append it to this's navigation handler list. |
92 | 0 | if (options.handler != nullptr) |
93 | 0 | TRY_OR_THROW_OOM(vm, m_navigation_handler_list.try_append(*options.handler)); |
94 | | |
95 | | // 7. If options["focusReset"] exists, then: |
96 | 0 | if (options.focus_reset.has_value()) { |
97 | | // 1. If this's focus reset behavior is not null, and it is not equal to options["focusReset"], |
98 | | // then the user agent may report a warning to the console indicating that the focusReset option |
99 | | // for a previous call to intercept() was overridden by this new value, and the previous value |
100 | | // will be ignored. |
101 | 0 | if (m_focus_reset_behavior.has_value() && *m_focus_reset_behavior != *options.focus_reset) { |
102 | 0 | auto& console = realm.intrinsics().console_object()->console(); |
103 | 0 | console.output_debug_message(JS::Console::LogLevel::Warn, |
104 | 0 | TRY_OR_THROW_OOM(vm, String::formatted("focusReset behavior on NavigationEvent overriden (was: {}, now: {})", *m_focus_reset_behavior, *options.focus_reset))); |
105 | 0 | } |
106 | | |
107 | | // 2. Set this's focus reset behavior to options["focusReset"]. |
108 | 0 | m_focus_reset_behavior = options.focus_reset; |
109 | 0 | } |
110 | | |
111 | | // 8. If options["scroll"] exists, then: |
112 | 0 | if (options.scroll.has_value()) { |
113 | | // 1. If this's scroll behavior is not null, and it is not equal to options["scroll"], then the user |
114 | | // agent may report a warning to the console indicating that the scroll option for a previous call |
115 | | // to intercept() was overridden by this new value, and the previous value will be ignored. |
116 | 0 | if (m_scroll_behavior.has_value() && *m_scroll_behavior != *options.scroll) { |
117 | 0 | auto& console = realm.intrinsics().console_object()->console(); |
118 | 0 | console.output_debug_message(JS::Console::LogLevel::Warn, |
119 | 0 | TRY_OR_THROW_OOM(vm, String::formatted("scroll option on NavigationEvent overriden (was: {}, now: {})", *m_scroll_behavior, *options.scroll))); |
120 | 0 | } |
121 | | |
122 | | // 2. Set this's scroll behavior to options["scroll"]. |
123 | 0 | m_scroll_behavior = options.scroll; |
124 | 0 | } |
125 | 0 | return {}; |
126 | 0 | } |
127 | | |
128 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-scroll |
129 | | WebIDL::ExceptionOr<void> NavigateEvent::scroll() |
130 | 0 | { |
131 | | // The scroll() method steps are: |
132 | | // 1. Perform shared checks given this. |
133 | 0 | TRY(perform_shared_checks()); |
134 | | |
135 | | // 2. If this's interception state is not "committed", then throw an "InvalidStateError" DOMException. |
136 | 0 | if (m_interception_state != InterceptionState::Committed) |
137 | 0 | return WebIDL::InvalidStateError::create(realm(), "Cannot scroll NavigationEvent that is not committed"_string); |
138 | | |
139 | | // 3. Process scroll behavior given this. |
140 | 0 | process_scroll_behavior(); |
141 | |
|
142 | 0 | return {}; |
143 | 0 | } |
144 | | |
145 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigateevent-perform-shared-checks |
146 | | WebIDL::ExceptionOr<void> NavigateEvent::perform_shared_checks() |
147 | 0 | { |
148 | | // To perform shared checks for a NavigateEvent event: |
149 | | |
150 | | // 1. If event's relevant global object's associated Document is not fully active, |
151 | | // then throw an "InvalidStateError" DOMException. |
152 | 0 | auto& associated_document = verify_cast<HTML::Window>(relevant_global_object(*this)).associated_document(); |
153 | 0 | if (!associated_document.is_fully_active()) |
154 | 0 | return WebIDL::InvalidStateError::create(realm(), "Document is not fully active"_string); |
155 | | |
156 | | // 2. If event's isTrusted attribute was initialized to false, then throw a "SecurityError" DOMException. |
157 | 0 | if (!this->is_trusted()) |
158 | 0 | return WebIDL::SecurityError::create(realm(), "NavigateEvent is not trusted"_string); |
159 | | |
160 | | // 3. If event's canceled flag is set, then throw an "InvalidStateError" DOMException. |
161 | 0 | if (this->cancelled()) |
162 | 0 | return WebIDL::InvalidStateError::create(realm(), "NavigateEvent already cancelled"_string); |
163 | | |
164 | 0 | return {}; |
165 | 0 | } |
166 | | |
167 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#process-scroll-behavior |
168 | | void NavigateEvent::process_scroll_behavior() |
169 | 0 | { |
170 | | // To process scroll behavior given a NavigateEvent event: |
171 | | |
172 | | // 1. Assert: event's interception state is "committed". |
173 | 0 | VERIFY(m_interception_state == InterceptionState::Committed); |
174 | | |
175 | | // 2. Set event's interception state to "scrolled". |
176 | 0 | m_interception_state = InterceptionState::Scrolled; |
177 | | |
178 | | // FIXME: 3. If event's navigationType was initialized to "traverse" or "reload", then restore scroll position data |
179 | | // given event's relevant global object's navigable's active session history entry. |
180 | 0 | if (m_navigation_type == Bindings::NavigationType::Traverse || m_navigation_type == Bindings::NavigationType::Reload) { |
181 | 0 | dbgln("FIXME: restore scroll position data after traversal or reload navigation"); |
182 | 0 | } |
183 | | |
184 | | // 4. Otherwise: |
185 | 0 | else { |
186 | | // 1. Let document be event's relevant global object's associated Document. |
187 | 0 | auto& document = verify_cast<HTML::Window>(relevant_global_object(*this)).associated_document(); |
188 | | |
189 | | // 2. If document's indicated part is null, then scroll to the beginning of the document given document. [CSSOMVIEW] |
190 | 0 | auto indicated_part = document.determine_the_indicated_part(); |
191 | 0 | if (indicated_part.has<DOM::Element*>() && indicated_part.get<DOM::Element*>() == nullptr) { |
192 | 0 | document.scroll_to_the_beginning_of_the_document(); |
193 | 0 | } |
194 | | |
195 | | // 3. Otherwise, scroll to the fragment given document. |
196 | 0 | else { |
197 | | // FIXME: This will re-determine the indicated part. Can we avoid this extra work? |
198 | 0 | document.scroll_to_the_fragment(); |
199 | 0 | } |
200 | 0 | } |
201 | 0 | } |
202 | | |
203 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#potentially-process-scroll-behavior |
204 | | void NavigateEvent::potentially_process_scroll_behavior() |
205 | 0 | { |
206 | | // 1. Assert: event's interception state is "committed" or "scrolled". |
207 | 0 | VERIFY(m_interception_state != InterceptionState::Committed && m_interception_state != InterceptionState::Scrolled); |
208 | | |
209 | | // 2. If event's interception state is "scrolled", then return. |
210 | 0 | if (m_interception_state == InterceptionState::Scrolled) |
211 | 0 | return; |
212 | | |
213 | | // 3. If event's scroll behavior is "manual", then return. |
214 | | // NOTE: If it was left as null, then we treat that as "after-transition", and continue onward. |
215 | 0 | if (m_scroll_behavior == Bindings::NavigationScrollBehavior::Manual) |
216 | 0 | return; |
217 | | |
218 | | // 4. Process scroll behavior given event. |
219 | 0 | process_scroll_behavior(); |
220 | 0 | } |
221 | | |
222 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#potentially-reset-the-focus |
223 | | void NavigateEvent::potentially_reset_the_focus() |
224 | 0 | { |
225 | | // 1. Assert: event's interception state is "committed" or "scrolled". |
226 | 0 | VERIFY(m_interception_state == InterceptionState::Committed || m_interception_state == InterceptionState::Scrolled); |
227 | | |
228 | | // 2. Let navigation be event's relevant global object's navigation API. |
229 | 0 | auto& relevant_global_object = verify_cast<Window>(HTML::relevant_global_object(*this)); |
230 | 0 | auto navigation = relevant_global_object.navigation(); |
231 | | |
232 | | // 3. Let focusChanged be navigation's focus changed during ongoing navigation. |
233 | 0 | auto focus_changed = navigation->focus_changed_during_ongoing_navigation(); |
234 | | |
235 | | // 4. Set navigation's focus changed during ongoing navigation to false. |
236 | 0 | navigation->set_focus_changed_during_ongoing_navigation(false); |
237 | | |
238 | | // 5. If focusChanged is true, then return. |
239 | 0 | if (focus_changed) |
240 | 0 | return; |
241 | | |
242 | | // 6. If event's focus reset behavior is "manual", then return. |
243 | | // NOTE: If it was left as null, then we treat that as "after-transition", and continue onward. |
244 | 0 | if (m_focus_reset_behavior == Bindings::NavigationFocusReset::Manual) |
245 | 0 | return; |
246 | | |
247 | | // 7. Let document be event's relevant global object's associated Document. |
248 | 0 | auto& document = relevant_global_object.associated_document(); |
249 | | |
250 | | // 8. FIXME: Let focusTarget be the autofocus delegate for document. |
251 | 0 | JS::GCPtr<DOM::Node> focus_target = nullptr; |
252 | | |
253 | | // 9. If focusTarget is null, then set focusTarget to document's body element. |
254 | 0 | if (focus_target == nullptr) |
255 | 0 | focus_target = document.body(); |
256 | | |
257 | | // 10. If focusTarget is null, then set focusTarget to document's document element. |
258 | 0 | if (focus_target == nullptr) |
259 | 0 | focus_target = document.document_element(); |
260 | | |
261 | | // FIXME: 11. Run the focusing steps for focusTarget, with document's viewport as the fallback target. |
262 | 0 | run_focusing_steps(focus_target, nullptr); |
263 | | |
264 | | // FIXME: 12. Move the sequential focus navigation starting point to focusTarget. |
265 | 0 | } |
266 | | |
267 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigateevent-finish |
268 | | void NavigateEvent::finish(bool did_fulfill) |
269 | 0 | { |
270 | | // 1. Assert: event's interception state is not "intercepted" or "finished". |
271 | 0 | VERIFY(m_interception_state != InterceptionState::Intercepted && m_interception_state != InterceptionState::Finished); |
272 | | |
273 | | // 2. If event's interception state is "none", then return. |
274 | 0 | if (m_interception_state == InterceptionState::None) |
275 | 0 | return; |
276 | | |
277 | | // 3. Potentially reset the focus given event. |
278 | 0 | potentially_reset_the_focus(); |
279 | | |
280 | | // 4. If didFulfill is true, then potentially process scroll behavior given event. |
281 | 0 | if (did_fulfill) |
282 | 0 | potentially_process_scroll_behavior(); |
283 | | |
284 | | // 5. Set event's interception state to "finished". |
285 | 0 | m_interception_state = InterceptionState::Finished; |
286 | 0 | } |
287 | | |
288 | | } |