diff --git a/Tests/LibWeb/Text/expected/HTML/drag-and-drop.txt b/Tests/LibWeb/Text/expected/HTML/drag-and-drop.txt new file mode 100644 index 00000000000..65dcfd8ef17 --- /dev/null +++ b/Tests/LibWeb/Text/expected/HTML/drag-and-drop.txt @@ -0,0 +1,26 @@ + Simple drag and drop: +dragenter +dragover +drop + +Drag enter and leave: +dragenter +dragover +dragleave +dragenter +dragover +drop + +Drag enter not accepted: +dragenter + +Drag over not accepted: +dragenter +dragover +dragover +dragleave + +Drop not accepted: +dragenter +dragover +drop diff --git a/Tests/LibWeb/Text/input/HTML/drag-and-drop.html b/Tests/LibWeb/Text/input/HTML/drag-and-drop.html new file mode 100644 index 00000000000..a2e52f672df --- /dev/null +++ b/Tests/LibWeb/Text/input/HTML/drag-and-drop.html @@ -0,0 +1,65 @@ +
+ + diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 6689f21e579..6ee0426333e 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -543,6 +543,7 @@ set(SOURCES NavigationTiming/EntryNames.cpp NavigationTiming/PerformanceNavigation.cpp NavigationTiming/PerformanceTiming.cpp + Page/DragAndDropEventHandler.cpp Page/EditEventHandler.cpp Page/EventHandler.cpp Page/InputEvent.cpp diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 5c1dee0f699..a0d287c239d 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -11,6 +11,7 @@ #include namespace Web { +class DragAndDropEventHandler; class EditEventHandler; class EventHandler; class LoadRequest; diff --git a/Userland/Libraries/LibWeb/Internals/Internals.cpp b/Userland/Libraries/LibWeb/Internals/Internals.cpp index ad35be34eab..9363ba81d1e 100644 --- a/Userland/Libraries/LibWeb/Internals/Internals.cpp +++ b/Userland/Libraries/LibWeb/Internals/Internals.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -117,4 +118,25 @@ JS::NonnullGCPtr Internals::create_internal_animation return realm.heap().allocate(realm, realm); } +void Internals::simulate_drag_start(double x, double y, String const& name, String const& contents) +{ + Vector files; + files.empend(name.to_byte_string(), MUST(ByteBuffer::copy(contents.bytes()))); + + auto& page = global_object().browsing_context()->page(); + page.handle_drag_and_drop_event(DragEvent::Type::DragStart, { x, y }, { x, y }, UIEvents::MouseButton::Primary, 0, 0, move(files)); +} + +void Internals::simulate_drag_move(double x, double y) +{ + auto& page = global_object().browsing_context()->page(); + page.handle_drag_and_drop_event(DragEvent::Type::DragMove, { x, y }, { x, y }, UIEvents::MouseButton::Primary, 0, 0, {}); +} + +void Internals::simulate_drop(double x, double y) +{ + auto& page = global_object().browsing_context()->page(); + page.handle_drag_and_drop_event(DragEvent::Type::Drop, { x, y }, { x, y }, UIEvents::MouseButton::Primary, 0, 0, {}); +} + } diff --git a/Userland/Libraries/LibWeb/Internals/Internals.h b/Userland/Libraries/LibWeb/Internals/Internals.h index 6118e0360f0..c10ad0a6938 100644 --- a/Userland/Libraries/LibWeb/Internals/Internals.h +++ b/Userland/Libraries/LibWeb/Internals/Internals.h @@ -36,6 +36,10 @@ public: JS::NonnullGCPtr create_internal_animation_timeline(); + void simulate_drag_start(double x, double y, String const& name, String const& contents); + void simulate_drag_move(double x, double y); + void simulate_drop(double x, double y); + private: explicit Internals(JS::Realm&); virtual void initialize(JS::Realm&) override; diff --git a/Userland/Libraries/LibWeb/Internals/Internals.idl b/Userland/Libraries/LibWeb/Internals/Internals.idl index f770f83116c..f0e5b9c04d5 100644 --- a/Userland/Libraries/LibWeb/Internals/Internals.idl +++ b/Userland/Libraries/LibWeb/Internals/Internals.idl @@ -20,4 +20,8 @@ interface Internals { boolean dispatchUserActivatedEvent(EventTarget target, Event event); InternalAnimationTimeline createInternalAnimationTimeline(); + + undefined simulateDragStart(double x, double y, DOMString mimeType, DOMString contents); + undefined simulateDragMove(double x, double y); + undefined simulateDrop(double x, double y); }; diff --git a/Userland/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp b/Userland/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp new file mode 100644 index 00000000000..717aaa5466c --- /dev/null +++ b/Userland/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp @@ -0,0 +1,640 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web { + +void DragAndDropEventHandler::visit_edges(JS::Cell::Visitor& visitor) const +{ + visitor.visit(m_source_node); + visitor.visit(m_immediate_user_selection); + visitor.visit(m_current_target_element); +} + +// https://html.spec.whatwg.org/multipage/dnd.html#drag-and-drop-processing-model +bool DragAndDropEventHandler::handle_drag_start( + JS::Realm& realm, + CSSPixelPoint screen_position, + CSSPixelPoint page_offset, + CSSPixelPoint client_offset, + CSSPixelPoint offset, + unsigned button, + unsigned buttons, + unsigned modifiers, + Vector files) +{ + auto fire_a_drag_and_drop_event = [&](JS::GCPtr target, FlyString const& name, JS::GCPtr related_target = nullptr) { + return this->fire_a_drag_and_drop_event(realm, target, name, screen_position, page_offset, client_offset, offset, button, buttons, modifiers, related_target); + }; + + // 1. Determine what is being dragged, as follows: + // + // FIXME: If the drag operation was invoked on a selection, then it is the selection that is being dragged. + // + // FIXME: Otherwise, if the drag operation was invoked on a Document, it is the first element, going up the ancestor chain, + // starting at the node that the user tried to drag, that has the IDL attribute draggable set to true. If there is + // no such element, then nothing is being dragged; return, the drag-and-drop operation is never started. + // + // Otherwise, the drag operation was invoked outside the user agent's purview. What is being dragged is defined by + // the document or application where the drag was started. + + // 2. Create a drag data store. All the DND events fired subsequently by the steps in this section must use this drag + // data store. + m_drag_data_store.emplace(); + + // 3. Establish which DOM node is the source node, as follows: + // + // FIXME: If it is a selection that is being dragged, then the source node is the Text node that the user started the + // drag on (typically the Text node that the user originally clicked). If the user did not specify a particular + // node, for example if the user just told the user agent to begin a drag of "the selection", then the source + // node is the first Text node containing a part of the selection. + // + // FIXME: Otherwise, if it is an element that is being dragged, then the source node is the element that is being dragged. + // + // Otherwise, the source node is part of another document or application. When this specification requires that + // an event be dispatched at the source node in this case, the user agent must instead follow the platform-specific + // conventions relevant to that situation. + m_source_node = nullptr; + + // FIXME: 4. Determine the list of dragged nodes, as follows: + // + // If it is a selection that is being dragged, then the list of dragged nodes contains, in tree order, every node + // that is partially or completely included in the selection (including all their ancestors). + // + // Otherwise, the list of dragged nodes contains only the source node, if any. + + // 5. If it is a selection that is being dragged, then add an item to the drag data store item list, with its + // properties set as follows: + // + // The drag data item type string + // "text/plain" + // The drag data item kind + // Text + // The actual data + // The text of the selection + // + // Otherwise, if any files are being dragged, then add one item per file to the drag data store item list, with + // their properties set as follows: + // + // The drag data item type string + // The MIME type of the file, if known, or "application/octet-stream" otherwise. + // The drag data item kind + // File + // The actual data + // The file's contents and name. + for (auto& file : files) { + auto contents = file.take_contents(); + auto mime_type = MUST(MimeSniff::Resource::sniff(contents)); + + m_drag_data_store->add_item({ + .kind = HTML::DragDataStoreItem::Kind::File, + .type_string = mime_type.essence(), + .data = move(contents), + .file_name = file.name(), + }); + } + + // FIXME: 6. If the list of dragged nodes is not empty, then extract the microdata from those nodes into a JSON form, and + // add one item to the drag data store item list, with its properties set as follows: + // + // The drag data item type string + // application/microdata+json + // The drag data item kind + // Text + // The actual data + // The resulting JSON string. + + // FIXME: 7. Run the following substeps: + [&]() { + // 1. Let urls be « ». + + // 2. For each node in the list of dragged nodes: + // + // If the node is an a element with an href attribute + // Add to urls the result of encoding-parsing-and-serializing a URL given the element's href content + // attribute's value, relative to the element's node document. + // If the node is an img element with a src attribute + // Add to urls the result of encoding-parsing-and-serializing a URL given the element's src content + // attribute's value, relative to the element's node document. + + // 3. If urls is still empty, then return. + + // 4. Let url string be the result of concatenating the strings in urls, in the order they were added, separated + // by a U+000D CARRIAGE RETURN U+000A LINE FEED character pair (CRLF). + + // 5. Add one item to the drag data store item list, with its properties set as follows: + // + // The drag data item type string + // text/uri-list + // The drag data item kind + // Text + // The actual data + // url string + }(); + + // FIXME: 8. Update the drag data store default feedback as appropriate for the user agent (if the user is dragging the + // selection, then the selection would likely be the basis for this feedback; if the user is dragging an element, + // then that element's rendering would be used; if the drag began outside the user agent, then the platform + // conventions for determining the drag feedback should be used). + + // 9. Fire a DND event named dragstart at the source node. + auto drag_event = fire_a_drag_and_drop_event(m_source_node, HTML::EventNames::dragstart); + + // If the event is canceled, then the drag-and-drop operation should not occur; return. + if (drag_event->cancelled()) { + reset(); + return false; + } + + // FIXME: 10. Fire a pointer event at the source node named pointercancel, and fire any other follow-up events as + // required by Pointer Events. + + // 11. Initiate the drag-and-drop operation in a manner consistent with platform conventions, and as described below. + // + // The drag-and-drop feedback must be generated from the first of the following sources that is available: + // + // 1. The drag data store bitmap, if any. In this case, the drag data store hot spot coordinate should be + // used as hints for where to put the cursor relative to the resulting image. The values are expressed + // as distances in CSS pixels from the left side and from the top side of the image respectively. + // 2. The drag data store default feedback. + + return true; +} + +// https://html.spec.whatwg.org/multipage/dnd.html#drag-and-drop-processing-model:queue-a-task +bool DragAndDropEventHandler::handle_drag_move( + JS::Realm& realm, + JS::NonnullGCPtr document, + JS::NonnullGCPtr node, + CSSPixelPoint screen_position, + CSSPixelPoint page_offset, + CSSPixelPoint client_offset, + CSSPixelPoint offset, + unsigned button, + unsigned buttons, + unsigned modifiers) +{ + if (!m_drag_data_store.has_value()) + return false; + + auto fire_a_drag_and_drop_event = [&](JS::GCPtr target, FlyString const& name, JS::GCPtr related_target = nullptr) { + return this->fire_a_drag_and_drop_event(realm, target, name, screen_position, page_offset, client_offset, offset, button, buttons, modifiers, related_target); + }; + + // FIXME: 1. If the user agent is still performing the previous iteration of the sequence (if any) when the next iteration + // becomes due, return for this iteration (effectively "skipping missed frames" of the drag-and-drop operation). + + // 2. Fire a DND event named drag at the source node. If this event is canceled, the user agent must set the current + // drag operation to "none" (no drag operation). + auto drag_event = fire_a_drag_and_drop_event(m_source_node, HTML::EventNames::drag); + if (drag_event->cancelled()) + m_current_drag_operation = HTML::DataTransferEffect::none; + + // 3. If the drag event was not canceled and the user has not ended the drag-and-drop operation, check the state of + // the drag-and-drop operation, as follows: + if (!drag_event->cancelled()) { + JS::GCPtr previous_target_element = m_current_target_element; + + // 1. If the user is indicating a different immediate user selection than during the last iteration (or if this + // is the first iteration), and if this immediate user selection is not the same as the current target element, + // then update the current target element as follows: + if (m_immediate_user_selection != node && node != m_current_target_element) { + m_immediate_user_selection = node; + + // -> If the new immediate user selection is null + if (!m_immediate_user_selection) { + // Set the current target element to null also. + m_current_target_element = nullptr; + } + // FIXME: -> If the new immediate user selection is in a non-DOM document or application + else if (false) { + // Set the current target element to the immediate user selection. + m_current_target_element = m_immediate_user_selection; + } + // -> Otherwise + else { + // Fire a DND event named dragenter at the immediate user selection. + auto drag_event = fire_a_drag_and_drop_event(m_immediate_user_selection, HTML::EventNames::dragenter); + + // If the event is canceled, then set the current target element to the immediate user selection. + if (drag_event->cancelled()) { + m_current_target_element = m_immediate_user_selection; + } + // Otherwise, run the appropriate step from the following list: + else { + // -> If the immediate user selection is a text control (e.g., textarea, or an input element whose + // type attribute is in the Text state) or an editing host or editable element, and the drag data + // store item list has an item with the drag data item type string "text/plain" and the drag data + // item kind text + if (allow_text_drop(*m_immediate_user_selection)) { + // Set the current target element to the immediate user selection anyway. + m_current_target_element = m_immediate_user_selection; + } + // -> If the immediate user selection is the body element + else if (m_immediate_user_selection == document->body()) { + // Leave the current target element unchanged. + } + // -> Otherwise + else { + // Fire a DND event named dragenter at the body element, if there is one, or at the Document + // object, if not. Then, set the current target element to the body element, regardless of + // whether that event was canceled or not. + DOM::EventTarget* target = document->body(); + if (!target) + target = document; + + fire_a_drag_and_drop_event(target, HTML::EventNames::dragenter); + m_current_target_element = document->body(); + } + } + } + } + + // 2. If the previous step caused the current target element to change, and if the previous target element + // was not null or a part of a non-DOM document, then fire a DND event named dragleave at the previous + // target element, with the new current target element as the specific related target. + if (previous_target_element && previous_target_element != m_current_target_element) + fire_a_drag_and_drop_event(previous_target_element, HTML::EventNames::dragleave, m_current_target_element); + + // 3. If the current target element is a DOM element, then fire a DND event named dragover at this current + // target element. + if (m_current_target_element && is(*m_current_target_element)) { + auto drag_event = fire_a_drag_and_drop_event(m_current_target_element, HTML::EventNames::dragover); + + // If the dragover event is not canceled, run the appropriate step from the following list: + if (!drag_event->cancelled()) { + // -> If the current target element is a text control (e.g., textarea, or an input element whose type + // attribute is in the Text state) or an editing host or editable element, and the drag data store + // item list has an item with the drag data item type string "text/plain" and the drag data item kind + // text. + if (allow_text_drop(*m_current_target_element)) { + // Set the current drag operation to either "copy" or "move", as appropriate given the platform + // conventions. + m_current_drag_operation = HTML::DataTransferEffect::copy; + } + // -> Otherwise + else { + // Reset the current drag operation to "none". + m_current_drag_operation = HTML::DataTransferEffect::none; + } + } + // Otherwise (if the dragover event is canceled), set the current drag operation based on the values of the + // effectAllowed and dropEffect attributes of the DragEvent object's dataTransfer object as they stood after + // the event dispatch finished, as per the following table: + else { + auto const& effect_allowed = drag_event->data_transfer()->effect_allowed(); + auto const& drop_effect = drag_event->data_transfer()->drop_effect(); + + // effectAllowed | dropEffect | Drag operation + // --------------------------------------------------------------------------------------- + // "uninitialized", "copy", "copyLink", "copyMove", or "all" | "copy" | "copy" + // "uninitialized", "link", "copyLink", "linkMove", or "all" | "link" | "link" + // "uninitialized", "move", "copyMove", "linkMove", or "all" | "move" | "move" + // Any other case | | "none" + using namespace HTML::DataTransferEffect; + + if (effect_allowed.is_one_of(uninitialized, copy, copyLink, copyMove, all) && drop_effect == copy) + m_current_drag_operation = copy; + else if (effect_allowed.is_one_of(uninitialized, link, copyLink, linkMove, all) && drop_effect == link) + m_current_drag_operation = link; + else if (effect_allowed.is_one_of(uninitialized, move, copyMove, linkMove, all) && drop_effect == move) + m_current_drag_operation = move; + else + m_current_drag_operation = none; + } + } + } + + // Set 4 continues in handle_drag_end. + if (drag_event->cancelled()) + return handle_drag_end(realm, Cancelled::Yes, screen_position, page_offset, client_offset, offset, button, buttons, modifiers); + + return true; +} + +bool DragAndDropEventHandler::handle_drag_leave( + JS::Realm& realm, + CSSPixelPoint screen_position, + CSSPixelPoint page_offset, + CSSPixelPoint client_offset, + CSSPixelPoint offset, + unsigned button, + unsigned buttons, + unsigned modifiers) +{ + return handle_drag_end(realm, Cancelled::Yes, screen_position, page_offset, client_offset, offset, button, buttons, modifiers); +} + +bool DragAndDropEventHandler::handle_drop( + JS::Realm& realm, + CSSPixelPoint screen_position, + CSSPixelPoint page_offset, + CSSPixelPoint client_offset, + CSSPixelPoint offset, + unsigned button, + unsigned buttons, + unsigned modifiers) +{ + return handle_drag_end(realm, Cancelled::No, screen_position, page_offset, client_offset, offset, button, buttons, modifiers); +} + +// https://html.spec.whatwg.org/multipage/dnd.html#drag-and-drop-processing-model:event-dnd-drag-3 +bool DragAndDropEventHandler::handle_drag_end( + JS::Realm& realm, + Cancelled cancelled, + CSSPixelPoint screen_position, + CSSPixelPoint page_offset, + CSSPixelPoint client_offset, + CSSPixelPoint offset, + unsigned button, + unsigned buttons, + unsigned modifiers) +{ + if (!m_drag_data_store.has_value()) + return false; + + auto fire_a_drag_and_drop_event = [&](JS::GCPtr target, FlyString const& name, JS::GCPtr related_target = nullptr) { + return this->fire_a_drag_and_drop_event(realm, target, name, screen_position, page_offset, client_offset, offset, button, buttons, modifiers, related_target); + }; + + ScopeGuard guard { [&]() { reset(); } }; + + // 4. Otherwise, if the user ended the drag-and-drop operation (e.g. by releasing the mouse button in a mouse-driven + // drag-and-drop interface), or if the drag event was canceled, then this will be the last iteration. Run the + // following steps, then stop the drag-and-drop operation: + { + bool dropped = false; + + // 1. If the current drag operation is "none" (no drag operation), or, if the user ended the drag-and-drop + // operation by canceling it (e.g. by hitting the Escape key), or if the current target element is null, then + // the drag operation failed. Run these substeps: + if (m_current_drag_operation == HTML::DataTransferEffect::none || cancelled == Cancelled::Yes || !m_current_target_element) { + // 1. Let dropped be false. + dropped = false; + + // 2. If the current target element is a DOM element, fire a DND event named dragleave at it; otherwise, if + // it is not null, use platform-specific conventions for drag cancelation. + if (m_current_target_element && is(*m_current_target_element)) { + fire_a_drag_and_drop_event(m_current_target_element, HTML::EventNames::dragleave); + } else if (m_current_target_element) { + // FIXME: "use platform-specific conventions for drag cancelation" + } + + // 3. Set the current drag operation to "none". + m_current_drag_operation = HTML::DataTransferEffect::none; + } + // Otherwise, the drag operation might be a success; run these substeps: + else { + JS::GCPtr drag_event; + + // 1. Let dropped be true. + dropped = true; + + // 2. If the current target element is a DOM element, fire a DND event named drop at it; otherwise, use + // platform-specific conventions for indicating a drop. + if (is(*m_current_target_element)) { + drag_event = fire_a_drag_and_drop_event(m_current_target_element, HTML::EventNames::drop); + } else { + // FIXME: "use platform-specific conventions for indicating a drop" + } + + // 3. If the event is canceled, set the current drag operation to the value of the dropEffect attribute of + // the DragEvent object's dataTransfer object as it stood after the event dispatch finished. + if (drag_event && drag_event->cancelled()) { + m_current_drag_operation = drag_event->data_transfer()->drop_effect(); + } + + // Otherwise, the event is not canceled; perform the event's default action, which depends on the exact + // target as follows: + else { + // -> If the current target element is a text control (e.g., textarea, or an input element whose type + // attribute is in the Text state) or an editing host or editable element, and the drag data store + // item list has an item with the drag data item type string "text/plain" and the drag data item + // kind text + if (allow_text_drop(*m_current_target_element)) { + // FIXME: Insert the actual data of the first item in the drag data store item list to have a drag data item + // type string of "text/plain" and a drag data item kind that is text into the text control or editing + // host or editable element in a manner consistent with platform-specific conventions (e.g. inserting + // it at the current mouse cursor position, or inserting it at the end of the field). + } + // -> Otherwise + else { + // Reset the current drag operation to "none". + m_current_drag_operation = HTML::DataTransferEffect::none; + } + } + } + + // 2. Fire a DND event named dragend at the source node. + fire_a_drag_and_drop_event(m_source_node, HTML::EventNames::dragend); + + // 3. Run the appropriate steps from the following list as the default action of the dragend event: + + // -> If dropped is true, the current target element is a text control (see below), the current drag operation + // is "move", and the source of the drag-and-drop operation is a selection in the DOM that is entirely + // contained within an editing host + if (false) { + // FIXME: Delete the selection. + } + // -> If dropped is true, the current target element is a text control (see below), the current drag operation + // is "move", and the source of the drag-and-drop operation is a selection in a text control + else if (false) { + // FIXME: The user agent should delete the dragged selection from the relevant text control. + } + // -> If dropped is false or if the current drag operation is "none" + else if (!dropped || m_current_drag_operation == HTML::DataTransferEffect::none) { + // The drag was canceled. If the platform conventions dictate that this be represented to the user (e.g. by + // animating the dragged selection going back to the source of the drag-and-drop operation), then do so. + return false; + } + // -> Otherwise + else { + // The event has no default action. + } + } + + return true; +} + +// https://html.spec.whatwg.org/multipage/dnd.html#fire-a-dnd-event +JS::NonnullGCPtr DragAndDropEventHandler::fire_a_drag_and_drop_event( + JS::Realm& realm, + JS::GCPtr target, + FlyString const& name, + CSSPixelPoint screen_position, + CSSPixelPoint page_offset, + CSSPixelPoint client_offset, + CSSPixelPoint offset, + unsigned button, + unsigned buttons, + unsigned modifiers, + JS::GCPtr related_target) +{ + // NOTE: When the source node is determined above, the spec indicates we must follow platform-specific conventions + // for dispatching events at the source node if the source node is an out-of-document object. We currently + // handle this by allowing callers to pass a null `target` node. This allows us to execute all state-change + // operations in the fire-a-DND-event AO, and simply skip event dispatching for now if the target is null. + + // 1. Let dataDragStoreWasChanged be false. + bool drag_data_store_was_changed = false; + + // 2. If no specific related target was provided, set related target to null. + + // 3. Let window be the relevant global object of the Document object of the specified target element. + // NOTE: We defer this until it's needed later, to more easily handle when the target is not an element. + + // 4. If e is dragstart, then set the drag data store mode to the read/write mode and set dataDragStoreWasChanged to true. + if (name == HTML::EventNames::dragstart) { + m_drag_data_store->set_mode(HTML::DragDataStore::Mode::ReadWrite); + drag_data_store_was_changed = true; + } + + // 5. If e is drop, set the drag data store mode to the read-only mode. + else if (name == HTML::EventNames::drop) { + m_drag_data_store->set_mode(HTML::DragDataStore::Mode::ReadOnly); + } + + // 6. Let dataTransfer be a newly created DataTransfer object associated with the given drag data store. + auto data_transfer = HTML::DataTransfer::construct_impl(realm); + data_transfer->associate_with_drag_data_store(*m_drag_data_store); + + // 7. Set the effectAllowed attribute to the drag data store's drag data store allowed effects state. + data_transfer->set_effect_allowed_internal(m_drag_data_store->allowed_effects_state()); + + // 8. Set the dropEffect attribute to "none" if e is dragstart, drag, or dragleave; to the value corresponding to the + // current drag operation if e is drop or dragend; and to a value based on the effectAllowed attribute's value and + // the drag-and-drop source, as given by the following table, otherwise (i.e. if e is dragenter or dragover): + if (name.is_one_of(HTML::EventNames::dragstart, HTML::EventNames::drag, HTML::EventNames::dragleave)) { + data_transfer->set_drop_effect(HTML::DataTransferEffect::none); + } else if (name.is_one_of(HTML::EventNames::drop, HTML::EventNames::dragend)) { + data_transfer->set_drop_effect(m_current_drag_operation); + } else { + // effectAllowed | dropEffect + // --------------------------------------------------------------------------------------------------------------------------------------- + // "none" | "none" + // "copy" | "copy" + // "copyLink" | "copy", or, if appropriate, "link" + // "copyMove" | "copy", or, if appropriate, "move" + // "all" | "copy", or, if appropriate, either "link" or "move" + // "link" | "link" + // "linkMove" | "link", or, if appropriate, "move" + // "move" | "move" + // "uninitialized" when what is being dragged is a selection from a text control | "move", or, if appropriate, either "copy" or "link" + // "uninitialized" when what is being dragged is a selection | "copy", or, if appropriate, either "link" or "move" + // "uninitialized" when what is being dragged is an a element with an href attribute | "link", or, if appropriate, either "copy" or "move" + // Any other case | "copy", or, if appropriate, either "link" or "move" + using namespace HTML::DataTransferEffect; + + // clang-format off + if (data_transfer->effect_allowed() == none) data_transfer->set_drop_effect(none); + else if (data_transfer->effect_allowed() == copy) data_transfer->set_drop_effect(copy); + else if (data_transfer->effect_allowed() == copyLink) data_transfer->set_drop_effect(copy); + else if (data_transfer->effect_allowed() == copyMove) data_transfer->set_drop_effect(copy); + else if (data_transfer->effect_allowed() == all) data_transfer->set_drop_effect(copy); + else if (data_transfer->effect_allowed() == link) data_transfer->set_drop_effect(link); + else if (data_transfer->effect_allowed() == linkMove) data_transfer->set_drop_effect(link); + else if (data_transfer->effect_allowed() == move) data_transfer->set_drop_effect(move); + // FIXME: Handle "uninitialized" when element drag operations are supported. + else data_transfer->set_drop_effect(copy); + // clang-format on + } + + // 9. Let event be the result of creating an event using DragEvent. + // FIXME: Implement https://dom.spec.whatwg.org/#concept-event-create + HTML::DragEventInit event_init {}; + + // 10. Initialize event's type attribute to e, its bubbles attribute to true, its view attribute to window, its + // relatedTarget attribute to related target, and its dataTransfer attribute to dataTransfer. + event_init.bubbles = true; + event_init.related_target = related_target; + event_init.data_transfer = data_transfer; + + if (target) { + auto& window = static_cast(HTML::relevant_global_object(*target)); + event_init.view = window; + } + + // If e is not dragleave or dragend, then initialize event's cancelable attribute to true. + if (!name.is_one_of(HTML::EventNames::dragleave, HTML::EventNames::dragend)) + event_init.cancelable = true; + + // 11. Initialize event's mouse and key attributes initialized according to the state of the input devices as they + // would be for user interaction events. + event_init.ctrl_key = (modifiers & UIEvents::Mod_Ctrl) != 0; + event_init.shift_key = (modifiers & UIEvents::Mod_Shift) != 0; + event_init.alt_key = (modifiers & UIEvents::Mod_Alt) != 0; + event_init.meta_key = (modifiers & UIEvents::Mod_Super) != 0; + event_init.screen_x = screen_position.x().to_double(); + event_init.screen_y = screen_position.y().to_double(); + event_init.client_x = client_offset.x().to_double(); + event_init.client_y = client_offset.y().to_double(); + event_init.button = button; + event_init.buttons = buttons; + + auto event = HTML::DragEvent::create(realm, name, event_init, page_offset.x().to_double(), page_offset.y().to_double(), offset.x().to_double(), offset.y().to_double()); + + // The "create an event" AO in step 9 should set these. + event->set_is_trusted(true); + event->set_initialized(true); + event->set_composed(true); + + // 12. Dispatch event at the specified target element. + if (target) + target->dispatch_event(event); + + // 13. Set the drag data store allowed effects state to the current value of dataTransfer's effectAllowed attribute. + // (It can only have changed value if e is dragstart.) + m_drag_data_store->set_allowed_effects_state(data_transfer->effect_allowed()); + + // 14. If dataDragStoreWasChanged is true, then set the drag data store mode back to the protected mode. + if (drag_data_store_was_changed) + m_drag_data_store->set_mode(HTML::DragDataStore::Mode::Protected); + + // 15. Break the association between dataTransfer and the drag data store. + data_transfer->disassociate_with_drag_data_store(); + + return event; +} + +bool DragAndDropEventHandler::allow_text_drop(JS::NonnullGCPtr node) const +{ + if (!m_drag_data_store->has_text_item()) + return false; + + if (node->is_editable()) + return true; + + if (is(*node)) + return true; + + if (is(*node)) { + auto const& input = static_cast(*node); + return input.type_state() == HTML::HTMLInputElement::TypeAttributeState::Text; + } + + return false; +} + +void DragAndDropEventHandler::reset() +{ + // When the drag-and-drop operation has completed, we no longer need the drag data store and its related fields. + // Clear them, as we currently use the existence of the drag data store to ignore other input events. + m_drag_data_store.clear(); + m_source_node = nullptr; + m_immediate_user_selection = nullptr; + m_current_target_element = nullptr; + m_current_drag_operation = HTML::DataTransferEffect::none; +} + +} diff --git a/Userland/Libraries/LibWeb/Page/DragAndDropEventHandler.h b/Userland/Libraries/LibWeb/Page/DragAndDropEventHandler.h new file mode 100644 index 00000000000..995b1de9184 --- /dev/null +++ b/Userland/Libraries/LibWeb/Page/DragAndDropEventHandler.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Web { + +class DragAndDropEventHandler { +public: + void visit_edges(JS::Cell::Visitor& visitor) const; + + bool has_ongoing_drag_and_drop_operation() const { return m_drag_data_store.has_value(); } + + bool handle_drag_start(JS::Realm&, CSSPixelPoint screen_position, CSSPixelPoint page_offset, CSSPixelPoint client_offset, CSSPixelPoint offset, unsigned button, unsigned buttons, unsigned modifiers, Vector files); + bool handle_drag_move(JS::Realm&, JS::NonnullGCPtr, JS::NonnullGCPtr, CSSPixelPoint screen_position, CSSPixelPoint page_offset, CSSPixelPoint client_offset, CSSPixelPoint offset, unsigned button, unsigned buttons, unsigned modifiers); + bool handle_drag_leave(JS::Realm&, CSSPixelPoint screen_position, CSSPixelPoint page_offset, CSSPixelPoint client_offset, CSSPixelPoint offset, unsigned button, unsigned buttons, unsigned modifiers); + bool handle_drop(JS::Realm&, CSSPixelPoint screen_position, CSSPixelPoint page_offset, CSSPixelPoint client_offset, CSSPixelPoint offset, unsigned button, unsigned buttons, unsigned modifiers); + +private: + enum class Cancelled { + No, + Yes, + }; + bool handle_drag_end(JS::Realm&, Cancelled, CSSPixelPoint screen_position, CSSPixelPoint page_offset, CSSPixelPoint client_offset, CSSPixelPoint offset, unsigned button, unsigned buttons, unsigned modifiers); + + JS::NonnullGCPtr fire_a_drag_and_drop_event( + JS::Realm&, + JS::GCPtr target, + FlyString const& name, + CSSPixelPoint screen_position, + CSSPixelPoint page_offset, + CSSPixelPoint client_offset, + CSSPixelPoint offset, + unsigned button, + unsigned buttons, + unsigned modifiers, + JS::GCPtr related_target = nullptr); + + bool allow_text_drop(JS::NonnullGCPtr) const; + + void reset(); + + Optional m_drag_data_store; + + // https://html.spec.whatwg.org/multipage/dnd.html#source-node + JS::GCPtr m_source_node; + + // https://html.spec.whatwg.org/multipage/dnd.html#immediate-user-selection + JS::GCPtr m_immediate_user_selection; + + // https://html.spec.whatwg.org/multipage/dnd.html#current-target-element + JS::GCPtr m_current_target_element; + + // https://html.spec.whatwg.org/multipage/dnd.html#current-drag-operation + FlyString m_current_drag_operation; +}; + +} diff --git a/Userland/Libraries/LibWeb/Page/EventHandler.cpp b/Userland/Libraries/LibWeb/Page/EventHandler.cpp index 44d7689557c..27bbdb0941f 100644 --- a/Userland/Libraries/LibWeb/Page/EventHandler.cpp +++ b/Userland/Libraries/LibWeb/Page/EventHandler.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -141,6 +142,7 @@ static CSSPixelPoint compute_mouse_event_offset(CSSPixelPoint position, Layout:: EventHandler::EventHandler(Badge, HTML::Navigable& navigable) : m_navigable(navigable) , m_edit_event_handler(make()) + , m_drag_and_drop_event_handler(make()) { } @@ -162,6 +164,9 @@ Painting::PaintableBox const* EventHandler::paint_root() const bool EventHandler::handle_mousewheel(CSSPixelPoint viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers, int wheel_delta_x, int wheel_delta_y) { + if (should_ignore_device_input_event()) + return false; + if (!m_navigable->active_document()) return false; if (!m_navigable->active_document()->is_fully_active()) @@ -227,6 +232,9 @@ bool EventHandler::handle_mousewheel(CSSPixelPoint viewport_position, CSSPixelPo bool EventHandler::handle_mouseup(CSSPixelPoint viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers) { + if (should_ignore_device_input_event()) + return false; + if (!m_navigable->active_document()) return false; if (!m_navigable->active_document()->is_fully_active()) @@ -353,6 +361,9 @@ after_node_use: bool EventHandler::handle_mousedown(CSSPixelPoint viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers) { + if (should_ignore_device_input_event()) + return false; + if (!m_navigable->active_document()) return false; if (!m_navigable->active_document()->is_fully_active()) @@ -460,6 +471,9 @@ bool EventHandler::handle_mousedown(CSSPixelPoint viewport_position, CSSPixelPoi bool EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSPixelPoint screen_position, u32 buttons, u32 modifiers) { + if (should_ignore_device_input_event()) + return false; + if (!m_navigable->active_document()) return false; if (!m_navigable->active_document()->is_fully_active()) @@ -595,6 +609,9 @@ bool EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSPixelPoi bool EventHandler::handle_doubleclick(CSSPixelPoint viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers) { + if (should_ignore_device_input_event()) + return false; + if (!m_navigable->active_document()) return false; if (!m_navigable->active_document()->is_fully_active()) @@ -694,6 +711,53 @@ bool EventHandler::handle_doubleclick(CSSPixelPoint viewport_position, CSSPixelP return true; } +bool EventHandler::handle_drag_and_drop_event(DragEvent::Type type, CSSPixelPoint viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers, Vector files) +{ + if (!m_navigable->active_document()) + return false; + if (!m_navigable->active_document()->is_fully_active()) + return false; + + auto& document = *m_navigable->active_document(); + document.update_layout(); + + if (!paint_root()) + return false; + + JS::GCPtr paintable; + if (auto result = target_for_mouse_position(viewport_position); result.has_value()) + paintable = result->paintable; + else + return false; + + auto node = dom_node_for_event_dispatch(*paintable); + if (!node) + return false; + + if (is(*node)) { + if (auto content_navigable = static_cast(*node).content_navigable()) + return content_navigable->event_handler().handle_drag_and_drop_event(type, viewport_position.translated(compute_mouse_event_offset({}, paintable->layout_node())), screen_position, button, buttons, modifiers, move(files)); + return false; + } + + auto offset = compute_mouse_event_offset(viewport_position, paintable->layout_node()); + auto client_offset = compute_mouse_event_client_offset(viewport_position); + auto page_offset = compute_mouse_event_page_offset(client_offset); + + switch (type) { + case DragEvent::Type::DragStart: + return m_drag_and_drop_event_handler->handle_drag_start(document.realm(), screen_position, page_offset, client_offset, offset, button, buttons, modifiers, move(files)); + case DragEvent::Type::DragMove: + return m_drag_and_drop_event_handler->handle_drag_move(document.realm(), document, *node, screen_position, page_offset, client_offset, offset, button, buttons, modifiers); + case DragEvent::Type::DragEnd: + return m_drag_and_drop_event_handler->handle_drag_leave(document.realm(), screen_position, page_offset, client_offset, offset, button, buttons, modifiers); + case DragEvent::Type::Drop: + return m_drag_and_drop_event_handler->handle_drop(document.realm(), screen_position, page_offset, client_offset, offset, button, buttons, modifiers); + } + + VERIFY_NOT_REACHED(); +} + bool EventHandler::focus_next_element() { if (!m_navigable->active_document()) @@ -1025,8 +1089,16 @@ Optional EventHandler::target_for_mouse_position(CSSPixelP return {}; } +bool EventHandler::should_ignore_device_input_event() const +{ + // From the moment that the user agent is to initiate the drag-and-drop operation, until the end of the drag-and-drop + // operation, device input events (e.g. mouse and keyboard events) must be suppressed. + return m_drag_and_drop_event_handler->has_ongoing_drag_and_drop_operation(); +} + void EventHandler::visit_edges(JS::Cell::Visitor& visitor) const { + m_drag_and_drop_event_handler->visit_edges(visitor); visitor.visit(m_mouse_event_tracking_paintable); } diff --git a/Userland/Libraries/LibWeb/Page/EventHandler.h b/Userland/Libraries/LibWeb/Page/EventHandler.h index 3b035d47379..c76e95fa1c8 100644 --- a/Userland/Libraries/LibWeb/Page/EventHandler.h +++ b/Userland/Libraries/LibWeb/Page/EventHandler.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -29,6 +30,8 @@ public: bool handle_mousewheel(CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, int wheel_delta_x, int wheel_delta_y); bool handle_doubleclick(CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers); + bool handle_drag_and_drop_event(DragEvent::Type, CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector files); + bool handle_keydown(UIEvents::KeyCode, unsigned modifiers, u32 code_point); bool handle_keyup(UIEvents::KeyCode, unsigned modifiers, u32 code_point); @@ -56,6 +59,8 @@ private: Painting::PaintableBox* paint_root(); Painting::PaintableBox const* paint_root() const; + bool should_ignore_device_input_event() const; + JS::NonnullGCPtr m_navigable; bool m_in_mouse_selection { false }; @@ -63,6 +68,7 @@ private: JS::GCPtr m_mouse_event_tracking_paintable; NonnullOwnPtr m_edit_event_handler; + NonnullOwnPtr m_drag_and_drop_event_handler; WeakPtr m_mousedown_target; diff --git a/Userland/Libraries/LibWeb/Page/InputEvent.cpp b/Userland/Libraries/LibWeb/Page/InputEvent.cpp index 6e127070c4b..3fa38cf97cc 100644 --- a/Userland/Libraries/LibWeb/Page/InputEvent.cpp +++ b/Userland/Libraries/LibWeb/Page/InputEvent.cpp @@ -20,6 +20,11 @@ MouseEvent MouseEvent::clone_without_chrome_data() const return { type, position, screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, nullptr }; } +DragEvent DragEvent::clone_without_chrome_data() const +{ + return { type, position, screen_position, button, buttons, modifiers, {}, nullptr }; +} + } template<> diff --git a/Userland/Libraries/LibWeb/Page/InputEvent.h b/Userland/Libraries/LibWeb/Page/InputEvent.h index 591584bde89..61015f51b82 100644 --- a/Userland/Libraries/LibWeb/Page/InputEvent.h +++ b/Userland/Libraries/LibWeb/Page/InputEvent.h @@ -8,8 +8,10 @@ #include #include +#include #include #include +#include #include #include #include @@ -59,6 +61,27 @@ struct MouseEvent { OwnPtr chrome_data; }; +struct DragEvent { + enum class Type { + DragStart, + DragMove, + DragEnd, + Drop, + }; + + DragEvent clone_without_chrome_data() const; + + Type type; + Web::DevicePixelPoint position; + Web::DevicePixelPoint screen_position; + UIEvents::MouseButton button { UIEvents::MouseButton::None }; + UIEvents::MouseButton buttons { UIEvents::MouseButton::None }; + UIEvents::KeyModifier modifiers { UIEvents::KeyModifier::Mod_None }; + Vector files; + + OwnPtr chrome_data; +}; + using InputEvent = Variant; } diff --git a/Userland/Libraries/LibWeb/Page/Page.cpp b/Userland/Libraries/LibWeb/Page/Page.cpp index a4d52f97063..33af34ff197 100644 --- a/Userland/Libraries/LibWeb/Page/Page.cpp +++ b/Userland/Libraries/LibWeb/Page/Page.cpp @@ -211,6 +211,11 @@ bool Page::handle_doubleclick(DevicePixelPoint position, DevicePixelPoint screen return top_level_traversable()->event_handler().handle_doubleclick(device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers); } +bool Page::handle_drag_and_drop_event(DragEvent::Type type, DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector files) +{ + return top_level_traversable()->event_handler().handle_drag_and_drop_event(type, device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers, move(files)); +} + bool Page::handle_keydown(UIEvents::KeyCode key, unsigned modifiers, u32 code_point) { return focused_navigable().event_handler().handle_keydown(key, modifiers, code_point); diff --git a/Userland/Libraries/LibWeb/Page/Page.h b/Userland/Libraries/LibWeb/Page/Page.h index add8aa233be..55cb6d6ea2f 100644 --- a/Userland/Libraries/LibWeb/Page/Page.h +++ b/Userland/Libraries/LibWeb/Page/Page.h @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -95,6 +96,8 @@ public: bool handle_mousewheel(DevicePixelPoint, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, DevicePixels wheel_delta_x, DevicePixels wheel_delta_y); bool handle_doubleclick(DevicePixelPoint, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers); + bool handle_drag_and_drop_event(DragEvent::Type, DevicePixelPoint, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector files); + bool handle_keydown(UIEvents::KeyCode, unsigned modifiers, u32 code_point); bool handle_keyup(UIEvents::KeyCode, unsigned modifiers, u32 code_point);