@mideind/netskrafl-react 1.0.0-beta.1
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/.eslintignore +8 -0
- package/.eslintrc.json +13 -0
- package/README.md +63 -0
- package/dist/cjs/index.css +6837 -0
- package/dist/cjs/index.js +3046 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.css +6837 -0
- package/dist/esm/index.js +3046 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/package.json +63 -0
- package/rollup.config.js +60 -0
- package/src/components/index.ts +2 -0
- package/src/components/netskrafl/Netskrafl.stories.tsx +66 -0
- package/src/components/netskrafl/Netskrafl.tsx +135 -0
- package/src/components/netskrafl/Netskrafl.types.ts +7 -0
- package/src/components/netskrafl/index.ts +2 -0
- package/src/css/fonts.css +4 -0
- package/src/css/glyphs.css +224 -0
- package/src/css/skrafl-explo.css +6616 -0
- package/src/fonts/glyphicons-regular.eot +0 -0
- package/src/fonts/glyphicons-regular.ttf +0 -0
- package/src/fonts/glyphicons-regular.woff +0 -0
- package/src/index.ts +2 -0
- package/src/messages/messages.json +1576 -0
- package/src/mithril/actions.ts +319 -0
- package/src/mithril/bag.ts +65 -0
- package/src/mithril/bestdisplay.ts +74 -0
- package/src/mithril/blankdialog.ts +94 -0
- package/src/mithril/board.ts +336 -0
- package/src/mithril/buttons.ts +303 -0
- package/src/mithril/challengedialog.ts +186 -0
- package/src/mithril/channel.ts +162 -0
- package/src/mithril/chat.ts +228 -0
- package/src/mithril/components.ts +496 -0
- package/src/mithril/dragdrop.ts +219 -0
- package/src/mithril/elopage.ts +180 -0
- package/src/mithril/friend.ts +227 -0
- package/src/mithril/game.ts +1378 -0
- package/src/mithril/gameview.ts +111 -0
- package/src/mithril/globalstate.ts +33 -0
- package/src/mithril/i18n.ts +186 -0
- package/src/mithril/localstorage.ts +133 -0
- package/src/mithril/login.ts +122 -0
- package/src/mithril/logo.ts +270 -0
- package/src/mithril/main.ts +737 -0
- package/src/mithril/mithril.ts +29 -0
- package/src/mithril/model.ts +817 -0
- package/src/mithril/movelistitem.ts +226 -0
- package/src/mithril/page.ts +852 -0
- package/src/mithril/playername.ts +91 -0
- package/src/mithril/promodialog.ts +82 -0
- package/src/mithril/recentlist.ts +148 -0
- package/src/mithril/request.ts +52 -0
- package/src/mithril/review.ts +634 -0
- package/src/mithril/rightcolumn.ts +398 -0
- package/src/mithril/searchbutton.ts +118 -0
- package/src/mithril/statsdisplay.ts +109 -0
- package/src/mithril/tabs.ts +169 -0
- package/src/mithril/tile.ts +145 -0
- package/src/mithril/twoletter.ts +76 -0
- package/src/mithril/types.ts +379 -0
- package/src/mithril/userinfodialog.ts +171 -0
- package/src/mithril/util.ts +304 -0
- package/src/mithril/wait.ts +246 -0
- package/src/mithril/wordcheck.ts +102 -0
- package/tsconfig.json +28 -0
- package/vite.config.ts +12 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tabs.ts
|
|
3
|
+
|
|
4
|
+
Utility functions to set up tabbed views
|
|
5
|
+
|
|
6
|
+
Copyright (C) 2024 Miðeind ehf.
|
|
7
|
+
Author: Vilhjalmur Thorsteinsson
|
|
8
|
+
|
|
9
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
10
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
11
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
12
|
+
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { IView } from "./types";
|
|
16
|
+
import { m } from "./mithril";
|
|
17
|
+
import { getUrlVars } from "./util";
|
|
18
|
+
|
|
19
|
+
const ROUTE_PREFIX = "/page#!";
|
|
20
|
+
const ROUTE_PREFIX_LEN = ROUTE_PREFIX.length;
|
|
21
|
+
|
|
22
|
+
export type TabVnode = m.VnodeDOM<
|
|
23
|
+
Record<string, any>,
|
|
24
|
+
{
|
|
25
|
+
selected: number;
|
|
26
|
+
lis: HTMLElement[];
|
|
27
|
+
ids: string[];
|
|
28
|
+
}
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
export function makeTabs(view: IView, id: string, createFunc: ((vnode: TabVnode) => void) | undefined, wireHrefs: boolean, vnode: TabVnode) {
|
|
32
|
+
// When the tabs are displayed for the first time, wire'em up
|
|
33
|
+
let tabdiv = document.getElementById(id);
|
|
34
|
+
if (!tabdiv)
|
|
35
|
+
return;
|
|
36
|
+
// Add bunch of jQueryUI compatible classes
|
|
37
|
+
tabdiv.setAttribute("class", "ui-tabs ui-widget ui-widget-content ui-corner-all");
|
|
38
|
+
const tabul = document.querySelector("#" + id + " > ul");
|
|
39
|
+
if (tabul) {
|
|
40
|
+
tabul.setAttribute("class", "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");
|
|
41
|
+
tabul.setAttribute("role", "tablist");
|
|
42
|
+
}
|
|
43
|
+
let tablist = document.querySelectorAll("#" + id + " > ul > li > a") as NodeListOf<HTMLElement>;
|
|
44
|
+
let tabitems = document.querySelectorAll("#" + id + " > ul > li") as NodeListOf<HTMLElement>;
|
|
45
|
+
let ids: string[] = [];
|
|
46
|
+
let lis: HTMLElement[] = []; // The <li> elements
|
|
47
|
+
// Iterate over the <a> elements inside the <li> elements inside the <ul>
|
|
48
|
+
for (let i = 0; i < tablist.length; i++) {
|
|
49
|
+
const href = tablist[i].getAttribute("href");
|
|
50
|
+
if (!href) continue;
|
|
51
|
+
ids.push(href.slice(1));
|
|
52
|
+
// Decorate the <a> elements
|
|
53
|
+
tablist[i].onclick = (ev) => { selectTab(vnode, i); ev.preventDefault(); };
|
|
54
|
+
tablist[i].removeAttribute("href");
|
|
55
|
+
tablist[i].setAttribute("class", "ui-tabs-anchor sp"); // Single-page marker
|
|
56
|
+
tablist[i].setAttribute("role", "presentation");
|
|
57
|
+
// Also decorate the <li> elements
|
|
58
|
+
lis.push(tabitems[i]);
|
|
59
|
+
tabitems[i].setAttribute("class", "ui-state-default ui-corner-top");
|
|
60
|
+
tabitems[i].setAttribute("role", "tab");
|
|
61
|
+
tabitems[i].onmouseover = (ev) => {
|
|
62
|
+
(ev.currentTarget as HTMLElement).classList.toggle("ui-state-hover", true);
|
|
63
|
+
};
|
|
64
|
+
tabitems[i].onmouseout = (ev) => {
|
|
65
|
+
(ev.currentTarget as HTMLElement).classList.toggle("ui-state-hover", false);
|
|
66
|
+
};
|
|
67
|
+
// Find the tab's content <div>
|
|
68
|
+
const tabcontent = document.getElementById(ids[i]);
|
|
69
|
+
// Decorate it
|
|
70
|
+
if (tabcontent) {
|
|
71
|
+
tabcontent.setAttribute("class", "ui-tabs-panel ui-widget-content ui-corner-bottom");
|
|
72
|
+
tabcontent.setAttribute("role", "tabpanel");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Save the list of tab identifiers
|
|
76
|
+
vnode.state.ids = ids;
|
|
77
|
+
// Save the list of <li> elements
|
|
78
|
+
vnode.state.lis = lis;
|
|
79
|
+
// Select the first tab by default
|
|
80
|
+
vnode.state.selected = 0;
|
|
81
|
+
if (wireHrefs) {
|
|
82
|
+
// Wire all hrefs that point to single-page URLs
|
|
83
|
+
const model = view.model;
|
|
84
|
+
const clickURL = (ev: Event, href: string) => {
|
|
85
|
+
let uri = href.slice(ROUTE_PREFIX_LEN); // Cut the /page#!/ prefix off the route
|
|
86
|
+
let qix = uri.indexOf("?");
|
|
87
|
+
let route = (qix >= 0) ? uri.slice(0, qix) : uri;
|
|
88
|
+
let qparams = uri.slice(route.length + 1);
|
|
89
|
+
let params = qparams.length ? getUrlVars(qparams) : {};
|
|
90
|
+
m.route.set(route, params);
|
|
91
|
+
if (window.history)
|
|
92
|
+
window.history.pushState({}, "", href); // Enable the back button
|
|
93
|
+
ev.preventDefault();
|
|
94
|
+
};
|
|
95
|
+
const clickUserPrefs = (ev: Event) => {
|
|
96
|
+
if (model?.state?.userId)
|
|
97
|
+
// Don't show the userprefs if no user logged in
|
|
98
|
+
view.pushDialog("userprefs");
|
|
99
|
+
ev.preventDefault();
|
|
100
|
+
};
|
|
101
|
+
const clickTwoLetter = (ev: Event) => {
|
|
102
|
+
selectTab(vnode, 2); // Select tab number 2
|
|
103
|
+
ev.preventDefault();
|
|
104
|
+
};
|
|
105
|
+
const clickNewBag = (ev: Event) => {
|
|
106
|
+
selectTab(vnode, 3); // Select tab number 3
|
|
107
|
+
ev.preventDefault();
|
|
108
|
+
};
|
|
109
|
+
let anchors = tabdiv.querySelectorAll("a");
|
|
110
|
+
for (let i = 0; i < anchors.length; i++) {
|
|
111
|
+
let a = anchors[i];
|
|
112
|
+
let href = a.getAttribute("href");
|
|
113
|
+
if (href && href.slice(0, ROUTE_PREFIX_LEN) == ROUTE_PREFIX) {
|
|
114
|
+
// Single-page URL: wire it up (as if it had had an m.route.Link on it)
|
|
115
|
+
a.onclick = (ev) => clickURL(ev, href);
|
|
116
|
+
}
|
|
117
|
+
else
|
|
118
|
+
if (href && href === "$$userprefs$$") {
|
|
119
|
+
// Special marker indicating that this link invokes
|
|
120
|
+
// a user preference dialog
|
|
121
|
+
a.onclick = clickUserPrefs;
|
|
122
|
+
}
|
|
123
|
+
else
|
|
124
|
+
if (href && href === "$$twoletter$$") {
|
|
125
|
+
// Special marker indicating that this link invokes
|
|
126
|
+
// the two-letter word list or the opponents tab
|
|
127
|
+
a.onclick = clickTwoLetter;
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
if (href && href === "$$newbag$$") {
|
|
131
|
+
// Special marker indicating that this link invokes
|
|
132
|
+
// the explanation of the new bag
|
|
133
|
+
a.onclick = clickNewBag;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// If a createFunc was specified, run it now
|
|
138
|
+
if (createFunc)
|
|
139
|
+
createFunc(vnode);
|
|
140
|
+
// Finally, make the default tab visible and hide the others
|
|
141
|
+
updateTabVisibility(vnode);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function updateTabVisibility(vnode: TabVnode) {
|
|
145
|
+
// Shows the tab that is currently selected,
|
|
146
|
+
// i.e. the one whose index is in vnode.state.selected
|
|
147
|
+
const selected: number = vnode.state.selected;
|
|
148
|
+
const lis = vnode.state.lis;
|
|
149
|
+
vnode.state.ids.map((id: string, i: number) => {
|
|
150
|
+
document.getElementById(id)?.setAttribute("style", "display: " +
|
|
151
|
+
(i == selected ? "block" : "none"));
|
|
152
|
+
lis[i].classList.toggle("ui-tabs-active", i === selected);
|
|
153
|
+
lis[i].classList.toggle("ui-state-active", i === selected);
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function selectTab(vnode: TabVnode, i: number) {
|
|
159
|
+
// Selects the tab with the given index under the tab control vnode
|
|
160
|
+
vnode.state.selected = i;
|
|
161
|
+
updateTabVisibility(vnode);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function updateSelection(vnode: TabVnode) {
|
|
165
|
+
// Select a tab according to the ?tab= query parameter in the current route
|
|
166
|
+
var tab = m.route.param("tab");
|
|
167
|
+
if (tab !== undefined)
|
|
168
|
+
selectTab(vnode, parseInt(tab) || 0);
|
|
169
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Tile.ts
|
|
4
|
+
|
|
5
|
+
Tile component
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
8
|
+
Author: Vilhjalmur Thorsteinsson
|
|
9
|
+
|
|
10
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
11
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
12
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
13
|
+
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { EXTRA_WIDE_LETTERS, IView, WIDE_LETTERS } from "./types";
|
|
17
|
+
import { VnodeAttrs, ComponentFunc, m, MithrilMouseEvent, MithrilTouchEvent } from "./mithril";
|
|
18
|
+
import { nbsp } from "./util";
|
|
19
|
+
import { startDrag } from "./dragdrop";
|
|
20
|
+
|
|
21
|
+
interface IAttributes {
|
|
22
|
+
view: IView;
|
|
23
|
+
coord: string;
|
|
24
|
+
opponent: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Tile: ComponentFunc<IAttributes> = (initialVnode) => {
|
|
28
|
+
// Display a tile on the board or in the rack
|
|
29
|
+
const { view, coord, opponent } = initialVnode.attrs;
|
|
30
|
+
const model = view.model;
|
|
31
|
+
|
|
32
|
+
const dragHandler = (ev: MithrilMouseEvent | MithrilTouchEvent) => {
|
|
33
|
+
// Start a drag-and-drop process, for mouse or touch interaction
|
|
34
|
+
startDrag(ev, (_, target) => {
|
|
35
|
+
// Drop handler
|
|
36
|
+
const game = model.game;
|
|
37
|
+
if (!game) return;
|
|
38
|
+
const id = target.id;
|
|
39
|
+
if (!id) return;
|
|
40
|
+
let targetCoord: string = "";
|
|
41
|
+
if (id === "board-background") {
|
|
42
|
+
// Drop on the background: transfer back to the rack
|
|
43
|
+
targetCoord = "R1";
|
|
44
|
+
} else if (id.startsWith("sq_")) {
|
|
45
|
+
// Drop on a board square
|
|
46
|
+
targetCoord = id.slice(3);
|
|
47
|
+
}
|
|
48
|
+
if (targetCoord) {
|
|
49
|
+
try {
|
|
50
|
+
game.attemptMove(coord, targetCoord);
|
|
51
|
+
view.updateScale();
|
|
52
|
+
// Make sure that Mithril draws the updated state
|
|
53
|
+
m.redraw();
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// Something went wrong: display a console error
|
|
56
|
+
console.error(e);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
ev.redraw = false;
|
|
61
|
+
return false;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
view: () => {
|
|
66
|
+
const game = model.game;
|
|
67
|
+
if (!game) return undefined;
|
|
68
|
+
const isRackTile = coord[0] === 'R';
|
|
69
|
+
// A single tile, on the board or in the rack
|
|
70
|
+
const t = game.tiles[coord];
|
|
71
|
+
let classes = [".tile"];
|
|
72
|
+
let attrs: VnodeAttrs = {};
|
|
73
|
+
if (t.tile === '?')
|
|
74
|
+
classes.push("blanktile");
|
|
75
|
+
if (EXTRA_WIDE_LETTERS.includes(t.letter))
|
|
76
|
+
// Extra wide letter: handle specially
|
|
77
|
+
classes.push("extra-wide");
|
|
78
|
+
else if (WIDE_LETTERS.includes(t.letter))
|
|
79
|
+
// Wide letter: handle specially
|
|
80
|
+
classes.push("wide");
|
|
81
|
+
if (isRackTile || t.draggable) {
|
|
82
|
+
// Rack tile, or at least a draggable one
|
|
83
|
+
classes.push(opponent ? "freshtile" : "racktile");
|
|
84
|
+
if (isRackTile && game.showingDialog === "exchange") {
|
|
85
|
+
// Rack tile, and we're showing the exchange dialog
|
|
86
|
+
if (t.xchg)
|
|
87
|
+
// Chosen as an exchange tile
|
|
88
|
+
classes.push("xchgsel");
|
|
89
|
+
// Exchange dialog is live: add a click handler for the
|
|
90
|
+
// exchange state
|
|
91
|
+
attrs.onclick = (ev: Event) => {
|
|
92
|
+
// Toggle the exchange status of this tile
|
|
93
|
+
t.xchg = !t.xchg;
|
|
94
|
+
ev.preventDefault();
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (t.freshtile) {
|
|
99
|
+
// A fresh tile on the board that has
|
|
100
|
+
// just been played by the opponent
|
|
101
|
+
classes.push("freshtile");
|
|
102
|
+
}
|
|
103
|
+
if (t.index) {
|
|
104
|
+
// Make fresh or highlighted tiles appear sequentally by animation
|
|
105
|
+
const ANIMATION_STEP = 150; // Milliseconds
|
|
106
|
+
const delay = (t.index * ANIMATION_STEP).toString() + "ms";
|
|
107
|
+
attrs.style = `animation-delay: ${delay}`;
|
|
108
|
+
}
|
|
109
|
+
if (coord === game.selectedSq)
|
|
110
|
+
// Currently selected square
|
|
111
|
+
classes.push("sel"); // Blinks red
|
|
112
|
+
if (t.highlight !== undefined) {
|
|
113
|
+
// highlight0 is the local player color
|
|
114
|
+
// highlight1 is the remote player color
|
|
115
|
+
classes.push("highlight" + t.highlight);
|
|
116
|
+
/*
|
|
117
|
+
if (t.player == parseInt(t.highlight))
|
|
118
|
+
// This tile was originally laid down by the other player
|
|
119
|
+
classes.push("dim");
|
|
120
|
+
*/
|
|
121
|
+
}
|
|
122
|
+
if (game.showingDialog === null && !game.over) {
|
|
123
|
+
if (t.draggable) {
|
|
124
|
+
attrs.onmousedown = dragHandler;
|
|
125
|
+
attrs.ontouchstart = dragHandler;
|
|
126
|
+
/*
|
|
127
|
+
attrs.onclick = (ev: MouseEvent) => {
|
|
128
|
+
// When clicking a tile, make it selected (blinking)
|
|
129
|
+
if (coord === game.selectedSq)
|
|
130
|
+
// Clicking again: deselect
|
|
131
|
+
game.selectedSq = null;
|
|
132
|
+
else
|
|
133
|
+
game.selectedSq = coord;
|
|
134
|
+
ev.stopPropagation();
|
|
135
|
+
return false;
|
|
136
|
+
};
|
|
137
|
+
*/
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return m(classes.join("."), attrs,
|
|
141
|
+
[t.letter === " " ? nbsp() : t.letter, m(".letterscore", t.score)]
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Twoletter.ts
|
|
4
|
+
|
|
5
|
+
Two Letter Word display component
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2024 Miðeind ehf.
|
|
8
|
+
Author: Vilhjalmur Thorsteinsson
|
|
9
|
+
|
|
10
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
11
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
12
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
13
|
+
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { IView } from "./types";
|
|
17
|
+
import { Vnode, ComponentFunc, m, VnodeChildren } from "./mithril";
|
|
18
|
+
import { ts } from "./i18n";
|
|
19
|
+
|
|
20
|
+
interface IAttributes {
|
|
21
|
+
view: IView;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const TwoLetter: ComponentFunc<IAttributes> = (initialVnode) => {
|
|
25
|
+
|
|
26
|
+
// The two-letter-word list tab
|
|
27
|
+
const view = initialVnode.attrs.view;
|
|
28
|
+
const model = view.model;
|
|
29
|
+
let page = 0;
|
|
30
|
+
|
|
31
|
+
function renderWord(bold: boolean, w: string): Vnode {
|
|
32
|
+
// For the first two-letter word in each group,
|
|
33
|
+
// render the former letter in bold
|
|
34
|
+
if (!bold)
|
|
35
|
+
return m(".twoletter-word", w);
|
|
36
|
+
if (page == 0)
|
|
37
|
+
return m(".twoletter-word", [m("b", w[0]), w[1]]);
|
|
38
|
+
else
|
|
39
|
+
return m(".twoletter-word", [w[0], m("b", w[1])]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
view: () => {
|
|
44
|
+
if (!model.game) return;
|
|
45
|
+
const game = model.game;
|
|
46
|
+
const twoLetters = game.twoLetterWords();
|
|
47
|
+
const twoLetterWords = twoLetters[page];
|
|
48
|
+
const twoLetterList: VnodeChildren = [];
|
|
49
|
+
for (const tw of twoLetterWords) {
|
|
50
|
+
const twl = tw[1];
|
|
51
|
+
const sublist: VnodeChildren = [];
|
|
52
|
+
for (let j = 0; j < twl.length; j++)
|
|
53
|
+
sublist.push(renderWord(j == 0, twl[j]));
|
|
54
|
+
twoLetterList.push(
|
|
55
|
+
m(".twoletter-group", sublist)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return m(".twoletter",
|
|
59
|
+
{
|
|
60
|
+
// Switch between pages when clicked
|
|
61
|
+
onclick: () => { page = 1 - page; },
|
|
62
|
+
style: "z-index: 6" // Appear on top of board on mobile
|
|
63
|
+
},
|
|
64
|
+
// Show the requested page
|
|
65
|
+
m(".twoletter-area" + (game.showClock() ? ".with-clock" : ""),
|
|
66
|
+
{
|
|
67
|
+
title: page == 0 ?
|
|
68
|
+
ts("Smelltu til að raða eftir seinni staf") :
|
|
69
|
+
ts("Smelltu til að raða eftir fyrri staf")
|
|
70
|
+
},
|
|
71
|
+
twoLetterList
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
};
|