jsbeeb 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -177,7 +177,7 @@ sudo rpm -i out/dist/jsbeeb-1.0.1.x86_64.rpm
177
177
  Doesn't support the sth: pseudo URL unlike `disc` and `tape`, but if given a ZIP file will attempt to use the `.rom`
178
178
  file assumed to be within.
179
179
  - (mostly internal use) `logFdcCommands`, `logFdcStateChanges` - turn on logging in the disc controller.
180
- - `audioDebug=true` turns on some audio debug graphs.
180
+ - `audioDebug` - show audio queue stats chart.
181
181
 
182
182
  ## Patches
183
183
 
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "name": "jsbeeb",
8
8
  "description": "Emulate a BBC Micro",
9
9
  "repository": "git@github.com:mattgodbolt/jsbeeb.git",
10
- "version": "1.10.0",
10
+ "version": "1.12.0",
11
11
  "//": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
12
12
  "engines": {
13
13
  "node": ">=22.12.0"
@@ -30,13 +30,8 @@
30
30
  "argparse": "^2.0.1",
31
31
  "bootstrap": "^5.3.8",
32
32
  "bootswatch": "^5.3.8",
33
- "event-emitter-es6": "^1.1.5",
34
- "fflate": "^0.8.2",
35
- "jquery": "^4.0.0",
36
- "pako": "^2.1.0",
37
33
  "sharp": "^0.34.5",
38
- "smoothie": "^1.36.1",
39
- "underscore": "^1.13.7"
34
+ "smoothie": "^1.36.1"
40
35
  },
41
36
  "devDependencies": {
42
37
  "@eslint/js": "^10.0.1",
package/src/6502.js CHANGED
@@ -1106,7 +1106,7 @@ export class Cpu6502 extends Base6502 {
1106
1106
  const ramRomOs = this.ramRomOs;
1107
1107
  let data = await utils.loadData(name);
1108
1108
  if (/\.zip/i.test(name)) {
1109
- data = utils.unzipRomImage(data).data;
1109
+ data = (await utils.unzipRomImage(data)).data;
1110
1110
  }
1111
1111
  ramRomOs.set(data, offset);
1112
1112
  }
@@ -1208,6 +1208,13 @@ export class Cpu6502 extends Base6502 {
1208
1208
  if (state.roms) {
1209
1209
  this.ramRomOs.set(state.roms.slice(0, 16 * 16384), this.romOffset);
1210
1210
  }
1211
+ // Selectively overwrite individual sideways RAM banks (e.g. from BeebEm UEF import)
1212
+ // without touching ROM banks that jsbeeb has already loaded.
1213
+ if (state.swRamBanks) {
1214
+ for (const [bank, data] of Object.entries(state.swRamBanks)) {
1215
+ this.ramRomOs.set(data.slice(0, 16384), this.romOffset + Number(bank) * 16384);
1216
+ }
1217
+ }
1211
1218
  this.videoDisplayPage = state.videoDisplayPage;
1212
1219
  this.music5000PageSel = state.music5000PageSel;
1213
1220
 
package/src/app/app.js CHANGED
@@ -242,7 +242,7 @@ const template = [
242
242
  const result = await dialog.showOpenDialog(browserWindow, {
243
243
  title: "Load emulator state",
244
244
  filters: [
245
- { name: "Snapshot files", extensions: ["gz", "json", "snp"] },
245
+ { name: "Snapshot files", extensions: ["gz", "json", "snp", "uef"] },
246
246
  { name: "All files", extensions: ["*"] },
247
247
  ],
248
248
  properties: ["openFile"],
@@ -3,8 +3,6 @@
3
3
  // Electron integration for jsbeeb desktop application.
4
4
  // Handles IPC communication for loading disc/tape images and showing modals from Electron's main process.
5
5
 
6
- export let initialise = function () {};
7
-
8
6
  function init(args) {
9
7
  const { loadDiscImage, loadTapeImage, loadStateFile, processor, modals, actions, config } = args;
10
8
  const api = window.electronAPI;
@@ -57,15 +55,17 @@ function init(args) {
57
55
 
58
56
  // Save settings when they change
59
57
  if (config) {
60
- config.on("settings-changed", (settings) => {
61
- api.saveSettings(settings);
58
+ config.addEventListener("settings-changed", (e) => {
59
+ api.saveSettings(e.detail);
62
60
  });
63
- config.on("media-changed", (media) => {
64
- api.saveSettings(media);
61
+ config.addEventListener("media-changed", (e) => {
62
+ api.saveSettings(e.detail);
65
63
  });
66
64
  }
67
65
  }
68
66
 
69
- if (typeof window.electronAPI !== "undefined") {
70
- initialise = init;
67
+ export function initialise(args) {
68
+ if (typeof window.electronAPI !== "undefined") {
69
+ init(args);
70
+ }
71
71
  }
@@ -1,9 +1,12 @@
1
1
  "use strict";
2
+ import { decompress } from "./utils.js";
2
3
 
3
4
  // B-em snapshot format parser (versions 1 and 3).
4
5
  // v1 (BEMSNAP1): Fixed-size 327,885 byte packed struct. Reference: beebjit state.c
5
6
  // v3 (BEMSNAP3): Section-based with key+size headers, zlib-compressed memory. Reference: b-em savestate.c
6
7
 
8
+ import { volumeTable, buildVideoState, buildSnapshot } from "./snapshot-helpers.js";
9
+
7
10
  const BemV1Size = 327885;
8
11
 
9
12
  // v1 struct offsets
@@ -113,7 +116,7 @@ function convertViaState(bemVia, ic32) {
113
116
  t2l: bemVia.t2l,
114
117
  t1c: bemVia.t1c,
115
118
  t2c: bemVia.t2c,
116
- t1hit: !!bemVia.t1hit,
119
+ t1hit: bemVia.acr & 0x40 ? false : !!bemVia.t1hit,
117
120
  t2hit: !!bemVia.t2hit,
118
121
  portapins: 0xff,
119
122
  portbpins: 0xff,
@@ -134,17 +137,6 @@ function convertViaState(bemVia, ic32) {
134
137
  return result;
135
138
  }
136
139
 
137
- // Volume lookup table matching soundchip.js
138
- const volumeTable = new Float32Array(16);
139
- (() => {
140
- let f = 1.0;
141
- for (let i = 0; i < 15; ++i) {
142
- volumeTable[i] = f / 4;
143
- f *= Math.pow(10, -0.1);
144
- }
145
- volumeTable[15] = 0;
146
- })();
147
-
148
140
  function convertSoundState(snLatch, snCount, snStat, snVol, snNoise, snShift) {
149
141
  const registers = new Uint16Array(4);
150
142
  const counter = new Float32Array(4);
@@ -180,178 +172,6 @@ function convertSoundState(snLatch, snCount, snStat, snVol, snNoise, snShift) {
180
172
  };
181
173
  }
182
174
 
183
- function buildVideoState(ulaControl, ulaPalette, crtcRegs, nulaCollook, crtcCounters) {
184
- const regs = new Uint8Array(32);
185
- regs.set(crtcRegs.slice(0, 18));
186
- const actualPal = new Uint8Array(16);
187
- for (let i = 0; i < 16; i++) actualPal[i] = ulaPalette[i] & 0x0f;
188
-
189
- // Use NULA collook if provided, otherwise use default BBC palette
190
- const collook =
191
- nulaCollook ||
192
- new Int32Array([
193
- 0xff000000, 0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff, 0xff000000,
194
- 0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff,
195
- ]);
196
-
197
- // Compute ulaPal from actualPal + collook, matching jsbeeb's Ula._recomputeUlaPal
198
- const flashEnabled = !!(ulaControl & 1);
199
- const defaultFlash = new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1]);
200
- const flash = defaultFlash;
201
- const ulaPal = new Int32Array(16);
202
- for (let i = 0; i < 16; i++) {
203
- const palVal = actualPal[i];
204
- let colour = collook[(palVal & 0xf) ^ 7];
205
- if (palVal & 8 && flashEnabled && flash[(palVal & 7) ^ 7]) {
206
- colour = collook[palVal & 0xf];
207
- }
208
- ulaPal[i] = colour;
209
- }
210
-
211
- return {
212
- regs,
213
- bitmapX: 0,
214
- bitmapY: 0,
215
- oddClock: false,
216
- frameCount: 0,
217
- doEvenFrameLogic: false,
218
- isEvenRender: true,
219
- lastRenderWasEven: false,
220
- firstScanline: true,
221
- inHSync: false,
222
- inVSync: false,
223
- hadVSyncThisRow: false,
224
- checkVertAdjust: false,
225
- endOfMainLatched: false,
226
- endOfVertAdjustLatched: false,
227
- endOfFrameLatched: false,
228
- inVertAdjust: false,
229
- inDummyRaster: false,
230
- hpulseWidth: regs[3] & 0x0f,
231
- vpulseWidth: (regs[3] & 0xf0) >>> 4,
232
- hpulseCounter: 0,
233
- vpulseCounter: 0,
234
- dispEnabled: 0x3f,
235
- horizCounter: crtcCounters ? crtcCounters.hc : 0,
236
- vertCounter: crtcCounters ? crtcCounters.vc : 0,
237
- scanlineCounter: crtcCounters ? crtcCounters.sc : 0,
238
- vertAdjustCounter: 0,
239
- addr: crtcCounters ? crtcCounters.ma : (regs[13] | (regs[12] << 8)) & 0x3fff,
240
- lineStartAddr: crtcCounters ? crtcCounters.maback : (regs[13] | (regs[12] << 8)) & 0x3fff,
241
- nextLineStartAddr: crtcCounters ? crtcCounters.maback : (regs[13] | (regs[12] << 8)) & 0x3fff,
242
- ulactrl: ulaControl,
243
- pixelsPerChar: ulaControl & 0x10 ? 8 : 16,
244
- halfClock: !(ulaControl & 0x10),
245
- ulaMode: (ulaControl >>> 2) & 3,
246
- teletextMode: !!(ulaControl & 2),
247
- displayEnableSkew: Math.min((regs[8] & 0x30) >>> 4, 2),
248
- ulaPal,
249
- actualPal,
250
- cursorOn: false,
251
- cursorOff: false,
252
- cursorOnThisFrame: false,
253
- cursorDrawIndex: 0,
254
- cursorPos: (regs[15] | (regs[14] << 8)) & 0x3fff,
255
- interlacedSyncAndVideo: (regs[8] & 3) === 3,
256
- screenSubtract: 0,
257
- ula: {
258
- collook: collook.slice(),
259
- flash: new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1]),
260
- paletteWriteFlag: false,
261
- paletteFirstByte: 0,
262
- paletteMode: 0,
263
- horizontalOffset: 0,
264
- leftBlank: 0,
265
- disabled: false,
266
- attributeMode: 0,
267
- attributeText: 0,
268
- },
269
- crtc: { curReg: 0 },
270
- teletext: {
271
- prevCol: 0,
272
- col: 7,
273
- bg: 0,
274
- sep: false,
275
- dbl: false,
276
- oldDbl: false,
277
- secondHalfOfDouble: false,
278
- wasDbl: false,
279
- gfx: false,
280
- flash: false,
281
- flashOn: false,
282
- flashTime: 0,
283
- heldChar: 0,
284
- holdChar: false,
285
- dataQueue: [0, 0, 0, 0],
286
- scanlineCounter: 0,
287
- levelDEW: false,
288
- levelDISPTMG: false,
289
- levelRA0: false,
290
- nextGlyphs: "normal",
291
- curGlyphs: "normal",
292
- heldGlyphs: "normal",
293
- },
294
- };
295
- }
296
-
297
- const DefaultAcia = {
298
- sr: 0x02,
299
- cr: 0x00,
300
- dr: 0x00,
301
- rs423Selected: false,
302
- motorOn: false,
303
- tapeCarrierCount: 0,
304
- tapeDcdLineLevel: false,
305
- hadDcdHigh: false,
306
- serialReceiveRate: 19200,
307
- serialReceiveCyclesPerByte: 0,
308
- txCompleteTaskOffset: null,
309
- runTapeTaskOffset: null,
310
- runRs423TaskOffset: null,
311
- };
312
-
313
- const DefaultAdc = { status: 0x40, low: 0x00, high: 0x00, taskOffset: null };
314
-
315
- function buildSnapshot(modelName, cpuState, ram, roms, sysvia, uservia, video, soundChip) {
316
- return {
317
- format: "jsbeeb-snapshot",
318
- version: 1,
319
- model: modelName,
320
- timestamp: new Date().toISOString(),
321
- importedFrom: "b-em",
322
- state: {
323
- a: cpuState.a,
324
- x: cpuState.x,
325
- y: cpuState.y,
326
- s: cpuState.s,
327
- pc: cpuState.pc,
328
- p: cpuState.flags | 0x30,
329
- nmiLevel: !!cpuState.nmi,
330
- nmiEdge: false,
331
- halted: false,
332
- takeInt: false,
333
- romsel: cpuState.fe30 ?? 0,
334
- acccon: cpuState.fe34 ?? 0,
335
- videoDisplayPage: 0,
336
- currentCycles: 0,
337
- targetCycles: 0,
338
- cycleSeconds: 0,
339
- peripheralCycles: 0,
340
- videoCycles: 0,
341
- music5000PageSel: 0,
342
- ram,
343
- roms,
344
- scheduler: { epoch: 0 },
345
- sysvia,
346
- uservia,
347
- video,
348
- soundChip,
349
- acia: { ...DefaultAcia },
350
- adc: { ...DefaultAdc },
351
- },
352
- };
353
- }
354
-
355
175
  // ── V1 parser (BEMSNAP1, fixed struct) ──────────────────────────────
356
176
 
357
177
  function parseBemV1(buffer) {
@@ -401,6 +221,7 @@ function parseBemV1(buffer) {
401
221
  const snShift = view.getUint16(soundOff + 53, true);
402
222
 
403
223
  return buildSnapshot(
224
+ "b-em",
404
225
  "B",
405
226
  cpuState,
406
227
  ram,
@@ -434,39 +255,6 @@ function readString(data, pos) {
434
255
  return str;
435
256
  }
436
257
 
437
- async function inflateRaw(compressedData) {
438
- // Use DecompressionStream (available in modern browsers and Node 18+)
439
- if (typeof DecompressionStream !== "undefined") {
440
- const ds = new DecompressionStream("deflate");
441
- const writer = ds.writable.getWriter();
442
- const reader = ds.readable.getReader();
443
- const chunks = [];
444
-
445
- // Start reading before writing to avoid backpressure deadlock
446
- const readPromise = (async () => {
447
- for (;;) {
448
- const { done, value } = await reader.read();
449
- if (done) break;
450
- chunks.push(value);
451
- }
452
- })();
453
-
454
- await writer.write(compressedData);
455
- await writer.close();
456
- await readPromise;
457
-
458
- const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
459
- const result = new Uint8Array(totalLength);
460
- let offset = 0;
461
- for (const chunk of chunks) {
462
- result.set(chunk, offset);
463
- offset += chunk.length;
464
- }
465
- return result;
466
- }
467
- throw new Error("Zlib decompression not available (need DecompressionStream API)");
468
- }
469
-
470
258
  function parseBemV3(buffer) {
471
259
  const bytes = new Uint8Array(buffer);
472
260
  let offset = 8; // Skip "BEMSNAP3" signature
@@ -523,7 +311,7 @@ function parseBemV3(buffer) {
523
311
  if (memSection) {
524
312
  // Memory is zlib-compressed; decompression is async.
525
313
  // Decompressed layout: 2 bytes (fe30, fe34) + 64KB RAM + 256KB ROM
526
- return inflateRaw(memSection.data).then((memData) => {
314
+ return decompress(memSection.data, "deflate").then((memData) => {
527
315
  cpuState.fe30 = memData[0];
528
316
  cpuState.fe34 = memData[1];
529
317
  const ramStart = 2;
@@ -669,6 +457,7 @@ function finishV3Parse(modelName, cpuState, ram, roms, sections) {
669
457
  }
670
458
 
671
459
  return buildSnapshot(
460
+ "b-em",
672
461
  modelName,
673
462
  cpuState,
674
463
  ram,
package/src/config.js CHANGED
@@ -1,10 +1,8 @@
1
1
  "use strict";
2
- import $ from "jquery";
3
- import EventEmitter from "event-emitter-es6";
4
- import { findModel } from "./models.js";
2
+ import { allModels, findModel } from "./models.js";
5
3
  import { getFilterForMode } from "./canvas.js";
6
4
 
7
- export class Config extends EventEmitter {
5
+ export class Config extends EventTarget {
8
6
  constructor(onChange, onClose) {
9
7
  super();
10
8
  this.onChange = onChange;
@@ -12,8 +10,8 @@ export class Config extends EventEmitter {
12
10
  this.changed = {};
13
11
  this.model = null;
14
12
  this.coProcessor = null;
15
- const $configuration = document.getElementById("configuration");
16
- $configuration.addEventListener("show.bs.modal", () => {
13
+ const configuration = document.getElementById("configuration");
14
+ configuration.addEventListener("show.bs.modal", () => {
17
15
  this.changed = {};
18
16
  this.setDropdownText(this.model.name);
19
17
  this.set65c02(this.model.tube);
@@ -23,116 +21,134 @@ export class Config extends EventEmitter {
23
21
  this.setEconet(this.model.hasEconet);
24
22
  });
25
23
 
26
- $configuration.addEventListener("hide.bs.modal", () => {
24
+ configuration.addEventListener("hide.bs.modal", () => {
27
25
  this.onClose(this.changed);
28
26
  if (Object.keys(this.changed).length > 0) {
29
- this.emit("settings-changed", this.changed);
27
+ this.dispatchEvent(new CustomEvent("settings-changed", { detail: this.changed }));
30
28
  }
31
29
  });
32
30
 
33
- $(".model-menu a").on("click", (e) => {
34
- this.changed.model = $(e.target).attr("data-target");
35
- this.setDropdownText($(e.target).text());
31
+ const modelMenu = document.querySelector(".model-menu");
32
+ for (const model of allModels) {
33
+ if (model.synonyms.length === 0) continue; // skip non-selectable models (e.g. Tube65C02)
34
+ const li = document.createElement("li");
35
+ const a = document.createElement("a");
36
+ a.href = "#";
37
+ a.className = "dropdown-item";
38
+ a.dataset.target = model.synonyms[0];
39
+ a.textContent = model.name;
40
+ li.appendChild(a);
41
+ modelMenu.appendChild(li);
42
+ }
43
+ modelMenu.addEventListener("click", (e) => {
44
+ const link = e.target.closest("a[data-target]");
45
+ if (!link) return;
46
+ this.changed.model = link.dataset.target;
47
+ this.setDropdownText(link.textContent);
36
48
  });
37
49
 
38
- $("#65c02").on("click", () => {
39
- this.changed.coProcessor = $("#65c02").prop("checked");
40
- $("#tubeCpuMultiplier").prop("disabled", !$("#65c02").prop("checked"));
50
+ document.getElementById("65c02").addEventListener("click", () => {
51
+ this.changed.coProcessor = document.getElementById("65c02").checked;
52
+ document.getElementById("tubeCpuMultiplier").disabled = !document.getElementById("65c02").checked;
41
53
  });
42
54
 
43
- $("#tubeCpuMultiplier").on("input", () => {
44
- const val = parseInt($("#tubeCpuMultiplier").val(), 10);
45
- $("#tubeCpuMultiplierValue").text(val);
55
+ document.getElementById("tubeCpuMultiplier").addEventListener("input", () => {
56
+ const val = parseInt(document.getElementById("tubeCpuMultiplier").value, 10);
57
+ document.getElementById("tubeCpuMultiplierValue").textContent = val;
46
58
  this.changed.tubeCpuMultiplier = val;
47
59
  });
48
60
 
49
- $("#hasTeletextAdaptor").on("click", () => {
50
- this.changed.hasTeletextAdaptor = $("#hasTeletextAdaptor").prop("checked");
61
+ document.getElementById("hasTeletextAdaptor").addEventListener("click", () => {
62
+ this.changed.hasTeletextAdaptor = document.getElementById("hasTeletextAdaptor").checked;
51
63
  });
52
64
 
53
- $("#hasEconet").on("click", () => {
54
- this.changed.hasEconet = $("#hasEconet").prop("checked");
65
+ document.getElementById("hasEconet").addEventListener("click", () => {
66
+ this.changed.hasEconet = document.getElementById("hasEconet").checked;
55
67
  });
56
68
 
57
- $("#hasMusic5000").on("click", () => {
58
- this.changed.hasMusic5000 = $("#hasMusic5000").prop("checked");
69
+ document.getElementById("hasMusic5000").addEventListener("click", () => {
70
+ this.changed.hasMusic5000 = document.getElementById("hasMusic5000").checked;
59
71
  });
60
72
 
61
- $(".keyboard-menu a").on("click", (e) => {
62
- const keyLayout = $(e.target).attr("data-target");
63
- this.changed.keyLayout = keyLayout;
64
- this.setKeyLayout(keyLayout);
65
- });
73
+ for (const link of document.querySelectorAll(".keyboard-menu a")) {
74
+ link.addEventListener("click", (e) => {
75
+ const keyLayout = e.target.dataset.target;
76
+ this.changed.keyLayout = keyLayout;
77
+ this.setKeyLayout(keyLayout);
78
+ });
79
+ }
66
80
 
67
- $(".mic-channel-option").on("click", (e) => {
68
- const channelString = $(e.target).data("channel");
69
- const channel = channelString === "" ? undefined : parseInt($(e.target).data("channel"), 10);
70
- this.changed.microphoneChannel = channel;
71
- this.setMicrophoneChannel(channel);
72
- });
81
+ for (const option of document.querySelectorAll(".mic-channel-option")) {
82
+ option.addEventListener("click", (e) => {
83
+ const channelString = e.target.dataset.channel;
84
+ const channel = channelString === "" ? undefined : parseInt(channelString, 10);
85
+ this.changed.microphoneChannel = channel;
86
+ this.setMicrophoneChannel(channel);
87
+ });
88
+ }
73
89
 
74
- $("#mouseJoystickEnabled").on("click", () => {
75
- this.changed.mouseJoystickEnabled = $("#mouseJoystickEnabled").prop("checked");
90
+ document.getElementById("mouseJoystickEnabled").addEventListener("click", () => {
91
+ this.changed.mouseJoystickEnabled = document.getElementById("mouseJoystickEnabled").checked;
76
92
  });
77
93
 
78
- $("#speechOutput").on("click", () => {
79
- this.changed.speechOutput = $("#speechOutput").prop("checked");
94
+ document.getElementById("speechOutput").addEventListener("click", () => {
95
+ this.changed.speechOutput = document.getElementById("speechOutput").checked;
80
96
  });
81
97
 
82
- $(".display-mode-option").on("click", (e) => {
83
- const mode = $(e.target).data("mode");
84
- this.changed.displayMode = mode;
85
- this.setDisplayMode(mode);
86
- this.onChange({ displayMode: mode });
87
- });
98
+ for (const option of document.querySelectorAll(".display-mode-option")) {
99
+ option.addEventListener("click", (e) => {
100
+ const mode = e.target.dataset.mode;
101
+ this.changed.displayMode = mode;
102
+ this.setDisplayMode(mode);
103
+ this.onChange({ displayMode: mode });
104
+ });
105
+ }
88
106
  }
89
107
 
90
108
  setMicrophoneChannel(channel) {
91
- if (channel !== undefined) {
92
- $(".mic-channel-text").text(`Channel ${channel}`);
93
- } else {
94
- $(".mic-channel-text").text("Disabled");
95
- }
109
+ const text = channel !== undefined ? `Channel ${channel}` : "Disabled";
110
+ for (const el of document.querySelectorAll(".mic-channel-text")) el.textContent = text;
96
111
  }
97
112
 
98
113
  setMouseJoystickEnabled(enabled) {
99
- $("#mouseJoystickEnabled").prop("checked", !!enabled);
114
+ document.getElementById("mouseJoystickEnabled").checked = !!enabled;
100
115
  }
101
116
 
102
117
  setSpeechOutput(enabled) {
103
- $("#speechOutput").prop("checked", !!enabled);
118
+ document.getElementById("speechOutput").checked = !!enabled;
104
119
  }
105
120
 
106
121
  setDisplayMode(mode) {
107
122
  const config = getFilterForMode(mode).getDisplayConfig();
108
- $(".display-mode-text").text(config.name);
123
+ for (const el of document.querySelectorAll(".display-mode-text")) el.textContent = config.name;
109
124
  }
110
125
 
111
126
  setModel(modelName) {
112
127
  this.model = findModel(modelName);
113
- $(".bbc-model").text(this.model.name);
128
+ for (const el of document.querySelectorAll(".bbc-model")) el.textContent = this.model.name;
114
129
  }
115
130
 
116
131
  setKeyLayout(keyLayout) {
117
- $(".keyboard-layout").text(keyLayout[0].toUpperCase() + keyLayout.substring(1));
132
+ const text = keyLayout[0].toUpperCase() + keyLayout.substring(1);
133
+ for (const el of document.querySelectorAll(".keyboard-layout")) el.textContent = text;
118
134
  }
119
135
 
120
136
  set65c02(enabled) {
121
137
  enabled = !!enabled;
122
- $("#65c02").prop("checked", enabled);
138
+ document.getElementById("65c02").checked = enabled;
123
139
  this.model.tube = enabled ? findModel("Tube65c02") : null;
124
- $("#tubeCpuMultiplier").prop("disabled", !enabled);
140
+ document.getElementById("tubeCpuMultiplier").disabled = !enabled;
125
141
  }
126
142
 
127
143
  setTubeCpuMultiplier(value) {
128
144
  this.tubeCpuMultiplier = value;
129
- $("#tubeCpuMultiplier").val(value);
130
- $("#tubeCpuMultiplierValue").text(value);
145
+ document.getElementById("tubeCpuMultiplier").value = value;
146
+ document.getElementById("tubeCpuMultiplierValue").textContent = value;
131
147
  }
132
148
 
133
149
  setEconet(enabled) {
134
150
  enabled = !!enabled;
135
- $("#hasEconet").prop("checked", enabled);
151
+ document.getElementById("hasEconet").checked = enabled;
136
152
  this.model.hasEconet = enabled;
137
153
 
138
154
  if (enabled && this.model.isMaster) {
@@ -142,20 +158,21 @@ export class Config extends EventEmitter {
142
158
 
143
159
  setMusic5000(enabled) {
144
160
  enabled = !!enabled;
145
- $("#hasMusic5000").prop("checked", enabled);
161
+ document.getElementById("hasMusic5000").checked = enabled;
146
162
  this.model.hasMusic5000 = enabled;
147
163
  this.addRemoveROM("ample.rom", enabled);
148
164
  }
149
165
 
150
166
  setTeletext(enabled) {
151
167
  enabled = !!enabled;
152
- $("#hasTeletextAdaptor").prop("checked", enabled);
168
+ document.getElementById("hasTeletextAdaptor").checked = enabled;
153
169
  this.model.hasTeletextAdaptor = enabled;
154
170
  this.addRemoveROM("ats-3.0.rom", enabled);
155
171
  }
156
172
 
157
173
  setDropdownText(modelName) {
158
- $("#bbc-model-dropdown .bbc-model").text(modelName);
174
+ const el = document.querySelector("#bbc-model-dropdown .bbc-model");
175
+ if (el) el.textContent = modelName;
159
176
  }
160
177
 
161
178
  addRemoveROM(romName, required) {
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+
3
+ // Minimal DOM helpers to replace jQuery usage.
4
+
5
+ export function show(el) {
6
+ el.style.display = "";
7
+ }
8
+
9
+ export function hide(el) {
10
+ el.style.display = "none";
11
+ }
12
+
13
+ export function toggle(el, visible) {
14
+ el.style.display = visible ? "" : "none";
15
+ }
16
+
17
+ export function fadeIn(el, duration = 400) {
18
+ el.style.display = "";
19
+ el.style.transition = `opacity ${duration}ms`;
20
+ el.style.opacity = "0";
21
+ // Force reflow so the transition triggers.
22
+ void el.offsetHeight;
23
+ el.style.opacity = "1";
24
+ }
25
+
26
+ export function fadeOut(el, duration = 400) {
27
+ el.style.transition = `opacity ${duration}ms`;
28
+ el.style.opacity = "0";
29
+ setTimeout(() => {
30
+ if (el.style.opacity === "0") el.style.display = "none";
31
+ }, duration);
32
+ }