NavigateEvent.cpp 12 KB

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