Bug 1034110 - Provide a way to observe mutations for ::before/::after pseudo elements;r=smaug

Adds a new chrome-only MutationObserverInit option called nativeAnonymousChildList
that will cause a mutation to fire when a native anonymous root is bound or unbound
This commit is contained in:
Brian Grinstead 2015-09-24 08:23:32 -07:00
parent eb04a02f3c
commit 2d8c27349f
16 changed files with 399 additions and 3 deletions

View File

@ -738,6 +738,13 @@ DocAccessible::AttributeWillChange(nsIDocument* aDocument,
mStateBitWasOn = accessible->Unavailable();
}
void
DocAccessible::NativeAnonymousChildListChange(nsIDocument* aDocument,
nsIContent* aContent,
bool aIsRemove)
{
}
void
DocAccessible::AttributeChanged(nsIDocument* aDocument,
dom::Element* aElement,

View File

@ -332,6 +332,13 @@ nsSHEntryShared::AttributeWillChange(nsIDocument* aDocument,
{
}
void
nsSHEntryShared::NativeAnonymousChildListChange(nsIDocument* aDocument,
nsIContent* aContent,
bool aIsRemove)
{
}
void
nsSHEntryShared::AttributeChanged(nsIDocument* aDocument,
dom::Element* aElement,

View File

@ -1631,6 +1631,9 @@ Element::BindToTree(nsIDocument* aDocument, nsIContent* aParent,
}
nsNodeUtils::ParentChainChanged(this);
if (!hadParent && IsRootOfNativeAnonymousSubtree()) {
nsNodeUtils::NativeAnonymousChildListChange(this, false);
}
if (HasID()) {
AddToIdTable(DoGetID());
@ -1745,6 +1748,10 @@ Element::UnbindFromTree(bool aDeep, bool aNullParent)
}
}
if (this->IsRootOfNativeAnonymousSubtree()) {
nsNodeUtils::NativeAnonymousChildListChange(this, true);
}
if (GetParent()) {
nsRefPtr<nsINode> p;
p.swap(mParent);

View File

@ -114,6 +114,38 @@ nsMutationReceiver::Disconnect(bool aRemoveFromObserver)
}
}
void
nsMutationReceiver::NativeAnonymousChildListChange(nsIDocument* aDocument,
nsIContent* aContent,
bool aIsRemove) {
if (!NativeAnonymousChildList()) {
return;
}
nsINode* parent = aContent->GetParentNode();
if (!parent ||
(!Subtree() && Target() != parent) ||
(Subtree() && RegisterTarget()->SubtreeRoot() != parent->SubtreeRoot())) {
return;
}
nsDOMMutationRecord* m =
Observer()->CurrentRecord(nsGkAtoms::nativeAnonymousChildList);
if (m->mTarget) {
return;
}
m->mTarget = parent;
if (aIsRemove) {
m->mRemovedNodes = new nsSimpleContentList(parent);
m->mRemovedNodes->AppendElement(aContent);
} else {
m->mAddedNodes = new nsSimpleContentList(parent);
m->mAddedNodes->AppendElement(aContent);
}
}
void
nsMutationReceiver::AttributeWillChange(nsIDocument* aDocument,
mozilla::dom::Element* aElement,
@ -586,6 +618,8 @@ nsDOMMutationObserver::Observe(nsINode& aTarget,
bool attributeOldValue =
aOptions.mAttributeOldValue.WasPassed() &&
aOptions.mAttributeOldValue.Value();
bool nativeAnonymousChildList = aOptions.mNativeAnonymousChildList &&
nsContentUtils::ThreadsafeIsCallerChrome();
bool characterDataOldValue =
aOptions.mCharacterDataOldValue.WasPassed() &&
aOptions.mCharacterDataOldValue.Value();
@ -605,7 +639,8 @@ nsDOMMutationObserver::Observe(nsINode& aTarget,
characterData = true;
}
if (!(childList || attributes || characterData || animations)) {
if (!(childList || attributes || characterData || animations ||
nativeAnonymousChildList)) {
aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
return;
}
@ -655,6 +690,7 @@ nsDOMMutationObserver::Observe(nsINode& aTarget,
r->SetSubtree(subtree);
r->SetAttributeOldValue(attributeOldValue);
r->SetCharacterDataOldValue(characterDataOldValue);
r->SetNativeAnonymousChildList(nativeAnonymousChildList);
r->SetAttributeFilter(filters);
r->SetAllAttributes(allAttrs);
r->SetAnimations(animations);
@ -715,6 +751,7 @@ nsDOMMutationObserver::GetObservingInfo(
info.mSubtree = mr->Subtree();
info.mAttributeOldValue.Construct(mr->AttributeOldValue());
info.mCharacterDataOldValue.Construct(mr->CharacterDataOldValue());
info.mNativeAnonymousChildList = mr->NativeAnonymousChildList();
info.mAnimations.Construct(mr->Animations());
nsCOMArray<nsIAtom>& filters = mr->AttributeFilter();
if (filters.Count()) {

View File

@ -172,6 +172,16 @@ public:
mCharacterDataOldValue = aOldValue;
}
bool NativeAnonymousChildList()
{
return mParent ? mParent->NativeAnonymousChildList() : mNativeAnonymousChildList;
}
void SetNativeAnonymousChildList(bool aOldValue)
{
NS_ASSERTION(!mParent, "Shouldn't have parent");
mNativeAnonymousChildList = aOldValue;
}
bool Attributes() { return mParent ? mParent->Attributes() : mAttributes; }
void SetAttributes(bool aAttributes)
{
@ -298,6 +308,7 @@ private:
bool mChildList;
bool mCharacterData;
bool mCharacterDataOldValue;
bool mNativeAnonymousChildList;
bool mAttributes;
bool mAllAttributes;
bool mAttributeOldValue;
@ -362,6 +373,7 @@ public:
NS_DECL_ISUPPORTS
NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTEWILLCHANGE
NS_DECL_NSIMUTATIONOBSERVER_NATIVEANONYMOUSCHILDLISTCHANGE
NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATAWILLCHANGE
NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED

View File

@ -514,6 +514,8 @@ nsGenericDOMDataNode::BindToTree(nsIDocument* aDocument, nsIContent* aParent,
}
}
bool hadParent = !!GetParentNode();
// Set parent
if (aParent) {
if (!GetParent()) {
@ -548,6 +550,9 @@ nsGenericDOMDataNode::BindToTree(nsIDocument* aDocument, nsIContent* aParent,
}
nsNodeUtils::ParentChainChanged(this);
if (!hadParent && IsRootOfNativeAnonymousSubtree()) {
nsNodeUtils::NativeAnonymousChildListChange(this, false);
}
UpdateEditableState(false);
@ -570,6 +575,9 @@ nsGenericDOMDataNode::UnbindFromTree(bool aDeep, bool aNullParent)
HasFlag(NODE_FORCE_XBL_BINDINGS) ? OwnerDoc() : GetComposedDoc();
if (aNullParent) {
if (this->IsRootOfNativeAnonymousSubtree()) {
nsNodeUtils::NativeAnonymousChildListChange(this, true);
}
if (GetParent()) {
NS_RELEASE(mParent);
} else {

View File

@ -633,6 +633,7 @@ GK_ATOM(_namespace, "namespace")
GK_ATOM(namespaceAlias, "namespace-alias")
GK_ATOM(namespaceUri, "namespace-uri")
GK_ATOM(NaN, "NaN")
GK_ATOM(nativeAnonymousChildList, "nativeAnonymousChildList")
GK_ATOM(nav, "nav")
GK_ATOM(negate, "negate")
GK_ATOM(never, "never")

View File

@ -22,8 +22,8 @@ class Element;
} // namespace mozilla
#define NS_IMUTATION_OBSERVER_IID \
{ 0xdd74f0cc, 0x2849, 0x4d05, \
{ 0x9c, 0xe3, 0xb0, 0x95, 0x3e, 0xc2, 0xfd, 0x44 } }
{ 0x6d674c17, 0x0fbc, 0x4633, \
{ 0x8f, 0x46, 0x73, 0x4e, 0x87, 0xeb, 0xf0, 0xc7 } }
/**
* Information details about a characterdata change. Basically, we
@ -200,6 +200,18 @@ public:
int32_t aModType,
const nsAttrValue* aOldValue) = 0;
/**
* Notification that the root of a native anonymous has been added
* or removed.
*
* @param aDocument Owner doc of aContent
* @param aContent Anonymous node that's been added or removed
* @param aIsRemove True if it's a removal, false if an addition
*/
virtual void NativeAnonymousChildListChange(nsIDocument* aDocument,
nsIContent* aContent,
bool aIsRemove) {}
/**
* Notification that an attribute of an element has been
* set to the value it already had.
@ -346,6 +358,11 @@ NS_DEFINE_STATIC_IID_ACCESSOR(nsIMutationObserver, NS_IMUTATION_OBSERVER_IID)
int32_t aModType, \
const nsAttrValue* aNewValue) override;
#define NS_DECL_NSIMUTATIONOBSERVER_NATIVEANONYMOUSCHILDLISTCHANGE \
virtual void NativeAnonymousChildListChange(nsIDocument* aDocument, \
nsIContent* aContent, \
bool aIsRemove) override;
#define NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED \
virtual void AttributeChanged(nsIDocument* aDocument, \
mozilla::dom::Element* aElement, \
@ -383,6 +400,7 @@ NS_DEFINE_STATIC_IID_ACCESSOR(nsIMutationObserver, NS_IMUTATION_OBSERVER_IID)
NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATAWILLCHANGE \
NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED \
NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTEWILLCHANGE \
NS_DECL_NSIMUTATIONOBSERVER_NATIVEANONYMOUSCHILDLISTCHANGE \
NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED \
NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED \
NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED \
@ -419,6 +437,12 @@ _class::AttributeWillChange(nsIDocument* aDocument, \
{ \
} \
void \
_class::NativeAnonymousChildListChange(nsIDocument* aDocument, \
nsIContent* aContent, \
bool aIsRemove) \
{ \
} \
void \
_class::AttributeChanged(nsIDocument* aDocument, \
mozilla::dom::Element* aElement, \
int32_t aNameSpaceID, \

View File

@ -167,6 +167,15 @@ nsNodeUtils::ContentAppended(nsIContent* aContainer,
aNewIndexInContainer));
}
void
nsNodeUtils::NativeAnonymousChildListChange(nsIContent* aContent,
bool aIsRemove)
{
nsIDocument* doc = aContent->OwnerDoc();
IMPL_MUTATION_NOTIFICATION(NativeAnonymousChildListChange, aContent,
(doc, aContent, aIsRemove));
}
void
nsNodeUtils::ContentInserted(nsINode* aContainer,
nsIContent* aChild,

View File

@ -96,6 +96,15 @@ public:
nsIContent* aFirstNewContent,
int32_t aNewIndexInContainer);
/**
* Send NativeAnonymousChildList notifications to nsIMutationObservers
* @param aContent Anonymous node that's been added or removed
* @param aIsRemove True if it's a removal, false if an addition
* @see nsIMutationObserver::NativeAnonymousChildListChange
*/
static void NativeAnonymousChildListChange(nsIContent* aContent,
bool aIsRemove);
/**
* Send ContentInserted notifications to nsIMutationObservers
* @param aContainer Node into which new child was inserted

View File

@ -65,6 +65,7 @@ skip-if = buildapp == 'mulet'
[test_cpows.xul]
skip-if = buildapp == 'mulet'
[test_document_register.xul]
[test_mutationobserver_anonymous.html]
[test_registerElement_content.xul]
[test_registerElement_ep.xul]
[test_domparsing.xul]

View File

@ -0,0 +1,247 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1034110
-->
<head>
<title>Test for Bug 1034110</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1034110">Mozilla Bug 1034110</a>
<style type="text/css">
#pseudo.before::before { content: "before"; }
#pseudo.after::after { content: "after"; }
</style>
<div id="pseudo"></div>
<video id="video"></video>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
<script type="application/javascript;version=1.7">
/** Test for Bug 1034110 **/
SimpleTest.waitForExplicitFinish();
const {Cc, Ci, Cu} = SpecialPowers;
function getWalker(node) {
let walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].createInstance(Ci.inIDeepTreeWalker);
walker.showAnonymousContent = true;
walker.init(node.ownerDocument, Ci.nsIDOMNodeFilter.SHOW_ALL);
walker.currentNode = node;
return walker;
}
function getFirstChild(parent) {
return SpecialPowers.unwrap(getWalker(parent).firstChild());
}
function getLastChild(parent) {
return SpecialPowers.unwrap(getWalker(parent).lastChild());
}
function assertSamePseudoElement(which, node1, node2) {
is(node1.nodeName, "_moz_generated_content_" + which,
"Correct pseudo element type");
is(node1, node2,
"Referencing the same ::after element");
}
window.onload = function () {
testOneAdded();
};
function testOneAdded() {
let parent = document.getElementById("pseudo");
var m = new MutationObserver(function(records, observer) {
is(records.length, 1, "Correct number of records");
is(records[0].type, "nativeAnonymousChildList", "Correct record type");
is(records[0].target, parent, "Correct target");
is(records[0].addedNodes.length, 1, "Should have got addedNodes");
assertSamePseudoElement("before", records[0].addedNodes[0], getFirstChild(parent));
is(records[0].removedNodes.length, 0, "Shouldn't have got removedNodes");
observer.disconnect();
testAddedAndRemoved();
});
m.observe(parent, { nativeAnonymousChildList: true});
parent.className = "before";
}
function testAddedAndRemoved() {
let parent = document.getElementById("pseudo");
let originalBeforeElement = getFirstChild(parent);
var m = new MutationObserver(function(records, observer) {
is(records.length, 2, "Correct number of records");
is(records[0].type, "nativeAnonymousChildList", "Correct record type (1)");
is(records[1].type, "nativeAnonymousChildList", "Correct record type (2)");
is(records[0].target, parent, "Correct target (1)");
is(records[1].target, parent, "Correct target (2)");
// Two records are sent - one for removed and one for added.
is(records[0].addedNodes.length, 0, "Shouldn't have got addedNodes");
is(records[0].removedNodes.length, 1, "Should have got removedNodes");
assertSamePseudoElement("before", records[0].removedNodes[0], originalBeforeElement);
is(records[1].addedNodes.length, 1, "Should have got addedNodes");
assertSamePseudoElement("after", records[1].addedNodes[0], getLastChild(parent));
is(records[1].removedNodes.length, 0, "Shouldn't have got removedNodes");
observer.disconnect();
testRemoved();
});
m.observe(parent, { nativeAnonymousChildList: true});
parent.className = "after";
}
function testRemoved() {
let parent = document.getElementById("pseudo");
let originalAfterElement = getLastChild(parent);
var m = new MutationObserver(function(records, observer) {
is(records.length, 1, "Correct number of records");
is(records[0].type, "nativeAnonymousChildList", "Correct record type");
is(records[0].target, parent, "Correct target");
is(records[0].addedNodes.length, 0, "Shouldn't have got addedNodes");
is(records[0].removedNodes.length, 1, "Should have got removedNodes");
assertSamePseudoElement("after", records[0].removedNodes[0], originalAfterElement);
observer.disconnect();
testMultipleAdded();
});
m.observe(parent, { nativeAnonymousChildList: true });
parent.className = "";
}
function testMultipleAdded() {
let parent = document.getElementById("pseudo");
var m = new MutationObserver(function(records, observer) {
is(records.length, 2, "Correct number of records");
is(records[0].type, "nativeAnonymousChildList", "Correct record type (1)");
is(records[1].type, "nativeAnonymousChildList", "Correct record type (2)");
is(records[0].target, parent, "Correct target (1)");
is(records[1].target, parent, "Correct target (2)");
is(records[0].addedNodes.length, 1, "Should have got addedNodes");
assertSamePseudoElement("before", records[0].addedNodes[0], getFirstChild(parent));
is(records[0].removedNodes.length, 0, "Shouldn't have got removedNodes");
is(records[1].addedNodes.length, 1, "Should have got addedNodes");
assertSamePseudoElement("after", records[1].addedNodes[0], getLastChild(parent));
is(records[1].removedNodes.length, 0, "Shouldn't have got removedNodes");
observer.disconnect();
testRemovedDueToDisplay();
});
m.observe(parent, { nativeAnonymousChildList: true });
parent.className = "before after";
}
function testRemovedDueToDisplay() {
let parent = document.getElementById("pseudo");
let originalBeforeElement = getFirstChild(parent);
let originalAfterElement = getLastChild(parent);
var m = new MutationObserver(function(records, observer) {
is(records.length, 2, "Correct number of records");
is(records[0].type, "nativeAnonymousChildList", "Correct record type (1)");
is(records[1].type, "nativeAnonymousChildList", "Correct record type (2)");
is(records[0].target, parent, "Correct target (1)");
is(records[1].target, parent, "Correct target (2)");
is(records[0].addedNodes.length, 0, "Shouldn't have got addedNodes");
is(records[0].removedNodes.length, 1, "Should have got removedNodes");
assertSamePseudoElement("before", records[0].removedNodes[0], originalBeforeElement);
is(records[1].addedNodes.length, 0, "Shouldn't have got addedNodes");
is(records[1].removedNodes.length, 1, "Should have got removedNodes");
assertSamePseudoElement("after", records[1].removedNodes[0], originalAfterElement);
observer.disconnect();
testAddedDueToDisplay();
});
m.observe(parent, { nativeAnonymousChildList: true });
parent.style.display = "none";
}
function testAddedDueToDisplay() {
let parent = document.getElementById("pseudo");
var m = new MutationObserver(function(records, observer) {
is(records.length, 2, "Correct number of records");
is(records[0].type, "nativeAnonymousChildList", "Correct record type (1)");
is(records[1].type, "nativeAnonymousChildList", "Correct record type (2)");
is(records[0].target, parent, "Correct target (1)");
is(records[1].target, parent, "Correct target (2)");
is(records[0].addedNodes.length, 1, "Should have got addedNodes");
assertSamePseudoElement("before", records[0].addedNodes[0], getFirstChild(parent));
is(records[0].removedNodes.length, 0, "Shouldn't have got removedNodes");
is(records[1].addedNodes.length, 1, "Should have got addedNodes");
assertSamePseudoElement("after", records[1].addedNodes[0], getLastChild(parent));
is(records[1].removedNodes.length, 0, "Shouldn't have got removedNodes");
observer.disconnect();
testDifferentTargetNoSubtree();
});
m.observe(parent, { nativeAnonymousChildList: true });
parent.style.display = "block";
}
function testDifferentTargetNoSubtree() {
let parent = document.getElementById("pseudo");
var m = new MutationObserver(function(records, observer) {
ok(false,
"No mutation should fire when observing on a parent without subtree option.");
});
m.observe(document, { nativeAnonymousChildList: true });
parent.style.display = "none";
setTimeout(() => {
ok(!getFirstChild(parent), "Pseudo element has been removed, but no mutation");
ok(!getLastChild(parent), "Pseudo element has been removed, but no mutation");
testSubtree();
}, 0);
}
function testSubtree() {
let parent = document.getElementById("pseudo");
var m = new MutationObserver(function(records, observer) {
is(records.length, 2, "Correct number of records");
is(records[0].type, "nativeAnonymousChildList", "Correct record type (1)");
is(records[1].type, "nativeAnonymousChildList", "Correct record type (2)");
is(records[0].target, parent, "Correct target (1)");
is(records[1].target, parent, "Correct target (2)");
is(records[0].addedNodes.length, 1, "Should have got addedNodes");
assertSamePseudoElement("before", records[0].addedNodes[0], getFirstChild(parent));
is(records[0].removedNodes.length, 0, "Shouldn't have got removedNodes");
is(records[1].addedNodes.length, 1, "Should have got addedNodes");
assertSamePseudoElement("after", records[1].addedNodes[0], getLastChild(parent));
is(records[1].removedNodes.length, 0, "Shouldn't have got removedNodes");
observer.disconnect();
SimpleTest.finish();
});
m.observe(document, { nativeAnonymousChildList: true, subtree: true });
parent.style.display = "block";
}
</script>
</pre>
</body>
</html>

View File

@ -909,6 +909,19 @@ function testAttributeRecordMerging4() {
m.disconnect();
div.innerHTML = "";
div.removeAttribute("foo");
then(testChromeOnly);
}
function testChromeOnly() {
// Content can't access nativeAnonymousChildList
try {
var mo = new M(function(records, observer) { });
mo.observe(div, { nativeAnonymousChildList: true });
ok(false, "Should have thrown when trying to observe with chrome-only init");
} catch (e) {
ok(true, "Throws when trying to observe with chrome-only init");
}
then();
}

View File

@ -61,6 +61,8 @@ dictionary MutationObserverInit {
boolean attributeOldValue;
boolean characterDataOldValue;
// [ChromeOnly]
boolean nativeAnonymousChildList = false;
// [ChromeOnly]
boolean animations;
sequence<DOMString> attributeFilter;
};

View File

@ -194,6 +194,13 @@ nsFormFillController::AttributeWillChange(nsIDocument* aDocument,
{
}
void
nsFormFillController::NativeAnonymousChildListChange(nsIDocument* aDocument,
nsIContent* aContent,
bool aIsRemove)
{
}
void
nsFormFillController::ParentChainChanged(nsIContent* aContent)
{

View File

@ -106,6 +106,11 @@ void nsMenuGroupOwnerX::AttributeWillChange(nsIDocument* aDocument,
{
}
void nsMenuGroupOwnerX::NativeAnonymousChildListChange(nsIDocument* aDocument,
nsIContent* aContent,
bool aIsRemove)
{
}
void nsMenuGroupOwnerX::AttributeChanged(nsIDocument* aDocument,
dom::Element* aElement,