blob: 4b0ca8e13d9908b09075e5bb2c9d4271e686a171 [file] [log] [blame]
// 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 'browser_detection.dart';
import 'dom.dart';
import 'embedder.dart';
import 'safe_browser_api.dart';
import 'text_editing/text_editing.dart';
/// The interface required to host a flutter app in the DOM, and its tests.
///
/// Consider this as the intersection in functionality between [DomShadowRoot]
/// (preferred Flutter rendering method) and [DomDocument] (fallback).
///
/// Not to be confused with [DomDocumentOrShadowRoot].
///
/// This also handles the stylesheet that is applied to the different types of
/// HostNodes; for ShadowDOM there's not much to do, but for ElementNodes, the
/// stylesheet is "namespaced" by the `flt-glass-pane` prefix, so it "only"
/// affects things that Flutter web owns.
abstract class HostNode {
/// Returns an appropriate HostNode for the given [root].
///
/// If `attachShadow` is supported, this returns a [ShadowDomHostNode], else
/// this will fall-back to an [ElementHostNode].
factory HostNode.create(DomElement root, String defaultFont) {
if (getJsProperty<Object?>(root, 'attachShadow') != null) {
return ShadowDomHostNode(root, defaultFont);
} else {
// attachShadow not available, fall back to ElementHostNode.
return ElementHostNode(root, defaultFont);
}
}
/// Retrieves the [DomElement] that currently has focus.
///
/// See:
/// * [Document.activeElement](https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement)
DomElement? get activeElement;
/// Adds a node to the end of the child [nodes] list of this node.
///
/// If the node already exists in this document, it will be removed from its
/// current parent node, then added to this node.
///
/// This method is more efficient than `nodes.add`, and is the preferred
/// way of appending a child node.
///
/// See:
/// * [Node.appendChild](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild)
DomNode append(DomNode node);
/// Appends all of an [Iterable<DomNode>] to this [HostNode].
void appendAll(Iterable<DomNode> nodes);
/// Returns true if this node contains the specified node.
/// See:
/// * [Node.contains](https://developer.mozilla.org/en-US/docs/Web/API/Node.contains)
bool contains(DomNode? other);
/// Returns the currently wrapped [DomNode].
DomNode get node;
/// Finds the first descendant element of this document that matches the
/// specified group of selectors.
///
/// [selectors] should be a string using CSS selector syntax.
///
/// ```dart
/// var element1 = document.querySelector('.className');
/// var element2 = document.querySelector('#id');
/// ```
///
/// For details about CSS selector syntax, see the
/// [CSS selector specification](http://www.w3.org/TR/css3-selectors/).
///
/// See:
/// * [Document.querySelector](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
DomElement? querySelector(String selectors);
/// Finds all descendant elements of this document that match the specified
/// group of selectors.
///
/// [selectors] should be a string using CSS selector syntax.
///
/// ```dart
/// var items = document.querySelectorAll('.itemClassName');
/// ```
///
/// For details about CSS selector syntax, see the
/// [CSS selector specification](http://www.w3.org/TR/css3-selectors/).
///
/// See:
/// * [Document.querySelectorAll](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll)
Iterable<DomElement> querySelectorAll(String selectors);
}
/// A [HostNode] implementation, backed by a [DomShadowRoot].
///
/// This is the preferred flutter implementation, but it might not be supported
/// by all browsers yet.
///
/// The constructor might throw when calling `attachShadow`, if ShadowDOM is not
/// supported in the current environment. In this case, a fallback [ElementHostNode]
/// should be created instead.
class ShadowDomHostNode implements HostNode {
/// Build a HostNode by attaching a [DomShadowRoot] to the `root` element.
///
/// This also calls [applyGlobalCssRulesToSheet], with the [defaultFont]
/// to be used as the default font definition.
ShadowDomHostNode(DomElement root, String defaultFont)
: assert(
root.isConnected ?? true,
'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.'
) {
_shadow = root.attachShadow(<String, dynamic>{
'mode': 'open',
// This needs to stay false to prevent issues like this:
// - https://github.com/flutter/flutter/issues/85759
'delegatesFocus': false,
});
final DomHTMLStyleElement shadowRootStyleElement =
createDomHTMLStyleElement();
shadowRootStyleElement.id = 'flt-internals-stylesheet';
// The shadowRootStyleElement must be appended to the DOM, or its `sheet` will be null later.
_shadow.appendChild(shadowRootStyleElement);
applyGlobalCssRulesToSheet(
shadowRootStyleElement.sheet! as DomCSSStyleSheet,
hasAutofillOverlay: browserHasAutofillOverlay(),
defaultCssFont: defaultFont,
);
}
late DomShadowRoot _shadow;
@override
DomElement? get activeElement => _shadow.activeElement;
@override
DomElement? querySelector(String selectors) {
return _shadow.querySelector(selectors);
}
@override
Iterable<DomElement> querySelectorAll(String selectors) {
return _shadow.querySelectorAll(selectors);
}
@override
DomNode append(DomNode node) {
return _shadow.appendChild(node);
}
@override
bool contains(DomNode? other) {
return _shadow.contains(other);
}
@override
DomNode get node => _shadow;
@override
void appendAll(Iterable<DomNode> nodes) => nodes.forEach(append);
}
/// A [HostNode] implementation, backed by a [DomElement].
///
/// This is a fallback implementation, in case [ShadowDomHostNode] fails when
/// being constructed.
class ElementHostNode implements HostNode {
/// Build a HostNode by attaching a child [DomElement] to the `root` element.
ElementHostNode(DomElement root, String defaultFont) {
// Append the stylesheet here, so this class is completely symmetric to the
// ShadowDOM version.
final DomHTMLStyleElement styleElement = createDomHTMLStyleElement();
styleElement.id = 'flt-internals-stylesheet';
// The styleElement must be appended to the DOM, or its `sheet` will be null later.
root.appendChild(styleElement);
applyGlobalCssRulesToSheet(
styleElement.sheet! as DomCSSStyleSheet,
hasAutofillOverlay: browserHasAutofillOverlay(),
cssSelectorPrefix: FlutterViewEmbedder.glassPaneTagName,
defaultCssFont: defaultFont,
);
_element = domDocument.createElement('flt-element-host-node');
root.appendChild(_element);
}
late DomElement _element;
@override
DomElement? get activeElement => _element.ownerDocument?.activeElement;
@override
DomElement? querySelector(String selectors) {
return _element.querySelector(selectors);
}
@override
Iterable<DomElement> querySelectorAll(String selectors) {
return _element.querySelectorAll(selectors);
}
@override
DomNode append(DomNode node) {
return _element.appendChild(node);
}
@override
bool contains(DomNode? other) {
return _element.contains(other);
}
@override
DomNode get node => _element;
@override
void appendAll(Iterable<DomNode> nodes) => nodes.forEach(append);
}
// Applies the required global CSS to an incoming [DomCSSStyleSheet] `sheet`.
void applyGlobalCssRulesToSheet(
DomCSSStyleSheet sheet, {
required bool hasAutofillOverlay,
String cssSelectorPrefix = '',
required String defaultCssFont,
}) {
// TODO(web): use more efficient CSS selectors; descendant selectors are slow.
// More info: https://csswizardry.com/2011/09/writing-efficient-css-selectors
// These are intentionally outrageous font parameters to make sure that the
// apps fully specify their text styles.
//
// Fixes #115216 by ensuring that our parameters only affect the flt-scene-host children.
sheet.insertRule('''
$cssSelectorPrefix flt-scene-host {
color: red;
font: $defaultCssFont;
}
''', 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 (isSafari) {
sheet.insertRule('''
$cssSelectorPrefix * {
-webkit-tap-highlight-color: transparent;
}
''', sheet.cssRules.length);
}
if (isFirefox) {
// For firefox set line-height, otherwise text at same font-size will
// measure differently in ruler.
//
// - See: https://github.com/flutter/flutter/issues/44803
sheet.insertRule('''
$cssSelectorPrefix flt-paragraph,
$cssSelectorPrefix 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('''
$cssSelectorPrefix 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 (isSafari) {
sheet.insertRule('''
$cssSelectorPrefix flt-semantics input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
}
''', sheet.cssRules.length);
}
// The invisible semantic text field may have a visible cursor and selection
// highlight. The following 2 CSS rules force everything to be transparent.
sheet.insertRule('''
$cssSelectorPrefix input::selection {
background-color: transparent;
}
''', sheet.cssRules.length);
sheet.insertRule('''
$cssSelectorPrefix textarea::selection {
background-color: transparent;
}
''', sheet.cssRules.length);
sheet.insertRule('''
$cssSelectorPrefix flt-semantics input,
$cssSelectorPrefix flt-semantics textarea,
$cssSelectorPrefix flt-semantics [contentEditable="true"] {
caret-color: transparent;
}
''', sheet.cssRules.length);
// Hide placeholder text
sheet.insertRule('''
$cssSelectorPrefix .flt-text-editing::placeholder {
opacity: 0;
}
''', sheet.cssRules.length);
// This CSS makes the autofill overlay transparent in order to prevent it
// from overlaying on top of Flutter-rendered text inputs.
// See: https://github.com/flutter/flutter/issues/118337.
if (browserHasAutofillOverlay()) {
sheet.insertRule('''
$cssSelectorPrefix .transparentTextEditing:-webkit-autofill,
$cssSelectorPrefix .transparentTextEditing:-webkit-autofill:hover,
$cssSelectorPrefix .transparentTextEditing:-webkit-autofill:focus,
$cssSelectorPrefix .transparentTextEditing:-webkit-autofill:active {
opacity: 0 !important;
}
''', sheet.cssRules.length);
}
// Removes password reveal icon for text inputs in Edge browsers.
// Non-Edge browsers will crash trying to parse -ms-reveal CSS selector,
// so we guard it behind an isEdge check.
// Fixes: https://github.com/flutter/flutter/issues/83695
if (isEdge) {
// We try-catch this, because in testing, we fake Edge via the UserAgent,
// so the below will throw an exception (because only real Edge understands
// the ::-ms-reveal pseudo-selector).
try {
sheet.insertRule('''
$cssSelectorPrefix input::-ms-reveal {
display: none;
}
''', sheet.cssRules.length);
} on DomException catch (e) {
// Browsers that don't understand ::-ms-reveal throw a DOMException
// of type SyntaxError.
domWindow.console.warn(e);
// Add a fake rule if our code failed because we're under testing
assert(() {
sheet.insertRule('''
$cssSelectorPrefix input.fallback-for-fakey-browser-in-ci {
display: none;
}
''', sheet.cssRules.length);
return true;
}());
}
}
}