@mideind/netskrafl-react 2.0.1 → 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 +454 -71
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/css/netskrafl.css +131 -3
- package/dist/esm/index.js +454 -71
- 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,12 +31258,20 @@ 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
|
+
}
|
|
31231
31269
|
function handleKeydown(game, ev) {
|
|
31232
31270
|
// Escape key cancels the dialog
|
|
31233
31271
|
let { key } = ev;
|
|
31234
31272
|
if (key === 'Escape') {
|
|
31235
31273
|
ev.preventDefault();
|
|
31236
|
-
game
|
|
31274
|
+
cancel(game);
|
|
31237
31275
|
return;
|
|
31238
31276
|
}
|
|
31239
31277
|
if (key.length === 1) {
|
|
@@ -31241,7 +31279,7 @@ const BlankDialog = () => {
|
|
|
31241
31279
|
key = key.toLowerCase();
|
|
31242
31280
|
if (game.alphabet.includes(key)) {
|
|
31243
31281
|
ev.preventDefault();
|
|
31244
|
-
game
|
|
31282
|
+
place(game, key);
|
|
31245
31283
|
}
|
|
31246
31284
|
}
|
|
31247
31285
|
}
|
|
@@ -31258,8 +31296,8 @@ const BlankDialog = () => {
|
|
|
31258
31296
|
const letter = legalLetters[ix++];
|
|
31259
31297
|
c.push(m('td', {
|
|
31260
31298
|
onclick: (ev) => {
|
|
31261
|
-
game.placeBlank(letter);
|
|
31262
31299
|
ev.preventDefault();
|
|
31300
|
+
place(game, letter);
|
|
31263
31301
|
},
|
|
31264
31302
|
onmouseover: buttonOver,
|
|
31265
31303
|
onmouseout: buttonOut,
|
|
@@ -31289,7 +31327,7 @@ const BlankDialog = () => {
|
|
|
31289
31327
|
title: ts('Hætta við'),
|
|
31290
31328
|
onclick: (ev) => {
|
|
31291
31329
|
ev.preventDefault();
|
|
31292
|
-
game
|
|
31330
|
+
cancel(game);
|
|
31293
31331
|
},
|
|
31294
31332
|
}, glyph('remove')),
|
|
31295
31333
|
]));
|
|
@@ -31564,6 +31602,8 @@ const Buttons = {
|
|
|
31564
31602
|
drag-and-drop does not offer enough flexibility for our requirements.
|
|
31565
31603
|
|
|
31566
31604
|
*/
|
|
31605
|
+
// Minimum movement in pixels before a mousedown becomes a drag
|
|
31606
|
+
const DRAG_THRESHOLD = 10;
|
|
31567
31607
|
class DragManager {
|
|
31568
31608
|
getEventCoordinates(e) {
|
|
31569
31609
|
if (e instanceof MouseEvent) {
|
|
@@ -31583,28 +31623,58 @@ class DragManager {
|
|
|
31583
31623
|
p.y >= rect.top &&
|
|
31584
31624
|
p.y < rect.bottom);
|
|
31585
31625
|
}
|
|
31586
|
-
constructor(e, dropHandler) {
|
|
31626
|
+
constructor(e, dropHandler, clickHandler) {
|
|
31587
31627
|
this.parentElement = null;
|
|
31588
31628
|
this.parentRect = null;
|
|
31589
31629
|
this.offsetX = 0;
|
|
31590
31630
|
this.offsetY = 0;
|
|
31591
31631
|
this.centerX = 0;
|
|
31592
31632
|
this.centerY = 0;
|
|
31633
|
+
this.startX = 0;
|
|
31634
|
+
this.startY = 0;
|
|
31593
31635
|
this.lastX = 0;
|
|
31594
31636
|
this.lastY = 0;
|
|
31595
31637
|
this.currentDropTarget = null;
|
|
31596
|
-
|
|
31638
|
+
this.dragStarted = false;
|
|
31597
31639
|
// Note: We use e.currentTarget here, not e.target, as we may be
|
|
31598
31640
|
// handling events that were originally targeted at a child element
|
|
31599
31641
|
// (such as the letter or score elements within a tile).
|
|
31600
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
|
+
});
|
|
31601
31648
|
const coords = this.getEventCoordinates(e);
|
|
31649
|
+
this.startX = coords.x;
|
|
31650
|
+
this.startY = coords.y;
|
|
31602
31651
|
this.lastX = coords.x;
|
|
31603
31652
|
this.lastY = coords.y;
|
|
31604
31653
|
this.draggedElement = dragged;
|
|
31605
31654
|
this.dropHandler = dropHandler;
|
|
31655
|
+
this.clickHandler = clickHandler;
|
|
31606
31656
|
this.parentElement = dragged.parentElement;
|
|
31607
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;
|
|
31608
31678
|
// Find out the bounding rectangle of the element
|
|
31609
31679
|
// before starting to apply modifications
|
|
31610
31680
|
const rect = dragged.getBoundingClientRect();
|
|
@@ -31619,28 +31689,16 @@ class DragManager {
|
|
|
31619
31689
|
// Find out the dimensions of the element in its dragged state
|
|
31620
31690
|
const { offsetWidth, offsetHeight } = dragged;
|
|
31621
31691
|
// Offset of the click or touch within the dragged element
|
|
31622
|
-
|
|
31692
|
+
// Use the original start position for offset calculation
|
|
31693
|
+
this.offsetX =
|
|
31694
|
+
this.startX - rect.left + (offsetWidth - originalWidth) / 2;
|
|
31623
31695
|
this.offsetY =
|
|
31624
|
-
|
|
31696
|
+
this.startY - rect.top + (offsetHeight - originalHeight) / 2;
|
|
31625
31697
|
this.centerX = offsetWidth / 2;
|
|
31626
31698
|
this.centerY = offsetHeight / 2;
|
|
31627
|
-
// Create bound event handlers that properly assign 'this'
|
|
31628
|
-
this.boundEventHandlers = {
|
|
31629
|
-
drag: this.drag.bind(this),
|
|
31630
|
-
endDrag: this.endDrag.bind(this),
|
|
31631
|
-
};
|
|
31632
|
-
const { drag, endDrag } = this.boundEventHandlers;
|
|
31633
|
-
if (e instanceof MouseEvent) {
|
|
31634
|
-
document.addEventListener('mousemove', drag);
|
|
31635
|
-
document.addEventListener('mouseup', endDrag);
|
|
31636
|
-
}
|
|
31637
|
-
else if (e instanceof TouchEvent) {
|
|
31638
|
-
document.addEventListener('touchmove', drag, { passive: false });
|
|
31639
|
-
document.addEventListener('touchend', endDrag);
|
|
31640
|
-
}
|
|
31641
31699
|
// Do an initial position update, as the size of the dragged element
|
|
31642
31700
|
// may have changed, and it should remain centered
|
|
31643
|
-
this.updatePosition(
|
|
31701
|
+
this.updatePosition(this.lastX, this.lastY);
|
|
31644
31702
|
}
|
|
31645
31703
|
removeDragListeners() {
|
|
31646
31704
|
const { drag, endDrag } = this.boundEventHandlers;
|
|
@@ -31695,8 +31753,19 @@ class DragManager {
|
|
|
31695
31753
|
this.currentDropTarget = dropTarget;
|
|
31696
31754
|
}
|
|
31697
31755
|
drag(e) {
|
|
31698
|
-
e.
|
|
31756
|
+
e.stopPropagation();
|
|
31699
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();
|
|
31700
31769
|
// Update position for both mouse and touch events
|
|
31701
31770
|
this.updatePosition(coords.x, coords.y);
|
|
31702
31771
|
this.updateDropTarget(coords.x, coords.y);
|
|
@@ -31719,13 +31788,21 @@ class DragManager {
|
|
|
31719
31788
|
}
|
|
31720
31789
|
}
|
|
31721
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
|
+
}
|
|
31722
31800
|
e.preventDefault();
|
|
31723
31801
|
const coords = this.getEventCoordinates(e);
|
|
31724
31802
|
const dropTarget = this.findDropTargetAtPoint(coords.x, coords.y);
|
|
31725
31803
|
if (this.currentDropTarget) {
|
|
31726
31804
|
this.currentDropTarget.classList.remove('over');
|
|
31727
31805
|
}
|
|
31728
|
-
this.removeDragListeners();
|
|
31729
31806
|
// Avoid flicker by hiding the element while we are manipulating its
|
|
31730
31807
|
// position in the DOM tree and completing the drop operation
|
|
31731
31808
|
this.draggedElement.style.visibility = 'hidden';
|
|
@@ -31739,8 +31816,9 @@ class DragManager {
|
|
|
31739
31816
|
});
|
|
31740
31817
|
}
|
|
31741
31818
|
}
|
|
31742
|
-
const startDrag = (e, dropHandler) => {
|
|
31743
|
-
|
|
31819
|
+
const startDrag = (e, dropHandler, clickHandler) => {
|
|
31820
|
+
e.stopPropagation();
|
|
31821
|
+
new DragManager(e, dropHandler, clickHandler);
|
|
31744
31822
|
};
|
|
31745
31823
|
|
|
31746
31824
|
/*
|
|
@@ -31757,11 +31835,10 @@ const startDrag = (e, dropHandler) => {
|
|
|
31757
31835
|
For further information, see https://github.com/mideind/Netskrafl
|
|
31758
31836
|
|
|
31759
31837
|
*/
|
|
31760
|
-
const
|
|
31761
|
-
|
|
31762
|
-
|
|
31763
|
-
|
|
31764
|
-
// 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.
|
|
31765
31842
|
startDrag(ev, (_, target) => {
|
|
31766
31843
|
// Drop handler
|
|
31767
31844
|
if (!game)
|
|
@@ -31790,15 +31867,28 @@ const Tile = (initialVnode) => {
|
|
|
31790
31867
|
console.error(e);
|
|
31791
31868
|
}
|
|
31792
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();
|
|
31793
31880
|
});
|
|
31794
31881
|
ev.redraw = false;
|
|
31795
31882
|
return false;
|
|
31796
31883
|
};
|
|
31884
|
+
};
|
|
31885
|
+
const Tile = () => {
|
|
31886
|
+
// Display a tile on the board or in the rack
|
|
31797
31887
|
return {
|
|
31798
31888
|
view: (vnode) => {
|
|
31889
|
+
const { view, game, coord, opponent } = vnode.attrs;
|
|
31799
31890
|
if (!game)
|
|
31800
31891
|
return undefined;
|
|
31801
|
-
const { opponent } = vnode.attrs;
|
|
31802
31892
|
const isRackTile = coord[0] === 'R';
|
|
31803
31893
|
// A single tile, on the board or in the rack
|
|
31804
31894
|
const t = game.tiles[coord];
|
|
@@ -31854,20 +31944,9 @@ const Tile = (initialVnode) => {
|
|
|
31854
31944
|
*/
|
|
31855
31945
|
}
|
|
31856
31946
|
if (t.draggable && game.allowDragDrop()) {
|
|
31947
|
+
const dragHandler = createDragHandler(view, game, coord);
|
|
31857
31948
|
attrs.onmousedown = dragHandler;
|
|
31858
31949
|
attrs.ontouchstart = dragHandler;
|
|
31859
|
-
/*
|
|
31860
|
-
attrs.onclick = (ev: MouseEvent) => {
|
|
31861
|
-
// When clicking a tile, make it selected (blinking)
|
|
31862
|
-
if (coord === game.selectedSq)
|
|
31863
|
-
// Clicking again: deselect
|
|
31864
|
-
game.selectedSq = null;
|
|
31865
|
-
else
|
|
31866
|
-
game.selectedSq = coord;
|
|
31867
|
-
ev.stopPropagation();
|
|
31868
|
-
return false;
|
|
31869
|
-
};
|
|
31870
|
-
*/
|
|
31871
31950
|
}
|
|
31872
31951
|
return m(classes.join('.'), attrs, [
|
|
31873
31952
|
t.letter === ' ' ? nbsp() : t.letter,
|
|
@@ -31924,7 +32003,7 @@ const ReviewTileSquare = {
|
|
|
31924
32003
|
const DropTargetSquare = {
|
|
31925
32004
|
// Return a td element that is a target for dropping tiles
|
|
31926
32005
|
view: (vnode) => {
|
|
31927
|
-
const { view, game, coord } = vnode.attrs;
|
|
32006
|
+
const { view, game, coord, isKeyboardTarget, keyboardDirection } = vnode.attrs;
|
|
31928
32007
|
if (!game)
|
|
31929
32008
|
return undefined;
|
|
31930
32009
|
let cls = game.squareClass(coord) || '';
|
|
@@ -31937,6 +32016,12 @@ const DropTargetSquare = {
|
|
|
31937
32016
|
if (coord === game.startSquare && game.localturn)
|
|
31938
32017
|
// Unoccupied start square, first move
|
|
31939
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
|
+
}
|
|
31940
32025
|
return m(`td.drop-target${cls}`, {
|
|
31941
32026
|
id: `sq_${coord}`,
|
|
31942
32027
|
onclick: (ev) => {
|
|
@@ -32123,6 +32208,8 @@ const Board = {
|
|
|
32123
32208
|
if (scale !== 1.0)
|
|
32124
32209
|
attrs.style = `transform: scale(${scale})`;
|
|
32125
32210
|
*/
|
|
32211
|
+
// Get the next keyboard target square for visual feedback
|
|
32212
|
+
const nextKeyboardSquare = !review ? game.getNextKeyboardSquare() : null;
|
|
32126
32213
|
function colid() {
|
|
32127
32214
|
// The column identifier row
|
|
32128
32215
|
const r = [];
|
|
@@ -32162,6 +32249,8 @@ const Board = {
|
|
|
32162
32249
|
game,
|
|
32163
32250
|
key: coord,
|
|
32164
32251
|
coord: coord,
|
|
32252
|
+
isKeyboardTarget: coord === nextKeyboardSquare,
|
|
32253
|
+
keyboardDirection: game?.getEffectiveKeyboardDirection(),
|
|
32165
32254
|
}));
|
|
32166
32255
|
}
|
|
32167
32256
|
return m('tr', r);
|
|
@@ -33495,6 +33584,80 @@ const currentMoveState = (riddle) => {
|
|
|
33495
33584
|
return { selectedMoves, bestMove };
|
|
33496
33585
|
};
|
|
33497
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
|
+
|
|
33498
33661
|
/*
|
|
33499
33662
|
|
|
33500
33663
|
Localstorage.ts
|
|
@@ -33820,8 +33983,11 @@ class BaseGame {
|
|
|
33820
33983
|
// UI state
|
|
33821
33984
|
this.showingDialog = null;
|
|
33822
33985
|
this.selectedSq = null;
|
|
33823
|
-
this.sel = 'movelist';
|
|
33986
|
+
this.sel = 'movelist'; // Selected tab
|
|
33824
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
|
|
33825
33991
|
// Local storage
|
|
33826
33992
|
this.localStorage = null;
|
|
33827
33993
|
this.uuid = uuid;
|
|
@@ -33935,6 +34101,13 @@ class BaseGame {
|
|
|
33935
34101
|
this._moveTile(from, to);
|
|
33936
34102
|
// Clear error message, if any
|
|
33937
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
|
+
}
|
|
33938
34111
|
// Update the current word score
|
|
33939
34112
|
this.updateScore();
|
|
33940
34113
|
// Update the local storage
|
|
@@ -33959,12 +34132,12 @@ class BaseGame {
|
|
|
33959
34132
|
// Return a list of coordinates of tiles that the user has
|
|
33960
34133
|
// placed on the board by dragging from the rack
|
|
33961
34134
|
const r = [];
|
|
33962
|
-
for (const sq
|
|
33963
|
-
if (
|
|
33964
|
-
sq[0] !== 'R' &&
|
|
33965
|
-
this.tiles[sq].draggable)
|
|
34135
|
+
for (const sq of Object.keys(this.tiles)) {
|
|
34136
|
+
if (sq[0] !== 'R' && this.tiles[sq].draggable) {
|
|
33966
34137
|
// Found a non-rack tile that is not glued to the board
|
|
33967
34138
|
r.push(sq);
|
|
34139
|
+
}
|
|
34140
|
+
}
|
|
33968
34141
|
return r;
|
|
33969
34142
|
}
|
|
33970
34143
|
resetRack() {
|
|
@@ -33994,6 +34167,8 @@ class BaseGame {
|
|
|
33994
34167
|
}
|
|
33995
34168
|
// Reset current error message, if any
|
|
33996
34169
|
this.currentError = null;
|
|
34170
|
+
// Reset keyboard placement state
|
|
34171
|
+
this.resetKeyboardState();
|
|
33997
34172
|
}
|
|
33998
34173
|
rescrambleRack() {
|
|
33999
34174
|
// Reorder the rack randomly. Bound to the Backspace key.
|
|
@@ -34267,36 +34442,39 @@ class BaseGame {
|
|
|
34267
34442
|
// The saved destination square is empty:
|
|
34268
34443
|
// find the tile in the saved rack and move it there
|
|
34269
34444
|
const tile = savedTiles[i].tile;
|
|
34270
|
-
for (const sq
|
|
34271
|
-
if (
|
|
34272
|
-
rackTiles[sq].tile === tile.charAt(0)) {
|
|
34445
|
+
for (const sq of Object.keys(rackTiles)) {
|
|
34446
|
+
if (rackTiles[sq].tile === tile.charAt(0)) {
|
|
34273
34447
|
// Found the tile (or its equivalent) in the rack: move it
|
|
34274
|
-
if (tile.charAt(0) === '?')
|
|
34275
|
-
if (saved_sq.charAt(0) === 'R')
|
|
34448
|
+
if (tile.charAt(0) === '?') {
|
|
34449
|
+
if (saved_sq.charAt(0) === 'R') {
|
|
34276
34450
|
// Going to the rack: no associated letter
|
|
34277
34451
|
rackTiles[sq].letter = ' ';
|
|
34278
|
-
|
|
34279
|
-
|
|
34280
|
-
|
|
34452
|
+
}
|
|
34453
|
+
else {
|
|
34454
|
+
// Going to a board square: associate the originally
|
|
34455
|
+
// chosen and saved letter
|
|
34281
34456
|
rackTiles[sq].letter = tile.charAt(1);
|
|
34457
|
+
}
|
|
34458
|
+
}
|
|
34282
34459
|
// ...and assign it
|
|
34283
34460
|
this.tiles[saved_sq] = rackTiles[sq];
|
|
34284
34461
|
delete rackTiles[sq];
|
|
34285
34462
|
break;
|
|
34286
34463
|
}
|
|
34464
|
+
}
|
|
34287
34465
|
}
|
|
34288
34466
|
}
|
|
34289
34467
|
// Allocate any remaining tiles to free slots in the rack
|
|
34290
34468
|
let j = 1;
|
|
34291
|
-
for (const sq
|
|
34292
|
-
|
|
34293
|
-
|
|
34294
|
-
|
|
34295
|
-
|
|
34296
|
-
|
|
34297
|
-
|
|
34298
|
-
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];
|
|
34299
34476
|
}
|
|
34477
|
+
}
|
|
34300
34478
|
// The local storage may have been cleared before calling
|
|
34301
34479
|
// restoreTiles() so we must ensure that it is updated
|
|
34302
34480
|
this.saveTiles();
|
|
@@ -34306,6 +34484,192 @@ class BaseGame {
|
|
|
34306
34484
|
cleanup() {
|
|
34307
34485
|
// Base cleanup - can be overridden by subclasses
|
|
34308
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
|
+
}
|
|
34309
34673
|
}
|
|
34310
34674
|
|
|
34311
34675
|
/*
|
|
@@ -34683,9 +35047,9 @@ class Game extends BaseGame {
|
|
|
34683
35047
|
// Remember if the game was already won before this update
|
|
34684
35048
|
const wasWon = this.congratulate;
|
|
34685
35049
|
// Stop highlighting the previous opponent move, if any
|
|
34686
|
-
for (const sq
|
|
34687
|
-
|
|
34688
|
-
|
|
35050
|
+
for (const sq of Object.keys(this.tiles)) {
|
|
35051
|
+
this.tiles[sq].freshtile = false;
|
|
35052
|
+
}
|
|
34689
35053
|
this.init(srvGame);
|
|
34690
35054
|
if (this.currentError === null) {
|
|
34691
35055
|
if (this.succ_chall) {
|
|
@@ -37085,7 +37449,7 @@ const MoveListItem = () => {
|
|
|
37085
37449
|
if (tile === '?')
|
|
37086
37450
|
continue;
|
|
37087
37451
|
const sq = coord(row, col);
|
|
37088
|
-
if (sq &&
|
|
37452
|
+
if (sq && sq in game.tiles)
|
|
37089
37453
|
game.tiles[sq].highlight = show ? playerColor : undefined;
|
|
37090
37454
|
col += vec.dx;
|
|
37091
37455
|
row += vec.dy;
|
|
@@ -40530,6 +40894,11 @@ class View {
|
|
|
40530
40894
|
if (this.selectedTab === sel)
|
|
40531
40895
|
return false;
|
|
40532
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
|
+
}
|
|
40533
40902
|
return true;
|
|
40534
40903
|
}
|
|
40535
40904
|
// Globally available view functions
|
|
@@ -40686,6 +41055,13 @@ async function main$1(state, container) {
|
|
|
40686
41055
|
m.mount(container, {
|
|
40687
41056
|
view: () => m(GataDagsins$1, { view, date: validDate, locale }),
|
|
40688
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
|
+
});
|
|
40689
41065
|
}
|
|
40690
41066
|
catch (e) {
|
|
40691
41067
|
console.error('Exception during initialization: ', e);
|
|
@@ -40811,6 +41187,13 @@ async function main(state, container) {
|
|
|
40811
41187
|
const model = new Model(settings, state);
|
|
40812
41188
|
const actions = new Actions(model);
|
|
40813
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
|
+
});
|
|
40814
41197
|
// Run the Mithril router
|
|
40815
41198
|
const routeResolver = createRouteResolver(actions, view);
|
|
40816
41199
|
m.route(container, settings.defaultRoute, routeResolver);
|