Auto merge of #16883 - jdm:mutationobserver, r=jdm

Mutation Observer API

Rebased from #16668.

- [X] `./mach build -d` does not report any errors
- [X] `./mach test-tidy` does not report any errors
- [X] These changes fix (partially) #6633
- [X] There are tests for these changes

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/16883)
<!-- Reviewable:end -->
This commit is contained in:
bors-servo 2017-05-17 01:21:40 -05:00 committed by GitHub
commit 5da0aa9f11
15 changed files with 432 additions and 180 deletions

View file

@ -10,6 +10,8 @@ use dom::bindings::js::{LayoutJS, MutNullableJS, Root, RootedReference};
use dom::bindings::reflector::{Reflector, reflect_dom_object};
use dom::bindings::str::DOMString;
use dom::element::{AttributeMutation, Element};
use dom::mutationobserver::{Mutation, MutationObserver};
use dom::node::Node;
use dom::virtualmethods::vtable_for;
use dom::window::Window;
use dom_struct::dom_struct;
@ -170,6 +172,12 @@ impl AttrMethods for Attr {
impl Attr {
pub fn set_value(&self, mut value: AttrValue, owner: &Element) {
let name = self.local_name().clone();
let namespace = self.namespace().clone();
let old_value = DOMString::from(&**self.value());
let mutation = Mutation::Attribute { name, namespace, old_value };
MutationObserver::queue_a_mutation_record(owner.upcast::<Node>(), mutation);
assert!(Some(owner) == self.owner().r());
owner.will_mutate_attr(self);
self.swap_value(&mut value);

View file

@ -63,6 +63,7 @@ use dom::htmltablerowelement::{HTMLTableRowElement, HTMLTableRowElementLayoutHel
use dom::htmltablesectionelement::{HTMLTableSectionElement, HTMLTableSectionElementLayoutHelpers};
use dom::htmltemplateelement::HTMLTemplateElement;
use dom::htmltextareaelement::{HTMLTextAreaElement, LayoutHTMLTextAreaElementHelpers};
use dom::mutationobserver::{Mutation, MutationObserver};
use dom::namednodemap::NamedNodeMap;
use dom::node::{CLICK_IN_PROGRESS, ChildrenMutation, LayoutNodeHelpers, Node};
use dom::node::{NodeDamage, SEQUENTIALLY_FOCUSABLE, UnbindContext};
@ -1003,6 +1004,12 @@ impl Element {
}
pub fn push_attribute(&self, attr: &Attr) {
let name = attr.local_name().clone();
let namespace = attr.namespace().clone();
let old_value = DOMString::from(&**attr.value());
let mutation = Mutation::Attribute { name, namespace, old_value };
MutationObserver::queue_a_mutation_record(&self.node, mutation);
assert!(attr.GetOwnerElement().r() == Some(self));
self.will_mutate_attr(attr);
self.attrs.borrow_mut().push(JS::from_ref(attr));
@ -1125,13 +1132,18 @@ impl Element {
}
fn remove_first_matching_attribute<F>(&self, find: F) -> Option<Root<Attr>>
where F: Fn(&Attr) -> bool
{
where F: Fn(&Attr) -> bool {
let idx = self.attrs.borrow().iter().position(|attr| find(&attr));
idx.map(|idx| {
let attr = Root::from_ref(&*(*self.attrs.borrow())[idx]);
self.will_mutate_attr(&attr);
let name = attr.local_name().clone();
let namespace = attr.namespace().clone();
let old_value = DOMString::from(&**attr.value());
let mutation = Mutation::Attribute { name, namespace, old_value, };
MutationObserver::queue_a_mutation_record(&self.node, mutation);
self.attrs.borrow_mut().remove(idx);
attr.set_owner(None);
if attr.namespace() == &ns!() {

View file

@ -2,13 +2,22 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use dom::bindings::callback::ExceptionHandling;
use dom::bindings::cell::DOMRefCell;
use dom::bindings::codegen::Bindings::MutationObserverBinding;
use dom::bindings::codegen::Bindings::MutationObserverBinding::MutationCallback;
use dom::bindings::error::Fallible;
use dom::bindings::js::Root;
use dom::bindings::codegen::Bindings::MutationObserverBinding::MutationObserverBinding::MutationObserverMethods;
use dom::bindings::codegen::Bindings::MutationObserverBinding::MutationObserverInit;
use dom::bindings::error::{Error, Fallible};
use dom::bindings::js::{JS, Root};
use dom::bindings::reflector::{Reflector, reflect_dom_object};
use dom::bindings::str::DOMString;
use dom::mutationrecord::MutationRecord;
use dom::node::Node;
use dom::window::Window;
use dom_struct::dom_struct;
use html5ever::{Namespace, LocalName};
use microtask::Microtask;
use script_thread::ScriptThread;
use std::rc::Rc;
@ -17,6 +26,29 @@ pub struct MutationObserver {
reflector_: Reflector,
#[ignore_heap_size_of = "can't measure Rc values"]
callback: Rc<MutationCallback>,
record_queue: DOMRefCell<Vec<Root<MutationRecord>>>,
}
#[derive(Clone)]
pub enum Mutation {
Attribute { name: LocalName, namespace: Namespace, old_value: DOMString }
}
#[derive(HeapSizeOf, JSTraceable)]
pub struct RegisteredObserver {
observer: Root<MutationObserver>,
options: ObserverOptions,
}
#[derive(HeapSizeOf, JSTraceable)]
pub struct ObserverOptions {
attribute_old_value: bool,
attributes: bool,
character_data: bool,
character_data_old_value: bool,
child_list: bool,
subtree: bool,
attribute_filter: Vec<DOMString>,
}
impl MutationObserver {
@ -29,6 +61,7 @@ impl MutationObserver {
MutationObserver {
reflector_: Reflector::new(),
callback: callback,
record_queue: DOMRefCell::new(vec![]),
}
}
@ -37,4 +70,185 @@ impl MutationObserver {
ScriptThread::add_mutation_observer(&*observer);
Ok(observer)
}
/// https://dom.spec.whatwg.org/#queue-a-mutation-observer-compound-microtask
pub fn queue_mutation_observer_compound_microtask() {
// Step 1
if ScriptThread::is_mutation_observer_compound_microtask_queued() {
return;
}
// Step 2
ScriptThread::set_mutation_observer_compound_microtask_queued(true);
// Step 3
ScriptThread::enqueue_microtask(Microtask::NotifyMutationObservers);
}
/// https://dom.spec.whatwg.org/#notify-mutation-observers
pub fn notify_mutation_observers() {
// Step 1
ScriptThread::set_mutation_observer_compound_microtask_queued(false);
// Step 2
let notify_list = ScriptThread::get_mutation_observers();
// TODO: steps 3-4 (slots)
// Step 5
for mo in &notify_list {
let queue: Vec<Root<MutationRecord>> = mo.record_queue.borrow().clone();
mo.record_queue.borrow_mut().clear();
// TODO: Step 5.3 Remove all transient registered observers whose observer is mo.
if !queue.is_empty() {
let _ = mo.callback.Call_(&**mo, queue, &**mo, ExceptionHandling::Report);
}
}
// TODO: Step 6 (slot signals)
}
/// https://dom.spec.whatwg.org/#queueing-a-mutation-record
pub fn queue_a_mutation_record(target: &Node, attr_type: Mutation) {
// Step 1
let mut interestedObservers: Vec<(Root<MutationObserver>, Option<DOMString>)> = vec![];
// Step 2 & 3
for node in target.inclusive_ancestors() {
for registered in &*node.registered_mutation_observers() {
if &*node != target && !registered.options.subtree {
continue;
}
match attr_type {
Mutation::Attribute { ref name, ref namespace, ref old_value } => {
// Step 3.1
if !registered.options.attributes {
continue;
}
if !registered.options.attribute_filter.is_empty() {
if *namespace != ns!() {
continue;
}
if registered.options.attribute_filter.iter()
.find(|s| &**s == &**name).is_some() {
continue;
}
}
// Step 3.1.2
let paired_string = if registered.options.attribute_old_value {
Some(old_value.clone())
} else {
None
};
// Step 3.1.1
let idx = interestedObservers.iter().position(|&(ref o, _)|
&**o as *const _ == &*registered.observer as *const _);
if let Some(idx) = idx {
interestedObservers[idx].1 = paired_string;
} else {
interestedObservers.push((Root::from_ref(&*registered.observer),
paired_string));
}
}
}
}
}
// Step 4
for &(ref observer, ref paired_string) in &interestedObservers {
// Steps 4.1-4.7
let record = match attr_type {
Mutation::Attribute { ref name, ref namespace, .. } => {
let namespace = if *namespace != ns!() {
Some(namespace)
} else {
None
};
MutationRecord::attribute_mutated(target, name, namespace, paired_string.clone())
}
};
// Step 4.8
observer.record_queue.borrow_mut().push(record);
}
// Step 5
MutationObserver::queue_mutation_observer_compound_microtask();
}
}
impl MutationObserverMethods for MutationObserver {
/// https://dom.spec.whatwg.org/#dom-mutationobserver-observe
fn Observe(&self, target: &Node, options: &MutationObserverInit) -> Fallible<()> {
let attribute_filter = options.attributeFilter.clone().unwrap_or(vec![]);
let attribute_old_value = options.attributeOldValue.unwrap_or(false);
let mut attributes = options.attributes.unwrap_or(false);
let mut character_data = options.characterData.unwrap_or(false);
let character_data_old_value = options.characterDataOldValue.unwrap_or(false);
let child_list = options.childList;
let subtree = options.subtree;
// Step 1
if (options.attributeOldValue.is_some() || options.attributeFilter.is_some()) &&
options.attributes.is_none() {
attributes = true;
}
// Step 2
if options.characterDataOldValue.is_some() && options.characterData.is_none() {
character_data = true;
}
// Step 3
if !child_list && !attributes && !character_data {
return Err(Error::Type("One of childList, attributes, or characterData must be true".into()));
}
// Step 4
if attribute_old_value && !attributes {
return Err(Error::Type("attributeOldValue is true but attributes is false".into()));
}
// Step 5
if options.attributeFilter.is_some() && !attributes {
return Err(Error::Type("attributeFilter is present but attributes is false".into()));
}
// Step 6
if character_data_old_value && !character_data {
return Err(Error::Type("characterDataOldValue is true but characterData is false".into()));
}
// Step 7
let add_new_observer = {
let mut replaced = false;
for registered in &mut *target.registered_mutation_observers() {
if &*registered.observer as *const MutationObserver != self as *const MutationObserver {
continue;
}
// TODO: remove matching transient registered observers
registered.options.attribute_old_value = attribute_old_value;
registered.options.attributes = attributes;
registered.options.character_data = character_data;
registered.options.character_data_old_value = character_data_old_value;
registered.options.child_list = child_list;
registered.options.subtree = subtree;
registered.options.attribute_filter = attribute_filter.clone();
replaced = true;
}
!replaced
};
// Step 8
if add_new_observer {
target.registered_mutation_observers().push(RegisteredObserver {
observer: Root::from_ref(self),
options: ObserverOptions {
attributes,
attribute_old_value,
character_data,
character_data_old_value,
subtree,
attribute_filter,
child_list
},
});
}
Ok(())
}
}

View file

@ -2,22 +2,54 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use dom::bindings::codegen::Bindings::MutationRecordBinding::MutationRecordBinding;
use dom::bindings::codegen::Bindings::MutationRecordBinding::MutationRecordBinding::MutationRecordMethods;
use dom::bindings::js::{JS, Root};
use dom::bindings::reflector::Reflector;
use dom::bindings::reflector::{Reflector, reflect_dom_object};
use dom::bindings::str::DOMString;
use dom::node::Node;
use dom::node::{Node, window_from_node};
use dom::nodelist::NodeList;
use dom_struct::dom_struct;
use html5ever::{LocalName, Namespace};
#[dom_struct]
pub struct MutationRecord {
reflector_: Reflector,
//property for record type
record_type: DOMString,
//property for target node
target: JS<Node>,
attribute_name: Option<DOMString>,
attribute_namespace: Option<DOMString>,
old_value: Option<DOMString>,
}
impl MutationRecord {
#[allow(unrooted_must_root)]
pub fn attribute_mutated(target: &Node,
attribute_name: &LocalName,
attribute_namespace: Option<&Namespace>,
old_value: Option<DOMString>) -> Root<MutationRecord> {
let record = box MutationRecord::new_inherited("attributes",
target,
Some(DOMString::from(&**attribute_name)),
attribute_namespace.map(|n| DOMString::from(&**n)),
old_value);
reflect_dom_object(record, &*window_from_node(target), MutationRecordBinding::Wrap)
}
fn new_inherited(record_type: &str,
target: &Node,
attribute_name: Option<DOMString>,
attribute_namespace: Option<DOMString>,
old_value: Option<DOMString>) -> MutationRecord {
MutationRecord {
reflector_: Reflector::new(),
record_type: DOMString::from(record_type),
target: JS::from_ref(target),
attribute_name: attribute_name,
attribute_namespace: attribute_namespace,
old_value: old_value,
}
}
}
impl MutationRecordMethods for MutationRecord {
@ -28,7 +60,44 @@ impl MutationRecordMethods for MutationRecord {
// https://dom.spec.whatwg.org/#dom-mutationrecord-target
fn Target(&self) -> Root<Node> {
return Root::from_ref(&*self.target);
Root::from_ref(&*self.target)
}
// https://dom.spec.whatwg.org/#dom-mutationrecord-attributename
fn GetAttributeName(&self) -> Option<DOMString> {
self.attribute_name.clone()
}
// https://dom.spec.whatwg.org/#dom-mutationrecord-attributenamespace
fn GetAttributeNamespace(&self) -> Option<DOMString> {
self.attribute_namespace.clone()
}
// https://dom.spec.whatwg.org/#dom-mutationrecord-oldvalue
fn GetOldValue(&self) -> Option<DOMString> {
self.old_value.clone()
}
// https://dom.spec.whatwg.org/#dom-mutationrecord-addednodes
fn AddedNodes(&self) -> Root<NodeList> {
let window = window_from_node(&*self.target);
NodeList::empty(&window)
}
// https://dom.spec.whatwg.org/#dom-mutationrecord-removednodes
fn RemovedNodes(&self) -> Root<NodeList> {
let window = window_from_node(&*self.target);
NodeList::empty(&window)
}
// https://dom.spec.whatwg.org/#dom-mutationrecord-previoussibling
fn GetPreviousSibling(&self) -> Option<Root<Node>> {
None
}
// https://dom.spec.whatwg.org/#dom-mutationrecord-previoussibling
fn GetNextSibling(&self) -> Option<Root<Node>> {
None
}
}

View file

@ -7,6 +7,7 @@
use app_units::Au;
use devtools_traits::NodeInfo;
use document_loader::DocumentLoader;
use dom::bindings::cell::DOMRefCell;
use dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
use dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
use dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
@ -46,6 +47,7 @@ use dom::htmllinkelement::HTMLLinkElement;
use dom::htmlmetaelement::HTMLMetaElement;
use dom::htmlstyleelement::HTMLStyleElement;
use dom::htmltextareaelement::{HTMLTextAreaElement, LayoutHTMLTextAreaElementHelpers};
use dom::mutationobserver::RegisteredObserver;
use dom::nodelist::NodeList;
use dom::processinginstruction::ProcessingInstruction;
use dom::range::WeakRangeVec;
@ -72,7 +74,7 @@ use selectors::matching::matches_selector_list;
use selectors::parser::SelectorList;
use servo_url::ServoUrl;
use std::borrow::ToOwned;
use std::cell::{Cell, UnsafeCell};
use std::cell::{Cell, UnsafeCell, RefMut};
use std::cmp::max;
use std::default::Default;
use std::iter;
@ -138,6 +140,9 @@ pub struct Node {
/// node is finalized.
style_and_layout_data: Cell<Option<OpaqueStyleAndLayoutData>>,
/// Registered observers for this node.
mutation_observers: DOMRefCell<Vec<RegisteredObserver>>,
unique_id: UniqueId,
}
@ -363,6 +368,11 @@ impl Node {
}
}
/// Return all registered mutation observers for this node.
pub fn registered_mutation_observers(&self) -> RefMut<Vec<RegisteredObserver>> {
self.mutation_observers.borrow_mut()
}
/// Dumps the subtree rooted at this node, for debugging.
pub fn dump(&self) {
self.dump_indent(0);
@ -1411,6 +1421,8 @@ impl Node {
style_and_layout_data: Cell::new(None),
mutation_observers: Default::default(),
unique_id: UniqueId::new(),
}
}

View file

@ -9,7 +9,8 @@
// https://dom.spec.whatwg.org/#mutationobserver
[Pref="dom.mutation_observer.enabled", Constructor(MutationCallback callback)]
interface MutationObserver {
//void observe(Node target, optional MutationObserverInit options);
[Throws]
void observe(Node target, optional MutationObserverInit options);
//void disconnect();
//sequence<MutationRecord> takeRecords();
};

View file

@ -12,13 +12,13 @@ interface MutationRecord {
readonly attribute DOMString type;
[SameObject]
readonly attribute Node target;
//[SameObject]
//readonly attribute NodeList addedNodes;
//[SameObject]
//readonly attribute NodeList removedNodes;
//readonly attribute Node? previousSibling;
//readonly attribute Node? nextSibling;
//readonly attribute DOMString? attributeName;
//readonly attribute DOMString? attributeNamespace;
//readonly attribute DOMString? oldValue;
[SameObject]
readonly attribute NodeList addedNodes;
[SameObject]
readonly attribute NodeList removedNodes;
readonly attribute Node? previousSibling;
readonly attribute Node? nextSibling;
readonly attribute DOMString? attributeName;
readonly attribute DOMString? attributeNamespace;
readonly attribute DOMString? oldValue;
};