interpreter.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. function serialize_event(event: Event) {
  2. switch (event.type) {
  3. case "copy":
  4. case "cut":
  5. case "past":
  6. return {};
  7. case "compositionend":
  8. case "compositionstart":
  9. case "compositionupdate":
  10. let { data } = (event as CompositionEvent);
  11. return {
  12. data,
  13. };
  14. case "keydown":
  15. case "keypress":
  16. case "keyup":
  17. let {
  18. charCode,
  19. key,
  20. altKey,
  21. ctrlKey,
  22. metaKey,
  23. keyCode,
  24. shiftKey,
  25. location,
  26. repeat,
  27. which,
  28. } = (event as KeyboardEvent);
  29. return {
  30. char_code: charCode,
  31. key: key,
  32. alt_key: altKey,
  33. ctrl_key: ctrlKey,
  34. meta_key: metaKey,
  35. key_code: keyCode,
  36. shift_key: shiftKey,
  37. location: location,
  38. repeat: repeat,
  39. which: which,
  40. locale: "locale",
  41. };
  42. case "focus":
  43. case "blur":
  44. return {};
  45. case "change":
  46. let target = event.target as HTMLInputElement;
  47. let value;
  48. if (target.type === "checkbox" || target.type === "radio") {
  49. value = target.checked ? "true" : "false";
  50. } else {
  51. value = target.value ?? target.textContent;
  52. }
  53. return {
  54. value: value,
  55. };
  56. case "input":
  57. case "invalid":
  58. case "reset":
  59. case "submit": {
  60. let target = event.target as HTMLFormElement;
  61. let value = target.value ?? target.textContent;
  62. if (target.type == "checkbox") {
  63. value = target.checked ? "true" : "false";
  64. }
  65. return {
  66. value: value,
  67. };
  68. }
  69. case "click":
  70. case "contextmenu":
  71. case "doubleclick":
  72. case "drag":
  73. case "dragend":
  74. case "dragenter":
  75. case "dragexit":
  76. case "dragleave":
  77. case "dragover":
  78. case "dragstart":
  79. case "drop":
  80. case "mousedown":
  81. case "mouseenter":
  82. case "mouseleave":
  83. case "mousemove":
  84. case "mouseout":
  85. case "mouseover":
  86. case "mouseup": {
  87. const {
  88. altKey,
  89. button,
  90. buttons,
  91. clientX,
  92. clientY,
  93. ctrlKey,
  94. metaKey,
  95. pageX,
  96. pageY,
  97. screenX,
  98. screenY,
  99. shiftKey,
  100. } = event as MouseEvent;
  101. return {
  102. alt_key: altKey,
  103. button: button,
  104. buttons: buttons,
  105. client_x: clientX,
  106. client_y: clientY,
  107. ctrl_key: ctrlKey,
  108. meta_key: metaKey,
  109. page_x: pageX,
  110. page_y: pageY,
  111. screen_x: screenX,
  112. screen_y: screenY,
  113. shift_key: shiftKey,
  114. };
  115. }
  116. case "pointerdown":
  117. case "pointermove":
  118. case "pointerup":
  119. case "pointercancel":
  120. case "gotpointercapture":
  121. case "lostpointercapture":
  122. case "pointerenter":
  123. case "pointerleave":
  124. case "pointerover":
  125. case "pointerout": {
  126. const {
  127. altKey,
  128. button,
  129. buttons,
  130. clientX,
  131. clientY,
  132. ctrlKey,
  133. metaKey,
  134. pageX,
  135. pageY,
  136. screenX,
  137. screenY,
  138. shiftKey,
  139. pointerId,
  140. width,
  141. height,
  142. pressure,
  143. tangentialPressure,
  144. tiltX,
  145. tiltY,
  146. twist,
  147. pointerType,
  148. isPrimary,
  149. } = event as PointerEvent;
  150. return {
  151. alt_key: altKey,
  152. button: button,
  153. buttons: buttons,
  154. client_x: clientX,
  155. client_y: clientY,
  156. ctrl_key: ctrlKey,
  157. meta_key: metaKey,
  158. page_x: pageX,
  159. page_y: pageY,
  160. screen_x: screenX,
  161. screen_y: screenY,
  162. shift_key: shiftKey,
  163. pointer_id: pointerId,
  164. width: width,
  165. height: height,
  166. pressure: pressure,
  167. tangential_pressure: tangentialPressure,
  168. tilt_x: tiltX,
  169. tilt_y: tiltY,
  170. twist: twist,
  171. pointer_type: pointerType,
  172. is_primary: isPrimary,
  173. };
  174. }
  175. case "select":
  176. return {};
  177. case "touchcancel":
  178. case "touchend":
  179. case "touchmove":
  180. case "touchstart": {
  181. const {
  182. altKey,
  183. ctrlKey,
  184. metaKey,
  185. shiftKey,
  186. } = event as TouchEvent;
  187. return {
  188. // changed_touches: event.changedTouches,
  189. // target_touches: event.targetTouches,
  190. // touches: event.touches,
  191. alt_key: altKey,
  192. ctrl_key: ctrlKey,
  193. meta_key: metaKey,
  194. shift_key: shiftKey,
  195. };
  196. }
  197. case "scroll":
  198. return {};
  199. case "wheel": {
  200. const {
  201. deltaX,
  202. deltaY,
  203. deltaZ,
  204. deltaMode,
  205. } = event as WheelEvent;
  206. return {
  207. delta_x: deltaX,
  208. delta_y: deltaY,
  209. delta_z: deltaZ,
  210. delta_mode: deltaMode,
  211. };
  212. }
  213. case "animationstart":
  214. case "animationend":
  215. case "animationiteration": {
  216. const {
  217. animationName,
  218. elapsedTime,
  219. pseudoElement,
  220. } = event as AnimationEvent;
  221. return {
  222. animation_name: animationName,
  223. elapsed_time: elapsedTime,
  224. pseudo_element: pseudoElement,
  225. };
  226. }
  227. case "transitionend": {
  228. const {
  229. propertyName,
  230. elapsedTime,
  231. pseudoElement,
  232. } = event as TransitionEvent;
  233. return {
  234. property_name: propertyName,
  235. elapsed_time: elapsedTime,
  236. pseudo_element: pseudoElement,
  237. };
  238. }
  239. case "abort":
  240. case "canplay":
  241. case "canplaythrough":
  242. case "durationchange":
  243. case "emptied":
  244. case "encrypted":
  245. case "ended":
  246. case "error":
  247. case "loadeddata":
  248. case "loadedmetadata":
  249. case "loadstart":
  250. case "pause":
  251. case "play":
  252. case "playing":
  253. case "progress":
  254. case "ratechange":
  255. case "seeked":
  256. case "seeking":
  257. case "stalled":
  258. case "suspend":
  259. case "timeupdate":
  260. case "volumechange":
  261. case "waiting":
  262. return {};
  263. case "toggle":
  264. return {};
  265. default:
  266. return {};
  267. }
  268. }
  269. const bool_attrs = {
  270. allowfullscreen: true,
  271. allowpaymentrequest: true,
  272. async: true,
  273. autofocus: true,
  274. autoplay: true,
  275. checked: true,
  276. controls: true,
  277. default: true,
  278. defer: true,
  279. disabled: true,
  280. formnovalidate: true,
  281. hidden: true,
  282. ismap: true,
  283. itemscope: true,
  284. loop: true,
  285. multiple: true,
  286. muted: true,
  287. nomodule: true,
  288. novalidate: true,
  289. open: true,
  290. playsinline: true,
  291. readonly: true,
  292. required: true,
  293. reversed: true,
  294. selected: true,
  295. truespeed: true,
  296. };
  297. export class Interpreter {
  298. root: Element;
  299. stack: Element[];
  300. listeners: { [key: string]: (event: Event) => void };
  301. lastNodeWasText: boolean;
  302. nodes: Element[];
  303. constructor(root: Element) {
  304. this.root = root;
  305. this.stack = [root];
  306. this.listeners = {
  307. };
  308. this.lastNodeWasText = false;
  309. this.nodes = [root];
  310. }
  311. top() {
  312. return this.stack[this.stack.length - 1];
  313. }
  314. pop() {
  315. return this.stack.pop();
  316. }
  317. PushRoot(root: number) {
  318. const node = this.nodes[root];
  319. this.stack.push(node);
  320. }
  321. AppendChildren(many: number) {
  322. let root = this.stack[this.stack.length - (1 + many)];
  323. let to_add = this.stack.splice(this.stack.length - many);
  324. for (let i = 0; i < many; i++) {
  325. root.appendChild(to_add[i]);
  326. }
  327. }
  328. ReplaceWith(root_id: number, m: number) {
  329. let root = this.nodes[root_id] as Element;
  330. let els = this.stack.splice(this.stack.length - m);
  331. root.replaceWith(...els);
  332. }
  333. InsertAfter(root: number, n: number) {
  334. let old = this.nodes[root] as Element;
  335. let new_nodes = this.stack.splice(this.stack.length - n);
  336. old.after(...new_nodes);
  337. }
  338. InsertBefore(root: number, n: number) {
  339. let old = this.nodes[root] as Element;
  340. let new_nodes = this.stack.splice(this.stack.length - n);
  341. old.before(...new_nodes);
  342. }
  343. Remove(root: number) {
  344. let node = this.nodes[root] as Element;
  345. if (node !== undefined) {
  346. node.remove();
  347. }
  348. }
  349. CreateTextNode(text: string, root: number) {
  350. // todo: make it so the types are okay
  351. const node = document.createTextNode(text) as any as Element;
  352. this.nodes[root] = node;
  353. this.stack.push(node);
  354. }
  355. CreateElement(tag: string, root: number) {
  356. const el = document.createElement(tag);
  357. el.setAttribute("dioxus-id", `${root}`);
  358. this.nodes[root] = el;
  359. this.stack.push(el);
  360. }
  361. CreateElementNs(tag: string, root: number, ns: string) {
  362. let el = document.createElementNS(ns, tag);
  363. this.stack.push(el);
  364. this.nodes[root] = el;
  365. }
  366. CreatePlaceholder(root: number) {
  367. let el = document.createElement("pre");
  368. el.hidden = true;
  369. this.stack.push(el);
  370. this.nodes[root] = el;
  371. }
  372. NewEventListener(event_name: string, scope: number, root: number) {
  373. console.log('new event listener', event_name, root, scope);
  374. const element = this.nodes[root];
  375. element.setAttribute(
  376. `dioxus-event-${event_name}`,
  377. `${scope}.${root}`
  378. );
  379. // if (!this.listeners[event_name]) {
  380. // this.listeners[event_name] = handler;
  381. // this.root.addEventListener(event_name, handler);
  382. // }
  383. }
  384. RemoveEventListener(root: number, event_name: string, scope: number) {
  385. //
  386. }
  387. SetText(root: number, text: string) {
  388. this.nodes[root].textContent = text;
  389. }
  390. SetAttribute(root: number, field: string, value: string, ns: string | undefined) {
  391. const name = field;
  392. const node = this.nodes[root];
  393. if (ns == "style") {
  394. // @ts-ignore
  395. (node as HTMLElement).style[name] = value;
  396. } else if (ns != null || ns != undefined) {
  397. node.setAttributeNS(ns, name, value);
  398. } else {
  399. switch (name) {
  400. case "value":
  401. if (value != (node as HTMLInputElement).value) {
  402. (node as HTMLInputElement).value = value;
  403. }
  404. break;
  405. case "checked":
  406. (node as HTMLInputElement).checked = value === "true";
  407. break;
  408. case "selected":
  409. (node as HTMLOptionElement).selected = value === "true";
  410. break;
  411. case "dangerous_inner_html":
  412. node.innerHTML = value;
  413. break;
  414. default:
  415. // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
  416. if (value == "false" && bool_attrs.hasOwnProperty(name)) {
  417. node.removeAttribute(name);
  418. } else {
  419. node.setAttribute(name, value);
  420. }
  421. }
  422. }
  423. }
  424. RemoveAttribute(root: number, name: string) {
  425. const node = this.nodes[root];
  426. node.removeAttribute(name);
  427. if (name === "value") {
  428. (node as HTMLInputElement).value = "";
  429. }
  430. if (name === "checked") {
  431. (node as HTMLInputElement).checked = false;
  432. }
  433. if (name === "selected") {
  434. (node as HTMLOptionElement).selected = false;
  435. }
  436. }
  437. handleEdits(edits: DomEdit[]) {
  438. console.log("handling edits ", edits);
  439. this.stack.push(this.root);
  440. for (let edit of edits) {
  441. this.handleEdit(edit);
  442. }
  443. }
  444. handleEdit(edit: DomEdit) {
  445. switch (edit.type) {
  446. case "PushRoot":
  447. this.PushRoot(edit.root);
  448. break;
  449. case "AppendChildren":
  450. this.AppendChildren(edit.many);
  451. break;
  452. case "ReplaceWith":
  453. this.ReplaceWith(edit.root, edit.m);
  454. break;
  455. case "InsertAfter":
  456. this.InsertAfter(edit.root, edit.n);
  457. break;
  458. case "InsertBefore":
  459. this.InsertBefore(edit.root, edit.n);
  460. break;
  461. case "Remove":
  462. this.Remove(edit.root);
  463. break;
  464. case "CreateTextNode":
  465. this.CreateTextNode(edit.text, edit.root);
  466. break;
  467. case "CreateElement":
  468. this.CreateElement(edit.tag, edit.root);
  469. break;
  470. case "CreateElementNs":
  471. this.CreateElementNs(edit.tag, edit.root, edit.ns);
  472. break;
  473. case "CreatePlaceholder":
  474. this.CreatePlaceholder(edit.root);
  475. break;
  476. case "RemoveEventListener":
  477. this.RemoveEventListener(edit.root, edit.event_name, edit.scope);
  478. break;
  479. case "NewEventListener":
  480. // todo: only on desktop should we make our own handler
  481. let handler = (event: Event) => {
  482. const target = event.target as Element | null;
  483. console.log("event", event);
  484. if (target != null) {
  485. const real_id = target.getAttribute(`dioxus-id`);
  486. const should_prevent_default = target.getAttribute(
  487. `dioxus-prevent-default`
  488. );
  489. let contents = serialize_event(event);
  490. if (should_prevent_default === `on${event.type}`) {
  491. event.preventDefault();
  492. }
  493. if (real_id == null) {
  494. return;
  495. }
  496. window.rpc.call("user_event", {
  497. event: (edit as NewEventListener).event_name,
  498. mounted_dom_id: parseInt(real_id),
  499. contents: contents,
  500. });
  501. }
  502. };
  503. this.NewEventListener(edit.event_name, edit.scope, edit.root);
  504. // this.NewEventListener(edit, handler);
  505. break;
  506. case "SetText":
  507. this.SetText(edit.root, edit.text);
  508. break;
  509. case "SetAttribute":
  510. this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
  511. break;
  512. case "RemoveAttribute":
  513. this.RemoveAttribute(edit.root, edit.name);
  514. break;
  515. }
  516. }
  517. }
  518. function main() {
  519. let root = window.document.getElementById("main");
  520. if (root != null) {
  521. window.interpreter = new Interpreter(root);
  522. window.rpc.call("initialize");
  523. }
  524. }
  525. type PushRoot = { type: "PushRoot", root: number };
  526. type AppendChildren = { type: "AppendChildren", many: number };
  527. type ReplaceWith = { type: "ReplaceWith", root: number, m: number };
  528. type InsertAfter = { type: "InsertAfter", root: number, n: number };
  529. type InsertBefore = { type: "InsertBefore", root: number, n: number };
  530. type Remove = { type: "Remove", root: number };
  531. type CreateTextNode = { type: "CreateTextNode", text: string, root: number };
  532. type CreateElement = { type: "CreateElement", tag: string, root: number };
  533. type CreateElementNs = { type: "CreateElementNs", tag: string, root: number, ns: string };
  534. type CreatePlaceholder = { type: "CreatePlaceholder", root: number };
  535. type NewEventListener = { type: "NewEventListener", root: number, event_name: string, scope: number };
  536. type RemoveEventListener = { type: "RemoveEventListener", event_name: string, scope: number, root: number };
  537. type SetText = { type: "SetText", root: number, text: string };
  538. type SetAttribute = { type: "SetAttribute", root: number, field: string, value: string, ns: string | undefined };
  539. type RemoveAttribute = { type: "RemoveAttribute", root: number, name: string };
  540. type DomEdit =
  541. PushRoot |
  542. AppendChildren |
  543. ReplaceWith |
  544. InsertAfter |
  545. InsertBefore |
  546. Remove |
  547. CreateTextNode |
  548. CreateElement |
  549. CreateElementNs |
  550. CreatePlaceholder |
  551. NewEventListener |
  552. RemoveEventListener |
  553. SetText |
  554. SetAttribute |
  555. RemoveAttribute;
  556. export { };
  557. declare global {
  558. interface Window {
  559. interpreter: Interpreter;
  560. rpc: { call: (method: string, args?: any) => void };
  561. }
  562. }
  563. type Edits = DomEdit[];