PageSourceSearch

https://ivaprime.com/static/js/utilities.js

js ivaprime.com collected 2026-10-03 21:37:54 UTC 13,912 bytes, 428 lines download raw bytes

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.