1
0

interpreter.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  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. 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(edit: PushRoot) {
  318. const id = edit.root;
  319. const node = this.nodes[id];
  320. this.stack.push(node);
  321. }
  322. AppendChildren(edit: AppendChildren) {
  323. let root = this.stack[this.stack.length - (1 + edit.many)];
  324. let to_add = this.stack.splice(this.stack.length - edit.many);
  325. for (let i = 0; i < edit.many; i++) {
  326. root.appendChild(to_add[i]);
  327. }
  328. }
  329. ReplaceWith(edit: ReplaceWith) {
  330. let root = this.nodes[edit.root] as Element;
  331. let els = this.stack.splice(this.stack.length - edit.m);
  332. root.replaceWith(...els);
  333. }
  334. InsertAfter(edit: InsertAfter) {
  335. let old = this.nodes[edit.root] as Element;
  336. let new_nodes = this.stack.splice(this.stack.length - edit.n);
  337. old.after(...new_nodes);
  338. }
  339. InsertBefore(edit: InsertBefore) {
  340. let old = this.nodes[edit.root] as Element;
  341. let new_nodes = this.stack.splice(this.stack.length - edit.n);
  342. old.before(...new_nodes);
  343. }
  344. Remove(edit: Remove) {
  345. let node = this.nodes[edit.root] as Element;
  346. if (node !== undefined) {
  347. node.remove();
  348. }
  349. }
  350. CreateTextNode(edit: CreateTextNode) {
  351. // todo: make it so the types are okay
  352. const node = document.createTextNode(edit.text) as any as Element;
  353. this.nodes[edit.root] = node;
  354. this.stack.push(node);
  355. }
  356. CreateElement(edit: CreateElement) {
  357. const el = document.createElement(edit.tag);
  358. el.setAttribute("dioxus-id", `${edit.root}`);
  359. this.nodes[edit.root] = el;
  360. this.stack.push(el);
  361. }
  362. CreateElementNs(edit: CreateElementNs) {
  363. let el = document.createElementNS(edit.ns, edit.tag);
  364. this.stack.push(el);
  365. this.nodes[edit.root] = el;
  366. }
  367. CreatePlaceholder(edit: CreatePlaceholder) {
  368. let el = document.createElement("pre");
  369. el.hidden = true;
  370. this.stack.push(el);
  371. this.nodes[edit.root] = el;
  372. }
  373. RemoveEventListener(edit: RemoveEventListener) { }
  374. NewEventListener(edit: NewEventListener, handler: (event: Event) => void) {
  375. const event_name = edit.event_name;
  376. const mounted_node_id = edit.root;
  377. const scope = edit.scope;
  378. console.log('new event listener', event_name, mounted_node_id, scope);
  379. const element = this.nodes[edit.root];
  380. element.setAttribute(
  381. `dioxus-event-${event_name}`,
  382. `${scope}.${mounted_node_id}`
  383. );
  384. if (!this.listeners[event_name]) {
  385. this.listeners[event_name] = handler;
  386. this.root.addEventListener(event_name, handler);
  387. }
  388. }
  389. SetText(edit: SetText) {
  390. this.nodes[edit.root].textContent = edit.text;
  391. }
  392. SetAttribute(edit: SetAttribute) {
  393. // console.log("setting attr", edit);
  394. const name = edit.field;
  395. const value = edit.value;
  396. const ns = edit.ns;
  397. const node = this.nodes[edit.root];
  398. if (ns == "style") {
  399. // @ts-ignore
  400. (node as HTMLElement).style[name] = value;
  401. } else if (ns != null || ns != undefined) {
  402. node.setAttributeNS(ns, name, value);
  403. } else {
  404. switch (name) {
  405. case "value":
  406. if (value != (node as HTMLInputElement).value) {
  407. (node as HTMLInputElement).value = value;
  408. }
  409. break;
  410. case "checked":
  411. (node as HTMLInputElement).checked = value === "true";
  412. break;
  413. case "selected":
  414. (node as HTMLOptionElement).selected = value === "true";
  415. break;
  416. case "dangerous_inner_html":
  417. node.innerHTML = value;
  418. break;
  419. default:
  420. // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
  421. if (value == "false" && bool_attrs.hasOwnProperty(name)) {
  422. node.removeAttribute(name);
  423. } else {
  424. node.setAttribute(name, value);
  425. }
  426. }
  427. }
  428. }
  429. RemoveAttribute(edit: RemoveAttribute) {
  430. const name = edit.name;
  431. const node = this.nodes[edit.root];
  432. node.removeAttribute(name);
  433. if (name === "value") {
  434. (node as HTMLInputElement).value = "";
  435. }
  436. if (name === "checked") {
  437. (node as HTMLInputElement).checked = false;
  438. }
  439. if (name === "selected") {
  440. (node as HTMLOptionElement).selected = false;
  441. }
  442. }
  443. handleEdits(edits: DomEdit[]) {
  444. console.log("handling edits ", edits);
  445. this.stack.push(this.root);
  446. for (let edit of edits) {
  447. switch (edit.type) {
  448. case "AppendChildren":
  449. this.AppendChildren(edit);
  450. break;
  451. case "ReplaceWith":
  452. this.ReplaceWith(edit);
  453. break;
  454. case "InsertAfter":
  455. this.InsertAfter(edit);
  456. break;
  457. case "InsertBefore":
  458. this.InsertBefore(edit);
  459. break;
  460. case "Remove":
  461. this.Remove(edit);
  462. break;
  463. case "CreateTextNode":
  464. this.CreateTextNode(edit);
  465. break;
  466. case "CreateElement":
  467. this.CreateElement(edit);
  468. break;
  469. case "CreateElementNs":
  470. this.CreateElementNs(edit);
  471. break;
  472. case "CreatePlaceholder":
  473. this.CreatePlaceholder(edit);
  474. break;
  475. case "RemoveEventListener":
  476. this.RemoveEventListener(edit);
  477. break;
  478. case "NewEventListener":
  479. // todo: only on desktop should we make our own handler
  480. let handler = (event: Event) => {
  481. const target = event.target as Element | null;
  482. console.log("event", event);
  483. if (target != null) {
  484. const real_id = target.getAttribute(`dioxus-id`);
  485. const should_prevent_default = target.getAttribute(
  486. `dioxus-prevent-default`
  487. );
  488. let contents = serialize_event(event);
  489. if (should_prevent_default === `on${event.type}`) {
  490. event.preventDefault();
  491. }
  492. if (real_id == null) {
  493. return;
  494. }
  495. window.rpc.call("user_event", {
  496. event: (edit as NewEventListener).event_name,
  497. mounted_dom_id: parseInt(real_id),
  498. contents: contents,
  499. });
  500. }
  501. };
  502. this.NewEventListener(edit, handler);
  503. break;
  504. case "SetText":
  505. this.SetText(edit);
  506. break;
  507. case "SetAttribute":
  508. this.SetAttribute(edit);
  509. break;
  510. case "RemoveAttribute":
  511. this.RemoveAttribute(edit);
  512. break;
  513. }
  514. }
  515. }
  516. }
  517. function main() {
  518. let root = window.document.getElementById("main");
  519. if (root != null) {
  520. window.interpreter = new Interpreter(root);
  521. window.rpc.call("initialize");
  522. }
  523. }
  524. type PushRoot = { type: "PushRoot", root: number };
  525. type AppendChildren = { type: "AppendChildren", many: number };
  526. type ReplaceWith = { type: "ReplaceWith", root: number, m: number };
  527. type InsertAfter = { type: "InsertAfter", root: number, n: number };
  528. type InsertBefore = { type: "InsertBefore", root: number, n: number };
  529. type Remove = { type: "Remove", root: number };
  530. type CreateTextNode = { type: "CreateTextNode", text: string, root: number };
  531. type CreateElement = { type: "CreateElement", tag: string, root: number };
  532. type CreateElementNs = { type: "CreateElementNs", tag: string, root: number, ns: string };
  533. type CreatePlaceholder = { type: "CreatePlaceholder", root: number };
  534. type NewEventListener = { type: "NewEventListener", root: number, event_name: string, scope: number };
  535. type RemoveEventListener = { type: "RemoveEventListener", event_name: string, scope: number, root: number };
  536. type SetText = { type: "SetText", root: number, text: string };
  537. type SetAttribute = { type: "SetAttribute", root: number, field: string, value: string, ns: string | undefined };
  538. type RemoveAttribute = { type: "RemoveAttribute", root: number, name: string };
  539. type DomEdit =
  540. PushRoot |
  541. AppendChildren |
  542. ReplaceWith |
  543. InsertAfter |
  544. InsertBefore |
  545. Remove |
  546. CreateTextNode |
  547. CreateElement |
  548. CreateElementNs |
  549. CreatePlaceholder |
  550. NewEventListener |
  551. RemoveEventListener |
  552. SetText |
  553. SetAttribute |
  554. RemoveAttribute;
  555. export { };
  556. declare global {
  557. interface Window {
  558. interpreter: Interpreter;
  559. rpc: { call: (method: string, args?: any) => void };
  560. }
  561. }
  562. type Edits = DomEdit[];
  563. main();