Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement custom element reactions #17614

Merged
merged 4 commits into from Jul 18, 2017
Merged
Changes from 1 commit
Commits
File filter...
Filter file types
Jump to…
Jump to file
Failed to load files.

Always

Just for now

Support custom element callback reactions

  • Loading branch information
cbrewster committed Jul 18, 2017
commit 46659915036bb44e73e7ef2696ea9f35105f1659
@@ -9,13 +9,15 @@ use dom::bindings::inheritance::Castable;
use dom::bindings::js::{LayoutJS, MutNullableJS, Root, RootedReference};
use dom::bindings::reflector::{Reflector, reflect_dom_object};
use dom::bindings::str::DOMString;
use dom::customelementregistry::CallbackReaction;
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;
use html5ever::{Prefix, LocalName, Namespace};
use script_thread::ScriptThread;
use servo_atoms::Atom;
use std::borrow::ToOwned;
use std::cell::Ref;
@@ -175,9 +177,20 @@ impl Attr {
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 };
let new_value = DOMString::from(&*value);
let mutation = Mutation::Attribute {
name: name.clone(),
namespace: namespace.clone(),
old_value: old_value.clone(),
};

MutationObserver::queue_a_mutation_record(owner.upcast::<Node>(), mutation);

if owner.get_custom_element_definition().is_some() {
let reaction = CallbackReaction::AttributeChanged(name, Some(old_value), Some(new_value), namespace);
ScriptThread::enqueue_callback_reaction(owner, reaction);
}

assert!(Some(owner) == self.owner().r());
owner.will_mutate_attr(self);
self.swap_value(&mut value);
@@ -131,7 +131,10 @@ fn create_html_element(name: QualName,
CustomElementCreationMode::Synchronous => {
let local_name = name.local.clone();
return match definition.create_element(document, prefix.clone()) {
Ok(element) => element,
Ok(element) => {
element.set_custom_element_definition(definition.clone());
element
},
Err(error) => {
// Step 6. Recovering from exception.
let global = GlobalScope::current().unwrap_or_else(|| document.global());
@@ -2,7 +2,7 @@
* 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::CallbackContainer;
use dom::bindings::callback::{CallbackContainer, ExceptionHandling};
use dom::bindings::cell::DOMRefCell;
use dom::bindings::codegen::Bindings::CustomElementRegistryBinding;
use dom::bindings::codegen::Bindings::CustomElementRegistryBinding::CustomElementRegistryMethods;
@@ -24,13 +24,14 @@ use dom::node::Node;
use dom::promise::Promise;
use dom::window::Window;
use dom_struct::dom_struct;
use html5ever::{LocalName, Prefix};
use html5ever::{LocalName, Namespace, Prefix};
use js::conversions::ToJSValConvertible;
use js::jsapi::{Construct1, IsCallable, IsConstructor, HandleValueArray, HandleObject, MutableHandleValue};
use js::jsapi::{JS_GetProperty, JSAutoCompartment, JSContext};
use js::jsval::{JSVal, ObjectValue, UndefinedValue};
use js::jsapi::{Heap, JS_GetProperty, JSAutoCompartment, JSContext};
use js::jsval::{JSVal, NullValue, ObjectValue, UndefinedValue};
use std::cell::Cell;
use std::collections::HashMap;
use std::collections::{HashMap, VecDeque};
use std::ops::Deref;
use std::ptr;
use std::rc::Rc;

@@ -449,6 +450,197 @@ impl CustomElementDefinition {
}
}

#[derive(HeapSizeOf, JSTraceable)]
#[must_root]
pub enum CustomElementReaction {

This comment has been minimized.

@jdm

jdm Jul 12, 2017

Member

Add #[must_root] to this.

This comment has been minimized.

@cbrewster

cbrewster Jul 13, 2017

Author Member

Done.

// TODO: Support upgrade reactions
Callback(
#[ignore_heap_size_of = "Rc"]
Rc<Function>,
Box<[Heap<JSVal>]>
),
}

impl CustomElementReaction {
/// https://html.spec.whatwg.org/multipage/#invoke-custom-element-reactions
#[allow(unsafe_code)]
pub fn invoke(&self, element: &Element) {
// Step 2.1
match *self {
CustomElementReaction::Callback(ref callback, ref arguments) => {
let arguments = arguments.iter().map(|arg| arg.handle()).collect();
let _ = callback.Call_(&*element, arguments, ExceptionHandling::Report);
}
}
}
}

pub enum CallbackReaction {
Connected,
Disconnected,
Adopted(Root<Document>, Root<Document>),
AttributeChanged(LocalName, Option<DOMString>, Option<DOMString>, Namespace),
}

/// https://html.spec.whatwg.org/multipage/#processing-the-backup-element-queue
#[derive(HeapSizeOf, JSTraceable, Eq, PartialEq, Clone, Copy)]
enum BackupElementQueueFlag {
Processing,
NotProcessing,
}

/// https://html.spec.whatwg.org/multipage/#custom-element-reactions-stack
#[derive(HeapSizeOf, JSTraceable)]
#[must_root]
pub struct CustomElementReactionStack {
backup_queue: ElementQueue,
processing_backup_element_queue: Cell<BackupElementQueueFlag>,
}

impl CustomElementReactionStack {
pub fn new() -> CustomElementReactionStack {
CustomElementReactionStack {
backup_queue: ElementQueue::new(),
processing_backup_element_queue: Cell::new(BackupElementQueueFlag::NotProcessing),
}
}

/// https://html.spec.whatwg.org/multipage/#enqueue-an-element-on-the-appropriate-element-queue
/// Step 4
pub fn invoke_backup_element_queue(&self) {
// Step 4.1
self.backup_queue.invoke_reactions();

// Step 4.2
self.processing_backup_element_queue.set(BackupElementQueueFlag::NotProcessing);
}

/// https://html.spec.whatwg.org/multipage/#enqueue-an-element-on-the-appropriate-element-queue
pub fn enqueue_element(&self, element: &Element) {
// TODO: Steps 1 - 2
// Support multiple queues

// Step 1.1
self.backup_queue.append_element(element);

// Step 1.2
if self.processing_backup_element_queue.get() == BackupElementQueueFlag::Processing {
return;
}

// Step 1.3
self.processing_backup_element_queue.set(BackupElementQueueFlag::Processing);

// Step 4
// TODO: Invoke Microtask

// Step 4.1
self.backup_queue.invoke_reactions();

// Step 4.2
self.processing_backup_element_queue.set(BackupElementQueueFlag::NotProcessing);
}

/// https://html.spec.whatwg.org/multipage/#enqueue-a-custom-element-callback-reaction
#[allow(unsafe_code)]
pub fn enqueue_callback_reaction(&self, element: &Element, reaction: CallbackReaction) {
// Step 1
let definition = match element.get_custom_element_definition() {
Some(definition) => definition,
None => return,
};

// Step 2
let (callback, args) = match reaction {
CallbackReaction::Connected => (definition.callbacks.connected_callback.clone(), Vec::new()),
CallbackReaction::Disconnected => (definition.callbacks.disconnected_callback.clone(), Vec::new()),
CallbackReaction::Adopted(ref old_doc, ref new_doc) => {
let args = vec![Heap::default(), Heap::default()];
args[0].set(ObjectValue(old_doc.reflector().get_jsobject().get()));
args[1].set(ObjectValue(new_doc.reflector().get_jsobject().get()));
(definition.callbacks.adopted_callback.clone(), args)
},
CallbackReaction::AttributeChanged(local_name, old_val, val, namespace) => {
// Step 4
if !definition.observed_attributes.iter().any(|attr| *attr == *local_name) {
return;
}

let cx = element.global().get_cx();

let local_name = DOMString::from(&*local_name);
rooted!(in(cx) let mut name_value = UndefinedValue());
unsafe { local_name.to_jsval(cx, name_value.handle_mut()); }

rooted!(in(cx) let mut old_value = NullValue());
if let Some(old_val) = old_val {
unsafe { old_val.to_jsval(cx, old_value.handle_mut()); }
}

rooted!(in(cx) let mut value = NullValue());
if let Some(val) = val {
unsafe { val.to_jsval(cx, value.handle_mut()); }
}

let namespace = DOMString::from(&*namespace);
rooted!(in(cx) let mut namespace_value = UndefinedValue());
unsafe { namespace.to_jsval(cx, namespace_value.handle_mut()); }

let args = vec![Heap::default(), Heap::default(), Heap::default(), Heap::default()];
args[0].set(name_value.get());
args[1].set(old_value.get());
args[2].set(value.get());
args[3].set(namespace_value.get());

(definition.callbacks.attribute_changed_callback.clone(), args)
},
};

// Step 3
let callback = match callback {
Some(callback) => callback,
None => return,
};

// Step 5
element.push_callback_reaction(callback, args.into_boxed_slice());

// Step 6
self.enqueue_element(element);
}
}

/// https://html.spec.whatwg.org/multipage/#element-queue
#[derive(HeapSizeOf, JSTraceable)]
#[must_root]
struct ElementQueue {
queue: DOMRefCell<VecDeque<JS<Element>>>,
}

impl ElementQueue {
fn new() -> ElementQueue {
ElementQueue {
queue: Default::default(),
}
}

/// https://html.spec.whatwg.org/multipage/#invoke-custom-element-reactions
fn invoke_reactions(&self) {
// Steps 1-2
while let Some(element) = self.next_element() {
element.invoke_reactions()

This comment has been minimized.

@jdm

jdm Jul 12, 2017

Member

Can these reactions end up appending an element to this queue?

This comment has been minimized.

@cbrewster

cbrewster Jul 13, 2017

Author Member

I do not believe so, there are two queues an element can be added to:

  1. The current element queue.
  2. The backup element queue.

For the first case, the current element queue is popped from the custom element reaction stack thus it is no longer the current element queue which means no element will be added to it.

The second case is the backup element queue has the processing_backup_element_queue flag which prevents elements being enqueued until the flag is set to NotProcessing.

This comment has been minimized.

@cbrewster

cbrewster Jul 13, 2017

Author Member

Oh actually in the second case, an element can be appended to the backup element queue is being invoked. The flag check occurs after the element is appended to the queue. This is what causes the crash in the test expectations.

I want to change it so the queue is not borrowed for the duration of the loop. Instead if element are popped and processed, new element can be appended to the queue. But I always run into unrooted must root errors. any ideas?

while let Some(element) = self.queue.borrow_mut().pop_front().map(|e| Root::from_ref(&*e)) {
    element.invoke_reactions()
}

This comment has been minimized.

@cbrewster

cbrewster Jul 17, 2017

Author Member

Avoiding the closure appears to appease the lint

self.queue.borrow_mut().pop_front().as_ref().map(JS::deref).map(Root::from_ref)
}
}

fn next_element(&self) -> Option<Root<Element>> {
self.queue.borrow_mut().pop_front().as_ref().map(JS::deref).map(Root::from_ref)
}

fn append_element(&self, element: &Element) {
self.queue.borrow_mut().push_back(JS::from_ref(element));
}
}

/// https://html.spec.whatwg.org/multipage/#valid-custom-element-name
fn is_valid_custom_element_name(name: &str) -> bool {
// Custom elment names must match:
@@ -13,6 +13,7 @@ use dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
use dom::bindings::codegen::Bindings::ElementBinding;
use dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
use dom::bindings::codegen::Bindings::EventBinding::EventMethods;
use dom::bindings::codegen::Bindings::FunctionBinding::Function;
use dom::bindings::codegen::Bindings::HTMLTemplateElementBinding::HTMLTemplateElementMethods;
use dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
use dom::bindings::codegen::Bindings::WindowBinding::{ScrollBehavior, ScrollToOptions};
@@ -30,6 +31,7 @@ use dom::bindings::xmlname::{namespace_from_domstring, validate_and_extract, xml
use dom::bindings::xmlname::XMLName::InvalidXMLName;
use dom::characterdata::CharacterData;
use dom::create::create_element;
use dom::customelementregistry::{CallbackReaction, CustomElementDefinition, CustomElementReaction};
use dom::document::{Document, LayoutDocumentHelpers};
use dom::documentfragment::DocumentFragment;
use dom::domrect::DOMRect;
@@ -80,11 +82,12 @@ use html5ever::serialize;
use html5ever::serialize::SerializeOpts;
use html5ever::serialize::TraversalScope;
use html5ever::serialize::TraversalScope::{ChildrenOnly, IncludeNode};
use js::jsapi::{HandleValue, JSAutoCompartment};
use js::jsapi::{HandleValue, Heap, JSAutoCompartment};
use js::jsval::JSVal;
use net_traits::request::CorsSettings;
use ref_filter_map::ref_filter_map;
use script_layout_interface::message::ReflowQueryType;
use script_thread::Runnable;
use script_thread::{Runnable, ScriptThread};
use selectors::attr::{AttrSelectorOperation, NamespaceConstraint, CaseSensitivity};
use selectors::matching::{ElementSelectorFlags, LocalMatchingContext, MatchingContext, MatchingMode};
use selectors::matching::{HAS_EDGE_CHILD_SELECTOR, HAS_SLOW_SELECTOR, HAS_SLOW_SELECTOR_LATER_SIBLINGS};
@@ -141,6 +144,11 @@ pub struct Element {
/// when it has exclusive access to the element.
#[ignore_heap_size_of = "bitflags defined in rust-selectors"]
selector_flags: Cell<ElementSelectorFlags>,
/// https://html.spec.whatwg.org/multipage/#custom-element-reaction-queue
custom_element_reaction_queue: DOMRefCell<Vec<CustomElementReaction>>,
/// https://dom.spec.whatwg.org/#concept-element-custom-element-definition
#[ignore_heap_size_of = "Rc"]
custom_element_definition: DOMRefCell<Option<Rc<CustomElementDefinition>>>,

This comment has been minimized.

@jdm

jdm Jul 12, 2017

Member

Add links to appropriate specification concepts?

This comment has been minimized.

@cbrewster

cbrewster Jul 13, 2017

Author Member

Done.

}

impl fmt::Debug for Element {
@@ -244,6 +252,8 @@ impl Element {
class_list: Default::default(),
state: Cell::new(state),
selector_flags: Cell::new(ElementSelectorFlags::empty()),
custom_element_reaction_queue: Default::default(),
custom_element_definition: Default::default(),
}
}

@@ -278,6 +288,26 @@ impl Element {
self.is.borrow().clone()
}

pub fn set_custom_element_definition(&self, definition: Rc<CustomElementDefinition>) {
*self.custom_element_definition.borrow_mut() = Some(definition);
}

pub fn get_custom_element_definition(&self) -> Option<Rc<CustomElementDefinition>> {
(*self.custom_element_definition.borrow()).clone()
}

pub fn push_callback_reaction(&self, function: Rc<Function>, args: Box<[Heap<JSVal>]>) {
self.custom_element_reaction_queue.borrow_mut().push(CustomElementReaction::Callback(function, args));
}

pub fn invoke_reactions(&self) {
let mut reaction_queue = self.custom_element_reaction_queue.borrow_mut();
for reaction in reaction_queue.iter() {
reaction.invoke(self);
}
reaction_queue.clear();
}

// https://drafts.csswg.org/cssom-view/#css-layout-box
// Elements that have a computed value of the display property
// that is table-column or table-column-group
@@ -1036,10 +1066,20 @@ 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 };
let value = DOMString::from(&**attr.value());
let mutation = Mutation::Attribute {
name: name.clone(),
namespace: namespace.clone(),
old_value: value.clone(),
};

MutationObserver::queue_a_mutation_record(&self.node, mutation);

if self.get_custom_element_definition().is_some() {
let reaction = CallbackReaction::AttributeChanged(name, None, Some(value), namespace);
ScriptThread::enqueue_callback_reaction(self, reaction);
}

assert!(attr.GetOwnerElement().r() == Some(self));
self.will_mutate_attr(attr);
self.attrs.borrow_mut().push(JS::from_ref(attr));
@@ -1171,9 +1211,17 @@ impl Element {
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, };
let mutation = Mutation::Attribute {
name: name.clone(),
namespace: namespace.clone(),
old_value: old_value.clone(),
};

MutationObserver::queue_a_mutation_record(&self.node, mutation);

let reaction = CallbackReaction::AttributeChanged(name, Some(old_value), None, namespace);
ScriptThread::enqueue_callback_reaction(self, reaction);

self.attrs.borrow_mut().remove(idx);
attr.set_owner(None);
if attr.namespace() == &ns!() {
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.