1 2 3 4(function () { 5 6const BASE_URL = "/SiteAssets/V3/Componentes/interactive/halloween/"; 7 8 9 "use strict"; 10 11 // ======================== 12 // 0) Fade + LocalStorage 13 // ======================== 14 const FADE = { inMs: 700, outMs: 800 }; 15 const LS_KEY = "hw_halloween_state"; 16 17 function fadeIn(el, ms = FADE.inMs) { 18 if (!el) return; 19 el.style.opacity = "0"; 20 el.style.transition = `opacity ${ms}ms ease`; 21 // siguiente frame â 1 (evita aparición brusca) 22 requestAnimationFrame(() => { el.style.opacity = "1"; }); 23 } 24 function fadeOutThen(el, ms, after) { 25 if (!el) { after && after(); return; } 26 el.style.transition = `opacity ${ms}ms ease`; 27 el.style.opacity = "1"; 28 requestAnimationFrame(() => { el.style.opacity = "0"; }); 29 const done = () => { el.removeEventListener("transitionend", done); after && after(); }; 30 el.addEventListener("transitionend", done); 31 setTimeout(done, ms + 300); // respaldo 32 } 33 function saveState(patch) { 34 try { 35 const prev = JSON.parse(localStorage.getItem(LS_KEY) || "{}"); 36 const next = { ...prev, ...patch, lastUpdate: Date.now() }; 37 localStorage.setItem(LS_KEY, JSON.stringify(next)); 38 } catch {} 39 } 40 41 // ============================== 42 // 1) Hora local America/Bogota 43 // ============================== 44 function nowInTZ(tz = "America/Bogota") { 45 const d = new Date(); 46 const fmt = new Intl.DateTimeFormat("es-CO", { 47 timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit", 48 hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false 49 }); 50 const parts = fmt.formatToParts(d).reduce((acc, p) => (acc[p.type] = p.value, acc), {}); 51 const yyyy = parts.year, mm = parts.month, dd = parts.day; 52 const HH = parts.hour, MM = parts.minute, SS = parts.second; 53 return { 54 date: `${yyyy}-${mm}-${dd}`, 55 time: `${HH}:${MM}:${SS}`, 56 isoLocal: `${yyyy}-${mm}-${dd}T${HH}:${MM}:${SS}` 57 }; 58 } 59 60 // ========================= 61 // 2) Ventanas de visibilidad 62 // ========================= 63 function isNowInWindows(windows, tz = "America/Bogota") { 64 const { date, time, isoLocal } = nowInTZ(tz); 65 const tNum = toMinutes(time); 66 for (const w of windows) { 67 if (w.type === "daily") { 68 const s = toMinutes(w.start), e = toMinutes(w.end); 69 if (s <= e ? (tNum >= s && tNum <= e) : (tNum >= s || tNum <= e)) return true; 70 } else if (w.type === "datetime") { 71 if (isoLocal >= w.start && isoLocal <= w.end) return true; // YYYY-MM-DDTHH:MM[:SS] 72 } else if (w.type === "date") { 73 if (date >= w.start && date <= w.end) return true; 74 } 75 } 76 return false; 77 function toMinutes(hhmm) { const [h, m] = hhmm.split(":").map(Number); return h * 60 + m; } 78 } 79 80 // ============================== 81 // 3) Config de programación 82 // ============================== 83 const SCHEDULE = { 84 timeZone: "America/Bogota", 85 windows: [ 86 //{ type: "daily", start: "00:00", end: "23:59" }, 87 { type: "date", start: "2025-09-30", end: "2025-10-31" } 88 // { type: "datetime", start: "2025-10-31T18:00", end: "2025-10-31T23:59" } 89 ], 90 pollMs: 1000 // chequeo cada 1s para que el cambio sea inmediato y con fade 91 }; 92 93 // ========================================== 94 // 4) Montaje/Desmontaje (con FADES reales) 95 // ========================================== 96 let mountScrollbar = null, unmountScrollbar = null; 97 let mountCursors = null, unmountCursors = null; 98 let mountGifBar = null, unmountGifBar = null; 99 100 // 4.1 Scrollbar-araña 101 function createSpiderScrollbar(ws, opts) { 102 const { 103 svgUrl = `${BASE_URL}/assets/img/img_spider.svg`, 104 side = "right", 105 offset = 1, 106 topMargin = 0, 107 bottomMargin = 16, 108 minThumbPx = 32 109 } = (opts || {}); 110 111 const track = document.createElement("div"); 112 const rail = document.createElement("div"); 113 const web = document.createElement("div"); 114 const handle = document.createElement("div"); 115 const spider = document.createElement("img"); 116 117 track.id = "hw-spider-track"; 118 track.className = "spider-track"; 119 rail.className = "spider-rail"; 120 web.className = "spider-web"; 121 handle.className = "spider-handle"; 122 spider.className = "spider-icon"; 123 124 // ¡Clave para evitar el âflashâ! 125 track.style.opacity = "0"; // inicio invisible 126 track.style.transition = `opacity ${FADE.inMs}ms ease`; 127 128 handle.style.pointerEvents = "auto"; 129 spider.src = svgUrl; 130 spider.alt = "spider"; 131 132 track.appendChild(rail); 133 track.appendChild(web); 134 track.appendChild(handle); 135 track.appendChild(spider); 136 const wsParent = document.getElementById("s4-workspace"); 137 if (wsParent) { 138 wsParent.appendChild(track); 139 } else { 140 document.body.appendChild(track); // fallback por si no existe el div 141 } 142 143 function sizeTrack() {
144 track.style.top = topMargin + "px"; 145 track.style.height = (window.innerHeight - topMargin - bottomMargin) + "px"; 146 track.style[side] = offset + "px"; 147 } 148 149 function updateFromScroll() { 150 const trackH = track.getBoundingClientRect().height; 151 const maxScroll = Math.max(1, ws.scrollHeight - ws.clientHeight); 152 const p = ws.scrollTop / maxScroll; // 0..1 153 const thumbPx = Math.max(minThumbPx, (ws.clientHeight / ws.scrollHeight) * trackH); 154 const y = p * (trackH - thumbPx); 155 156 const webHeight = Math.max(0, y + Math.min(thumbPx * 0.5, 18)); 157 web.style.height = webHeight + "px"; 158 159 160 const spH = 20; 161 const spiderY = Math.max(0, Math.min(trackH - spH, webHeight - spH * 0.25)); 162 spider.style.top = spiderY + "px"; 163 164 } 165 166 let ticking = false; 167 function onWsScroll() { 168 if (!ticking) { 169 ticking = true; 170 requestAnimationFrame(() => { ticking = false; updateFromScroll(); }); 171 } 172 } 173 174 // Drag 175 let dragging = false; 176 function posToScroll(clientY) { 177 const rect = track.getBoundingClientRect(); 178 const yInTrack = Math.min(Math.max(0, clientY - rect.top), rect.height); 179 const trackH = rect.height; 180 const maxScroll = Math.max(1, ws.scrollHeight - ws.clientHeight); 181 const thumbPx = Math.max(minThumbPx, (ws.clientHeight / ws.scrollHeight) * trackH); 182 const maxY = trackH - thumbPx; 183 const clampedY = Math.min(Math.max(0, yInTrack - thumbPx / 2), maxY); 184 const p = clampedY / maxY; 185 return p * maxScroll; 186 } 187 function onDown(e) { dragging = true; handle.style.cursor = "grabbing"; e.preventDefault(); } 188 function onMove(e) { 189 if (!dragging) return; 190 const clientY = (e.touches ? e.touches[0].clientY : e.clientY); 191 ws.scrollTop = posToScroll(clientY); 192 e.preventDefault(); 193 } 194 function onUp() { if (!dragging) return; dragging = false; handle.style.cursor = "grab"; } 195 196 handle.addEventListener("mousedown", onDown); 197 window.addEventListener("mousemove", onMove, { passive: false }); 198 window.addEventListener("mouseup", onUp); 199 handle.addEventListener("touchstart", onDown, { passive: false }); 200 window.addEventListener("touchmove", onMove, { passive: false }); 201 window.addEventListener("touchend", onUp); 202 203 sizeTrack(); 204 updateFromScroll(); 205 ws.addEventListener("scroll", onWsScroll, { passive: true }); 206 window.addEventListener("resize", () => { sizeTrack(); updateFromScroll(); }); 207 208 const ro = ("ResizeObserver" in window) ? new ResizeObserver(updateFromScroll) : null; 209 if (ro) ro.observe(ws); 210 211 // Activa el fade-in tras montar 212 requestAnimationFrame(() => { track.style.opacity = "1"; }); 213 214 // cleanup 215 return function cleanup() { 216 ws.removeEventListener("scroll", onWsScroll); 217 window.removeEventListener("resize", sizeTrack); 218 window.removeEventListener("mousemove", onMove); 219 window.removeEventListener("mouseup", onUp); 220 window.removeEventListener("touchmove", onMove); 221 window.removeEventListener("touchend", onUp); 222 try { ro && ro.disconnect && ro.disconnect(); } catch (e) {} 223 try { document.getElementById("hw-spider-track")?.remove(); } catch (e) {} 224 }; 225 } 226 227 function createImage(){ 228 const imagen = document.createElement('img'); 229 imagen.id= "imgTop" 230 imagen.src = `${BASE_URL}/assets/img/telarana_sin_fondo.png`; 231 imagen.alt = "Top Image"; 232 imagen.style.cssText = ` 233 position: fixed; /* o absolute dentro de #s4-workspace */ 234 top: 14; left: -9px; 235 width: 128px; 236 height: auto; 237 color: rgba(27, 26, 26, 0.7); /* color del trazo (hereda currentColor) */ 238 pointer-events: none; 239 z-index: 99999; 240 `; 241 242 const wsParent = document.getElementById("s4-workspace"); 243 if (wsParent) { 244 wsParent.appendChild(imagen); 245 } else { 246 document.body.appendChild(imagen); // fallback por si no existe el div 247 } 248 } 249 250 // Aparece con fade-in 251 252 253 254 // 4.2 Cursores 255 function mountCursorsImpl(normalCursor) { 256 const style = document.createElement("style"); 257 style.id = "hw-cursors-style"; 258 style.innerHTML = ` 259 html, body, * { cursor: url('${normalCursor}'), auto !important; } 260 `; 261 document.head.appendChild(style); 262 } 263 function unmountCursorsImpl() { 264 document.getElementById("hw-cursors-style")?.remove(); 265 } 266 267 // 4.3 Barra GIF inferior 268 function mountGifBarImpl(url) { 269 const ID = "cemetery-gif-bar"; 270 document.getElementById(ID)?.remove(); 271 272 const img = document.createElement("img"); 273 img.id = ID; 274 img.src = url; 275 img.alt = "Halloween bar"; 276 img.style.cssText = ` 277 position: fixed; left: 0; bottom: 0; width: 100%; height: 60px; 278 pointer-events: none; z-index: 99999999; display: block; opacity:0; 279 transition: opacity ${FADE.inMs}ms ease; 280 `; 281 const ws = document.getElementById("s4-workspace"); 282 if (ws) { 283 ws.appendChild(img); 284 } else { 285 document.body.appendChild(img); // fallback por si no existe el div 286 } 287 // activar fade-in 288 requestAnimationFrame(() => { img.style.opacity = "1"; });
289 } 290 function unmountGifBarImpl() { 291 const el = document.getElementById("cemetery-gif-bar"); 292 fadeOutThen(el, FADE.outMs, () => el?.remove()); 293 } 294 295 // ====================================== 296// 5) Montadores públicos (no toques) 297// ====================================== 298 299// --- Helper: elegir GIF por "media query" de ancho --- 300function pickGifUrl() { 301 // Permite override por variables globales si las defines 302 const LG = window.CEMETERY_GIF_URL_LG || `${BASE_URL}/assets/img/gif_halloween_3440px.gif`; 303 const MD = window.CEMETERY_GIF_URL_MD || window.CEMETERY_GIF_URL || `${BASE_URL}/assets/img/gif_halloween_1366px.gif`; 304 const SM = window.CEMETERY_GIF_URL_SM || `${BASE_URL}/assets/img/gif_halloween_600px.gif`; 305 306 const w = window.innerWidth; 307 if (w > 1366) return LG; 308 if (w < 768) return SM; 309 return MD; 310} 311 312// --- GIF bar con selección por breakpoint + swap en resize --- 313let gifResizeHandler = null; 314 315mountGifBar = () => { 316 const ws = document.getElementById("s4-workspace"); 317 if (!ws) return; 318 if (getComputedStyle(ws).position === "static") { 319 ws.style.position = "relative"; 320 } 321 322 const url = pickGifUrl(); 323 mountGifBarImpl(url); 324 saveState({ gifBar: true, gifURL: url }); 325 326 // Cambiar GIF si el viewport cruza un breakpoint 327 gifResizeHandler = () => { 328 const next = pickGifUrl(); 329 const el = document.getElementById("cemetery-gif-bar"); 330 if (!el) return; 331 332 // Evita cambios innecesarios comparando href absoluto 333 const absNext = new URL(next, location.origin).href; 334 if (el.src === absNext) return; 335 336 // Suaviza el cambio: fade-out -> swap -> fade-in 337 el.style.opacity = "0"; 338 const onEnd = () => { 339 el.removeEventListener("transitionend", onEnd); 340 el.src = next; 341 requestAnimationFrame(() => { el.style.opacity = "1"; }); 342 saveState({ gifURL: next }); 343 }; 344 el.addEventListener("transitionend", onEnd); 345 }; 346 347 window.addEventListener("resize", gifResizeHandler); 348}; 349unmountGifBar = () => { 350 if (gifResizeHandler) { 351 window.removeEventListener("resize", gifResizeHandler); 352 gifResizeHandler = null; 353 } 354 unmountGifBarImpl(); 355 saveState({ gifBar: false }); 356}; 357 358let cleanupScrollbar = null; 359mountScrollbar = () => { 360 const ws = document.getElementById("s4-workspace"); 361 if (!ws) return; 362 cleanupScrollbar = createSpiderScrollbar(ws, { 363 svgUrl: `${BASE_URL}/assets/img/img_spider.svg`, 364 side: "right", offset: 1, topMargin: 0, bottomMargin: 16, minThumbPx: 32 365 }); 366 saveState({ scrollbar: true }); 367}; 368unmountScrollbar = () => { 369 const el = document.getElementById("hw-spider-track"); 370 fadeOutThen(el, FADE.outMs, () => { 371 if (cleanupScrollbar) { cleanupScrollbar(); cleanupScrollbar = null; } 372 saveState({ scrollbar: false }); 373 }); 374}; 375 376 377 // ================================== 378 // 6) Motor de activación en caliente 379 // ================================== 380 let active = false; 381 function refresh() { 382 const shouldBeActive = isNowInWindows(SCHEDULE.windows, SCHEDULE.timeZone); 383 if (shouldBeActive && !active) { 384 active = true; 385 mountScrollbar(); // ya entra con opacity:0 â 1 386 createImage(); // cursors no tienen fade, pero se activan en caliente 387 mountGifBar(); 388 setTimeout(applyAdminTopsNow, 500); // opacity:0 â 1 389 saveState({ active: true }); 390 } else if (!shouldBeActive && active) { 391 active = false; 392 // fade-out sin recargar 393 unmountScrollbar(); 394 unmountGifBar(); 395 document.getElementById("imgTop")?.remove(); 396 saveState({ active: false }); 397 } 398 } 399 400 // Primera pasada + evaluación cada 1s (sin reload) 401 refresh(); 402 setInterval(refresh, SCHEDULE.pollMs); 403 404 405 // --- Detecta si el usuario es Site Admin (SP REST) + aplica TOPs --- 406 async function isAdmin() { 407 try { 408 const resp = await fetch("/_api/web/currentuser", { 409 headers: { 410 Accept: "application/json;odata=verbose", 411 "Content-Type": "application/json;odata=verbose", 412 }, 413 credentials: "include", 414 }); 415 if (!resp.ok) { 416 const txt = await resp.text(); 417 console.error("isAdmin(): SharePoint API no OK ->", txt); 418 return false; 419 } 420 const data = await resp.json(); 421 return !!(data?.d?.IsSiteAdmin); 422 } catch (err) { 423 console.error("isAdmin(): error de red/parseo ->", err); 424 return false; 425 } 426 } 427 428 // Cambia los top de: 429 // - #imgTop 430 // - otro elemento por clase (ajusta el selector si lo necesitas) 431 // - #hw-spider-track 432 function applyTopForAdmin(isAdminUser) { 433 // 1) #imgTop 434 const imgTopEl = document.getElementById("imgTop"); 435 if (imgTopEl) { 436 // Admin: conserva su top original (-14px). No admin: muévelo. 437 imgTopEl.style.top = isAdminUser ? "55px" : "-14px"; 438 } 439 440 // 3) Spider track 441 const spider = document.getElementById("hw-spider-track"); 442 if (spider) { 443 // Admin: 0px; No admin: 65px 444 spider.style.top = isAdminUser ? "65px" : "0"; 445 // Nota: en un resize, createSpiderScrollbar vuelve a calcular top con "topMargin". 446 // Si tu página cambia mucho de tamaño, vuelve a llamar applyAdminTopsNow(). 447 } 448 } 449 450 function applyAdminTopsNow() { 451 isAdmin().then(applyTopForAdmin).catch(console.error); 452 } 453 454})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.