281 lines
6.4 KiB
TypeScript
281 lines
6.4 KiB
TypeScript
import { FolkShape } from "./folk-shape";
|
|
import { css, html } from "./tags";
|
|
|
|
const styles = css`
|
|
:host {
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
min-width: 260px;
|
|
min-height: 180px;
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 12px;
|
|
background: #059669;
|
|
color: white;
|
|
border-radius: 8px 8px 0 0;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: move;
|
|
}
|
|
|
|
.header-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.header-actions button {
|
|
background: transparent;
|
|
border: none;
|
|
color: white;
|
|
cursor: pointer;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.header-actions button:hover {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
.dest-body {
|
|
padding: 12px;
|
|
}
|
|
|
|
.dest-name {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
color: #1e293b;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.dest-country {
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.dest-dates {
|
|
display: flex;
|
|
gap: 12px;
|
|
font-size: 11px;
|
|
color: #475569;
|
|
margin-bottom: 10px;
|
|
padding: 6px 8px;
|
|
background: #f1f5f9;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.dest-dates span {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.dest-dates .label {
|
|
color: #94a3b8;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.dest-notes {
|
|
font-size: 12px;
|
|
color: #475569;
|
|
line-height: 1.5;
|
|
padding: 8px;
|
|
background: #fafaf9;
|
|
border-radius: 4px;
|
|
border: 1px solid #e2e8f0;
|
|
min-height: 40px;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.dest-notes[contenteditable]:focus {
|
|
outline: none;
|
|
border-color: #059669;
|
|
}
|
|
|
|
.coords {
|
|
font-size: 10px;
|
|
color: #94a3b8;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.empty-name {
|
|
color: #94a3b8;
|
|
font-style: italic;
|
|
}
|
|
`;
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"folk-destination": FolkDestination;
|
|
}
|
|
}
|
|
|
|
export class FolkDestination extends FolkShape {
|
|
static override tagName = "folk-destination";
|
|
|
|
static {
|
|
const sheet = new CSSStyleSheet();
|
|
const parentRules = Array.from(FolkShape.styles.cssRules)
|
|
.map((r) => r.cssText)
|
|
.join("\n");
|
|
const childRules = Array.from(styles.cssRules)
|
|
.map((r) => r.cssText)
|
|
.join("\n");
|
|
sheet.replaceSync(`${parentRules}\n${childRules}`);
|
|
this.styles = sheet;
|
|
}
|
|
|
|
#destName = "";
|
|
#country = "";
|
|
#lat: number | null = null;
|
|
#lng: number | null = null;
|
|
#arrivalDate = "";
|
|
#departureDate = "";
|
|
#notes = "";
|
|
#nameEl: HTMLElement | null = null;
|
|
#countryEl: HTMLElement | null = null;
|
|
#notesEl: HTMLElement | null = null;
|
|
#datesEl: HTMLElement | null = null;
|
|
|
|
get destName() { return this.#destName; }
|
|
set destName(v: string) {
|
|
this.#destName = v;
|
|
if (this.#nameEl) this.#nameEl.textContent = v || "Unnamed destination";
|
|
this.dispatchEvent(new CustomEvent("content-change"));
|
|
}
|
|
|
|
get country() { return this.#country; }
|
|
set country(v: string) {
|
|
this.#country = v;
|
|
if (this.#countryEl) this.#countryEl.textContent = v;
|
|
this.dispatchEvent(new CustomEvent("content-change"));
|
|
}
|
|
|
|
get lat() { return this.#lat; }
|
|
set lat(v: number | null) { this.#lat = v; }
|
|
|
|
get lng() { return this.#lng; }
|
|
set lng(v: number | null) { this.#lng = v; }
|
|
|
|
get arrivalDate() { return this.#arrivalDate; }
|
|
set arrivalDate(v: string) {
|
|
this.#arrivalDate = v;
|
|
this.#renderDates();
|
|
this.dispatchEvent(new CustomEvent("content-change"));
|
|
}
|
|
|
|
get departureDate() { return this.#departureDate; }
|
|
set departureDate(v: string) {
|
|
this.#departureDate = v;
|
|
this.#renderDates();
|
|
this.dispatchEvent(new CustomEvent("content-change"));
|
|
}
|
|
|
|
get notes() { return this.#notes; }
|
|
set notes(v: string) {
|
|
this.#notes = v;
|
|
if (this.#notesEl && this.#notesEl.textContent !== v) {
|
|
this.#notesEl.textContent = v;
|
|
}
|
|
this.dispatchEvent(new CustomEvent("content-change"));
|
|
}
|
|
|
|
override createRenderRoot() {
|
|
const root = super.createRenderRoot();
|
|
|
|
const wrapper = document.createElement("div");
|
|
wrapper.innerHTML = html`
|
|
<div class="header">
|
|
<span class="header-title">
|
|
<span>\uD83D\uDCCD</span>
|
|
<span>Destination</span>
|
|
</span>
|
|
<div class="header-actions">
|
|
<button class="close-btn" title="Close">\u00D7</button>
|
|
</div>
|
|
</div>
|
|
<div class="dest-body">
|
|
<div class="dest-name"></div>
|
|
<div class="dest-country"></div>
|
|
<div class="dest-dates"></div>
|
|
<div class="dest-notes" contenteditable="true"></div>
|
|
</div>
|
|
`;
|
|
|
|
const slot = root.querySelector("slot");
|
|
const containerDiv = slot?.parentElement as HTMLElement;
|
|
if (containerDiv) {
|
|
containerDiv.replaceWith(wrapper);
|
|
}
|
|
|
|
this.#nameEl = wrapper.querySelector(".dest-name");
|
|
this.#countryEl = wrapper.querySelector(".dest-country");
|
|
this.#datesEl = wrapper.querySelector(".dest-dates");
|
|
this.#notesEl = wrapper.querySelector(".dest-notes");
|
|
|
|
if (this.#nameEl) {
|
|
this.#nameEl.textContent = this.#destName || "Unnamed destination";
|
|
if (!this.#destName) this.#nameEl.classList.add("empty-name");
|
|
}
|
|
if (this.#countryEl) this.#countryEl.textContent = this.#country;
|
|
|
|
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
|
closeBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
this.dispatchEvent(new CustomEvent("close"));
|
|
});
|
|
|
|
if (this.#notesEl) {
|
|
this.#notesEl.textContent = this.#notes;
|
|
this.#notesEl.addEventListener("input", () => {
|
|
this.#notes = this.#notesEl!.textContent || "";
|
|
this.dispatchEvent(new CustomEvent("content-change"));
|
|
});
|
|
this.#notesEl.addEventListener("click", (e) => e.stopPropagation());
|
|
}
|
|
|
|
this.#renderDates();
|
|
return root;
|
|
}
|
|
|
|
#renderDates() {
|
|
if (!this.#datesEl) return;
|
|
if (!this.#arrivalDate && !this.#departureDate) {
|
|
this.#datesEl.style.display = "none";
|
|
return;
|
|
}
|
|
this.#datesEl.style.display = "flex";
|
|
let result = "";
|
|
if (this.#arrivalDate) {
|
|
const d = new Date(this.#arrivalDate);
|
|
result += `<span><span class="label">Arrive:</span> ${d.toLocaleDateString("en-US", { month: "short", day: "numeric" })}</span>`;
|
|
}
|
|
if (this.#departureDate) {
|
|
const d = new Date(this.#departureDate);
|
|
result += `<span><span class="label">Depart:</span> ${d.toLocaleDateString("en-US", { month: "short", day: "numeric" })}</span>`;
|
|
}
|
|
this.#datesEl.innerHTML = result;
|
|
}
|
|
|
|
override toJSON() {
|
|
return {
|
|
...super.toJSON(),
|
|
type: "folk-destination",
|
|
destName: this.#destName,
|
|
country: this.#country,
|
|
lat: this.#lat,
|
|
lng: this.#lng,
|
|
arrivalDate: this.#arrivalDate,
|
|
departureDate: this.#departureDate,
|
|
notes: this.#notes,
|
|
};
|
|
}
|
|
}
|