diff --git a/integration_tests/snapshots/css/css-selectors/child-selectors.ts.3726a6511.png b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.3726a6511.png
new file mode 100644
index 0000000000..8a5049a4fb
Binary files /dev/null and b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.3726a6511.png differ
diff --git a/integration_tests/snapshots/css/css-selectors/child-selectors.ts.3726a6512.png b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.3726a6512.png
new file mode 100644
index 0000000000..8bb4030942
Binary files /dev/null and b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.3726a6512.png differ
diff --git a/integration_tests/snapshots/css/css-selectors/child-selectors.ts.3726a6513.png b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.3726a6513.png
new file mode 100644
index 0000000000..8c601d3701
Binary files /dev/null and b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.3726a6513.png differ
diff --git a/integration_tests/snapshots/css/css-selectors/child-selectors.ts.5214bfb91.png b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.5214bfb91.png
new file mode 100644
index 0000000000..8d64421a95
Binary files /dev/null and b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.5214bfb91.png differ
diff --git a/integration_tests/snapshots/css/css-selectors/child-selectors.ts.5214bfb92.png b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.5214bfb92.png
new file mode 100644
index 0000000000..77c80d9b4d
Binary files /dev/null and b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.5214bfb92.png differ
diff --git a/integration_tests/snapshots/css/css-selectors/child-selectors.ts.5214bfb93.png b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.5214bfb93.png
new file mode 100644
index 0000000000..f6a8729d83
Binary files /dev/null and b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.5214bfb93.png differ
diff --git a/integration_tests/snapshots/css/css-selectors/child-selectors.ts.c51e455f1.png b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.c51e455f1.png
new file mode 100644
index 0000000000..25fa5bba92
Binary files /dev/null and b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.c51e455f1.png differ
diff --git a/integration_tests/snapshots/css/css-selectors/child-selectors.ts.c51e455f2.png b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.c51e455f2.png
new file mode 100644
index 0000000000..a3c6144e58
Binary files /dev/null and b/integration_tests/snapshots/css/css-selectors/child-selectors.ts.c51e455f2.png differ
diff --git a/integration_tests/specs/css/css-selectors/child-selectors.ts b/integration_tests/specs/css/css-selectors/child-selectors.ts
index 35e45b56da..895fec15d7 100644
--- a/integration_tests/specs/css/css-selectors/child-selectors.ts
+++ b/integration_tests/specs/css/css-selectors/child-selectors.ts
@@ -304,4 +304,133 @@ describe('css child selector', () => {
document.body.appendChild(p9);
await snapshot();
});
+
+ it("015", async () => {
+ const style = (
+
+ );
+ const div =
;
+ const item1 = 1
;
+ const item2 = 2
;
+ div.appendChild(item1);
+ div.appendChild(item2);
+ document.head.appendChild(style);
+ document.body.appendChild(div);
+ await snapshot();
+
+ const item3 = 3
;
+ div.appendChild(item3);
+ await snapshot(0.5);
+
+ div.removeChild(item2);
+ await snapshot(0.5);
+ });
+
+
+ it("016", async () => {
+ const style = (
+
+ );
+ const div = ;
+ const item1 = 1
;
+ const item2 = 2
;
+ div.appendChild(item1);
+ div.appendChild(item2);
+ document.head.appendChild(style);
+ document.body.appendChild(div);
+ await snapshot();
+
+ const item3 = 3
;
+ div.appendChild(item3);
+ await snapshot(0.5);
+
+ div.removeChild(item1);
+ await snapshot(0.5);
+ });
+
+ fit("017", async () => {
+ const style = (
+
+ );
+ const div = ;
+ const item1 = 1
;
+ const item2 = 2
;
+ div.appendChild(item1);
+ div.appendChild(item2);
+ document.head.appendChild(style);
+ document.body.appendChild(div);
+ await snapshot();
+
+ div.removeChild(item2);
+ await snapshot(0.5);
+ });
});
diff --git a/webf/lib/src/css/query_selector.dart b/webf/lib/src/css/query_selector.dart
index 0305e68f61..2117789055 100644
--- a/webf/lib/src/css/query_selector.dart
+++ b/webf/lib/src/css/query_selector.dart
@@ -192,6 +192,9 @@ class SelectorEvaluator extends SelectorVisitor {
// http://dev.w3.org/csswg/selectors-4/#the-first-child-pseudo
case 'first-child':
+ if (_element!.parentElement != null) {
+ _element!.parentElement!.addFlag(DynamicRestyleFlag.ChildrenAffectedByFirstChildRules);
+ }
if (_element!.previousElementSibling != null) {
return _element!.previousElementSibling is HeadElement;
}
@@ -199,6 +202,9 @@ class SelectorEvaluator extends SelectorVisitor {
// http://dev.w3.org/csswg/selectors-4/#the-last-child-pseudo
case 'last-child':
+ if (_element!.nextSibling != null && _element!.parentElement != null) {
+ _element!.parentElement!.addFlag(DynamicRestyleFlag.ChildrenAffectedByLastChildRules);
+ }
return _element!.nextSibling == null;
//http://drafts.csswg.org/selectors-4/#first-of-type-pseudo
@@ -218,14 +224,23 @@ class SelectorEvaluator extends SelectorVisitor {
var isLast = index == children.length - 1;
if (isFirst && node.name == 'first-of-type') {
+ if (_element!.parentElement != null) {
+ _element!.parentElement!.addFlag(DynamicRestyleFlag.ChildrenAffectedByForwardPositionalRules);
+ }
return true;
}
if (isLast && node.name == 'last-of-type') {
+ if (_element!.parentElement != null) {
+ _element!.parentElement!.addFlag(DynamicRestyleFlag.ChildrenAffectedByBackwardPositionalRules);
+ }
return true;
}
if (isFirst && isLast && node.name == 'only-of-type') {
+ if (_element!.parentElement != null) {
+ _element!.parentElement!.addFlag(DynamicRestyleFlag.ChildrenAffectedByFirstChildRules);
+ }
return true;
}
@@ -235,8 +250,14 @@ class SelectorEvaluator extends SelectorVisitor {
break;
// http://dev.w3.org/csswg/selectors-4/#the-only-child-pseudo
case 'only-child':
- return _element!.previousSibling == null && _element!.nextSibling == null;
-
+ if (_element!.parentElement != null) {
+ _element!.parentElement!.addFlag(DynamicRestyleFlag.ChildrenAffectedByFirstChildRules);
+ _element!.parentElement!.addFlag(DynamicRestyleFlag.ChildrenAffectedByLastChildRules);
+ }
+ if (_element!.previousSibling == null && _element!.nextSibling == null) {
+ return true;
+ }
+ return false;
// http://dev.w3.org/csswg/selectors-4/#link
case 'link':
return _element!.attributes['href'] != null;
diff --git a/webf/lib/src/dom/container_node.dart b/webf/lib/src/dom/container_node.dart
index 7193f756ba..e48e32ba44 100644
--- a/webf/lib/src/dom/container_node.dart
+++ b/webf/lib/src/dom/container_node.dart
@@ -10,6 +10,27 @@ import 'package:webf/src/dom/node_traversal.dart';
typedef InsertNodeHandler = void Function(ContainerNode container, Node child, Node? next);
+enum DynamicRestyleFlag {
+ ChildrenAffectedByFirstChildRules,
+ ChildrenAffectedByLastChildRules,
+ ChildrenAffectedByDirectAdjacentRules,
+ ChildrenAffectedByForwardPositionalRules,
+ ChildrenAffectedByBackwardPositionalRules,
+}
+
+extension StructuralRules on DynamicRestyleFlag {
+ bool childrenAffectedByStructuralRules() {
+ if (this == DynamicRestyleFlag.ChildrenAffectedByFirstChildRules ||
+ this == DynamicRestyleFlag.ChildrenAffectedByLastChildRules ||
+ this == DynamicRestyleFlag.ChildrenAffectedByDirectAdjacentRules ||
+ this == DynamicRestyleFlag.ChildrenAffectedByForwardPositionalRules ||
+ this == DynamicRestyleFlag.ChildrenAffectedByBackwardPositionalRules) {
+ return true;
+ }
+ return false;
+ }
+}
+
bool collectChildrenAndRemoveFromOldParent(Node node, List nodes) {
if (node is DocumentFragment) {
getChildNodes(node, nodes);
@@ -34,6 +55,15 @@ void getChildNodes(ContainerNode node, List nodes) {
abstract class ContainerNode extends Node {
ContainerNode(NodeType nodeType, [BindingContext? context]) : super(nodeType, context);
+ List? restyleFlags;
+
+ void addFlag(DynamicRestyleFlag flag) {
+ restyleFlags ??= [];
+ if (restyleFlags?.contains(flag) == false) {
+ restyleFlags?.add(flag);
+ }
+ }
+
void _adoptAndAppendChild(ContainerNode container, Node child, Node? next) {
child.parentOrShadowHostNode = this;
if (lastChild != null) {
@@ -333,6 +363,70 @@ abstract class ContainerNode extends Node {
}
}
+ void checkForSiblingStyleChanges(Element parent, bool isRemoved, Node? nodeBeforeChange, Node? nodeAfterChange) {
+
+ if (!isRendererAttached) {
+ return;
+ }
+
+ final elementBeforeChange = nodeBeforeChange as Element?;
+ final elementAfterChange = nodeAfterChange as Element?;
+
+ // :first-child. In the parser callback case, we don't have to check anything, since we were right the first time.
+ // In the DOM case, we only need to do something if |afterChange| is not 0.
+ // |afterChange| is 0 in the parser case, so it works out that we'll skip this block.
+ if (elementAfterChange != null &&
+ restyleFlags?.contains(DynamicRestyleFlag.ChildrenAffectedByFirstChildRules) == true) {
+ // Find our new first child.
+ final newFirstElement = parent.firstChild as Element?;
+
+ // This is the insert/append case.
+ if (newFirstElement != elementAfterChange && elementAfterChange.isRendererAttached) {
+ elementAfterChange.recalculateStyle();
+ }
+
+ if (newFirstElement != null && isRemoved && newFirstElement == elementAfterChange) {
+ newFirstElement.recalculateStyle();
+ }
+ }
+
+ if (elementBeforeChange != null &&
+ restyleFlags?.contains(DynamicRestyleFlag.ChildrenAffectedByLastChildRules) == true) {
+ // Find our new first child.
+ final newLastElement = parent.lastChild as Element?;
+
+ // This is the insert/append case.
+ if (newLastElement != elementBeforeChange && elementBeforeChange.isRendererAttached) {
+ elementBeforeChange.recalculateStyle();
+ }
+
+ if (newLastElement != null && isRemoved && newLastElement == elementBeforeChange) {
+ newLastElement.recalculateStyle();
+ }
+ }
+
+ // The + selector. We need to invalidate the first element following the insertion point. It is the only possible element
+ // that could be affected by this DOM change.
+ if (restyleFlags?.contains(DynamicRestyleFlag.ChildrenAffectedByDirectAdjacentRules) == true && elementAfterChange != null) {
+ elementAfterChange.recalculateStyle();
+ }
+
+ // Forward positional selectors include the ~ selector, nth-child, nth-of-type, first-of-type and only-of-type.
+ // Backward positional selectors include nth-last-child, nth-last-of-type, last-of-type and only-of-type.
+ // We have to invalidate everything following the insertion point in the forward case, and everything before the insertion point in the
+ // backward case.
+ // |afterChange| is 0 in the parser callback case, so we won't do any work for the forward case if we don't have to.
+ // For performance reasons we just mark the parent node as changed, since we don't want to make childrenChanged O(n^2) by crawling all our kids
+ // here. recalcStyle will then force a walk of the children when it sees that this has happened.
+ if (elementAfterChange != null &&
+ restyleFlags?.contains(DynamicRestyleFlag.ChildrenAffectedByForwardPositionalRules) == true) {
+ parent.recalculateStyle();
+ } else if (elementBeforeChange != null &&
+ restyleFlags?.contains(DynamicRestyleFlag.ChildrenAffectedByBackwardPositionalRules) == true) {
+ parent.recalculateStyle();
+ }
+ }
+
Node? _firstChild;
@override
diff --git a/webf/lib/src/dom/element.dart b/webf/lib/src/dom/element.dart
index 8f3e4cd2ee..f0f58ee4dc 100644
--- a/webf/lib/src/dom/element.dart
+++ b/webf/lib/src/dom/element.dart
@@ -1193,6 +1193,20 @@ abstract class Element extends ContainerNode with ElementBase, ElementEventMixin
}
}
+ @override
+ void childrenChanged(ChildrenChange change) {
+ super.childrenChanged(change);
+
+ if (change.byParser != ChildrenChangeSource.PARSER && change.isChildElementChange()) {
+ final changedElement = change.siblingChanged as Element?;
+ final removed = change.type == ChildrenChangeType.ELEMENT_REMOVED;
+ if (changedElement != null) {
+ checkForSiblingStyleChanges(this, removed, change.siblingBeforeChange,
+ change.siblingAfterChange);
+ }
+ }
+ }
+
void _updateNameMap(String? newName, {String? oldName}) {
if (oldName != null && oldName.isNotEmpty) {
final elements = ownerDocument.elementsByName[oldName];
@@ -1796,6 +1810,7 @@ abstract class Element extends ContainerNode with ElementBase, ElementEventMixin
}
void _applySheetStyle(CSSStyleDeclaration style) {
+
CSSStyleDeclaration matchRule = _elementRuleCollector.collectionFromRuleSet(ownerDocument.ruleSet, this);
style.union(matchRule);
}