b35b436cf5
Change-Id: Id0257e4d00ca4a7acc759d7f06fa5b64c744894b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/256126 Reviewed-by: Srujan Gaddam <srujzs@google.com> Commit-Queue: Lasse Nielsen <lrn@google.com> Reviewed-by: Michael Thomsen <mit@google.com> Reviewed-by: Nicholas Shahan <nshahan@google.com>
300 lines
7.9 KiB
Dart
300 lines
7.9 KiB
Dart
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
part of html;
|
|
|
|
abstract class _AttributeMap extends MapBase<String, String> {
|
|
final Element _element;
|
|
|
|
_AttributeMap(this._element);
|
|
|
|
void addAll(Map<String, String> other) {
|
|
other.forEach((k, v) {
|
|
this[k] = v;
|
|
});
|
|
}
|
|
|
|
Map<K, V> cast<K, V>() => Map.castFrom<String, String, K, V>(this);
|
|
bool containsValue(Object? value) {
|
|
for (var v in this.values) {
|
|
if (value == v) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
String putIfAbsent(String key, String ifAbsent()) {
|
|
if (!containsKey(key)) {
|
|
this[key] = ifAbsent();
|
|
}
|
|
return this[key] as String;
|
|
}
|
|
|
|
void clear() {
|
|
for (var key in keys) {
|
|
remove(key);
|
|
}
|
|
}
|
|
|
|
void forEach(void f(String key, String value)) {
|
|
for (var key in keys) {
|
|
var value = this[key];
|
|
f(key, value as String);
|
|
}
|
|
}
|
|
|
|
Iterable<String> get keys {
|
|
// TODO: generate a lazy collection instead.
|
|
var attributes = _element._attributes!;
|
|
var keys = <String>[];
|
|
for (int i = 0, len = attributes.length; i < len; i++) {
|
|
_Attr attr = attributes[i] as _Attr;
|
|
if (_matches(attr)) {
|
|
keys.add(attr.name!);
|
|
}
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
Iterable<String> get values {
|
|
// TODO: generate a lazy collection instead.
|
|
var attributes = _element._attributes!;
|
|
var values = <String>[];
|
|
for (int i = 0, len = attributes.length; i < len; i++) {
|
|
_Attr attr = attributes[i] as _Attr;
|
|
if (_matches(attr)) {
|
|
values.add(attr.value!);
|
|
}
|
|
}
|
|
return values;
|
|
}
|
|
|
|
/**
|
|
* Returns true if there is no {key, value} pair in the map.
|
|
*/
|
|
bool get isEmpty {
|
|
return length == 0;
|
|
}
|
|
|
|
/**
|
|
* Returns true if there is at least one {key, value} pair in the map.
|
|
*/
|
|
bool get isNotEmpty => !isEmpty;
|
|
|
|
/**
|
|
* Checks to see if the node should be included in this map.
|
|
*/
|
|
bool _matches(_Attr node);
|
|
}
|
|
|
|
/**
|
|
* Wrapper to expose [Element.attributes] as a typed map.
|
|
*/
|
|
class _ElementAttributeMap extends _AttributeMap {
|
|
_ElementAttributeMap(Element element) : super(element);
|
|
|
|
bool containsKey(Object? key) {
|
|
return key is String && _element._hasAttribute(key);
|
|
}
|
|
|
|
String? operator [](Object? key) {
|
|
return _element.getAttribute(key as String);
|
|
}
|
|
|
|
void operator []=(String key, String value) {
|
|
_element.setAttribute(key, value);
|
|
}
|
|
|
|
@pragma('dart2js:tryInline')
|
|
String? remove(Object? key) => key is String ? _remove(_element, key) : null;
|
|
|
|
/**
|
|
* The number of {key, value} pairs in the map.
|
|
*/
|
|
int get length {
|
|
return keys.length;
|
|
}
|
|
|
|
bool _matches(_Attr node) => node._namespaceUri == null;
|
|
|
|
// Inline this because almost all call sites of [remove] do not use [value],
|
|
// and the annotations on the `getAttribute` call allow it to be removed.
|
|
@pragma('dart2js:tryInline')
|
|
static String? _remove(Element element, String key) {
|
|
String? value = JS(
|
|
// throws:null(1) is not accurate since [key] could be malformed, but
|
|
// [key] is checked again by `removeAttributeNS`.
|
|
'returns:String|Null;depends:all;effects:none;throws:null(1)',
|
|
'#.getAttribute(#)',
|
|
element,
|
|
key);
|
|
JS('', '#.removeAttribute(#)', element, key);
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrapper to expose namespaced attributes as a typed map.
|
|
*/
|
|
class _NamespacedAttributeMap extends _AttributeMap {
|
|
final String? _namespace;
|
|
|
|
_NamespacedAttributeMap(Element element, this._namespace) : super(element);
|
|
|
|
bool containsKey(Object? key) {
|
|
return key is String && _element._hasAttributeNS(_namespace, key);
|
|
}
|
|
|
|
String? operator [](Object? key) {
|
|
return _element.getAttributeNS(_namespace, key as String);
|
|
}
|
|
|
|
void operator []=(String key, String value) {
|
|
_element.setAttributeNS(_namespace, key, value);
|
|
}
|
|
|
|
@pragma('dart2js:tryInline')
|
|
String? remove(Object? key) =>
|
|
key is String ? _remove(_namespace, _element, key) : null;
|
|
|
|
/**
|
|
* The number of {key, value} pairs in the map.
|
|
*/
|
|
int get length {
|
|
return keys.length;
|
|
}
|
|
|
|
bool _matches(_Attr node) => node._namespaceUri == _namespace;
|
|
|
|
// Inline this because almost all call sites of [remove] do not use the
|
|
// returned [value], and the annotations on the `getAttributeNS` call allow it
|
|
// to be removed.
|
|
@pragma('dart2js:tryInline')
|
|
static String? _remove(String? namespace, Element element, String key) {
|
|
String? value = JS(
|
|
// throws:null(1) is not accurate since [key] could be malformed, but
|
|
// [key] is checked again by `removeAttributeNS`.
|
|
'returns:String|Null;depends:all;effects:none;throws:null(1)',
|
|
'#.getAttributeNS(#, #)',
|
|
element,
|
|
namespace,
|
|
key);
|
|
JS('', '#.removeAttributeNS(#, #)', element, namespace, key);
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides a Map abstraction on top of data-* attributes, similar to the
|
|
* dataSet in the old DOM.
|
|
*/
|
|
class _DataAttributeMap extends MapBase<String, String> {
|
|
final Map<String, String> _attributes;
|
|
|
|
_DataAttributeMap(this._attributes);
|
|
|
|
// interface Map
|
|
|
|
void addAll(Map<String, String> other) {
|
|
other.forEach((k, v) {
|
|
this[k] = v;
|
|
});
|
|
}
|
|
|
|
Map<K, V> cast<K, V>() => Map.castFrom<String, String, K, V>(this);
|
|
// TODO: Use lazy iterator when it is available on Map.
|
|
bool containsValue(Object? value) => values.any((v) => v == value);
|
|
|
|
bool containsKey(Object? key) =>
|
|
_attributes.containsKey(_attr(key as String));
|
|
|
|
String? operator [](Object? key) => _attributes[_attr(key as String)];
|
|
|
|
void operator []=(String key, String value) {
|
|
_attributes[_attr(key)] = value;
|
|
}
|
|
|
|
String putIfAbsent(String key, String ifAbsent()) =>
|
|
_attributes.putIfAbsent(_attr(key), ifAbsent);
|
|
|
|
String? remove(Object? key) => _attributes.remove(_attr(key as String));
|
|
|
|
void clear() {
|
|
// Needs to operate on a snapshot since we are mutating the collection.
|
|
for (String key in keys) {
|
|
remove(key);
|
|
}
|
|
}
|
|
|
|
void forEach(void f(String key, String value)) {
|
|
_attributes.forEach((String key, String value) {
|
|
if (_matches(key)) {
|
|
f(_strip(key), value);
|
|
}
|
|
});
|
|
}
|
|
|
|
Iterable<String> get keys {
|
|
final keys = <String>[];
|
|
_attributes.forEach((String key, String value) {
|
|
if (_matches(key)) {
|
|
keys.add(_strip(key));
|
|
}
|
|
});
|
|
return keys;
|
|
}
|
|
|
|
Iterable<String> get values {
|
|
final values = <String>[];
|
|
_attributes.forEach((String key, String value) {
|
|
if (_matches(key)) {
|
|
values.add(value);
|
|
}
|
|
});
|
|
return values;
|
|
}
|
|
|
|
int get length => keys.length;
|
|
|
|
// TODO: Use lazy iterator when it is available on Map.
|
|
bool get isEmpty => length == 0;
|
|
|
|
bool get isNotEmpty => !isEmpty;
|
|
|
|
// Helpers.
|
|
String _attr(String key) => 'data-${_toHyphenedName(key)}';
|
|
bool _matches(String key) => key.startsWith('data-');
|
|
String _strip(String key) => _toCamelCase(key.substring(5));
|
|
|
|
/**
|
|
* Converts a string name with hyphens into an identifier, by removing hyphens
|
|
* and capitalizing the following letter. Optionally [startUppercase] to
|
|
* capitalize the first letter.
|
|
*/
|
|
String _toCamelCase(String hyphenedName, {bool startUppercase = false}) {
|
|
var segments = hyphenedName.split('-');
|
|
int start = startUppercase ? 0 : 1;
|
|
for (int i = start; i < segments.length; i++) {
|
|
var segment = segments[i];
|
|
if (segment.length > 0) {
|
|
// Character between 'a'..'z' mapped to 'A'..'Z'
|
|
segments[i] = '${segment[0].toUpperCase()}${segment.substring(1)}';
|
|
}
|
|
}
|
|
return segments.join('');
|
|
}
|
|
|
|
/** Reverse of [toCamelCase]. */
|
|
String _toHyphenedName(String word) {
|
|
var sb = new StringBuffer();
|
|
for (int i = 0; i < word.length; i++) {
|
|
var lower = word[i].toLowerCase();
|
|
if (word[i] != lower && i > 0) sb.write('-');
|
|
sb.write(lower);
|
|
}
|
|
return sb.toString();
|
|
}
|
|
}
|