PageSourceSearch

https://survival.es/assets/controllers/numiko/indigenous_voices_controller-b0e02197.js

js survival.es collected 2026-09-24 10:00:10 UTC 6,405 bytes, 198 lines download raw bytes

1import { Controller } from "@hotwired/stimulus";
2
3// Connects to data-controller="numiko--indigenous-voices"
4export default class extends Controller {
5  static values = {
6    actNowText: String,
7    videos: Array,
8  };
9
10  connect() {
11    this.renderVideoGrid();
12    this.hidePlayerSection();
13  }
14
15  hidePlayerSection() {
16    const playerSection = document.getElementById("video-player-section");
17    if (playerSection) {
18      playerSection.style.display = "none";
19    }
20  }
21
22  renderVideoGrid() {
23    const videoGrid = document.getElementById("video-grid");
24    if (!videoGrid) return;
25
26    const videos = this.videosValue || [];
27
28    if (videos.length === 0) {
29      videoGrid.innerHTML = `<div class="col-12"><p>No videos available.</p></div>`;
30      return;
31    }
32
33    videoGrid.innerHTML = videos
34      .map(
35        (video, index) => {
36          let colClass = "col-12 col-sm-6 col-md-4 col-lg-3";
37          let offsetClass = "";
38
39          // Adjust offset for the first video of the third row if there are exactly 10 videos
40          if (index === 8 && videos.length === 10) {
41            // First video of third row - add offset to center
42            offsetClass = "offset-lg-3";
43          }
44
45          return `
46      <div class="${colClass} ${offsetClass}">
47        <div class="card video-card"
48             data-action="click->numiko--indigenous-voices#playVideo"
49             data-video-id="${this.escapeDataAttr(video.video_id)}"
50             data-video-title="${this.escapeDataAttr(video.name)}"
51             data-video-description="${this.escapeDataAttr(video.description || '')}"
52             data-video-link="${this.escapeDataAttr(video.link || '')}"
53             style="cursor: pointer;">
54          <img src="${video.thumbnail_url}" class="card-img-top" alt="${this.escapeHtml(video.name)}" loading="lazy">
55        </div>
56      </div>
57    `;
58        }
59      )
60      .join("");
61  }
62
63  playVideo(event) {
64    const card = event.currentTarget;
65    const videoId = card.dataset.videoId;
66    const title = card.dataset.videoTitle;
67    const description = card.dataset.videoDescription;
68
69    this.displayVideo(videoId, title, description, true);
70
71    // Scroll to player section
72    const playerSection = document.getElementById("video-player-section");
73    if (playerSection) {
74      playerSection.scrollIntoView({ behavior: "smooth", block: "start" });
75    }
76  }
77
78  closeVideo(event) {
79    event.preventDefault();
80    const playerSection = document.getElementById("video-player-section");
81
82    if (playerSection) {
83      // Add closing class for animation
84      playerSection.classList.add("closing");
85
86      // Wait for animation to complete, then hide
87      setTimeout(() => {
88        playerSection.style.display = "none";
89        playerSection.classList.remove("closing");
90
91        // Clear the iframe to stop playback
92        const iframeContainer = document.getElementById("vimeo-iframe-container");
93        if (iframeContainer) {
94          iframeContainer.innerHTML = "";
95        }
96      }, 300); // Match the animation duration
97    }
98  }
99
100  displayVideo(videoId, title, description) {
101    // Update video player section
102    const playerSection = document.getElementById("video-player-section");
103    const titleElement = document.getElementById("video-title");
104    const iframeContainer = document.getElementById("vimeo-iframe-container");
105    const descriptionElement = document.getElementById("video-description");
106    const actionButton = document.getElementById("video-action-button");
107
108    if (!playerSection || !titleElement || !iframeContainer || !descriptionElement || !actionButton) {
109      console.error("Missing video player elements", { playerSection, titleElement, iframeContainer, descriptionElement, actionButton });
110      return;
111    }
112
113    // Show player section
114    playerSection.style.display = "block";
115
116    // Set title
117    titleElement.textContent = title;
118
119    // Create iframe
120    iframeContainer.innerHTML = `
121      <iframe
122        src="https://player.vimeo.com/video/${videoId}?autoplay=0"
123        frameborder="0"
124        allow="autoplay; fullscreen; picture-in-picture"
125        allowfullscreen
126        style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
127      </iframe>
128    `;
129
130    // Parse description and extract link
131    const { formattedDescription, actionLink, actionText } = this.formatDescription(description);
132
133    // Set description with proper formatting
134    descriptionElement.innerHTML = formattedDescription;
135
136    // Set action button
137    if (actionLink) {
138      actionButton.href = actionLink;
139      actionButton.textContent = actionText || this.actNowTextValue;
140      actionButton.style.display = "inline-block";
141    } else {
142      actionButton.style.display = "none";
143    }
144  }
145
146  // Extract the action link from the description if it exists and format the description text into paragraphs.
147  // The description is expected to have the format:
148  // Description text...
149  //
150  // -- (separator)
151  //
152  // Act now: <url>
153  formatDescription(description) {
154    if (!description) return { formattedDescription: "", actionLink: null, actionText: null };
155
156    let actionLink = null;
157    let actionText = null;
158    let descriptionText = description;
159
160    // Check if description contains the action link separator "--"
161    const parts = description.split(/\n--\n|\n--$/);
162
163    if (parts.length > 1) {
164      // First part is the description, second part contains the action link
165      descriptionText = parts[0].trim();
166      const actionPart = parts[1].trim();
167
168      // Extract link and text from format: "<any text>: <url>"
169      const linkMatch = actionPart.match(/^(.+?):\s*(https?:\/\/[^\s]+)/i);
170      if (linkMatch) {
171        actionText = linkMatch[1].trim();
172        actionLink = linkMatch[2];
173      }
174    }
175
176    // Format the description text into paragraphs
177    const formattedDescription = descriptionText
178      .split("\n\n")
179      .map((para) => `<p>${para.replace(/\n/g, "<br>")}</p>`)
180      .join("");
181
182    return { formattedDescription, actionLink, actionText };
183  }
184
185  escapeHtml(text) {
186    const div = document.createElement("div");
187    div.textContent = text;
188    return div.innerHTML;
189  }
190
191  escapeDataAttr(text) {
192    if (!text) return "";
193    // For data attributes, we only need to escape quotes and backslashes
194    return String(text)
195      .replace(/\\/g, "\\\\")
196      .replace(/"/g, "&quot;");
197  }
198}

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.