1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'dart:js_interop'; |
6 | import 'dart:ui_web' as ui_web; |
7 | |
8 | import 'package:flutter/rendering.dart'; |
9 | |
10 | import '../web.dart' as web; |
11 | import 'basic.dart'; |
12 | import 'framework.dart'; |
13 | import 'platform_view.dart'; |
14 | import 'selection_container.dart'; |
15 | |
16 | const String _viewType = 'Browser__WebContextMenuViewType__' ; |
17 | const String _kClassName = 'web-selectable-region-context-menu' ; |
18 | // These css rules hides the dom element with the class name. |
19 | const String _kClassSelectionRule = '. $_kClassName::selection { background: transparent; }' ; |
20 | const String _kClassRule = ''' |
21 | . $_kClassName { |
22 | color: transparent; |
23 | user-select: text; |
24 | -webkit-user-select: text; /* Safari */ |
25 | -moz-user-select: text; /* Firefox */ |
26 | -ms-user-select: text; /* IE10+ */ |
27 | } |
28 | ''' ; |
29 | const int _kRightClickButton = 2; |
30 | |
31 | typedef _WebSelectionCallBack = void Function(web.HTMLElement, web.MouseEvent); |
32 | |
33 | /// Function signature for `ui_web.platformViewRegistry.registerViewFactory`. |
34 | @visibleForTesting |
35 | typedef RegisterViewFactory = void Function(String, Object Function(int viewId), {bool isVisible}); |
36 | |
37 | /// See `_platform_selectable_region_context_menu_io.dart` for full |
38 | /// documentation. |
39 | class PlatformSelectableRegionContextMenu extends StatelessWidget { |
40 | /// See `_platform_selectable_region_context_menu_io.dart`. |
41 | PlatformSelectableRegionContextMenu({required this.child, super.key}) { |
42 | if (_registeredViewType == null) { |
43 | _register(); |
44 | } |
45 | } |
46 | |
47 | /// See `_platform_selectable_region_context_menu_io.dart`. |
48 | final Widget child; |
49 | |
50 | /// See `_platform_selectable_region_context_menu_io.dart`. |
51 | // ignore: use_setters_to_change_properties |
52 | static void attach(SelectionContainerDelegate client) { |
53 | _activeClient = client; |
54 | } |
55 | |
56 | /// See `_platform_selectable_region_context_menu_io.dart`. |
57 | static void detach(SelectionContainerDelegate client) { |
58 | if (_activeClient != client) { |
59 | _activeClient = null; |
60 | } |
61 | } |
62 | |
63 | static SelectionContainerDelegate? _activeClient; |
64 | |
65 | // Keeps track if this widget has already registered its view factories or not. |
66 | static String? _registeredViewType; |
67 | |
68 | static RegisterViewFactory get _registerViewFactory => |
69 | debugOverrideRegisterViewFactory ?? ui_web.platformViewRegistry.registerViewFactory; |
70 | |
71 | /// Override this to provide a custom implementation of [ui_web.platformViewRegistry.registerViewFactory]. |
72 | /// |
73 | /// This should only be used for testing. |
74 | // See `_platform_selectable_region_context_menu_io.dart`. |
75 | @visibleForTesting |
76 | static RegisterViewFactory? debugOverrideRegisterViewFactory; |
77 | |
78 | /// Resets the view factory registration to its initial state. |
79 | @visibleForTesting |
80 | static void debugResetRegistry() { |
81 | _registeredViewType = null; |
82 | } |
83 | |
84 | // Registers the view factories for the interceptor widgets. |
85 | static void _register() { |
86 | assert(_registeredViewType == null); |
87 | _registeredViewType = _registerWebSelectionCallback(( |
88 | web.HTMLElement element, |
89 | web.MouseEvent event, |
90 | ) { |
91 | final SelectionContainerDelegate? client = _activeClient; |
92 | if (client != null) { |
93 | // Converts the html right click event to flutter coordinate. |
94 | final Offset localOffset = Offset(event.offsetX.toDouble(), event.offsetY.toDouble()); |
95 | final Matrix4 transform = client.getTransformTo(null); |
96 | final Offset globalOffset = MatrixUtils.transformPoint(transform, localOffset); |
97 | client.dispatchSelectionEvent(SelectWordSelectionEvent(globalPosition: globalOffset)); |
98 | // The innerText must contain the text in order to be selected by |
99 | // the browser. |
100 | element.innerText = client.getSelectedContent()?.plainText ?? '' ; |
101 | |
102 | // Programmatically select the dom element in browser. |
103 | final web.Range range = web.document.createRange()..selectNode(element); |
104 | |
105 | web.window.getSelection() |
106 | ?..removeAllRanges() |
107 | ..addRange(range); |
108 | } |
109 | }); |
110 | } |
111 | |
112 | static String _registerWebSelectionCallback(_WebSelectionCallBack callback) { |
113 | // Create css style for _kClassName. |
114 | final web.HTMLStyleElement styleElement = |
115 | web.document.createElement('style' ) as web.HTMLStyleElement; |
116 | web.document.head!.append(styleElement as JSAny); |
117 | final web.CSSStyleSheet sheet = styleElement.sheet!; |
118 | sheet.insertRule(_kClassRule, 0); |
119 | sheet.insertRule(_kClassSelectionRule, 1); |
120 | |
121 | _registerViewFactory(_viewType, (int viewId, {Object? params}) { |
122 | final web.HTMLElement htmlElement = web.document.createElement('div' ) as web.HTMLElement; |
123 | htmlElement |
124 | ..style.width = '100%' |
125 | ..style.height = '100%' |
126 | ..classList.add(_kClassName); |
127 | |
128 | htmlElement.addEventListener( |
129 | 'mousedown' , |
130 | (web.Event event) { |
131 | final web.MouseEvent mouseEvent = event as web.MouseEvent; |
132 | if (mouseEvent.button != _kRightClickButton) { |
133 | return; |
134 | } |
135 | callback(htmlElement, mouseEvent); |
136 | }.toJS, |
137 | ); |
138 | return htmlElement; |
139 | }, isVisible: false); |
140 | return _viewType; |
141 | } |
142 | |
143 | @override |
144 | Widget build(BuildContext context) { |
145 | return Stack( |
146 | children: <Widget>[const Positioned.fill(child: HtmlElementView(viewType: _viewType)), child], |
147 | ); |
148 | } |
149 | } |
150 | |