interpreter.ts 16 KB

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