blob: be1eeda02570d4470aae28231316c64360525b10 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:html' as html;
import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart';
import 'platform_view.dart';
import 'selection_container.dart';
const String _viewType = 'Browser__WebContextMenuViewType__';
const String _kClassName = 'web-electable-region-context-menu';
// These css rules hides the dom element with the class name.
const String _kClassSelectionRule = '.$_kClassName::selection { background: transparent; }';
const String _kClassRule = '''
.$_kClassName {
color: transparent;
user-select: text;
-webkit-user-select: text; /* Safari */
-moz-user-select: text; /* Firefox */
-ms-user-select: text; /* IE10+ */
}
''';
const int _kRightClickButton = 2;
typedef _WebSelectionCallBack = void Function(html.Element, html.MouseEvent);
/// Function signature for `ui.platformViewRegistry.registerViewFactory`.
@visibleForTesting
typedef RegisterViewFactory = void Function(String, Object Function(int viewId), {bool isVisible});
/// See `_platform_selectable_region_context_menu_io.dart` for full
/// documentation.
class PlatformSelectableRegionContextMenu extends StatelessWidget {
/// See `_platform_selectable_region_context_menu_io.dart`.
PlatformSelectableRegionContextMenu({
required this.child,
super.key,
}) {
if (_registeredViewType == null) {
_register();
}
}
/// See `_platform_selectable_region_context_menu_io.dart`.
final Widget child;
/// See `_platform_selectable_region_context_menu_io.dart`.
// ignore: use_setters_to_change_properties
static void attach(SelectionContainerDelegate client) {
_activeClient = client;
}
/// See `_platform_selectable_region_context_menu_io.dart`.
static void detach(SelectionContainerDelegate client) {
if (_activeClient != client) {
_activeClient = null;
}
}
static SelectionContainerDelegate? _activeClient;
// Keeps track if this widget has already registered its view factories or not.
static String? _registeredViewType;
/// See `_platform_selectable_region_context_menu_io.dart`.
@visibleForTesting
// ignore: undefined_prefixed_name, invalid_assignment, avoid_dynamic_calls
static RegisterViewFactory registerViewFactory = ui.platformViewRegistry.registerViewFactory;
// Registers the view factories for the interceptor widgets.
static void _register() {
assert(_registeredViewType == null);
_registeredViewType = _registerWebSelectionCallback((html.Element element, html.MouseEvent event) {
final SelectionContainerDelegate? client = _activeClient;
if (client != null) {
// Converts the html right click event to flutter coordinate.
final Offset localOffset = Offset(event.offset.x.toDouble(), event.offset.y.toDouble());
final Matrix4 transform = client.getTransformTo(null);
final Offset globalOffset = MatrixUtils.transformPoint(transform, localOffset);
client.dispatchSelectionEvent(SelectWordSelectionEvent(globalPosition: globalOffset));
// The innerText must contain the text in order to be selected by
// the browser.
element.innerText = client.getSelectedContent()?.plainText ?? '';
// Programmatically select the dom element in browser.
final html.Range range = html.document.createRange();
range.selectNode(element);
final html.Selection? selection = html.window.getSelection();
if (selection != null) {
selection.removeAllRanges();
selection.addRange(range);
}
}
});
}
static String _registerWebSelectionCallback(_WebSelectionCallBack callback) {
registerViewFactory(_viewType, (int viewId) {
final html.Element htmlElement = html.DivElement();
htmlElement
..style.width = '100%'
..style.height = '100%'
..classes.add(_kClassName);
// Create css style for _kClassName.
final html.StyleElement styleElement = html.StyleElement();
html.document.head!.append(styleElement);
final html.CssStyleSheet sheet = styleElement.sheet! as html.CssStyleSheet;
sheet.insertRule(_kClassRule, 0);
sheet.insertRule(_kClassSelectionRule, 1);
htmlElement.onMouseDown.listen((html.MouseEvent event) {
if (event.button != _kRightClickButton) {
return;
}
callback(htmlElement, event);
});
return htmlElement;
}, isVisible: false);
return _viewType;
}
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: <Widget>[
const Positioned.fill(
child: HtmlElementView(
viewType: _viewType,
),
),
child,
],
);
}
}