@mideind/netskrafl-react 2.0.0 → 2.1.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/dist/cjs/css/netskrafl.css +131 -3
- package/dist/cjs/index.js +502 -142
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/css/netskrafl.css +131 -3
- package/dist/esm/index.js +502 -142
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -30015,6 +30015,34 @@ function formatDate(dateStr) {
|
|
|
30015
30015
|
const month = ts(MONTH_KEYS[date.getUTCMonth()]);
|
|
30016
30016
|
return ts('date_format', { day, month });
|
|
30017
30017
|
}
|
|
30018
|
+
function setDefaultFocus(delay = 0) {
|
|
30019
|
+
// Set focus to the main container element to enable keyboard input.
|
|
30020
|
+
// Use class selector to work with both Netskrafl and Gáta dagsins.
|
|
30021
|
+
// An optional delay can be specified to wait for rendering to complete.
|
|
30022
|
+
const doFocus = () => {
|
|
30023
|
+
const container = document.querySelector('.netskrafl-container');
|
|
30024
|
+
container?.focus();
|
|
30025
|
+
};
|
|
30026
|
+
if (delay > 0) {
|
|
30027
|
+
setTimeout(doFocus, delay);
|
|
30028
|
+
}
|
|
30029
|
+
else {
|
|
30030
|
+
setTimeout(doFocus);
|
|
30031
|
+
}
|
|
30032
|
+
}
|
|
30033
|
+
function setKeyboardHandler(handler, delay = 100) {
|
|
30034
|
+
// Set up keyboard handling on the main container element.
|
|
30035
|
+
// Use class selector to work with both Netskrafl and Gáta dagsins.
|
|
30036
|
+
// The handler is attached after a delay to ensure the DOM is ready.
|
|
30037
|
+
setTimeout(() => {
|
|
30038
|
+
const container = document.querySelector('.netskrafl-container');
|
|
30039
|
+
if (container) {
|
|
30040
|
+
container.tabIndex = 0;
|
|
30041
|
+
container.addEventListener('keydown', handler);
|
|
30042
|
+
container.focus();
|
|
30043
|
+
}
|
|
30044
|
+
}, delay);
|
|
30045
|
+
}
|
|
30018
30046
|
|
|
30019
30047
|
/*
|
|
30020
30048
|
|
|
@@ -30064,6 +30092,8 @@ class Actions {
|
|
|
30064
30092
|
if (!model.state?.uiFullscreen)
|
|
30065
30093
|
// Mobile UI: show board tab
|
|
30066
30094
|
view.setSelectedTab('board');
|
|
30095
|
+
// Focus the container to enable keyboard input
|
|
30096
|
+
setDefaultFocus(100);
|
|
30067
30097
|
}, deleteZombie);
|
|
30068
30098
|
if (model.game !== null && model.game !== undefined) {
|
|
30069
30099
|
logEvent('game_open', {
|
|
@@ -31226,6 +31256,31 @@ const TogglerFairplay = () => {
|
|
|
31226
31256
|
const BLANK_TILES_PER_LINE = 6;
|
|
31227
31257
|
const BlankDialog = () => {
|
|
31228
31258
|
// A dialog for choosing the meaning of a blank tile
|
|
31259
|
+
function cancel(game) {
|
|
31260
|
+
game.cancelBlankDialog();
|
|
31261
|
+
setDefaultFocus();
|
|
31262
|
+
}
|
|
31263
|
+
function place(game, letter) {
|
|
31264
|
+
game.placeBlank(letter);
|
|
31265
|
+
setDefaultFocus();
|
|
31266
|
+
}
|
|
31267
|
+
function handleKeydown(game, ev) {
|
|
31268
|
+
// Escape key cancels the dialog
|
|
31269
|
+
let { key } = ev;
|
|
31270
|
+
if (key === 'Escape') {
|
|
31271
|
+
ev.preventDefault();
|
|
31272
|
+
cancel(game);
|
|
31273
|
+
return;
|
|
31274
|
+
}
|
|
31275
|
+
if (key.length === 1) {
|
|
31276
|
+
// Check if the pressed key matches a valid letter
|
|
31277
|
+
key = key.toLowerCase();
|
|
31278
|
+
if (game.alphabet.includes(key)) {
|
|
31279
|
+
ev.preventDefault();
|
|
31280
|
+
place(game, key);
|
|
31281
|
+
}
|
|
31282
|
+
}
|
|
31283
|
+
}
|
|
31229
31284
|
function blankLetters(game) {
|
|
31230
31285
|
const legalLetters = game.alphabet;
|
|
31231
31286
|
let len = legalLetters.length;
|
|
@@ -31239,8 +31294,8 @@ const BlankDialog = () => {
|
|
|
31239
31294
|
const letter = legalLetters[ix++];
|
|
31240
31295
|
c.push(m('td', {
|
|
31241
31296
|
onclick: (ev) => {
|
|
31242
|
-
game.placeBlank(letter);
|
|
31243
31297
|
ev.preventDefault();
|
|
31298
|
+
place(game, letter);
|
|
31244
31299
|
},
|
|
31245
31300
|
onmouseover: buttonOver,
|
|
31246
31301
|
onmouseout: buttonOut,
|
|
@@ -31259,6 +31314,9 @@ const BlankDialog = () => {
|
|
|
31259
31314
|
return m('.modal-dialog', {
|
|
31260
31315
|
id: 'blank-dialog',
|
|
31261
31316
|
style: { visibility: 'visible' },
|
|
31317
|
+
tabindex: -1,
|
|
31318
|
+
onkeydown: (ev) => handleKeydown(game, ev),
|
|
31319
|
+
oncreate: (vnode) => vnode.dom.focus(),
|
|
31262
31320
|
}, m('.ui-widget.ui-widget-content.ui-corner-all', { id: 'blank-form' }, [
|
|
31263
31321
|
mt('p', 'Hvaða staf táknar auða flísin?'),
|
|
31264
31322
|
m('.rack.blank-rack', m('table.board', { id: 'blank-meaning' }, blankLetters(game))),
|
|
@@ -31267,7 +31325,7 @@ const BlankDialog = () => {
|
|
|
31267
31325
|
title: ts('Hætta við'),
|
|
31268
31326
|
onclick: (ev) => {
|
|
31269
31327
|
ev.preventDefault();
|
|
31270
|
-
game
|
|
31328
|
+
cancel(game);
|
|
31271
31329
|
},
|
|
31272
31330
|
}, glyph('remove')),
|
|
31273
31331
|
]));
|
|
@@ -31542,6 +31600,8 @@ const Buttons = {
|
|
|
31542
31600
|
drag-and-drop does not offer enough flexibility for our requirements.
|
|
31543
31601
|
|
|
31544
31602
|
*/
|
|
31603
|
+
// Minimum movement in pixels before a mousedown becomes a drag
|
|
31604
|
+
const DRAG_THRESHOLD = 10;
|
|
31545
31605
|
class DragManager {
|
|
31546
31606
|
getEventCoordinates(e) {
|
|
31547
31607
|
if (e instanceof MouseEvent) {
|
|
@@ -31561,28 +31621,58 @@ class DragManager {
|
|
|
31561
31621
|
p.y >= rect.top &&
|
|
31562
31622
|
p.y < rect.bottom);
|
|
31563
31623
|
}
|
|
31564
|
-
constructor(e, dropHandler) {
|
|
31624
|
+
constructor(e, dropHandler, clickHandler) {
|
|
31565
31625
|
this.parentElement = null;
|
|
31566
31626
|
this.parentRect = null;
|
|
31567
31627
|
this.offsetX = 0;
|
|
31568
31628
|
this.offsetY = 0;
|
|
31569
31629
|
this.centerX = 0;
|
|
31570
31630
|
this.centerY = 0;
|
|
31631
|
+
this.startX = 0;
|
|
31632
|
+
this.startY = 0;
|
|
31571
31633
|
this.lastX = 0;
|
|
31572
31634
|
this.lastY = 0;
|
|
31573
31635
|
this.currentDropTarget = null;
|
|
31574
|
-
|
|
31636
|
+
this.dragStarted = false;
|
|
31575
31637
|
// Note: We use e.currentTarget here, not e.target, as we may be
|
|
31576
31638
|
// handling events that were originally targeted at a child element
|
|
31577
31639
|
// (such as the letter or score elements within a tile).
|
|
31578
31640
|
const dragged = e.currentTarget;
|
|
31641
|
+
// Prevent the click event (which fires after mouseup) from bubbling
|
|
31642
|
+
// to parent elements like DropTargetSquare
|
|
31643
|
+
dragged.addEventListener('click', (ev) => ev.stopPropagation(), {
|
|
31644
|
+
once: true,
|
|
31645
|
+
});
|
|
31579
31646
|
const coords = this.getEventCoordinates(e);
|
|
31647
|
+
this.startX = coords.x;
|
|
31648
|
+
this.startY = coords.y;
|
|
31580
31649
|
this.lastX = coords.x;
|
|
31581
31650
|
this.lastY = coords.y;
|
|
31582
31651
|
this.draggedElement = dragged;
|
|
31583
31652
|
this.dropHandler = dropHandler;
|
|
31653
|
+
this.clickHandler = clickHandler;
|
|
31584
31654
|
this.parentElement = dragged.parentElement;
|
|
31585
31655
|
this.parentRect = this.parentElement?.getBoundingClientRect() ?? null;
|
|
31656
|
+
// Create bound event handlers that properly assign 'this'
|
|
31657
|
+
this.boundEventHandlers = {
|
|
31658
|
+
drag: this.drag.bind(this),
|
|
31659
|
+
endDrag: this.endDrag.bind(this),
|
|
31660
|
+
};
|
|
31661
|
+
const { drag, endDrag } = this.boundEventHandlers;
|
|
31662
|
+
if (e instanceof MouseEvent) {
|
|
31663
|
+
document.addEventListener('mousemove', drag);
|
|
31664
|
+
document.addEventListener('mouseup', endDrag);
|
|
31665
|
+
}
|
|
31666
|
+
else if (e instanceof TouchEvent) {
|
|
31667
|
+
document.addEventListener('touchmove', drag, { passive: false });
|
|
31668
|
+
document.addEventListener('touchend', endDrag);
|
|
31669
|
+
}
|
|
31670
|
+
}
|
|
31671
|
+
initiateDrag(e) {
|
|
31672
|
+
// Called when movement exceeds the drag threshold
|
|
31673
|
+
e.preventDefault();
|
|
31674
|
+
this.dragStarted = true;
|
|
31675
|
+
const dragged = this.draggedElement;
|
|
31586
31676
|
// Find out the bounding rectangle of the element
|
|
31587
31677
|
// before starting to apply modifications
|
|
31588
31678
|
const rect = dragged.getBoundingClientRect();
|
|
@@ -31597,28 +31687,16 @@ class DragManager {
|
|
|
31597
31687
|
// Find out the dimensions of the element in its dragged state
|
|
31598
31688
|
const { offsetWidth, offsetHeight } = dragged;
|
|
31599
31689
|
// Offset of the click or touch within the dragged element
|
|
31600
|
-
|
|
31690
|
+
// Use the original start position for offset calculation
|
|
31691
|
+
this.offsetX =
|
|
31692
|
+
this.startX - rect.left + (offsetWidth - originalWidth) / 2;
|
|
31601
31693
|
this.offsetY =
|
|
31602
|
-
|
|
31694
|
+
this.startY - rect.top + (offsetHeight - originalHeight) / 2;
|
|
31603
31695
|
this.centerX = offsetWidth / 2;
|
|
31604
31696
|
this.centerY = offsetHeight / 2;
|
|
31605
|
-
// Create bound event handlers that properly assign 'this'
|
|
31606
|
-
this.boundEventHandlers = {
|
|
31607
|
-
drag: this.drag.bind(this),
|
|
31608
|
-
endDrag: this.endDrag.bind(this),
|
|
31609
|
-
};
|
|
31610
|
-
const { drag, endDrag } = this.boundEventHandlers;
|
|
31611
|
-
if (e instanceof MouseEvent) {
|
|
31612
|
-
document.addEventListener('mousemove', drag);
|
|
31613
|
-
document.addEventListener('mouseup', endDrag);
|
|
31614
|
-
}
|
|
31615
|
-
else if (e instanceof TouchEvent) {
|
|
31616
|
-
document.addEventListener('touchmove', drag, { passive: false });
|
|
31617
|
-
document.addEventListener('touchend', endDrag);
|
|
31618
|
-
}
|
|
31619
31697
|
// Do an initial position update, as the size of the dragged element
|
|
31620
31698
|
// may have changed, and it should remain centered
|
|
31621
|
-
this.updatePosition(
|
|
31699
|
+
this.updatePosition(this.lastX, this.lastY);
|
|
31622
31700
|
}
|
|
31623
31701
|
removeDragListeners() {
|
|
31624
31702
|
const { drag, endDrag } = this.boundEventHandlers;
|
|
@@ -31673,8 +31751,19 @@ class DragManager {
|
|
|
31673
31751
|
this.currentDropTarget = dropTarget;
|
|
31674
31752
|
}
|
|
31675
31753
|
drag(e) {
|
|
31676
|
-
e.
|
|
31754
|
+
e.stopPropagation();
|
|
31677
31755
|
const coords = this.getEventCoordinates(e);
|
|
31756
|
+
this.lastX = coords.x;
|
|
31757
|
+
this.lastY = coords.y;
|
|
31758
|
+
if (!this.dragStarted) {
|
|
31759
|
+
// Check if movement exceeds threshold
|
|
31760
|
+
const distance = Math.sqrt((coords.x - this.startX) ** 2 + (coords.y - this.startY) ** 2);
|
|
31761
|
+
if (distance >= DRAG_THRESHOLD) {
|
|
31762
|
+
this.initiateDrag(e);
|
|
31763
|
+
}
|
|
31764
|
+
return;
|
|
31765
|
+
}
|
|
31766
|
+
e.preventDefault();
|
|
31678
31767
|
// Update position for both mouse and touch events
|
|
31679
31768
|
this.updatePosition(coords.x, coords.y);
|
|
31680
31769
|
this.updateDropTarget(coords.x, coords.y);
|
|
@@ -31697,13 +31786,21 @@ class DragManager {
|
|
|
31697
31786
|
}
|
|
31698
31787
|
}
|
|
31699
31788
|
endDrag(e) {
|
|
31789
|
+
e.stopPropagation();
|
|
31790
|
+
this.removeDragListeners();
|
|
31791
|
+
if (!this.dragStarted) {
|
|
31792
|
+
// No drag occurred - treat as a click
|
|
31793
|
+
if (this.clickHandler) {
|
|
31794
|
+
this.clickHandler(this.draggedElement);
|
|
31795
|
+
}
|
|
31796
|
+
return;
|
|
31797
|
+
}
|
|
31700
31798
|
e.preventDefault();
|
|
31701
31799
|
const coords = this.getEventCoordinates(e);
|
|
31702
31800
|
const dropTarget = this.findDropTargetAtPoint(coords.x, coords.y);
|
|
31703
31801
|
if (this.currentDropTarget) {
|
|
31704
31802
|
this.currentDropTarget.classList.remove('over');
|
|
31705
31803
|
}
|
|
31706
|
-
this.removeDragListeners();
|
|
31707
31804
|
// Avoid flicker by hiding the element while we are manipulating its
|
|
31708
31805
|
// position in the DOM tree and completing the drop operation
|
|
31709
31806
|
this.draggedElement.style.visibility = 'hidden';
|
|
@@ -31717,8 +31814,9 @@ class DragManager {
|
|
|
31717
31814
|
});
|
|
31718
31815
|
}
|
|
31719
31816
|
}
|
|
31720
|
-
const startDrag = (e, dropHandler) => {
|
|
31721
|
-
|
|
31817
|
+
const startDrag = (e, dropHandler, clickHandler) => {
|
|
31818
|
+
e.stopPropagation();
|
|
31819
|
+
new DragManager(e, dropHandler, clickHandler);
|
|
31722
31820
|
};
|
|
31723
31821
|
|
|
31724
31822
|
/*
|
|
@@ -31735,11 +31833,10 @@ const startDrag = (e, dropHandler) => {
|
|
|
31735
31833
|
For further information, see https://github.com/mideind/Netskrafl
|
|
31736
31834
|
|
|
31737
31835
|
*/
|
|
31738
|
-
const
|
|
31739
|
-
|
|
31740
|
-
|
|
31741
|
-
|
|
31742
|
-
// Start a drag-and-drop process, for mouse or touch interaction
|
|
31836
|
+
const createDragHandler = (view, game, coord) => {
|
|
31837
|
+
return (ev) => {
|
|
31838
|
+
// Start a drag-and-drop process, for mouse or touch interaction.
|
|
31839
|
+
// If the user clicks without dragging, toggle tile selection.
|
|
31743
31840
|
startDrag(ev, (_, target) => {
|
|
31744
31841
|
// Drop handler
|
|
31745
31842
|
if (!game)
|
|
@@ -31768,15 +31865,28 @@ const Tile = (initialVnode) => {
|
|
|
31768
31865
|
console.error(e);
|
|
31769
31866
|
}
|
|
31770
31867
|
}
|
|
31868
|
+
}, () => {
|
|
31869
|
+
// Click handler: toggle tile selection
|
|
31870
|
+
if (!game)
|
|
31871
|
+
return;
|
|
31872
|
+
if (coord === game.selectedSq)
|
|
31873
|
+
// Clicking again: deselect
|
|
31874
|
+
game.selectedSq = null;
|
|
31875
|
+
else
|
|
31876
|
+
game.selectedSq = coord;
|
|
31877
|
+
m.redraw();
|
|
31771
31878
|
});
|
|
31772
31879
|
ev.redraw = false;
|
|
31773
31880
|
return false;
|
|
31774
31881
|
};
|
|
31882
|
+
};
|
|
31883
|
+
const Tile = () => {
|
|
31884
|
+
// Display a tile on the board or in the rack
|
|
31775
31885
|
return {
|
|
31776
31886
|
view: (vnode) => {
|
|
31887
|
+
const { view, game, coord, opponent } = vnode.attrs;
|
|
31777
31888
|
if (!game)
|
|
31778
31889
|
return undefined;
|
|
31779
|
-
const { opponent } = vnode.attrs;
|
|
31780
31890
|
const isRackTile = coord[0] === 'R';
|
|
31781
31891
|
// A single tile, on the board or in the rack
|
|
31782
31892
|
const t = game.tiles[coord];
|
|
@@ -31832,20 +31942,9 @@ const Tile = (initialVnode) => {
|
|
|
31832
31942
|
*/
|
|
31833
31943
|
}
|
|
31834
31944
|
if (t.draggable && game.allowDragDrop()) {
|
|
31945
|
+
const dragHandler = createDragHandler(view, game, coord);
|
|
31835
31946
|
attrs.onmousedown = dragHandler;
|
|
31836
31947
|
attrs.ontouchstart = dragHandler;
|
|
31837
|
-
/*
|
|
31838
|
-
attrs.onclick = (ev: MouseEvent) => {
|
|
31839
|
-
// When clicking a tile, make it selected (blinking)
|
|
31840
|
-
if (coord === game.selectedSq)
|
|
31841
|
-
// Clicking again: deselect
|
|
31842
|
-
game.selectedSq = null;
|
|
31843
|
-
else
|
|
31844
|
-
game.selectedSq = coord;
|
|
31845
|
-
ev.stopPropagation();
|
|
31846
|
-
return false;
|
|
31847
|
-
};
|
|
31848
|
-
*/
|
|
31849
31948
|
}
|
|
31850
31949
|
return m(classes.join('.'), attrs, [
|
|
31851
31950
|
t.letter === ' ' ? nbsp() : t.letter,
|
|
@@ -31902,7 +32001,7 @@ const ReviewTileSquare = {
|
|
|
31902
32001
|
const DropTargetSquare = {
|
|
31903
32002
|
// Return a td element that is a target for dropping tiles
|
|
31904
32003
|
view: (vnode) => {
|
|
31905
|
-
const { view, game, coord } = vnode.attrs;
|
|
32004
|
+
const { view, game, coord, isKeyboardTarget, keyboardDirection } = vnode.attrs;
|
|
31906
32005
|
if (!game)
|
|
31907
32006
|
return undefined;
|
|
31908
32007
|
let cls = game.squareClass(coord) || '';
|
|
@@ -31915,6 +32014,12 @@ const DropTargetSquare = {
|
|
|
31915
32014
|
if (coord === game.startSquare && game.localturn)
|
|
31916
32015
|
// Unoccupied start square, first move
|
|
31917
32016
|
cls += '.center';
|
|
32017
|
+
// Mark the cell as the keyboard target if applicable
|
|
32018
|
+
if (isKeyboardTarget) {
|
|
32019
|
+
cls += '.keyboard-target';
|
|
32020
|
+
if (keyboardDirection === 'V')
|
|
32021
|
+
cls += '.vertical';
|
|
32022
|
+
}
|
|
31918
32023
|
return m(`td.drop-target${cls}`, {
|
|
31919
32024
|
id: `sq_${coord}`,
|
|
31920
32025
|
onclick: (ev) => {
|
|
@@ -32101,6 +32206,8 @@ const Board = {
|
|
|
32101
32206
|
if (scale !== 1.0)
|
|
32102
32207
|
attrs.style = `transform: scale(${scale})`;
|
|
32103
32208
|
*/
|
|
32209
|
+
// Get the next keyboard target square for visual feedback
|
|
32210
|
+
const nextKeyboardSquare = !review ? game.getNextKeyboardSquare() : null;
|
|
32104
32211
|
function colid() {
|
|
32105
32212
|
// The column identifier row
|
|
32106
32213
|
const r = [];
|
|
@@ -32140,6 +32247,8 @@ const Board = {
|
|
|
32140
32247
|
game,
|
|
32141
32248
|
key: coord,
|
|
32142
32249
|
coord: coord,
|
|
32250
|
+
isKeyboardTarget: coord === nextKeyboardSquare,
|
|
32251
|
+
keyboardDirection: game?.getEffectiveKeyboardDirection(),
|
|
32143
32252
|
}));
|
|
32144
32253
|
}
|
|
32145
32254
|
return m('tr', r);
|
|
@@ -33473,6 +33582,80 @@ const currentMoveState = (riddle) => {
|
|
|
33473
33582
|
return { selectedMoves, bestMove };
|
|
33474
33583
|
};
|
|
33475
33584
|
|
|
33585
|
+
/*
|
|
33586
|
+
|
|
33587
|
+
Keyboard.ts
|
|
33588
|
+
|
|
33589
|
+
Keyboard event handling for tile placement
|
|
33590
|
+
|
|
33591
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
33592
|
+
Author: Vilhjálmur Þorsteinsson
|
|
33593
|
+
|
|
33594
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
33595
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
33596
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
33597
|
+
|
|
33598
|
+
*/
|
|
33599
|
+
function handleGameKeydown(game, view, ev) {
|
|
33600
|
+
// Skip if the target is an input element (text fields, textareas, etc.)
|
|
33601
|
+
const target = ev.target;
|
|
33602
|
+
if (target.tagName === 'INPUT' ||
|
|
33603
|
+
target.tagName === 'TEXTAREA' ||
|
|
33604
|
+
target.tagName === 'SELECT' ||
|
|
33605
|
+
target.isContentEditable) {
|
|
33606
|
+
return;
|
|
33607
|
+
}
|
|
33608
|
+
// Skip if dialog is showing (except for blank dialog which has its own handler)
|
|
33609
|
+
if (game.showingDialog)
|
|
33610
|
+
return;
|
|
33611
|
+
// Skip if blank tile dialog is active (it has its own keyboard handler)
|
|
33612
|
+
if (game.askingForBlank !== null)
|
|
33613
|
+
return;
|
|
33614
|
+
const { key } = ev;
|
|
33615
|
+
// Arrow keys: set direction
|
|
33616
|
+
if (key === 'ArrowRight') {
|
|
33617
|
+
ev.preventDefault();
|
|
33618
|
+
game.setKeyboardDirection('H');
|
|
33619
|
+
m.redraw();
|
|
33620
|
+
return;
|
|
33621
|
+
}
|
|
33622
|
+
if (key === 'ArrowDown') {
|
|
33623
|
+
ev.preventDefault();
|
|
33624
|
+
game.setKeyboardDirection('V');
|
|
33625
|
+
m.redraw();
|
|
33626
|
+
return;
|
|
33627
|
+
}
|
|
33628
|
+
// Backspace: undo last keyboard tile
|
|
33629
|
+
if (key === 'Backspace') {
|
|
33630
|
+
ev.preventDefault();
|
|
33631
|
+
if (game.undoLastKeyboardTile()) {
|
|
33632
|
+
view.updateScale();
|
|
33633
|
+
m.redraw();
|
|
33634
|
+
}
|
|
33635
|
+
return;
|
|
33636
|
+
}
|
|
33637
|
+
// Escape: recall all tiles to rack
|
|
33638
|
+
if (key === 'Escape') {
|
|
33639
|
+
ev.preventDefault();
|
|
33640
|
+
game.resetRack();
|
|
33641
|
+
view.updateScale();
|
|
33642
|
+
m.redraw();
|
|
33643
|
+
return;
|
|
33644
|
+
}
|
|
33645
|
+
// Letter keys: place tile
|
|
33646
|
+
if (key.length === 1) {
|
|
33647
|
+
const letter = key.toLowerCase();
|
|
33648
|
+
if (game.alphabet.includes(letter)) {
|
|
33649
|
+
ev.preventDefault();
|
|
33650
|
+
if (game.placeKeyboardTile(letter)) {
|
|
33651
|
+
view.updateScale();
|
|
33652
|
+
m.redraw();
|
|
33653
|
+
}
|
|
33654
|
+
return;
|
|
33655
|
+
}
|
|
33656
|
+
}
|
|
33657
|
+
}
|
|
33658
|
+
|
|
33476
33659
|
/*
|
|
33477
33660
|
|
|
33478
33661
|
Localstorage.ts
|
|
@@ -33798,8 +33981,11 @@ class BaseGame {
|
|
|
33798
33981
|
// UI state
|
|
33799
33982
|
this.showingDialog = null;
|
|
33800
33983
|
this.selectedSq = null;
|
|
33801
|
-
this.sel = 'movelist';
|
|
33984
|
+
this.sel = 'movelist'; // Selected tab
|
|
33802
33985
|
this.askingForBlank = null;
|
|
33986
|
+
// Keyboard tile placement state
|
|
33987
|
+
this.keyboardDirection = 'H'; // Default to horizontal
|
|
33988
|
+
this.keyboardPlacedTiles = []; // Stack of tiles placed via keyboard
|
|
33803
33989
|
// Local storage
|
|
33804
33990
|
this.localStorage = null;
|
|
33805
33991
|
this.uuid = uuid;
|
|
@@ -33913,6 +34099,13 @@ class BaseGame {
|
|
|
33913
34099
|
this._moveTile(from, to);
|
|
33914
34100
|
// Clear error message, if any
|
|
33915
34101
|
this.currentError = this.currentMessage = null;
|
|
34102
|
+
// If placing the first tile on the board, set default direction
|
|
34103
|
+
if (from[0] === 'R' && to[0] !== 'R') {
|
|
34104
|
+
const placed = this.tilesPlaced();
|
|
34105
|
+
if (placed.length === 1) {
|
|
34106
|
+
this.setDefaultDirection(to);
|
|
34107
|
+
}
|
|
34108
|
+
}
|
|
33916
34109
|
// Update the current word score
|
|
33917
34110
|
this.updateScore();
|
|
33918
34111
|
// Update the local storage
|
|
@@ -33937,12 +34130,12 @@ class BaseGame {
|
|
|
33937
34130
|
// Return a list of coordinates of tiles that the user has
|
|
33938
34131
|
// placed on the board by dragging from the rack
|
|
33939
34132
|
const r = [];
|
|
33940
|
-
for (const sq
|
|
33941
|
-
if (
|
|
33942
|
-
sq[0] !== 'R' &&
|
|
33943
|
-
this.tiles[sq].draggable)
|
|
34133
|
+
for (const sq of Object.keys(this.tiles)) {
|
|
34134
|
+
if (sq[0] !== 'R' && this.tiles[sq].draggable) {
|
|
33944
34135
|
// Found a non-rack tile that is not glued to the board
|
|
33945
34136
|
r.push(sq);
|
|
34137
|
+
}
|
|
34138
|
+
}
|
|
33946
34139
|
return r;
|
|
33947
34140
|
}
|
|
33948
34141
|
resetRack() {
|
|
@@ -33972,6 +34165,8 @@ class BaseGame {
|
|
|
33972
34165
|
}
|
|
33973
34166
|
// Reset current error message, if any
|
|
33974
34167
|
this.currentError = null;
|
|
34168
|
+
// Reset keyboard placement state
|
|
34169
|
+
this.resetKeyboardState();
|
|
33975
34170
|
}
|
|
33976
34171
|
rescrambleRack() {
|
|
33977
34172
|
// Reorder the rack randomly. Bound to the Backspace key.
|
|
@@ -34245,36 +34440,39 @@ class BaseGame {
|
|
|
34245
34440
|
// The saved destination square is empty:
|
|
34246
34441
|
// find the tile in the saved rack and move it there
|
|
34247
34442
|
const tile = savedTiles[i].tile;
|
|
34248
|
-
for (const sq
|
|
34249
|
-
if (
|
|
34250
|
-
rackTiles[sq].tile === tile.charAt(0)) {
|
|
34443
|
+
for (const sq of Object.keys(rackTiles)) {
|
|
34444
|
+
if (rackTiles[sq].tile === tile.charAt(0)) {
|
|
34251
34445
|
// Found the tile (or its equivalent) in the rack: move it
|
|
34252
|
-
if (tile.charAt(0) === '?')
|
|
34253
|
-
if (saved_sq.charAt(0) === 'R')
|
|
34446
|
+
if (tile.charAt(0) === '?') {
|
|
34447
|
+
if (saved_sq.charAt(0) === 'R') {
|
|
34254
34448
|
// Going to the rack: no associated letter
|
|
34255
34449
|
rackTiles[sq].letter = ' ';
|
|
34256
|
-
|
|
34257
|
-
|
|
34258
|
-
|
|
34450
|
+
}
|
|
34451
|
+
else {
|
|
34452
|
+
// Going to a board square: associate the originally
|
|
34453
|
+
// chosen and saved letter
|
|
34259
34454
|
rackTiles[sq].letter = tile.charAt(1);
|
|
34455
|
+
}
|
|
34456
|
+
}
|
|
34260
34457
|
// ...and assign it
|
|
34261
34458
|
this.tiles[saved_sq] = rackTiles[sq];
|
|
34262
34459
|
delete rackTiles[sq];
|
|
34263
34460
|
break;
|
|
34264
34461
|
}
|
|
34462
|
+
}
|
|
34265
34463
|
}
|
|
34266
34464
|
}
|
|
34267
34465
|
// Allocate any remaining tiles to free slots in the rack
|
|
34268
34466
|
let j = 1;
|
|
34269
|
-
for (const sq
|
|
34270
|
-
|
|
34271
|
-
|
|
34272
|
-
|
|
34273
|
-
|
|
34274
|
-
|
|
34275
|
-
|
|
34276
|
-
this.tiles[`R${j}`] = rackTiles[sq];
|
|
34467
|
+
for (const sq of Object.keys(rackTiles)) {
|
|
34468
|
+
// Look for a free slot in the rack
|
|
34469
|
+
while (`R${j}` in this.tiles)
|
|
34470
|
+
j++;
|
|
34471
|
+
if (j <= RACK_SIZE) {
|
|
34472
|
+
// Should always be true unless something is very wrong
|
|
34473
|
+
this.tiles[`R${j}`] = rackTiles[sq];
|
|
34277
34474
|
}
|
|
34475
|
+
}
|
|
34278
34476
|
// The local storage may have been cleared before calling
|
|
34279
34477
|
// restoreTiles() so we must ensure that it is updated
|
|
34280
34478
|
this.saveTiles();
|
|
@@ -34284,6 +34482,192 @@ class BaseGame {
|
|
|
34284
34482
|
cleanup() {
|
|
34285
34483
|
// Base cleanup - can be overridden by subclasses
|
|
34286
34484
|
}
|
|
34485
|
+
// Keyboard tile placement methods
|
|
34486
|
+
getLockedDirection() {
|
|
34487
|
+
// Check if direction is locked by 2+ tiles in a row or column.
|
|
34488
|
+
// Returns 'H' if locked horizontal, 'V' if locked vertical, null if not locked.
|
|
34489
|
+
const placed = this.tilesPlaced();
|
|
34490
|
+
if (placed.length < 2)
|
|
34491
|
+
return null;
|
|
34492
|
+
const rows = new Set();
|
|
34493
|
+
const cols = new Set();
|
|
34494
|
+
for (const sq of placed) {
|
|
34495
|
+
rows.add(sq.charAt(0));
|
|
34496
|
+
cols.add(parseInt(sq.slice(1), 10));
|
|
34497
|
+
}
|
|
34498
|
+
// If all tiles are in the same row, lock to horizontal
|
|
34499
|
+
if (rows.size === 1 && cols.size > 1) {
|
|
34500
|
+
return 'H';
|
|
34501
|
+
}
|
|
34502
|
+
// If all tiles are in the same column, lock to vertical
|
|
34503
|
+
if (cols.size === 1 && rows.size > 1) {
|
|
34504
|
+
return 'V';
|
|
34505
|
+
}
|
|
34506
|
+
return null;
|
|
34507
|
+
}
|
|
34508
|
+
setKeyboardDirection(direction) {
|
|
34509
|
+
// Only allow direction change if not locked by placed tiles
|
|
34510
|
+
const locked = this.getLockedDirection();
|
|
34511
|
+
if (locked === null) {
|
|
34512
|
+
this.keyboardDirection = direction;
|
|
34513
|
+
}
|
|
34514
|
+
}
|
|
34515
|
+
getEffectiveKeyboardDirection() {
|
|
34516
|
+
// Return the effective direction, considering any lock from placed tiles
|
|
34517
|
+
return this.getLockedDirection() ?? this.keyboardDirection;
|
|
34518
|
+
}
|
|
34519
|
+
findNextEmptySquare(startRow, startCol, direction) {
|
|
34520
|
+
// Find the next empty square starting from the given position,
|
|
34521
|
+
// moving in the specified direction. Skips over all occupied squares
|
|
34522
|
+
// (both fixed tiles and tiles we've placed).
|
|
34523
|
+
const dx = direction === 'H' ? 1 : 0;
|
|
34524
|
+
const dy = direction === 'V' ? 1 : 0;
|
|
34525
|
+
let row = startRow;
|
|
34526
|
+
let col = startCol;
|
|
34527
|
+
while (row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE) {
|
|
34528
|
+
const sq = coord(row, col);
|
|
34529
|
+
if (sq && !(sq in this.tiles)) {
|
|
34530
|
+
// Found an empty square
|
|
34531
|
+
return sq;
|
|
34532
|
+
}
|
|
34533
|
+
// Square is occupied - continue scanning
|
|
34534
|
+
row += dy;
|
|
34535
|
+
col += dx;
|
|
34536
|
+
}
|
|
34537
|
+
// Reached board edge with no empty square found
|
|
34538
|
+
return null;
|
|
34539
|
+
}
|
|
34540
|
+
canContinueInDirection(sq, direction) {
|
|
34541
|
+
// Check if there's any empty square in the given direction from sq
|
|
34542
|
+
const vec = toVector(sq);
|
|
34543
|
+
const dx = direction === 'H' ? 1 : 0;
|
|
34544
|
+
const dy = direction === 'V' ? 1 : 0;
|
|
34545
|
+
return (this.findNextEmptySquare(vec.row + dy, vec.col + dx, direction) !==
|
|
34546
|
+
null);
|
|
34547
|
+
}
|
|
34548
|
+
setDefaultDirection(sq) {
|
|
34549
|
+
// When placing the first tile, set direction based on available space
|
|
34550
|
+
// Default to horizontal if possible, otherwise vertical
|
|
34551
|
+
if (this.canContinueInDirection(sq, 'H')) {
|
|
34552
|
+
this.keyboardDirection = 'H';
|
|
34553
|
+
}
|
|
34554
|
+
else {
|
|
34555
|
+
this.keyboardDirection = 'V';
|
|
34556
|
+
}
|
|
34557
|
+
}
|
|
34558
|
+
getNextKeyboardSquare() {
|
|
34559
|
+
// Get the next target square for keyboard tile placement
|
|
34560
|
+
const placed = this.tilesPlaced();
|
|
34561
|
+
// Don't show target indicator if no tiles have been placed yet,
|
|
34562
|
+
// since we don't know where the user wants to start their move
|
|
34563
|
+
if (placed.length === 0) {
|
|
34564
|
+
return null;
|
|
34565
|
+
}
|
|
34566
|
+
// Don't show target indicator if the rack is empty (no tiles left to place)
|
|
34567
|
+
let rackEmpty = true;
|
|
34568
|
+
for (let i = 1; i <= RACK_SIZE; i++) {
|
|
34569
|
+
if (`R${i}` in this.tiles) {
|
|
34570
|
+
rackEmpty = false;
|
|
34571
|
+
break;
|
|
34572
|
+
}
|
|
34573
|
+
}
|
|
34574
|
+
if (rackEmpty) {
|
|
34575
|
+
return null;
|
|
34576
|
+
}
|
|
34577
|
+
// Use locked direction if set, otherwise use user's preferred direction
|
|
34578
|
+
const locked = this.getLockedDirection();
|
|
34579
|
+
const direction = locked ?? this.keyboardDirection;
|
|
34580
|
+
// Find the rightmost (horizontal) or bottommost (vertical) placed tile
|
|
34581
|
+
let maxRow = -1;
|
|
34582
|
+
let maxCol = -1;
|
|
34583
|
+
for (const sq of placed) {
|
|
34584
|
+
const row = ROWIDS.indexOf(sq.charAt(0));
|
|
34585
|
+
const col = parseInt(sq.slice(1), 10) - 1;
|
|
34586
|
+
if (direction === 'H') {
|
|
34587
|
+
if (col > maxCol || (col === maxCol && row > maxRow)) {
|
|
34588
|
+
maxCol = col;
|
|
34589
|
+
maxRow = row;
|
|
34590
|
+
}
|
|
34591
|
+
}
|
|
34592
|
+
else {
|
|
34593
|
+
if (row > maxRow || (row === maxRow && col > maxCol)) {
|
|
34594
|
+
maxRow = row;
|
|
34595
|
+
maxCol = col;
|
|
34596
|
+
}
|
|
34597
|
+
}
|
|
34598
|
+
}
|
|
34599
|
+
// Find the next empty square in the direction
|
|
34600
|
+
const dx = direction === 'H' ? 1 : 0;
|
|
34601
|
+
const dy = direction === 'V' ? 1 : 0;
|
|
34602
|
+
return this.findNextEmptySquare(maxRow + dy, maxCol + dx, direction);
|
|
34603
|
+
}
|
|
34604
|
+
findRackTileForLetter(letter) {
|
|
34605
|
+
// Find a rack tile matching the given letter
|
|
34606
|
+
// Prefers real tiles over blanks
|
|
34607
|
+
let blankSlot = null;
|
|
34608
|
+
for (let i = 1; i <= RACK_SIZE; i++) {
|
|
34609
|
+
const slot = `R${i}`;
|
|
34610
|
+
if (slot in this.tiles) {
|
|
34611
|
+
const tile = this.tiles[slot];
|
|
34612
|
+
if (tile.tile === letter) {
|
|
34613
|
+
// Found an exact match
|
|
34614
|
+
return slot;
|
|
34615
|
+
}
|
|
34616
|
+
if (tile.tile === '?' && blankSlot === null) {
|
|
34617
|
+
// Remember the first blank we find
|
|
34618
|
+
blankSlot = slot;
|
|
34619
|
+
}
|
|
34620
|
+
}
|
|
34621
|
+
}
|
|
34622
|
+
// Return blank if no exact match found
|
|
34623
|
+
return blankSlot;
|
|
34624
|
+
}
|
|
34625
|
+
placeKeyboardTile(letter) {
|
|
34626
|
+
// Place a tile via keyboard
|
|
34627
|
+
const targetSquare = this.getNextKeyboardSquare();
|
|
34628
|
+
if (!targetSquare)
|
|
34629
|
+
return false;
|
|
34630
|
+
const rackSlot = this.findRackTileForLetter(letter);
|
|
34631
|
+
if (!rackSlot)
|
|
34632
|
+
return false;
|
|
34633
|
+
const tile = this.tiles[rackSlot];
|
|
34634
|
+
// If using a blank tile, assign the letter
|
|
34635
|
+
if (tile.tile === '?') {
|
|
34636
|
+
tile.letter = letter;
|
|
34637
|
+
}
|
|
34638
|
+
// Move the tile to the target square
|
|
34639
|
+
this.moveTile(rackSlot, targetSquare);
|
|
34640
|
+
// Track this tile for backspace functionality
|
|
34641
|
+
this.keyboardPlacedTiles.push(targetSquare);
|
|
34642
|
+
return true;
|
|
34643
|
+
}
|
|
34644
|
+
undoLastKeyboardTile() {
|
|
34645
|
+
// Undo the last keyboard-placed tile
|
|
34646
|
+
if (this.keyboardPlacedTiles.length === 0)
|
|
34647
|
+
return false;
|
|
34648
|
+
const lastSquare = this.keyboardPlacedTiles.pop();
|
|
34649
|
+
if (!lastSquare || !(lastSquare in this.tiles))
|
|
34650
|
+
return false;
|
|
34651
|
+
// Find a free slot in the rack
|
|
34652
|
+
let freeSlot = null;
|
|
34653
|
+
for (let i = 1; i <= RACK_SIZE; i++) {
|
|
34654
|
+
const slot = `R${i}`;
|
|
34655
|
+
if (!(slot in this.tiles)) {
|
|
34656
|
+
freeSlot = slot;
|
|
34657
|
+
break;
|
|
34658
|
+
}
|
|
34659
|
+
}
|
|
34660
|
+
if (!freeSlot)
|
|
34661
|
+
return false;
|
|
34662
|
+
// Move the tile back to the rack
|
|
34663
|
+
this.moveTile(lastSquare, freeSlot);
|
|
34664
|
+
return true;
|
|
34665
|
+
}
|
|
34666
|
+
resetKeyboardState() {
|
|
34667
|
+
// Reset keyboard placement state
|
|
34668
|
+
this.keyboardDirection = 'H';
|
|
34669
|
+
this.keyboardPlacedTiles = [];
|
|
34670
|
+
}
|
|
34287
34671
|
}
|
|
34288
34672
|
|
|
34289
34673
|
/*
|
|
@@ -34661,9 +35045,9 @@ class Game extends BaseGame {
|
|
|
34661
35045
|
// Remember if the game was already won before this update
|
|
34662
35046
|
const wasWon = this.congratulate;
|
|
34663
35047
|
// Stop highlighting the previous opponent move, if any
|
|
34664
|
-
for (const sq
|
|
34665
|
-
|
|
34666
|
-
|
|
35048
|
+
for (const sq of Object.keys(this.tiles)) {
|
|
35049
|
+
this.tiles[sq].freshtile = false;
|
|
35050
|
+
}
|
|
34667
35051
|
this.init(srvGame);
|
|
34668
35052
|
if (this.currentError === null) {
|
|
34669
35053
|
if (this.succ_chall) {
|
|
@@ -37063,7 +37447,7 @@ const MoveListItem = () => {
|
|
|
37063
37447
|
if (tile === '?')
|
|
37064
37448
|
continue;
|
|
37065
37449
|
const sq = coord(row, col);
|
|
37066
|
-
if (sq &&
|
|
37450
|
+
if (sq && sq in game.tiles)
|
|
37067
37451
|
game.tiles[sq].highlight = show ? playerColor : undefined;
|
|
37068
37452
|
col += vec.dx;
|
|
37069
37453
|
row += vec.dy;
|
|
@@ -40508,6 +40892,11 @@ class View {
|
|
|
40508
40892
|
if (this.selectedTab === sel)
|
|
40509
40893
|
return false;
|
|
40510
40894
|
this.selectedTab = sel;
|
|
40895
|
+
// When switching to a non-chat tab, focus the container
|
|
40896
|
+
// to restore keyboard input for tile placement
|
|
40897
|
+
if (sel !== 'chat') {
|
|
40898
|
+
setDefaultFocus();
|
|
40899
|
+
}
|
|
40511
40900
|
return true;
|
|
40512
40901
|
}
|
|
40513
40902
|
// Globally available view functions
|
|
@@ -40664,6 +41053,13 @@ async function main$1(state, container) {
|
|
|
40664
41053
|
m.mount(container, {
|
|
40665
41054
|
view: () => m(GataDagsins$1, { view, date: validDate, locale }),
|
|
40666
41055
|
});
|
|
41056
|
+
// Set up keyboard handling on the container element
|
|
41057
|
+
setKeyboardHandler((ev) => {
|
|
41058
|
+
const riddle = model.riddle;
|
|
41059
|
+
if (riddle) {
|
|
41060
|
+
handleGameKeydown(riddle, view, ev);
|
|
41061
|
+
}
|
|
41062
|
+
});
|
|
40667
41063
|
}
|
|
40668
41064
|
catch (e) {
|
|
40669
41065
|
console.error('Exception during initialization: ', e);
|
|
@@ -40789,6 +41185,13 @@ async function main(state, container) {
|
|
|
40789
41185
|
const model = new Model(settings, state);
|
|
40790
41186
|
const actions = new Actions(model);
|
|
40791
41187
|
const view = new View(actions);
|
|
41188
|
+
// Set up keyboard handling on the container element
|
|
41189
|
+
setKeyboardHandler((ev) => {
|
|
41190
|
+
const game = model.game;
|
|
41191
|
+
if (game) {
|
|
41192
|
+
handleGameKeydown(game, view, ev);
|
|
41193
|
+
}
|
|
41194
|
+
});
|
|
40792
41195
|
// Run the Mithril router
|
|
40793
41196
|
const routeResolver = createRouteResolver(actions, view);
|
|
40794
41197
|
m.route(container, settings.defaultRoute, routeResolver);
|
|
@@ -40799,60 +41202,34 @@ async function main(state, container) {
|
|
|
40799
41202
|
}
|
|
40800
41203
|
return 'success';
|
|
40801
41204
|
}
|
|
41205
|
+
function unmount(container) {
|
|
41206
|
+
// Unmount the Mithril UI completely
|
|
41207
|
+
// First unmount via m.mount to clean up Mithril's internal state
|
|
41208
|
+
m.mount(container, null);
|
|
41209
|
+
// Then clear the container to ensure a clean slate for next mount
|
|
41210
|
+
container.innerHTML = '';
|
|
41211
|
+
}
|
|
40802
41212
|
|
|
40803
|
-
const mountForUser =
|
|
40804
|
-
//
|
|
40805
|
-
// for the user specified in the state object
|
|
41213
|
+
const mountForUser = (state, container) => {
|
|
41214
|
+
// Mount a Netskrafl UI directly into the container
|
|
40806
41215
|
const { userEmail } = state;
|
|
40807
41216
|
if (!userEmail) {
|
|
40808
|
-
|
|
40809
|
-
throw new Error('No user specified for Netskrafl UI');
|
|
41217
|
+
return Promise.reject(new Error('No user specified for Netskrafl UI'));
|
|
40810
41218
|
}
|
|
40811
|
-
|
|
40812
|
-
|
|
40813
|
-
|
|
40814
|
-
|
|
40815
|
-
|
|
40816
|
-
return existing;
|
|
40817
|
-
}
|
|
40818
|
-
// Create a new div element to hold the UI
|
|
40819
|
-
const root = document.createElement('div');
|
|
40820
|
-
root.id = elemId;
|
|
40821
|
-
root.className = 'netskrafl-loading';
|
|
40822
|
-
// Attach the partially-mounted div to the document body
|
|
40823
|
-
// as a placeholder while the UI is being mounted
|
|
40824
|
-
document.body.appendChild(root);
|
|
40825
|
-
const loginResult = await main(state, root);
|
|
40826
|
-
if (loginResult === 'success') {
|
|
40827
|
-
// The UI was successfully mounted
|
|
40828
|
-
root.className = 'netskrafl-user';
|
|
40829
|
-
return root;
|
|
40830
|
-
}
|
|
40831
|
-
// console.error("Failed to mount Netskrafl UI for user", userEmail);
|
|
40832
|
-
throw new Error('Failed to mount Netskrafl UI');
|
|
41219
|
+
return main(state, container).then((loginResult) => {
|
|
41220
|
+
if (loginResult !== 'success') {
|
|
41221
|
+
throw new Error('Failed to mount Netskrafl UI');
|
|
41222
|
+
}
|
|
41223
|
+
});
|
|
40833
41224
|
};
|
|
40834
41225
|
const NetskraflImpl = ({ state, tokenExpired }) => {
|
|
40835
|
-
const ref = React.
|
|
41226
|
+
const ref = React.useRef(null);
|
|
41227
|
+
const mountedRef = React.useRef(false);
|
|
40836
41228
|
const completeState = makeGlobalState({ ...state, tokenExpired });
|
|
40837
41229
|
const { userEmail } = completeState;
|
|
40838
|
-
/*
|
|
40839
|
-
useEffect(() => {
|
|
40840
|
-
// Check whether the stylesheet is already present
|
|
40841
|
-
if (document.getElementById(CSS_LINK_ID)) return;
|
|
40842
|
-
// Load and link the stylesheet into the document head
|
|
40843
|
-
const link = document.createElement("link");
|
|
40844
|
-
const styleUrl = `${window.location.origin}/static/css/netskrafl.css`;
|
|
40845
|
-
link.id = CSS_LINK_ID;
|
|
40846
|
-
link.rel = "stylesheet";
|
|
40847
|
-
link.type = "text/css";
|
|
40848
|
-
link.href = styleUrl;
|
|
40849
|
-
document.head.appendChild(link);
|
|
40850
|
-
// We don't bother to remove the stylesheet when the component is unmounted
|
|
40851
|
-
}, []);
|
|
40852
|
-
*/
|
|
40853
41230
|
// biome-ignore lint/correctness/useExhaustiveDependencies: The dependency is only on userEmail
|
|
40854
41231
|
useEffect(() => {
|
|
40855
|
-
//
|
|
41232
|
+
// Mount the Netskrafl (Mithril) UI for the current user
|
|
40856
41233
|
if (!userEmail)
|
|
40857
41234
|
return;
|
|
40858
41235
|
const container = ref.current;
|
|
@@ -40860,36 +41237,19 @@ const NetskraflImpl = ({ state, tokenExpired }) => {
|
|
|
40860
41237
|
console.error('No container for Netskrafl UI');
|
|
40861
41238
|
return;
|
|
40862
41239
|
}
|
|
40863
|
-
|
|
40864
|
-
|
|
40865
|
-
|
|
40866
|
-
|
|
40867
|
-
}
|
|
40868
|
-
|
|
40869
|
-
|
|
40870
|
-
|
|
40871
|
-
// instead of any previous children
|
|
40872
|
-
const container = ref.current;
|
|
40873
|
-
if (container) {
|
|
40874
|
-
container.innerHTML = '';
|
|
40875
|
-
container.appendChild(div);
|
|
40876
|
-
}
|
|
40877
|
-
});
|
|
40878
|
-
}
|
|
40879
|
-
catch (_) {
|
|
40880
|
-
console.error('Failed to mount Netskrafl UI for user', userEmail);
|
|
40881
|
-
const container = document.getElementById('netskrafl-container');
|
|
40882
|
-
if (container)
|
|
40883
|
-
container.innerHTML = '';
|
|
40884
|
-
}
|
|
41240
|
+
// Mount Mithril directly into the container
|
|
41241
|
+
mountForUser(completeState, container)
|
|
41242
|
+
.then(() => {
|
|
41243
|
+
mountedRef.current = true;
|
|
41244
|
+
})
|
|
41245
|
+
.catch((error) => {
|
|
41246
|
+
console.error('Failed to mount Netskrafl UI:', error);
|
|
41247
|
+
});
|
|
40885
41248
|
return () => {
|
|
40886
|
-
//
|
|
40887
|
-
|
|
40888
|
-
|
|
40889
|
-
|
|
40890
|
-
const div = container?.firstElementChild;
|
|
40891
|
-
if (div?.id === elemId) {
|
|
40892
|
-
document.body.appendChild(div);
|
|
41249
|
+
// Only unmount if mounting actually succeeded
|
|
41250
|
+
if (mountedRef.current) {
|
|
41251
|
+
unmount(container);
|
|
41252
|
+
mountedRef.current = false;
|
|
40893
41253
|
}
|
|
40894
41254
|
};
|
|
40895
41255
|
}, [userEmail]);
|