PageSourceSearch

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

js ivaprime.com collected 2026-10-03 21:38:04 UTC 12,813 bytes, 372 lines download raw bytes

1const Toolbar = new class {
2    constructor() {
3        this.equationImages = {
4            "TmAlgorithm": {
5                "oligoCalc": "/static/assets/equations/oligoCalc_equation.png",
6                "nnSantaLucia": "/static/assets/equations/nnSantaLucia_equation.png"
7            },
8            "saltCorr": {
9                "SchildkrautLifson": "/static/assets/equations/SchildkrautLifson_equation.png",
10                "Owczarzy": "/static/assets/equations/Owczarzy_equation.png"
11            },
12        };
13
14        document.addEventListener("DOMContentLoaded", function() {
15            Toolbar.populateSettings();
16
17            Toolbar.updateEquationImage("TmAlgorithm", "meltingTempAlgorithmEquationImage");
18            Toolbar.updateEquationImage("saltCorr", "saltCorrectionEquationImage");
19
20            const calcinput1 = document.getElementById("tm-calc-input1");
21            calcinput1.addEventListener("input", function () {
22                Toolbar.calculatePrimerTm(this.value, 1)
23            });
24            const calcinput2 = document.getElementById("tm-calc-input2");
25            calcinput2.addEventListener("input", function () {
26                Toolbar.calculatePrimerTm(this.value, 2)
27            });
28        });
29
30        document.addEventListener("click", function (event) {
31            if (!event.target.closest(".toolbar-panel") && !event.target.closest(".toolbar")) {
32                Toolbar.hideAllPanels();
33            };
34        });
35    };
36
37    updateEquationImage(selectId, imageId) {
38
39        const selectElement = document.getElementById(selectId);
40        const imageElement = document.getElementById(imageId);
41
42        if (selectElement && imageElement) {
43            selectElement.addEventListener("change", function () {
44                const selectedValue = this.value;
45                if (Toolbar.equationImages[selectId] && Toolbar.equationImages[selectId][selectedValue]) {
46                    imageElement.src = Toolbar.equationImages[selectId][selectedValue];
47                }
48            });
49
50            // Initial update to set the correct image on page load
51            const selectedValue = selectElement.value;
52            if (Toolbar.equationImages[selectId][selectedValue]) {
53                imageElement.src = Toolbar.equationImages[selectId][selectedValue];
54            };
55        };
56    };
57
58
59    togglePanel(id, button, alignBottomRight=false) {
60        const panel = document.getElementById(id);
61        if (!panel) return;
62
63        if (!panel.hasAttribute("visible")) {
64            this.hideAllPanels();
65            this.showPanel(panel, button, alignBottomRight)
66        } else {
67            this.hidePanel(panel);
68        };
69    };
70
71
72    showPanel(panel, button, alignBottomRight) {
73        this.positionPanel(panel, button, alignBottomRight);
74        panel.setAttribute("visible", "");
75    };
76
77
78    positionPanel(panel, button, alignBottomRight) {
79        const buttonRect = button.getBoundingClientRect();
80        const toolbar = document.querySelector(".toolbar");
81        const toolbarRect = toolbar.getBoundingClientRect();
82        const footer = document.querySelector(".footer");
83        const footerRect = footer.getBoundingClientRect();
84
85        const panelHeight = panel.offsetHeight;
86        const viewportHeight = window.innerHeight;
87
88        let x = toolbarRect.left + 2;
89        let y = buttonRect.top + window.scrollY
90
91        if (alignBottomRight) {
92            y = buttonRect.bottom - window.scrollY - panelHeight;
93            y = viewportHeight - buttonRect.bottom
94
95            panel.style.bottom = `${y}px`;
96            panel.style.left = `${x}px`;
97            panel.style.top = `auto`;
98        } else {
99            if (y + panelHeight > viewportHeight) {
100                y = footerRect.top + 2 - panelHeight;
101            };
102
103            panel.style.top = `${y}px`;
104            panel.style.left = `${x}px`;
105        };
106
107    };
108
109
110    hidePanel(panel) {
111        panel.removeAttribute("visible");
112    };
113
114
115    hideAllPanels() {
116        const panels = document.querySelectorAll(".toolbar-panel");
117        for(let i = 0; i < panels.length; i++) {
118            this.hidePanel(panels[i])
119        };
120    };
121
122
123    toggleAdvancedSettings(header) {
124        header.toggleAttribute("collapsed");
125        header.nextElementSibling.toggleAttribute("collapsed");
126    };
127
128
129    applyToAllInputs(action) {
130        const settingsPanel = document.getElementById("toolbar-panel-settings");
131        const inputs = settingsPanel.querySelectorAll("input, select");
132
133        let preferences = {};
134
135        inputs.forEach((element) => {
136            const prefName = element.id || element.name;
137            if (!prefName) return;
138
139            const value = action(element, prefName);
140            if (value !== undefined) {
141                preferences[prefName] = value;
142            };
143        });
144
145        return preferences;
146    }
147
148
149    populateSettings() {
150        const preferences = this.applyToAllInputs((element, prefName) => {
151            const storedValue = UserPreferences.get(prefName);
152            if (storedValue !== undefined) {
153                element.type === "checkbox"
154                    ? element.checked = storedValue
155                    : element.value = storedValue;
156            };
157        });
158
159        this.attachValidationListeners();
160
161        Utilities.updateTheme();
162    };
163
164
165    updateSettings() {
166        Toolbar.hideAllPanels();
167
168        const preferences = this.applyToAllInputs((element) => {
169            return element.type === "checkbox"
170                ? element.checked
171                : element.value;
172        });
173
174        UserPreferences.setBulk(preferences);
175
176        Utilities.updateTheme();
177    };
178
179
180    resetToDefaultSettings() {
181        const preferences = this.applyToAllInputs((element, prefName) => {
182            const defaultValue = UserPreferences.defaultSettings[prefName];
183            if (defaultValue !== undefined) {
184                element.type === "checkbox"
185                    ? element.checked = defaultValue
186                    : element.value = defaultValue;
187                return defaultValue;
188            };
189        });
190
191        UserPreferences.setBulk(preferences);
192
193        Utilities.updateTheme();
194
195        let inputEvent = new Event('input', { bubbles: false, });
196        document.getElementById("toolbar-panel-settings").querySelectorAll("input[validator]").forEach(function (input) {
197            input.dispatchEvent(inputEvent);
198        });
199    };
200
201
202    attachValidationListeners() {
203        const settingsPanel = document.getElementById("toolbar-panel-settings");
204        const inputs = settingsPanel.querySelectorAll("input, select");
205
206        inputs.forEach((element) => {
207            element.addEventListener("input", this.checkValidationState);
208        });
209
210        this.checkValidationState();
211    };
212
213    checkValidationState() {
214        const settingsPanel = document.getElementById("toolbar-panel-settings");
215        const updateButton = document.getElementById("update-settings-button");
216
217
218        const inputFailedValidator = settingsPanel.querySelector("[incorrect]") !== null;
219
220        if (inputFailedValidator) {
221            updateButton.setAttribute("disabled", "");
222        } else {
223            updateButton.removeAttribute("disabled");
224        };
225    };
226
227
228    calculatePrimerTm(seq, index) {
229        const tmSpan = document.getElementById(`tm-calc-tm${index}`);
230        const infoSpan = document.getElementById(`tm-calc-info${index}`);
231        
232        if (seq.length > 0 && Nucleotides.isNucleotideSequence(seq) && /^[ATCG]*$/i.test(seq)) {
233            tmSpan.textContent = Nucleotides.getMeltingTemperature(seq).toFixed(2);
234            infoSpan.textContent = `(${UserPreferences.get("TmAlgorithm")}, ${UserPreferences.get("primerConc")} nM)`;
235
236        } else {
237            tmSpan.textContent = "--";
238            infoSpan.textContent = "--";
239
240        };
241
242        this.calculatePrimerOverlap();
243    };
244
245
246    updateUndoRedoButtonsStates() {
247        const activePlasmid = Session.activePlasmid();
248        if (!activePlasmid) return;
249
250        const stateIndex = activePlasmid.stateIndex;
251        const stateHistoryLength = activePlasmid.stateHistory.length;
252
253        const undoButton = document.getElementById("undo-button");
254        const redoButton = document.getElementById("redo-button");
255
256        console.log("Toolbar.updateUndoRedoButtonsStates ->", stateIndex, stateHistoryLength);
257
258        if (stateIndex >= stateHistoryLength - 1) {
259            // We're on the earliest state, disalbe redo button
260            undoButton.setAttribute("disabled", "");
261
262            undoButton.setAttribute(
263                "title",
264                `Undo (Ctrl + Z)`,
265            );
266        } else {
267            undoButton.removeAttribute("disabled");
268
269            undoButton.setAttribute(
270                "title",
271                `Undo: ${activePlasmid.stateHistory[stateIndex].actionDescription} (Ctrl + Z)`,
272            );
273        };
274
275        if (stateIndex <= 0) {
276            // We're on latest state, disable redo button
277            redoButton.setAttribute("disabled", "");
278
279            redoButton.setAttribute(
280                "title",
281                `Redo (Ctrl + Shift + Z)`,
282            );
283        } else {
284            redoButton.removeAttribute("disabled");
285
286            redoButton.setAttribute(
287                "title",
288                `Redo: ${activePlasmid.stateHistory[stateIndex - 1].actionDescription} (Ctrl + Shift + Z)`,
289            );
290        };
291    };
292
293
294    calculatePrimerOverlap(seq1 = null, seq2 = null) {
295        const primer1 = (seq1) ? seq1 : document.getElementById("tm-calc-input1").value;
296        if (!/^[ATCG]*$/i.test(primer1) || primer1.trim().length === 0) return;
297
298        const primer2 = (seq2) ? seq2 : document.getElementById("tm-calc-input2").value;
299        if (!/^[ATCG]*$/i.test(primer2) || primer2.trim().length === 0) return;
300
301        const primer2RC = Nucleotides.reverseComplementary(primer2);
302
303        console.log(primer1, primer2RC)
304        
305        const minLength = Math.min(primer1.length, primer2RC.length);
306        let overlap = 0;
307        for (let i = minLength; i > 0; i--) {
308            if (primer2RC.slice(-i) === primer1.slice(0, i)) {
309                overlap = i;
310                break;
311            };
312        };
313        const primer2Rev = primer2.split("").reverse().join("")
314
315        const overlappingSequence = primer1.slice(0, overlap);
316
317        const svgWrapper = document.getElementById("tm-overlap-svg-wrapper");
318        if (svgWrapper.firstElementChild) {
319            svgWrapper.removeChild(svgWrapper.firstElementChild);
320        };
321
322        const overlapTmSpan = document.getElementById("tm-calc-overlap-tm");
323        const overlapLengthSpan = document.getElementById("tm-calc-overlap-length");
324        const overlapInfoSpan = document.getElementById("tm-calc-overlap-info");
325        if (overlap === 0) {
326            overlapTmSpan.textContent = "--";
327            overlapLengthSpan.textContent = "";
328            overlapInfoSpan.textContent = "--";
329        } else {
330            overlapTmSpan.textContent = Nucleotides.getMeltingTemperature(overlappingSequence).toFixed(2);
331            overlapLengthSpan.textContent = `(${overlap} bp)`
332            overlapInfoSpan.textContent = `(${UserPreferences.get("TmAlgorithm")}, ${UserPreferences.get("primerConc")} nM)`;
333        };
334        
335
336        const maxLength = 40;
337        const primer1Truncated = primer1.slice(0, Math.min(maxLength, primer1.length));
338        const primer2Truncated = primer1.slice(-Math.min(maxLength, primer2.length));
339
340        const maxWidth = primer2Truncated.length + primer1Truncated.length - overlap;
341        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
342        svgWrapper.appendChild(svg);
343        svg.id = "toolbar-panel-tm-overlap-svg";
344        svg.classList.add("toolbar-panel-tm-overlap-svg");
345        
346        
347        const scaleFactor = 14; // Font size in px
348        svg.setAttribute("viewBox", `0 0 ${maxWidth*scaleFactor} ${2*scaleFactor}`);
349
350        const primer1Offset = primer2Truncated.length - overlap;
351        for (let i = 0; i < primer1.length; i++) {
352            svg.appendChild(PlasmidViewer._text(
353                [(i + 0.5 + primer1Offset)*scaleFactor, 0*scaleFactor],
354                primer1.slice(i, i+1),
355                null,
356                "toolbar-panel-tm-overlap-text",
357                "middle",
358                "1em"
359            ))
360        };
361        for (let i = 0; i < primer2Rev.length; i++) {
362            svg.appendChild(PlasmidViewer._text(
363                [(i + 0.5)*scaleFactor, 1*scaleFactor],
364                primer2Rev.slice(i, i+1),
365                null,
366                "toolbar-panel-tm-overlap-text",
367                "middle",
368                "1em"
369            ))
370        };
371    };
372};

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.