// Copyright 2016 Workiva Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
library dom_util;
import 'dart:html';
import 'package:platform_detect/platform_detect.dart';
import './string_util.dart';
import './validation_util.dart';
/// Returns whether [root] is the same as or contains the [other] node.
///
/// Returns false if either [root] or [other] is null.
bool isOrContains(Element root, Element other) => (
(root != null && other != null) &&
(root == other || root.contains(other))
);
/// Returns an Iterable of [element] and all its ancestors, in ascending order.
Iterable<Element> _hierarchy(Element element) sync* {
var current = element;
do {
yield current;
} while ((current = current.parent) != null);
}
/// Returns the closest element in the hierarchy of [lowerBound] up to an optional [upperBound] (both inclusive)
/// that matches [selector], or `null if no matches are found.
Element closest(Element lowerBound, String selector, {Element upperBound}) {
for (var element in _hierarchy(lowerBound)) {
if (element.matches(selector)) return element;
if (upperBound != null && upperBound == element) break;
}
return null;
}
/// Returns the currently focused element, or `null` if there is none.
Element getActiveElement() {
var activeElement = document.activeElement;
if (activeElement is! Element || activeElement == document.body) return null;
return activeElement;
}
/// A list of the `type` attribute values for an HTML `<input>` element that implement [TextInputElementBase].
///
/// Necessary because of the circular inheritance hierarchy in Dart's [InputElement] class structure.
///
/// See: <https://github.com/dart-lang/sdk/issues/22967>
///
/// Related: [isTextInputElementBase]
const List<String> inputTypesWithSelectionRangeSupport = const [
'search',
'text',
'url',
'tel',
'email',
'password',
'number',
];
/// Returns whether the provided [element] supports `setSelectionRange`.
///
/// Necessary in part because of the circular inheritance hierarchy in Dart's [InputElement] class structure,
/// and in part because the classes do not correspond to whether setSelectionRange is supported (e.g. number inputs).
///
/// See: <https://github.com/dart-lang/sdk/issues/22967>
bool supportsSelectionRange(InputElement element) {
// Uncomment once https://github.com/dart-lang/sdk/issues/22967 is fixed.
// if (element is TextInputElementBase) return true;
final type = element.getAttribute('type');
return inputTypesWithSelectionRangeSupport.contains(type);
}
/// Custom implementation to prevent the error that [TextInputElementBase.setSelectionRange] throws when called
/// on an [EmailInputElement] or [NumberInputElement] since ONLY Chrome does not support it.
///
/// A warning will be displayed in the console instead of an error.
///
/// __Example that will throw an exception in Chrome:__
/// InputElement inputNodeRef;
///
/// // This will throw an exception in Chrome when the node is focused.
/// renderEmailInput() {
/// return (Dom.input()
/// ..type = 'email'
/// ..onFocus = (_) {
/// inputNodeRef.setSelectionRange(inputNodeRef.value.length, inputNodeRef.value.length);
/// }
/// ..ref = (instance) { inputNodeRef = instance; }
/// )();
/// }
///
/// __Example that will not throw:__
/// InputElement inputNodeRef;
///
/// // This will not throw an exception - and will work in all
/// // browsers except Chrome until
/// // https://bugs.chromium.org/p/chromium/issues/detail?id=324360
/// // is fixed.
/// renderChromeSafeEmailInput() {
/// return (Dom.input()
/// ..type = 'email'
/// ..onFocus = (_) {
/// setSelectionRange(inputNodeRef, inputNodeRef.value.length, inputNodeRef.value.length);
/// }
/// ..ref = (instance) { inputNodeRef = instance; }
/// )();
/// }
///
/// See: <https://bugs.chromium.org/p/chromium/issues/detail?id=324360>
void setSelectionRange(/* TextInputElement | TextAreaElement */Element input, int start, int end, [String direction]) {
if (input is TextAreaElement) {
input.setSelectionRange(start, end, direction);
} else if (input is InputElement && supportsSelectionRange(input)) {
if (browser.isChrome) {
final inputType = input.getAttribute('type');
if (inputType == 'email' || inputType == 'number') {
assert(ValidationUtil.warn(unindent(
'''
Google Chrome does not support `setSelectionRange` on email or number inputs.
See: https://bugs.chromium.org/p/chromium/issues/detail?id=324360
'''
)));
return;
}
}
input.setSelectionRange(start, end, direction);
} else {
throw new ArgumentError.value(input, 'input', 'must be an instance of `TextInputElementBase`, `NumberInputElement` or `TextAreaElement`');
}
}