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); }