emccpre.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  1. /*
  2. * emccpre.js: one of the Javascript components of an Emscripten-based
  3. * web/Javascript front end for Puzzles.
  4. *
  5. * The other parts of this system live in emcc.c and emcclib.js. It
  6. * also depends on being run in the context of a web page containing
  7. * an appropriate collection of bits and pieces (a canvas, some
  8. * buttons and links etc), which is generated for each puzzle by the
  9. * script html/jspage.pl.
  10. *
  11. * This file contains the Javascript code which is prefixed unmodified
  12. * to Emscripten's output via the --pre-js option. It declares all our
  13. * global variables, and provides the puzzle init function and a
  14. * couple of other helper functions.
  15. */
  16. // Because this script is run using <script defer>, we can guarantee
  17. // that the DOM is complete, so it's OK to look up elements
  18. // immediately. On the other hand, the Emscripten runtime hasn't
  19. // started yet, so Module.cwrap isn't safe.
  20. // Error handler to make any failures from here on visible to the
  21. // user, maybe.
  22. window.addEventListener("error", function (e) {
  23. alert(e.message);
  24. });
  25. // To avoid flicker while doing complicated drawing, we use two
  26. // canvases, the same size. One is actually on the web page, and the
  27. // other is off-screen. We do all our drawing on the off-screen one
  28. // first, and then copy rectangles of it to the on-screen canvas in
  29. // response to draw_update() calls by the game backend.
  30. var onscreen_canvas, offscreen_canvas;
  31. // A persistent drawing context for the offscreen canvas, to save
  32. // requesting it for each individual graphics operation.
  33. var ctx;
  34. // Bounding rectangle for the copy to the onscreen canvas that will be
  35. // done at drawing end time. Updated by js_canvas_draw_update and used
  36. // by js_canvas_end_draw.
  37. var update_xmin, update_xmax, update_ymin, update_ymax;
  38. // Module object for Emscripten. We fill in these parameters to ensure
  39. // that when main() returns nothing will get cleaned up so we remain
  40. // able to call the puzzle's various callbacks.
  41. //
  42. // Page loading order:
  43. //
  44. // 1. The browser starts reading *.html (which comes from jspage.pl)
  45. // 2. It finds the <script> tag. This is marked defer, so the
  46. // browser will start fetching and parsing it, but not execute it
  47. // until the page has loaded.
  48. //
  49. // Now the browser is loading *.html and *.js in parallel. The
  50. // html is rendered as we go, and the js is deferred.
  51. //
  52. // 3. The HTML finishes loading. The browser is about to fire the
  53. // `DOMContentLoaded` event (ie `onload`) but before that, it
  54. // actually runs the deferred JS. This consists of
  55. //
  56. // (i) emccpre.js (this file). This sets up various JS variables
  57. // including the emscripten Module object, which includes the
  58. // environment variables and argv seen by main().
  59. //
  60. // (ii) emscripten's JS. This starts the WASM loading.
  61. //
  62. // When this JS execution is complete, the browser fires the `onload`
  63. // event. This is ignored. It continues loading the WASM.
  64. //
  65. // 4. The WASM loading and initialisation completes. Emscripten's
  66. // runtime calls the C `main` to actually start the puzzle. It
  67. // then calls initPuzzle, which:
  68. //
  69. // (a) finds various DOM elements and bind them to variables,
  70. // which depends on the HTML having loaded (it has).
  71. //
  72. // (b) makes various `cwrap` calls into the emscripten module to
  73. // set up hooks; this depends on the emscripten JS having been
  74. // loaded (it has).
  75. var Module = {
  76. 'preRun': function() {
  77. // Merge environment variables from HTML script element.
  78. // This means you can add something like this to the HTML:
  79. // <script id="environment" type="application/json">
  80. // { "LOOPY_DEFAULT": "20x10t11dh" }
  81. // </script>
  82. var envscript = document.getElementById("environment");
  83. var k, v;
  84. if (envscript !== null)
  85. for ([k, v] of
  86. Object.entries(JSON.parse(envscript.textContent)))
  87. ENV[k] = v;
  88. },
  89. // Pass argv[1] as the fragment identifier (so that permalinks of
  90. // the form puzzle.html#game-id can launch the specified id).
  91. 'arguments': [decodeURIComponent(location.hash)],
  92. 'noExitRuntime': true
  93. };
  94. // Variables used by js_canvas_find_font_midpoint().
  95. var midpoint_test_str = "ABCDEFGHIKLMNOPRSTUVWXYZ0123456789";
  96. var midpoint_cache = [];
  97. // Variables used by js_activate_timer() and js_deactivate_timer().
  98. var timer;
  99. var timer_reference;
  100. // void timer_callback(double tplus);
  101. //
  102. // Called every frame while timing is active.
  103. var timer_callback;
  104. // The status bar object, if we have one.
  105. var statusbar = document.getElementById("statusbar");
  106. // Currently live blitters. We keep an integer id for each one on the
  107. // JS side; the C side, which expects a blitter to look like a struct,
  108. // simply defines the struct to contain that integer id.
  109. var blittercount = 0;
  110. var blitters = [];
  111. // State for the dialog-box mechanism. dlg_dimmer and dlg_form are the
  112. // page-darkening overlay and the actual dialog box respectively;
  113. // dlg_return_funcs is a list of JS functions to be called when the OK
  114. // button is pressed, to pass the results back to C.
  115. var dlg_dimmer = null, dlg_form = null;
  116. var dlg_return_funcs = null;
  117. // void dlg_return_sval(int index, const char *val);
  118. // void dlg_return_ival(int index, int val);
  119. //
  120. // C-side entry points called by functions in dlg_return_funcs, to
  121. // pass back the final value in each dialog control.
  122. var dlg_return_sval, dlg_return_ival;
  123. // Callback for reading from a savefile. This will be filled in with
  124. // a suitable closure by the JS loading code and called by
  125. // js_savefile_read(). This assumes that only one file can be in the
  126. // process of loading at a time.
  127. var savefile_read_callback;
  128. // void prefs_load_callback(midend *me, const char *prefs);
  129. //
  130. // Callback for passing in preferences data retrieved from localStorage.
  131. var prefs_load_callback;
  132. // The <ul> object implementing the game-type drop-down, and a list of
  133. // the sub-lists inside it. Used by js_add_preset().
  134. var gametypelist = document.getElementById("gametype");
  135. var gametypesubmenus = [gametypelist];
  136. // C entry point for miscellaneous events.
  137. var command;
  138. // The <form> encapsulating the menus. Used by
  139. // js_get_selected_preset() and js_select_preset().
  140. var menuform = document.getElementById("gamemenu");
  141. // The two anchors used to give permalinks to the current puzzle. Used
  142. // by js_update_permalinks().
  143. var permalink_seed = document.getElementById("permalink-seed");
  144. var permalink_desc = document.getElementById("permalink-desc");
  145. // The various buttons. Undo and redo are used by js_enable_undo_redo().
  146. var specific_button = document.getElementById("specific");
  147. var random_button = document.getElementById("random");
  148. var prefs_button = document.getElementById("prefs");
  149. var new_button = document.getElementById("new");
  150. var restart_button = document.getElementById("restart");
  151. var undo_button = document.getElementById("undo");
  152. var redo_button = document.getElementById("redo");
  153. var solve_button = document.getElementById("solve");
  154. var save_button = document.getElementById("save");
  155. var load_button = document.getElementById("load");
  156. // A div element enclosing both the puzzle and its status bar, used
  157. // for positioning the resize handle.
  158. var resizable_div = document.getElementById("resizable");
  159. // Alternatively, an extrinsically sized div that we will size the
  160. // puzzle to fit.
  161. var containing_div = document.getElementById("puzzlecanvascontain");
  162. // Helper function to find the absolute position of a given DOM
  163. // element on a page, by iterating upwards through the DOM finding
  164. // each element's offset from its parent, and thus calculating the
  165. // page-relative position of the target element.
  166. function element_coords(element) {
  167. var ex = 0, ey = 0;
  168. while (element.offsetParent) {
  169. ex += element.offsetLeft;
  170. ey += element.offsetTop;
  171. element = element.offsetParent;
  172. }
  173. return {x: ex, y:ey};
  174. }
  175. // Helper function which is passed a mouse event object and a DOM
  176. // element, and returns the coordinates of the mouse event relative to
  177. // the top left corner of the element by subtracting element_coords
  178. // from event.page{X,Y}.
  179. function relative_mouse_coords(event, element) {
  180. var ecoords = element_coords(element);
  181. return {x: event.pageX - ecoords.x,
  182. y: event.pageY - ecoords.y};
  183. }
  184. // Higher-level mouse helper function to specifically map mouse
  185. // coordinates into the coordinates on a canvas that appear under it.
  186. // This depends on the details of how a canvas gets scaled by CSS.
  187. function canvas_mouse_coords(event, element) {
  188. var rcoords = relative_mouse_coords(event, element);
  189. // Assume that the CSS object-fit property is "fill" (the default).
  190. var xscale = element.width / element.offsetWidth;
  191. var yscale = element.height / element.offsetHeight;
  192. return {x: rcoords.x * xscale, y: rcoords.y * yscale}
  193. }
  194. // Set the font on a CanvasRenderingContext2d based on the CSS font
  195. // for the canvas, the requested size, and whether we want something
  196. // monospaced.
  197. function canvas_set_font(ctx, size, monospaced) {
  198. var s = window.getComputedStyle(onscreen_canvas);
  199. // First set something that we're certain will work. Constructing
  200. // the font string from the computed style is a bit fragile, so
  201. // this acts as a fallback.
  202. ctx.font = `${size}px ` + (monospaced ? "monospace" : "sans-serif");
  203. // In CSS Fonts Module Level 4, "font-stretch" gets serialised as
  204. // a percentage, which can't be used in
  205. // CanvasRenderingContext2d.font, so we omit it.
  206. ctx.font = `${s.fontStyle} ${s.fontWeight} ${size}px ` +
  207. (monospaced ? "monospace" : s.fontFamily);
  208. }
  209. // Enable and disable items in the CSS menus.
  210. function disable_menu_item(item, disabledFlag) {
  211. item.disabled = disabledFlag;
  212. }
  213. // Dialog-box functions called from both C and JS.
  214. function dialog_init(titletext) {
  215. // Forward compatibility: Delete form and dimmer if they already
  216. // exist.
  217. dlg_dimmer = document.getElementById("dlgdimmer");
  218. if (dlg_dimmer) dlg_dimmer.parentElement.removeChild(dlg_dimmer);
  219. dlg_form = document.getElementById("dlgform");
  220. if (dlg_form) dlg_form.parentElement.removeChild(dlg_form);
  221. // Create an overlay on the page which darkens everything
  222. // beneath it.
  223. dlg_dimmer = document.createElement("div");
  224. dlg_dimmer.id = "dlgdimmer";
  225. // Now create a form which sits on top of that in turn.
  226. dlg_form = document.createElement("form");
  227. dlg_form.id = "dlgform";
  228. var title = document.createElement("h2");
  229. title.appendChild(document.createTextNode(titletext));
  230. dlg_form.appendChild(title);
  231. dlg_return_funcs = [];
  232. }
  233. function dialog_launch(ok_function, cancel_function) {
  234. // Put in the OK and Cancel buttons at the bottom.
  235. var button;
  236. if (ok_function) {
  237. button = document.createElement("input");
  238. button.type = "button";
  239. button.value = "OK";
  240. button.onclick = ok_function;
  241. dlg_form.appendChild(button);
  242. }
  243. if (cancel_function) {
  244. button = document.createElement("input");
  245. button.type = "button";
  246. button.value = "Cancel";
  247. button.onclick = cancel_function;
  248. dlg_form.appendChild(button);
  249. }
  250. document.body.appendChild(dlg_dimmer);
  251. document.body.appendChild(dlg_form);
  252. dlg_form.querySelector("input,select,a").focus();
  253. }
  254. function dialog_cleanup() {
  255. document.body.removeChild(dlg_dimmer);
  256. document.body.removeChild(dlg_form);
  257. dlg_dimmer = dlg_form = null;
  258. onscreen_canvas.focus();
  259. }
  260. function set_capture(element, event) {
  261. // This is only needed if we don't have Pointer Events available.
  262. if (element.setCapture !== undefined &&
  263. element.setPointerCapture === undefined) {
  264. element.setCapture(true);
  265. return;
  266. }
  267. }
  268. // Init function called early in main().
  269. function initPuzzle() {
  270. // Construct the off-screen canvas used for double buffering.
  271. onscreen_canvas = document.getElementById("puzzlecanvas");
  272. offscreen_canvas = document.createElement("canvas");
  273. ctx = offscreen_canvas.getContext('2d', { alpha: false });
  274. // Stop right-clicks on the puzzle from popping up a context menu.
  275. // We need those right-clicks!
  276. onscreen_canvas.oncontextmenu = function(event) { return false; }
  277. // Set up mouse handlers. We do a bit of tracking of the currently
  278. // pressed mouse buttons, to avoid sending mousemoves with no
  279. // button down (our puzzles don't want those events).
  280. var mousedown = Module.cwrap('mousedown', 'boolean',
  281. ['number', 'number', 'number']);
  282. var button_phys2log = [null, null, null];
  283. var buttons_down = function() {
  284. var i, toret = 0;
  285. for (i = 0; i < 3; i++)
  286. if (button_phys2log[i] !== null)
  287. toret |= 1 << button_phys2log[i];
  288. return toret;
  289. };
  290. onscreen_canvas.onpointerdown = function(event) {
  291. // Arrange that all mouse (and pointer) events are sent to
  292. // this element until all buttons are released. We can assume
  293. // that if we managed to receive a pointerdown event,
  294. // Element.setPointerCapture() is available.
  295. onscreen_canvas.setPointerCapture(event.pointerId);
  296. }
  297. onscreen_canvas.onmousedown = function(event) {
  298. if (event.button >= 3)
  299. return;
  300. var xy = canvas_mouse_coords(event, onscreen_canvas);
  301. var logbutton = event.button;
  302. if (event.shiftKey)
  303. logbutton = 1; // Shift-click overrides to middle button
  304. else if (event.ctrlKey)
  305. logbutton = 2; // Ctrl-click overrides to right button
  306. if (mousedown(xy.x, xy.y, logbutton))
  307. event.preventDefault();
  308. button_phys2log[event.button] = logbutton;
  309. set_capture(onscreen_canvas, event);
  310. };
  311. var mousemove = Module.cwrap('mousemove', 'boolean',
  312. ['number', 'number', 'number']);
  313. onscreen_canvas.onmousemove = function(event) {
  314. var down = buttons_down();
  315. if (down) {
  316. var xy = canvas_mouse_coords(event, onscreen_canvas);
  317. if (mousemove(xy.x, xy.y, down))
  318. event.preventDefault();
  319. }
  320. };
  321. var mouseup = Module.cwrap('mouseup', 'boolean',
  322. ['number', 'number', 'number']);
  323. onscreen_canvas.onmouseup = function(event) {
  324. if (event.button >= 3)
  325. return;
  326. if (button_phys2log[event.button] !== null) {
  327. var xy = canvas_mouse_coords(event, onscreen_canvas);
  328. if (mouseup(xy.x, xy.y, button_phys2log[event.button]))
  329. event.preventDefault();
  330. button_phys2log[event.button] = null;
  331. }
  332. };
  333. // Set up keyboard handlers. We call event.preventDefault()
  334. // in the keydown handler if it looks like we might have
  335. // done something with the key. This means that users
  336. // of this puzzle collection in other media
  337. // can indulge their instinct to press ^R for redo, for example,
  338. // without accidentally reloading the page.
  339. var key = Module.cwrap('key', 'boolean', ['number', 'string', 'string',
  340. 'number', 'number', 'number']);
  341. onscreen_canvas.onkeydown = function(event) {
  342. if (key(event.keyCode, event.key, event.char, event.location,
  343. event.shiftKey ? 1 : 0, event.ctrlKey ? 1 : 0))
  344. event.preventDefault();
  345. };
  346. // command() is a C function called to pass back events which
  347. // don't fall into other categories like mouse and key events.
  348. // Mostly those are button presses, but there's also one for the
  349. // game-type dropdown having been changed.
  350. command = Module.cwrap('command', 'void', ['number']);
  351. // Event handlers for buttons and things, which call command().
  352. if (specific_button) specific_button.onclick = function(event) {
  353. // Ensure we don't accidentally process these events when a
  354. // dialog is actually active, e.g. because the button still
  355. // has keyboard focus
  356. if (dlg_dimmer === null)
  357. command(0);
  358. };
  359. if (random_button) random_button.onclick = function(event) {
  360. if (dlg_dimmer === null)
  361. command(1);
  362. };
  363. if (new_button) new_button.onclick = function(event) {
  364. if (dlg_dimmer === null)
  365. command(5);
  366. };
  367. if (restart_button) restart_button.onclick = function(event) {
  368. if (dlg_dimmer === null)
  369. command(6);
  370. };
  371. if (undo_button) undo_button.onclick = function(event) {
  372. if (dlg_dimmer === null)
  373. command(7);
  374. };
  375. if (redo_button) redo_button.onclick = function(event) {
  376. if (dlg_dimmer === null)
  377. command(8);
  378. };
  379. if (solve_button) solve_button.onclick = function(event) {
  380. if (dlg_dimmer === null)
  381. command(9);
  382. };
  383. if (prefs_button) prefs_button.onclick = function(event) {
  384. if (dlg_dimmer === null)
  385. command(10);
  386. };
  387. // 'number' is used for C pointers
  388. var get_save_file = Module.cwrap('get_save_file', 'number', []);
  389. var free_save_file = Module.cwrap('free_save_file', 'void', ['number']);
  390. var load_game = Module.cwrap('load_game', 'void', []);
  391. if (save_button) save_button.onclick = function(event) {
  392. if (dlg_dimmer === null) {
  393. var savefile_ptr = get_save_file();
  394. var savefile_text = UTF8ToString(savefile_ptr);
  395. free_save_file(savefile_ptr);
  396. dialog_init("Download saved-game file");
  397. dlg_form.appendChild(document.createTextNode(
  398. "Click to download the "));
  399. var a = document.createElement("a");
  400. a.download = "puzzle.sav";
  401. a.href = "data:application/octet-stream," +
  402. encodeURIComponent(savefile_text);
  403. a.appendChild(document.createTextNode("saved-game file"));
  404. dlg_form.appendChild(a);
  405. dlg_form.appendChild(document.createTextNode("."));
  406. dlg_form.appendChild(document.createElement("br"));
  407. dialog_launch(function(event) {
  408. dialog_cleanup();
  409. });
  410. }
  411. };
  412. if (load_button) load_button.onclick = function(event) {
  413. if (dlg_dimmer === null) {
  414. var input = document.createElement("input");
  415. input.type = "file";
  416. input.multiple = false;
  417. input.addEventListener("change", function(event) {
  418. if (input.files.length == 1) {
  419. var file = input.files.item(0);
  420. var reader = new FileReader();
  421. reader.addEventListener("load", function() {
  422. var pos = 0;
  423. savefile_read_callback = function(buf, len) {
  424. if (pos + len > reader.result.byteLength)
  425. return false;
  426. writeArrayToMemory(
  427. new Int8Array(reader.result, pos, len), buf);
  428. pos += len;
  429. return true;
  430. }
  431. load_game();
  432. savefile_read_callback = null;
  433. });
  434. reader.addEventListener("error", function() {
  435. alert("An error occured while loading the file");
  436. });
  437. reader.readAsArrayBuffer(file);
  438. }
  439. });
  440. input.click();
  441. onscreen_canvas.focus();
  442. }
  443. };
  444. // Find the next or previous item in a menu, or null if there
  445. // isn't one. Skip list items that don't have a child (i.e.
  446. // separators) or whose child is disabled.
  447. function isuseful(item) {
  448. return item.querySelector(":scope > :not(:disabled)");
  449. }
  450. function nextmenuitem(item) {
  451. do item = item.nextElementSibling;
  452. while (item !== null && !isuseful(item));
  453. return item;
  454. }
  455. function prevmenuitem(item) {
  456. do item = item.previousElementSibling;
  457. while (item !== null && !isuseful(item));
  458. return item;
  459. }
  460. function firstmenuitem(menu) {
  461. var item = menu && menu.firstElementChild;
  462. while (item !== null && !isuseful(item))
  463. item = item.nextElementSibling;
  464. return item;
  465. }
  466. function lastmenuitem(menu) {
  467. var item = menu && menu.lastElementChild;
  468. while (item !== null && !isuseful(item))
  469. item = item.previousElementSibling;
  470. return item;
  471. }
  472. // Keyboard handlers for the menus.
  473. function menukey(event) {
  474. var target = event.target;
  475. var key = event.key;
  476. var thisitem = target.closest("li");
  477. var thismenu = thisitem.closest("ul");
  478. var targetitem = null;
  479. var parentitem;
  480. var parentitem_up = null;
  481. var parentitem_sideways = null;
  482. var submenu;
  483. function ishorizontal(menu) {
  484. // Which direction does this menu go in?
  485. var cs = window.getComputedStyle(menu);
  486. return cs.display == "flex" && cs.flexDirection == "row";
  487. }
  488. if (dlg_dimmer !== null)
  489. return;
  490. if (["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]
  491. .includes(key)) {
  492. var shortcutitem = thismenu.querySelectorAll(
  493. ":scope > li:not([role='separator']")[(Number(key) + 9) % 10];
  494. if (shortcutitem) {
  495. target = shortcutitem.firstElementChild;
  496. target.focus();
  497. thisitem = target.closest("li");
  498. key = "Enter";
  499. }
  500. }
  501. if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Enter",
  502. "Escape", "Backspace", "SoftRight", "F10"]
  503. .includes(key))
  504. return;
  505. if (ishorizontal(thismenu)) {
  506. // Top-level menu bar.
  507. if (key == "ArrowLeft")
  508. targetitem = prevmenuitem(thisitem) || lastmenuitem(thismenu);
  509. else if (key == "ArrowRight")
  510. targetitem = nextmenuitem(thisitem) || firstmenuitem(thismenu);
  511. else if (key == "ArrowUp")
  512. targetitem = lastmenuitem(thisitem.querySelector("ul"));
  513. else if (key == "ArrowDown" || key == "Enter")
  514. targetitem = firstmenuitem(thisitem.querySelector("ul"));
  515. } else {
  516. // Ordinary vertical menu.
  517. parentitem = thismenu.closest("li");
  518. if (parentitem) {
  519. if (ishorizontal(parentitem.closest("ul")))
  520. parentitem_up = parentitem;
  521. else
  522. parentitem_sideways = parentitem;
  523. }
  524. if (key == "ArrowUp")
  525. targetitem = prevmenuitem(thisitem) || parentitem_up ||
  526. lastmenuitem(thismenu);
  527. else if (key == "ArrowDown")
  528. targetitem = nextmenuitem(thisitem) || parentitem_up ||
  529. firstmenuitem(thismenu);
  530. else if (key == "ArrowRight")
  531. targetitem = thisitem.querySelector("li") ||
  532. (parentitem_up && nextmenuitem(parentitem_up));
  533. else if (key == "Enter")
  534. targetitem = thisitem.querySelector("li");
  535. else if (key == "ArrowLeft")
  536. targetitem = parentitem_sideways ||
  537. (parentitem_up && prevmenuitem(parentitem_up));
  538. else if (key == "Backspace")
  539. targetitem = parentitem;
  540. }
  541. if (targetitem)
  542. targetitem.firstElementChild.focus();
  543. else if (key == "Enter")
  544. target.click();
  545. else if (key == "Escape" || key == "SoftRight" ||
  546. key == "F10" || key == "Backspace")
  547. // Leave the menu entirely.
  548. onscreen_canvas.focus();
  549. // Prevent default even if we didn't do anything, as long as this
  550. // was an interesting key.
  551. event.preventDefault();
  552. event.stopPropagation();
  553. }
  554. menuform.addEventListener("keydown", menukey);
  555. // Open documentation links within the application in KaiOS.
  556. for (var elem of document.querySelectorAll("#gamemenu a[href]")) {
  557. elem.addEventListener("click", function(event) {
  558. window.open(event.target.href);
  559. event.preventDefault();
  560. });
  561. }
  562. // In IE, the canvas doesn't automatically gain focus on a mouse
  563. // click, so make sure it does
  564. onscreen_canvas.addEventListener("mousedown", function(event) {
  565. onscreen_canvas.focus();
  566. });
  567. // In our dialog boxes, Return and Escape should be like pressing
  568. // OK and Cancel respectively
  569. document.addEventListener("keydown", function(event) {
  570. if (dlg_dimmer !== null && event.keyCode == 13) {
  571. for (var i in dlg_return_funcs)
  572. dlg_return_funcs[i]();
  573. command(3);
  574. event.preventDefault();
  575. event.stopPropagation();
  576. }
  577. if (dlg_dimmer !== null && event.keyCode == 27) {
  578. command(4);
  579. event.preventDefault();
  580. event.stopPropagation();
  581. }
  582. }, true);
  583. // Arrange that the softkey labels are clickable. This logically
  584. // belongs as a click handler, but by the time the click event
  585. // fires, the input focus is in the wrong place.
  586. function button_to_key(key) {
  587. return function(mevent) {
  588. mevent.stopPropagation();
  589. mevent.preventDefault();
  590. var kevent = new KeyboardEvent("keydown", {
  591. key: key, view: window, bubbles: true});
  592. document.activeElement.dispatchEvent(kevent);
  593. };
  594. }
  595. for (var elem of document.querySelectorAll(".lsk"))
  596. elem.addEventListener("mousedown", button_to_key("SoftLeft"));
  597. for (var elem of document.querySelectorAll(".csk"))
  598. elem.addEventListener("mousedown", button_to_key("Enter"));
  599. for (var elem of document.querySelectorAll(".rsk"))
  600. elem.addEventListener("mousedown", button_to_key("SoftRight"));
  601. document.addEventListener("keydown", function(event) {
  602. // Key to open the menu on KaiOS.
  603. if ((event.key == "SoftRight" || event.key == "F10") &&
  604. !menuform.contains(document.activeElement)) {
  605. menuform.querySelector("li div, li button").focus();
  606. event.preventDefault();
  607. event.stopPropagation();
  608. }
  609. });
  610. // Handle "copy" actions. Browsers don't reliably target the
  611. // "copy" event at the canvas when it's focused. Firefox 102
  612. // targets the containing <div> while Chromium 114 targets the
  613. // <body>. So we catch the event at the document level and work
  614. // out if it's relevant ourselves.
  615. var get_text_format = Module.cwrap('get_text_format', 'number', []);
  616. var free_text_format = Module.cwrap('free_text_format', 'void', ['number']);
  617. document.addEventListener("copy", function(event) {
  618. // Make sure the target is an ancestor of the canvas. And if
  619. // there's a selection assume the user wants to copy that and
  620. // not the puzzle.
  621. if (event.target.contains(onscreen_canvas) &&
  622. window.getSelection().isCollapsed) {
  623. var ptr = get_text_format();
  624. event.clipboardData.setData('text/plain', UTF8ToString(ptr));
  625. event.preventDefault();
  626. free_text_format(ptr);
  627. }
  628. });
  629. // Event handler to fake :focus-within on browsers too old for
  630. // it (like KaiOS 2.5). Browsers without :focus-within are also
  631. // too old for focusin/out events, so we have to use focus events
  632. // which don't bubble but can be captured.
  633. //
  634. // A button losing focus because it was disabled doesn't generate
  635. // a blur event, so we do this entirely in the focus handler.
  636. document.documentElement.addEventListener("focus", function(event) {
  637. for (var elem = event.target; elem; elem = elem.parentElement)
  638. elem.classList.add("focus-within");
  639. for (elem of
  640. Array.from(document.getElementsByClassName("focus-within")))
  641. if (!elem.contains(event.target))
  642. elem.classList.remove("focus-within");
  643. }, true);
  644. // Set up the function pointers we haven't already grabbed.
  645. dlg_return_sval = Module.cwrap('dlg_return_sval', 'void',
  646. ['number','string']);
  647. dlg_return_ival = Module.cwrap('dlg_return_ival', 'void',
  648. ['number','number']);
  649. timer_callback = Module.cwrap('timer_callback', 'void', ['number']);
  650. prefs_load_callback = Module.cwrap('prefs_load_callback', 'void',
  651. ['number','number']);
  652. if (resizable_div !== null) {
  653. var resize_handle = document.getElementById("resizehandle");
  654. var resize_xbase = null, resize_ybase = null, restore_pending = false;
  655. var resize_xoffset = null, resize_yoffset = null;
  656. var resize_puzzle = Module.cwrap('resize_puzzle',
  657. 'void', ['number', 'number']);
  658. var restore_puzzle_size = Module.cwrap('restore_puzzle_size',
  659. 'void', []);
  660. resize_handle.oncontextmenu = function(event) { return false; }
  661. resize_handle.onpointerdown = function(event) {
  662. resize_handle.setPointerCapture(event.pointerId);
  663. }
  664. resize_handle.onmousedown = function(event) {
  665. if (event.button == 0) {
  666. var xy = element_coords(onscreen_canvas);
  667. resize_xbase = xy.x + onscreen_canvas.offsetWidth / 2;
  668. resize_ybase = xy.y;
  669. resize_xoffset =
  670. xy.x + onscreen_canvas.offsetWidth - event.pageX;
  671. resize_yoffset =
  672. xy.y + onscreen_canvas.offsetHeight - event.pageY;
  673. } else {
  674. restore_pending = true;
  675. }
  676. set_capture(resize_handle, event);
  677. event.preventDefault();
  678. };
  679. window.addEventListener("mousemove", function(event) {
  680. if (resize_xbase !== null && resize_ybase !== null) {
  681. var dpr = window.devicePixelRatio || 1;
  682. resize_puzzle(
  683. (event.pageX + resize_xoffset - resize_xbase) * dpr * 2,
  684. (event.pageY + resize_yoffset - resize_ybase) * dpr);
  685. event.preventDefault();
  686. // Chrome insists on selecting text during a resize drag
  687. // no matter what I do
  688. if (window.getSelection)
  689. window.getSelection().removeAllRanges();
  690. else
  691. document.selection.empty(); }
  692. });
  693. window.addEventListener("mouseup", function(event) {
  694. if (resize_xbase !== null && resize_ybase !== null) {
  695. resize_xbase = null;
  696. resize_ybase = null;
  697. onscreen_canvas.focus(); // return focus to the puzzle
  698. event.preventDefault();
  699. } else if (restore_pending) {
  700. // If you have the puzzle at larger than normal size and
  701. // then right-click to restore, I haven't found any way to
  702. // stop Chrome and IE popping up a context menu on the
  703. // revealed piece of document when you release the button
  704. // except by putting the actual restore into a setTimeout.
  705. // Gah.
  706. setTimeout(function() {
  707. restore_pending = false;
  708. restore_puzzle_size();
  709. onscreen_canvas.focus();
  710. }, 20);
  711. event.preventDefault();
  712. }
  713. });
  714. }
  715. var rescale_puzzle = Module.cwrap('rescale_puzzle', 'void', []);
  716. /*
  717. * If the puzzle is sized to fit the page, try to detect changes
  718. * of size of the containing element. Ideally this would use a
  719. * ResizeObserver on the containing_div, but I want this to work
  720. * on KaiOS 2.5, which doesn't have ResizeObserver. Instead we
  721. * watch events that might indicate that the div has changed size.
  722. */
  723. if (containing_div !== null) {
  724. var resize_handler = function(event) {
  725. rescale_puzzle();
  726. }
  727. window.addEventListener("resize", resize_handler);
  728. // Also catch the point when the document finishes loading,
  729. // since sometimes we seem to get the div's size too early.
  730. window.addEventListener("load", resize_handler);
  731. }
  732. }
  733. function post_init() {
  734. /*
  735. * Arrange to detect changes of device pixel ratio. Adapted from
  736. * <https://developer.mozilla.org/en-US/docs/Web/API/Window/
  737. * devicePixelRatio> (CC0) to work on older browsers.
  738. */
  739. var rescale_puzzle = Module.cwrap('rescale_puzzle', 'void', []);
  740. var mql = null;
  741. var update_pixel_ratio = function() {
  742. var dpr = window.devicePixelRatio;
  743. if (mql !== null)
  744. mql.removeListener(update_pixel_ratio);
  745. mql = window.matchMedia(`(resolution: ${dpr}dppx)`);
  746. mql.addListener(update_pixel_ratio);
  747. rescale_puzzle();
  748. }
  749. update_pixel_ratio();
  750. // If we get here with everything having gone smoothly, i.e.
  751. // we haven't crashed for one reason or another during setup, then
  752. // it's probably safe to hide the 'sorry, no puzzle here' div and
  753. // show the div containing the actual puzzle.
  754. var apology = document.getElementById("apology");
  755. if (apology !== null) apology.style.display = "none";
  756. document.getElementById("puzzle").style.display = "";
  757. // Default to giving keyboard focus to the puzzle.
  758. onscreen_canvas.focus();
  759. }