| // Copyright 2013 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:async'; |
| import 'dart:html' as html; |
| |
| import 'package:ui/ui.dart' as ui; |
| |
| import '../engine.dart' show buildMode, registerHotRestartListener; |
| import 'browser_detection.dart'; |
| import 'canvaskit/initialization.dart'; |
| import 'configuration.dart'; |
| import 'host_node.dart'; |
| import 'keyboard_binding.dart'; |
| import 'platform_dispatcher.dart'; |
| import 'pointer_binding.dart'; |
| import 'safe_browser_api.dart'; |
| import 'semantics.dart'; |
| import 'text_editing/text_editing.dart'; |
| import 'util.dart'; |
| import 'window.dart'; |
| |
| /// Controls the placement and lifecycle of a Flutter view on the web page. |
| /// |
| /// Manages several top-level elements that host Flutter-generated content, |
| /// including: |
| /// |
| /// - [glassPaneElement], the root element of a Flutter view. |
| /// - [glassPaneShadow], the shadow root used to isolate Flutter-rendered |
| /// content from the surrounding page content, including from the platform |
| /// views. |
| /// - [sceneElement], the element that hosts Flutter layers and pictures, and |
| /// projects platform views. |
| /// - [sceneHostElement], the anchor that provides a stable location in the DOM |
| /// tree for the [sceneElement]. |
| /// - [semanticsHostElement], hosts the ARIA-annotated semantics tree. |
| class FlutterViewEmbedder { |
| FlutterViewEmbedder() { |
| assert(() { |
| _setupHotRestart(); |
| return true; |
| }()); |
| reset(); |
| assert(() { |
| _registerHotRestartCleanUp(); |
| return true; |
| }()); |
| } |
| |
| // The tag name for the root view of the flutter app (glass-pane) |
| static const String _glassPaneTagName = 'flt-glass-pane'; |
| |
| /// Fires when browser language preferences change. |
| static const html.EventStreamProvider<html.Event> languageChangeEvent = |
| html.EventStreamProvider<html.Event>('languagechange'); |
| |
| /// Listens to window resize events. |
| StreamSubscription<html.Event>? _resizeSubscription; |
| |
| /// Listens to window locale events. |
| StreamSubscription<html.Event>? _localeSubscription; |
| |
| /// Contains Flutter-specific CSS rules, such as default margins and |
| /// paddings. |
| html.StyleElement? _styleElement; |
| |
| /// Configures the screen, such as scaling. |
| html.MetaElement? _viewportMeta; |
| |
| /// The element that contains the [sceneElement]. |
| /// |
| /// This element is created and inserted in the HTML DOM once. It is never |
| /// removed or moved. However the [sceneElement] may be replaced inside it. |
| /// |
| /// This element is inserted after the [semanticsHostElement] so that |
| /// platform views take precedence in DOM event handling. |
| html.Element? get sceneHostElement => _sceneHostElement; |
| html.Element? _sceneHostElement; |
| |
| /// A child element of body outside the shadowroot that hosts |
| /// global resources such svg filters and clip paths when using webkit. |
| html.Element? _resourcesHost; |
| |
| /// The element that contains the semantics tree. |
| /// |
| /// This element is created and inserted in the HTML DOM once. It is never |
| /// removed or moved. |
| /// |
| /// Render semantics inside the glasspane for proper focus and event |
| /// handling. If semantics is behind the glasspane, the phone will disable |
| /// focusing by touch, only by tabbing around the UI. If semantics is in |
| /// front of glasspane, then DOM event won't bubble up to the glasspane so |
| /// it can forward events to the framework. |
| /// |
| /// This element is inserted before the [semanticsHostElement] so that |
| /// platform views take precedence in DOM event handling. |
| html.Element? get semanticsHostElement => _semanticsHostElement; |
| html.Element? _semanticsHostElement; |
| |
| /// The last scene element rendered by the [render] method. |
| html.Element? get sceneElement => _sceneElement; |
| html.Element? _sceneElement; |
| |
| /// This is state persistent across hot restarts that indicates what |
| /// to clear. Delay removal of old visible state to make the |
| /// transition appear smooth. |
| static const String _staleHotRestartStore = '__flutter_state'; |
| List<html.Element?>? _staleHotRestartState; |
| |
| /// Creates a container for DOM elements that need to be cleaned up between |
| /// hot restarts. |
| /// |
| /// If a contains already exists, reuses the existing one. |
| void _setupHotRestart() { |
| // This persists across hot restarts to clear stale DOM. |
| _staleHotRestartState = getJsProperty<List<html.Element?>?>(html.window, _staleHotRestartStore); |
| if (_staleHotRestartState == null) { |
| _staleHotRestartState = <html.Element?>[]; |
| setJsProperty( |
| html.window, _staleHotRestartStore, _staleHotRestartState); |
| } |
| } |
| |
| /// Registers DOM elements that need to be cleaned up before hot restarting. |
| /// |
| /// [_setupHotRestart] must have been called prior to calling this method. |
| void _registerHotRestartCleanUp() { |
| registerHotRestartListener(() { |
| _resizeSubscription?.cancel(); |
| _localeSubscription?.cancel(); |
| _staleHotRestartState!.addAll(<html.Element?>[ |
| _glassPaneElement, |
| _styleElement, |
| _viewportMeta, |
| ]); |
| }); |
| } |
| |
| void _clearOnHotRestart() { |
| if (_staleHotRestartState!.isNotEmpty) { |
| for (final html.Element? element in _staleHotRestartState!) { |
| element?.remove(); |
| } |
| _staleHotRestartState!.clear(); |
| } |
| } |
| |
| /// Don't unnecessarily move DOM nodes around. If a DOM node is |
| /// already in the right place, skip DOM mutation. This is both faster and |
| /// more correct, because moving DOM nodes loses internal state, such as |
| /// text selection. |
| void addSceneToSceneHost(html.Element? sceneElement) { |
| if (sceneElement != _sceneElement) { |
| _sceneElement?.remove(); |
| _sceneElement = sceneElement; |
| _sceneHostElement!.append(sceneElement!); |
| } |
| assert(() { |
| _clearOnHotRestart(); |
| return true; |
| }()); |
| } |
| |
| /// The element that captures input events, such as pointer events. |
| /// |
| /// If semantics is enabled this element also contains the semantics DOM tree, |
| /// which captures semantics input events. The semantics DOM tree must be a |
| /// child of the glass pane element so that events bubble up to the glass pane |
| /// if they are not handled by semantics. |
| html.Element? get glassPaneElement => _glassPaneElement; |
| html.Element? _glassPaneElement; |
| |
| /// The [HostNode] of the [glassPaneElement], which contains the whole Flutter app. |
| HostNode? get glassPaneShadow => _glassPaneShadow; |
| HostNode? _glassPaneShadow; |
| |
| final html.Element rootElement = html.document.body!; |
| |
| static const String defaultFontStyle = 'normal'; |
| static const String defaultFontWeight = 'normal'; |
| static const double defaultFontSize = 14; |
| static const String defaultFontFamily = 'sans-serif'; |
| static const String defaultCssFont = |
| '$defaultFontStyle $defaultFontWeight ${defaultFontSize}px $defaultFontFamily'; |
| |
| void reset() { |
| final bool isWebKit = browserEngine == BrowserEngine.webkit; |
| |
| _styleElement?.remove(); |
| _styleElement = html.StyleElement(); |
| _resourcesHost?.remove(); |
| _resourcesHost = null; |
| html.document.head!.append(_styleElement!); |
| final html.CssStyleSheet sheet = _styleElement!.sheet! as html.CssStyleSheet; |
| applyGlobalCssRulesToSheet( |
| sheet, |
| browserEngine: browserEngine, |
| hasAutofillOverlay: browserHasAutofillOverlay(), |
| ); |
| |
| final html.BodyElement bodyElement = html.document.body!; |
| |
| bodyElement.setAttribute( |
| 'flt-renderer', |
| '${useCanvasKit ? 'canvaskit' : 'html'} (${FlutterConfiguration.flutterWebAutoDetect ? 'auto-selected' : 'requested explicitly'})', |
| ); |
| bodyElement.setAttribute('flt-build-mode', buildMode); |
| |
| setElementStyle(bodyElement, 'position', 'fixed'); |
| setElementStyle(bodyElement, 'top', '0'); |
| setElementStyle(bodyElement, 'right', '0'); |
| setElementStyle(bodyElement, 'bottom', '0'); |
| setElementStyle(bodyElement, 'left', '0'); |
| setElementStyle(bodyElement, 'overflow', 'hidden'); |
| setElementStyle(bodyElement, 'padding', '0'); |
| setElementStyle(bodyElement, 'margin', '0'); |
| |
| // TODO(yjbanov): fix this when KVM I/O support is added. Currently scroll |
| // using drag, and text selection interferes. |
| setElementStyle(bodyElement, 'user-select', 'none'); |
| setElementStyle(bodyElement, '-webkit-user-select', 'none'); |
| setElementStyle(bodyElement, '-ms-user-select', 'none'); |
| setElementStyle(bodyElement, '-moz-user-select', 'none'); |
| |
| // This is required to prevent the browser from doing any native touch |
| // handling. If this is not done, the browser doesn't report 'pointermove' |
| // events properly. |
| setElementStyle(bodyElement, 'touch-action', 'none'); |
| |
| // These are intentionally outrageous font parameters to make sure that the |
| // apps fully specify their text styles. |
| setElementStyle(bodyElement, 'font', defaultCssFont); |
| setElementStyle(bodyElement, 'color', 'red'); |
| |
| // TODO(mdebbar): Disable spellcheck until changes in the framework and |
| // engine are complete. |
| bodyElement.spellcheck = false; |
| |
| for (final html.Element viewportMeta |
| in html.document.head!.querySelectorAll('meta[name="viewport"]')) { |
| if (assertionsEnabled) { |
| // Filter out the meta tag that the engine placed on the page. This is |
| // to avoid UI flicker during hot restart. Hot restart will clean up the |
| // old meta tag synchronously with the first post-restart frame. |
| if (!viewportMeta.hasAttribute('flt-viewport')) { |
| print( |
| 'WARNING: found an existing <meta name="viewport"> tag. Flutter ' |
| 'Web uses its own viewport configuration for better compatibility ' |
| 'with Flutter. This tag will be replaced.', |
| ); |
| } |
| } |
| viewportMeta.remove(); |
| } |
| |
| // This removes a previously created meta tag. Note, however, that this does |
| // not remove the meta tag during hot restart. Hot restart resets all static |
| // variables, so this will be null upon hot restart. Instead, this tag is |
| // removed by _clearOnHotRestart. |
| _viewportMeta?.remove(); |
| _viewportMeta = html.MetaElement() |
| ..setAttribute('flt-viewport', '') |
| ..name = 'viewport' |
| ..content = 'width=device-width, initial-scale=1.0, ' |
| 'maximum-scale=1.0, user-scalable=no'; |
| html.document.head!.append(_viewportMeta!); |
| |
| // IMPORTANT: the glass pane element must come after the scene element in the DOM node list so |
| // it can intercept input events. |
| _glassPaneElement?.remove(); |
| final html.Element glassPaneElement = html.document.createElement(_glassPaneTagName); |
| _glassPaneElement = glassPaneElement; |
| glassPaneElement.style |
| ..position = 'absolute' |
| ..top = '0' |
| ..right = '0' |
| ..bottom = '0' |
| ..left = '0'; |
| |
| // This must be appended to the body, so the engine can create a host node |
| // properly. |
| bodyElement.append(glassPaneElement); |
| |
| // Create a [HostNode] under the glass pane element, and attach everything |
| // there, instead of directly underneath the glass panel. |
| final HostNode glassPaneElementHostNode = _createHostNode(glassPaneElement); |
| _glassPaneShadow = glassPaneElementHostNode; |
| |
| // Don't allow the scene to receive pointer events. |
| _sceneHostElement = html.document.createElement('flt-scene-host') |
| ..style.pointerEvents = 'none'; |
| |
| /// CanvasKit uses a static scene element that never gets replaced, so it's |
| /// added eagerly during initialization here and never touched, unless the |
| /// system is reset due to hot restart or in a test. |
| if (useCanvasKit) { |
| skiaSceneHost = html.Element.tag('flt-scene'); |
| addSceneToSceneHost(skiaSceneHost); |
| } |
| |
| final html.Element semanticsHostElement = |
| html.document.createElement('flt-semantics-host'); |
| semanticsHostElement.style |
| ..position = 'absolute' |
| ..transformOrigin = '0 0 0'; |
| _semanticsHostElement = semanticsHostElement; |
| updateSemanticsScreenProperties(); |
| |
| final html.Element _accessibilityPlaceholder = EngineSemanticsOwner |
| .instance.semanticsHelper |
| .prepareAccessibilityPlaceholder(); |
| |
| glassPaneElementHostNode.nodes.addAll(<html.Node>[ |
| _accessibilityPlaceholder, |
| _sceneHostElement!, |
| |
| // The semantic host goes last because hit-test order-wise it must be |
| // first. If semantics goes under the scene host, platform views will |
| // obscure semantic elements. |
| // |
| // You may be wondering: wouldn't semantics obscure platform views and |
| // make then not accessible? At least with some careful planning, that |
| // should not be the case. The semantics tree makes all of its non-leaf |
| // elements transparent. This way, if a platform view appears among other |
| // interactive Flutter widgets, as long as those widgets do not intersect |
| // with the platform view, the platform view will be reachable. |
| semanticsHostElement, |
| ]); |
| |
| // When debugging semantics, make the scene semi-transparent so that the |
| // semantics tree is more prominent. |
| if (configuration.debugShowSemanticsNodes) { |
| _sceneHostElement!.style.opacity = '0.3'; |
| } |
| |
| PointerBinding.initInstance(glassPaneElement); |
| KeyboardBinding.initInstance(glassPaneElement); |
| |
| if (html.window.visualViewport == null && isWebKit) { |
| // Older Safari versions sometimes give us bogus innerWidth/innerHeight |
| // values when the page loads. When it changes the values to correct ones |
| // it does not notify of the change via `onResize`. As a workaround, we |
| // set up a temporary periodic timer that polls innerWidth and triggers |
| // the resizeListener so that the framework can react to the change. |
| // |
| // Safari 13 has implemented visualViewport API so it doesn't need this |
| // timer. |
| // |
| // VisualViewport API is not enabled in Firefox as well. On the other hand |
| // Firefox returns correct values for innerHeight, innerWidth. |
| // Firefox also triggers html.window.onResize therefore this timer does |
| // not need to be set up for Firefox. |
| final int initialInnerWidth = html.window.innerWidth!; |
| // Counts how many times screen size was checked. It is checked up to 5 |
| // times. |
| int checkCount = 0; |
| Timer.periodic(const Duration(milliseconds: 100), (Timer t) { |
| checkCount += 1; |
| if (initialInnerWidth != html.window.innerWidth) { |
| // Window size changed. Notify. |
| t.cancel(); |
| _metricsDidChange(null); |
| } else if (checkCount > 5) { |
| // Checked enough times. Stop. |
| t.cancel(); |
| } |
| }); |
| } |
| |
| if (html.window.visualViewport != null) { |
| _resizeSubscription = |
| html.window.visualViewport!.onResize.listen(_metricsDidChange); |
| } else { |
| _resizeSubscription = html.window.onResize.listen(_metricsDidChange); |
| } |
| _localeSubscription = |
| languageChangeEvent.forTarget(html.window).listen(_languageDidChange); |
| EnginePlatformDispatcher.instance.updateLocales(); |
| } |
| |
| // Creates a [HostNode] into a `root` [html.Element]. |
| HostNode _createHostNode(html.Element root) { |
| if (getJsProperty<Object?>(root, 'attachShadow') != null) { |
| return ShadowDomHostNode(root); |
| } else { |
| // attachShadow not available, fall back to ElementHostNode. |
| return ElementHostNode(root); |
| } |
| } |
| |
| /// The framework specifies semantics in physical pixels, but CSS uses |
| /// logical pixels. To compensate, an inverse scale is injected at the root |
| /// level. |
| void updateSemanticsScreenProperties() { |
| _semanticsHostElement!.style.transform = |
| 'scale(${1 / html.window.devicePixelRatio})'; |
| } |
| |
| /// Called immediately after browser window metrics change. |
| /// |
| /// When there is a text editing going on in mobile devices, do not change |
| /// the physicalSize, change the [window.viewInsets]. See: |
| /// https://api.flutter.dev/flutter/dart-ui/FlutterView/viewInsets.html |
| /// https://api.flutter.dev/flutter/dart-ui/FlutterView/physicalSize.html |
| /// |
| /// Note: always check for rotations for a mobile device. Update the physical |
| /// size if the change is caused by a rotation. |
| void _metricsDidChange(html.Event? event) { |
| updateSemanticsScreenProperties(); |
| if (isMobile && !window.isRotation() && textEditing.isEditing) { |
| window.computeOnScreenKeyboardInsets(true); |
| EnginePlatformDispatcher.instance.invokeOnMetricsChanged(); |
| } else { |
| window.computePhysicalSize(); |
| // When physical size changes this value has to be recalculated. |
| window.computeOnScreenKeyboardInsets(false); |
| EnginePlatformDispatcher.instance.invokeOnMetricsChanged(); |
| } |
| } |
| |
| /// Called immediately after browser window language change. |
| void _languageDidChange(html.Event event) { |
| EnginePlatformDispatcher.instance.updateLocales(); |
| if (ui.window.onLocaleChanged != null) { |
| ui.window.onLocaleChanged!(); |
| } |
| } |
| |
| static const String orientationLockTypeAny = 'any'; |
| static const String orientationLockTypeNatural = 'natural'; |
| static const String orientationLockTypeLandscape = 'landscape'; |
| static const String orientationLockTypePortrait = 'portrait'; |
| static const String orientationLockTypePortraitPrimary = 'portrait-primary'; |
| static const String orientationLockTypePortraitSecondary = |
| 'portrait-secondary'; |
| static const String orientationLockTypeLandscapePrimary = 'landscape-primary'; |
| static const String orientationLockTypeLandscapeSecondary = |
| 'landscape-secondary'; |
| |
| /// Sets preferred screen orientation. |
| /// |
| /// Specifies the set of orientations the application interface can be |
| /// displayed in. |
| /// |
| /// The [orientations] argument is a list of DeviceOrientation values. |
| /// The empty list uses Screen unlock api and causes the application to |
| /// defer to the operating system default. |
| /// |
| /// See w3c screen api: https://www.w3.org/TR/screen-orientation/ |
| Future<bool> setPreferredOrientation(List<dynamic> orientations) { |
| final html.Screen screen = html.window.screen!; |
| if (!unsafeIsNull(screen)) { |
| final html.ScreenOrientation? screenOrientation = screen.orientation; |
| if (!unsafeIsNull(screenOrientation)) { |
| if (orientations.isEmpty) { |
| screenOrientation!.unlock(); |
| return Future<bool>.value(true); |
| } else { |
| final String? lockType = |
| _deviceOrientationToLockType(orientations.first as String?); |
| if (lockType != null) { |
| final Completer<bool> completer = Completer<bool>(); |
| try { |
| screenOrientation!.lock(lockType).then((dynamic _) { |
| completer.complete(true); |
| }).catchError((dynamic error) { |
| // On Chrome desktop an error with 'not supported on this device |
| // error' is fired. |
| completer.complete(false); |
| }); |
| } catch (_) { |
| return Future<bool>.value(false); |
| } |
| return completer.future; |
| } |
| } |
| } |
| } |
| // API is not supported on this browser return false. |
| return Future<bool>.value(false); |
| } |
| |
| // Converts device orientation to w3c OrientationLockType enum. |
| static String? _deviceOrientationToLockType(String? deviceOrientation) { |
| switch (deviceOrientation) { |
| case 'DeviceOrientation.portraitUp': |
| return orientationLockTypePortraitPrimary; |
| case 'DeviceOrientation.landscapeLeft': |
| return orientationLockTypePortraitSecondary; |
| case 'DeviceOrientation.portraitDown': |
| return orientationLockTypeLandscapePrimary; |
| case 'DeviceOrientation.landscapeRight': |
| return orientationLockTypeLandscapeSecondary; |
| default: |
| return null; |
| } |
| } |
| |
| /// The element corresponding to the only child of the root surface. |
| html.Element? get _rootApplicationElement { |
| final html.Element lastElement = rootElement.children.last; |
| for (final html.Element child in lastElement.children) { |
| if (child.tagName == 'FLT-SCENE') { |
| return child; |
| } |
| } |
| return null; |
| } |
| |
| /// Add an element as a global resource to be referenced by CSS. |
| /// |
| /// This call create a global resource host element on demand and either |
| /// place it as first element of body(webkit), or as a child of |
| /// glass pane element for other browsers to make sure url resolution |
| /// works correctly when content is inside a shadow root. |
| void addResource(html.Element element) { |
| final bool isWebKit = browserEngine == BrowserEngine.webkit; |
| if (_resourcesHost == null) { |
| _resourcesHost = html.DivElement() |
| ..style.visibility = 'hidden'; |
| if (isWebKit) { |
| final html.Node bodyNode = html.document.body!; |
| bodyNode.insertBefore(_resourcesHost!, bodyNode.firstChild); |
| } else { |
| _glassPaneShadow!.node.insertBefore( |
| _resourcesHost!, _glassPaneShadow!.node.firstChild); |
| } |
| } |
| _resourcesHost!.append(element); |
| } |
| |
| /// Removes a global resource element. |
| void removeResource(html.Element? element) { |
| if (element == null) { |
| return; |
| } |
| assert(element.parent == _resourcesHost); |
| element.remove(); |
| } |
| |
| String get currentHtml => _rootApplicationElement?.outerHtml ?? ''; |
| } |
| |
| // Applies the required global CSS to an incoming [html.CssStyleSheet] `sheet`. |
| void applyGlobalCssRulesToSheet( |
| html.CssStyleSheet sheet, { |
| required BrowserEngine browserEngine, |
| required bool hasAutofillOverlay, |
| String glassPaneTagName = FlutterViewEmbedder._glassPaneTagName, |
| }) { |
| final bool isWebKit = browserEngine == BrowserEngine.webkit; |
| final bool isFirefox = browserEngine == BrowserEngine.firefox; |
| // TODO(web): use more efficient CSS selectors; descendant selectors are slow. |
| // More info: https://csswizardry.com/2011/09/writing-efficient-css-selectors |
| |
| if (isFirefox) { |
| // For firefox set line-height, otherwise textx at same font-size will |
| // measure differently in ruler. |
| // |
| // - See: https://github.com/flutter/flutter/issues/44803 |
| sheet.insertRule( |
| 'flt-paragraph, flt-span {line-height: 100%;}', |
| sheet.cssRules.length, |
| ); |
| } |
| |
| // This undoes browser's default painting and layout attributes of range |
| // input, which is used in semantics. |
| sheet.insertRule( |
| ''' |
| flt-semantics input[type=range] { |
| appearance: none; |
| -webkit-appearance: none; |
| width: 100%; |
| position: absolute; |
| border: none; |
| top: 0; |
| right: 0; |
| bottom: 0; |
| left: 0; |
| } |
| ''', |
| sheet.cssRules.length, |
| ); |
| |
| if (isWebKit) { |
| sheet.insertRule( |
| 'flt-semantics input[type=range]::-webkit-slider-thumb {' |
| ' -webkit-appearance: none;' |
| '}', |
| sheet.cssRules.length); |
| } |
| |
| if (isFirefox) { |
| sheet.insertRule( |
| 'input::-moz-selection {' |
| ' background-color: transparent;' |
| '}', |
| sheet.cssRules.length); |
| sheet.insertRule( |
| 'textarea::-moz-selection {' |
| ' background-color: transparent;' |
| '}', |
| sheet.cssRules.length); |
| } else { |
| // On iOS, the invisible semantic text field has a visible cursor and |
| // selection highlight. The following 2 CSS rules force everything to be |
| // transparent. |
| sheet.insertRule( |
| 'input::selection {' |
| ' background-color: transparent;' |
| '}', |
| sheet.cssRules.length); |
| sheet.insertRule( |
| 'textarea::selection {' |
| ' background-color: transparent;' |
| '}', |
| sheet.cssRules.length); |
| } |
| sheet.insertRule(''' |
| flt-semantics input, |
| flt-semantics textarea, |
| flt-semantics [contentEditable="true"] { |
| caret-color: transparent; |
| } |
| ''', sheet.cssRules.length); |
| |
| // By default on iOS, Safari would highlight the element that's being tapped |
| // on using gray background. This CSS rule disables that. |
| if (isWebKit) { |
| sheet.insertRule(''' |
| $glassPaneTagName * { |
| -webkit-tap-highlight-color: transparent; |
| } |
| ''', sheet.cssRules.length); |
| } |
| |
| // Hide placeholder text |
| sheet.insertRule( |
| ''' |
| .flt-text-editing::placeholder { |
| opacity: 0; |
| } |
| ''', |
| sheet.cssRules.length, |
| ); |
| |
| // This css prevents an autofill overlay brought by the browser during |
| // text field autofill by delaying the transition effect. |
| // See: https://github.com/flutter/flutter/issues/61132. |
| if (browserHasAutofillOverlay()) { |
| sheet.insertRule(''' |
| .transparentTextEditing:-webkit-autofill, |
| .transparentTextEditing:-webkit-autofill:hover, |
| .transparentTextEditing:-webkit-autofill:focus, |
| .transparentTextEditing:-webkit-autofill:active { |
| -webkit-transition-delay: 99999s; |
| } |
| ''', sheet.cssRules.length); |
| } |
| } |
| |
| /// The embedder singleton. |
| /// |
| /// [ensureFlutterViewEmbedderInitialized] must be called prior to calling this |
| /// getter. |
| FlutterViewEmbedder get flutterViewEmbedder { |
| final FlutterViewEmbedder? embedder = _flutterViewEmbedder; |
| assert(() { |
| if (embedder == null) { |
| throw StateError( |
| 'FlutterViewEmbedder not initialized. Call `ensureFlutterViewEmbedderInitialized()` ' |
| 'prior to calling the `flutterViewEmbedder` getter.' |
| ); |
| } |
| return true; |
| }()); |
| return embedder!; |
| } |
| FlutterViewEmbedder? _flutterViewEmbedder; |
| |
| /// Initializes the [FlutterViewEmbedder], if it's not already initialized. |
| FlutterViewEmbedder ensureFlutterViewEmbedderInitialized() => _flutterViewEmbedder ??= FlutterViewEmbedder(); |