mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-07 09:00:47 +00:00
fix(web): prevent browser context menu over task menus (#1627)
This commit is contained in:
1 parent
c0b70d319d
commit
8719166dbf
9 files changed
+420
-1
No files matched your search
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'browser_context_menu_region_stub.dart'
|
||||
if (dart.library.js_interop) 'browser_context_menu_region_web.dart'
|
||||
as platform;
|
||||
|
||||
/// Keeps the browser menu out of an application's context-menu region on Web.
|
||||
///
|
||||
/// Captured themes carry the policy into shadcn's menu and submenu overlays.
|
||||
/// Overlay regions defer to their children so a full-screen positioning layer
|
||||
/// does not suppress the browser menu over the rest of the page.
|
||||
class BrowserContextMenuRegion extends StatelessWidget {
|
||||
const BrowserContextMenuRegion({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!kIsWeb) return child;
|
||||
return _ContextMenuTheme(
|
||||
child: _ContextMenuRegion(behavior: HitTestBehavior.translucent, child: child),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ContextMenuTheme extends InheritedTheme {
|
||||
const _ContextMenuTheme({required super.child});
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return _ContextMenuTheme(child: _ContextMenuRegion(child: child));
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_ContextMenuTheme oldWidget) => false;
|
||||
}
|
||||
|
||||
class _ContextMenuRegion extends SingleChildRenderObjectWidget {
|
||||
const _ContextMenuRegion({this.behavior = HitTestBehavior.deferToChild, required super.child});
|
||||
|
||||
final HitTestBehavior behavior;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderContextMenuRegion(View.of(context).viewId, behavior);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, _RenderContextMenuRegion renderObject) {
|
||||
renderObject
|
||||
..viewId = View.of(context).viewId
|
||||
..behavior = behavior;
|
||||
}
|
||||
}
|
||||
|
||||
class _RenderContextMenuRegion extends RenderProxyBoxWithHitTestBehavior {
|
||||
_RenderContextMenuRegion(this._viewId, HitTestBehavior behavior) : super(behavior: behavior);
|
||||
|
||||
int _viewId;
|
||||
|
||||
set viewId(int value) {
|
||||
if (value == _viewId) return;
|
||||
if (attached) platform.unregisterRegion(this, _viewId);
|
||||
_viewId = value;
|
||||
if (attached) platform.registerRegion(this, _viewId);
|
||||
}
|
||||
|
||||
@override
|
||||
void attach(PipelineOwner owner) {
|
||||
super.attach(owner);
|
||||
platform.registerRegion(this, _viewId);
|
||||
}
|
||||
|
||||
@override
|
||||
void detach() {
|
||||
platform.unregisterRegion(this, _viewId);
|
||||
super.detach();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
void registerRegion(RenderBox region, int viewId) {}
|
||||
|
||||
void unregisterRegion(RenderBox region, int viewId) {}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'dart:js_interop';
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:ui_web' as ui_web;
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
final _regions = <int, Set<RenderBox>>{};
|
||||
JSFunction? _listener;
|
||||
|
||||
void registerRegion(RenderBox region, int viewId) {
|
||||
_regions.putIfAbsent(viewId, () => <RenderBox>{}).add(region);
|
||||
if (_listener != null) return;
|
||||
_listener = _onContextMenu.toJS;
|
||||
web.document.addEventListener('contextmenu', _listener, true.toJS);
|
||||
}
|
||||
|
||||
void unregisterRegion(RenderBox region, int viewId) {
|
||||
final regions = _regions[viewId];
|
||||
regions?.remove(region);
|
||||
if (regions?.isEmpty ?? false) _regions.remove(viewId);
|
||||
if (_regions.isNotEmpty) return;
|
||||
web.document.removeEventListener('contextmenu', _listener, true.toJS);
|
||||
_listener = null;
|
||||
}
|
||||
|
||||
void _onContextMenu(web.MouseEvent event) {
|
||||
if (!event.cancelable || event.defaultPrevented) return;
|
||||
for (final view in ui.PlatformDispatcher.instance.views) {
|
||||
final regions = _regions[view.viewId];
|
||||
if (regions == null) continue;
|
||||
final host = ui_web.views.getHostElement(view.viewId) as web.HTMLElement?;
|
||||
if (host == null || !event.composedPath().toDart.contains(host)) continue;
|
||||
final bounds = host.getBoundingClientRect();
|
||||
if (bounds.width <= 0 || bounds.height <= 0) continue;
|
||||
final x = event.clientX - bounds.left;
|
||||
final y = event.clientY - bounds.top;
|
||||
if (x < 0 || y < 0 || x >= bounds.width || y >= bounds.height) continue;
|
||||
|
||||
// DOM client coordinates are CSS pixels. Map them through the public view
|
||||
// host bounds rather than assuming a DPR, page origin or browser zoom.
|
||||
final logicalSize = view.physicalSize / view.devicePixelRatio;
|
||||
final position = Offset(x * logicalSize.width / bounds.width, y * logicalSize.height / bounds.height);
|
||||
final result = HitTestResult();
|
||||
RendererBinding.instance.hitTestInView(result, position, view.viewId);
|
||||
if (result.path.any((entry) => regions.contains(entry.target))) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user