Addition of FlagInputModal (#1606)

## Description:

The UI for selecting flags differs from other existing UIs, so this
change allows it to use .
The actual contents for flag selection will be included in the next PR.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I have read and accepted the CLA agreement (only required once).

## Please put your Discord username so you can be contacted if a bug or
regression is found:

aotumuri

---------

Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
This commit is contained in:
Aotumuri
2025-08-04 08:19:57 +09:00
committed by GitHub
parent a05814a7fb
commit fcd6f5d404
5 changed files with 166 additions and 91 deletions
+32 -90
View File
@@ -1,13 +1,11 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import Countries from "./data/countries.json";
import { renderPlayerFlag } from "../core/CustomFlag";
const flagKey: string = "flag";
@customElement("flag-input")
export class FlagInput extends LitElement {
@state() private flag: string = "";
@state() private search: string = "";
@state() private showModal: boolean = false;
@state() public flag: string = "";
static styles = css`
@media (max-width: 768px) {
@@ -21,19 +19,6 @@ export class FlagInput extends LitElement {
}
`;
private handleSearch(e: Event) {
this.search = String((e.target as HTMLInputElement).value);
}
private setFlag(flag: string) {
if (flag === "xx") {
flag = "";
}
this.flag = flag;
this.showModal = false;
this.storeFlag(flag);
}
public getCurrentFlag(): string {
return this.flag;
}
@@ -46,14 +31,6 @@ export class FlagInput extends LitElement {
return "";
}
private storeFlag(flag: string) {
if (flag) {
localStorage.setItem(flagKey, flag);
} else if (flag === "") {
localStorage.removeItem(flagKey);
}
}
private dispatchFlagEvent() {
this.dispatchEvent(
new CustomEvent("flag-change", {
@@ -68,86 +45,51 @@ export class FlagInput extends LitElement {
super.connectedCallback();
this.flag = this.getStoredFlag();
this.dispatchFlagEvent();
window.addEventListener("keydown", this.handleKeyDown);
}
disconnectedCallback() {
window.removeEventListener("keydown", this.handleKeyDown);
super.disconnectedCallback();
}
private handleKeyDown = (e: KeyboardEvent) => {
if (e.code === "Escape") {
e.preventDefault();
this.showModal = false;
}
};
createRenderRoot() {
return this;
}
render() {
return html`
<div
class="absolute left-0 top-0 w-full h-full ${this.showModal
? ""
: "hidden"}"
@click=${() => (this.showModal = false)}
></div>
<div class="flex relative">
<button
@click=${() => (this.showModal = !this.showModal)}
id="flag-input_"
class="border p-[4px] rounded-lg flex cursor-pointer border-black/30 dark:border-gray-300/60 bg-white/70 dark:bg-[rgba(55,65,81,0.7)]"
title="Pick a flag!"
>
<img class="size-[48px]" src="/flags/${this.flag || "xx"}.svg" />
<span
id="flag-preview"
style="display:inline-block;width:48px;height:64px;vertical-align:middle;background:#333;border-radius:6px;overflow:hidden;"
></span>
</button>
${this.showModal
? html`
<div
class="text-white flex flex-col gap-[0.5rem] absolute top-[60px] left-[0px] w-[780%] h-[500px] max-h-[50vh] max-w-[87vw] bg-gray-900/80 backdrop-blur-md p-[10px] rounded-[8px] z-[3] ${this
.showModal
? ""
: "hidden"}"
>
<input
class="h-[2rem] border-none text-center border border-gray-300 rounded-xl shadow-sm text-2xl text-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-black dark:border-gray-300/60 dark:bg-gray-700 dark:text-white"
type="text"
placeholder="Search..."
@change=${this.handleSearch}
@keyup=${this.handleSearch}
/>
<div
class="flex flex-wrap justify-evenly gap-[1rem] overflow-y-auto overflow-x-hidden"
>
${Countries.filter(
(country) =>
country.name
.toLowerCase()
.includes(this.search.toLowerCase()) ||
country.code
.toLowerCase()
.includes(this.search.toLowerCase()),
).map(
(country) => html`
<button
@click=${() => this.setFlag(country.code)}
class="text-center cursor-pointer border-none bg-none opacity-70 sm:w-[calc(33.3333%-15px) w-[calc(100%/3-15px)] md:w-[calc(100%/4-15px)]"
>
<img
class="country-flag w-full h-auto"
src="/flags/${country.code}.svg"
/>
<span class="country-name">${country.name}</span>
</button>
`,
)}
</div>
</div>
`
: ""}
</div>
`;
}
updated() {
const preview = this.renderRoot.querySelector(
"#flag-preview",
) as HTMLElement;
if (!preview) return;
preview.innerHTML = "";
if (this.flag?.startsWith("!")) {
renderPlayerFlag(this.flag, preview);
} else {
const img = document.createElement("img");
img.src = this.flag ? `/flags/${this.flag}.svg` : `/flags/xx.svg`;
img.style.width = "100%";
img.style.height = "100%";
img.style.objectFit = "contain";
img.onerror = () => {
if (!img.src.endsWith("/flags/xx.svg")) {
img.src = "/flags/xx.svg";
}
};
preview.appendChild(img);
}
}
}
+105
View File
@@ -0,0 +1,105 @@
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import Countries from "./data/countries.json";
@customElement("flag-input-modal")
export class FlagInputModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
@state() private search: string = "";
createRenderRoot() {
return this;
}
render() {
return html`
<o-modal title="Flag Selector Modal" alwaysMaximized>
<input
class="h-[2rem] border-none text-center border border-gray-300 rounded-xl shadow-sm text-2xl text-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-black dark:border-gray-300/60 dark:bg-gray-700 dark:text-white"
type="text"
placeholder="Search..."
@change=${this.handleSearch}
@keyup=${this.handleSearch}
/>
<div
class="flex flex-wrap justify-evenly gap-[1rem] overflow-y-auto overflow-x-hidden h-[90%]"
>
${Countries.filter(
(country) =>
country.name.toLowerCase().includes(this.search.toLowerCase()) ||
country.code.toLowerCase().includes(this.search.toLowerCase()),
).map(
(country) => html`
<button
@click=${() => {
this.setFlag(country.code);
this.close();
}}
class="text-center cursor-pointer border-none bg-none opacity-70
w-[calc(100%/2-15px)] sm:w-[calc(100%/4-15px)]
md:w-[calc(100%/6-15px)] lg:w-[calc(100%/8-15px)]
xl:w-[calc(100%/10-15px)] min-w-[80px]"
>
<img
class="country-flag w-full h-auto"
src="/flags/${country.code}.svg"
@error=${(e: Event) => {
const img = e.currentTarget as HTMLImageElement;
const fallback = "/flags/xx.svg";
if (img.src && !img.src.endsWith(fallback)) {
img.src = fallback;
}
}}
/>
<span class="country-name">${country.name}</span>
</button>
`,
)}
</div>
</o-modal>
`;
}
private handleSearch(event: Event) {
this.search = (event.target as HTMLInputElement).value;
}
private setFlag(flag: string) {
localStorage.setItem("flag", flag);
this.dispatchEvent(
new CustomEvent("flag-change", {
detail: { flag },
bubbles: true,
composed: true,
}),
);
}
public open() {
this.modalEl?.open();
}
public close() {
this.modalEl?.close();
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("keydown", this.handleKeyDown);
}
disconnectedCallback() {
window.removeEventListener("keydown", this.handleKeyDown);
super.disconnectedCallback();
}
private handleKeyDown = (e: KeyboardEvent) => {
if (e.code === "Escape") {
e.preventDefault();
this.close();
}
};
}
+11
View File
@@ -11,6 +11,7 @@ import "./DarkModeButton";
import { DarkModeButton } from "./DarkModeButton";
import "./FlagInput";
import { FlagInput } from "./FlagInput";
import { FlagInputModal } from "./FlagInputModal";
import { GameStartingModal } from "./GameStartingModal";
import "./GoogleAdElement";
import { HelpModal } from "./HelpModal";
@@ -194,6 +195,16 @@ class Client {
hlpModal.open();
});
const flagInputModal = document.querySelector(
"flag-input-modal",
) as FlagInputModal;
flagInputModal instanceof FlagInputModal;
const flgInput = document.getElementById("flag-input_");
if (flgInput === null) throw new Error("Missing flag-input_");
flgInput.addEventListener("click", () => {
flagInputModal.open();
});
const territoryModal = document.querySelector(
"territory-patterns-modal",
) as TerritoryPatternsModal;
+17 -1
View File
@@ -7,6 +7,7 @@ export class OModal extends LitElement {
@state() public isModalOpen = false;
@property({ type: String }) title = "";
@property({ type: String }) translationKey = "";
@property({ type: Boolean }) alwaysMaximized = false;
static styles = css`
.c-modal {
@@ -31,6 +32,17 @@ export class OModal extends LitElement {
max-width: 860px;
}
.c-modal__wrapper.always-maximized {
width: 100%;
min-width: 340px;
max-width: 860px;
min-height: 320px;
/* Fallback for older browsers */
height: 60vh;
/* Use dvh if supported for dynamic viewport handling */
height: 60dvh;
}
.c-modal__header {
position: relative;
border-top-left-radius: 4px;
@@ -74,7 +86,11 @@ export class OModal extends LitElement {
${this.isModalOpen
? html`
<aside class="c-modal">
<div class="c-modal__wrapper">
<div
class="c-modal__wrapper ${this.alwaysMaximized
? "always-maximized"
: ""}"
>
<header class="c-modal__header">
${`${this.translationKey}` === ""
? `${this.title}`
+1
View File
@@ -411,6 +411,7 @@
<news-modal></news-modal>
<game-left-sidebar></game-left-sidebar>
<spawn-ad></spawn-ad>
<flag-input-modal></flag-input-modal>
<fps-display></fps-display>
<div
id="language-modal"