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