interpreter.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830
  1. export function main() {
  2. let root = window.document.getElementById("main");
  3. if (root != null) {
  4. window.interpreter = new Interpreter(root);
  5. window.ipc.postMessage(serializeIpcMessage("initialize"));
  6. }
  7. }
  8. class ListenerMap {
  9. constructor(root) {
  10. // bubbling events can listen at the root element
  11. this.global = {};
  12. // non bubbling events listen at the element the listener was created at
  13. this.local = {};
  14. this.root = root;
  15. }
  16. createBubbling(event_name, handler) {
  17. if (this.global[event_name] === undefined) {
  18. this.global[event_name] = {};
  19. this.global[event_name].active = 1;
  20. this.global[event_name].callback = handler;
  21. this.root.addEventListener(event_name, handler);
  22. } else {
  23. this.global[event_name].active++;
  24. }
  25. }
  26. createNonBubbling(event_name, element, handler) {
  27. const id = element.getAttribute("data-dioxus-id");
  28. if (!this.local[id]) {
  29. this.local[id] = {};
  30. }
  31. this.local[id][event_name] = handler;
  32. element.addEventListener(event_name, handler);
  33. }
  34. removeBubbling(event_name) {
  35. this.global[event_name].active--;
  36. if (this.global[event_name].active === 0) {
  37. this.root.removeEventListener(event_name, this.global[event_name].callback);
  38. delete this.global[event_name];
  39. }
  40. }
  41. removeNonBubbling(element, event_name) {
  42. const id = element.getAttribute("data-dioxus-id");
  43. delete this.local[id][event_name];
  44. if (this.local[id].length === 0) {
  45. delete this.local[id];
  46. }
  47. element.removeEventListener(event_name, handler);
  48. }
  49. removeAllNonBubbling(element) {
  50. const id = element.getAttribute("data-dioxus-id");
  51. delete this.local[id];
  52. }
  53. }
  54. export class Interpreter {
  55. constructor(root) {
  56. this.root = root;
  57. this.stack = [root];
  58. this.listeners = new ListenerMap(root);
  59. this.handlers = {};
  60. this.lastNodeWasText = false;
  61. this.nodes = [root];
  62. }
  63. top() {
  64. return this.stack[this.stack.length - 1];
  65. }
  66. pop() {
  67. return this.stack.pop();
  68. }
  69. SetNode(id, node) {
  70. this.nodes[id] = node;
  71. }
  72. PushRoot(root) {
  73. const node = this.nodes[root];
  74. this.stack.push(node);
  75. }
  76. PopRoot() {
  77. this.stack.pop();
  78. }
  79. AppendChildren(many) {
  80. let root = this.stack[this.stack.length - (1 + many)];
  81. let to_add = this.stack.splice(this.stack.length - many);
  82. for (let i = 0; i < many; i++) {
  83. root.appendChild(to_add[i]);
  84. }
  85. }
  86. ReplaceWith(root_id, m) {
  87. let root = this.nodes[root_id];
  88. let els = this.stack.splice(this.stack.length - m);
  89. this.listeners.removeAllNonBubbling(root);
  90. root.replaceWith(...els);
  91. }
  92. InsertAfter(root, n) {
  93. let old = this.nodes[root];
  94. let new_nodes = this.stack.splice(this.stack.length - n);
  95. old.after(...new_nodes);
  96. }
  97. InsertBefore(root, n) {
  98. let old = this.nodes[root];
  99. let new_nodes = this.stack.splice(this.stack.length - n);
  100. old.before(...new_nodes);
  101. }
  102. Remove(root) {
  103. let node = this.nodes[root];
  104. this.listeners.removeAllNonBubbling(node);
  105. if (node !== undefined) {
  106. node.remove();
  107. }
  108. }
  109. CreateTextNode(text, root) {
  110. const node = document.createTextNode(text);
  111. this.nodes[root] = node;
  112. this.stack.push(node);
  113. }
  114. CreateElement(tag, root) {
  115. const el = document.createElement(tag);
  116. this.nodes[root] = el;
  117. this.stack.push(el);
  118. }
  119. CreateElementNs(tag, root, ns) {
  120. let el = document.createElementNS(ns, tag);
  121. this.stack.push(el);
  122. this.nodes[root] = el;
  123. }
  124. CreatePlaceholder(root) {
  125. let el = document.createElement("pre");
  126. el.hidden = true;
  127. this.stack.push(el);
  128. this.nodes[root] = el;
  129. }
  130. NewEventListener(event_name, root, handler, bubbles) {
  131. const element = this.nodes[root];
  132. element.setAttribute("data-dioxus-id", `${root}`);
  133. if (bubbles) {
  134. this.listeners.createBubbling(event_name, handler);
  135. }
  136. else {
  137. this.listeners.createNonBubbling(event_name, element, handler);
  138. }
  139. }
  140. RemoveEventListener(root, event_name, bubbles) {
  141. const element = this.nodes[root];
  142. element.removeAttribute(`data-dioxus-id`);
  143. if (bubbles) {
  144. this.listeners.removeBubbling(event_name)
  145. }
  146. else {
  147. this.listeners.removeNonBubbling(element, event_name);
  148. }
  149. }
  150. SetText(root, text) {
  151. this.nodes[root].textContent = text;
  152. }
  153. SetAttribute(root, field, value, ns) {
  154. const name = field;
  155. const node = this.nodes[root];
  156. if (ns === "style") {
  157. // @ts-ignore
  158. node.style[name] = value;
  159. } else if (ns != null || ns != undefined) {
  160. node.setAttributeNS(ns, name, value);
  161. } else {
  162. switch (name) {
  163. case "value":
  164. if (value !== node.value) {
  165. node.value = value;
  166. }
  167. break;
  168. case "checked":
  169. node.checked = value === "true";
  170. break;
  171. case "selected":
  172. node.selected = value === "true";
  173. break;
  174. case "dangerous_inner_html":
  175. node.innerHTML = value;
  176. break;
  177. default:
  178. // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
  179. if (value === "false" && bool_attrs.hasOwnProperty(name)) {
  180. node.removeAttribute(name);
  181. } else {
  182. node.setAttribute(name, value);
  183. }
  184. }
  185. }
  186. }
  187. RemoveAttribute(root, field, ns) {
  188. const name = field;
  189. const node = this.nodes[root];
  190. if (ns == "style") {
  191. node.style.removeProperty(name);
  192. } else if (ns !== null || ns !== undefined) {
  193. node.removeAttributeNS(ns, name);
  194. } else if (name === "value") {
  195. node.value = "";
  196. } else if (name === "checked") {
  197. node.checked = false;
  198. } else if (name === "selected") {
  199. node.selected = false;
  200. } else if (name === "dangerous_inner_html") {
  201. node.innerHTML = "";
  202. } else {
  203. node.removeAttribute(name);
  204. }
  205. }
  206. handleEdits(edits) {
  207. this.stack.push(this.root);
  208. for (let edit of edits) {
  209. this.handleEdit(edit);
  210. }
  211. }
  212. handleEdit(edit) {
  213. switch (edit.type) {
  214. case "PushRoot":
  215. this.PushRoot(edit.root);
  216. break;
  217. case "AppendChildren":
  218. this.AppendChildren(edit.many);
  219. break;
  220. case "ReplaceWith":
  221. this.ReplaceWith(edit.root, edit.m);
  222. break;
  223. case "InsertAfter":
  224. this.InsertAfter(edit.root, edit.n);
  225. break;
  226. case "InsertBefore":
  227. this.InsertBefore(edit.root, edit.n);
  228. break;
  229. case "Remove":
  230. this.Remove(edit.root);
  231. break;
  232. case "CreateTextNode":
  233. this.CreateTextNode(edit.text, edit.root);
  234. break;
  235. case "CreateElement":
  236. this.CreateElement(edit.tag, edit.root);
  237. break;
  238. case "CreateElementNs":
  239. this.CreateElementNs(edit.tag, edit.root, edit.ns);
  240. break;
  241. case "CreatePlaceholder":
  242. this.CreatePlaceholder(edit.root);
  243. break;
  244. case "RemoveEventListener":
  245. this.RemoveEventListener(edit.root, edit.event_name);
  246. break;
  247. case "NewEventListener":
  248. // this handler is only provided on desktop implementations since this
  249. // method is not used by the web implementation
  250. let handler = (event) => {
  251. let target = event.target;
  252. if (target != null) {
  253. let realId = target.getAttribute(`data-dioxus-id`);
  254. let shouldPreventDefault = target.getAttribute(
  255. `dioxus-prevent-default`
  256. );
  257. if (event.type === "click") {
  258. // todo call prevent default if it's the right type of event
  259. if (shouldPreventDefault !== `onclick`) {
  260. if (target.tagName === "A") {
  261. event.preventDefault();
  262. const href = target.getAttribute("href");
  263. if (href !== "" && href !== null && href !== undefined) {
  264. window.ipc.postMessage(
  265. serializeIpcMessage("browser_open", { href })
  266. );
  267. }
  268. }
  269. }
  270. // also prevent buttons from submitting
  271. if (target.tagName === "BUTTON" && event.type == "submit") {
  272. event.preventDefault();
  273. }
  274. }
  275. // walk the tree to find the real element
  276. while (realId == null) {
  277. // we've reached the root we don't want to send an event
  278. if (target.parentElement === null) {
  279. return;
  280. }
  281. target = target.parentElement;
  282. realId = target.getAttribute(`data-dioxus-id`);
  283. }
  284. shouldPreventDefault = target.getAttribute(
  285. `dioxus-prevent-default`
  286. );
  287. let contents = serialize_event(event);
  288. if (shouldPreventDefault === `on${event.type}`) {
  289. event.preventDefault();
  290. }
  291. if (event.type === "submit") {
  292. event.preventDefault();
  293. }
  294. if (
  295. target.tagName === "FORM" &&
  296. (event.type === "submit" || event.type === "input")
  297. ) {
  298. for (let x = 0; x < target.elements.length; x++) {
  299. let element = target.elements[x];
  300. let name = element.getAttribute("name");
  301. if (name != null) {
  302. if (element.getAttribute("type") === "checkbox") {
  303. // @ts-ignore
  304. contents.values[name] = element.checked ? "true" : "false";
  305. } else if (element.getAttribute("type") === "radio") {
  306. if (element.checked) {
  307. contents.values[name] = element.value;
  308. }
  309. } else {
  310. // @ts-ignore
  311. contents.values[name] =
  312. element.value ?? element.textContent;
  313. }
  314. }
  315. }
  316. }
  317. if (realId === null) {
  318. return;
  319. }
  320. window.ipc.postMessage(
  321. serializeIpcMessage("user_event", {
  322. event: edit.event_name,
  323. mounted_dom_id: parseInt(realId),
  324. contents: contents,
  325. })
  326. );
  327. }
  328. };
  329. this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name));
  330. break;
  331. case "SetText":
  332. this.SetText(edit.root, edit.text);
  333. break;
  334. case "SetAttribute":
  335. this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
  336. break;
  337. case "RemoveAttribute":
  338. this.RemoveAttribute(edit.root, edit.name, edit.ns);
  339. break;
  340. }
  341. }
  342. }
  343. export function serialize_event(event) {
  344. switch (event.type) {
  345. case "copy":
  346. case "cut":
  347. case "past": {
  348. return {};
  349. }
  350. case "compositionend":
  351. case "compositionstart":
  352. case "compositionupdate": {
  353. let { data } = event;
  354. return {
  355. data,
  356. };
  357. }
  358. case "keydown":
  359. case "keypress":
  360. case "keyup": {
  361. let {
  362. charCode,
  363. key,
  364. altKey,
  365. ctrlKey,
  366. metaKey,
  367. keyCode,
  368. shiftKey,
  369. location,
  370. repeat,
  371. which,
  372. } = event;
  373. return {
  374. char_code: charCode,
  375. key: key,
  376. alt_key: altKey,
  377. ctrl_key: ctrlKey,
  378. meta_key: metaKey,
  379. key_code: keyCode,
  380. shift_key: shiftKey,
  381. location: location,
  382. repeat: repeat,
  383. which: which,
  384. locale: "locale",
  385. };
  386. }
  387. case "focus":
  388. case "blur": {
  389. return {};
  390. }
  391. case "change": {
  392. let target = event.target;
  393. let value;
  394. if (target.type === "checkbox" || target.type === "radio") {
  395. value = target.checked ? "true" : "false";
  396. } else {
  397. value = target.value ?? target.textContent;
  398. }
  399. return {
  400. value: value,
  401. values: {},
  402. };
  403. }
  404. case "input":
  405. case "invalid":
  406. case "reset":
  407. case "submit": {
  408. let target = event.target;
  409. let value = target.value ?? target.textContent;
  410. if (target.type === "checkbox") {
  411. value = target.checked ? "true" : "false";
  412. }
  413. return {
  414. value: value,
  415. values: {},
  416. };
  417. }
  418. case "click":
  419. case "contextmenu":
  420. case "doubleclick":
  421. case "dblclick":
  422. case "drag":
  423. case "dragend":
  424. case "dragenter":
  425. case "dragexit":
  426. case "dragleave":
  427. case "dragover":
  428. case "dragstart":
  429. case "drop":
  430. case "mousedown":
  431. case "mouseenter":
  432. case "mouseleave":
  433. case "mousemove":
  434. case "mouseout":
  435. case "mouseover":
  436. case "mouseup": {
  437. const {
  438. altKey,
  439. button,
  440. buttons,
  441. clientX,
  442. clientY,
  443. ctrlKey,
  444. metaKey,
  445. offsetX,
  446. offsetY,
  447. pageX,
  448. pageY,
  449. screenX,
  450. screenY,
  451. shiftKey,
  452. } = event;
  453. return {
  454. alt_key: altKey,
  455. button: button,
  456. buttons: buttons,
  457. client_x: clientX,
  458. client_y: clientY,
  459. ctrl_key: ctrlKey,
  460. meta_key: metaKey,
  461. offset_x: offsetX,
  462. offset_y: offsetY,
  463. page_x: pageX,
  464. page_y: pageY,
  465. screen_x: screenX,
  466. screen_y: screenY,
  467. shift_key: shiftKey,
  468. };
  469. }
  470. case "pointerdown":
  471. case "pointermove":
  472. case "pointerup":
  473. case "pointercancel":
  474. case "gotpointercapture":
  475. case "lostpointercapture":
  476. case "pointerenter":
  477. case "pointerleave":
  478. case "pointerover":
  479. case "pointerout": {
  480. const {
  481. altKey,
  482. button,
  483. buttons,
  484. clientX,
  485. clientY,
  486. ctrlKey,
  487. metaKey,
  488. pageX,
  489. pageY,
  490. screenX,
  491. screenY,
  492. shiftKey,
  493. pointerId,
  494. width,
  495. height,
  496. pressure,
  497. tangentialPressure,
  498. tiltX,
  499. tiltY,
  500. twist,
  501. pointerType,
  502. isPrimary,
  503. } = event;
  504. return {
  505. alt_key: altKey,
  506. button: button,
  507. buttons: buttons,
  508. client_x: clientX,
  509. client_y: clientY,
  510. ctrl_key: ctrlKey,
  511. meta_key: metaKey,
  512. page_x: pageX,
  513. page_y: pageY,
  514. screen_x: screenX,
  515. screen_y: screenY,
  516. shift_key: shiftKey,
  517. pointer_id: pointerId,
  518. width: width,
  519. height: height,
  520. pressure: pressure,
  521. tangential_pressure: tangentialPressure,
  522. tilt_x: tiltX,
  523. tilt_y: tiltY,
  524. twist: twist,
  525. pointer_type: pointerType,
  526. is_primary: isPrimary,
  527. };
  528. }
  529. case "select": {
  530. return {};
  531. }
  532. case "touchcancel":
  533. case "touchend":
  534. case "touchmove":
  535. case "touchstart": {
  536. const { altKey, ctrlKey, metaKey, shiftKey } = event;
  537. return {
  538. // changed_touches: event.changedTouches,
  539. // target_touches: event.targetTouches,
  540. // touches: event.touches,
  541. alt_key: altKey,
  542. ctrl_key: ctrlKey,
  543. meta_key: metaKey,
  544. shift_key: shiftKey,
  545. };
  546. }
  547. case "scroll": {
  548. return {};
  549. }
  550. case "wheel": {
  551. const { deltaX, deltaY, deltaZ, deltaMode } = event;
  552. return {
  553. delta_x: deltaX,
  554. delta_y: deltaY,
  555. delta_z: deltaZ,
  556. delta_mode: deltaMode,
  557. };
  558. }
  559. case "animationstart":
  560. case "animationend":
  561. case "animationiteration": {
  562. const { animationName, elapsedTime, pseudoElement } = event;
  563. return {
  564. animation_name: animationName,
  565. elapsed_time: elapsedTime,
  566. pseudo_element: pseudoElement,
  567. };
  568. }
  569. case "transitionend": {
  570. const { propertyName, elapsedTime, pseudoElement } = event;
  571. return {
  572. property_name: propertyName,
  573. elapsed_time: elapsedTime,
  574. pseudo_element: pseudoElement,
  575. };
  576. }
  577. case "abort":
  578. case "canplay":
  579. case "canplaythrough":
  580. case "durationchange":
  581. case "emptied":
  582. case "encrypted":
  583. case "ended":
  584. case "error":
  585. case "loadeddata":
  586. case "loadedmetadata":
  587. case "loadstart":
  588. case "pause":
  589. case "play":
  590. case "playing":
  591. case "progress":
  592. case "ratechange":
  593. case "seeked":
  594. case "seeking":
  595. case "stalled":
  596. case "suspend":
  597. case "timeupdate":
  598. case "volumechange":
  599. case "waiting": {
  600. return {};
  601. }
  602. case "toggle": {
  603. return {};
  604. }
  605. default: {
  606. return {};
  607. }
  608. }
  609. }
  610. function serializeIpcMessage(method, params = {}) {
  611. return JSON.stringify({ method, params });
  612. }
  613. const bool_attrs = {
  614. allowfullscreen: true,
  615. allowpaymentrequest: true,
  616. async: true,
  617. autofocus: true,
  618. autoplay: true,
  619. checked: true,
  620. controls: true,
  621. default: true,
  622. defer: true,
  623. disabled: true,
  624. formnovalidate: true,
  625. hidden: true,
  626. ismap: true,
  627. itemscope: true,
  628. loop: true,
  629. multiple: true,
  630. muted: true,
  631. nomodule: true,
  632. novalidate: true,
  633. open: true,
  634. playsinline: true,
  635. readonly: true,
  636. required: true,
  637. reversed: true,
  638. selected: true,
  639. truespeed: true,
  640. };
  641. function event_bubbles(event) {
  642. switch (event) {
  643. case "copy":
  644. return true;
  645. case "cut":
  646. return true;
  647. case "paste":
  648. return true;
  649. case "compositionend":
  650. return true;
  651. case "compositionstart":
  652. return true;
  653. case "compositionupdate":
  654. return true;
  655. case "keydown":
  656. return true;
  657. case "keypress":
  658. return true;
  659. case "keyup":
  660. return true;
  661. case "focus":
  662. return false;
  663. case "focusout":
  664. return true;
  665. case "focusin":
  666. return true;
  667. case "blur":
  668. return false;
  669. case "change":
  670. return true;
  671. case "input":
  672. return true;
  673. case "invalid":
  674. return true;
  675. case "reset":
  676. return true;
  677. case "submit":
  678. return true;
  679. case "click":
  680. return true;
  681. case "contextmenu":
  682. return true;
  683. case "doubleclick":
  684. return true;
  685. case "dblclick":
  686. return true;
  687. case "drag":
  688. return true;
  689. case "dragend":
  690. return true;
  691. case "dragenter":
  692. return false;
  693. case "dragexit":
  694. return false;
  695. case "dragleave":
  696. return true;
  697. case "dragover":
  698. return true;
  699. case "dragstart":
  700. return true;
  701. case "drop":
  702. return true;
  703. case "mousedown":
  704. return true;
  705. case "mouseenter":
  706. return false;
  707. case "mouseleave":
  708. return false;
  709. case "mousemove":
  710. return true;
  711. case "mouseout":
  712. return true;
  713. case "scroll":
  714. return false;
  715. case "mouseover":
  716. return true;
  717. case "mouseup":
  718. return true;
  719. case "pointerdown":
  720. return true;
  721. case "pointermove":
  722. return true;
  723. case "pointerup":
  724. return true;
  725. case "pointercancel":
  726. return true;
  727. case "gotpointercapture":
  728. return true;
  729. case "lostpointercapture":
  730. return true;
  731. case "pointerenter":
  732. return false;
  733. case "pointerleave":
  734. return false;
  735. case "pointerover":
  736. return true;
  737. case "pointerout":
  738. return true;
  739. case "select":
  740. return true;
  741. case "touchcancel":
  742. return true;
  743. case "touchend":
  744. return true;
  745. case "touchmove":
  746. return true;
  747. case "touchstart":
  748. return true;
  749. case "wheel":
  750. return true;
  751. case "abort":
  752. return false;
  753. case "canplay":
  754. return true;
  755. case "canplaythrough":
  756. return true;
  757. case "durationchange":
  758. return true;
  759. case "emptied":
  760. return true;
  761. case "encrypted":
  762. return true;
  763. case "ended":
  764. return true;
  765. case "error":
  766. return false;
  767. case "loadeddata":
  768. return true;
  769. case "loadedmetadata":
  770. return true;
  771. case "loadstart":
  772. return false;
  773. case "pause":
  774. return true;
  775. case "play":
  776. return true;
  777. case "playing":
  778. return true;
  779. case "progress":
  780. return false;
  781. case "ratechange":
  782. return true;
  783. case "seeked":
  784. return true;
  785. case "seeking":
  786. return true;
  787. case "stalled":
  788. return true;
  789. case "suspend":
  790. return true;
  791. case "timeupdate":
  792. return true;
  793. case "volumechange":
  794. return true;
  795. case "waiting":
  796. return true;
  797. case "animationstart":
  798. return true;
  799. case "animationend":
  800. return true;
  801. case "animationiteration":
  802. return true;
  803. case "transitionend":
  804. return true;
  805. case "toggle":
  806. return true;
  807. }
  808. }