fix navigating when files are dropped

This commit is contained in:
Jonathan Kelley 2024-03-01 23:37:46 -08:00
parent 0ff0eb7846
commit 199173a409
No known key found for this signature in database
GPG key ID: 1FBB50F7EB0A08BE
23 changed files with 1244 additions and 1028 deletions

View file

@ -43,10 +43,8 @@ fn app() -> Element {
checked: enable_directory_upload,
oninput: move |evt| enable_directory_upload.set(evt.checked()),
},
label {
r#for: "directory-upload",
"Enable directory upload"
}
label { r#for: "directory-upload", "Enable directory upload" }
input {
r#type: "file",
@ -60,7 +58,6 @@ fn app() -> Element {
// cheating with a little bit of JS...
"ondragover": "this.style.backgroundColor='#88FF88';",
"ondragleave": "this.style.backgroundColor='#FFFFFF';",
id: "drop-zone",
prevent_default: "ondrop dragover dragenter",
ondrop: handle_file_drop,

View file

@ -15,23 +15,92 @@ fn app() -> Element {
rsx! {
div {
h1 { "Form" }
// The form element is used to create an HTML form for user input
// You can attach regular attributes to it
form {
oninput: move |ev| values.set(ev.values()),
id: "cool-form",
style: "display: flex; flex-direction: column;",
// You can attach a handler to the entire form
oninput: move |ev| {
println!("Input event: {:#?}", ev);
values.set(ev.values());
},
// On desktop/liveview, the form will not navigate the page - the expectation is that you handle
// The form event.
// Howver, if your form doesn't have a submit handler, it might navigate the page depending on the webview.
// We suggest always attaching a submit handler to the form.
onsubmit: move |ev| {
println!("Submit event: {:#?}", ev);
},
// Regular text inputs with handlers
label { r#for: "username", "Username" }
input {
r#type: "text",
name: "username",
oninput: move |ev| values.set(ev.values())
oninput: move |ev| {
println!("setting username");
values.set(ev.values());
}
}
// And then the various inputs that might exist
// Note for a value to be returned in .values(), it must be named!
label { r#for: "full-name", "Full Name" }
input { r#type: "text", name: "full-name" }
label { r#for: "email", "Email" }
input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" }
label { r#for: "password", "Password" }
input { r#type: "password", name: "password" }
input { r#type: "radio", name: "color", value: "red" }
label { r#for: "color", "Color" }
input { r#type: "radio", checked: true, name: "color", value: "red" }
input { r#type: "radio", name: "color", value: "blue" }
input { r#type: "radio", name: "color", value: "green" }
// Select multiple comes in as a comma separated list of selected values
// You should split them on the comma to get the values manually
label { r#for: "country", "Country" }
select {
name: "country",
multiple: true,
oninput: move |ev| {
println!("Input event: {:#?}", ev);
println!("Values: {:#?}", ev.value().split(',').collect::<Vec<_>>());
},
option { value: "usa", "USA" }
option { value: "canada", "Canada" }
option { value: "mexico", "Mexico" }
}
// Safari can be quirky with color inputs on mac.
// We recommend always providing a text input for color as a fallback.
label { r#for: "color", "Color" }
input { r#type: "color", value: "#000002", name: "head", id: "head" }
// Dates!
input {
min: "2018-01-01",
value: "2018-07-22",
r#type: "date",
name: "trip-start",
max: "2025-12-31",
id: "start"
}
// Buttons will submit your form by default.
button { r#type: "submit", value: "Submit", "Submit the form" }
}
}
div {
h1 { "Oninput Values" }
"{values:#?}"
pre { "{values:#?}" }
}
}
}

View file

@ -20,6 +20,10 @@ fn app() -> Element {
let received = RECEIVED_EVENTS();
let expected = utils::EXPECTED_EVENTS();
use_effect(move || {
println!("expecting {} events", utils::EXPECTED_EVENTS());
});
if expected != 0 && received == expected {
println!("all events recieved");
desktop_context.close();
@ -41,11 +45,15 @@ fn app() -> Element {
test_focus_in_div {}
test_focus_out_div {}
test_form_input {}
test_form_submit {}
test_select_multiple_options {}
}
}
}
fn test_mounted() -> Element {
use_hook(|| utils::EXPECTED_EVENTS.with_mut(|x| *x += 1));
rsx! {
div {
width: "100px",
@ -433,20 +441,24 @@ fn test_form_input() -> Element {
utils::mock_event_with_extra(
"form-username",
r#"new Event("input", {
bubbles: true,
cancelable: true,
composed: true,
})"#,
r#"new Event("input", { bubbles: true, cancelable: true, composed: true })"#,
r#"element.value = "hello";"#,
);
let set_values = move |ev: FormEvent| {
let set_username = move |ev: FormEvent| {
values.set(ev.values());
eprintln!("values: {:?}", values);
// The value of the input should match
assert_eq!(ev.value(), "hello");
// And then the value the form gives us should also match
values.with_mut(|x| {
assert_eq!(x.get("username").unwrap().deref(), &["hello"]);
assert_eq!(x.get("username").unwrap().deref(), "hello");
assert_eq!(x.get("full-name").unwrap().deref(), "lorem");
assert_eq!(x.get("password").unwrap().deref(), "ipsum");
assert_eq!(x.get("color").unwrap().deref(), "red");
});
RECEIVED_EVENTS.with_mut(|x| *x += 1);
};
rsx! {
@ -454,19 +466,90 @@ fn test_form_input() -> Element {
h1 { "Form" }
form {
id: "form",
oninput: move |ev| values.set(ev.values()),
oninput: move |ev| {
values.set(ev.values());
},
onsubmit: move |ev| {
println!("{:?}", ev);
},
input {
r#type: "text",
name: "username",
id: "form-username",
oninput: set_values,
oninput: set_username,
}
input { r#type: "text", name: "full-name" }
input { r#type: "password", name: "password" }
input { r#type: "radio", name: "color", value: "red" }
input { r#type: "text", name: "full-name", value: "lorem" }
input { r#type: "password", name: "password", value: "ipsum" }
input { r#type: "radio", name: "color", value: "red", checked: true }
input { r#type: "radio", name: "color", value: "blue" }
button { r#type: "submit", value: "Submit", "Submit the form" }
}
}
}
}
fn test_form_submit() -> Element {
let mut values = use_signal(|| HashMap::new());
utils::mock_event_with_extra(
"form-submitter",
r#"new Event("submit", { bubbles: true, cancelable: true, composed: true })"#,
r#"element.submit();"#,
);
let set_values = move |ev: FormEvent| {
values.set(ev.values());
values.with_mut(|x| {
assert_eq!(x.get("username").unwrap().deref(), "goodbye");
assert_eq!(x.get("full-name").unwrap().deref(), "lorem");
assert_eq!(x.get("password").unwrap().deref(), "ipsum");
assert_eq!(x.get("color").unwrap().deref(), "red");
});
RECEIVED_EVENTS.with_mut(|x| *x += 1);
};
rsx! {
div {
h1 { "Form" }
form {
id: "form-submitter",
onsubmit: set_values,
input { r#type: "text", name: "username", id: "username", value: "goodbye" }
input { r#type: "text", name: "full-name", value: "lorem" }
input { r#type: "password", name: "password", value: "ipsum" }
input { r#type: "radio", name: "color", value: "red", checked: true }
input { r#type: "radio", name: "color", value: "blue" }
button { r#type: "submit", value: "Submit", "Submit the form" }
}
}
}
}
fn test_select_multiple_options() -> Element {
utils::mock_event_with_extra(
"select-many",
r#"new Event("input", { bubbles: true, cancelable: true, composed: true })"#,
r#"
document.getElementById('usa').selected = true;
document.getElementById('canada').selected = true;
document.getElementById('mexico').selected = false;
"#,
);
rsx! {
select {
id: "select-many",
name: "country",
multiple: true,
oninput: move |ev| {
let values = ev.value();
let values = values.split(',').collect::<Vec<_>>();
assert_eq!(values, vec!["usa", "canada"]);
RECEIVED_EVENTS.with_mut(|x| *x += 1);
},
option { id: "usa", value: "usa", "USA" }
option { id: "canada", value: "canada", "Canada" }
option { id: "mexico", value: "mexico", selected: true, "Mexico" }
}
}
}

View file

@ -30,23 +30,19 @@ pub fn mock_event(id: &'static str, value: &'static str) {
}
pub fn mock_event_with_extra(id: &'static str, value: &'static str, extra: &'static str) {
EXPECTED_EVENTS.with_mut(|x| *x += 1);
use_hook(move || {
EXPECTED_EVENTS.with_mut(|x| *x += 1);
spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(5000)).await;
let js = format!(
r#"
//console.log("ran");
// Dispatch a synthetic event
let event = {};
let element = document.getElementById('{}');
console.log(element, event);
{}
let event = {value};
let element = document.getElementById('{id}');
{extra}
element.dispatchEvent(event);
"#,
value, id, extra
"#
);
eval(&js).await.unwrap();

View file

@ -1,5 +1,5 @@
use crate::{assets::*, edits::EditQueue};
use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
use dioxus_interpreter_js::unified_bindings::{native_js, SLEDGEHAMMER_JS};
use std::path::{Path, PathBuf};
use wry::{
http::{status::StatusCode, Request, Response},
@ -38,7 +38,7 @@ fn handle_edits_code() -> String {
}}"#
);
let mut interpreter = SLEDGEHAMMER_JS
let mut interpreter = native_js()
.replace("/*POST_HANDLE_EDITS*/", PREVENT_FILE_UPLOAD)
.replace("export", "")
+ &polling_request;

View file

@ -110,6 +110,8 @@ impl PointerInteraction for DragData {
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedDragData {
mouse: crate::point_interaction::SerializedPointInteraction,
#[serde(default)]
files: Option<crate::file_data::SerializedFileEngine>,
}

View file

@ -1,56 +1,11 @@
use crate::file_data::FileEngine;
use crate::file_data::HasFileData;
use std::ops::Deref;
use std::{collections::HashMap, fmt::Debug};
use dioxus_core::Event;
pub type FormEvent = Event<FormData>;
/// A form value that may either be a list of values or a single value
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
// this will serialize Text(String) -> String and VecText(Vec<String>) to Vec<String>
serde(untagged)
)]
#[derive(Debug, Clone, PartialEq)]
pub enum FormValue {
Text(String),
VecText(Vec<String>),
}
impl From<FormValue> for Vec<String> {
fn from(value: FormValue) -> Self {
match value {
FormValue::Text(s) => vec![s],
FormValue::VecText(vec) => vec,
}
}
}
impl Deref for FormValue {
type Target = [String];
fn deref(&self) -> &Self::Target {
self.as_slice()
}
}
impl FormValue {
/// Convenient way to represent Value as slice
pub fn as_slice(&self) -> &[String] {
match self {
FormValue::Text(s) => std::slice::from_ref(s),
FormValue::VecText(vec) => vec.as_slice(),
}
}
/// Convert into Vec<String>
pub fn to_vec(self) -> Vec<String> {
self.into()
}
}
/* DOMEvent: Send + SyncTarget relatedTarget */
pub struct FormData {
inner: Box<dyn HasFormData>,
@ -73,6 +28,7 @@ impl Debug for FormData {
f.debug_struct("FormEvent")
.field("value", &self.value())
.field("values", &self.values())
.field("valid", &self.valid())
.finish()
}
}
@ -106,8 +62,10 @@ impl FormData {
self.value().parse().unwrap_or(false)
}
/// Get the values of the form event
pub fn values(&self) -> HashMap<String, FormValue> {
/// Collect all the named form values from the containing form.
///
/// Every input must be named!
pub fn values(&self) -> HashMap<String, String> {
self.inner.values()
}
@ -120,6 +78,11 @@ impl FormData {
pub fn downcast<T: 'static>(&self) -> Option<&T> {
self.inner.as_any().downcast_ref::<T>()
}
/// Did this form pass its own validation?
pub fn valid(&self) -> bool {
self.inner.value().is_empty()
}
}
/// An object that has all the data for a form event
@ -128,7 +91,11 @@ pub trait HasFormData: HasFileData + std::any::Any {
Default::default()
}
fn values(&self) -> HashMap<String, FormValue> {
fn valid(&self) -> bool {
true
}
fn values(&self) -> HashMap<String, String> {
Default::default()
}
@ -164,8 +131,16 @@ impl FormData {
/// A serialized form data object
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedFormData {
#[serde(default)]
value: String,
values: HashMap<String, FormValue>,
#[serde(default)]
values: HashMap<String, String>,
#[serde(default)]
valid: bool,
#[serde(default)]
files: Option<crate::file_data::SerializedFileEngine>,
}
@ -174,13 +149,14 @@ impl SerializedFormData {
/// Create a new serialized form data object
pub fn new(
value: String,
values: HashMap<String, FormValue>,
values: HashMap<String, String>,
files: Option<crate::file_data::SerializedFileEngine>,
) -> Self {
Self {
value,
values,
files,
valid: true,
}
}
@ -189,6 +165,7 @@ impl SerializedFormData {
Self {
value: data.value(),
values: data.values(),
valid: data.valid(),
files: match data.files() {
Some(files) => {
let mut resolved_files = HashMap::new();
@ -211,6 +188,7 @@ impl SerializedFormData {
Self {
value: data.value(),
values: data.values(),
valid: data.valid(),
files: None,
}
}
@ -222,10 +200,14 @@ impl HasFormData for SerializedFormData {
self.value.clone()
}
fn values(&self) -> HashMap<String, FormValue> {
fn values(&self) -> HashMap<String, String> {
self.values.clone()
}
fn valid(&self) -> bool {
self.valid
}
fn as_any(&self) -> &dyn std::any::Any {
self
}

View file

@ -34,8 +34,19 @@ impl<'de> Deserialize<'de> for HtmlEvent {
data,
} = Inner::deserialize(deserializer)?;
// in debug mode let's try and be helpful as to why the deserialization failed
#[cfg(debug_assertions)]
{
_ = deserialize_raw(&name, data.clone()).unwrap_or_else(|e| {
panic!(
"Failed to deserialize event data for event {}: {:#?}\n'{:#?}'",
name, e, data,
)
});
}
Ok(HtmlEvent {
data: fun_name(&name, data).unwrap(),
data: deserialize_raw(&name, data).unwrap(),
element,
bubbles,
name,
@ -44,7 +55,7 @@ impl<'de> Deserialize<'de> for HtmlEvent {
}
#[cfg(feature = "serialize")]
fn fun_name(
fn deserialize_raw(
name: &str,
data: serde_value::Value,
) -> Result<EventData, serde_value::DeserializerError> {

View file

@ -27,13 +27,15 @@ This crate features bindings for the web and sledgehammer for increased performa
## Architecture
We use TypeScript to write the bindings and a very simple build.rs to convert them to javascript, minify them, and glue them into the rest of the project.
We use TypeScript to write the bindings and a very simple build.rs along with bun to convert them to javascript, minify them, and glue them into the rest of the project.
Not every snippet of JS will be used, so we split out the snippets from the core interpreter.
In theory, we *could* use Rust in the browser to do everything these bindings are doing. In reality, we want to stick with JS to skip the need for a WASM build step when running the LiveView and WebView renderers. We also want to use JS to prevent diverging behavior of things like canceling events, uploading files, and collecting form inputs. These details are tough to ensure 1:1 compatibility when implementing them in two languages.
If you want to contribute to the bindings, you'll need to have the typescript compiler installed on your machine, accessible via `tsc`.
If you want to contribute to the bindings, you'll need to have the typescript compiler installed on your machine as well as bun:
https://bun.sh/docs/installation
## Contributing

View file

@ -6,8 +6,7 @@ use std::{
fn main() {
// If any TS changes, re-run the build script
println!("cargo:rerun-if-changed=src/ts/*.ts");
println!("cargo:rerun-if-changed=*.json");
println!("cargo:rerun-if-changed=src/ts/*.ts,*.json");
// Compute the hash of the ts files
let hash = hash_dir("src/ts");
@ -21,8 +20,8 @@ fn main() {
// Otherwise, generate the bindings and write the new hash to disk
// Generate the bindings for both native and web
gen_bindings("native");
gen_bindings("web");
gen_bindings("interpreter_native", "native");
gen_bindings("interpreter_web", "web");
std::fs::write("src/js/hash.txt", hash.to_string()).unwrap();
}
@ -51,18 +50,21 @@ fn hash_dir(dir: &str) -> u64 {
// we need to hash each of the .ts files and add that hash to the JS files
// if the hashes don't match, we need to fail the build
// that way we also don't need
fn gen_bindings(name: &str) {
fn gen_bindings(input_name: &str, output_name: &str) {
// If the file is generated, and the hash is different, we need to generate it
let status = Command::new("tsc")
.arg("--p")
.arg(format!("tsconfig.{name}.json"))
let status = Command::new("bun")
.arg("build")
.arg(format!("src/ts/{input_name}.ts"))
.arg("--outfile")
.arg(format!("src/js/{output_name}.js"))
// .arg("--minify")
.status()
.unwrap();
if !status.success() {
panic!(
"Failed to generate bindings for {}. Make sure you have tsc installed",
name
input_name
);
}
}

View file

@ -1 +1 @@
13493771420133770074
4429706825984325407

File diff suppressed because it is too large Load diff

View file

@ -1,159 +1,130 @@
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
System.register("interpreter_core", [], function (exports_1, context_1) {
"use strict";
var Interpreter;
var __moduleName = context_1 && context_1.id;
return {
setters: [],
execute: function () {
Interpreter = (function () {
function Interpreter(root, handler) {
this.root = root;
this.nodes = [root];
this.stack = [root];
this.global = {};
this.local = {};
this.handler = handler;
}
Interpreter.prototype.createListener = function (event_name, element, bubbles) {
if (bubbles) {
if (this.global[event_name] === undefined) {
this.global[event_name] = { active: 1, callback: this.handler };
this.root.addEventListener(event_name, this.handler);
}
else {
this.global[event_name].active++;
}
}
else {
var id = element.getAttribute("data-dioxus-id");
if (!this.local[id]) {
this.local[id] = {};
}
element.addEventListener(event_name, this.handler);
}
};
Interpreter.prototype.removeListener = function (element, event_name, bubbles) {
if (bubbles) {
this.removeBubblingListener(event_name);
}
else {
this.removeNonBubblingListener(element, event_name);
}
};
Interpreter.prototype.removeBubblingListener = function (event_name) {
this.global[event_name].active--;
if (this.global[event_name].active === 0) {
this.root.removeEventListener(event_name, this.global[event_name].callback);
delete this.global[event_name];
}
};
Interpreter.prototype.removeNonBubblingListener = function (element, event_name) {
var id = element.getAttribute("data-dioxus-id");
delete this.local[id][event_name];
if (Object.keys(this.local[id]).length === 0) {
delete this.local[id];
}
element.removeEventListener(event_name, this.handler);
};
Interpreter.prototype.removeAllNonBubblingListeners = function (element) {
var id = element.getAttribute("data-dioxus-id");
delete this.local[id];
};
Interpreter.prototype.getNode = function (id) {
return this.nodes[id];
};
Interpreter.prototype.appendChildren = function (id, many) {
var root = this.nodes[id];
var els = this.stack.splice(this.stack.length - many);
for (var k = 0; k < many; k++) {
root.appendChild(els[k]);
}
};
return Interpreter;
}());
exports_1("Interpreter", Interpreter);
// src/ts/interpreter_core.ts
class Interpreter {
global;
local;
root;
handler;
nodes;
stack;
templates;
constructor(root, handler) {
this.handler = handler;
this.initialize(root);
}
initialize(root) {
this.global = {};
this.local = {};
this.root = root;
this.nodes = [root];
this.stack = [root];
this.templates = {};
}
createListener(event_name, element, bubbles) {
if (bubbles) {
if (this.global[event_name] === undefined) {
this.global[event_name] = { active: 1, callback: this.handler };
this.root.addEventListener(event_name, this.handler);
} else {
this.global[event_name].active++;
}
} else {
const id = element.getAttribute("data-dioxus-id");
if (!this.local[id]) {
this.local[id] = {};
}
element.addEventListener(event_name, this.handler);
}
}
removeListener(element, event_name, bubbles) {
if (bubbles) {
this.removeBubblingListener(event_name);
} else {
this.removeNonBubblingListener(element, event_name);
}
}
removeBubblingListener(event_name) {
this.global[event_name].active--;
if (this.global[event_name].active === 0) {
this.root.removeEventListener(event_name, this.global[event_name].callback);
delete this.global[event_name];
}
}
removeNonBubblingListener(element, event_name) {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id][event_name];
if (Object.keys(this.local[id]).length === 0) {
delete this.local[id];
}
element.removeEventListener(event_name, this.handler);
}
removeAllNonBubblingListeners(element) {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id];
}
getNode(id) {
return this.nodes[id];
}
appendChildren(id, many) {
const root = this.nodes[id];
const els = this.stack.splice(this.stack.length - many);
for (let k = 0;k < many; k++) {
root.appendChild(els[k]);
}
}
}
// src/ts/interpreter_web.ts
class PlatformInterpreter extends Interpreter {
m;
constructor(root, handler) {
super(root, handler);
}
LoadChild(ptr, len) {
let node = this.stack[this.stack.length - 1];
let ptr_end = ptr + len;
for (;ptr < ptr_end; ptr++) {
let end = this.m.getUint8(ptr);
for (node = node.firstChild;end > 0; end--) {
node = node.nextSibling;
}
}
return node;
}
saveTemplate(nodes, tmpl_id) {
this.templates[tmpl_id] = nodes;
}
hydrateRoot(ids) {
const hydrateNodes = document.querySelectorAll("[data-node-hydration]");
for (let i = 0;i < hydrateNodes.length; i++) {
const hydrateNode = hydrateNodes[i];
const hydration = hydrateNode.getAttribute("data-node-hydration");
const split = hydration.split(",");
const id = ids[parseInt(split[0])];
this.nodes[id] = hydrateNode;
if (split.length > 1) {
hydrateNode.listening = split.length - 1;
hydrateNode.setAttribute("data-dioxus-id", id.toString());
for (let j = 1;j < split.length; j++) {
const listener = split[j];
const split2 = listener.split(":");
const event_name = split2[0];
const bubbles = split2[1] === "1";
this.createListener(event_name, hydrateNode, bubbles);
}
};
});
System.register("interpreter_web", ["interpreter_core"], function (exports_2, context_2) {
"use strict";
var interpreter_core_1, WebInterpreter;
var __moduleName = context_2 && context_2.id;
return {
setters: [
function (interpreter_core_1_1) {
interpreter_core_1 = interpreter_core_1_1;
}
],
execute: function () {
WebInterpreter = (function (_super) {
__extends(WebInterpreter, _super);
function WebInterpreter(root, handler) {
return _super.call(this, root, handler) || this;
}
WebInterpreter.prototype.LoadChild = function (ptr, len) {
var node = this.stack[this.stack.length - 1];
var ptr_end = ptr + len;
for (; ptr < ptr_end; ptr++) {
var end = this.m.getUint8(ptr);
for (node = node.firstChild; end > 0; end--) {
node = node.nextSibling;
}
}
return node;
};
WebInterpreter.prototype.saveTemplate = function (nodes, tmpl_id) {
this.templates[tmpl_id] = nodes;
};
WebInterpreter.prototype.hydrateRoot = function (ids) {
var hydrateNodes = document.querySelectorAll('[data-node-hydration]');
for (var i = 0; i < hydrateNodes.length; i++) {
var hydrateNode = hydrateNodes[i];
var hydration = hydrateNode.getAttribute('data-node-hydration');
var split = hydration.split(',');
var id = ids[parseInt(split[0])];
this.nodes[id] = hydrateNode;
if (split.length > 1) {
hydrateNode.listening = split.length - 1;
hydrateNode.setAttribute('data-dioxus-id', id.toString());
for (var j = 1; j < split.length; j++) {
var listener = split[j];
var split2 = listener.split(':');
var event_name = split2[0];
var bubbles = split2[1] === '1';
this.createListener(event_name, hydrateNode, bubbles);
}
}
}
var treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
var currentNode = treeWalker.nextNode();
while (currentNode) {
var id = currentNode.textContent;
var split = id.split('node-id');
if (split.length > 1) {
this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling;
}
currentNode = treeWalker.nextNode();
}
};
return WebInterpreter;
}(interpreter_core_1.Interpreter));
exports_2("WebInterpreter", WebInterpreter);
}
};
});
}
}
const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
let currentNode = treeWalker.nextNode();
while (currentNode) {
const id = currentNode.textContent;
const split = id.split("node-id");
if (split.length > 1) {
this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling;
}
currentNode = treeWalker.nextNode();
}
}
}
export {
PlatformInterpreter
};

View file

@ -24,6 +24,7 @@ pub mod minimal_bindings {
/// Set the attribute of the node
pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>);
/// Roll up all the values from the node into a JS object that we can deserialize
pub fn collectFormValues(node: JsValue) -> JsValue;
}
}

View file

@ -1,53 +1,58 @@
// Consistently deserialize forms and form elements for use across web/desktop/mobile
type FormValues = { [key: string]: FormDataEntryValue[] };
type FormValues = {
valid?: boolean;
values: { [key: string]: FormDataEntryValue };
}
export function retriveValues(event: Event, target: HTMLElement): FormValues {
const contents: FormValues = {};
let contents: FormValues = {
values: {}
};
if (target instanceof HTMLFormElement && (event.type === "submit" || event.type === "input")) {
retrieveFormValues(target, contents);
}
// If there's a form...
let form = target.closest("form");
if (target instanceof HTMLSelectElement && (event.type === "input" || event.type === "change")) {
retriveInputsValues(target, contents);
// If the target is an input, and the event is input or change, we want to get the value without going through the form
if (form) {
if (
event.type === "input"
|| event.type === "change"
|| event.type === "submit"
|| event.type === "reset"
|| event.type === "click"
) {
contents = retrieveFormValues(form);
}
}
return contents;
}
export function retrieveFormValues(form: HTMLFormElement, contents: FormValues) {
// todo: maybe encode spaces or something?
// We encode select multiple as a comma separated list which breaks... when there's commas in the values
export function retrieveFormValues(form: HTMLFormElement): FormValues {
const formData = new FormData(form);
for (let name in formData.keys()) {
let element = form.elements.namedItem(name);
// todo: this is going to be a problem for select-multiple?
if (!(element instanceof HTMLInputElement)) {
continue;
const contents: { [key: string]: FormDataEntryValue } = {};
formData.forEach((value, key) => {
if (contents[key]) {
contents[key] += "," + value;
} else {
contents[key] = value;
}
switch (element.type) {
case "select-multiple":
contents[name] = formData.getAll(name);
break;
// By default, it's just a single value
default:
contents[name] = [formData.get(name)];
break;
}
}
});
return {
valid: form.checkValidity(),
values: contents
};
}
export function retriveInputsValues(target: HTMLSelectElement, contents: FormValues,) {
const selectData = target.options;
contents["options"] = [];
for (let i = 0; i < selectData.length; i++) {
let option = selectData[i];
if (option.selected) {
contents["options"].push(option.value.toString());
}
export function retriveSelectValue(target: HTMLSelectElement): string[] {
// there might be multiple...
let options = target.selectedOptions;
let values = [];
for (let i = 0; i < options.length; i++) {
values.push(options[i].value);
}
return values;
}

View file

@ -1,6 +1,8 @@
// The root interpreter class that holds state about the mapping between DOM and VirtualDom
// This always lives in the JS side of things, and is extended by the native and web interpreters
export type NodeId = number;
export class Interpreter {
// non bubbling events listen at the element the listener was created at
global: {
@ -22,12 +24,18 @@ export class Interpreter {
};
constructor(root: HTMLElement, handler: EventListener) {
this.root = root;
this.nodes = [root];
this.stack = [root];
this.handler = handler;
this.initialize(root);
}
initialize(root: HTMLElement) {
this.global = {};
this.local = {};
this.handler = handler;
this.root = root;
this.nodes = [root];
this.stack = [root];
this.templates = {};
}
createListener(event_name: string, element: HTMLElement, bubbles: boolean) {
@ -77,11 +85,11 @@ export class Interpreter {
delete this.local[id];
}
getNode(id: number): Node {
getNode(id: NodeId): Node {
return this.nodes[id];
}
appendChildren(id: number, many: number) {
appendChildren(id: NodeId, many: number) {
const root = this.nodes[id];
const els = this.stack.splice(this.stack.length - many);
for (let k = 0; k < many; k++) {

View file

@ -3,12 +3,11 @@
// This file lives on the renderer, not the host. It's basically a polyfill over functionality that the host can't
// provide since it doesn't have access to the dom.
import { retriveValues } from "./form";
import { Interpreter } from "./interpreter_core";
import { Interpreter, NodeId } from "./interpreter_core";
import { SerializedEvent, serializeEvent } from "./serialize";
import { setAttributeInner } from "./set_attribute";
export class NativeInterpreter extends Interpreter {
export class PlatformInterpreter extends Interpreter {
intercept_link_redirects: boolean;
ipc: any;
@ -21,6 +20,23 @@ export class NativeInterpreter extends Interpreter {
this.intercept_link_redirects = true;
this.liveview = false;
// attach an event listener on the body that prevents file drops from navigating
// this is because the browser will try to navigate to the file if it's dropped on the window
window.addEventListener("dragover", function (e) {
// check which element is our target
if (e.target instanceof Element && e.target.tagName != "INPUT") {
e.preventDefault();
}
}, false);
window.addEventListener("drop", function (e) {
// check which element is our target
if (e.target instanceof Element && e.target.tagName != "INPUT") {
e.preventDefault();
}
}, false);
// @ts-ignore - wry gives us this
this.ipc = window.ipc;
}
@ -29,14 +45,18 @@ export class NativeInterpreter extends Interpreter {
return JSON.stringify({ method, params });
}
scrollTo(id: number, behavior: ScrollBehavior) {
setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) {
setAttributeInner(node, field, value, ns);
}
scrollTo(id: NodeId, behavior: ScrollBehavior) {
const node = this.nodes[id];
if (node instanceof HTMLElement) {
node.scrollIntoView({ behavior });
}
}
getClientRect(id: number) {
getClientRect(id: NodeId): { type: string; origin: number[]; size: number[]; } | undefined {
const node = this.nodes[id];
if (node instanceof HTMLElement) {
const rect = node.getBoundingClientRect();
@ -48,7 +68,7 @@ export class NativeInterpreter extends Interpreter {
}
}
setFocus(id: number, focus: boolean) {
setFocus(id: NodeId, focus: boolean) {
const node = this.nodes[id];
if (node instanceof HTMLElement) {
@ -74,7 +94,7 @@ export class NativeInterpreter extends Interpreter {
return node;
}
AppendChildren(id: number, many: number) {
AppendChildren(id: NodeId, many: number) {
const root = this.nodes[id];
const els = this.stack.splice(this.stack.length - many);
@ -86,12 +106,7 @@ export class NativeInterpreter extends Interpreter {
handleEvent(event: Event, name: string, bubbles: boolean) {
const target = event.target!;
const realId = targetId(target)!;
const contents = serializeEvent(event);
// Attempt to retrive the values from the form and inputs
if (target instanceof HTMLElement) {
contents.values = retriveValues(event, target);
}
const contents = serializeEvent(event, target);
// Handle the event on the virtualdom and then preventDefault if it also preventsDefault
// Some listeners
@ -119,21 +134,26 @@ export class NativeInterpreter extends Interpreter {
}
} else {
// Run the event handler on the virtualdom
// capture/prevent default of the event if the virtualdom wants to
const res = handleVirtualdomEventSync(JSON.stringify(body));
const message = this.serializeIpcMessage("user_event", body);
this.ipc.postMessage(message);
if (res.preventDefault) {
event.preventDefault();
}
// // Run the event handler on the virtualdom
// // capture/prevent default of the event if the virtualdom wants to
// const res = handleVirtualdomEventSync(JSON.stringify(body));
if (res.stopPropagation) {
event.stopPropagation();
}
// if (res.preventDefault) {
// event.preventDefault();
// }
// if (res.stopPropagation) {
// event.stopPropagation();
// }
}
}
async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: number, name: string) {
// A liveview only function
// Desktop will intercept the event before it hits this
async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) {
let files = target.files!;
let file_contents: { [name: string]: number[] } = {};
@ -251,7 +271,7 @@ function handleVirtualdomEventSync(contents: string): EventSyncResult {
return JSON.parse(xhr.responseText);
}
function targetId(target: EventTarget): number | null {
function targetId(target: EventTarget): NodeId | null {
// Ensure that the target is a node, sometimes it's nota
if (!(target instanceof Node)) {
return null;

View file

@ -6,7 +6,7 @@
import { Interpreter } from "./interpreter_core";
export class WebInterpreter extends Interpreter {
export class PlatformInterpreter extends Interpreter {
m: any;
constructor(root: HTMLElement, handler: EventListener) {

View file

@ -1,159 +1,217 @@
// Handle serialization of the event data across the IPC boundarytype SerialziedEvent = {};
import { retriveSelectValue, retriveValues } from "./form";
export type AppTouchEvent = TouchEvent;
export type SerializedEvent = {
values?: { [key: string]: FormDataEntryValue[] };
values?: { [key: string]: FormDataEntryValue };
value?: string;
[key: string]: any;
};
export function serializeEvent(event: Event): SerializedEvent {
if (event instanceof InputEvent) {
if (event.target instanceof HTMLInputElement) {
let target = event.target;
let value = target.value ?? target.textContent;
if (target.type === "checkbox") {
value = target.checked ? "true" : "false";
}
return {
value: value,
values: {},
};
}
return {};
}
if (event instanceof KeyboardEvent) {
return {
char_code: event.charCode,
is_composing: event.isComposing,
key: event.key,
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
key_code: event.keyCode,
shift_key: event.shiftKey,
location: event.location,
repeat: event.repeat,
which: event.which,
code: event.code,
};
}
if (event instanceof MouseEvent) {
return {
alt_key: event.altKey,
button: event.button,
buttons: event.buttons,
client_x: event.clientX,
client_y: event.clientY,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
offset_x: event.offsetX,
offset_y: event.offsetY,
page_x: event.pageX,
page_y: event.pageY,
screen_x: event.screenX,
screen_y: event.screenY,
shift_key: event.shiftKey,
};
}
if (event instanceof PointerEvent) {
return {
alt_key: event.altKey,
button: event.button,
buttons: event.buttons,
client_x: event.clientX,
client_y: event.clientY,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
page_x: event.pageX,
page_y: event.pageY,
screen_x: event.screenX,
screen_y: event.screenY,
shift_key: event.shiftKey,
pointer_id: event.pointerId,
width: event.width,
height: event.height,
pressure: event.pressure,
tangential_pressure: event.tangentialPressure,
tilt_x: event.tiltX,
tilt_y: event.tiltY,
twist: event.twist,
pointer_type: event.pointerType,
is_primary: event.isPrimary,
};
}
if (event instanceof TouchEvent) {
return {
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
shift_key: event.shiftKey,
changed_touches: event.changedTouches,
target_touches: event.targetTouches,
touches: event.touches,
};
}
if (event instanceof WheelEvent) {
return {
delta_x: event.deltaX,
delta_y: event.deltaY,
delta_z: event.deltaZ,
delta_mode: event.deltaMode,
};
}
if (event instanceof AnimationEvent) {
return {
animation_name: event.animationName,
elapsed_time: event.elapsedTime,
pseudo_element: event.pseudoElement,
};
}
if (event instanceof TransitionEvent) {
return {
property_name: event.propertyName,
elapsed_time: event.elapsedTime,
pseudo_element: event.pseudoElement,
};
}
if (event instanceof ClipboardEvent) {
return {};
}
if (event instanceof CompositionEvent) {
return {
data: event.data,
};
export function serializeEvent(event: Event, target: EventTarget): SerializedEvent {
let contents = {};
// merge the object into the contents
let extend = (obj: any) => (contents = { ...contents, ...obj });
if (event instanceof WheelEvent) { extend(serializeWheelEvent(event)) };
if (event instanceof MouseEvent) { extend(serializeMouseEvent(event)) }
if (event instanceof KeyboardEvent) { extend(serializeKeyboardEvent(event)) }
if (event instanceof InputEvent) { extend(serializeInputEvent(event, target)) }
if (event instanceof PointerEvent) { extend(serializePointerEvent(event)) }
if (event instanceof AnimationEvent) { extend(serializeAnimationEvent(event)) }
if (event instanceof TransitionEvent) { extend({ property_name: event.propertyName, elapsed_time: event.elapsedTime, pseudo_element: event.pseudoElement, }) }
if (event instanceof CompositionEvent) { extend({ data: event.data, }) }
if (event instanceof DragEvent) { extend(serializeDragEvent(event)) }
if (event instanceof FocusEvent) { extend({}) }
if (event instanceof ClipboardEvent) { extend({}) }
// safari is quirky and doesn't have TouchEvent
if (typeof TouchEvent !== 'undefined' && event instanceof TouchEvent) { extend(serializeTouchEvent(event)); }
if (event.type === "submit" || event.type === "reset" || event.type === "click" || event.type === "change" || event.type === "input") {
extend(serializeInputEvent(event as InputEvent, target));
}
// If there's any files, we need to serialize them
if (event instanceof DragEvent) {
// let files = [];
// if (event.dataTransfer && event.dataTransfer.files) {
// files = ["a", "b", "c"];
// // files = await serializeFileList(event.dataTransfer.files);
// }
// return { mouse: get_mouse_data(event), files };
return {
mouse: {
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
shift_key: event.shiftKey,
},
files: [],
};
// let files: { [key: string]: Uint8Array } = {};
// if (event.dataTransfer && event.dataTransfer.files) {
// files["a"] = new Uint8Array(0);
// // files = {
// // entries: Array.from(event.dataTransfer.files).map((file) => {
// // return {
// // name: file.name,
// // type: file.type,
// // size: file.size,
// // last_modified: file.lastModified,
// // };
// // }
// // };
// // files = await serializeFileList(event.dataTransfer.files);
// }
// extend({ files: files });
}
if (event instanceof FocusEvent) {
return {};
}
return {};
return contents;
}
function serializeInputEvent(event: InputEvent, target: EventTarget): SerializedEvent {
let contents: SerializedEvent = {};
// Attempt to retrieve the values from the form
if (target instanceof HTMLElement) {
let values = retriveValues(event, target);
contents.values = values.values;
contents.valid = values.valid;
}
if (event.target instanceof HTMLInputElement) {
let target = event.target;
let value = target.value ?? target.textContent ?? "";
if (target.type === "checkbox") {
value = target.checked ? "true" : "false";
} else if (target.type === "radio") {
value = target.value;
}
contents.value = value;
}
if (event.target instanceof HTMLTextAreaElement) {
contents.value = event.target.value;
}
if (event.target instanceof HTMLSelectElement) {
contents.value = retriveSelectValue(event.target).join(",");
}
// Ensure the serializer isn't quirky
if (contents.value === undefined) {
contents.value = "";
}
return contents;
}
function serializeWheelEvent(event: WheelEvent): SerializedEvent {
return {
delta_x: event.deltaX,
delta_y: event.deltaY,
delta_z: event.deltaZ,
delta_mode: event.deltaMode,
};
}
function serializeTouchEvent(event: TouchEvent): SerializedEvent {
return {
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
shift_key: event.shiftKey,
changed_touches: event.changedTouches,
target_touches: event.targetTouches,
touches: event.touches,
};
}
function serializePointerEvent(event: PointerEvent): SerializedEvent {
return {
alt_key: event.altKey,
button: event.button,
buttons: event.buttons,
client_x: event.clientX,
client_y: event.clientY,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
page_x: event.pageX,
page_y: event.pageY,
screen_x: event.screenX,
screen_y: event.screenY,
shift_key: event.shiftKey,
pointer_id: event.pointerId,
width: event.width,
height: event.height,
pressure: event.pressure,
tangential_pressure: event.tangentialPressure,
tilt_x: event.tiltX,
tilt_y: event.tiltY,
twist: event.twist,
pointer_type: event.pointerType,
is_primary: event.isPrimary,
};
}
function serializeMouseEvent(event: MouseEvent): SerializedEvent {
return {
alt_key: event.altKey,
button: event.button,
buttons: event.buttons,
client_x: event.clientX,
client_y: event.clientY,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
offset_x: event.offsetX,
offset_y: event.offsetY,
page_x: event.pageX,
page_y: event.pageY,
screen_x: event.screenX,
screen_y: event.screenY,
shift_key: event.shiftKey,
};
}
function serializeKeyboardEvent(event: KeyboardEvent): SerializedEvent {
return {
char_code: event.charCode,
is_composing: event.isComposing,
key: event.key,
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
key_code: event.keyCode,
shift_key: event.shiftKey,
location: event.location,
repeat: event.repeat,
which: event.which,
code: event.code,
};
}
function serializeAnimationEvent(event: AnimationEvent): SerializedEvent {
return {
animation_name: event.animationName,
elapsed_time: event.elapsedTime,
pseudo_element: event.pseudoElement,
};
}
function serializeDragEvent(event: DragEvent): SerializedEvent {
// let files = [];
// if (event.dataTransfer && event.dataTransfer.files) {
// files = ["a", "b", "c"];
// // files = await serializeFileList(event.dataTransfer.files);
// }
// return { mouse: get_mouse_data(event), files };
return {
mouse: {
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
shift_key: event.shiftKey,
...serializeMouseEvent(event),
},
files: {
files: {
"a": [1, 2, 3],
}
},
};
}

View file

@ -17,37 +17,36 @@ export function setAttributeInner(node: HTMLElement, field: string, value: strin
// A few attributes are need to be set with either boolean values or require some sort of translation
switch (field) {
case "value":
if (value !== node.getAttribute("value")) {
node.setAttribute(field, value);
// @ts-ignore
if (node.value !== value) {
// @ts-ignore
node.value = value;
}
break;
case "initial_value":
node.setAttribute("defaultValue", value);
// @ts-ignore
node.defaultValue = value;
break;
case "checked":
if (node instanceof HTMLInputElement) {
node.checked = truthy(value);
}
// @ts-ignore
node.checked = truthy(value);
break;
case "initial_checked":
if (node instanceof HTMLInputElement) {
node.defaultChecked = truthy(value);
}
// @ts-ignore
node.defaultChecked = truthy(value);
break;
case "selected":
if (node instanceof HTMLOptionElement) {
node.selected = truthy(value);
}
// @ts-ignore
node.selected = truthy(value);
break;
case "initial_selected":
if (node instanceof HTMLOptionElement) {
node.defaultSelected = truthy(value);
}
// @ts-ignore
node.defaultSelected = truthy(value);
break;
case "dangerous_inner_html":

View file

@ -6,6 +6,10 @@ use wasm_bindgen::prelude::wasm_bindgen;
use sledgehammer_bindgen::bindgen;
pub fn native_js() -> String {
format!("{}\n{}", include_str!("./js/native.js"), GENERATED_JS,)
}
pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
/// Extensions to the interpreter that are specific to the web platform.
@ -55,7 +59,7 @@ mod js {
"{this.stack.pop();}"
}
fn replace_with(id: u32, n: u16) {
"{const root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...this.els);}"
"{const root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.removeAllNonBubblingListeners(root); } root.replaceWith(...this.els);}"
}
fn insert_after(id: u32, n: u16) {
"{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}"
@ -64,7 +68,7 @@ mod js {
"{this.nodes[$id$].before(...this.stack.splice(this.stack.length-$n$));}"
}
fn remove(id: u32) {
"{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.listeners.removeAllNonBubbling(node); } node.remove(); }}"
"{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.removeAllNonBubblingListeners(node); } node.remove(); }}"
}
fn create_raw_text(text: &str) {
"{this.stack.push(document.createTextNode($text$));}"
@ -76,10 +80,10 @@ mod js {
"{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node); this.nodes[$id$] = node;}"
}
fn new_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
r#"let node = this.nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); this.listeners.create($event_name$, node, $bubbles$);"#
r#"let node = this.nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); this.createListener($event_name$, node, $bubbles$);"#
}
fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
"{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.listeners.remove(node, $event_name$, $bubbles$);}"
"{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.removeListener(node, $event_name$, $bubbles$);}"
}
fn set_text(id: u32, text: &str) {
"{this.nodes[$id$].textContent = $text$;}"
@ -173,13 +177,13 @@ mod js {
fn foreign_event_listener(event: &str<u8, evt>, id: u32, bubbles: u8) {
r#"
bubbles = bubbles == 1;
let node = this.nodes[id];
if(node.listening){
node.listening += 1;
let this_node = this.nodes[id];
if(this_node.listening){
this_node.listening += 1;
} else {
node.listening = 1;
this_node.listening = 1;
}
node.setAttribute('data-dioxus-id', `\${id}`);
this_node.setAttribute('data-dioxus-id', `\${id}`);
const event_name = $event$;
// if this is a mounted listener, we send the event immediately
@ -193,7 +197,7 @@ mod js {
})
);
} else {
this.listeners.create(event_name, node, bubbles, (event) => {
this.createListener(event_name, this_node, bubbles, (event) => {
this.handler(event, event_name, bubbles);
});
}"#

View file

@ -181,7 +181,8 @@ impl WriteMutations for MutationState {
}
fn create_event_listener(&mut self, name: &'static str, id: dioxus_core::ElementId) {
// note that we use the foreign event listener here
// note that we use the foreign event listener here instead of the native one
// the native method assumes we have direct access to the dom, which we don't.
self.channel
.foreign_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
}

View file

@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "system",
"module": "CommonJS",
"lib": [
"ES2015",
"DOM",