4.8 ממשקי דפדפן הרצאה
צופה בצמתים - IntersectionObserver¶
IntersectionObserver הוא API שמאפשר לדעת מתי אלמנט נכנס או יוצא מאזור הנראות (viewport) או מתוך אלמנט אב אחר.
- לא חוסם את ה-thread הראשי (בניגוד ל-scroll event)
- הדפדפן מנהל את הבדיקה בצורה יעילה
- שימושי במיוחד לטעינה עצלה (lazy loading) וגלילה אינסופית (infinite scroll)
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
console.log(entry.target); // the observed element
console.log(entry.isIntersecting); // true if visible
console.log(entry.intersectionRatio);// 0 to 1 (how much is visible)
});
});
// start observing an element
const element = document.querySelector("#myElement");
observer.observe(element);
// stop observing
observer.unobserve(element);
// stop all observations
observer.disconnect();
אפשרויות - Options¶
const observer = new IntersectionObserver(callback, {
root: null, // null = viewport, or a parent element
rootMargin: "0px", // margin around root (like CSS margin)
threshold: 0 // 0 to 1, or array [0, 0.25, 0.5, 0.75, 1]
});
root- האלמנט שמשמש כאזור ההתייחסות. null = החלוןrootMargin- מרווח סביב ה-root. שימושי לטעינה מראש (למשל "200px" יפעיל כשהאלמנט 200px לפני שנכנס לתצוגה)threshold- באיזה אחוז חפיפה להפעיל את הקולבק. 0 = ברגע שפיקסל אחד נכנס, 1 = כשכולו גלוי
טעינה עצלה - Lazy Loading עם IntersectionObserver¶
טעינה עצלה פירושה לטעון תמונות רק כשהן קרובות לאזור הנראות:
<img data-src="heavy-image.jpg" class="lazy" alt="...">
<img data-src="another-image.jpg" class="lazy" alt="...">
const lazyObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // load the real image
img.classList.remove("lazy");
lazyObserver.unobserve(img); // stop watching this image
}
});
}, {
rootMargin: "200px" // start loading 200px before visible
});
// observe all lazy images
document.querySelectorAll("img.lazy").forEach(img => {
lazyObserver.observe(img);
});
הערה: דפדפנים מודרניים תומכים גם ב-loading="lazy" על תגית img, אבל IntersectionObserver נותן יותר שליטה.
גלילה אינסופית - Infinite Scroll¶
const container = document.getElementById("items");
const sentinel = document.getElementById("sentinel");
let page = 1;
let loading = false;
const scrollObserver = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting && !loading) {
loading = true;
const response = await fetch(`/api/items?page=${page}`);
const items = await response.json();
items.forEach(item => {
const div = document.createElement("div");
div.textContent = item.title;
container.appendChild(div);
});
page++;
loading = false;
}
});
scrollObserver.observe(sentinel);
- ה-sentinel הוא אלמנט ריק בתחתית הרשימה
- כשהוא נכנס לתצוגה, טוענים עוד פריטים
- הפריטים החדשים דוחפים את ה-sentinel למטה, וכשגוללים שוב - טוענים עוד
צופה בגודל - ResizeObserver¶
ResizeObserver מאפשר לדעת מתי אלמנט משנה גודל:
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
console.log(`Size: ${width} x ${height}`);
});
});
const element = document.querySelector("#resizable");
resizeObserver.observe(element);
// stop observing
resizeObserver.unobserve(element);
resizeObserver.disconnect();
שימושים:
- התאמת גודל canvas לגודל האלמנט שמכיל אותו
- החלפת layout כשאלמנט מסוים (לא החלון) משנה גודל
- responsive components שלא תלויים בגודל החלון אלא בגודל ה-container שלהם
// responsive chart that adapts to container size
const chartContainer = document.querySelector("#chart");
const observer = new ResizeObserver(entries => {
const { width, height } = entries[0].contentRect;
redrawChart(width, height);
});
observer.observe(chartContainer);
מיקום גאוגרפי - Geolocation¶
ה-Geolocation API מאפשר לקבל את המיקום הנוכחי של המשתמש (דורש הרשאה):
// get current position (one time)
navigator.geolocation.getCurrentPosition(
(position) => {
console.log("Latitude:", position.coords.latitude);
console.log("Longitude:", position.coords.longitude);
console.log("Accuracy:", position.coords.accuracy, "meters");
},
(error) => {
switch (error.code) {
case error.PERMISSION_DENIED:
console.log("User denied geolocation");
break;
case error.POSITION_UNAVAILABLE:
console.log("Location unavailable");
break;
case error.TIMEOUT:
console.log("Request timed out");
break;
}
},
{
enableHighAccuracy: true, // use GPS if available
timeout: 10000, // max wait time in ms
maximumAge: 60000 // cache position for 60 seconds
}
);
מעקב רציף¶
// watch position continuously
const watchId = navigator.geolocation.watchPosition(
(position) => {
updateMap(position.coords.latitude, position.coords.longitude);
},
(error) => {
console.error("Watch error:", error.message);
}
);
// stop watching
navigator.geolocation.clearWatch(watchId);
לוח - Clipboard API¶
ה-Clipboard API מאפשר לקרוא ולכתוב ללוח ההעתקה:
// copy text to clipboard
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
console.log("Copied!");
} catch (error) {
console.error("Failed to copy:", error);
}
}
// read text from clipboard (requires permission)
async function pasteFromClipboard() {
try {
const text = await navigator.clipboard.readText();
console.log("Pasted:", text);
return text;
} catch (error) {
console.error("Failed to paste:", error);
}
}
כפתור "העתק" מעשי¶
const copyButton = document.querySelector("#copyBtn");
const codeBlock = document.querySelector("#code");
copyButton.addEventListener("click", async () => {
await navigator.clipboard.writeText(codeBlock.textContent);
copyButton.textContent = "Copied!";
setTimeout(() => {
copyButton.textContent = "Copy";
}, 2000);
});
חשוב:
- עובד רק ב-HTTPS (או localhost)
- writeText עובד בלי הרשאה מיוחדת (בתגובה ללחיצת משתמש)
- readText דורש הרשאה מפורשת מהמשתמש
התראות - Notifications API¶
ה-Notifications API מאפשר להציג התראות מחוץ לדפדפן:
// request permission
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
console.log("Permission:", permission); // "granted", "denied", or "default"
return permission;
}
// show notification
function showNotification(title, body) {
if (Notification.permission !== "granted") {
console.log("No notification permission");
return;
}
const notification = new Notification(title, {
body: body,
icon: "/icon.png"
});
notification.onclick = () => {
window.focus();
notification.close();
};
// auto close after 5 seconds
setTimeout(() => notification.close(), 5000);
}
// practical example
async function notifyNewMessage(sender, message) {
if (Notification.permission === "default") {
await Notification.requestPermission();
}
if (Notification.permission === "granted") {
showNotification(`New message from ${sender}`, message);
}
}
כתובת - URL ו-URLSearchParams¶
ה-URL API מספק דרך מובנית לפרסר ולבנות כתובות:
const url = new URL("https://example.com:8080/path/page?q=hello&page=2#section");
console.log(url.protocol); // "https:"
console.log(url.hostname); // "example.com"
console.log(url.port); // "8080"
console.log(url.pathname); // "/path/page"
console.log(url.search); // "?q=hello&page=2"
console.log(url.hash); // "#section"
console.log(url.origin); // "https://example.com:8080"
URLSearchParams¶
// parse existing query string
const params = new URLSearchParams("?q=hello&page=2");
console.log(params.get("q")); // "hello"
console.log(params.get("page")); // "2"
console.log(params.has("q")); // true
// build query string
const newParams = new URLSearchParams();
newParams.set("search", "javascript");
newParams.set("sort", "date");
newParams.append("tag", "frontend");
newParams.append("tag", "web");
console.log(newParams.toString()); // "search=javascript&sort=date&tag=frontend&tag=web"
// get all values for a key
console.log(newParams.getAll("tag")); // ["frontend", "web"]
// iterate
for (const [key, value] of newParams) {
console.log(`${key}: ${value}`);
}
// delete
newParams.delete("sort");
שילוב URL ו-URLSearchParams¶
const url = new URL("https://api.example.com/search");
url.searchParams.set("q", "hello world");
url.searchParams.set("page", "1");
console.log(url.toString());
// "https://api.example.com/search?q=hello+world&page=1"
// modify existing URL
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set("view", "grid");
window.history.pushState({}, "", currentUrl);
היסטוריה - History API¶
ה-History API מאפשר לנהל את היסטוריית הניווט בלי לטעון דף מחדש:
// add entry to history (URL changes, page doesn't reload)
history.pushState({ page: 1 }, "", "/page/1");
// replace current entry (doesn't add to history)
history.replaceState({ page: 2 }, "", "/page/2");
// navigate back/forward
history.back();
history.forward();
history.go(-2); // go back 2 pages
// listen for back/forward navigation
window.addEventListener("popstate", (event) => {
console.log("Navigation:", event.state);
// update UI based on event.state
});
ניווט - SPA Router פשוט¶
function navigate(path) {
history.pushState({ path }, "", path);
renderPage(path);
}
function renderPage(path) {
const content = document.getElementById("content");
switch (path) {
case "/":
content.innerHTML = "<h1>Home</h1>";
break;
case "/about":
content.innerHTML = "<h1>About</h1>";
break;
case "/contact":
content.innerHTML = "<h1>Contact</h1>";
break;
default:
content.innerHTML = "<h1>404</h1>";
}
}
// handle back/forward buttons
window.addEventListener("popstate", (event) => {
renderPage(window.location.pathname);
});
// handle link clicks
document.addEventListener("click", (event) => {
if (event.target.matches("a[data-link]")) {
event.preventDefault();
navigate(event.target.getAttribute("href"));
}
});
בדיקת מדיה - matchMedia¶
matchMedia מאפשר לבדוק media queries מ-JS ולהגיב לשינויים:
// check if screen is narrow
const mobileQuery = window.matchMedia("(max-width: 768px)");
console.log(mobileQuery.matches); // true or false
// listen for changes
mobileQuery.addEventListener("change", (event) => {
if (event.matches) {
console.log("Switched to mobile layout");
} else {
console.log("Switched to desktop layout");
}
});
בדיקת מצב כהה - Dark Mode¶
const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)");
function applyTheme(isDark) {
document.body.classList.toggle("dark", isDark);
}
// apply on load
applyTheme(darkModeQuery.matches);
// react to system changes
darkModeQuery.addEventListener("change", (event) => {
applyTheme(event.matches);
});
בדיקות נוספות¶
// reduced motion preference
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
if (reducedMotion.matches) {
// disable animations
}
// screen orientation
const portrait = window.matchMedia("(orientation: portrait)");
portrait.addEventListener("change", (event) => {
console.log(event.matches ? "Portrait" : "Landscape");
});
// high resolution display
const retina = window.matchMedia("(min-resolution: 2dppx)");
if (retina.matches) {
// load higher resolution images
}
סיכום¶
- IntersectionObserver - מעקב אחרי נראות אלמנטים (lazy loading, infinite scroll)
- ResizeObserver - מעקב אחרי שינויי גודל אלמנטים
- Geolocation - קבלת מיקום המשתמש (דורש הרשאה)
- Clipboard - קריאה וכתיבה ללוח ההעתקה
- Notifications - הצגת התראות מחוץ לדפדפן (דורש הרשאה)
- URL / URLSearchParams - פירסור ובניית כתובות
- History API - ניהול היסטוריית ניווט (בסיס ל-SPA routing)
- matchMedia - בדיקת media queries מ-JS (responsive, dark mode)