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 support for React-Router with `<Link /> #657

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion bridge/core/dom/events/event_target.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ namespace webf {
struct EventDispatchResult : public DartReadable {
bool canceled{false};
bool propagationStopped{false};
bool preventDefaulted{false};
};

struct DartEventListenerOptions : public DartReadable {
Expand Down Expand Up @@ -443,7 +444,7 @@ NativeValue EventTarget::HandleDispatchEventFromDart(int32_t argc, const NativeV
GetExecutingContext()->dartIsolateContext()->profiler()->FinishTrackSteps();

auto* result = new EventDispatchResult{.canceled = dispatch_result == DispatchEventResult::kCanceledByEventHandler,
.propagationStopped = event->propagationStopped()};
.propagationStopped = event->propagationStopped(), .preventDefaulted = event->defaultPrevented()};
return NativeValueConverter<NativeTypePointer<EventDispatchResult>>::ToNativeValue(result);
}

Expand Down
4 changes: 2 additions & 2 deletions bridge/core/events/mouse_event.cc
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ MouseEvent::MouseEvent(ExecutingContext* context,
MouseEvent::MouseEvent(ExecutingContext* context, const AtomicString& type, NativeMouseEvent* native_mouse_event)
: UIEvent(context, type, &native_mouse_event->native_event),
// alt_key_(native_mouse_event->altKey),
// button_(native_mouse_event->button),
// buttons_(native_mouse_event->buttons),
button_(native_mouse_event->button),
// buttons_(native_mouse_event->buttons),
client_x_(native_mouse_event->clientX),
client_y_(native_mouse_event->clientY),
// ctrl_key_(native_mouse_event->ctrlKey),
Expand Down
2 changes: 1 addition & 1 deletion bridge/core/events/mouse_event.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {MouseEventInit} from "./mouse_event_init";
/** Events that occur due to the user interacting with a pointing device (such as a mouse). Common events using this interface include click, dblclick, mouseup, mousedown. */
interface MouseEvent extends UIEvent {
// readonly altKey: boolean;
// readonly button: number;
readonly button: number;
// readonly buttons: number;
readonly clientX: number;
readonly clientY: number;
Expand Down
4 changes: 2 additions & 2 deletions bridge/core/events/mouse_event.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ class MouseEvent : public UIEvent {

private:
bool alt_key_;
double button_;
double buttons_;
double button_{0};
double buttons_{0};
double client_x_;
double client_y_;
bool ctrl_key_;
Expand Down
3 changes: 2 additions & 1 deletion bridge/core/html/html_anchor_element.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Element} from "../dom/element";
import {HTMLElement} from "./html_element";

interface HTMLAnchorElement extends Element {
interface HTMLAnchorElement extends HTMLElement {
target: DartImpl<string>;
accessKey: DartImpl<string>;
download: DartImpl<string>;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions integration_tests/specs/dom/events/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,4 +786,42 @@ describe('Event', () => {
el.style.width = '102px';
el2.style.width = '102px';
});

it('should works with preventDefault in `<a /> element', async (done) => {
const anchorElement = createElement('a', {}, [createText('')]);
BODY.append(anchorElement);

anchorElement.addEventListener('click', async (e) => {
e.preventDefault();

BODY.append(createText('Nothing happened'));

await snapshot();
done();
});

anchorElement.click();
});

it('should satisfy react-router event check', (done) => {
function isModifiedEvent(event: MouseEvent) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}
function shouldProcessLinkClick(event: MouseEvent) {
return event.button === 0 &&
// Let browser handle "target=_blank" etc.
!isModifiedEvent(event) // Ignore clicks with modifier keys
;
}

const anchorElement = createElement('a', {}, []);
BODY.append(anchorElement);

anchorElement.addEventListener('click', async (e) => {
expect(shouldProcessLinkClick(e));
done();
});

anchorElement.click();
});
});
5 changes: 3 additions & 2 deletions webf/lib/src/bridge/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ void _handleDispatchResult(Object contextHandle, Pointer<NativeValue> returnValu
Event event = context.event;
event.cancelable = dispatchResult.ref.canceled;
event.propagationStopped = dispatchResult.ref.propagationStopped;
event.defaultPrevented = dispatchResult.ref.preventDefaulted;
event.sharedJSProps = Pointer.fromAddress(context.rawEvent.ref.bytes.elementAt(8).value);
event.propLen = context.rawEvent.ref.bytes.elementAt(9).value;
event.allocateLen = context.rawEvent.ref.bytes.elementAt(10).value;
Expand Down Expand Up @@ -103,9 +104,9 @@ class _DispatchEventResultContext {
Future<void> _dispatchEventToNative(Event event, bool isCapture) async {
Pointer<NativeBindingObject>? pointer = event.currentTarget?.pointer;
double? contextId = event.target?.contextId;
WebFController controller = WebFController.getControllerOfJSContextId(contextId)!;
WebFController? controller = WebFController.getControllerOfJSContextId(contextId);

if (controller.view.disposed) return;
if (controller == null || controller.view.disposed) return;

if (contextId != null &&
pointer != null &&
Expand Down
3 changes: 3 additions & 0 deletions webf/lib/src/bridge/native_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class EventDispatchResult extends Struct {

@Bool()
external bool propagationStopped;

@Bool()
external bool preventDefaulted;
}

class AddEventListenerOptions extends Struct {
Expand Down
19 changes: 11 additions & 8 deletions webf/lib/src/dom/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,18 @@ mixin ElementEventMixin on ElementBase {
}

@override
void addEventListener(String eventType, EventHandler handler, {EventListenerOptions? addEventListenerOptions}) {
super.addEventListener(eventType, handler, addEventListenerOptions: addEventListenerOptions);
void addEventListener(String eventType, EventHandler handler,
{EventListenerOptions? addEventListenerOptions, bool builtInCallback = false}) {
super.addEventListener(eventType, handler, addEventListenerOptions: addEventListenerOptions, builtInCallback: builtInCallback);
RenderBoxModel? renderBox = renderBoxModel;
if (renderBox != null) {
ensureEventResponderBound();
}
}

@override
void removeEventListener(String eventType, EventHandler handler, {bool isCapture = false}) {
super.removeEventListener(eventType, handler, isCapture: isCapture);
void removeEventListener(String eventType, EventHandler handler, {bool isCapture = false, bool builtInCallback = false}) {
super.removeEventListener(eventType, handler, isCapture: isCapture, builtInCallback: builtInCallback);
RenderBoxModel? renderBox = renderBoxModel;
if (renderBox != null) {
ensureEventResponderBound();
Expand Down Expand Up @@ -225,8 +226,8 @@ class Event {
(_target != null && _target.pointer != null) ? _target.pointer!.address : nullptr.address,
(_currentTarget != null && _currentTarget.pointer != null) ? _currentTarget.pointer!.address : nullptr.address,
sharedJSProps.address, // EventProps* props
propLen, // int64_t props_len
allocateLen // int64_t alloc_size;
propLen, // int64_t props_len
allocateLen // int64_t alloc_size;
];

// Allocate extra bytes to store subclass's members.
Expand Down Expand Up @@ -271,7 +272,7 @@ class HybridRouterChangeEvent extends Event {
final String kind;
final String name;

HybridRouterChangeEvent({this.state, required this.kind, required this.name}): super(EVENT_HYBRID_ROUTER_CHANGE);
HybridRouterChangeEvent({this.state, required this.kind, required this.name}) : super(EVENT_HYBRID_ROUTER_CHANGE);

@override
Pointer<NativeType> toRaw([int extraLength = 0, bool isCustomEvent = false]) {
Expand Down Expand Up @@ -384,6 +385,7 @@ class FocusEvent extends UIEvent {

/// reference: https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent
class MouseEvent extends UIEvent {
double button = 0;
double clientX;
double clientY;
double offsetX;
Expand All @@ -403,10 +405,11 @@ class MouseEvent extends UIEvent {
@override
Pointer toRaw([int extraLength = 0, bool isCustomEvent = false]) {
List<int> methods = [
doubleToUint64(button),
doubleToUint64(clientX),
doubleToUint64(clientY),
doubleToUint64(offsetX),
doubleToUint64(offsetY)
doubleToUint64(offsetY),
];

Pointer<RawEvent> rawEvent = super.toRaw(methods.length + extraLength).cast<RawEvent>();
Expand Down
57 changes: 41 additions & 16 deletions webf/lib/src/dom/event_target.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,62 @@ abstract class EventTarget extends DynamicBindingObject {
@protected
final Map<String, List<EventHandler>> _eventHandlers = {};

@protected
final Map<String, List<EventHandler>> _builtInEventHandlers = {};

@protected
final Map<String, List<EventHandler>> _eventCaptureHandlers = {};

@protected
final Map<String, List<EventHandler>> _builtInEventCaptureHandlers = {};

Map<String, List<EventHandler>> getEventHandlers() => _eventHandlers;
Map<String, List<EventHandler>> getBuiltInEventHandlers() => _builtInEventHandlers;

Map<String, List<EventHandler>> getCaptureEventHandlers() => _eventCaptureHandlers;
Map<String, List<EventHandler>> getBuiltInCaptureEventHandlers() => _builtInEventCaptureHandlers;

@protected
bool hasEventListener(String type) => _eventHandlers.containsKey(type);
bool hasEventListener(String type) => _eventHandlers.containsKey(type) || _builtInEventHandlers.containsKey(type);

// TODO: Support addEventListener options: capture, once, passive, signal.
@mustCallSuper
void addEventListener(String eventType, EventHandler eventHandler, {EventListenerOptions? addEventListenerOptions}) {
void addEventListener(String eventType, EventHandler eventHandler,
{EventListenerOptions? addEventListenerOptions, bool builtInCallback = false}) {
if (_disposed) return;
bool capture = false;
if (addEventListenerOptions != null)
capture = addEventListenerOptions.capture;
List<EventHandler>? existHandler = capture ? _eventCaptureHandlers[eventType] : _eventHandlers[eventType];

if (addEventListenerOptions != null) capture = addEventListenerOptions.capture;
final eventCaptureHandlers = builtInCallback ? _builtInEventCaptureHandlers : _eventCaptureHandlers;
final eventBubbleHandlers = builtInCallback ? _builtInEventHandlers : _eventHandlers;

List<EventHandler>? existHandler = capture
? eventCaptureHandlers[eventType]
: eventBubbleHandlers[eventType];
if (existHandler == null) {
if (capture)
_eventCaptureHandlers[eventType] = existHandler = [];
eventCaptureHandlers[eventType] = existHandler = [];
else
_eventHandlers[eventType] = existHandler = [];
eventBubbleHandlers[eventType] = existHandler = [];
}
existHandler.add(eventHandler);
}

@mustCallSuper
void removeEventListener(String eventType, EventHandler eventHandler, {bool isCapture = false}) {
void removeEventListener(String eventType, EventHandler eventHandler, {bool isCapture = false, bool builtInCallback = false}) {
if (_disposed) return;

List<EventHandler>? currentHandlers = isCapture ? _eventCaptureHandlers[eventType] : _eventHandlers[eventType];
final eventCaptureHandlers = builtInCallback ? _builtInEventCaptureHandlers : _eventCaptureHandlers;
final eventBubbleHandlers = builtInCallback ? _builtInEventHandlers : _eventHandlers;

List<EventHandler>? currentHandlers = isCapture ? eventCaptureHandlers[eventType] : eventBubbleHandlers[eventType];
if (currentHandlers != null) {
currentHandlers.remove(eventHandler);
if (currentHandlers.isEmpty) {
if (isCapture) {
_eventCaptureHandlers.remove(eventType);
eventCaptureHandlers.remove(eventType);
} else {
_eventHandlers.remove(eventType);
eventBubbleHandlers.remove(eventType);
}
}
}
Expand All @@ -73,11 +90,15 @@ abstract class EventTarget extends DynamicBindingObject {

await _handlerCaptureEvent(event);
await _dispatchEventInDOM(event);

await _handlerCaptureEvent(event, builtInCallback: true);
await _dispatchEventInDOM(event, builtInCallback: true);
}
Future<void> _handlerCaptureEvent(Event event) async {
Future<void> _handlerCaptureEvent(Event event, { bool builtInCallback = false }) async {
await parentEventTarget?._handlerCaptureEvent(event);
String eventType = event.type;
List<EventHandler>? existHandler = _eventCaptureHandlers[eventType];
final eventCaptureHandlers = builtInCallback ? _builtInEventCaptureHandlers : _eventCaptureHandlers;
List<EventHandler>? existHandler = eventCaptureHandlers[eventType];
if (existHandler != null) {
// Modify currentTarget before the handler call, otherwise currentTarget may be modified by the previous handler.
event.currentTarget = this;
Expand All @@ -94,11 +115,12 @@ abstract class EventTarget extends DynamicBindingObject {
}
}
// Refs: https://github.com/WebKit/WebKit/blob/main/Source/WebCore/dom/EventDispatcher.cpp#L85
Future<void> _dispatchEventInDOM(Event event) async {
Future<void> _dispatchEventInDOM(Event event, { bool builtInCallback = false }) async {
// TODO: Invoke capturing event listeners in the reverse order.

String eventType = event.type;
List<EventHandler>? existHandler = _eventHandlers[eventType];
final eventBubbleHandlers = builtInCallback ? _builtInEventHandlers : _eventHandlers;
List<EventHandler>? existHandler = eventBubbleHandlers[eventType];
if (existHandler != null) {
// Modify currentTarget before the handler call, otherwise currentTarget may be modified by the previous handler.
event.currentTarget = this;
Expand All @@ -125,6 +147,9 @@ abstract class EventTarget extends DynamicBindingObject {
void dispose() async {
_disposed = true;
_eventHandlers.clear();
_eventCaptureHandlers.clear();
_builtInEventHandlers.clear();
_builtInEventCaptureHandlers.clear();
super.dispose();
}

Expand All @@ -140,8 +165,8 @@ abstract class EventTarget extends DynamicBindingObject {
return path;
}
}
class EventListenerOptions {

class EventListenerOptions {
bool capture;
bool passive;
bool once;
Expand Down
4 changes: 2 additions & 2 deletions webf/lib/src/dom/window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class Window extends EventTarget {
}

@override
void addEventListener(String eventType, EventHandler handler, {EventListenerOptions? addEventListenerOptions}) {
void addEventListener(String eventType, EventHandler handler, {EventListenerOptions? addEventListenerOptions, bool builtInCallback = false}) {
super.addEventListener(eventType, handler, addEventListenerOptions: addEventListenerOptions);
switch (eventType) {
case EVENT_SCROLL:
Expand All @@ -143,7 +143,7 @@ class Window extends EventTarget {
}

@override
void removeEventListener(String eventType, EventHandler handler, {bool isCapture = false}) {
void removeEventListener(String eventType, EventHandler handler, {bool isCapture = false, bool builtInCallback = false}) {
super.removeEventListener(eventType, handler, isCapture: isCapture);
switch (eventType) {
case EVENT_SCROLL:
Expand Down
3 changes: 3 additions & 0 deletions webf/lib/src/gesture/gesture_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ class GestureDispatcher {
eventTarget.getEventHandlers().keys.forEach((eventType) {
_eventsInPath[eventType] = true;
});
eventTarget.getBuiltInEventHandlers().keys.forEach((eventType) {
_eventsInPath[eventType] = true;
});
}
}

Expand Down
7 changes: 4 additions & 3 deletions webf/lib/src/html/a.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ const String _TARGET_SELF = 'self';

class HTMLAnchorElement extends Element {
HTMLAnchorElement([BindingContext? context]) : super(context) {
addEventListener(EVENT_CLICK, _handleClick);
addEventListener(EVENT_CLICK, _handleClick, builtInCallback: true);
}

Future<void> _handleClick(Event event) async {
if (event.defaultPrevented) return;
String? href = attributes['href'];
if (href != null && href.isNotEmpty) {
String baseUrl = ownerDocument.controller.url;
Expand All @@ -23,9 +24,9 @@ class HTMLAnchorElement extends Element {
if (href.trim().startsWith('#')) {
HistoryModule historyModule = ownerDocument.controller.module.moduleManager.getModule('History')!;
historyModule.pushState(null, url: href);
ownerView.window.dispatchEvent(HashChangeEvent(newUrl: resolvedUri.toString(), oldUrl: baseUrl));
await ownerView.window.dispatchEvent(HashChangeEvent(newUrl: resolvedUri.toString(), oldUrl: baseUrl));
} else {
ownerDocument.controller.view
await ownerDocument.controller.view
.handleNavigationAction(baseUrl, resolvedUri.toString(), _getNavigationType(resolvedUri.scheme));
}
}
Expand Down
Loading
Loading