part of dart._engine;
// 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.


/// 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;
      }());
    }
  }
}
