1const Utilities = new class { 2 constructor () { 3 this.updateTheme(); 4 5 this.defaultAnnotationColors = [ 6 "#ff7a8e", "#bc99ee", "#ff8756", "#5aa8d9", "#f45ba8", "#74e374", "#ffcc5c", 7 "#7ec8e3", "#ffa07a", "#c3a6ff", "#ff6699", "#80ffdb", "#ffb3e6", "#a5f2e3", 8 "#ff9a8a", "#95b8d1", "#ffad5a", "#91d18b", "#ff6b6b", "#c4e17f", "#a29bfe", 9 "#fdcb6e", "#6c5ce7", "#00cec9", "#fab1a0", "#ff7675", "#55efc4", "#e17055", 10 "#fd79a8", "#81ecec", "#ff9ff3", "#a8e6cf" 11 ]; 12 13 14 document.addEventListener('DOMContentLoaded', function() { 15 Utilities.getScrollbarWidth(); 16 }); 17 18 this.validators = { 19 integer: (value) => /^-?\d+$/.test(value), 20 float: (value) => /^-?\d*\.?\d*$/.test(value), 21 dna: (value) => /^[ATCG]*$/i.test(value), 22 iupacBases: (value) => /^[ACGTURYSWKMBDHVN.-]*$/i.test(value), 23 aa: (value) => /^[ACDEFGHIKLMNPQRSTVWY*X-]*$/i.test(value), 24 aaOrSpecific: (value) => 25 /^[ACDEFGHIKLMNPQRSTVWY*X-]*$/i.test(value) || 26 /^[ACDEFGHIKLMNPQRSTVWY][0-9]+$/i.test(value) 27 }; 28 document.addEventListener("DOMContentLoaded", function (event) { 29 const inputElements = document.querySelectorAll("input[validator]"); 30 31 inputElements.forEach((input) => { 32 const validatorType = input.getAttribute("validator"); 33 34 Utilities.addInputValidator(input, validatorType); 35 }); 36 }); 37 }; 38 39 /** 40 * Generates a random UUID. 41 * 42 * @returns - UUID string 43 */ 44 newUUID() { 45 const uuidSegments = []; 46 47 for (let i = 0; i < 36; i++) { 48 if (i === 8 || i === 13 || i === 18 || i === 23) { 49 uuidSegments[i] = '-'; 50 } else if (i === 14) { 51 uuidSegments[i] = '4'; // The version 4 UUID identifier 52 } else if (i === 19) { 53 // The first character of this segment should be 8, 9, A, or B 54 uuidSegments[i] = (Math.random() * 4 + 8 | 0).toString(16); 55 } else { 56 // Generate a random hex digit 57 uuidSegments[i] = (Math.random() * 16 | 0).toString(16); 58 }; 59 }; 60 61 // Combine the segments into a single string 62 return uuidSegments.join(''); 63 }; 64 65 66 /** 67 * Gives a random color from the default colors, 68 * avoiding using the same color twice. 69 * 70 * @param {String} recentColor - Hex color that was just used. 71 * @returns {String} 72 */ 73 getRandomDefaultColor(recentColor="") { 74 const remainingColors = this.defaultAnnotationColors.filter(color => color !== recentColor); 75 const randomIndex = Math.floor(Math.random() * remainingColors.length); 76 77 return remainingColors[randomIndex]; 78 }; 79 80 81 /** 82 * Decide if text should be black or white based on 83 * background color luminance. 84 * 85 * @param {String} bgColor - Hex color of background. 86 * @returns 87 */ 88 getTextColorBasedOnBg(bgColor) { 89 if (!bgColor) return "black"; 90 // Remove the '#' if present 91 const hex = bgColor.replace('#', ''); 92 93 // Parse the HEX color into RGB components 94 const r = parseInt(hex.substring(0, 2), 16); 95 const g = parseInt(hex.substring(2, 4), 16); 96 const b = parseInt(hex.substring(4, 6), 16); 97 98 // Calculate relative luminance 99 const luminance = (r / 255) * 0.2126 + (g / 255) * 0.7152 + (b / 255) * 0.0722; 100 101 // Return white for dark backgrounds and black for light backgrounds 102 return luminance > 0.5 ? 'black' : 'white'; 103 }; 104 105 /** 106 * Sort the features dict by span so that the features appear 107 * in order in the sidebar. 108 * 109 * @param {Object} inputDict - Dictionary to be sorted. 110 * @returns {Object} - Sorted dictionary. 111 */ 112 sortFeaturesDictBySpan(inputDict) { 113 // Convert the dictionary to an array of key-value pairs 114 const valueKey = "span"; 115 const features = Object.entries(inputDict); 116 117 // Sort the array based on the first number in the value key 118 features.sort((a, b) => { 119 const rangeStartA = a[1][valueKey][0]; 120 const rangeStartB = b[1][valueKey][0]; 121 return rangeStartA - rangeStartB; 122 }); 123 124 // Convert the sorted array back to a dictionary and return 125 return Object.fromEntries(features); 126 }; 127 128 129 deepClone(obj) { 130 if (obj === null || typeof obj !== "object") return obj; 131 let copy = Array.isArray(obj) ? [] : {}; 132 for (let key in obj) { 133 if (obj.hasOwnProperty(key)) { 134 copy[key] = this.deepClone(obj[key]); 135 }; 136 }; 137 return copy; 138 }; 139 140 141 /** 142 * Find width of the scrollbar by generating a temporary div. 143 * 144 * @returns {Number} - Width of scrollbar. 145 */ 146 getScrollbarWidth() { 147 if (this._scrollbarWidth !== undefined) return this._scrollbarWidth; // Cache result 148 149 const tempDiv = document.createElement('div'); 150 Object.assign(tempDiv.style, { 151 position: 'absolute', 152 top: '-9999px', 153 width: '100px', 154 height: '100px', 155 overflow: 'scroll' 156 }); 157 158 document.body.appendChild(tempDiv); 159 this._scrollbarWidth = tempDiv.offsetWidth - tempDiv.clientWidth; 160 document.body.removeChild(tempDiv); 161 162 return this._scrollbarWidth; 163 }; 164 165 166 /** 167 * Count occurrences of a substring in an input string. 168 * 169 * @param {String} subString - String to be searched for 170 * @param {String} inputString - String to search inside for substring 171 * @returns {Number} - Number of occurences 172 */ 173 countSubstringOccurences(subString, inputString) { 174 let count = 0; 175 let currentIndex = inputString.indexOf(subString); 176 while (currentIndex !== -1) { 177 count++; 178 currentIndex = inputString.indexOf(subString, currentIndex + 1); 179 }; 180 return count; 181 }; 182 183 184 /** 185 * Modified slice() function that allows for negative indices or indices longer than the string length by assuming 186 * the string loops. 187 * 188 * Example: 189 * startIndex endIndex 190 * â¼ â¼ 191 * -3 -2 -1 0 1 2 3 4 5 6 7 8 9 192 * str -> _ _ _ A B C D E F G _ _ _ 193 * 194 * Result -> FGABCDEFGA 195 * 196 * @param {string} str - String to be sliced 197 * @param {number} startIndex - Start index 198 * @param {number} endIndex - End index 199 * @returns {string} 200 */ 201 repeatingSlice(str, startIndex, endIndex) { 202 const repeatedStr = str.repeat(3); // Copy the string 3 times: ABC_ABC_ABC 203 // Remap indices to new string then return 204 return repeatedStr.slice(startIndex + str.length, endIndex + str.length); 205 }; 206 207 208 /** 209 * Get user browser and version 210 */ 211 getBrowserInfo() { 212 const userAgent = navigator.userAgent; 213 let browserName = "Unknown"; 214 let browserVersion = "Unknown"; 215 216 if (/Edg\/(\d+\.\d+)/.test(userAgent)) { 217 // Edge (Chromium-based) 218 browserName = "Edge"; 219 browserVersion = userAgent.match(/Edg\/(\d+\.\d+)/)[1]; 220 } else if (/Chrome\/(\d+\.\d+)/.test(userAgent) && !/Edg|OPR|Brave/.test(userAgent)) { 221 // Chrome (not Edge or Opera or Brave) 222 browserName = "Chrome"; 223 browserVersion = userAgent.match(/Chrome\/(\d+\.\d+)/)[1]; 224 } else if (/Firefox\/(\d+\.\d+)/.test(userAgent)) { 225 // Firefox 226 browserName = "Firefox"; 227 browserVersion = userAgent.match(/Firefox\/(\d+\.\d+)/)[1]; 228 } else if (/Safari\/(\d+\.\d+)/.test(userAgent) && !/Chrome|Edg|OPR|Brave/.test(userAgent)) { 229 // Safari (not Chrome, Edge, Opera, or Brave) 230 browserName = "Safari"; 231 browserVersion = userAgent.match(/Version\/(\d+\.\d+)/)[1]; 232 } else if (/OPR\/(\d+\.\d+)/.test(userAgent)) { 233 // Opera 234 browserName = "Opera"; 235 browserVersion = userAgent.match(/OPR\/(\d+\.\d+)/)[1]; 236 } else if (/MSIE (\d+\.\d+)|rv:(\d+\.\d+)/.test(userAgent)) { 237 // Internet Explorer 238 browserName = "Internet Explorer"; 239 browserVersion = userAgent.match(/MSIE (\d+\.\d+)|rv:(\d+\.\d+)/)[1]; 240 }; 241 242 return { browserName, browserVersion }; 243 }; 244 245 246 /** 247 * 248 * @param {*} string 249 * @param {*} html 250 */ 251 copyToClipboard(string, html="") { 252 if (this.getBrowserInfo().browserName !== "Safari") { 253 function dummyCopyListener(event) { 254 event.clipboardData.setData("text/html", (html === "") ? string: html); 255 event.clipboardData.setData("text/plain", string); 256 event.preventDefault(); 257 }; 258 259 document.addEventListener("copy", dummyCopyListener); 260 document.execCommand("copy"); 261 document.removeEventListener("copy", dummyCopyListener); 262 } else { 263 const el = document.createElement('textarea'); 264 el.value = string; 265 document.body.appendChild(el); 266 el.select(); 267 document.execCommand('copy'); 268 document.body.removeChild(el); 269 }; 270 }; 271 272 273 /** 274 * 275 * @param {*} mode 276 * @returns 277 */ 278 copySequence(mode="") { 279 const activePlasmid = Session.activePlasmid() 280 const selectionIndices = activePlasmid.getSelectionIndices() 281 if ( 282 !selectionIndices || 283 selectionIndices === null || 284 selectionIndices.filter(i => i !== null).length !== 2 285 ) return; 286 287 let selection = activePlasmid.sequence.slice(selectionIndices[
2870] - 1, selectionIndices[1]); 288 289 switch (mode) { 290 case "reverse": 291 selection = selection.split("").reverse().join(""); 292 break; 293 case "complement": 294 selection = Nucleotides.complementary(selection); 295 break; 296 case "reverse complement": 297 selection = Nucleotides.reverseComplementary(selection); 298 break; 299 }; 300 301 console.log("Utilities.copySequence ->", selection, mode); 302 this.copyToClipboard(selection); 303 Alerts.showAlert( 304 "Copied sequence to clipboard", 305 `Sequence: "${selection.length > 12 ? selection.slice(0, 6) + "..." + selection.slice(-6) : selection}" (${selection.length} bp).`, 306 3, 307 "green", 308 ); 309 }; 310 311 312 /** 313 * 314 * @param {*} mode 315 * @returns 316 */ 317 copyAASequence(mode="") { 318 const activePlasmid = Session.activePlasmid() 319 const selectionIndices = activePlasmid.getSelectionIndices() 320 if ( 321 !selectionIndices || 322 selectionIndices === null || 323 selectionIndices.filter(i => i !== null).length !== 2 324 ) return; 325 326 let dnaSeq = activePlasmid.sequence.slice(selectionIndices[0] - 1, selectionIndices[1]); 327 328 if (mode === "reverse") dnaSeq = Nucleotides.reverseComplementary(dnaSeq); 329 330 const aaSeq = Nucleotides.translate(dnaSeq) 331 332 console.log("Utilities.copyAASequence ->", aaSeq, mode); 333 Alerts.showAlert( 334 "Copied AA sequence to clipboard", 335 `Sequence: "${aaSeq.length > 12 ? aaSeq.slice(0, 6) + "..." + aaSeq.slice(-6) : aaSeq}" (${aaSeq.length} AA).`, 336 3, 337 "green", 338 ) 339 this.copyToClipboard(aaSeq); 340 }; 341 342 343 /** 344 * Remove traditionally made selections 345 */ 346 removeUserSelection() { 347 if (window.getSelection) { 348 window.getSelection().removeAllRanges(); 349 } else if (document.selection) { // IE 350 document.selection.empty(); 351 }; 352 }; 353 354 355 356 /** 357 * Update current theme. 358 */ 359 updateTheme() { 360 document.querySelector("html").setAttribute("data-theme", UserPreferences.get("theme")); 361 if (Coloris) { 362 Coloris({ 363 themeMode: UserPreferences.get("theme"), 364 alpha: false 365 }); 366 }; 367 }; 368 369 370 addInputValidator(input, validatorType) { 371 function validate() { 372 const isValid = Utilities.validators[validatorType] ? Utilities.validators[validatorType](input.value.replace(/\s+/g, '')) : true; 373 374 if (!isValid) { 375 input.setAttribute("incorrect", "true") 376 } else { 377 input.removeAttribute("incorrect") 378 }; 379 }; 380 381 if (input.currentValidator) { 382 input.removeEventListener("input", input.currentValidator); 383 }; 384 385 input.currentValidator = validate; 386 input.addEventListener("input", validate); 387 }; 388 389 390 findCSSRuleByClassName(className) { 391 const targetSelector = `.${className}`; 392 393 for (let sheet of document.styleSheets) { 394 // Skip cross-site style sheets 395 if (sheet.href && !sheet.href.startsWith(window.location.origin)) continue; 396 397 const rules = sheet.cssRules; 398 for (let rule of rules) { 399 if (rule.selectorText === targetSelector) return rule; 400 }; 401 }; 402 403 return null; 404 }; 405 406 407 /** 408 * Checks if the app is currently running in Tauri app 409 * 410 * @returns 411 */ 412 isTauriApp() { 413 return !!window.__TAURI__; 414 }; 415}; 416 417 418/** 419 * Configure Coloris color picker 420 */ 421Coloris({ 422 el: '.coloris', 423 wrap: true, 424 theme: 'polaroid', 425 swatches: Utilities.defaultAnnotationColors, 426 closeButton: true, 427 closeLabel: 'Save', 428});
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.