interpreter.ts 15 KB

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