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, """); 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.