@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/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,12 +31256,20 @@ 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
|
+
}
|
|
31229
31267
|
function handleKeydown(game, ev) {
|
|
31230
31268
|
// Escape key cancels the dialog
|
|
31231
31269
|
let { key } = ev;
|
|
31232
31270
|
if (key === 'Escape') {
|
|
31233
31271
|
ev.preventDefault();
|
|
31234
|
-
game
|
|
31272
|
+
cancel(game);
|
|
31235
31273
|
return;
|
|
31236
31274
|
}
|
|
31237
31275
|
if (key.length === 1) {
|
|
@@ -31239,7 +31277,7 @@ const BlankDialog = () => {
|
|
|
31239
31277
|
key = key.toLowerCase();
|
|
31240
31278
|
if (game.alphabet.includes(key)) {
|
|
31241
31279
|
ev.preventDefault();
|
|
31242
|
-
game
|
|
31280
|
+
place(game, key);
|
|
31243
31281
|
}
|
|
31244
31282
|
}
|
|
31245
31283
|
}
|
|
@@ -31256,8 +31294,8 @@ const BlankDialog = () => {
|
|
|
31256
31294
|
const letter = legalLetters[ix++];
|
|
31257
31295
|
c.push(m('td', {
|
|
31258
31296
|
onclick: (ev) => {
|
|
31259
|
-
game.placeBlank(letter);
|
|
31260
31297
|
ev.preventDefault();
|
|
31298
|
+
place(game, letter);
|
|
31261
31299
|
},
|
|
31262
31300
|
onmouseover: buttonOver,
|
|
31263
31301
|
onmouseout: buttonOut,
|
|
@@ -31287,7 +31325,7 @@ const BlankDialog = () => {
|
|
|
31287
31325
|
title: ts('Hætta við'),
|
|
31288
31326
|
onclick: (ev) => {
|
|
31289
31327
|
ev.preventDefault();
|
|
31290
|
-
game
|
|
31328
|
+
cancel(game);
|
|
31291
31329
|
},
|
|
31292
31330
|
}, glyph('remove')),
|
|
31293
31331
|
]));
|
|
@@ -31562,6 +31600,8 @@ const Buttons = {
|
|
|
31562
31600
|
drag-and-drop does not offer enough flexibility for our requirements.
|
|
31563
31601
|
|
|
31564
31602
|
*/
|
|
31603
|
+
// Minimum movement in pixels before a mousedown becomes a drag
|
|
31604
|
+
const DRAG_THRESHOLD = 10;
|
|
31565
31605
|
class DragManager {
|
|
31566
31606
|
getEventCoordinates(e) {
|
|
31567
31607
|
if (e instanceof MouseEvent) {
|
|
@@ -31581,28 +31621,58 @@ class DragManager {
|
|
|
31581
31621
|
p.y >= rect.top &&
|
|
31582
31622
|
p.y < rect.bottom);
|
|
31583
31623
|
}
|
|
31584
|
-
constructor(e, dropHandler) {
|
|
31624
|
+
constructor(e, dropHandler, clickHandler) {
|
|
31585
31625
|
this.parentElement = null;
|
|
31586
31626
|
this.parentRect = null;
|
|
31587
31627
|
this.offsetX = 0;
|
|
31588
31628
|
this.offsetY = 0;
|
|
31589
31629
|
this.centerX = 0;
|
|
31590
31630
|
this.centerY = 0;
|
|
31631
|
+
this.startX = 0;
|
|
31632
|
+
this.startY = 0;
|
|
31591
31633
|
this.lastX = 0;
|
|
31592
31634
|
this.lastY = 0;
|
|
31593
31635
|
this.currentDropTarget = null;
|
|
31594
|
-
|
|
31636
|
+
this.dragStarted = false;
|
|
31595
31637
|
// Note: We use e.currentTarget here, not e.target, as we may be
|
|
31596
31638
|
// handling events that were originally targeted at a child element
|
|
31597
31639
|
// (such as the letter or score elements within a tile).
|
|
31598
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
|
+
});
|
|
31599
31646
|
const coords = this.getEventCoordinates(e);
|
|
31647
|
+
this.startX = coords.x;
|
|
31648
|
+
this.startY = coords.y;
|
|
31600
31649
|
this.lastX = coords.x;
|
|
31601
31650
|
this.lastY = coords.y;
|
|
31602
31651
|
this.draggedElement = dragged;
|
|
31603
31652
|
this.dropHandler = dropHandler;
|
|
31653
|
+
this.clickHandler = clickHandler;
|
|
31604
31654
|
this.parentElement = dragged.parentElement;
|
|
31605
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;
|
|
31606
31676
|
// Find out the bounding rectangle of the element
|
|
31607
31677
|
// before starting to apply modifications
|
|
31608
31678
|
const rect = dragged.getBoundingClientRect();
|
|
@@ -31617,28 +31687,16 @@ class DragManager {
|
|
|
31617
31687
|
// Find out the dimensions of the element in its dragged state
|
|
31618
31688
|
const { offsetWidth, offsetHeight } = dragged;
|
|
31619
31689
|
// Offset of the click or touch within the dragged element
|
|
31620
|
-
|
|
31690
|
+
// Use the original start position for offset calculation
|
|
31691
|
+
this.offsetX =
|
|
31692
|
+
this.startX - rect.left + (offsetWidth - originalWidth) / 2;
|
|
31621
31693
|
this.offsetY =
|
|
31622
|
-
|
|
31694
|
+
this.startY - rect.top + (offsetHeight - originalHeight) / 2;
|
|
31623
31695
|
this.centerX = offsetWidth / 2;
|
|
31624
31696
|
this.centerY = offsetHeight / 2;
|
|
31625
|
-
// Create bound event handlers that properly assign 'this'
|
|
31626
|
-
this.boundEventHandlers = {
|
|
31627
|
-
drag: this.drag.bind(this),
|
|
31628
|
-
endDrag: this.endDrag.bind(this),
|
|
31629
|
-
};
|
|
31630
|
-
const { drag, endDrag } = this.boundEventHandlers;
|
|
31631
|
-
if (e instanceof MouseEvent) {
|
|
31632
|
-
document.addEventListener('mousemove', drag);
|
|
31633
|
-
document.addEventListener('mouseup', endDrag);
|
|
31634
|
-
}
|
|
31635
|
-
else if (e instanceof TouchEvent) {
|
|
31636
|
-
document.addEventListener('touchmove', drag, { passive: false });
|
|
31637
|
-
document.addEventListener('touchend', endDrag);
|
|
31638
|
-
}
|
|
31639
31697
|
// Do an initial position update, as the size of the dragged element
|
|
31640
31698
|
// may have changed, and it should remain centered
|
|
31641
|
-
this.updatePosition(
|
|
31699
|
+
this.updatePosition(this.lastX, this.lastY);
|
|
31642
31700
|
}
|
|
31643
31701
|
removeDragListeners() {
|
|
31644
31702
|
const { drag, endDrag } = this.boundEventHandlers;
|
|
@@ -31693,8 +31751,19 @@ class DragManager {
|
|
|
31693
31751
|
this.currentDropTarget = dropTarget;
|
|
31694
31752
|
}
|
|
31695
31753
|
drag(e) {
|
|
31696
|
-
e.
|
|
31754
|
+
e.stopPropagation();
|
|
31697
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();
|
|
31698
31767
|
// Update position for both mouse and touch events
|
|
31699
31768
|
this.updatePosition(coords.x, coords.y);
|
|
31700
31769
|
this.updateDropTarget(coords.x, coords.y);
|
|
@@ -31717,13 +31786,21 @@ class DragManager {
|
|
|
31717
31786
|
}
|
|
31718
31787
|
}
|
|
31719
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
|
+
}
|
|
31720
31798
|
e.preventDefault();
|
|
31721
31799
|
const coords = this.getEventCoordinates(e);
|
|
31722
31800
|
const dropTarget = this.findDropTargetAtPoint(coords.x, coords.y);
|
|
31723
31801
|
if (this.currentDropTarget) {
|
|
31724
31802
|
this.currentDropTarget.classList.remove('over');
|
|
31725
31803
|
}
|
|
31726
|
-
this.removeDragListeners();
|
|
31727
31804
|
// Avoid flicker by hiding the element while we are manipulating its
|
|
31728
31805
|
// position in the DOM tree and completing the drop operation
|
|
31729
31806
|
this.draggedElement.style.visibility = 'hidden';
|
|
@@ -31737,8 +31814,9 @@ class DragManager {
|
|
|
31737
31814
|
});
|
|
31738
31815
|
}
|
|
31739
31816
|
}
|
|
31740
|
-
const startDrag = (e, dropHandler) => {
|
|
31741
|
-
|
|
31817
|
+
const startDrag = (e, dropHandler, clickHandler) => {
|
|
31818
|
+
e.stopPropagation();
|
|
31819
|
+
new DragManager(e, dropHandler, clickHandler);
|
|
31742
31820
|
};
|
|
31743
31821
|
|
|
31744
31822
|
/*
|
|
@@ -31755,11 +31833,10 @@ const startDrag = (e, dropHandler) => {
|
|
|
31755
31833
|
For further information, see https://github.com/mideind/Netskrafl
|
|
31756
31834
|
|
|
31757
31835
|
*/
|
|
31758
|
-
const
|
|
31759
|
-
|
|
31760
|
-
|
|
31761
|
-
|
|
31762
|
-
// 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.
|
|
31763
31840
|
startDrag(ev, (_, target) => {
|
|
31764
31841
|
// Drop handler
|
|
31765
31842
|
if (!game)
|
|
@@ -31788,15 +31865,28 @@ const Tile = (initialVnode) => {
|
|
|
31788
31865
|
console.error(e);
|
|
31789
31866
|
}
|
|
31790
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();
|
|
31791
31878
|
});
|
|
31792
31879
|
ev.redraw = false;
|
|
31793
31880
|
return false;
|
|
31794
31881
|
};
|
|
31882
|
+
};
|
|
31883
|
+
const Tile = () => {
|
|
31884
|
+
// Display a tile on the board or in the rack
|
|
31795
31885
|
return {
|
|
31796
31886
|
view: (vnode) => {
|
|
31887
|
+
const { view, game, coord, opponent } = vnode.attrs;
|
|
31797
31888
|
if (!game)
|
|
31798
31889
|
return undefined;
|
|
31799
|
-
const { opponent } = vnode.attrs;
|
|
31800
31890
|
const isRackTile = coord[0] === 'R';
|
|
31801
31891
|
// A single tile, on the board or in the rack
|
|
31802
31892
|
const t = game.tiles[coord];
|
|
@@ -31852,20 +31942,9 @@ const Tile = (initialVnode) => {
|
|
|
31852
31942
|
*/
|
|
31853
31943
|
}
|
|
31854
31944
|
if (t.draggable && game.allowDragDrop()) {
|
|
31945
|
+
const dragHandler = createDragHandler(view, game, coord);
|
|
31855
31946
|
attrs.onmousedown = dragHandler;
|
|
31856
31947
|
attrs.ontouchstart = dragHandler;
|
|
31857
|
-
/*
|
|
31858
|
-
attrs.onclick = (ev: MouseEvent) => {
|
|
31859
|
-
// When clicking a tile, make it selected (blinking)
|
|
31860
|
-
if (coord === game.selectedSq)
|
|
31861
|
-
// Clicking again: deselect
|
|
31862
|
-
game.selectedSq = null;
|
|
31863
|
-
else
|
|
31864
|
-
game.selectedSq = coord;
|
|
31865
|
-
ev.stopPropagation();
|
|
31866
|
-
return false;
|
|
31867
|
-
};
|
|
31868
|
-
*/
|
|
31869
31948
|
}
|
|
31870
31949
|
return m(classes.join('.'), attrs, [
|
|
31871
31950
|
t.letter === ' ' ? nbsp() : t.letter,
|
|
@@ -31922,7 +32001,7 @@ const ReviewTileSquare = {
|
|
|
31922
32001
|
const DropTargetSquare = {
|
|
31923
32002
|
// Return a td element that is a target for dropping tiles
|
|
31924
32003
|
view: (vnode) => {
|
|
31925
|
-
const { view, game, coord } = vnode.attrs;
|
|
32004
|
+
const { view, game, coord, isKeyboardTarget, keyboardDirection } = vnode.attrs;
|
|
31926
32005
|
if (!game)
|
|
31927
32006
|
return undefined;
|
|
31928
32007
|
let cls = game.squareClass(coord) || '';
|
|
@@ -31935,6 +32014,12 @@ const DropTargetSquare = {
|
|
|
31935
32014
|
if (coord === game.startSquare && game.localturn)
|
|
31936
32015
|
// Unoccupied start square, first move
|
|
31937
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
|
+
}
|
|
31938
32023
|
return m(`td.drop-target${cls}`, {
|
|
31939
32024
|
id: `sq_${coord}`,
|
|
31940
32025
|
onclick: (ev) => {
|
|
@@ -32121,6 +32206,8 @@ const Board = {
|
|
|
32121
32206
|
if (scale !== 1.0)
|
|
32122
32207
|
attrs.style = `transform: scale(${scale})`;
|
|
32123
32208
|
*/
|
|
32209
|
+
// Get the next keyboard target square for visual feedback
|
|
32210
|
+
const nextKeyboardSquare = !review ? game.getNextKeyboardSquare() : null;
|
|
32124
32211
|
function colid() {
|
|
32125
32212
|
// The column identifier row
|
|
32126
32213
|
const r = [];
|
|
@@ -32160,6 +32247,8 @@ const Board = {
|
|
|
32160
32247
|
game,
|
|
32161
32248
|
key: coord,
|
|
32162
32249
|
coord: coord,
|
|
32250
|
+
isKeyboardTarget: coord === nextKeyboardSquare,
|
|
32251
|
+
keyboardDirection: game?.getEffectiveKeyboardDirection(),
|
|
32163
32252
|
}));
|
|
32164
32253
|
}
|
|
32165
32254
|
return m('tr', r);
|
|
@@ -33493,6 +33582,80 @@ const currentMoveState = (riddle) => {
|
|
|
33493
33582
|
return { selectedMoves, bestMove };
|
|
33494
33583
|
};
|
|
33495
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
|
+
|
|
33496
33659
|
/*
|
|
33497
33660
|
|
|
33498
33661
|
Localstorage.ts
|
|
@@ -33818,8 +33981,11 @@ class BaseGame {
|
|
|
33818
33981
|
// UI state
|
|
33819
33982
|
this.showingDialog = null;
|
|
33820
33983
|
this.selectedSq = null;
|
|
33821
|
-
this.sel = 'movelist';
|
|
33984
|
+
this.sel = 'movelist'; // Selected tab
|
|
33822
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
|
|
33823
33989
|
// Local storage
|
|
33824
33990
|
this.localStorage = null;
|
|
33825
33991
|
this.uuid = uuid;
|
|
@@ -33933,6 +34099,13 @@ class BaseGame {
|
|
|
33933
34099
|
this._moveTile(from, to);
|
|
33934
34100
|
// Clear error message, if any
|
|
33935
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
|
+
}
|
|
33936
34109
|
// Update the current word score
|
|
33937
34110
|
this.updateScore();
|
|
33938
34111
|
// Update the local storage
|
|
@@ -33957,12 +34130,12 @@ class BaseGame {
|
|
|
33957
34130
|
// Return a list of coordinates of tiles that the user has
|
|
33958
34131
|
// placed on the board by dragging from the rack
|
|
33959
34132
|
const r = [];
|
|
33960
|
-
for (const sq
|
|
33961
|
-
if (
|
|
33962
|
-
sq[0] !== 'R' &&
|
|
33963
|
-
this.tiles[sq].draggable)
|
|
34133
|
+
for (const sq of Object.keys(this.tiles)) {
|
|
34134
|
+
if (sq[0] !== 'R' && this.tiles[sq].draggable) {
|
|
33964
34135
|
// Found a non-rack tile that is not glued to the board
|
|
33965
34136
|
r.push(sq);
|
|
34137
|
+
}
|
|
34138
|
+
}
|
|
33966
34139
|
return r;
|
|
33967
34140
|
}
|
|
33968
34141
|
resetRack() {
|
|
@@ -33992,6 +34165,8 @@ class BaseGame {
|
|
|
33992
34165
|
}
|
|
33993
34166
|
// Reset current error message, if any
|
|
33994
34167
|
this.currentError = null;
|
|
34168
|
+
// Reset keyboard placement state
|
|
34169
|
+
this.resetKeyboardState();
|
|
33995
34170
|
}
|
|
33996
34171
|
rescrambleRack() {
|
|
33997
34172
|
// Reorder the rack randomly. Bound to the Backspace key.
|
|
@@ -34265,36 +34440,39 @@ class BaseGame {
|
|
|
34265
34440
|
// The saved destination square is empty:
|
|
34266
34441
|
// find the tile in the saved rack and move it there
|
|
34267
34442
|
const tile = savedTiles[i].tile;
|
|
34268
|
-
for (const sq
|
|
34269
|
-
if (
|
|
34270
|
-
rackTiles[sq].tile === tile.charAt(0)) {
|
|
34443
|
+
for (const sq of Object.keys(rackTiles)) {
|
|
34444
|
+
if (rackTiles[sq].tile === tile.charAt(0)) {
|
|
34271
34445
|
// Found the tile (or its equivalent) in the rack: move it
|
|
34272
|
-
if (tile.charAt(0) === '?')
|
|
34273
|
-
if (saved_sq.charAt(0) === 'R')
|
|
34446
|
+
if (tile.charAt(0) === '?') {
|
|
34447
|
+
if (saved_sq.charAt(0) === 'R') {
|
|
34274
34448
|
// Going to the rack: no associated letter
|
|
34275
34449
|
rackTiles[sq].letter = ' ';
|
|
34276
|
-
|
|
34277
|
-
|
|
34278
|
-
|
|
34450
|
+
}
|
|
34451
|
+
else {
|
|
34452
|
+
// Going to a board square: associate the originally
|
|
34453
|
+
// chosen and saved letter
|
|
34279
34454
|
rackTiles[sq].letter = tile.charAt(1);
|
|
34455
|
+
}
|
|
34456
|
+
}
|
|
34280
34457
|
// ...and assign it
|
|
34281
34458
|
this.tiles[saved_sq] = rackTiles[sq];
|
|
34282
34459
|
delete rackTiles[sq];
|
|
34283
34460
|
break;
|
|
34284
34461
|
}
|
|
34462
|
+
}
|
|
34285
34463
|
}
|
|
34286
34464
|
}
|
|
34287
34465
|
// Allocate any remaining tiles to free slots in the rack
|
|
34288
34466
|
let j = 1;
|
|
34289
|
-
for (const sq
|
|
34290
|
-
|
|
34291
|
-
|
|
34292
|
-
|
|
34293
|
-
|
|
34294
|
-
|
|
34295
|
-
|
|
34296
|
-
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];
|
|
34297
34474
|
}
|
|
34475
|
+
}
|
|
34298
34476
|
// The local storage may have been cleared before calling
|
|
34299
34477
|
// restoreTiles() so we must ensure that it is updated
|
|
34300
34478
|
this.saveTiles();
|
|
@@ -34304,6 +34482,192 @@ class BaseGame {
|
|
|
34304
34482
|
cleanup() {
|
|
34305
34483
|
// Base cleanup - can be overridden by subclasses
|
|
34306
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
|
+
}
|
|
34307
34671
|
}
|
|
34308
34672
|
|
|
34309
34673
|
/*
|
|
@@ -34681,9 +35045,9 @@ class Game extends BaseGame {
|
|
|
34681
35045
|
// Remember if the game was already won before this update
|
|
34682
35046
|
const wasWon = this.congratulate;
|
|
34683
35047
|
// Stop highlighting the previous opponent move, if any
|
|
34684
|
-
for (const sq
|
|
34685
|
-
|
|
34686
|
-
|
|
35048
|
+
for (const sq of Object.keys(this.tiles)) {
|
|
35049
|
+
this.tiles[sq].freshtile = false;
|
|
35050
|
+
}
|
|
34687
35051
|
this.init(srvGame);
|
|
34688
35052
|
if (this.currentError === null) {
|
|
34689
35053
|
if (this.succ_chall) {
|
|
@@ -37083,7 +37447,7 @@ const MoveListItem = () => {
|
|
|
37083
37447
|
if (tile === '?')
|
|
37084
37448
|
continue;
|
|
37085
37449
|
const sq = coord(row, col);
|
|
37086
|
-
if (sq &&
|
|
37450
|
+
if (sq && sq in game.tiles)
|
|
37087
37451
|
game.tiles[sq].highlight = show ? playerColor : undefined;
|
|
37088
37452
|
col += vec.dx;
|
|
37089
37453
|
row += vec.dy;
|
|
@@ -40528,6 +40892,11 @@ class View {
|
|
|
40528
40892
|
if (this.selectedTab === sel)
|
|
40529
40893
|
return false;
|
|
40530
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
|
+
}
|
|
40531
40900
|
return true;
|
|
40532
40901
|
}
|
|
40533
40902
|
// Globally available view functions
|
|
@@ -40684,6 +41053,13 @@ async function main$1(state, container) {
|
|
|
40684
41053
|
m.mount(container, {
|
|
40685
41054
|
view: () => m(GataDagsins$1, { view, date: validDate, locale }),
|
|
40686
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
|
+
});
|
|
40687
41063
|
}
|
|
40688
41064
|
catch (e) {
|
|
40689
41065
|
console.error('Exception during initialization: ', e);
|
|
@@ -40809,6 +41185,13 @@ async function main(state, container) {
|
|
|
40809
41185
|
const model = new Model(settings, state);
|
|
40810
41186
|
const actions = new Actions(model);
|
|
40811
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
|
+
});
|
|
40812
41195
|
// Run the Mithril router
|
|
40813
41196
|
const routeResolver = createRouteResolver(actions, view);
|
|
40814
41197
|
m.route(container, settings.defaultRoute, routeResolver);
|