Skip to content
Open
5 changes: 5 additions & 0 deletions web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
- Added `URL.toDart` and `Uri.toJS` extension methods.
- Added missing `Document` and `Window` pointer event getters: `onDrag*`,
`onTouch*`, `onMouse*`.
- Added `HTMLCollectionListWrapper` and `NodeListListWrapper` to support
mutable operations on node lists.
- Added `childNodesAsList` to `Node` and `childrenAsList` to `Element` via
extensions.
- Added `asList` to `NodeList` via extension.

## 1.1.1

Expand Down
16 changes: 16 additions & 0 deletions web/lib/src/helpers/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import 'dart:convert';
import 'dart:js_interop';

import '../dom.dart';
import 'lists.dart';

export 'cross_origin.dart'
show CrossOriginContentWindowExtension, CrossOriginWindowExtension;
Expand Down Expand Up @@ -103,3 +104,18 @@ extension UriToURL on Uri {
}
}
}

extension NodeExtension on Node {
/// Returns [childNodes] as a modifiable [List].
List<Node> get childNodesAsList => NodeListListWrapper(this, childNodes);
}

extension ElementExtension on Element {
/// Returns [children] as a modifiable [List].
List<Element> get childrenAsList => HTMLCollectionListWrapper(this, children);
}

extension NodeListExtension on NodeList {
/// Returns node list as a modifiable [List].
List<Element> get asList => JSImmutableListWrapper(this);
}
309 changes: 308 additions & 1 deletion web/lib/src/helpers/lists.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'dart:collection';
import 'dart:js_interop';
import '../dom/dom.dart';

/// `_JSList` acts as a wrapper around a JS list object providing an interface to
/// access the list items and list length while also allowing us to specify the
Expand Down Expand Up @@ -65,7 +66,313 @@ class JSImmutableListWrapper<T extends JSObject, U extends JSObject>
if (length > 1) throw StateError('More than one element');
return first;
}
}

/// This mixin exists to avoid repetition in `NodeListListWrapper` and `HTMLCollectionListWrapper`
/// It can be also used for `HTMLCollection` and `NodeList` that is
/// [live](https://developer.mozilla.org/en-US/docs/Web/API/NodeList#live_vs._static_nodelists)
/// and can be safely modified at runtime.
/// This requires an instance of `P`, a container that elements would be added to or removed from.
abstract mixin class _LiveNodeListMixin<P extends Node, U extends Node> {
P get _parent;
_JSList<U> get _list;

bool contains(Object? element) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this is an optimization over having to check the contents of the list.

// TODO(srujzs): migrate this ifs to isJSAny once we have it
// ignore: invalid_runtime_check_with_js_interop_types
if ((element is JSAny?) && (element?.isA<Node>() ?? false)) {
if ((element as Node).parentNode.strictEquals(_parent).toDart) {
return true;
}
}
return false;
}

bool remove(Object? element) {
if (contains(element)) {
_parent.removeChild(element as Node);
return true;
} else {
return false;
}
}

int get length => _list.length;

set length(int value) {
if (value > length) {
throw UnsupportedError('Cannot add empty nodes.');
}
for (var i = length - 1; i >= value; i--) {
_parent.removeChild(_list.item(i));
}
}

U operator [](int index) {
if (index > length || index < 0) {
throw IndexError.withLength(index, length, indexable: this);
}
return _list.item(index);
}

void operator []=(int index, U value) {
RangeError.checkValidRange(index, null, length);
_parent.replaceChild(value, _list.item(index));
}

void add(U value) {
_parent.appendChild(value);
}

void removeRange(int start, int end) {
RangeError.checkValidRange(start, end, length);
for (var i = 0; i < end - start; i++) {
_parent.removeChild(this[start]);
}
}

U removeAt(int index) {
final result = this[index];
_parent.removeChild(result);
return result;
}

void fillRange(int start, int end, [U? fill]) {
// without cloning the element we would end up with one `fill` instance
// this method does not make much sense in nodes lists
throw UnsupportedError('Cannot fillRange on Node list');
}

U get last;

U removeLast() {
final result = last;
_parent.removeChild(result);
return result;
}

void removeWhere(bool Function(U element) test) {
_filter(test, true);
}

void retainWhere(bool Function(U element) test) {
_filter(test, false);
}

Iterator<U> get iterator;

void _filter(bool Function(U element) test, bool removeMatching) {
// This implementation of removeWhere/retainWhere is more efficient
// than the default in ListBase. Child nodes can be removed in constant
// time.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can just short-circuit if removeMatching is false?

final i = iterator;
U? removeMe;
while (i.moveNext()) {
if (removeMe != null) {
_parent.removeChild(removeMe);
removeMe = null;
}
if (test(i.current) == removeMatching) {
removeMe = i.current;
}
}
if (removeMe != null) {
_parent.removeChild(removeMe);
removeMe = null;
}
}

void insert(int index, U element) {
if (index < 0 || index > length) {
throw RangeError.range(index, 0, length);
}
if (index == length) {
_parent.appendChild(element);
} else {
_parent.insertBefore(element, this[index]);
}
}

void addAll(Iterable<U> iterable) {
if (iterable is _LiveNodeListMixin) {
final otherList = iterable as _LiveNodeListMixin;
if (otherList._parent.strictEquals(_parent).toDart) {
throw ArgumentError('Cannot add nodes from same parent');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a no-op in _ChildNodeListLazy. Maybe we should return?

}
// Optimized route for copying between nodes.
for (var len = otherList.length; len > 0; --len) {
_parent.appendChild(otherList._parent.firstChild!);
}
}

for (var element in iterable) {
_parent.appendChild(element);
}
}

void insertAll(int index, Iterable<U> iterable) {
if (index == length) {
addAll(iterable);
} else {
final child = this[index];
if (iterable is _LiveNodeListMixin) {
final otherList = iterable as _LiveNodeListMixin;
if (otherList._parent.strictEquals(_parent).toDart) {
throw ArgumentError('Cannot add nodes from same parent');
}
// Optimized route for copying between nodes.
for (var len = otherList.length; len > 0; --len) {
_parent.insertBefore(otherList._parent.firstChild!, child);
}
} else {
for (var node in iterable) {
_parent.insertBefore(node, child);
}
}
}
}
}

/// Allows iterating `HTMLCollection` with `nextElementSibling` for optimisation and easier encapsulation
class _HTMLCollectionIterator implements Iterator<Element> {
@override
Element get current => _current!;

Element? _current;
bool start = true;

_HTMLCollectionIterator(this._current);

@override
U elementAt(int index) => this[index];
bool moveNext() {
if (start) {
start = false;
} else {
_current = _current?.nextElementSibling;
}
return _current != null;
}
}

/// Wrapper for `HTMLCollection` returned from `children` that implements modifiable list interface and allows easier DOM manipulation.
/// This is loosely based on `_ChildrenElementList` from `dart:html` to preserve compatibility
class HTMLCollectionListWrapper
with ListMixin<Element>, _LiveNodeListMixin<Element, Element> {
@override
final Element _parent;
@override
_JSList<Element> get _list => _JSList<Element>(_htmlCollection);

final HTMLCollection _htmlCollection;

HTMLCollectionListWrapper(this._parent, this._htmlCollection);

@override
Iterator<Element> get iterator =>
_HTMLCollectionIterator(_parent.firstElementChild);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Do we need a separate iterator class vs toList().iterator like dart:html does instead?


@override
bool get isEmpty {
return _parent.firstElementChild == null;
}

@override
Element get first {
final result = _parent.firstElementChild;
if (result == null) throw StateError('No elements');
return result;
}

@override
Element get last {
final result = _parent.lastElementChild;
if (result == null) throw StateError('No elements');
return result;
}

@override
Element get single {
final l = length;
if (l == 0) throw StateError('No elements');
if (l > 1) throw StateError('More than one element');
return _parent.firstElementChild!;
}

@override
void clear() {
while (_parent.firstElementChild != null) {
_parent.removeChild(_parent.firstElementChild!);
}
}
}

/// Allows iterating `NodeList` with `nextSibling` for optimisation and easier encapsulation
class _NodeListIterator implements Iterator<Node> {
@override
Node get current => _current!;

Node? _current;
bool start = true;

_NodeListIterator(this._current);

@override
bool moveNext() {
if (start) {
start = false;
} else {
_current = _current?.nextSibling;
}
return _current != null;
}
}

/// Wrapper for `NodeList` returned from `childNodes` that implements modifiable list interface and allows easier DOM manipulation.
/// This is loosely based on `_ChildNodeListLazy` from `dart:html` to preserve compatibility
class NodeListListWrapper with ListMixin<Node>, _LiveNodeListMixin<Node, Node> {
@override
final Node _parent;
@override
_JSList<Node> get _list => _JSList<Node>(_nodeList);

final NodeList _nodeList;

NodeListListWrapper(this._parent, this._nodeList);

@override
Iterator<Node> get iterator => _NodeListIterator(_parent.firstChild);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above.


@override
bool get isEmpty {
return _parent.firstChild == null;
}

@override
Node get first {
final result = _parent.firstChild;
if (result == null) throw StateError('No elements');
return result;
}

@override
Node get last {
final result = _parent.lastChild;
if (result == null) throw StateError('No elements');
return result;
}

@override
Node get single {
final l = length;
if (l == 0) throw StateError('No elements');
if (l > 1) throw StateError('More than one element');
return _parent.firstChild!;
}

@override
void clear() {
while (_parent.firstChild != null) {
_parent.removeChild(_parent.firstChild!);
}
}
}
Loading