aboutsummaryrefslogtreecommitdiffstats
path: root/app/components.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/components.js')
-rw-r--r--app/components.js812
1 files changed, 364 insertions, 448 deletions
diff --git a/app/components.js b/app/components.js
index 42d7242..e1506a3 100644
--- a/app/components.js
+++ b/app/components.js
@@ -15,16 +15,17 @@ export class Histogram {
#currentParameter = HouseParameter.price;
/**
- * @param {number[]} values
- * @param {number} bins
- * @param {(min: number, max: number) => void} onBarClick
- * @param {string} parameter
+ * @param {Object} options
+ * @param {number[]} options.values
+ * @param {number} options.bins
+ * @param {(min: number, max: number) => void} options.onBarClick
+ * @param {string} options.parameter
*/
- constructor(values, bins, onBarClick, parameter) {
- this.#values = values.filter((v) => !Number.isNaN(v) && Number.isFinite(v));
- this.#bins = bins;
- this.#onBarClick = onBarClick;
- this.#currentParameter = parameter;
+ constructor(options) {
+ this.#values = options.values.filter((v) => !Number.isNaN(v) && Number.isFinite(v));
+ this.#bins = options.bins;
+ this.#onBarClick = options.onBarClick;
+ this.#currentParameter = options.parameter;
this.#rootElement = this.#render();
}
@@ -191,14 +192,13 @@ export class Histogram {
}),
);
- // Add event listeners
bar.addEventListener("mouseenter", () => {
bar.setAttribute("fill", "#2e7d32");
});
bar.addEventListener("mouseleave", () => {
bar.setAttribute("fill", "#4caf50");
});
- bar.addEventListener("click", () => {
+ bar.addEventListener("pointerdown", () => {
this.#onBarClick(bin.min, bin.max);
});
@@ -336,6 +336,131 @@ export class Histogram {
export class Widgets {
/**
+ * Create a range filter with label
+ * @param {string} label - Filter label
+ * @param {number} min - Minimum value
+ * @param {number} max - Maximum value
+ * @param {number} currentMin - Current minimum value
+ * @param {number} currentMax - Current maximum value
+ * @param {(min: number, max: number) => void} onChange - Change callback
+ * @param {number} step - Step size (default: 1)
+ * @param {DomOptions} domOptions - DOM options
+ * @returns {HTMLElement}
+ */
+ static range(
+ label,
+ min,
+ max,
+ currentMin,
+ currentMax,
+ onChange,
+ step = 1,
+ domOptions = new DomOptions(),
+ ) {
+ const id = domOptions.id || `range-${label.toLowerCase().replace(/\s+/g, "-")}`;
+
+ return Dom.div(
+ new DomOptions({
+ attributes: domOptions.attributes,
+ children: [
+ Dom.label(
+ id,
+ label,
+ new DomOptions({
+ styles: {
+ fontSize: "0.85rem",
+ fontWeight: "bold",
+ marginBottom: "0.25rem",
+ },
+ }),
+ ),
+ Dom.range(
+ min,
+ max,
+ currentMin,
+ currentMax,
+ step,
+ onChange,
+ new DomOptions({
+ id,
+ styles: {
+ marginBottom: "1.5rem",
+ ...domOptions.styles,
+ },
+ }),
+ ),
+ ],
+ classes: domOptions.classes,
+ styles: {
+ display: "flex",
+ flexDirection: "column",
+ ...domOptions.styles,
+ },
+ }),
+ );
+ }
+
+ /**
+ * Create a dropdown (select) component with label
+ * @param {string} label - Label text
+ * @param {Array<{value: string, text: string}>} options - Dropdown options
+ * @param {string} defaultValue - Default selected value
+ * @param {(value: string) => void} onChange - Change callback
+ * @param {DomOptions} domOptions - DOM options for the container
+ * @returns {HTMLDivElement}
+ */
+ static dropdown(label, options, defaultValue, onChange, domOptions = new DomOptions()) {
+ const selectId = domOptions.id || `dropdown-${Math.random().toString(36).substr(2, 9)}`;
+
+ return Dom.div(
+ new DomOptions({
+ attributes: domOptions.attributes,
+ children: [
+ Dom.label(
+ selectId,
+ label,
+ new DomOptions({
+ styles: {
+ fontSize: "0.85rem",
+ fontWeight: "bold",
+ marginBottom: "0.25rem",
+ },
+ }),
+ ),
+ Dom.select(
+ defaultValue,
+ (e) => {
+ const target = /** @type {HTMLSelectElement} */ (e.target);
+ onChange(target.value);
+ },
+ new DomOptions({
+ children: options.map((opt) =>
+ Dom.option(opt.value, opt.text, opt.value === defaultValue),
+ ),
+ id: selectId,
+ styles: {
+ border: "1px solid #ddd",
+ borderRadius: "4px",
+ fontSize: "0.9rem",
+ padding: "0.5rem",
+ width: "100%",
+ },
+ }),
+ ),
+ ],
+ classes: domOptions.classes,
+ id: domOptions.id ? `${domOptions.id}-container` : undefined,
+ styles: {
+ display: "flex",
+ flexDirection: "column",
+ marginBottom: "1rem",
+ ...domOptions.styles,
+ },
+ }),
+ );
+ }
+
+ /**
* Show toast notification
* @param {string} message
* @param {ToastType} [type=ToastType.error]
@@ -490,63 +615,23 @@ export class Widgets {
}),
);
}
-
- /**
- * Create a number filter input
- * @param {string} id
- * @param {string} labelText
- * @param {string|number} value
- * @param {(value: number | null) => void} onChange
- * @returns {HTMLElement}
- */
- static numberFilter(id, labelText, value, onChange) {
- return Dom.div(
- new DomOptions({
- children: [
- Dom.label(
- id,
- labelText,
- new DomOptions({
- styles: { fontSize: "0.85rem", fontWeight: "bold", marginBottom: "0.25rem" },
- }),
- ),
- Dom.input(
- "number",
- (e) => {
- const target = /** @type {HTMLInputElement} */ (e.target);
- const raw = target.value.trim();
- onChange(raw === "" ? null : Number(raw));
- },
- value,
- "any",
- new DomOptions({
- id,
- styles: {
- border: "1px solid #ddd",
- borderRadius: "4px",
- fontSize: "0.9rem",
- padding: "0.5rem",
- },
- }),
- ),
- ],
- styles: { display: "flex", flexDirection: "column", marginBottom: "1.75rem" },
- }),
- );
- }
}
export class Sidebar {
/** @type {Histogram} */
#histogram;
- /** @type {House[]|null} */
- #allHouses = [];
+ /** @type {House[]} */
+ #allHouses;
/** @type {HTMLElement} */
#rootElement;
/** @type {boolean} */
#collapsed = false;
/** @type {Filters} */
#filters;
+ /** @type {AreaParam} */
+ #areaParam;
+ /** @type {HouseParameter} */
+ #houseParam;
/** @type {Weights} */
#weights;
/** @type {() => void} */
@@ -561,39 +646,35 @@ export class Sidebar {
#filtersSectionElement;
/**
- * @param {House[]|null} allHouses
- * @param {Filters} filters
- * @param {Weights} weights
- * @param {() => void} onFilterChange
- * @param {(key: string, value: number) => void} onWeightChange
- * @param {(param: string) => void} onColorChange
- * @param {(param: string) => void} onAreaColorChange
+ * @param {Object} options
+ * @param {House[]} options.allHouses
+ * @param {AreaParam} options.areaParam *
+ * @param {AreaParam} options.houseParam
+ * @param {Filters} options.filters
+ * @param {Weights} options.weights
+ * @param {() => void} options.onFilterChange
+ * @param {(key: string, value: number) => void} options.onWeightChange
+ * @param {(param: string) => void} options.onHouseParamChange
+ * @param {(param: string) => void} options.onAreaParamChange
*/
- constructor(
- allHouses,
- filters,
- weights,
- onFilterChange,
- onWeightChange,
- onColorChange,
- onAreaColorChange,
- ) {
- this.#allHouses = allHouses;
- this.#filters = filters;
- this.#weights = weights;
- this.#onFilterChange = onFilterChange;
- this.#onWeightChange = onWeightChange;
- this.#onColorChange = onColorChange;
- this.#onAreaColorChange = onAreaColorChange;
-
- const initialParam = HouseParameter.price;
- const initialValues = this.#allHouses?.map((house) => house.get(initialParam));
- this.#histogram = new Histogram(
- initialValues || [],
- 5,
- (min, max) => this.#handleHistogramClick(min, max),
- initialParam,
- );
+ constructor(options) {
+ this.#areaParam = options.areaParam;
+ this.#houseParam = options.houseParam;
+ this.#allHouses = options.allHouses;
+ this.#filters = options.filters;
+ this.#weights = options.weights;
+ this.#onFilterChange = options.onFilterChange;
+ this.#onWeightChange = options.onWeightChange;
+ this.#onColorChange = options.onHouseParamChange;
+ this.#onAreaColorChange = options.onAreaParamChange;
+
+ const initialValues = this.#allHouses?.map((house) => house.get(this.#houseParam));
+ this.#histogram = new Histogram({
+ bins: 5,
+ onBarClick: (min, max) => this.#handleHistogramClick(min, max),
+ parameter: this.#houseParam,
+ values: initialValues || [],
+ });
this.#filtersSectionElement = null;
this.#rootElement = this.#render();
@@ -697,10 +778,12 @@ export class Sidebar {
}
/**
+ * @param {AreaParam} areaParam
+ * @param {HouseParameter} houseParam
* @param {(param: string) => void} onHouseChange
* @param {(param: string) => void} onAreaChange
*/
- static dataSection(onHouseChange, onAreaChange) {
+ static dataSection(areaParam, houseParam, onHouseChange, onAreaChange) {
return Dom.div(
new DomOptions({
children: [
@@ -715,89 +798,36 @@ export class Sidebar {
},
}),
),
- Dom.div(
+ Widgets.dropdown(
+ "Color houses by",
+ [
+ { text: "Price", value: HouseParameter.price },
+ { text: "Score", value: HouseParameter.score },
+ { text: "Construction Year", value: HouseParameter.year },
+ { text: "Living Area", value: HouseParameter.area },
+ ],
+ houseParam,
+ (value) => onHouseChange(value),
new DomOptions({
- children: [
- Dom.label(
- "color-parameter",
- "Color houses by",
- new DomOptions({
- styles: {
- fontSize: "0.85rem",
- fontWeight: "bold",
- marginBottom: "0.25rem",
- },
- }),
- ),
- Dom.select(
- (e) => {
- const target = /** @type {HTMLSelectElement} */ (e.target);
- onHouseChange(target.value);
- },
- new DomOptions({
- children: [
- Dom.option(HouseParameter.price, "Price"),
- Dom.option(HouseParameter.score, "Score"),
- Dom.option(HouseParameter.year, "Construction Year"),
- Dom.option(HouseParameter.area, "Living Area"),
- ],
- id: "color-parameter",
- styles: {
- border: "1px solid #ddd",
- borderRadius: "4px",
- fontSize: "0.9rem",
- padding: "0.5rem",
- width: "100%",
- },
- }),
- ),
- ],
+ id: "color-parameter",
styles: {
- display: "flex",
- flexDirection: "column",
marginBottom: "1rem",
},
}),
),
- Dom.div(
+ Widgets.dropdown(
+ "Color areas by",
+ [
+ { text: "None", value: AreaParam.none },
+ { text: "Foreign speakers", value: AreaParam.foreignSpeakers },
+ { text: "Unemployment rate", value: AreaParam.unemploymentRate },
+ { text: "Average income", value: AreaParam.averageIncome },
+ { text: "Higher education", value: AreaParam.higherEducation },
+ ],
+ areaParam,
+ (value) => onAreaChange(value),
new DomOptions({
- children: [
- Dom.label(
- "area-color-parameter",
- "Color areas by",
- new DomOptions({
- styles: {
- fontSize: "0.85rem",
- fontWeight: "bold",
- marginBottom: "0.25rem",
- },
- }),
- ),
- Dom.select(
- (e) => {
- const target = /** @type {HTMLSelectElement} */ (e.target);
- onAreaChange(target.value);
- },
- new DomOptions({
- children: [
- Dom.option(AreaParam.none, "None"),
- Dom.option(AreaParam.foreignSpeakers, "Foreign speakers"),
- Dom.option(AreaParam.unemploymentRate, "Unemployment rate"),
- Dom.option(AreaParam.averageIncome, "Average income"),
- Dom.option(AreaParam.higherEducation, "Higher education"),
- ],
- id: "area-color-parameter",
- styles: {
- border: "1px solid #ddd",
- borderRadius: "4px",
- fontSize: "0.9rem",
- padding: "0.5rem",
- width: "100%",
- },
- }),
- ),
- ],
- styles: { display: "flex", flexDirection: "column" },
+ id: "area-color-parameter",
}),
),
],
@@ -810,31 +840,31 @@ export class Sidebar {
}
/**
* @param {Filters} filters
+ * @param {House[]} houses
* @param {() => void} onChange
*/
- static filtersSection(filters, onChange) {
- // Calculate reasonable ranges based on typical values
+ static filtersSection(filters, houses, onChange) {
const priceRange = {
- max: 1000000,
- min: 0,
+ max: filters.maxPrice,
+ min: filters.minPrice,
step: 10000,
};
const yearRange = {
- max: new Date().getFullYear(),
- min: 1800,
+ max: filters.maxYear,
+ min: filters.minYear,
step: 1,
};
const areaRange = {
- max: 500,
- min: 0,
+ max: filters.maxArea,
+ min: filters.minArea,
step: 10,
};
const lotRange = {
- max: 5000,
- min: 0,
+ max: filters.maxArea,
+ min: filters.minArea,
step: 100,
};
@@ -853,167 +883,69 @@ export class Sidebar {
}),
),
- // Price Range
- Dom.div(
+ Widgets.range(
+ "Price Range (€)",
+ priceRange.min,
+ priceRange.max,
+ filters.minPrice,
+ filters.maxPrice,
+ (min, max) => {
+ filters.minPrice = min;
+ filters.maxPrice = max === priceRange.max ? Number.POSITIVE_INFINITY : max;
+ onChange();
+ },
+ priceRange.step,
new DomOptions({
- children: [
- Dom.label(
- "price-range",
- "Price Range (€)",
- new DomOptions({
- styles: {
- fontSize: "0.85rem",
- fontWeight: "bold",
- marginBottom: "0.25rem",
- },
- }),
- ),
- Dom.range(
- priceRange.min,
- priceRange.max,
- filters.minPrice || priceRange.min,
- filters.maxPrice === Number.POSITIVE_INFINITY ? priceRange.max : filters.maxPrice,
- priceRange.step,
- (min, max) => {
- filters.minPrice = min;
- filters.maxPrice = max === priceRange.max ? Number.POSITIVE_INFINITY : max;
- onChange();
- },
- new DomOptions({
- id: "price-range",
- styles: {
- marginBottom: "1.5rem",
- },
- }),
- ),
- ],
- styles: {
- display: "flex",
- flexDirection: "column",
- },
+ id: "price-range",
}),
),
-
- // Construction Year Range
- Dom.div(
+ Widgets.range(
+ "Construction Year",
+ yearRange.min,
+ yearRange.max,
+ filters.minYear,
+ filters.maxYear,
+ (min, max) => {
+ filters.minYear = min;
+ filters.maxYear = max === yearRange.max ? Number.POSITIVE_INFINITY : max;
+ onChange();
+ },
+ yearRange.step,
new DomOptions({
- children: [
- Dom.label(
- "year-range",
- "Construction Year",
- new DomOptions({
- styles: {
- fontSize: "0.85rem",
- fontWeight: "bold",
- marginBottom: "0.25rem",
- },
- }),
- ),
- Dom.range(
- yearRange.min,
- yearRange.max,
- filters.minYear || yearRange.min,
- filters.maxYear === Number.POSITIVE_INFINITY ? yearRange.max : filters.maxYear,
- yearRange.step,
- (min, max) => {
- filters.minYear = min;
- filters.maxYear = max === yearRange.max ? Number.POSITIVE_INFINITY : max;
- onChange();
- },
- new DomOptions({
- id: "year-range",
- styles: {
- marginBottom: "1.5rem",
- },
- }),
- ),
- ],
- styles: {
- display: "flex",
- flexDirection: "column",
- },
+ id: "year-range",
}),
),
- // Living Area Range
- Dom.div(
+ Widgets.range(
+ "Living Area (m²)",
+ areaRange.min,
+ areaRange.max,
+ filters.minArea,
+ filters.maxArea,
+ (min, max) => {
+ filters.minArea = min;
+ filters.maxArea = max === areaRange.max ? Number.POSITIVE_INFINITY : max;
+ onChange();
+ },
+ areaRange.step,
new DomOptions({
- children: [
- Dom.label(
- "area-range",
- "Living Area (m²)",
- new DomOptions({
- styles: {
- fontSize: "0.85rem",
- fontWeight: "bold",
- marginBottom: "0.25rem",
- },
- }),
- ),
- Dom.range(
- areaRange.min,
- areaRange.max,
- filters.minArea || areaRange.min,
- filters.maxArea === Number.POSITIVE_INFINITY ? areaRange.max : filters.maxArea,
- areaRange.step,
- (min, max) => {
- filters.minArea = min;
- filters.maxArea = max === areaRange.max ? Number.POSITIVE_INFINITY : max;
- onChange();
- },
- new DomOptions({
- id: "area-range",
- styles: {
- marginBottom: "1.5rem",
- },
- }),
- ),
- ],
- styles: {
- display: "flex",
- flexDirection: "column",
- },
+ id: "area-range",
}),
),
-
- // Lot Size Range
- Dom.div(
+ Widgets.range(
+ "Lot Size (m²)",
+ lotRange.min,
+ lotRange.max,
+ filters.minLot,
+ filters.maxLot,
+ (min, max) => {
+ filters.minLot = min;
+ filters.maxLot = max === lotRange.max ? Number.POSITIVE_INFINITY : max;
+ onChange();
+ },
+ lotRange.step,
new DomOptions({
- children: [
- Dom.label(
- "lot-range",
- "Lot Size (m²)",
- new DomOptions({
- styles: {
- fontSize: "0.85rem",
- fontWeight: "bold",
- marginBottom: "0.25rem",
- },
- }),
- ),
- Dom.range(
- lotRange.min,
- lotRange.max,
- filters.minLot || lotRange.min,
- filters.maxLot === Number.POSITIVE_INFINITY ? lotRange.max : filters.maxLot,
- lotRange.step,
- (min, max) => {
- filters.minLot = min;
- filters.maxLot = max === lotRange.max ? Number.POSITIVE_INFINITY : max;
- onChange();
- },
- new DomOptions({
- id: "lot-range",
- styles: {
- marginBottom: "1.5rem",
- },
- }),
- ),
- ],
- styles: {
- display: "flex",
- flexDirection: "column",
- },
+ id: "lot-range",
}),
),
@@ -1033,6 +965,7 @@ export class Sidebar {
}),
),
Dom.select(
+ undefined,
(e) => {
const target = /** @type {HTMLSelectElement} */ (e.target);
const selectedOptions = Array.from(target.selectedOptions).map(
@@ -1043,7 +976,7 @@ export class Sidebar {
},
new DomOptions({
attributes: { multiple: "true" },
- children: [],
+ children: [...Sidebar.#renderDistrictOptions(houses)],
id: "district-select",
styles: {
border: "1px solid #ddd",
@@ -1067,15 +1000,7 @@ export class Sidebar {
"Clear All Filters",
() => {
// Reset all ranges to their maximum possible values
- filters.minPrice = 0;
- filters.maxPrice = Number.POSITIVE_INFINITY;
- filters.minYear = 1800;
- filters.maxYear = Number.POSITIVE_INFINITY;
- filters.minArea = 0;
- filters.maxArea = Number.POSITIVE_INFINITY;
- filters.minLot = 0;
- filters.maxLot = Number.POSITIVE_INFINITY;
- filters.districts = [];
+ filters.reset();
// Update the UI by triggering onChange
onChange();
@@ -1105,30 +1030,26 @@ export class Sidebar {
/**
* Update sidebar with new data
- * @param {House[]} houses
- * @param {string} histogramParameter
+ * @param {HouseParameter} houseParam
*/
- update(houses, histogramParameter) {
- this.#allHouses = houses;
-
- // Update histogram
- const values = houses.map((house) => house.get(histogramParameter));
- this.#histogram.update(values, histogramParameter);
-
- // Update filters section
- this.#updateFiltersSection();
-
- // Update districts
- this.updateDistricts(houses);
+ update(houseParam) {
+ const values = this.#allHouses.map((house) => house.get(houseParam));
+ this.#histogram.update(values, houseParam);
+ this.#updateFiltersSection(this.#allHouses);
}
/**
* Update filters section with current data
+ * @param {House[]} houses
*/
- #updateFiltersSection() {
- if (!this.#allHouses || this.#allHouses.length === 0) return;
+ #updateFiltersSection(houses) {
+ if (!houses || houses.length === 0) return;
- const newFiltersSection = Sidebar.filtersSection(this.#filters, this.#onFilterChange);
+ const newFiltersSection = Sidebar.filtersSection(
+ this.#filters,
+ this.#allHouses,
+ this.#onFilterChange,
+ );
if (this.#filtersSectionElement) {
this.#filtersSectionElement.replaceWith(newFiltersSection);
@@ -1143,7 +1064,7 @@ export class Sidebar {
* @param {number} max
*/
#handleHistogramClick(min, max) {
- const param = document.getElementById("color-parameter")?.value || HouseParameter.price;
+ const param = this.#houseParam;
switch (param) {
case HouseParameter.price: {
@@ -1231,8 +1152,13 @@ export class Sidebar {
new DomOptions({
children: [
Sidebar.histogramSection(this.#histogram),
- Sidebar.dataSection(this.#onColorChange, this.#onAreaColorChange),
- Sidebar.filtersSection(this.#filters, this.#onFilterChange),
+ Sidebar.dataSection(
+ this.#areaParam,
+ this.#houseParam,
+ this.#onColorChange,
+ this.#onAreaColorChange,
+ ),
+ Sidebar.filtersSection(this.#filters, this.#allHouses, this.#onFilterChange),
Sidebar.weightSection(this.#weights, this.#onWeightChange),
],
id: "sidebar-content",
@@ -1303,22 +1229,10 @@ export class Sidebar {
}
/**
- * Update district options in the multi-select
- * @param {House[]} houses
- */
- updateDistricts(houses) {
- const districtOptions = this.#renderDistrictOptions(houses);
- const districtSelect = this.#rootElement.querySelector("#district-select");
- if (districtSelect) {
- districtSelect.append(...districtOptions);
- }
- }
-
- /**
* Set the area color parameter in the dropdown
* @param {string} param
*/
- setAreaColorParameter(param) {
+ setAreaParameter(param) {
const areaColorSelect = this.#rootElement.querySelector("#area-color-parameter");
if (areaColorSelect) {
areaColorSelect.value = param;
@@ -1330,7 +1244,7 @@ export class Sidebar {
* @param {House[]} houses
* @returns {HTMLOptionElement[]}
*/
- #renderDistrictOptions(houses) {
+ static #renderDistrictOptions(houses) {
const houseDistricts = [...new Set(houses.map((h) => h.district).filter((d) => d))].sort();
return houseDistricts.map((districtName) => Dom.option(districtName, districtName));
}
@@ -1351,6 +1265,86 @@ export class Modal {
#onClearMapTimer;
/**
+ * @param {Object} options
+ * @param {House} options.house
+ * @param {boolean} options.persistent
+ * @param {object} options.positionStyles
+ * @param {() => void} options.onHide
+ * @param {() => void} options.onClearMapTimer
+ */
+ constructor(options) {
+ this.#persistent = options.persistent;
+ this.#onHide = options.onHide;
+ this.#onClearMapTimer = options.onClearMapTimer;
+
+ this.#abortController = new AbortController();
+ this.#dialog = document.createElement("dialog");
+
+ Object.assign(
+ this.#dialog.style,
+ {
+ background: "white",
+ border: "none",
+ borderRadius: "8px",
+ boxShadow: "0 4px 20px rgba(0,0,0,0.2)",
+ maxHeight: "80vh",
+ maxWidth: "600px",
+ overflowY: "auto",
+ padding: "20px",
+ position: "fixed",
+ top: "50%",
+ transform: "translateY(-50%)",
+ width: "90%",
+ zIndex: "1000",
+ },
+ options.positionStyles,
+ );
+
+ this.#dialog.append(
+ Dom.button(
+ "x",
+ () => this.hide(),
+ new DomOptions({
+ id: "close-modal-btn",
+ styles: {
+ background: "none",
+ border: "none",
+ color: "#666",
+ cursor: "pointer",
+ fontSize: "24px",
+ position: "absolute",
+ right: "10px",
+ top: "10px",
+ },
+ }),
+ ),
+ Modal.content(options.house),
+ );
+
+ // Add event listeners with AbortController
+ this.#dialog.addEventListener("close", () => this.hide(), {
+ signal: this.#abortController.signal,
+ });
+ this.#dialog.addEventListener(
+ "mouseenter",
+ () => {
+ clearTimeout(this.#timer);
+ this.#onClearMapTimer();
+ },
+ { signal: this.#abortController.signal },
+ );
+ this.#dialog.addEventListener(
+ "mouseleave",
+ () => {
+ if (!this.#persistent) {
+ this.#timer = window.setTimeout(() => this.hide(), 200);
+ }
+ },
+ { signal: this.#abortController.signal },
+ );
+ }
+
+ /**
* @param {House} house
*/
static imageSection(house) {
@@ -1562,84 +1556,6 @@ export class Modal {
return frag;
}
- /**
- * @param {House} house
- * @param {boolean} persistent
- * @param {object} positionStyles
- * @param {() => void} onHide
- * @param {() => void} onClearMapTimer
- */
- constructor(house, persistent, positionStyles, onHide, onClearMapTimer) {
- this.#persistent = persistent;
- this.#onHide = onHide;
- this.#onClearMapTimer = onClearMapTimer;
- this.#abortController = new AbortController();
- this.#dialog = document.createElement("dialog");
-
- Object.assign(
- this.#dialog.style,
- {
- background: "white",
- border: "none",
- borderRadius: "8px",
- boxShadow: "0 4px 20px rgba(0,0,0,0.2)",
- maxHeight: "80vh",
- maxWidth: "600px",
- overflowY: "auto",
- padding: "20px",
- position: "fixed",
- top: "50%",
- transform: "translateY(-50%)",
- width: "90%",
- zIndex: "1000",
- },
- positionStyles,
- );
-
- this.#dialog.append(
- Dom.button(
- "x",
- () => this.hide(),
- new DomOptions({
- id: "close-modal-btn",
- styles: {
- background: "none",
- border: "none",
- color: "#666",
- cursor: "pointer",
- fontSize: "24px",
- position: "absolute",
- right: "10px",
- top: "10px",
- },
- }),
- ),
- Modal.content(house),
- );
-
- // Add event listeners with AbortController
- this.#dialog.addEventListener("close", () => this.hide(), {
- signal: this.#abortController.signal,
- });
- this.#dialog.addEventListener(
- "mouseenter",
- () => {
- clearTimeout(this.#timer);
- this.#onClearMapTimer();
- },
- { signal: this.#abortController.signal },
- );
- this.#dialog.addEventListener(
- "mouseleave",
- () => {
- if (!this.#persistent) {
- this.#timer = window.setTimeout(() => this.hide(), 200);
- }
- },
- { signal: this.#abortController.signal },
- );
- }
-
render() {
return this.#dialog;
}