PageSourceSearch

https://www.alilauro.it/wp-content/themes/alilauro/script/alilauro-widget.js?v=1771257568

js alilauro.it collected 2026-09-24 18:35:46 UTC 22,046 bytes, 618 lines download raw bytes

1class AlilauroTimetable extends HTMLElement {
2  constructor() {
3    super();
4    this._root = this.attachShadow({ mode: "open" });
5    this._state = { rows: [], dep: "", arr: "", day: "" }; // day: "", "today", "tomorrow"
6    this._t = null;
7    this._resizeObs = null;
8    this._isCard = false;
9  }
10
11  static get observedAttributes() { return ["src", "dep", "arr", "sid"]; }
12  attributeChangedCallback() { this._schedule(); }
13  connectedCallback() { this._renderSkeleton(); this._schedule(); this._setupResizeObserver(); }
14  disconnectedCallback() {
15    if (this._t) { clearInterval(this._t); this._t = null; }
16    if (this._resizeObs) { this._resizeObs.disconnect(); this._resizeObs = null; }
17  }
18
19    _renderSkeleton() {
20    this._root.innerHTML = `
21      <style>
22         :host{
23          display:block;
24          width: 100%;
25          max-width: 100%; 
26          margin: var(--tt-gap-top, 24px) auto var(--tt-gap-bottom, 40px) auto;
27          padding: 0 12px;
28          box-sizing: border-box;
29          color:#202857;
30        }
31        .tt-card{
32          background:#fff;
33          border:1px solid #e5e7eb;
34          border-radius:12px;
35          box-shadow:0 6px 18px rgba(16,24,40,.06);
36          overflow:hidden;
37        }
38        .toolbar{
39          display:flex;
40          gap:.75rem;
41          flex-wrap:wrap;
42          align-items:end;
43          justify-content:center;
44          margin:0 0 .75rem 0;
45        }
46        label{ font-weight:600; font-size:.9rem; }
47        select{
48          padding:.45rem .6rem;
49          border:1px solid #d0d7de;
50          border-radius:8px;
51          background:#fff;
52          min-width:220px;
53          font-family:"Aptos Narrow", system-ui,-apple-system, Segoe UI, Roboto, Arial, sans-serif;
54        }
55        .truncate{
56          display:inline-block;
57          max-width:100%;
58          overflow:hidden;
59          text-overflow:ellipsis;
60          white-space:nowrap;
61          min-width:0;
62          vertical-align:bottom;
63        }
64        .table-wrap{
65          width:100%;
66          overflow:auto;
67          -webkit-overflow-scrolling:touch
68        }
69        .table{
70          width:100%;
71          border-collapse:separate;
72          border-spacing:0;
73          margin:0;
74        }
75        thead th{
76          position:sticky;
77          top:0;
78          z-index:1;
79          background:#f6f8fa;
80          text-align:left;
81          padding:.6rem .7rem;
82          font-weight:700;
83          border-bottom:1px solid #e5e7eb;
84          white-space:nowrap;
85          color:#0a66c2; /* testata blu come richiesto */
86        }
87        tbody td{
88          padding:.65rem .7rem;
89          border-bottom:1px solid #f0f2f4;
90          vertical-align:middle;
91        }
92        tbody tr:nth-child(odd){ background:#fafbfc; }
93        .time{
94          display:inline-block;
95          min-width:64px;
96          text-align:center;
97          padding:.35rem .55rem;
98          border-radius:999px;
99          font-weight:700;
100          background:#0a66c2; /* pill orario blu */
101          color:#fff;
102        }
103        .dot{
104          display:inline-block;
105          width: 20px;
106          height: 20px;
107          border-radius:50%;
108          vertical-align:middle;
109          box-shadow:0 0 0 2px rgba(0,0,0,.05);
110          margin-right:.4rem;
111        }
112        .open{ background:#1e9e37; }
113        .pending{ background:#f59e0b; }
114        .closed{ background:#dc3545; }
115        .muted{ color:#6b7280; }
116        .empty{ text-align:center; color:#6b7280; padding:1rem; }
117        tbody td:nth-child(6) .truncate, tbody td:nth-child(7) .truncate{ display:block; }
118        .cards{
119          display:grid;
120          gap:10px;
121          padding:10px;
122        }
123        .card{
124          border:1px solid #e5e7eb;
125          border-radius:12px;
126          padding:12px;
127          background:#fff;
128          display:grid;
129          grid-template-columns: 64px 1fr;
130          gap:12px;
131          align-items:center;
132        }
133        .badge-time{
134          display:flex;
135          flex-direction:column;
136          align-items:center;
137          gap:6px;
138          min-width:0;
139        }
140        .badge-time .time{ min-width:auto; padding:.35rem .55rem; }
141        .status-pill{
142          display:inline-flex;
143          align-items:center;
144          gap:6px;
145          font-size:.85rem;
146          font-weight:600;
147          min-width:0;
148        }
149        .route{ font-weight:700; min-width:0; }
150        .small{ font-size:.85rem; color:#6b7280; }
151        .meta{
152          display:grid;
153          grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
154          gap:8px;
155          font-size:.9rem;
156          color:#374151;
157          min-width:0;
158        }
159        .pill{
160          display:inline-flex;
161          align-items:center;
162          gap:6px;
163          padding:4px 8px;
164          border:1px dashed #e5e7eb;
165          border-radius:999px;
166          max-width:100%;
167          min-width:0;
168        }
169        @media (max-width: 992px){
170          select{ min-width:180px; }
171        }
172        @media (max-width: 768px){
173          thead th:nth-child(7),
174          thead th:nth-child(8),
175          tbody td:nth-child(7),
176          tbody td:nth-child(8){ display:none }
177          .time{ min-width:auto; padding:.3rem .5rem; }
178        }
179        @media (max-width: 480px){
180          thead th:nth-child(2),
181          tbody td:nth-child(2){ display:none }
182          select{ min-width:150px; }
183        }
184        select:focus{
185          outline:2px solid #0a66c2;
186          outline-offset:1px;
187        }
188        .btn{
189          display:inline-block;
190          font-weight:700;
191          text-decoration:none;
192          padding:.45rem .8rem;
193          border-radius:999px;
194          border:1px solid #0a66c2;
195          background:#0a66c2;
196          color:#fff;
197          white-space:nowrap;
198          font-family:"Aptos Narrow", system-ui,-apple-system, Segoe UI, Roboto, Arial, sans-serif;
199        }
200        .btn:focus{
201          outline:2px solid #0a66c2;
202          outline-offset:2px;
203        }
204        .btn:hover{ filter:brightness(0.95); }
205
206        /* Separatori per cambio data */
207        .sep-row td{
208          padding:.4rem .7rem;
209          background:#f1f5f9;
210          border-bottom:1px solid #e5e7eb;
211          text-align:center;
212        }
213        .sep-chip{
214          display:inline-block;
215          font-weight:700;
216          padding:.25rem .6rem;
217          border-radius:999px;
218          border:1px solid #cbd5e1;
219          background:#fff;
220          text-align:center;
221          color:202857;
222        }
223        .cards-sep{
224          font-weight:700;
225          padding:8px 10px;
226          background:#f1f5f9;
227          border:1px solid #e5e7eb;
228          border-radius:10px;
229        }
230      </style>
231
232      <style>
233        :host{
234          display:block;
235          max-width: var(--tt-max, 1200px);
236          margin: var(--tt-gap-top, 24px) auto var(--tt-gap-bottom, 40px) auto;
237          padding: 0 12px;
238          color:#202857;
239        }
240        ...
241        .btn:hover{ filter:brightness(0.95); }
242
243        /* === MODE SIMPLE (senza Arrivo e Durata) === */
244        :host([mode="simple"]) thead th:nth-child(2),
245        :host([mode="simple"]) tbody td:nth-child(2),
246        :host([mode="simple"]) thead th:nth-child(7),
247        :host([mode="simple"]) tbody td:nth-child(7){
248          display:none;
249        }
250
251        :host([mode="simple"]) .badge-time .small{
252          display:none;
253        }
254
255        /* Separatori per cambio data (coerenti con il primo blocco) */
256        .sep-row td{
257          padding:.4rem .7rem;
258          background:#f1f5f9;
259          border-bottom:1px solid #e5e7eb;
260          text-align:center;
261        }
262        .sep-chip{
263          display:inline-block;
264          font-weight:700;
265          padding:.25rem .6rem;
266          border-radius:999px;
267          border:1px solid #cbd5e1;
268          background:#fff;
269          text-align:center;
270          color:#202857
271        }
272        .cards-sep{
273          font-weight:700;
274          padding:8px 10px;
275          background:#f1f5f9;
276          border:1px solid #e5e7eb;
277          border-radius:10px;
278        }
279      </style>
280
281      <div class="toolbar" part="filters">
282        <label>Partenza da
283          <select id="dep" aria-label="Filtro porto di partenza"><option value="">Tutti</option></select>
284        </label>
285        <label>Arrivo a
286          <select id="arr" aria-label="Filtro porto di arrivo"><option value="">Tutti</option></select>
287        </label>
288        <label>Data
289          <select id="day" aria-label="Filtro data">
290            <option value="">Tutte</option>
291            <option value="today">Oggi</option>
292            <option value="tomorrow">Domani</option>
293          </select>
294        </label>
295      </div>
296
297      <div class="tt-card">
298        <div id="tableWrap" class="table-wrap" role="region" aria-label="Tabella partenze" tabindex="0">
299          <table class="table" role="table" aria-label="Prossime partenze">
300            <thead><tr>
301              <th>Orario</th>
302              <th>Arrivo</th>
303              <th>Da</th>
304              <th>A</th>
305              <th>Nave</th>
306              <th>operata da</th>
307              <th>Durata</th>
308              <th>Status</th>
309              <th>Acquista ora</th>
310            </tr></thead>
311            <tbody id="body"><tr><td colspan="9" class="empty">Caricamento…</td></tr></tbody>
312          </table>
313        </div>
314        <div id="cardsWrap" class="cards" role="list" aria-label="Partenze" hidden></div>
315      </div>
316    `;
317
318    this._root.getElementById("dep").addEventListener("change", (e) => { this._state.dep = e.target.value; this._renderRows(); });
319    this._root.getElementById("arr").addEventListener("change", (e) => { this._state.arr = e.target.value; this._renderRows(); });
320    this._root.getElementById("day").addEventListener("change", (e) => { this._state.day = e.target.value; this._renderRows(); });
321  }
322
323
324  _setupResizeObserver() {
325    const decide = () => {
326      const w = this._root.host.getBoundingClientRect().width || 0;
327      const nextIsCard = w < 768;
328      if (nextIsCard !== this._isCard) { this._isCard = nextIsCard; this._renderRows(); }
329    };
330    decide();
331    this._resizeObs = new ResizeObserver(decide);
332    this._resizeObs.observe(this._root.host);
333  }
334
335  _schedule() {
336    const src = this.getAttribute("src");
337    if (!src) return;
338    if (this._t) { clearInterval(this._t); this._t = null; }
339    this._fetchAndRender();
340  }
341
342  async _fetchAndRender() {
343    const src = this.getAttribute("src");
344    if (!src) return;
345    const sid = this.getAttribute("sid") || "";
346    const u = new URL(src, window.location.href);
347    if (sid) u.searchParams.set("sid", sid);
348
349    try {
350      const r = await fetch(u.toString(), { cache: "no-store" });
351      if (!r.ok) throw new Error(`HTTP ${r.status}`);
352      const j = await r.json();
353      const rows = Array.isArray(j.rows) ? j.rows : Array.isArray(j) ? j : [];
354      this._state.rows = rows;
355      this._populateFilters();
356      this._renderRows();
357      this._postHeight();
358    } catch (err) {
359      console.error("[alilauro-timetable] fetch error:", err);
360      this._root.getElementById("body").innerHTML = `<tr><td colspan="9" class="empty">Nessun dato disponibile</td></tr>`;
361      const cardsWrap = this._root.getElementById("cardsWrap");
362      cardsWrap.innerHTML = `<div role="listitem" class="small">Nessun dato disponibile</div>`;
363      this._postHeight();
364    }
365  }
366
367  _populateFilters() {
368    const depSel = this._root.getElementById("dep");
369    const arrSel = this._root.getElementById("arr");
370    const depAttr = this.getAttribute("dep") || "";
371    const arrAttr = this.getAttribute("arr") || "";
372
373    const deps = Array.from(new Set(this._state.rows.map((r) =>
373 r.depName).filter(Boolean)))
374      .sort((a, b) => a.localeCompare(b, "it", { sensitivity: "base" }));
375    const arrs = Array.from(new Set(this._state.rows.map((r) => r.arrName).filter(Boolean)))
376      .sort((a, b) => a.localeCompare(b, "it", { sensitivity: "base" }));
377
378    depSel.innerHTML = `<option value="">Tutti</option>` + deps.map(
379      (d) => `<option value="${this._esc(d)}"${d === depAttr ? " selected" : ""}>${this._esc(this._cap(d))}</option>`
380    ).join("");
381    arrSel.innerHTML = `<option value="">Tutti</option>` + arrs.map(
382      (a) => `<option value="${this._esc(a)}"${a === arrAttr ? " selected" : ""}>${this._esc(this._cap(a))}</option>`
383    ).join("");
384
385    this._state.dep = depSel.value;
386    this._state.arr = arrSel.value;
387  }
388
389  _bookingUrl(r) {
390    const dep = r.depCode || r.dep_code || r.dep || "";
391    const arr = r.arrCode || r.arr_code || r.arr || "";
392    const now = new Date();
393    const dd = String(now.getDate()).padStart(2, "0");
394    const mm = String(now.getMonth() + 1).padStart(2, "0");
395    const yyyy = String(now.getFullYear());
396    const dateA = `undefined/undefined/${dd}/${mm}/${yyyy}`;
397    const u = new URL("https://booking.alilauro.it/index.html");
398    u.searchParams.set("search", "run");
399    u.searchParams.set("dep", dep);
400    u.searchParams.set("arr", arr);
401    u.searchParams.set("pass", "1");
402    u.searchParams.set("ar", "0");
403    u.searchParams.set("dateA", dateA);
404    return u.toString();
405  }
406
407  _labelForDate(ymd){
408    const today = this._todayYmd();
409    const tomorrow = this._tomorrowYmd();
410    const d = this._formatIt(ymd);
411    if (ymd === today) return `Oggi — ${d}`;
412    if (ymd === tomorrow) return `Domani — ${d}`;
413    return d;
414  }
415  _todayYmd(){
416    const now = new Date();
417    const y = now.getFullYear();
418    const m = String(now.getMonth()+1).padStart(2,'0');
419    const d = String(now.getDate()).padStart(2,'0');
420    return `${y}-${m}-${d}`;
421  }
422  _tomorrowYmd(){
423    const now = new Date();
424    now.setDate(now.getDate()+1);
425    const y = now.getFullYear();
426    const m = String(now.getMonth()+1).padStart(2,'0');
427    const d = String(now.getDate()).padStart(2,'0');
428    return `${y}-${m}-${d}`;
429  }
430  _formatIt(ymd){
431    const [Y,M,D] = (ymd||'').split('-');
432    if (!Y||!M||!D) return ymd||'';
433    return `${D}/${M}/${Y}`;
434  }
435
436  _renderRows() {
437    const dep = this._state.dep;
438    const arr = this._state.arr;
439    const day = this._state.day;
440    const today = this._todayYmd();
441    const tomorrow = this._tomorrowYmd();
442
443    const rows = this._state.rows.filter((r) => {
444      if (dep && r.depName !== dep) return false;
445      if (arr && r.arrName !== arr) return false;
446      if (day === "today" && r.date !== today) return false;
447      if (day === "tomorrow" && r.date !== tomorrow) return false;
448      return true;
449    });
450
451    const tableWrap = this._root.getElementById("tableWrap");
452    const body = this._root.getElementById("body");
453    const cardsWrap = this._root.getElementById("cardsWrap");
454
455    if (!rows.length) {
456      body.innerHTML = `<tr><td colspan="9" class="empty">Nessuna partenza trovata</td></tr>`;
457      cardsWrap.innerHTML = `<div role="listitem" class="small">Nessuna partenza trovata</div>`;
458      tableWrap.hidden = this._isCard;
459      cardsWrap.hidden = !this._isCard;
460      this._postHeight();
461      return;
462    }
463
464    if (this._isCard) {
465      // Raggruppo per data
466      const groups = [];
467      const map = new Map();
468      for (const r of rows){
469        const key = r.date || "";
470        if (!map.has(key)){ map.set(key, []); groups.push(key); }
471        map.get(key).push(r);
472      }
473      const parts = [];
474      for (const key of groups){
475        if (key){
476          parts.push(`<div class="cards-sep" role="heading" aria-level="2" style="text-align:center">${this._labelForDate(key)}</div>`);
477        }
478        for (const r of map.get(key)){
479          const st = (r.status ?? r.state ?? r.flag ?? "").toString().trim().toUpperCase();
480          const isOpen  = st === "O";
481          const isIssue = st === "I";
482          const dotClass = isOpen ? "open" : (isIssue ? "pending" : "closed");
483          const statusLabel = isOpen ? "Corsa regolare" : (isIssue ? "Corsa in attesa di conferma" : "Corsa sospes
483a");
484
485          const vesselObj = this._vesselDisplay(r.vessel);
486          const company = this._cap(r.companyName || "");
487          const durationLabel = this._durationLabel(r.duration);
488
489          parts.push(`
490            <div class="card" role="listitem" aria-label="${this._esc(this._cap(r.depName))} → ${this._esc(this._cap(r.arrName))} alle ${this._esc(r.departure_hm)}">
491              <div class="badge-time">
492                <span class="time">${this._esc(r.departure_hm)}</span>
493                <span class="small">${this._esc(r.arrival_hm)} arr.</span>
494              </div>
495              <div>
496                <div class="route">${this._esc(this._cap(r.depName))} → ${this._esc(this._cap(r.arrName))}</div>
497                <div class="status-pill">
498                  <span class="dot ${dotClass}"></span>
499                  <span class="${isOpen ? "" : "muted"}">${this._esc(statusLabel)}</span>
500                </div>
501                <div class="meta" aria-hidden="false">
502                  <span class="pill" title="${this._esc(vesselObj.title)}">
503                    🛥️ <span class="truncate">${vesselObj.html}</span>
504                  </span>
505                  ${ company ? `<span class="pill" title="${this._esc(company)}">🏢 <span class="truncate">${this._esc(company)}</span></span>` : "" }
506                  ${ durationLabel ? `<span class="pill" title="${this._esc(durationLabel)}">⏱️ <span class="truncate">${this._esc(durationLabel)}</span></span>` : "" }
507                </div>
508                ${ isOpen ? `
509                  <div style="margin-top:8px">
510                    <a class="btn" href="${this._bookingUrl(r)}" target="_blank" rel="noopener"
511                       aria-label="Prenota ${this._esc(this._cap(r.depName))} ${this._esc(r.departure_hm)} verso ${this._esc(this._cap(r.arrName))}">
512                      Acquista ora
513                    </a>
514                  </div>` : "" }
515              </div>
516            </div>`);
517        }
518      }
519      cardsWrap.innerHTML = parts.join("");
520      tableWrap.hidden = true;
521      cardsWrap.hidden = false;
522    } else {
523      // Tabella con separatori per data
524      let lastDate = null;
525      let html = "";
526      for (const r of rows){
527        const curDate = r.date || "";
528        if (curDate && curDate !== lastDate){
529          html += `<tr class="sep-row"><td colspan="9"><span class="sep-chip">${this._labelForDate(curDate)}</span></td></tr>`;
530          lastDate = curDate;
531        }
532        const st = (r.status ?? r.state ?? r.flag ?? "").toString().trim().toUpperCase();
533        const isOpen  = st === "O";
534        const isIssue = st === "I";
535        const dotClass = isOpen ? "open" : (isIssue ? "pending" : "closed");
536        const statusLabel = isOpen ? "Corsa regolare" : (isIssue ? "Corsa in attesa di conferma" : "Corsa sospesa");
537
538        const vesselObj = this._vesselDisplay(r.vessel);
539        const companyTitle = this._cap(r.companyName || "");
540        const durationLabel = this._durationLabel(r.duration);
541
542        html += `<tr>
543          <td><span class="time">${this._esc(r.departure_hm)}</span></td>
544          <td>${this._esc(r.arrival_hm)}</td>
545          <td>${this._esc(this._cap(r.depName))}</td>
546          <td>${this._esc(this._cap(r.arrName))}</td>
547          <td>
548            <span class="truncate" title="${this._esc(vesselObj.title)}">
549              ${vesselObj.html}
550            </span>
551          </td>
552          <td><span class="truncate" title="${this._esc(companyTitle)}">${this._esc(companyTitle)}</span></td>
553          <td>${durationLabel ? this._esc(durationLabel) : ""}</td>
554          <td><span class="dot ${dotClass}"></span><span class="muted">${this._esc(statusLabel)}</span></td>
555          <td>${
556            isOpen ? `<a class="btn" href="${this._bookingUrl(r)}" target="_blank" rel="noopener"
557              aria-label="Prenota ${this._esc(this._cap(r.depName))} ${this._esc(r.departure_hm)} verso ${this._esc(this._cap(r.arrName))}">Acquista ora</a>` : ""
558          }</td>
559        </tr>`;
560      }
561      body.innerHTML = html;
562      tableWrap.hidden = false;
563      cardsWrap.hidden = true;
564    }
565    this._postHeight();
566  }
567
568  _postHeight() {
569    try {
570      this.dispatchEvent(new CustomEvent("alilauro:height", {
571        detail: this._root.host.getBoundingClientRect().height,
572      }));
573    } catch {}
574  }
575
576  _esc(s) {
577    const d = document.createElement("div");
578    d.textContent = String(s ?? "");
579    return d.innerHTML;
580  }
581
582  // lowercase + capitalize
583  _cap(s) {
584    const str = String(s ?? "").toLowerCase();
585    return str.replace(/\b\w/g, (ch) => ch.toUpperCase());
586  }
587
588  // Durata con "ca." davanti (senza duplicare)
589  _durationLabel(val){
590    const raw = String(val ?? "").trim();
591    if (!raw) return "";
592    if (/ca\./i.test(raw)) return raw;
593    return `ca. ${raw}`;
594  }
595
596  // Gestione nave:
597  // - se vuota / "nessuno" => "-"
598  // - capitalize
599  // - Giove / Celestina / Busnavetta in corsivo
600   // Gestione nave:
601  // - se vuota / "nessuno" => "-"
602  // - capitalize
603  // - TUTTE le navi in corsivo
604  _vesselDisplay(v){
605    let raw = String(v ?? "").trim();
606    if (!raw || /^nessuno$/i.test(raw)) {
607      return { html: "-", title: "-" };
608    }
609    // capitalize
610    const cap = this._cap(raw);
611    // tutto il nome nave in corsivo
612    const html = `<em>${cap}</em>`;
613    const title = cap; // title senza tag
614    return { html, title };
615  }
616
617}
618customElements.define("alilauro-timetable", AlilauroTimetable);

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.