PageSourceSearch

https://palermo-huila.gov.co/SiteAssets/V3/Componentes/interactive/halloween/src/js/scroll.js

js palermo-huila.gov.co collected 2026-10-02 00:05:37 UTC 15,800 bytes, 454 lines download raw bytes

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.