1. قبل از شروع
این کد لبه به شما می آموزد که چگونه یک برنامه جستجوی محلی کاملا تعاملی با استفاده از کیت رابط کاربری Google Maps Platform Places بسازید.
پیش نیازها
- یک پروژه Google Cloud با APIها و اعتبارنامههای لازم پیکربندی شده است.
- دانش اولیه HTML و CSS.
- آشنایی با جاوا اسکریپت مدرن
- یک مرورگر وب مدرن، مانند آخرین نسخه کروم.
- یک ویرایشگر متن به انتخاب شما.
کاری که خواهی کرد
- یک برنامه نقشه برداری را با استفاده از کلاس جاوا اسکریپت ساختار دهید.
- از Web Components برای نمایش نقشه استفاده کنید
- از عنصر جستجوی مکان برای انجام و نمایش نتایج جستجوی متن استفاده کنید.
- نشانگرهای نقشه
AdvancedMarkerElement
سفارشی را به صورت برنامه ای ایجاد و مدیریت کنید. - هنگامی که کاربر مکانی را انتخاب می کند، عنصر جزئیات مکان را نمایش دهید.
- از Geocoding API برای ایجاد یک رابط کاربری پویا و کاربرپسند استفاده کنید.
آنچه شما نیاز دارید
- یک پروژه Google Cloud با فعال کردن صورتحساب
- یک کلید API پلتفرم Google Maps
- شناسه نقشه
- API های زیر فعال هستند:
- Maps JavaScript API
- کیت UI مکان ها
- API کدگذاری جغرافیایی
2. راه اندازی شوید
برای مرحله فعال سازی زیر، باید Maps JavaScript API، Places UI Kit و Geocoding API را فعال کنید.
پلتفرم نقشه های گوگل را راه اندازی کنید
اگر قبلاً یک حساب Google Cloud Platform و پروژهای با صورتحساب فعال ندارید، لطفاً راهنمای شروع به کار با Google Maps Platform را برای ایجاد یک حساب صورتحساب و یک پروژه ببینید.
- در Cloud Console ، روی منوی کشویی پروژه کلیک کنید و پروژه ای را که می خواهید برای این کد لبه استفاده کنید انتخاب کنید.
- APIها و SDKهای پلتفرم Google Maps مورد نیاز برای این لبه کد را در Google Cloud Marketplace فعال کنید. برای انجام این کار، مراحل این ویدیو یا این مستند را دنبال کنید.
- یک کلید API در صفحه Credentials در Cloud Console ایجاد کنید. می توانید مراحل این ویدئو یا این مستند را دنبال کنید. همه درخواستها به پلتفرم Google Maps به یک کلید API نیاز دارند.
3. پوسته برنامه و نقشه عملکردی
در این مرحله اول، ما طرح بصری کاملی را برای برنامه خود ایجاد می کنیم و یک ساختار تمیز و مبتنی بر کلاس برای جاوا اسکریپت خود ایجاد می کنیم. این به ما یک پایه محکم برای ساختن می دهد. در پایان این بخش، یک صفحه استایل دار خواهید داشت که یک نقشه تعاملی را نمایش می دهد.
فایل HTML را ایجاد کنید
ابتدا یک فایل با نام index.html
ایجاد کنید. این فایل شامل ساختار کامل برنامه ما، از جمله هدر، فیلترهای جستجو، نوار کناری، ظرف نقشه و اجزای وب ضروری است.
کد زیر را در index.html
کپی کنید. مطمئن شوید که YOUR_API_KEY_HERE
با کلید API پلتفرم Google Maps خود و DEMO_MAP_ID
با شناسه نقشه پلتفرم Google Maps خود جایگزین کنید.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Local Search App</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Google Fonts: Roboto -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<!-- GMP Bootstrap Loader -->
<script>
(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
key: "YOUR_API_KEY_HERE",
v: "weekly",
libraries: "places,maps,marker,geocoding"
});
</script>
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<!-- Header for search controls -->
<header class="top-header">
<div class="logo">
<svg viewBox="0 0 24 24" width="28" height="28"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" fill="currentColor"></path></svg>
<span>PlaceFinder</span>
</div>
<div class="search-container">
<input
type="text"
id="query-input"
placeholder="e.g., burger in New York"
value="burger"
/>
<button id="search-button" aria-label="Search">Search</button>
</div>
<div class="filter-container">
<label class="open-now-label">
<input type="checkbox" id="open-now-filter"> Open Now
</label>
<select id="rating-filter" aria-label="Minimum rating">
<option value="0" selected>Any rating</option>
<option value="1">1+ ★</option>
<option value="2">2+ ★★</option>
<option value="3">3+ ★★★</option>
<option value="4">4+ ★★★★</option>
<option value="5">5 ★★★★★</option>
</select>
<select id="price-filter" aria-label="Price level">
<option value="0" selected>Any Price</option>
<option value="1">$</option>
<option value="2">$$</option>
<option value="3">$$$</option>
<option value="4">$$$$</option>
</select>
</div>
</header>
<!-- Main content area -->
<div class="app-container">
<!-- Left Panel: Results -->
<div class="sidebar">
<div class="results-header">
<h2 id="results-header-text">Results</h2>
</div>
<div class="results-container">
<gmp-place-search id="place-search-list" class="hidden" selectable>
<gmp-place-all-content></gmp-place-all-content>
<gmp-place-text-search-request></gmp-place-text-search-request>
</gmp-place-search>
<div id="placeholder-message" class="placeholder">
<p>Your search results will appear here.</p>
</div>
<div id="loading-spinner" class="spinner-overlay">
<div class="spinner"></div>
</div>
</div>
</div>
<!-- Right Panel: Map -->
<div class="map-container">
<gmp-map
center="40.758896,-73.985130"
zoom="13"
map-id="DEMO_MAP_ID"
>
</gmp-map>
<div id="details-container">
<gmp-place-details-compact>
<gmp-place-details-place-request></gmp-place-details-place-request>
<gmp-place-all-content></gmp-place-all-content>
</gmp-place-details-compact>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
فایل CSS را ایجاد کنید
سپس یک فایل با نام style.css
ایجاد کنید. ما اکنون تمام استایل های لازم را اضافه می کنیم تا از همان ابتدا ظاهری تمیز و مدرن ایجاد کنیم. این CSS طرح کلی، رنگ ها، فونت ها و ظاهر تمام عناصر رابط کاربری ما را کنترل می کند.
کد زیر را در style.css
کپی کنید:
/* style.css */
:root {
--primary-color: #1a73e8;
--text-color: #202124;
--text-color-light: #5f6368;
--background-color: #f8f9fa;
--panel-background: #ffffff;
--border-color: #dadce0;
--shadow-color: rgba(0, 0, 0, 0.1);
}
body {
font-family: 'Roboto', sans-serif;
margin: 0;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
background-color: var(--background-color);
color: var(--text-color);
}
.hidden {
display: none !important;
}
.top-header {
display: flex;
align-items: center;
padding: 12px 24px;
border-bottom: 1px solid var(--border-color);
background-color: var(--panel-background);
gap: 24px;
flex-shrink: 0;
}
.logo {
display: flex;
align-items: center;
gap: 8px;
font-size: 22px;
font-weight: 700;
color: var(--primary-color);
}
.search-container {
display: flex;
flex-grow: 1;
max-width: 720px;
}
.search-container input {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: 8px 0 0 8px;
font-size: 16px;
transition: box-shadow 0.2s ease;
}
.search-container input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
}
.search-container button {
padding: 0 20px;
border: 1px solid var(--primary-color);
border-radius: 0 8px 8px 0;
background-color: var(--primary-color);
color: white;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: background-color 0.2s ease;
}
.search-container button:hover {
background-color: #185abc;
}
.filter-container {
display: flex;
gap: 12px;
align-items: center;
}
.filter-container select, .open-now-label {
padding: 10px 14px;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--panel-background);
font-size: 14px;
cursor: pointer;
transition: border-color 0.2s ease;
}
.filter-container select:hover, .open-now-label:hover {
border-color: #c0c2c5;
}
.open-now-label {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.app-container {
display: flex;
flex-grow: 1;
overflow: hidden;
}
.sidebar {
width: 35%;
min-width: 380px;
max-width: 480px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-color);
background-color: var(--panel-background);
overflow: hidden;
}
.results-header {
padding: 16px 24px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.results-header h2 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.results-container {
flex-grow: 1;
position: relative;
overflow-y: auto;
overflow-x: hidden;
}
.placeholder {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 2rem;
box-sizing: border-box;
}
.placeholder p {
color: var(--text-color-light);
font-size: 1.1rem;
}
gmp-place-search {
width: 100%;
}
.map-container {
flex-grow: 1;
position: relative;
}
gmp-map {
width: 100%;
height: 100%;
}
.spinner-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.spinner-overlay.visible {
opacity: 1;
visibility: visible;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid #e0e0e0;
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
gmp-place-details-compact {
width: 350px;
display: none;
border: none;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
gmp-place-details-compact::after {
content: '';
position: absolute;
bottom: -12px;
left: 50%;
transform: translateX(-50%);
width: 24px;
height: 12px;
background-color: var(--panel-background);
clip-path: polygon(50% 100%, 0 0, 100% 0);
}
کلاس برنامه جاوا اسکریپت را ایجاد کنید
در نهایت یک فایل با نام script.js
ایجاد کنید. ما برنامه خود را در یک کلاس جاوا اسکریپت به نام PlaceFinderApp
ساختار خواهیم داد. این کد ما را مرتب نگه می دارد و حالت را به طور تمیز مدیریت می کند.
این کد اولیه کلاس را تعریف میکند، تمام عناصر HTML ما را در constructor
پیدا میکند و یک متد init()
برای بارگذاری کتابخانههای پلتفرم Google Maps ایجاد میکند.
کد زیر را در script.js
کپی کنید:
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
// We will add more initialization logic here in later steps.
}
}
// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
محدودیت های کلید API
ممکن است لازم باشد یک محدودیت جدید به کلید API خود اضافه کنید تا این Codelab کار کند. برای اطلاعات بیشتر و راهنمایی در مورد نحوه انجام این کار ، به محدود کردن کلیدهای API خود مراجعه کنید.
کار خود را بررسی کنید
فایل index.html
را در مرورگر وب خود باز کنید. شما باید صفحه ای را با هدر حاوی نوار جستجو و فیلترها، نوار کناری با پیام "نتایج جستجوی شما در اینجا ظاهر می شود" و یک نقشه بزرگ در مرکز شهر نیویورک ببینید. در این مرحله، کنترل های جستجو هنوز کاربردی نیستند.
4. یک تابع جستجو را پیاده سازی کنید
در این بخش، با اجرای عملکرد جستجوی اصلی، برنامه خود را زنده می کنیم. ما کدی را می نویسیم که وقتی کاربر روی دکمه "جستجو" کلیک می کند اجرا می شود. ما این تابع را از همان ابتدا با بهترین شیوه ها ایجاد خواهیم کرد تا تعاملات کاربر را به خوبی مدیریت کنیم و از اشکالات رایج مانند شرایط مسابقه جلوگیری کنیم.
در پایان این مرحله، میتوانید روی دکمه جستجو کلیک کنید و در حالی که برنامه دادهها را در پسزمینه واکشی میکند، اسپینر بارگیری ظاهر میشود.
روش جستجو را ایجاد کنید
ابتدا متد performSearch
را در کلاس PlaceFinderApp
خود تعریف کنید. این تابع قلب منطق جستجوی ما خواهد بود. ما همچنین یک متغیر نمونه، isSearchInProgress
معرفی خواهیم کرد تا به عنوان یک "دروازه بان" عمل کند. این باعث میشود کاربر نتواند یک جستجوی جدید را در حالی که در حال انجام است شروع کند، که میتواند منجر به خطا شود.
منطق داخل performSearch
ممکن است پیچیده به نظر برسد، بنابراین ما آن را تجزیه می کنیم:
- ابتدا بررسی می کند که آیا جستجو از قبل در حال انجام است یا خیر. اگر چنین است، هیچ کاری نمی کند.
- پرچم
isSearchInProgress
را رویtrue
تنظیم می کند تا عملکرد را "قفل" کند. - اسپینر بارگیری را نشان می دهد و رابط کاربری را برای نتایج جدید آماده می کند.
- ویژگی
textQuery
درخواست جستجو راnull
می کند. این یک گام مهم است که مؤلفه وب را مجبور میکند تشخیص دهد که یک درخواست جدید در حال آمدن است. - از
setTimeout
با تاخیر0
استفاده می کند. این تکنیک استاندارد جاوا اسکریپت بقیه کد ما را برای اجرا در کار بعدی مرورگر زمانبندی میکند و مطمئن میشود که مولفه ابتدا مقدارnull
را پردازش کرده است. حتی اگر کاربر دقیقاً یک مورد را دو بار جستجو کند، جستجوی جدید همیشه فعال می شود.
شنوندگان رویداد را اضافه کنید
در مرحله بعد، زمانی که کاربر با برنامه تعامل دارد، باید متد performSearch
خود را فراخوانی کنیم. ما یک روش جدید ایجاد می کنیم، attachEventListeners
، تا همه کدهای مربوط به مدیریت رویداد را در یک مکان نگه داریم. در حال حاضر، ما فقط یک شنونده برای رویداد click
دکمه جستجو اضافه می کنیم. همچنین یک مکان نگهدار برای رویداد دیگر اضافه می کنیم، gmp-load
، که در مرحله بعد از آن استفاده می کنیم.
فایل جاوا اسکریپت را به روز کنید
فایل script.js
خود را با کد زیر به روز کنید. بخش های جدید یا تغییر یافته عبارتند از متد attachEventListeners
و متد performSearch
.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
// Call the new method to set up listeners
this.attachEventListeners();
}
// NEW: Method to set up all event listeners
attachEventListeners() {
this.searchButton.addEventListener('click', this.performSearch.bind(this));
// We will add the gmp-load listener in the next step
}
// NEW: Core search method
async performSearch() {
// Exit if a search is already in progress
if (this.isSearchInProgress) {
return;
}
// Set the lock
this.isSearchInProgress = true;
// Show the placeholder and spinner
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
// Force a state change by clearing the query first.
this.searchRequest.textQuery = null;
// Defer setting the real properties to the next event loop cycle.
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
// If the query is empty, release the lock and hide the spinner
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
// For now, we just set the textQuery. We'll add filters later.
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
}, 0);
}
// NEW: Helper method to show/hide the spinner
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
کار خود را بررسی کنید
فایل script.js
خود را ذخیره کرده و index.html
در مرورگر خود بازخوانی کنید. صفحه باید مانند قبل باشد. اکنون روی دکمه "جستجو" در هدر کلیک کنید.
شما باید ببینید دو اتفاق می افتد:
- پیام نگهدارنده مکان "نتایج جستجوی شما در اینجا ظاهر می شود" ناپدید می شود.
- چرخاننده بارگیری ظاهر می شود و به چرخش ادامه می دهد.
اسپینر برای همیشه می چرخد زیرا هنوز به آن نگفته ایم که چه زمانی متوقف شود. ما این کار را در بخش بعدی هنگامی که نتایج را نمایش می دهیم انجام خواهیم داد. این تأیید می کند که عملکرد جستجوی ما به درستی راه اندازی شده است.
5. نمایش نتایج و اضافه کردن نشانگر
اکنون که ماشه جستجو فعال است، وظیفه بعدی نمایش نتایج روی صفحه است. کد موجود در این بخش، منطق جستجو را به UI متصل می کند. هنگامی که عنصر جستجوی مکان بارگیری داده ها را به پایان رساند، "قفل" جستجو را آزاد می کند، چرخان بارگیری را پنهان می کند و برای هر نتیجه یک نشانگر روی نقشه نمایش می دهد.
برای تکمیل جستجو گوش دهید
عنصر جستجوی مکان یک رویداد gmp-load
را زمانی که دادهها را با موفقیت واکشی کرد، اجرا میکند. این سیگنال عالی برای پردازش نتایج است.
ابتدا یک شنونده رویداد برای این رویداد در متد attachEventListeners
خود اضافه کنید.
روش های مدیریت نشانگر را ایجاد کنید
در مرحله بعد، دو روش کمکی جدید ایجاد خواهیم کرد: clearMarkers
و addMarkers
.
-
clearMarkers()
هر نشانگر را از جستجوی قبلی حذف می کند. -
addMarkers()
توسط شنوندهgmp-load
ما فراخوانی می شود. فهرست مکانهایی را که توسط جستجو بازگردانده شدهاند مرور میکند و برای هر کدام یکAdvancedMarkerElement
جدید ایجاد میکند. همچنین در اینجاست که ما اسپینر بارگیری را پنهان می کنیم و قفلisSearchInProgress
را آزاد می کنیم و چرخه جستجو را تکمیل می کنیم.
توجه داشته باشید که ما نشانگرها را در یک شی ( this.markers
) با استفاده از شناسه مکان به عنوان کلید ذخیره می کنیم. این راهی برای مدیریت نشانگرها است و به ما امکان می دهد بعداً یک نشانگر خاص را پیدا کنیم.
در نهایت، ما باید در شروع هر جستجوی جدید clearMarkers()
فراخوانی کنیم. بهترین مکان برای این کار داخل performSearch
است.
فایل جاوا اسکریپت را به روز کنید
فایل script.js
خود را با روش های جدید و تغییرات attachEventListeners
و performSearch
به روز کنید.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
this.attachEventListeners();
}
attachEventListeners() {
this.searchButton.addEventListener('click', this.performSearch.bind(this));
// NEW: Listen for when the search component has loaded results
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
}
// NEW: Method to clear markers from a previous search
clearMarkers() {
for (const marker of Object.values(this.markers)) {
marker.map = null;
}
this.markers = {};
}
// NEW: Method to add markers for new search results
addMarkers() {
// Release the lock and hide the spinner
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
// Create a new marker for each place result
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
// Store marker by its place ID for access later
this.markers[place.id] = marker;
}
}
async performSearch() {
if (this.isSearchInProgress) {
return;
}
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
// NEW: Clear old markers before starting a new search
this.clearMarkers();
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
}, 0);
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
کار خود را بررسی کنید
فایل های خود را ذخیره کنید و صفحه را در مرورگر خود به روز کنید. روی دکمه "جستجو" کلیک کنید.
اسپینر بارگیری اکنون باید برای لحظه ای ظاهر شود و سپس ناپدید شود. نوار کناری با لیستی از مکانهای مرتبط با عبارت جستجو پر میشود و باید نشانگرهای مربوطه را روی نقشه مشاهده کنید. نشانگرها با کلیک کردن هنوز کاری انجام نمی دهند. ما آن تعامل را در بخش بعدی اضافه خواهیم کرد.
6. فیلترهای جستجو و تعامل لیست را فعال کنید
برنامه ما اکنون می تواند نتایج جستجو را نمایش دهد، اما هنوز تعاملی نیست. در این بخش تمام کنترل های کاربر را زنده می کنیم. ما فیلترها را فعال می کنیم، جستجو را با کلید "Enter" فعال می کنیم و موارد موجود در لیست نتایج را به مکان های مربوطه خود در نقشه متصل می کنیم.
در پایان این مرحله، برنامه کاملاً به ورودی کاربر پاسخگو خواهد بود.
فیلترهای جستجو را فعال کنید
ابتدا، متد performSearch
برای خواندن مقادیر از تمام کنترلهای فیلتر در هدر بهروزرسانی میشود. برای هر فیلتر (قیمت، رتبهبندی و "Open Now")، ویژگی مربوطه روی شی searchRequest
قبل از اجرای جستجو تنظیم میشود.
شنوندگان رویداد را برای همه کنترلها اضافه کنید
در مرحله بعد، متد attachEventListeners
خود را گسترش خواهیم داد. شنوندههایی را برای رویداد change
در هر کنترل فیلتر، و همچنین یک شنونده keydown
در ورودی جستجو اضافه میکنیم تا زمانی که کاربر کلید «Enter» را فشار میدهد، تشخیص دهد. همه این شنوندگان جدید متد performSearch
را فراخوانی خواهند کرد.
لیست نتایج را به نقشه وصل کنید
برای ایجاد یک تجربه یکپارچه، با کلیک بر روی یک مورد در لیست نتایج نوار کناری باید نقشه را روی آن مکان متمرکز کنید.
یک روش جدید، handleResultClick
، به رویداد gmp-select
گوش می دهد، که با کلیک روی یک مورد توسط عنصر جستجوی مکان فعال می شود. این تابع مکان مکان مرتبط را پیدا می کند و به آرامی نقشه را به آن جابجا می کند.
برای اینکه این کار انجام شود، مطمئن شوید که ویژگی selectable
در مؤلفه gmp-place-search
شما در index.html
وجود دارد.
<gmp-place-search id="place-search-list" class="hidden" selectable>
<gmp-place-all-content></gmp-place-all-content>
<gmp-place-text-search-request></gmp-place-text-search-request>
</gmp-place-search>
فایل جاوا اسکریپت را به روز کنید
فایل script.js
خود را با کد کامل زیر به روز کنید. این نسخه شامل روش جدید handleResultClick
و منطق به روز شده در attachEventListeners
و performSearch
است.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
this.attachEventListeners();
}
// UPDATED: All event listeners are now attached
attachEventListeners() {
// Listen for the 'Enter' key press in the search input
this.queryInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.performSearch();
}
});
// Listen for a sidebar result click
this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
this.searchButton.addEventListener('click', this.performSearch.bind(this));
this.priceFilter.addEventListener('change', this.performSearch.bind(this));
this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
}
clearMarkers() {
for (const marker of Object.values(this.markers)) {
marker.map = null;
}
this.markers = {};
}
addMarkers() {
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
this.markers[place.id] = marker;
}
}
// NEW: Function to handle clicks on the results list
handleResultClick(event) {
const place = event.place;
if (!place || !place.location) return;
// Pan the map to the selected place
this.map.panTo(place.location);
}
// UPDATED: Search function now includes all filters
async performSearch() {
if (this.isSearchInProgress) {
return;
}
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
this.clearMarkers();
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
// Add filter values to the request
const selectedPrice = this.priceFilter.value;
let priceLevels = [];
switch (selectedPrice) {
case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
default: priceLevels = null; break;
}
this.searchRequest.priceLevels = priceLevels;
const selectedRating = parseFloat(this.ratingFilter.value);
this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
}, 0);
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
کار خود را بررسی کنید
فایل script.js
خود را ذخیره کرده و صفحه را بازخوانی کنید. اکنون برنامه باید بسیار تعاملی باشد.
موارد زیر را تأیید کنید:
- جستجو با فشار دادن "Enter" در کادر جستجو کار می کند.
- تغییر هر یک از فیلترها (قیمت، رتبه بندی، اکنون باز کنید) یک جستجوی جدید را آغاز می کند و نتایج را به روز می کند.
- اکنون با کلیک بر روی یک مورد در لیست نتایج نوار کناری، نقشه به آرامی به مکان آن مورد منتقل می شود.
در بخش بعدی، کارت جزئیات را که با کلیک روی یک نشانگر ظاهر می شود، پیاده سازی می کنیم.
7. عنصر جزئیات مکان را پیاده سازی کنید
برنامه ما اکنون کاملاً تعاملی است، اما یک ویژگی کلیدی را ندارد: توانایی دیدن اطلاعات بیشتر در مورد یک مکان انتخاب شده. در این بخش، عنصر جزئیات مکان را پیاده سازی می کنیم که زمانی که کاربر روی یک نشانگر روی نقشه کلیک می کند یا یک مورد را در عنصر جستجوی مکان انتخاب می کند ظاهر می شود.
یک ظرف کارت جزئیات قابل استفاده مجدد ایجاد کنید
کارآمدترین راه برای نمایش جزئیات مکان بر روی نقشه، ایجاد یک ظرف واحد و قابل استفاده مجدد است. ما از AdvancedMarkerElement
به عنوان این ظرف استفاده خواهیم کرد. محتوای آن ویجت پنهان gmp-place-details-compact
خواهد بود که قبلاً در index.html
خود داریم.
یک روش جدید، initDetailsPopup
، ایجاد این نشانگر قابل استفاده مجدد را مدیریت می کند. هنگامی که برنامه بارگیری می شود، یک بار ایجاد می شود و مخفی می شود. همچنین در این روش یک شنونده به نقشه اصلی اضافه می کنیم تا با کلیک روی هر نقطه از نقشه، کارت جزئیات پنهان شود.
رفتار کلیک نشانگر را به روز کنید
در مرحله بعد، باید بهروزرسانی کنیم که وقتی کاربر روی نشانگر مکان کلیک میکند چه اتفاقی میافتد. شنونده 'click'
در متد addMarkers
اکنون مسئول نمایش کارت جزئیات خواهد بود.
هنگامی که یک نشانگر کلیک می شود، شنونده این کار را انجام می دهد:
- نقشه را به مکان نشانگر حرکت دهید.
- کارت جزئیات را با اطلاعات مربوط به آن مکان خاص به روز کنید.
- کارت جزئیات را در محل نشانگر قرار دهید و آن را قابل مشاهده کنید.
کلیک لیست را به کلیک نشانگر متصل کنید
در نهایت، روش handleResultClick
به روز می کنیم. به جای اینکه فقط نقشه را جابجا کنید، اکنون به صورت برنامهریزی رویداد click
روی نشانگر مربوطه را فعال میکند. این یک الگوی قدرتمند است که به ما امکان میدهد تا از منطق مشابهی برای هر دو تعامل مجدد استفاده کنیم و کد خود را تمیز و قابل نگهداری نگه داریم.
فایل جاوا اسکریپت را به روز کنید
فایل script.js
خود را با کد زیر به روز کنید. بخش های جدید یا تغییر یافته عبارتند از روش initDetailsPopup
و متدهای addMarkers
و handleResultClick
به روز شده.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
// NEW: Call the method to initialize the details card
this.initDetailsPopup();
this.attachEventListeners();
}
attachEventListeners() {
this.queryInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.performSearch();
}
});
this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
this.searchButton.addEventListener('click', this.performSearch.bind(this));
this.priceFilter.addEventListener('change', this.performSearch.bind(this));
this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
}
// NEW: Method to set up the reusable details card
initDetailsPopup() {
this.detailsPopup = new this.AdvancedMarkerElement({
content: this.placeDetailsWidget,
map: null,
zIndex: 100
});
this.map.addListener('click', () => { this.detailsPopup.map = null; });
}
clearMarkers() {
for (const marker of Object.values(this.markers)) {
marker.map = null;
}
this.markers = {};
}
// UPDATED: The marker's click listener now shows the details card
addMarkers() {
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
// Add the click listener to show the details card
marker.addListener('click', (event) => {
event.stop();
this.map.panTo(place.location);
this.placeDetailsRequest.place = place;
this.placeDetailsWidget.style.display = 'block';
this.detailsPopup.position = place.location;
this.detailsPopup.map = this.map;
});
this.markers[place.id] = marker;
}
}
// UPDATED: This now triggers the marker's click event
handleResultClick(event) {
const place = event.place;
if (!place || !place.id) return;
const marker = this.markers[place.id];
if (marker) {
// Programmatically trigger the marker's click event
marker.click();
}
}
async performSearch() {
if (this.isSearchInProgress) return;
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
this.clearMarkers();
// Hide the details card when a new search starts
if (this.detailsPopup) this.detailsPopup.map = null;
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
const selectedPrice = this.priceFilter.value;
let priceLevels = [];
switch (selectedPrice) {
case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
default: priceLevels = null; break;
}
this.searchRequest.priceLevels = priceLevels;
const selectedRating = parseFloat(this.ratingFilter.value);
this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
}, 0);
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
کار خود را بررسی کنید
فایل script.js
خود را ذخیره کرده و صفحه را بازخوانی کنید. اکنون برنامه باید جزئیات درخواستی را نشان دهد.
موارد زیر را تأیید کنید:
- اکنون با کلیک بر روی یک نشانگر روی نقشه، نقشه را در مرکز قرار داده و یک کارت جزئیات مدلدار روی نشانگر باز میشود.
- کلیک کردن روی یک مورد در لیست نتایج نوار کناری دقیقاً همان کار را انجام می دهد.
- با کلیک بر روی نقشه دور از کارت، آن بسته می شود.
- شروع جستجوی جدید همچنین هر کارت جزئیات باز را میبندد.
8. پولیش نهایی را اضافه کنید
برنامه ما اکنون کاملاً کاربردی است، اما چند نکته نهایی وجود دارد که می توانیم برای بهبود تجربه کاربر اضافه کنیم. در این بخش پایانی، دو ویژگی کلیدی را پیادهسازی میکنیم: یک هدر پویا که زمینه بهتری را برای نتایج جستجو فراهم میکند و قالببندی خودکار برای درخواست جستجوی کاربر.
یک هدر نتایج پویا ایجاد کنید
در حال حاضر، هدر نوار کناری همیشه "نتایج" را نشان می دهد. ما میتوانیم با بهروزرسانی آن برای انعکاس جستجوی فعلی، این اطلاعات را آموزندهتر کنیم. به عنوان مثال، "برگرها در نزدیکی نیویورک".
برای انجام این کار، از Geocoding API برای تبدیل مختصات مرکزی نقشه به یک مکان قابل خواندن برای انسان، مانند نام شهر، استفاده می کنیم. یک روش async
جدید، updateResultsHeader
، این منطق را مدیریت خواهد کرد. هر بار که جستجو انجام شود، فراخوانی می شود.
پرس و جوی جستجوی کاربر را قالب بندی کنید
برای اطمینان از اینکه رابط کاربری تمیز و سازگار به نظر میرسد، عبارت جستجوی کاربر را بهطور خودکار به «مورد عنوان» قالببندی میکنیم (مثلاً «رستوران برگر» به «رستوران برگر» تبدیل میشود). یک تابع کمکی، toTitleCase
، این تبدیل را مدیریت خواهد کرد. روش performSearch
برای استفاده از این تابع در ورودی کاربر قبل از انجام جستجو و بهروزرسانی هدر بهروزرسانی میشود.
فایل جاوا اسکریپت را به روز کنید
فایل script.js
خود را با نسخه نهایی کد به روز کنید. این شامل روشهای جدید toTitleCase
و updateResultsHeader
، و روش بهروزرسانیشده performSearch
است که آنها را ادغام میکند.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
this.initDetailsPopup();
this.attachEventListeners();
}
attachEventListeners() {
this.queryInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.performSearch();
}
});
this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
this.searchButton.addEventListener('click', this.performSearch.bind(this));
this.priceFilter.addEventListener('change', this.performSearch.bind(this));
this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
}
initDetailsPopup() {
this.detailsPopup = new this.AdvancedMarkerElement({
content: this.placeDetailsWidget,
map: null,
zIndex: 100
});
this.map.addListener('click', () => { this.detailsPopup.map = null; });
}
// NEW: Helper function to format text to Title Case
toTitleCase(str) {
if (!str) return '';
return str.toLowerCase().split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
clearMarkers() {
for (const marker of Object.values(this.markers)) { marker.map = null; }
this.markers = {};
}
addMarkers() {
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
marker.addListener('click', (event) => {
event.stop();
this.map.panTo(place.location);
this.placeDetailsRequest.place = place;
this.placeDetailsWidget.style.display = 'block';
this.detailsPopup.position = place.location;
this.detailsPopup.map = this.map;
});
this.markers[place.id] = marker;
}
}
handleResultClick(event) {
const place = event.place;
if (!place || !place.id) return;
const marker = this.markers[place.id];
if (marker) {
marker.click();
}
}
// UPDATED: Now integrates formatting and the dynamic header
async performSearch() {
if (this.isSearchInProgress) return;
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
this.clearMarkers();
if (this.detailsPopup) this.detailsPopup.map = null;
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
// Format the query and update the input box value
const formattedQuery = this.toTitleCase(rawQuery);
this.queryInput.value = formattedQuery;
// Update the header with the new query and location
await this.updateResultsHeader(formattedQuery);
// Pass the formatted query to the search request
this.searchRequest.textQuery = formattedQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
const selectedPrice = this.priceFilter.value;
let priceLevels = [];
switch (selectedPrice) {
case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
default: priceLevels = null; break;
}
this.searchRequest.priceLevels = priceLevels;
const selectedRating = parseFloat(this.ratingFilter.value);
this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
}, 0);
}
// NEW: Method to update the sidebar header with geocoded location
async updateResultsHeader(query) {
try {
const response = await this.geocoder.geocode({ location: this.map.getCenter() });
if (response.results && response.results.length > 0) {
const cityResult = response.results.find(r => r.types.includes('locality')) || response.results[0];
const city = cityResult.address_components[0].long_name;
this.resultsHeaderText.textContent = `${query} near ${city}`;
} else {
this.resultsHeaderText.textContent = `${query} near current map area`;
}
} catch (error) {
console.error("Geocoding failed:", error);
this.resultsHeaderText.textContent = `Results for ${query}`;
}
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
کار خود را بررسی کنید
فایل script.js
خود را ذخیره کرده و صفحه را بازخوانی کنید.
بررسی ویژگی ها:
-
pizza
(همه حروف کوچک) را در کادر جستجو تایپ کنید و روی جستجو کلیک کنید. نوشتار موجود در کادر باید به «Pizza» تغییر کند، و سرصفحه در نوار کناری باید بهروزرسانی شود تا بگوید «Pizza near New York». - نقشه را به شهر دیگری مانند بوستون حرکت دهید و دوباره جستجو کنید. سرصفحه باید به "Pizza near Boston" به روز شود.
9. تبریک می گویم
شما با موفقیت یک برنامه جستجوی محلی کامل و تعاملی ساختهاید که سادگی Places UI Kit را با قدرت APIهای جاوا اسکریپت پلتفرم Google Maps اصلی ترکیب میکند.
چیزی که یاد گرفتی
- نحوه ساختار یک برنامه نقشه برداری با استفاده از کلاس جاوا اسکریپت برای مدیریت حالت و منطق.
- نحوه استفاده از Places UI Kit با Google Maps JavaScript API برای توسعه سریع رابط کاربری.
- نحوه اضافه کردن و مدیریت نشانگرهای پیشرفته برای نمایش نقاط مورد علاقه سفارشی بر روی نقشه.
- نحوه استفاده از سرویس کدگذاری جغرافیایی برای تبدیل مختصات به آدرس های قابل خواندن توسط انسان برای تجربه کاربری بهتر.
- نحوه شناسایی و رفع شرایط مسابقه رایج در یک برنامه تعاملی با استفاده از پرچم های ایالت و اطمینان از به روز رسانی صحیح ویژگی های مؤلفه.
بعدش چی؟
- با تغییر رنگ، مقیاس یا حتی استفاده از HTML سفارشی، درباره سفارشی کردن نشانگرهای پیشرفته بیشتر بیاموزید.
- برای سفارشی کردن ظاهر و احساس نقشه خود برای مطابقت با نام تجاری خود ، استایل نقشه مبتنی بر ابر را کاوش کنید.
- سعی کنید کتابخانه طراحی را اضافه کنید تا به کاربران امکان دهد اشکالی را روی نقشه ترسیم کنند تا مناطق جستجو را تعریف کنند.
- با پاسخ دادن به نظرسنجی زیر به ما در ایجاد محتوایی که برای شما مفیدتر است کمک کنید:
دوست دارید چه کدهای دیگری را ببینید؟
نمی توانید کد لبه مورد علاقه خود را پیدا کنید؟ آن را با یک شماره جدید در اینجا درخواست کنید .