Adding in interning system for strings

This commit is contained in:
Pauan 2019-06-21 01:57:47 +02:00
parent ec85fef904
commit fae50b2401
17 changed files with 398 additions and 385 deletions

View File

@ -30,36 +30,36 @@ features = ["futures_0_3"]
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3.22" version = "0.3.22"
features = [ features = [
"CharacterData", #"CharacterData",
"Comment", "Comment",
"CssRule", #"CssRule",
"CssRuleList", #"CssRuleList",
"CssStyleDeclaration", #"CssStyleDeclaration",
"CssStyleRule", "CssStyleRule",
"CssStyleSheet", "CssStyleSheet",
"Document", #"Document",
"DocumentFragment", #"DocumentFragment",
"DomTokenList", #"DomTokenList",
"Element", "Element",
"Event", #"Event",
"EventTarget", #"EventTarget",
"FocusEvent", "FocusEvent",
"History", #"History",
"InputEvent", "InputEvent",
"HtmlElement", "HtmlElement",
"HtmlHeadElement", #"HtmlHeadElement",
"HtmlInputElement", "HtmlInputElement",
"HtmlStyleElement", #"HtmlStyleElement",
"HtmlTextAreaElement", "HtmlTextAreaElement",
"KeyboardEvent", "KeyboardEvent",
"Location", #"Location",
"MouseEvent", "MouseEvent",
"Node", "Node",
"NodeList", #"NodeList",
"StyleSheet", #"StyleSheet",
"SvgElement", "SvgElement",
"Text", "Text",
"Url", #"Url",
"Window", "Window",
] ]

View File

@ -98,7 +98,7 @@ impl Drop for State {
// TODO move this into gloo // TODO move this into gloo
fn set_interval<F>(ms: i32, f: F) where F: FnMut() + 'static { fn set_interval<F>(ms: i32, f: F) where F: FnMut() + 'static {
let f = wasm_bindgen::closure::Closure::wrap(Box::new(f) as Box<FnMut()>); let f = wasm_bindgen::closure::Closure::wrap(Box::new(f) as Box<dyn FnMut()>);
web_sys::window() web_sys::window()
.unwrap_throw() .unwrap_throw()

View File

@ -18,10 +18,7 @@ module.exports = {
liveReload: true, liveReload: true,
open: true, open: true,
noInfo: true, noInfo: true,
overlay: { overlay: true
warnings: true,
errors: true
}
}, },
plugins: [ plugins: [
new CopyPlugin([ new CopyPlugin([
@ -29,7 +26,8 @@ module.exports = {
]), ]),
new WasmPackPlugin({ new WasmPackPlugin({
crateDirectory: __dirname crateDirectory: __dirname,
extraArgs: "--out-name index"
}) })
] ]
}; };

View File

@ -18,10 +18,7 @@ module.exports = {
liveReload: true, liveReload: true,
open: true, open: true,
noInfo: true, noInfo: true,
overlay: { overlay: true
warnings: true,
errors: true
}
}, },
plugins: [ plugins: [
new CopyPlugin([ new CopyPlugin([
@ -29,7 +26,8 @@ module.exports = {
]), ]),
new WasmPackPlugin({ new WasmPackPlugin({
crateDirectory: __dirname crateDirectory: __dirname,
extraArgs: "--out-name index"
}) })
] ]
}; };

View File

@ -29,6 +29,7 @@ features = ["rc"]
version = "0.3.22" version = "0.3.22"
features = [ features = [
"Storage", "Storage",
"Url",
] ]
[target."cfg(debug_assertions)".dependencies] [target."cfg(debug_assertions)".dependencies]

View File

@ -3,7 +3,7 @@ use std::cell::Cell;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use serde_derive::{Serialize, Deserialize}; use serde_derive::{Serialize, Deserialize};
use web_sys::{window, HtmlElement, Storage}; use web_sys::{window, HtmlElement, Storage, Url};
use futures_signals::map_ref; use futures_signals::map_ref;
use futures_signals::signal::{Signal, SignalExt, Mutable}; use futures_signals::signal::{Signal, SignalExt, Mutable};
use futures_signals::signal_vec::{SignalVecExt, MutableVec}; use futures_signals::signal_vec::{SignalVecExt, MutableVec};
@ -37,7 +37,9 @@ enum Filter {
impl Filter { impl Filter {
fn signal() -> impl Signal<Item = Self> { fn signal() -> impl Signal<Item = Self> {
routing::url().map(|url| { routing::url()
.signal_ref(|url| Url::new(&url).unwrap_throw())
.map(|url| {
match url.hash().as_str() { match url.hash().as_str() {
"#/active" => Filter::Active, "#/active" => Filter::Active,
"#/completed" => Filter::Completed, "#/completed" => Filter::Completed,

View File

@ -18,10 +18,7 @@ module.exports = {
liveReload: true, liveReload: true,
open: true, open: true,
noInfo: true, noInfo: true,
overlay: { overlay: true
warnings: true,
errors: true
}
}, },
plugins: [ plugins: [
new CopyPlugin([ new CopyPlugin([
@ -29,7 +26,8 @@ module.exports = {
]), ]),
new WasmPackPlugin({ new WasmPackPlugin({
crateDirectory: __dirname crateDirectory: __dirname,
extraArgs: "--out-name index"
}) })
] ]
}; };

View File

@ -374,7 +374,7 @@ impl<A, F, S> SignalVec for AnimatedMap<S, F>
}, },
VecDiff::UpdateAt { index, value } => { VecDiff::UpdateAt { index, value } => {
let index = self.find_index(index).expect("Could not find value"); let index = self.find_index(index).unwrap_throw();
let state = { let state = {
let state = &self.as_mut().animations()[index]; let state = &self.as_mut().animations()[index];
AnimatedMapBroadcaster(state.animation.raw_clone()) AnimatedMapBroadcaster(state.animation.raw_clone())
@ -386,7 +386,7 @@ impl<A, F, S> SignalVec for AnimatedMap<S, F>
// TODO test this // TODO test this
// TODO should this be treated as a removal + insertion ? // TODO should this be treated as a removal + insertion ?
VecDiff::Move { old_index, new_index } => { VecDiff::Move { old_index, new_index } => {
let old_index = self.find_index(old_index).expect("Could not find value"); let old_index = self.find_index(old_index).unwrap_throw();
let state = self.as_mut().animations().remove(old_index); let state = self.as_mut().animations().remove(old_index);
@ -398,7 +398,7 @@ impl<A, F, S> SignalVec for AnimatedMap<S, F>
}, },
VecDiff::RemoveAt { index } => { VecDiff::RemoveAt { index } => {
let index = self.find_index(index).expect("Could not find value"); let index = self.find_index(index).unwrap_throw();
if self.as_mut().should_remove(cx, index) { if self.as_mut().should_remove(cx, index) {
self.remove_index(index) self.remove_index(index)
@ -409,7 +409,7 @@ impl<A, F, S> SignalVec for AnimatedMap<S, F>
}, },
VecDiff::Pop {} => { VecDiff::Pop {} => {
let index = self.find_last_index().expect("Cannot pop from empty vec"); let index = self.find_last_index().unwrap_throw();
if self.as_mut().should_remove(cx, index) { if self.as_mut().should_remove(cx, index) {
self.remove_index(index) self.remove_index(index)

145
src/bindings.rs Normal file
View File

@ -0,0 +1,145 @@
use wasm_bindgen::prelude::*;
use js_sys::JsString;
use web_sys::{HtmlElement, Element, Node, Window, Text, Comment, CssStyleSheet, CssStyleRule};
use crate::cache::intern;
#[wasm_bindgen(inline_js = "
export function body() { return document.body; }
export function _window() { return window; }
export function ready_state() { return document.readyState; }
export function current_url() { return location.href; }
export function go_to_url(url) { history.pushState(null, \"\", url); }
export function create_stylesheet() {
// TODO use createElementNS ?
var e = document.createElement(\"style\");
e.type = \"text/css\";
document.head.appendChild(e);
return e.sheet;
}
export function make_style_rule(sheet, selector) {
var rules = sheet.cssRules;
var length = rules.length;
sheet.insertRule(selector + \" {}\", length);
return rules[length];
}
export function create_element(name) { return document.createElement(name); }
export function create_element_ns(namespace, name) { return document.createElementNS(namespace, name); }
export function create_text_node(value) { return document.createTextNode(value); }
// http://jsperf.com/textnode-performance
export function set_text(elem, value) { elem.data = value; }
export function create_comment(value) { return document.createComment(value); }
export function set_attribute(elem, key, value) { elem.setAttribute(key, value); }
export function set_attribute_ns(elem, namespace, key, value) { elem.setAttributeNS(namespace, key, value); }
export function remove_attribute(elem, key) { elem.removeAttribute(key); }
export function remove_attribute_ns(elem, namespace, key) { elem.removeAttributeNS(namespace, key); }
export function add_class(elem, value) { elem.classList.add(value); }
export function remove_class(elem, value) { elem.classList.remove(value); }
export function set_text_content(elem, value) { elem.textContent = value; }
export function get_style(elem, name) { return elem.style.getPropertyValue(name); }
export function remove_style(elem, name) { return elem.style.removeProperty(name); }
export function set_style(elem, name, value, important) {
elem.style.setProperty(name, value, (important ? \"important\" : \"\"));
}
export function get_at(parent, index) { return parent.childNodes[index]; }
export function insert_child_before(parent, child, other) { parent.insertBefore(child, other); }
export function replace_child(parent, child, other) { parent.replaceChild(child, other); }
export function append_child(parent, child) { parent.appendChild(child); }
export function remove_child(parent, child) { parent.removeChild(child); }
export function focus(elem) { elem.focus(); }
export function blur(elem) { elem.blur(); }
export function set_property(obj, name, value) { obj[name] = value; }
")]
extern "C" {
pub(crate) fn body() -> HtmlElement;
#[wasm_bindgen(js_name = _window)]
pub(crate) fn window() -> Window;
pub(crate) fn ready_state() -> JsString;
pub(crate) fn current_url() -> JsString;
pub(crate) fn go_to_url(url: &JsString);
pub(crate) fn create_stylesheet() -> CssStyleSheet;
pub(crate) fn make_style_rule(sheet: &CssStyleSheet, selector: &JsString) -> CssStyleRule;
pub(crate) fn create_element(name: &JsString) -> Element;
pub(crate) fn create_element_ns(namespace: &JsString, name: &JsString) -> Element;
pub(crate) fn create_text_node(value: &JsString) -> Text;
pub(crate) fn set_text(elem: &Text, value: &JsString);
pub(crate) fn create_comment(value: &JsString) -> Comment;
// TODO check that the attribute *actually* was changed
pub(crate) fn set_attribute(elem: &Element, key: &JsString, value: &JsString);
pub(crate) fn set_attribute_ns(elem: &Element, namespace: &JsString, key: &JsString, value: &JsString);
pub(crate) fn remove_attribute(elem: &Element, key: &JsString);
pub(crate) fn remove_attribute_ns(elem: &Element, namespace: &JsString, key: &JsString);
pub(crate) fn add_class(elem: &Element, value: &JsString);
pub(crate) fn remove_class(elem: &Element, value: &JsString);
pub(crate) fn set_text_content(elem: &Node, value: &JsString);
// TODO better type for elem
pub(crate) fn get_style(elem: &JsValue, name: &JsString) -> JsString;
pub(crate) fn remove_style(elem: &JsValue, name: &JsString);
pub(crate) fn set_style(elem: &JsValue, name: &JsString, value: &JsString, important: bool);
pub(crate) fn get_at(parent: &Node, index: u32) -> Node;
pub(crate) fn insert_child_before(parent: &Node, child: &Node, other: &Node);
pub(crate) fn replace_child(parent: &Node, child: &Node, other: &Node);
pub(crate) fn append_child(parent: &Node, child: &Node);
pub(crate) fn remove_child(parent: &Node, child: &Node);
pub(crate) fn focus(elem: &HtmlElement);
pub(crate) fn blur(elem: &HtmlElement);
// TODO maybe use Object for obj ?
pub(crate) fn set_property(obj: &JsValue, name: &JsString, value: &JsValue);
}
#[inline]
pub(crate) fn remove_all_children(node: &Node) {
set_text_content(node, &intern(""));
}
// TODO make this more efficient
#[inline]
pub(crate) fn insert_at(parent: &Node, index: u32, child: &Node) {
insert_child_before(parent, child, &get_at(parent, index));
}
// TODO make this more efficient
#[inline]
pub(crate) fn update_at(parent: &Node, index: u32, child: &Node) {
replace_child(parent, child, &get_at(parent, index));
}
// TODO make this more efficient
#[inline]
pub(crate) fn remove_at(parent: &Node, index: u32) {
remove_child(parent, &get_at(parent, index));
}

33
src/cache.rs Normal file
View File

@ -0,0 +1,33 @@
use std::cell::RefCell;
use std::collections::HashMap;
use js_sys::JsString;
thread_local! {
// TODO is it possible to avoid the RefCell ?
static CACHE: RefCell<HashMap<String, JsString>> = RefCell::new(HashMap::new());
}
// TODO make this more efficient
pub(crate) fn intern(x: &str) -> JsString {
CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
match cache.get(x) {
Some(value) => value.clone(),
None => {
let js = JsString::from(x);
cache.insert(x.to_owned(), js.clone());
js
},
}
})
}
#[doc(hidden)]
pub fn debug_cache() {
CACHE.with(|cache| {
let cache = cache.borrow();
web_sys::console::log_1(&wasm_bindgen::JsValue::from(format!("{:#?}", cache.keys())));
});
}

View File

@ -12,16 +12,17 @@ use futures_util::FutureExt;
use futures_channel::oneshot; use futures_channel::oneshot;
use discard::{Discard, DiscardOnDrop}; use discard::{Discard, DiscardOnDrop};
use wasm_bindgen::{JsValue, UnwrapThrowExt, JsCast}; use wasm_bindgen::{JsValue, UnwrapThrowExt, JsCast};
use js_sys::Reflect; use js_sys::JsString;
use web_sys::{window, HtmlElement, Node, EventTarget, Element, CssStyleSheet, HtmlStyleElement, CssStyleRule, CssStyleDeclaration}; use web_sys::{HtmlElement, Node, EventTarget, Element, CssStyleSheet, CssStyleRule};
use gloo::events::{EventListener, EventListenerOptions}; use gloo::events::{EventListener, EventListenerOptions};
use crate::cache::intern;
use crate::bindings;
use crate::callbacks::Callbacks; use crate::callbacks::Callbacks;
use crate::traits::*; use crate::traits::*;
use crate::operations; use crate::operations;
use crate::operations::{for_each, spawn_future}; use crate::operations::{for_each, spawn_future};
use crate::dom_operations; use crate::utils::{on, on_with_options, ValueDiscard, FnDiscard, EventDiscard};
use crate::utils::{document, on, on_with_options, ValueDiscard, FnDiscard, EventDiscard};
pub struct RefFn<A, B, C> where B: ?Sized { pub struct RefFn<A, B, C> where B: ?Sized {
@ -92,7 +93,7 @@ lazy_static! {
// TODO should return HtmlBodyElement ? // TODO should return HtmlBodyElement ?
pub fn body() -> HtmlElement { pub fn body() -> HtmlElement {
document().body().unwrap_throw() bindings::body()
} }
@ -104,14 +105,14 @@ pub struct DomHandle {
impl Discard for DomHandle { impl Discard for DomHandle {
#[inline] #[inline]
fn discard(self) { fn discard(self) {
self.parent.remove_child(&self.dom.element).unwrap_throw(); bindings::remove_child(&self.parent, &self.dom.element);
self.dom.callbacks.discard(); self.dom.callbacks.discard();
} }
} }
#[inline] #[inline]
pub fn append_dom(parent: &Node, mut dom: Dom) -> DomHandle { pub fn append_dom(parent: &Node, mut dom: Dom) -> DomHandle {
parent.append_child(&dom.element).unwrap_throw(); bindings::append_child(&parent, &dom.element);
dom.callbacks.trigger_after_insert(); dom.callbacks.trigger_after_insert();
@ -141,7 +142,7 @@ impl Signal for IsWindowLoaded {
fn poll_change(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> { fn poll_change(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
let result = match *self { let result = match *self {
IsWindowLoaded::Initial {} => { IsWindowLoaded::Initial {} => {
let is_ready = document().ready_state() == "complete"; let is_ready = bindings::ready_state() == "complete";
if is_ready { if is_ready {
Poll::Ready(Some(true)) Poll::Ready(Some(true))
@ -151,7 +152,7 @@ impl Signal for IsWindowLoaded {
*self = IsWindowLoaded::Pending { *self = IsWindowLoaded::Pending {
receiver, receiver,
_event: EventListener::once(&window().unwrap_throw(), "load", move |_| { _event: EventListener::once(&bindings::window(), "load", move |_| {
// TODO test this // TODO test this
sender.send(Some(true)).unwrap_throw(); sender.send(Some(true)).unwrap_throw();
}), }),
@ -185,7 +186,7 @@ pub fn is_window_loaded() -> impl Signal<Item = bool> {
#[inline] #[inline]
pub fn text(value: &str) -> Dom { pub fn text(value: &str) -> Dom {
Dom::new(document().create_text_node(value).into()) Dom::new(bindings::create_text_node(&intern(value)).into())
} }
@ -194,7 +195,7 @@ pub fn text_signal<A, B>(value: B) -> Dom
where A: AsStr, where A: AsStr,
B: Signal<Item = A> + 'static { B: Signal<Item = A> + 'static {
let element = document().create_text_node(""); let element = bindings::create_text_node(&intern(""));
let mut callbacks = Callbacks::new(); let mut callbacks = Callbacks::new();
@ -204,8 +205,8 @@ pub fn text_signal<A, B>(value: B) -> Dom
callbacks.after_remove(for_each(value, move |value| { callbacks.after_remove(for_each(value, move |value| {
let value = value.as_str(); let value = value.as_str();
// http://jsperf.com/textnode-performance // TODO maybe this should intern ?
element.set_data(value); bindings::set_text(&element, &JsString::from(value));
})); }));
} }
@ -236,7 +237,7 @@ impl Dom {
#[inline] #[inline]
pub fn empty() -> Self { pub fn empty() -> Self {
// TODO is there a better way of doing this ? // TODO is there a better way of doing this ?
Self::new(document().create_comment("").into()) Self::new(bindings::create_comment(&intern("")).into())
} }
#[inline] #[inline]
@ -255,12 +256,12 @@ impl Dom {
#[inline] #[inline]
pub fn create_element<A>(name: &str) -> A where A: JsCast { pub fn create_element<A>(name: &str) -> A where A: JsCast {
document().create_element(name).unwrap_throw().dyn_into().unwrap_throw() bindings::create_element(&intern(name)).dyn_into().unwrap_throw()
} }
#[inline] #[inline]
pub fn create_element_ns<A>(name: &str, namespace: &str) -> A where A: JsCast { pub fn create_element_ns<A>(name: &str, namespace: &str) -> A where A: JsCast {
document().create_element_ns(Some(namespace), name).unwrap_throw().dyn_into().unwrap_throw() bindings::create_element_ns(&intern(namespace), &intern(name)).dyn_into().unwrap_throw()
} }
@ -289,37 +290,50 @@ fn set_option<A, B, C, D, F>(element: A, callbacks: &mut Callbacks, value: D, mu
})); }));
} }
fn set_style<A, B>(style: &CssStyleDeclaration, name: &A, value: B, important: bool) fn set_style<A, B>(element: &JsValue, name: &A, value: B, important: bool, should_intern: bool)
where A: MultiStr, where A: MultiStr,
B: MultiStr { B: MultiStr {
let mut names = vec![]; let mut names = vec![];
let mut values = vec![]; let mut values = vec![];
fn try_set_style(style: &CssStyleDeclaration, names: &mut Vec<String>, values: &mut Vec<String>, name: &str, value: &str, important: bool) -> Result<bool, JsValue> { fn try_set_style(element: &JsValue, names: &mut Vec<String>, values: &mut Vec<String>, name: &JsString, value: &JsString, important: bool) -> bool {
assert!(value != ""); // TODO move this out of this function ?
let empty = intern("");
assert!(*value != empty);
// TODO handle browser prefixes ? // TODO handle browser prefixes ?
style.remove_property(name)?; bindings::remove_style(element, name);
style.set_property_with_priority(name, value, if important { "important" } else { "" })?; bindings::set_style(element, name, value, important);
// TODO maybe use cfg(debug_assertions) ? // TODO maybe use cfg(debug_assertions) ?
let is_changed = style.get_property_value(name)? != ""; let is_changed = bindings::get_style(element, name) != empty;
if is_changed { if is_changed {
Ok(true) true
} else { } else {
names.push(name.to_string()); names.push(String::from(name));
values.push(value.to_string()); values.push(String::from(value));
Ok(false) false
} }
} }
let okay = name.any(|name| { let okay = name.any(|name| {
let name = intern(name);
value.any(|value| { value.any(|value| {
try_set_style(style, &mut names, &mut values, name, value, important).unwrap_throw() // TODO maybe always intern the value ?
let value = if should_intern {
intern(value)
} else {
JsString::from(value)
};
try_set_style(element, &mut names, &mut values, &name, &value, important)
}) })
}); });
@ -329,21 +343,24 @@ fn set_style<A, B>(style: &CssStyleDeclaration, name: &A, value: B, important: b
} }
} }
fn set_style_signal<A, B, C, D>(style: CssStyleDeclaration, callbacks: &mut Callbacks, name: A, value: D, important: bool) fn set_style_signal<A, B, C, D>(element: &JsValue, callbacks: &mut Callbacks, name: A, value: D, important: bool)
where A: MultiStr + 'static, where A: MultiStr + 'static,
B: MultiStr, B: MultiStr,
C: OptionStr<Output = B>, C: OptionStr<Output = B>,
D: Signal<Item = C> + 'static { D: Signal<Item = C> + 'static {
set_option(style, callbacks, value, move |style, value| { // TODO verify that this is always a cheap O(1) clone
let element = element.clone();
set_option(element, callbacks, value, move |element, value| {
match value { match value {
Some(value) => { Some(value) => {
set_style(style, &name, value, important); set_style(element, &name, value, important, false);
}, },
None => { None => {
name.each(|name| { name.each(|name| {
// TODO handle browser prefixes ? // TODO handle browser prefixes ?
style.remove_property(name).unwrap_throw(); bindings::remove_style(element, &intern(name));
}); });
}, },
} }
@ -351,13 +368,13 @@ fn set_style_signal<A, B, C, D>(style: CssStyleDeclaration, callbacks: &mut Call
} }
// TODO check that the property *actually* was changed ? // TODO check that the property *actually* was changed ?
// TODO maybe use AsRef<Object> ?
fn set_property<A, B, C>(element: &A, name: &B, value: C) where A: AsRef<JsValue>, B: MultiStr, C: Into<JsValue> { fn set_property<A, B, C>(element: &A, name: &B, value: C) where A: AsRef<JsValue>, B: MultiStr, C: Into<JsValue> {
let element = element.as_ref(); let element = element.as_ref();
let value = value.into(); let value = value.into();
name.each(|name| { name.each(|name| {
// TODO can this be made more efficient ? bindings::set_property(element, &intern(name), &value);
assert!(Reflect::set(element, &JsValue::from(name), &value).unwrap_throw());
}); });
} }
@ -400,7 +417,7 @@ impl<A> DomBuilder<A> {
pub fn global_event<T, F>(mut self, listener: F) -> Self pub fn global_event<T, F>(mut self, listener: F) -> Self
where T: StaticEvent, where T: StaticEvent,
F: FnMut(T) + 'static { F: FnMut(T) + 'static {
self._event(&window().unwrap_throw(), listener); self._event(&bindings::window(), listener);
self self
} }
@ -531,14 +548,14 @@ impl<A> DomBuilder<A> where A: AsRef<Node> {
#[inline] #[inline]
pub fn children<'a, B: IntoIterator<Item = &'a mut Dom>>(mut self, children: B) -> Self { pub fn children<'a, B: IntoIterator<Item = &'a mut Dom>>(mut self, children: B) -> Self {
self.check_children(); self.check_children();
operations::insert_children_iter(self.element.as_ref(), &mut self.callbacks, children).unwrap_throw(); operations::insert_children_iter(self.element.as_ref(), &mut self.callbacks, children);
self self
} }
#[inline] #[inline]
pub fn text(mut self, value: &str) -> Self { pub fn text(mut self, value: &str) -> Self {
self.check_children(); self.check_children();
self.element.as_ref().set_text_content(Some(value)); bindings::set_text_content(self.element.as_ref(), &intern(value));
self self
} }
@ -550,7 +567,8 @@ impl<A> DomBuilder<A> where A: AsRef<Node> {
self.callbacks.after_remove(for_each(value, move |value| { self.callbacks.after_remove(for_each(value, move |value| {
let value = value.as_str(); let value = value.as_str();
element.set_text_content(Some(value)); // TODO maybe intern this ?
bindings::set_text_content(&element, &JsString::from(value));
})); }));
} }
@ -581,26 +599,35 @@ impl<A> DomBuilder<A> where A: AsRef<Node> {
impl<A> DomBuilder<A> where A: AsRef<Element> { impl<A> DomBuilder<A> where A: AsRef<Element> {
#[inline] #[inline]
pub fn attribute<B>(self, name: B, value: &str) -> Self where B: MultiStr { pub fn attribute<B>(self, name: B, value: &str) -> Self where B: MultiStr {
let element = self.element.as_ref();
let value = intern(value);
name.each(|name| { name.each(|name| {
dom_operations::set_attribute(self.element.as_ref(), name, value).unwrap_throw(); bindings::set_attribute(element, &intern(name), &value);
}); });
self self
} }
#[inline] #[inline]
pub fn attribute_namespace<B>(self, namespace: &str, name: B, value: &str) -> Self where B: MultiStr { pub fn attribute_namespace<B>(self, namespace: &str, name: B, value: &str) -> Self where B: MultiStr {
let element = self.element.as_ref();
let namespace = intern(namespace);
let value = intern(value);
name.each(|name| { name.each(|name| {
dom_operations::set_attribute_ns(self.element.as_ref(), namespace, name, value).unwrap_throw(); bindings::set_attribute_ns(element, &namespace, &intern(name), &value);
}); });
self self
} }
#[inline] #[inline]
pub fn class<B>(self, name: B) -> Self where B: MultiStr { pub fn class<B>(self, name: B) -> Self where B: MultiStr {
let list = self.element.as_ref().class_list(); let element = self.element.as_ref();
name.each(|name| { name.each(|name| {
dom_operations::add_class(&list, name).unwrap_throw(); bindings::add_class(element, &intern(name));
}); });
self self
@ -630,14 +657,16 @@ impl<A> DomBuilder<A> where A: AsRef<Element> {
match value { match value {
Some(value) => { Some(value) => {
let value = value.as_str(); let value = value.as_str();
// TODO should this intern ?
let value = intern(value);
name.each(|name| { name.each(|name| {
dom_operations::set_attribute(element, &name, value).unwrap_throw(); bindings::set_attribute(element, &intern(name), &value);
}); });
}, },
None => { None => {
name.each(|name| { name.each(|name| {
dom_operations::remove_attribute(element, &name).unwrap_throw(); bindings::remove_attribute(element, &intern(name));
}); });
}, },
} }
@ -662,20 +691,22 @@ impl<A> DomBuilder<A> where A: AsRef<Element> {
D: OptionStr<Output = C>, D: OptionStr<Output = C>,
E: Signal<Item = D> + 'static { E: Signal<Item = D> + 'static {
let namespace = namespace.to_owned(); let namespace = intern(namespace);
set_option(self.element.as_ref().clone(), &mut self.callbacks, value, move |element, value| { set_option(self.element.as_ref().clone(), &mut self.callbacks, value, move |element, value| {
match value { match value {
Some(value) => { Some(value) => {
let value = value.as_str(); let value = value.as_str();
// TODO should this intern ?
let value = intern(value);
name.each(|name| { name.each(|name| {
dom_operations::set_attribute_ns(element, &namespace, &name, value).unwrap_throw(); bindings::set_attribute_ns(element, &namespace, &intern(name), &value);
}); });
}, },
None => { None => {
name.each(|name| { name.each(|name| {
dom_operations::remove_attribute_ns(element, &namespace, &name).unwrap_throw(); bindings::remove_attribute_ns(element, &namespace, &intern(name));
}); });
}, },
} }
@ -698,7 +729,7 @@ impl<A> DomBuilder<A> where A: AsRef<Element> {
where B: MultiStr + 'static, where B: MultiStr + 'static,
C: Signal<Item = bool> + 'static { C: Signal<Item = bool> + 'static {
let list = self.element.as_ref().class_list(); let element = self.element.as_ref().clone();
let mut is_set = false; let mut is_set = false;
@ -708,7 +739,7 @@ impl<A> DomBuilder<A> where A: AsRef<Element> {
is_set = true; is_set = true;
name.each(|name| { name.each(|name| {
dom_operations::add_class(&list, name).unwrap_throw(); bindings::add_class(&element, &intern(name));
}); });
} }
@ -717,7 +748,7 @@ impl<A> DomBuilder<A> where A: AsRef<Element> {
is_set = false; is_set = false;
name.each(|name| { name.each(|name| {
dom_operations::remove_class(&list, name).unwrap_throw(); bindings::remove_class(&element, &intern(name));
}); });
} }
} }
@ -759,12 +790,14 @@ impl<A> DomBuilder<A> where A: AsRef<Element> {
#[inline] #[inline]
pub fn scroll_left_signal<B>(mut self, signal: B) -> Self where B: Signal<Item = Option<i32>> + 'static { pub fn scroll_left_signal<B>(mut self, signal: B) -> Self where B: Signal<Item = Option<i32>> + 'static {
// TODO bindings function for this ?
self.set_scroll_signal(signal, Element::set_scroll_left); self.set_scroll_signal(signal, Element::set_scroll_left);
self self
} }
#[inline] #[inline]
pub fn scroll_top_signal<B>(mut self, signal: B) -> Self where B: Signal<Item = Option<i32>> + 'static { pub fn scroll_top_signal<B>(mut self, signal: B) -> Self where B: Signal<Item = Option<i32>> + 'static {
// TODO bindings function for this ?
self.set_scroll_signal(signal, Element::set_scroll_top); self.set_scroll_signal(signal, Element::set_scroll_top);
self self
} }
@ -775,7 +808,7 @@ impl<A> DomBuilder<A> where A: AsRef<HtmlElement> {
pub fn style<B, C>(self, name: B, value: C) -> Self pub fn style<B, C>(self, name: B, value: C) -> Self
where B: MultiStr, where B: MultiStr,
C: MultiStr { C: MultiStr {
set_style(&self.element.as_ref().style(), &name, value, false); set_style(self.element.as_ref(), &name, value, false, true);
self self
} }
@ -783,7 +816,7 @@ impl<A> DomBuilder<A> where A: AsRef<HtmlElement> {
pub fn style_important<B, C>(self, name: B, value: C) -> Self pub fn style_important<B, C>(self, name: B, value: C) -> Self
where B: MultiStr, where B: MultiStr,
C: MultiStr { C: MultiStr {
set_style(&self.element.as_ref().style(), &name, value, true); set_style(self.element.as_ref(), &name, value, true, true);
self self
} }
} }
@ -796,7 +829,7 @@ impl<A> DomBuilder<A> where A: AsRef<HtmlElement> {
D: OptionStr<Output = C>, D: OptionStr<Output = C>,
E: Signal<Item = D> + 'static { E: Signal<Item = D> + 'static {
set_style_signal(self.element.as_ref().style(), &mut self.callbacks, name, value, false); set_style_signal(self.element.as_ref(), &mut self.callbacks, name, value, false);
self self
} }
@ -807,7 +840,7 @@ impl<A> DomBuilder<A> where A: AsRef<HtmlElement> {
D: OptionStr<Output = C>, D: OptionStr<Output = C>,
E: Signal<Item = D> + 'static { E: Signal<Item = D> + 'static {
set_style_signal(self.element.as_ref().style(), &mut self.callbacks, name, value, true); set_style_signal(self.element.as_ref(), &mut self.callbacks, name, value, true);
self self
} }
@ -820,7 +853,12 @@ impl<A> DomBuilder<A> where A: AsRef<HtmlElement> {
// This needs to use `after_insert` because calling `.focus()` on an element before it is in the DOM has no effect // This needs to use `after_insert` because calling `.focus()` on an element before it is in the DOM has no effect
self.callbacks.after_insert(move |_| { self.callbacks.after_insert(move |_| {
// TODO avoid updating if the focused state hasn't changed ? // TODO avoid updating if the focused state hasn't changed ?
dom_operations::set_focused(&element, value).unwrap_throw(); if value {
bindings::focus(&element);
} else {
bindings::blur(&element);
}
}); });
self self
@ -837,7 +875,12 @@ impl<A> DomBuilder<A> where A: AsRef<HtmlElement> {
// TODO verify that this is correct under all circumstances // TODO verify that this is correct under all circumstances
callbacks.after_remove(for_each(value, move |value| { callbacks.after_remove(for_each(value, move |value| {
// TODO avoid updating if the focused state hasn't changed ? // TODO avoid updating if the focused state hasn't changed ?
dom_operations::set_focused(&element, value).unwrap_throw(); if value {
bindings::focus(&element);
} else {
bindings::blur(&element);
}
})); }));
}); });
} }
@ -855,7 +898,7 @@ impl<A> DomBuilder<A> where A: AsRef<HtmlElement> {
// TODO better warning message for must_use // TODO better warning message for must_use
#[must_use] #[must_use]
pub struct StylesheetBuilder { pub struct StylesheetBuilder {
element: CssStyleDeclaration, element: CssStyleRule,
callbacks: Callbacks, callbacks: Callbacks,
} }
@ -866,30 +909,14 @@ impl StylesheetBuilder {
// TODO can this be made faster ? // TODO can this be made faster ?
// TODO somehow share this safely between threads ? // TODO somehow share this safely between threads ?
thread_local! { thread_local! {
static STYLESHEET: CssStyleSheet = { static STYLESHEET: CssStyleSheet = bindings::create_stylesheet();
// TODO use createElementNS ?
let e = document().create_element("style").unwrap_throw();
// TODO maybe don't use unchecked ?
let e: &HtmlStyleElement = e.unchecked_ref();
e.set_type("text/css");
document().head().unwrap_throw().append_child(e).unwrap_throw();
// TODO maybe don't use unchecked ?
e.sheet().unwrap_throw().unchecked_into()
};
} }
let element = STYLESHEET.with(|stylesheet| { // TODO maybe intern ?
let rules = stylesheet.css_rules().unwrap_throw(); let selector = JsString::from(selector);
let length = rules.length(); let element = STYLESHEET.with(move |stylesheet| {
bindings::make_style_rule(stylesheet, &selector)
stylesheet.insert_rule_with_index(&format!("{}{{}}", selector), length).unwrap_throw();
// TODO maybe don't use unchecked ?
rules.get(length).unwrap_throw().unchecked_ref::<CssStyleRule>().style()
}); });
Self { Self {
@ -902,7 +929,7 @@ impl StylesheetBuilder {
pub fn style<B, C>(self, name: B, value: C) -> Self pub fn style<B, C>(self, name: B, value: C) -> Self
where B: MultiStr, where B: MultiStr,
C: MultiStr { C: MultiStr {
set_style(&self.element, &name, value, false); set_style(&self.element, &name, value, false, true);
self self
} }
@ -910,7 +937,7 @@ impl StylesheetBuilder {
pub fn style_important<B, C>(self, name: B, value: C) -> Self pub fn style_important<B, C>(self, name: B, value: C) -> Self
where B: MultiStr, where B: MultiStr,
C: MultiStr { C: MultiStr {
set_style(&self.element, &name, value, true); set_style(&self.element, &name, value, true, true);
self self
} }
@ -921,7 +948,7 @@ impl StylesheetBuilder {
D: OptionStr<Output = C>, D: OptionStr<Output = C>,
E: Signal<Item = D> + 'static { E: Signal<Item = D> + 'static {
set_style_signal(self.element.clone(), &mut self.callbacks, name, value, false); set_style_signal(&self.element, &mut self.callbacks, name, value, false);
self self
} }
@ -932,7 +959,7 @@ impl StylesheetBuilder {
D: OptionStr<Output = C>, D: OptionStr<Output = C>,
E: Signal<Item = D> + 'static { E: Signal<Item = D> + 'static {
set_style_signal(self.element.clone(), &mut self.callbacks, name, value, true); set_style_signal(&self.element, &mut self.callbacks, name, value, true);
self self
} }

View File

@ -1,100 +0,0 @@
use wasm_bindgen::{JsValue, UnwrapThrowExt};
use web_sys::{Node, HtmlElement, Element, DomTokenList};
#[inline]
pub(crate) fn get_at(parent: &Node, index: u32) -> Node {
parent.child_nodes().get(index).unwrap_throw()
}
// TODO make this more efficient
#[inline]
pub(crate) fn move_from_to(parent: &Node, old_index: u32, new_index: u32) -> Result<(), JsValue> {
let child = get_at(parent, old_index);
parent.remove_child(&child)?;
insert_at(parent, new_index, &child)?;
Ok(())
}
// TODO make this more efficient
#[inline]
pub(crate) fn insert_at(parent: &Node, index: u32, child: &Node) -> Result<(), JsValue> {
parent.insert_before(child, Some(&get_at(parent, index)))?;
Ok(())
}
// TODO make this more efficient
#[inline]
pub(crate) fn update_at(parent: &Node, index: u32, child: &Node) -> Result<(), JsValue> {
parent.replace_child(child, &get_at(parent, index))?;
Ok(())
}
// TODO make this more efficient
#[inline]
pub(crate) fn remove_at(parent: &Node, index: u32) -> Result<(), JsValue> {
parent.remove_child(&get_at(parent, index))?;
Ok(())
}
#[inline]
pub(crate) fn set_focused(element: &HtmlElement, focused: bool) -> Result<(), JsValue> {
if focused {
element.focus()?;
} else {
element.blur()?;
}
Ok(())
}
#[inline]
pub(crate) fn add_class(list: &DomTokenList, name: &str) -> Result<(), JsValue> {
list.add_1(name)?;
Ok(())
}
#[inline]
pub(crate) fn remove_class(list: &DomTokenList, name: &str) -> Result<(), JsValue> {
list.remove_1(name)?;
Ok(())
}
// TODO check that the attribute *actually* was changed
#[inline]
pub(crate) fn set_attribute(element: &Element, name: &str, value: &str) -> Result<(), JsValue> {
element.set_attribute(name, value)?;
Ok(())
}
// TODO check that the attribute *actually* was changed
#[inline]
pub(crate) fn set_attribute_ns(element: &Element, namespace: &str, name: &str, value: &str) -> Result<(), JsValue> {
element.set_attribute_ns(Some(namespace), name, value)?;
Ok(())
}
#[inline]
pub(crate) fn remove_attribute_ns(element: &Element, namespace: &str, name: &str) -> Result<(), JsValue> {
element.remove_attribute_ns(Some(namespace), name)?;
Ok(())
}
#[inline]
pub(crate) fn remove_attribute(element: &Element, name: &str) -> Result<(), JsValue> {
element.remove_attribute(name)?;
Ok(())
}
#[inline]
pub(crate) fn remove_all_children(node: &Node) {
node.set_text_content(None);
}

View File

@ -82,6 +82,7 @@ make_event!(Input, "input" => web_sys::InputEvent);
impl Input { impl Input {
// TODO should this work on other types as well ? // TODO should this work on other types as well ?
// TODO return JsString ?
pub fn value(&self) -> Option<String> { pub fn value(&self) -> Option<String> {
let target = self.target()?; let target = self.target()?;

View File

@ -6,12 +6,14 @@
#[macro_use] #[macro_use]
mod macros; mod macros;
mod utils; mod utils;
mod cache;
mod bindings;
mod callbacks; mod callbacks;
mod operations; mod operations;
mod dom_operations;
mod dom; mod dom;
pub use dom::*; pub use dom::*;
pub use cache::*;
pub mod traits; pub mod traits;
pub mod animation; pub mod animation;
pub mod routing; pub mod routing;

View File

@ -8,13 +8,12 @@ use futures_signals::{cancelable_future, CancelableFutureHandle};
use futures_signals::signal::{Signal, SignalExt}; use futures_signals::signal::{Signal, SignalExt};
use futures_signals::signal_vec::{VecDiff, SignalVec, SignalVecExt}; use futures_signals::signal_vec::{VecDiff, SignalVec, SignalVecExt};
use web_sys::Node; use web_sys::Node;
use wasm_bindgen::{JsValue, UnwrapThrowExt}; use wasm_bindgen::UnwrapThrowExt;
use wasm_bindgen_futures::futures_0_3::spawn_local; use wasm_bindgen_futures::futures_0_3::spawn_local;
use crate::dom_operations; use crate::bindings;
use crate::dom::Dom; use crate::dom::Dom;
use crate::callbacks::Callbacks; use crate::callbacks::Callbacks;
use crate::utils::document;
#[inline] #[inline]
@ -53,45 +52,15 @@ fn for_each_vec<A, B>(signal: A, mut callback: B) -> CancelableFutureHandle
} }
/*
// TODO inline this ?
pub fn insert_children_signal<A, B, C>(element: &A, callbacks: &mut Callbacks, signal: C)
where A: INode + Clone + 'static,
B: IntoIterator<Item = Dom>,
C: Signal<Item = B> + 'static {
let element = element.clone();
let mut old_children: Vec<Dom> = vec![];
let handle = for_each(signal, move |value| {
dom_operations::remove_all_children(&element);
old_children = value.into_iter().map(|mut dom| {
element.append_child(&dom.element);
// TODO don't trigger this if the parent isn't inserted into the DOM
dom.callbacks.trigger_after_insert();
dom
}).collect();
});
// TODO verify that this will drop `old_children`
callbacks.after_remove(handle);
}*/
#[inline] #[inline]
pub(crate) fn insert_children_iter<'a, A: IntoIterator<Item = &'a mut Dom>>(element: &Node, callbacks: &mut Callbacks, value: A) -> Result<(), JsValue> { pub(crate) fn insert_children_iter<'a, A: IntoIterator<Item = &'a mut Dom>>(element: &Node, callbacks: &mut Callbacks, value: A) {
for dom in value.into_iter() { for dom in value.into_iter() {
// TODO can this be made more efficient ? // TODO can this be made more efficient ?
callbacks.after_insert.append(&mut dom.callbacks.after_insert); callbacks.after_insert.append(&mut dom.callbacks.after_insert);
callbacks.after_remove.append(&mut dom.callbacks.after_remove); callbacks.after_remove.append(&mut dom.callbacks.after_remove);
element.append_child(&dom.element)?; bindings::append_child(element, &dom.element);
} }
Ok(())
} }
@ -126,12 +95,12 @@ pub(crate) fn insert_children_signal_vec<A>(element: Node, callbacks: &mut Callb
}); });
} }
fn process_change(state: &mut State, element: &Node, change: VecDiff<Dom>) -> Result<(), JsValue> { fn process_change(state: &mut State, element: &Node, change: VecDiff<Dom>) {
match change { match change {
VecDiff::Replace { values } => { VecDiff::Replace { values } => {
// TODO is this correct ? // TODO is this correct ?
if state.children.len() > 0 { if state.children.len() > 0 {
dom_operations::remove_all_children(element); bindings::remove_all_children(element);
for dom in state.children.drain(..) { for dom in state.children.drain(..) {
dom.callbacks.discard(); dom.callbacks.discard();
@ -142,25 +111,12 @@ pub(crate) fn insert_children_signal_vec<A>(element: Node, callbacks: &mut Callb
let is_inserted = state.is_inserted; let is_inserted = state.is_inserted;
let mut has_inserts = false;
// TODO is it worth it to use fragments ?
let fragment = document().create_document_fragment();
for dom in state.children.iter_mut() { for dom in state.children.iter_mut() {
dom.callbacks.leak(); dom.callbacks.leak();
fragment.append_child(&dom.element)?; bindings::append_child(element, &dom.element);
if !dom.callbacks.after_insert.is_empty() { if is_inserted {
has_inserts = true;
}
}
element.append_child(&fragment)?;
if is_inserted && has_inserts {
for dom in state.children.iter_mut() {
dom.callbacks.trigger_after_insert(); dom.callbacks.trigger_after_insert();
} }
} }
@ -168,7 +124,7 @@ pub(crate) fn insert_children_signal_vec<A>(element: Node, callbacks: &mut Callb
VecDiff::InsertAt { index, mut value } => { VecDiff::InsertAt { index, mut value } => {
// TODO better usize -> u32 conversion // TODO better usize -> u32 conversion
dom_operations::insert_at(element, index as u32, &value.element)?; bindings::insert_at(element, index as u32, &value.element);
value.callbacks.leak(); value.callbacks.leak();
@ -181,7 +137,7 @@ pub(crate) fn insert_children_signal_vec<A>(element: Node, callbacks: &mut Callb
}, },
VecDiff::Push { mut value } => { VecDiff::Push { mut value } => {
element.append_child(&value.element)?; bindings::append_child(element, &value.element);
value.callbacks.leak(); value.callbacks.leak();
@ -195,7 +151,7 @@ pub(crate) fn insert_children_signal_vec<A>(element: Node, callbacks: &mut Callb
VecDiff::UpdateAt { index, mut value } => { VecDiff::UpdateAt { index, mut value } => {
// TODO better usize -> u32 conversion // TODO better usize -> u32 conversion
dom_operations::update_at(element, index as u32, &value.element)?; bindings::update_at(element, index as u32, &value.element);
value.callbacks.leak(); value.callbacks.leak();
@ -213,15 +169,16 @@ pub(crate) fn insert_children_signal_vec<A>(element: Node, callbacks: &mut Callb
VecDiff::Move { old_index, new_index } => { VecDiff::Move { old_index, new_index } => {
let value = state.children.remove(old_index); let value = state.children.remove(old_index);
state.children.insert(new_index, value); bindings::remove_child(element, &value.element);
// TODO better usize -> u32 conversion // TODO better usize -> u32 conversion
dom_operations::move_from_to(element, old_index as u32, new_index as u32)?; bindings::insert_at(element, new_index as u32, &value.element);
state.children.insert(new_index, value);
}, },
VecDiff::RemoveAt { index } => { VecDiff::RemoveAt { index } => {
// TODO better usize -> u32 conversion // TODO better usize -> u32 conversion
dom_operations::remove_at(element, index as u32)?; bindings::remove_at(element, index as u32);
state.children.remove(index).callbacks.discard(); state.children.remove(index).callbacks.discard();
}, },
@ -231,7 +188,7 @@ pub(crate) fn insert_children_signal_vec<A>(element: Node, callbacks: &mut Callb
// TODO create remove_last_child function ? // TODO create remove_last_child function ?
// TODO better usize -> u32 conversion // TODO better usize -> u32 conversion
dom_operations::remove_at(element, index as u32)?; bindings::remove_at(element, index as u32);
state.children.pop().unwrap_throw().callbacks.discard(); state.children.pop().unwrap_throw().callbacks.discard();
}, },
@ -240,7 +197,7 @@ pub(crate) fn insert_children_signal_vec<A>(element: Node, callbacks: &mut Callb
// TODO is this correct ? // TODO is this correct ?
// TODO is this needed, or is it guaranteed by VecDiff ? // TODO is this needed, or is it guaranteed by VecDiff ?
if state.children.len() > 0 { if state.children.len() > 0 {
dom_operations::remove_all_children(element); bindings::remove_all_children(element);
for dom in state.children.drain(..) { for dom in state.children.drain(..) {
dom.callbacks.discard(); dom.callbacks.discard();
@ -248,14 +205,12 @@ pub(crate) fn insert_children_signal_vec<A>(element: Node, callbacks: &mut Callb
} }
}, },
} }
Ok(())
} }
// TODO verify that this will drop `children` // TODO verify that this will drop `children`
callbacks.after_remove(for_each_vec(signal, move |change| { callbacks.after_remove(for_each_vec(signal, move |change| {
let mut state = state.lock().unwrap_throw(); let mut state = state.lock().unwrap_throw();
process_change(&mut state, &element, change).unwrap_throw(); process_change(&mut state, &element, change);
})); }));
} }

View File

@ -1,128 +1,81 @@
use wasm_bindgen::{JsValue, UnwrapThrowExt}; use std::borrow::Cow;
use web_sys::{window, Url, EventTarget, HtmlElement};
use futures_signals::signal::{Mutable, Signal}; use js_sys::JsString;
use web_sys::{EventTarget, HtmlElement};
use lazy_static::lazy_static;
use futures_signals::signal::{Mutable, ReadOnlyMutable};
use gloo::events::EventListener; use gloo::events::EventListener;
use crate::bindings;
use crate::dom::{Dom, DomBuilder}; use crate::dom::{Dom, DomBuilder};
use crate::events; use crate::events;
/*pub struct State<A> {
value: Mutable<Option<A>>,
callback: Value,
}
impl<A> State<A> {
pub fn new() -> Self {
// TODO replace with stdweb function
let value = Mutable::new(js!( return history.state; ).try_into().unwrap_throw());
let callback = |state: Option<A>| {
value.set(state);
};
Self {
value,
callback: js!(
var callback = @{callback};
addEventListener("popstate", function (e) {
callback(e.state);
}, true);
return callback;
),
}
}
pub fn set(&self, value: A) {
window().history().replace_state(value, "", None).unwrap_throw();
self.value.set(value);
}
}*/
fn current_url_string() -> Result<String, JsValue> {
Ok(window().unwrap_throw().location().href()?)
}
// TODO inline ? // TODO inline ?
fn change_url(mutable: &Mutable<Url>) -> Result<(), JsValue> { fn change_url(mutable: &Mutable<String>) {
let mut lock = mutable.lock_mut(); let mut lock = mutable.lock_mut();
let new_url = current_url_string()?; let new_url = String::from(bindings::current_url());
// TODO test that this doesn't notify if the URLs are the same
// TODO helper method for this // TODO helper method for this
// TODO can this be made more efficient ? // TODO can this be made more efficient ?
if lock.href() != new_url { if *lock != new_url {
*lock = Url::new(&new_url)?; *lock = new_url;
} }
Ok(())
} }
struct CurrentUrl { struct CurrentUrl {
value: Mutable<Url>, value: Mutable<String>,
_listener: EventListener,
} }
impl CurrentUrl { impl CurrentUrl {
fn new() -> Result<Self, JsValue> { fn new() -> Self {
// TODO can this be made more efficient ? // TODO can this be made more efficient ?
let value = Mutable::new(Url::new(&current_url_string()?)?); let value = Mutable::new(String::from(bindings::current_url()));
Ok(Self { // TODO clean this up somehow ?
_listener: EventListener::new(&window().unwrap_throw(), "popstate", { EventListener::new(&bindings::window(), "popstate", {
let value = value.clone(); let value = value.clone();
move |_| { move |_| {
change_url(&value).unwrap_throw(); change_url(&value);
} }
}), }).forget();
Self {
value, value,
}) }
} }
} }
// TODO somehow share this safely between threads ?
thread_local! { lazy_static! {
static URL: CurrentUrl = CurrentUrl::new().unwrap_throw(); static ref URL: CurrentUrl = CurrentUrl::new();
} }
#[inline] #[inline]
pub fn current_url() -> Url { pub fn url() -> ReadOnlyMutable<String> {
URL.with(|url| url.value.get_cloned()) URL.value.read_only()
}
#[inline]
pub fn url() -> impl Signal<Item = Url> {
URL.with(|url| url.value.signal_cloned())
} }
// TODO if URL hasn't been created yet, don't create it // TODO if URL hasn't been created yet, don't create it
#[inline] #[inline]
pub fn go_to_url(new_url: &str) { pub fn go_to_url(new_url: &str) {
window() // TODO intern ?
.unwrap_throw() bindings::go_to_url(&JsString::from(new_url));
.history()
.unwrap_throw()
// TODO is this the best state object to use ?
.push_state_with_url(&JsValue::NULL, "", Some(new_url))
.unwrap_throw();
URL.with(|url| { change_url(&URL.value);
change_url(&url.value).unwrap_throw();
});
} }
// TODO somehow use &str rather than String, maybe Cow ?
#[inline] #[inline]
pub fn on_click_go_to_url<A>(new_url: String) -> impl FnOnce(DomBuilder<A>) -> DomBuilder<A> where A: AsRef<EventTarget> { pub fn on_click_go_to_url<A, B>(new_url: A) -> impl FnOnce(DomBuilder<B>) -> DomBuilder<B>
where A: Into<Cow<'static, str>>,
B: AsRef<EventTarget> {
let new_url = new_url.into();
#[inline] #[inline]
move |dom| { move |dom| {
dom.event_preventable(move |e: events::Click| { dom.event_preventable(move |e: events::Click| {
@ -134,12 +87,16 @@ pub fn on_click_go_to_url<A>(new_url: String) -> impl FnOnce(DomBuilder<A>) -> D
// TODO better type than HtmlElement // TODO better type than HtmlElement
// TODO maybe make this a macro ?
#[inline] #[inline]
pub fn link<F>(url: &str, f: F) -> Dom where F: FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement> { pub fn link<A, F>(url: A, f: F) -> Dom
where A: Into<Cow<'static, str>>,
F: FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement> {
let url = url.into();
html!("a", { html!("a", {
.attribute("href", url) .attribute("href", &url)
// TODO somehow avoid this allocation .apply(on_click_go_to_url(url))
.apply(on_click_go_to_url(url.to_string()))
.apply(f) .apply(f)
}) })
} }

View File

@ -1,8 +1,9 @@
use std::mem::ManuallyDrop; use std::mem::ManuallyDrop;
use discard::Discard; use discard::Discard;
use wasm_bindgen::UnwrapThrowExt; use web_sys::{EventTarget};
use web_sys::{window, Document, EventTarget};
use gloo::events::{EventListener, EventListenerOptions}; use gloo::events::{EventListener, EventListenerOptions};
use crate::traits::StaticEvent; use crate::traits::StaticEvent;
@ -23,11 +24,6 @@ pub(crate) fn on_with_options<E, F>(element: &EventTarget, options: EventListene
} }
pub(crate) fn document() -> Document {
window().unwrap_throw().document().unwrap_throw()
}
// TODO move this into the discard crate // TODO move this into the discard crate
// TODO verify that this is correct and doesn't leak memory or cause memory safety // TODO verify that this is correct and doesn't leak memory or cause memory safety
pub(crate) struct ValueDiscard<A>(ManuallyDrop<A>); pub(crate) struct ValueDiscard<A>(ManuallyDrop<A>);