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.