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>📍</span>
|
||
<span>Destination</span>
|
||
</span>
|
||
<div class="header-actions">
|
||
<button class="close-btn" title="Close">×</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,
|
||
};
|
||
}
|
||
}
|