1const Search = new class { 2 constructor() { 3 /** 4 * Search bar 5 */ 6 this.searchResults = []; 7 this.searchFocusIndex = null; 8 document.addEventListener('DOMContentLoaded', function() { 9 const searchBar = document.getElementById("search-bar") 10 searchBar.addEventListener("input", function() { 11 Search.search(); 12 }); 13 14 const searchAACheckbox = document.getElementById("search-aa").checked; 15 Utilities.addInputValidator( 16 searchBar, 17 (searchAACheckbox) ? "aa": "dna", 18 ); 19 20 searchBar.addEventListener("keydown", function(event) { 21 if (event.key === "ArrowUp") { 22 event.preventDefault(); 23 Search.navigateResults(-1); 24 } else if (event.key === "ArrowDown") { 25 event.preventDefault(); 26 Search.navigateResults(1); 27 }; 28 }); 29 }); 30 }; 31 32 /** 33 * Clears the search bar 34 */ 35 clear(clearInput=true) { 36 if (clearInput) { 37 document.getElementById("search-bar").value = ""; 38 }; 39 PlasmidViewer.unhighlightBases("base-search"); 40 PlasmidViewer.unhighlightBases("base-search-focus"); 41 this.searchResults = []; 42 this.searchFocusIndex = null; 43 this.updateBarInfo(); 44 }; 45 46 47 /** 48 * Search for DNA or AA sequence in plasmid 49 */ 50 search() { 51 const searchBar = document.getElementById("search-bar"); 52 const searchAASeq = document.getElementById("search-aa")?.checked; 53 const query = searchBar?.value?.toUpperCase().replace(/\s+/g, ""); 54 55 this.clear(false); 56 57 if (!query) return; 58 59 if (searchAASeq) { 60 if (/^[a-zA-Z][0-9]+$/.test(query)) { 61 this.searchSpecificAA(query); 62 return; 63 }; 64 65 this.searchAA(query); 66 return; 67 }; 68 69 70 this.searchDNA(query); 71 return; 72 }; 73 74 75 /** 76 * Search for DNA sequence in plasmid 77 * 78 * @param {String} query - DNA Sequence 79 */ 80 searchDNA(query) { 81 if (!Nucleotides.isNucleotideSequence(query)) return; 82 83 const activePlasmid = Session.activePlasmid(); 84 if (!activePlasmid) return; 85 86 const sequences = [activePlasmid.sequence, activePlasmid.complementarySequence]; 87 const queries = [query, query.split("").reverse().join("")]; 88 89 for (let i = 0; i < 2; i++) { 90 const query = queries[i] 91 const sequence = sequences[i]; 92 const strand = ["fwd", "rev"][i] 93 let indices = []; 94 let index = sequence.indexOf(query); 95 while (index !== -1) { 96 indices.push(index); 97 index = sequence.indexOf(query, index + 1); 98 }; 99 100 for (let j = 0; j < indices.length; j++) { 101 const span = [indices[j]+1, indices[j]+query.length]; 102 PlasmidViewer.highlightBases( 103 span, 104 "base-search", 105 strand, 106 ); 107 108 this.searchResults.push({strand: strand, span: span}); 109 }; 110 }; 111 112 this.findNearestResult(); 113 this.focusResult(); 114 this.updateBarInfo(); 115 }; 116 117 118 /** 119 * Search for AA sequence in plasmid 120 * 121 * @param {String} query - AA sequence 122 */ 123 searchAA(query) { 124 const activePlasmid = Session.activePlasmid(); 125 if (!activePlasmid) return; // No plasmid loaded 126 127 // Iterate over both strands 128 const sequences = [activePlasmid.sequence, Nucleotides.reverseComplementary(activePlasmid.sequence)]; 129 const strands = ["fwd", "rev"]; 130 for (let i = 0; i < 2; i++) { 131 const sequence = sequences[i]; 132 const strand = strands[i]; 133 134 // Get the 3 possible reading frames 135 const dnaFrames = [ 136 sequence, 137 sequence.slice(-1) + sequence.slice(0, -1), 138 sequence.slice(-2) + sequence.slice(0, -2) 139 ]; 140 141 // Iterate over reading frames 142 for (let j = 0; j < 3; j++) { 143 const dnaFrame = dnaFrames[j]; 144 145 // Translate reading frame 146 let aaFrame = ""; 147 for (let k = 0; k+3 <= dnaFrame.length; k += 3) { 148 aaFrame += Nucleotides.codonTable[dnaFrame.slice(k, k+3)] ?? "X"; 149 }; 150 console.log(j, aaFrame) 151 152 153 // Search for query and save its index 154 let indices = []; 155 let index = aaFrame.indexOf(query); 156 while (index !== -1) { 157 if (strand === "fwd") { 158 const aaIndex = index*3 + 1 - j; 159 indices.push(aaIndex); 160 } else { 161 const aaIndex = index*3 + 1 - j; 162 indices.push(dnaFrame.length - aaIndex) 163 } 164 index = aaFrame.indexOf(query, index + 1); 165 }; 166 167 // Highlight search results 168 for (let k = 0; k < indices.length; k++) { 169 const span = (strand === "fwd") 170 ? [indices[k], indices[k] + query.length*3 - 1] 171 : [indices[k] - query.length*3 + 2, indices[k] + 1] 172 173 console.log("Search.searchAA ->", query, span); 174 175 PlasmidViewer.highlightBases( 176 span, 177 "base-search", 178 strand, 179 ); 180 181 this.searchResults.push({strand: strand, span: span}); 182 }; 183 }; 184 }; 185 186 this.findNearestResult(); 187 this.focusResult(); 188 this.updateBarInfo(); 189 }; 190 191 192 searchSpecificAA(query) { 193 const activePlasmid = Session.activePlasmid(); 194 if (!activePlasmid) return; // No plasmid loaded 195 196 const aaCode = query[0].toUpperCase(); 197 const aaIndex = parseInt(query.slice(1)); 198 199 const matchingAABlocks = document.querySelectorAll(`g[aa-index="${aaIndex - 1}"][aa="${aaCode}"]`); 200 201 for (let i = 0; i < matchingAABlocks.length; i++) { 202 const currAABlock = matchingAABlocks[i]; 203 let aaSpan = currAABlock.getAttribute("aa-span").split(",").map(s => parseInt(s)); 204 205 206 let strand = (aaSpan[0] < aaSpan[1]) ? "fwd": "rev"; 207 aaSpan.sort() 208 209 PlasmidViewer.highlightBases( 210 aaSpan, 211 "base-search", 212 strand, 213 ); 214 215 this.searchResults.push({strand: strand, span: aaSpan}); 216 }; 217 218 this.findNearestResult(); 219 this.focusResult(); 220 this.updateBarInfo(); 221 }; 222 223 224 findNearestResult() { 225 const gridViewContainer = document.getElementById("grid-view-container"); 226 227 const bases = Array.from(gridViewContainer.getElementsByClassName("base-search")); 228 229 const containerRect = gridViewContainer.getBoundingClientRect(); 230 let firstBaseInView; 231 for (let i = 0; i < bases.length; i++) { 232 const base = bases[i] 233 const rect = base.getBoundingClientRect(); 234 if ( 235 rect.top >= containerRect.top && 236 rect.bottom <= containerRect.bottom 237 ) { 238 firstBaseInView = base; 239 }; 240 }; 241 242 const targetBase = (firstBaseInView) ? firstBaseInView: bases[0]; 243 if (!targetBase) return; 244 245 const targetBaseIndex = parseInt(targetBase.getAttribute("base-index")); 246 247 this.searchResults.sort((a, b) => a.span[0] - b.span[0]); 248 for (let i = 0; i < this.searchResults.length; i++) { 249 const resultSpan = this.searchResults[i].span; 250 if (resultSpan[0] <= targetBaseIndex <= resultSpan[1]) { 251 this.searchFocusIndex = i; 252 break; 253 }; 254 }; 255 }; 256 257 258 navigateResults(increment) { 259 PlasmidViewer.unhighlightBases("base-search-focus"); 260 if (this.searchFocusIndex + increment < 0) { 261 this.searchFocusIndex = this.searchResults.length - 1; 262 } else if (this.searchFocusIndex + increment >= this.searchResults.length) { 263 this.searchFocusIndex = 0; 264 } else { 265 this.searchFocusIndex += increment; 266 }; 267 this.focusResult(); 268 this.updateBarInfo(); 269 }; 270 271 272 focusResult() { 273 const searchResult = this.searchResults[this.searchFocusIndex]; 274 if (!searchResult) return; 275 276 PlasmidViewer.highlightBases(searchResult.span, "base-search-focus", searchResult.strand); 277 278 const container = document.getElementById("grid-view"); 279 const containerRect = container.getBoundingClientRect(); 280 const basesInSearchResult = Array.from(container.querySelectorAll(".base-search-focus")).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top); 281 282 283 const basesHeightDifference = basesInSearchResult[basesInSearchResult.length - 1].getBoundingClientRect().bottom 284 - basesInSearchResult[0].getBoundingClientRect().top 285 286 const targetBase = searchResult.strand === "fwd" 287 ? basesInSearchResult[0] 288 : basesInSearchResult.at(-1); 289 290 const targetSVG = targetBase.closest("svg"); 291 const targetSVGRect = targetSVG.getBoundingClientRect(); 292 const targetTop = targetSVGRect.top - containerRect.top + container.scrollTop; 293 const targetBottom = targetSVGRect.bottom - containerRect.top + container.scrollTop; 294 const halfContainer = containerRect.height / 2; 295 const halfBases = basesHeightDifference / 2; 296 297 let targetHeight; 298 299 if (basesHeightDifference > containerRect.height) { 300 targetHeight = searchResult.strand === "fwd" 301 ? targetTop 302 : targetBottom - containerRect.height; 303 } else { 304 targetHeight = searchResult.strand === "fwd" 305 ? targetTop - halfContainer + halfBases 306 : targetBottom - halfContainer - halfBases; 307 }; 308 309 document.getElementById("grid-view").scrollTo({ top: targetHeight, behavior: "smooth" }); 310 }; 311 312 313 updateBarInfo() { 314 const infoSpan = document.getElementById("search-bar-info"); 315 316 if (this.searchResults.length === 0) { 317 infoSpan.innerText = ""; 318 } else { 319 infoSpan.innerText = (this.searchFocusIndex+1) + " / " + this.searchResults.length; 320 }; 321 }; 322 323 324 searchAACheckboxCallback(event) { 325 const isChecked = event.target.checked; 326 327 Utilities.addInputValidator( 328 document.getElementById("search-bar"), 329 (isChecked) ? "aaOrSpecific": "dna", 330 ); 331 332 let inputEvent = new Event('input', { bubbles: true, }); 333 document.getElementById("search-bar").dispatchEvent(inputEvent); 334 335 this.search(); 336 }; 337};
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.