| // 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 Function() ifAbsent) { | |
| if (!containsKey(key)) { | |
| this[key] = ifAbsent(); | |
| } | |
| return this[key] as String; | |
| } | |
| void clear() { | |
| for (var key in keys) { | |
| remove(key); | |
| } | |
| } | |
| void forEach(void Function(String key, String value) f) { | |
| 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 Function() 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 Function(String key, String value) f) { | |
| _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(); | |
| } | |
| } |