264 lines
8.5 KiB
JavaScript
264 lines
8.5 KiB
JavaScript
// markerMedia.js - Media, image, video, gallery handling
|
|
|
|
import { escapeHtml } from "./markerUtils";
|
|
|
|
export function normalizeImageUrl(raw) {
|
|
const value = (raw || "").toString().trim();
|
|
if (!value) return "";
|
|
if (value.startsWith("//")) return `https:${value}`;
|
|
if (value.startsWith("http://")) return `https://${value.slice(7)}`;
|
|
return value;
|
|
}
|
|
|
|
export function normalizeMediaUrl(raw) {
|
|
const value = (raw || "").toString().trim();
|
|
if (!value) return "";
|
|
if (value.startsWith("//")) return `https:${value}`;
|
|
if (value.startsWith("http://")) return `https://${value.slice(7)}`;
|
|
return value;
|
|
}
|
|
|
|
export function resolvePostImage(post) {
|
|
return normalizeImageUrl(
|
|
post?.image_small ||
|
|
post?.image_medium ||
|
|
post?.image_large ||
|
|
post?.image ||
|
|
post?.media_url ||
|
|
""
|
|
);
|
|
}
|
|
|
|
export function heroImageForPost(post) {
|
|
const raw =
|
|
post?.image_medium ||
|
|
post?.image_large ||
|
|
post?.image_small ||
|
|
post?.image ||
|
|
post?.media_url ||
|
|
"";
|
|
const v = normalizeImageUrl(raw);
|
|
if (v) return v;
|
|
const cat = (post?.category || "").toString().trim();
|
|
return `/og/category?cat=${encodeURIComponent(cat || "NEWS")}`;
|
|
}
|
|
|
|
export function fullImageForPost(post) {
|
|
const raw =
|
|
post?.image_large ||
|
|
post?.image_medium ||
|
|
post?.image_small ||
|
|
post?.image ||
|
|
post?.media_url ||
|
|
"";
|
|
return normalizeImageUrl(raw);
|
|
}
|
|
|
|
export function collectGalleryImages(post) {
|
|
const result = [];
|
|
const seen = new Set();
|
|
const add = (value) => {
|
|
const normalized = normalizeImageUrl(value);
|
|
if (!normalized) return;
|
|
if (seen.has(normalized)) return;
|
|
seen.add(normalized);
|
|
result.push(normalized);
|
|
};
|
|
if (Array.isArray(post?.media_urls)) {
|
|
post.media_urls.forEach(add);
|
|
}
|
|
["image_large", "image_medium", "image_small", "image", "media_url"].forEach((key) => {
|
|
add(post?.[key]);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
// Extract camera ID from Quebec 511 URL
|
|
export function extractCameraId(url) {
|
|
if (!url) return null;
|
|
const match = url.match(/[?&]id=(\d+)/);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
// Build stream proxy URL for camera
|
|
export function getCameraStreamUrl(postData) {
|
|
const embedUrl = normalizeMediaUrl(postData?.embed_url || postData?.video_url || '');
|
|
const cameraId = extractCameraId(embedUrl);
|
|
if (cameraId) {
|
|
return `/live/api/stream/${cameraId}`;
|
|
}
|
|
return embedUrl;
|
|
}
|
|
|
|
export function renderTagPills(tags, topicImpacts = {}) {
|
|
if (!Array.isArray(tags) || tags.length === 0) return "";
|
|
const uniq = [];
|
|
const seen = new Set();
|
|
for (const raw of tags) {
|
|
const t = (raw || "").toString().trim().toLowerCase();
|
|
if (!t || seen.has(t)) continue;
|
|
seen.add(t);
|
|
uniq.push(t);
|
|
if (uniq.length >= 6) break;
|
|
}
|
|
if (uniq.length === 0) return "";
|
|
|
|
// Get impact color class: negative=red, neutral=blue, positive=green
|
|
const getImpactClass = (tag) => {
|
|
const impact = topicImpacts?.[tag] ?? topicImpacts?.[tag.toLowerCase()] ?? 0;
|
|
if (impact < 0) return "sw-tag-negative";
|
|
if (impact > 0) return "sw-tag-positive";
|
|
return "sw-tag-neutral";
|
|
};
|
|
|
|
return `
|
|
<div class="sw-modal-tags">
|
|
${uniq.map((t) => `<span class="sw-modal-tag ${getImpactClass(t)}">#${escapeHtml(t)}</span>`).join("")}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
export function setModalTags(modalWrap, post) {
|
|
if (!modalWrap) return;
|
|
const tagsHtml = renderTagPills(post?.event_tags || post?.tags, post?.topic_impacts);
|
|
const existing = modalWrap.querySelector(".sw-modal-tags");
|
|
if (tagsHtml) {
|
|
if (existing) {
|
|
existing.outerHTML = tagsHtml;
|
|
} else {
|
|
const titleEl = modalWrap.querySelector(".sw-modal-title") || modalWrap.querySelector(".sw-cluster-title");
|
|
if (titleEl) titleEl.insertAdjacentHTML("afterend", tagsHtml);
|
|
}
|
|
} else if (existing) {
|
|
existing.remove();
|
|
}
|
|
}
|
|
|
|
export function bindHeroLightbox(modalWrap) {
|
|
if (!modalWrap) return;
|
|
const img = modalWrap.querySelector(".sw-modal-hero img, .sw-cluster-hero-media img");
|
|
if (!img || img.dataset.zoomBound === "1") return;
|
|
img.dataset.zoomBound = "1";
|
|
img.style.cursor = "zoom-in";
|
|
img.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const src = (img.getAttribute("data-fullsrc") || img.getAttribute("src") || "").trim();
|
|
if (src) {
|
|
openImageLightbox(src);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function initModalGallery(modalWrap, gallery) {
|
|
if (!modalWrap || !Array.isArray(gallery) || gallery.length === 0) return;
|
|
const heroImg = modalWrap.querySelector(".sw-modal-hero img, .sw-cluster-hero-media img");
|
|
if (!heroImg) return;
|
|
const arrows = modalWrap.querySelectorAll(".sw-modal-hero-arrow");
|
|
let activeIndex = 0;
|
|
const indicator = modalWrap.querySelector(".sw-modal-gallery-indicator");
|
|
const currentIndicator = indicator?.querySelector(".sw-modal-gallery-current");
|
|
const totalIndicator = indicator?.querySelector(".sw-modal-gallery-total");
|
|
if (totalIndicator) {
|
|
totalIndicator.textContent = String(gallery.length);
|
|
}
|
|
const setActive = (idx) => {
|
|
if (!Number.isFinite(idx)) return;
|
|
const normalized = (idx + gallery.length) % gallery.length;
|
|
activeIndex = normalized;
|
|
heroImg.src = gallery[normalized];
|
|
heroImg.dataset.fullsrc = gallery[normalized];
|
|
heroImg.dataset.galleryIndex = String(normalized);
|
|
if (currentIndicator) {
|
|
currentIndicator.textContent = String(normalized + 1);
|
|
}
|
|
};
|
|
arrows.forEach((arrow) => {
|
|
const dir = arrow.classList.contains("sw-modal-hero-arrow-prev") ? -1 : 1;
|
|
arrow.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setActive(activeIndex + dir);
|
|
});
|
|
});
|
|
// Touch swipe for gallery
|
|
let touchStartX = 0;
|
|
const touchThreshold = 50;
|
|
heroImg.addEventListener("touchstart", (event) => {
|
|
touchStartX = event.touches[0].clientX;
|
|
});
|
|
heroImg.addEventListener("touchend", (event) => {
|
|
const dx = event.changedTouches[0].clientX - touchStartX;
|
|
if (Math.abs(dx) < touchThreshold) return;
|
|
const direction = dx > 0 ? -1 : 1;
|
|
const nextIndex = (activeIndex + direction + gallery.length) % gallery.length;
|
|
setActive(nextIndex);
|
|
});
|
|
}
|
|
|
|
export function openImageLightbox(src) {
|
|
if (!src) return;
|
|
const overlay = document.createElement("div");
|
|
overlay.className = "sw-image-lightbox";
|
|
overlay.style.position = "fixed";
|
|
overlay.style.inset = "0";
|
|
overlay.style.background = "rgba(8,10,16,0.86)";
|
|
overlay.style.zIndex = "6000"; /* Image lightbox - above post modal (5000), below auth (10000) */
|
|
overlay.style.display = "flex";
|
|
overlay.style.alignItems = "center";
|
|
overlay.style.justifyContent = "center";
|
|
overlay.style.padding = "16px";
|
|
const img = document.createElement("img");
|
|
img.src = src;
|
|
img.alt = "";
|
|
img.style.maxWidth = "96vw";
|
|
img.style.maxHeight = "96vh";
|
|
img.style.objectFit = "contain";
|
|
img.style.borderRadius = "14px";
|
|
img.style.boxShadow = "0 20px 50px rgba(0,0,0,0.45)";
|
|
overlay.appendChild(img);
|
|
|
|
const close = (opts = {}) => {
|
|
try { document.removeEventListener("keydown", onKey); } catch {}
|
|
try { window.removeEventListener("popstate", onPop); } catch {}
|
|
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
if (!opts.skipHistory && overlay.__swPushed) {
|
|
overlay.__swPushed = false;
|
|
try { history.back(); } catch {}
|
|
}
|
|
};
|
|
const onKey = (e) => {
|
|
if (e.key === "Escape") close();
|
|
};
|
|
const onPop = () => close({ skipHistory: true });
|
|
overlay.addEventListener("click", close);
|
|
document.addEventListener("keydown", onKey);
|
|
window.addEventListener("popstate", onPop);
|
|
|
|
try {
|
|
history.pushState({ sw: "image" }, "");
|
|
overlay.__swPushed = true;
|
|
} catch {}
|
|
document.body.appendChild(overlay);
|
|
}
|
|
|
|
export function buildClusterItemsHtml(items) {
|
|
if (!Array.isArray(items) || items.length === 0) return "";
|
|
const rows = items.slice(0, 10).map((item) => {
|
|
const title = escapeHtml(item?.title || "");
|
|
const source = escapeHtml(item?.source || "");
|
|
const url = item?.url || "";
|
|
const img = normalizeImageUrl(item?.image_small || item?.image_medium || item?.image_large || "");
|
|
return `
|
|
<a class="sw-cluster-row" href="${escapeHtml(url)}" target="_blank" rel="noopener" title="${title}">
|
|
${img ? `<img src="${escapeHtml(img)}" class="sw-cluster-row-thumb" alt="" loading="lazy" />` : `<div class="sw-cluster-row-thumb sw-cluster-row-thumb--empty"></div>`}
|
|
<div class="sw-cluster-row-info">
|
|
<div class="sw-cluster-row-title">${title}</div>
|
|
<div class="sw-cluster-row-source">${source}</div>
|
|
</div>
|
|
</a>
|
|
`;
|
|
});
|
|
return rows.join("");
|
|
}
|