1const Sidebar = new class { 2 constructor() { 3 this.primerRegionsClasses = { 4 "INS": "primer-sequence-ins", 5 "HR": "primer-sequence-hr", 6 "TBR": "primer-sequence-tbr", 7 "subHR": "primer-sequence-subhr", 8 "subTBR": "primer-sequence-subtbr", 9 }; 10 this.baseClasses = { 11 "INS": "base-primer-ins", 12 "HR": "base-primer-hr", 13 "TBR": "base-primer-tbr", 14 "subHR": "base-primer-subhr", 15 "subTBR": "base-primer-subtbr", 16 }; 17 18 19 /** 20 * Sidebar resizing 21 */ 22 this.isResizing = false; 23 24 document.addEventListener("DOMContentLoaded", function () { 25 document.getElementById("sidebar-resizer").addEventListener("mousedown", function () { 26 console.log(`Sidebar.resizeSidebar -> started`); 27 Sidebar.isResizing = true; 28 document.getElementById("viewer").style.display = "none"; 29 document.addEventListener("mousemove", Sidebar.resizeSidebar); 30 document.addEventListener("mouseup", Sidebar.stopResizeSidebar); 31 }); 32 }); 33 }; 34 35 resizeSidebar(event) { 36 if (!Sidebar.isResizing) return; 37 38 let minWidth = window.innerWidth * 0.15; 39 let maxWidth = window.innerWidth * 0.5; 40 41 let newWidth = event.clientX; 42 if (newWidth < minWidth) newWidth = minWidth; 43 if (newWidth > maxWidth) newWidth = maxWidth; 44 45 document.documentElement.style.setProperty('--sidebar-width', `${newWidth}px`); 46 }; 47 48 stopResizeSidebar() { 49 Sidebar.isResizing = false; 50 document.getElementById("viewer").style.display = "block"; 51 PlasmidViewer.redraw(); 52 Utilities.removeUserSelection(); 53 54 document.removeEventListener("mousemove", Sidebar.resizeSidebar); 55 document.removeEventListener("mouseup", Sidebar.stopResizeSidebar); 56 console.log(`Sidebar.resizeSidebar -> done`); 57 }; 58 59 60 /** 61 * Update sidebar. 62 */ 63 update() { 64 this.updatePrimersTable(); 65 this.updateFeaturesTable(); 66 }; 67 68
69 generatePrimersTable(plasmidIndex=null) { 70 const primerSets = (plasmidIndex) ? Session.getPlasmid(plasmidIndex).primers: Session.activePlasmid().primers; 71 //console.log(`Sidebar.updatePrimersTable -> primers=\n${JSON.stringify(primersSets, null, 2)}`); 72 73 return this.createPrimersTableContainer(primerSets); 74 }; 75 76 77 createPrimersTableContainer(primerSets) { 78 const primersTable = document.createElement("DIV"); 79 primersTable.id = "primers-table"; 80 primersTable.classList.add("primers-table"); 81 82 for (let i = 0; i < primerSets.length; i++) { 83 const primerSet = primerSets[i]; 84 85 primersTable.appendChild( 86 this._createPrimerSetContainer( 87 i, 88 primerSet, 89 ) 90 ) 91 }; 92 93 return primersTable; 94 }; 95 96 97 _createPrimerSetContainer(primerSetIndex, primerSet) { 98 const primerSetContainer = document.createElement("DIV"); 99 primerSetContainer.classList.add("primers-set"); 100 101 /** 102 * Header 103 */ 104 const primerSetHeader = document.createElement("DIV"); 105 primerSetHeader.classList.add("primers-set-header"); 106 primerSetContainer.appendChild(primerSetHeader); 107 108 const primerSetHeaderTitle = document.createElement("span"); 109 primerSetHeaderTitle.classList.add("primers-set-header-title"); 110 primerSetHeaderTitle.innerText = primerSet.title; 111 primerSetHeader.appendChild(primerSetHeaderTitle); 112 113 const primerSetHeaderRenameButton = document.createElement("div"); 114 primerSetHeaderRenameButton.classList.add( 115 "toolbar-button", 116 "footer-button", 117 "primers-set-header-rename-button", 118 ); 119 primerSetHeaderRenameButton.title = "Rename primers"; 120 primerSetHeaderRenameButton.appendChild(document.createElement("span")) 121 primerSetHeader.appendChild(primerSetHeaderRenameButton); 122 123 primerSetHeaderRenameButton.onclick = function() { 124 Modals.createRenamePrimersModal(primerSetIndex); 125 }; 126 127 128 /** 129 * Body 130 */ 131 const primerSetBody = document.createElement("DIV"); 132 primerSetBody.classList.add("primers-set-body"); 133 primerSetContainer.appendChild(primerSetBody); 134 135 /** Primer set HR info */ 136 const hrInfo = document.createElement("DIV"); 137 hrInfo.classList.add("primers-set-hr-info"); 138 //hrInfo.innerHTML = "<span>HR Info</span>"; 139 primerSetBody.appendChild(hrInfo); 140 141 const primersContainer = document.createElement("DIV"); 142 primersContainer.classList.add("primers-container"); 143 primerSetBody.appendChild(primersContainer); 144 145 primerSet.primers.forEach((primer, primerIndex) => { 146 primersContainer.appendChild( 147 this._createPrimerContainer( 148 primerSetIndex, 149 primerIndex, 150 primer, 151 ) 152 ); 153 }); 154 155 return primerSetContainer; 156 }; 157 158 159 _createPrimerContainer(primerSetIndex, primerIndex, primer) { 160 const primerContainer = document.createElement("DIV"); 161 primerContainer.classList.add("primer-container"); 162 163 /** Primer header */ 164 const primerHeader = document.createElement("DIV"); 165 primerHeader.classList.add("primer-header"); 166 primerContainer.appendChild(primerHeader); 167 168 const primerTitle = document.createElement("DIV"); 169 primerTitle.classList.add("primer-title"); 170 primerTitle.innerText = primer.label; 171 primerHeader.appendChild(primerTitle); 172 173 const primerHeaderButtonsContainer = document.createElement("DIV"); 174 primerHeaderButtonsContainer.classList.add("primer-header-buttons-container"); 175 primerHeader.appendChild(primerHeaderButtonsContainer); 176 177 178 /** Copy primer button */ 179 const copyPrimerButton = document.createElement("span"); 180 copyPrimerButton.classList.add("primer-copy-button", "toolbar-button", "primer-header-button"), 181 copyPrimerButton.title = "Copy sequence to clipboard"; 182 copyPrimerButton.appendChild(document.createElement("span")); 183 primerHeaderButtonsContainer.appendChild(copyPrimerButton); 184 185 copyPrimerButton.onclick = function() { 186 const primerRegionSpans = primerSequence.querySelectorAll("span"); 187 const combinedSequence = Array.from(primerRegionSpans).map(span => span.textContent).join(""); 188 Utilities.copyToClipboard(combinedSequence); 189 }; 190 191 192 const highlightPrimerButton = document.createElement("DIV"); 193 highlightPrimerButton.classList.add("primer-highlight-button","toolbar-button", "primer-header-button"); 194 highlightPrimerButton.title = "Highlight primer sequence"; 195 const primerDict = Session.activePlasmid().primers[primerSetIndex].primers[primerIndex]; 196 if ("isHighlighted" in primerDict && primerDict["isHighlighted"] === true) { 197 highlightPrimerButton.setAttribute("active", "") 198 }; 199 highlightPrimerButton.appendChild(document.createElement("span")); 200 primerHeaderButtonsContainer.appendChild(highlightPrimerButton); 201 202 highlightPrimerButton.onclick = function(event) { 203 if (!highlightPrimerButton.hasAttribute("active")) { 204 highlightPrimerButton.setAttribute("active", ""); 205 Session.activePlasmid().primers[primerSetIndex].primers[primerIndex]["isHighlighted"] = true; 206 Sidebar.highlightPrimerBindingSite(event.target); 207 } else { 208 highlightPrimerButton.removeAttribute("active"); 209 Session.activePlasmid().primers[primerSetIndex].primers[primerIndex]["isHighlighted"] = false;
210 Sidebar.unhighlightPrimerBindingSite(event.target); 211 //Sidebar.unhighlightPrimerBindingSites(); 212 }; 213 }; 214 215 /** Increment primer sequence buttons */ 216 const incrementPrimerButtonsWrapper = document.createElement("DIV"); 217 incrementPrimerButtonsWrapper.classList.add("primer-increment-buttons-wrapper"); 218 primerContainer.appendChild(incrementPrimerButtonsWrapper); // Append 219 220 const incrementPrimerButtonsLabel = document.createElement("DIV"); 221 incrementPrimerButtonsLabel.classList.add("primer-increment-buttons-label"); 222 incrementPrimerButtonsLabel.innerText = "Adjust primer sequences: " 223 incrementPrimerButtonsWrapper.appendChild(incrementPrimerButtonsLabel); // Append 224 225 const incrementPrimerButtonsContainer = document.createElement("DIV"); 226 incrementPrimerButtonsContainer.classList.add("primer-increment-buttons-container"); 227 incrementPrimerButtonsWrapper.appendChild(incrementPrimerButtonsContainer); // Append 228 229 // Add 5' button set 230 incrementPrimerButtonsContainer.appendChild( 231 this._createIncrementPrimerButtonSet( 232 primerSetIndex, 233 primerIndex, 234 "5'", 235 [ 236 { increment: 1, title: "Increment primer in the 5' direction" }, 237 { increment: -1, title: "Increment primer in the 3' direction" } 238 ], 239 ) 240 ); 241 242 const divider = document.createElement("DIV"); 243 divider.classList.add("primer-increment-buttons-divider") 244 incrementPrimerButtonsContainer.appendChild(divider); 245 246 // Add 3' button set 247 incrementPrimerButtonsContainer.appendChild( 248 this._createIncrementPrimerButtonSet( 249 primerSetIndex, 250 primerIndex, 251 "3'", 252 [ 253 { increment: 1, title: "Increment primer in the 3' direction" }, 254 { increment: -1, title: "Increment primer in the 5' direction" } 255 ], 256 ) 257 ); 258 259 260 261 const primerSequence = document.createElement("DIV"); 262 primerSequence.classList.add("primer-sequence-regions"); 263 primerContainer.appendChild(primerSequence); 264 265 266 /** Primer sequence */ 267 let totalPrimerLength = 0; 268 primer.regions.forEach(region => { 269 if (region.sequence.length === 0) {return}; 270 271 const regionSequence = document.createElement("span"); 272 regionSequence.classList.add("primer-sequence"); 273 regionSequence.classList.add(this.primerRegionsClasses[region.type]); 274 regionSequence.setAttribute("type", region.type); 275 regionSequence.setAttribute("start", region.start); 276 regionSequence.setAttribute("direction", region.direction); 277 regionSequence.innerText = region.sequence; 278 primerSequence.appendChild(regionSequence); 279 280 //regionSequence.addEventListener("mouseenter", (event) => { 281 // this.highlightPrimerBindingSite(event.target); 282 //}); 283 //regionSequence.addEventListener("mouseleave", () => { 284 // this.unhighlightPrimerBindingSites(); 285 //}); 286 287 totalPrimerLength += region.sequence.length; 288 }); 289 290 /** Primer info */ 291 const primerInfo = document.createElement("DIV"); 292 primerInfo.classList.add("primer-info") 293 const TBRSequence = primer.regions[primer.regions.length - 1].sequence; 294 primerInfo.innerHTML = ` 295 <div> 296 <span>TBR</span> ${TBRSequence.length} nt (${Nucleotides.getMeltingTemperature(TBRSequence).toFixed(2)} °C) 297 </div> 298 <div> 299 <span>Total</span> ${totalPrimerLength} nt 300 </div> 301 `; 302 primerContainer.appendChild(primerInfo); 303 304 return primerContainer; 305 }; 306 307 308 _createIncrementPrimerButtonSet(primerSetIndex, primerIndex, direction, buttons) { 309 const container = document.createElement("DIV"); 310 container.classList.add("primer-increment-buttons-container"); 311 312 const labelDiv = document.createElement("DIV"); 313 labelDiv.classList.add("primer-increment-label"); 314 labelDiv.innerText = direction; 315 container.appendChild(labelDiv); 316 317 const buttonsToRender = direction === "5'" ? buttons: [...buttons].reverse(); 318 319 buttonsToRender.forEach(({ increment, title }) => { 320 const button = document.createElement("span"); 321 button.classList.add("primer-increment-button", `primer-increment-${(increment === 1) ? "plus": "minus"}`, "toolbar-button"); 322 button.title = title; 323 button.appendChild(document.createElement("span")); 324 325 button.onclick = function (event) { 326 //const parentPrimerContainer = button.closest(".primer-container"); 327 //const highlightButton = parentPrimerContainer.querySelector(".primer-highlight-button"); 328 //Sidebar.unhighlightPrimerBindingSite(highlightButton); 329 Session.activePlasmid().incrementPrimerSequence( 330 primerSetIndex, 331 primerIndex, 332 direction, 333 increment, 334 ); 335 }; 336 337 container.appendChild(button); 338 }); 339 340 return container; 341 }; 342 343 344 /** 345 * Update the sidebar with the current primers. 346 */ 347 // TO DO: Hr info 348 // TO DO: Extend primer sequence buttons 349 // TO DO: download primers button 350 updatePrimersTable() { 351 const primersTable = this.generatePrimersTable(); 352 353 // Update sidebar table 354 const primersTableContainer = document.getElementById("primers-table-container"); 355 const prevPrimersTable = document.getElementById("primers-table"); 356 if (prevPrimersTable) { 357 primersTableContainer.removeChild(prevPrimersTable) 358 }; 359 primersTableContainer.appendChild(primersTable); 360 361 this.unhighlightPrimerBindingSites(); 362 this.highlightActivePrimerBindingSites(); 363 364 return; 365 }; 366 367 findPrimerBindingSites(primerSequence, direction) { 368 const activePlasmid = Session.activePlasmid(); 369 if (!activePlasmid) return; 370 371 let sequence = {"fwd": activePlasmid.sequence, "rev": activePlasmid.complementarySequence}[direction]; 372 const query = {"fwd": primerSequence, "rev": primerSequence.split("").reverse().join("")}[direction];; 373 374 let indices = []; 375 if (Session.activePlasmid().topology === "linear") { 376 let index = sequenceRepeating.indexOf(query); 377 while (index !== -1) { 378 indices.push(index); 379 index = sequence.indexOf(query, index + 1); 380 }; 381 } else { 382 const sequenceLength = sequence.length; 383 const sequenceRepeating = sequence.repeat(3); 384 let index = sequenceRepeating.indexOf(query); 385 while (index !== -1) { 386 const resultSpan = [index - sequenceLength, index + query.length - sequenceLength] 387 if (resultSpan[1] >= 0 && resultSpan[1] <= sequenceLength ) { 388 indices.push(index - sequenceLength); 389 }; 390 index = sequenceRepeating.indexOf(query, index + 1); 391 }; 392 }; 393 394 return indices; 395 }; 396 397 398 highlightActivePrimerBindingSites() { 399 console.log("Sidebar.highlightActivePrimerBindingSites -> ") 400 const primersTable = document.getElementById("primers-table"); 401 402 const primerContainers = primersTable.querySelectorAll(".primer-container");
403 primerContainers.forEach((primerContainer) => { 404 const highlightButton = primerContainer.querySelector(".primer-highlight-button"); 405 406 if (!highlightButton.hasAttribute("active")) return; 407 408 Sidebar.highlightPrimerBindingSite(highlightButton); 409 }); 410 }; 411 412 413 highlightPrimerBindingSite(sender) { 414 const primersContainer = sender.closest(".primer-container"); 415 416 const primerSequence = primersContainer.querySelector(".primer-sequence-regions").innerText; 417 let primerRegions = primersContainer.querySelectorAll(".primer-sequence") 418 419 const direction = primerRegions[0].getAttribute("direction") 420 421 // Find binding site starting index 422 const activePlasmid = Session.activePlasmid(); 423 if (!activePlasmid) return; 424 425 426 let indices = this.findPrimerBindingSites(primerSequence, direction); 427 console.log("Sidebar.hightlightPrimerBindingSite ->", primerSequence, direction, indices) 428 429 let index = indices[0] 430 431 if (direction == "rev") primerRegions = Array.from(primerRegions).reverse(); 432 433 primerRegions.forEach((primerRegion) => { 434 const type = primerRegion.getAttribute("type"); 435 const baseClass = this.baseClasses[type]; 436 437 const primerRegionSequence = primerRegion.innerText; 438 PlasmidViewer.highlightBases( 439 [index + 1, index + primerRegionSequence.length], 440 baseClass, 441 direction, 442 ); 443 index += primerRegionSequence.length; 444 }); 445 }; 446 447 448 unhighlightPrimerBindingSite(sender) { 449 const primersContainer = sender.closest(".primer-container"); 450 451 const primerSequence = primersContainer.querySelector(".primer-sequence-regions").innerText; 452 const primerRegions = primersContainer.querySelectorAll(".primer-sequence") 453 454 const direction = primerRegions[0].getAttribute("direction"); 455 456 let indices = this.findPrimerBindingSites(primerSequence, direction); 457 if (indices.length === 0) return; 458 459 let highlightedRange = [indices[0] + 1, indices[0] + primerSequence.length] 460 461 const plasmidSequenceLength = Session.activePlasmid().sequence.length; 462 463 const classes = Object.values(this.baseClasses); 464 // Iterate over highlight classes 465 for (let i = 0; i < classes.length; i++) { 466 const targetClass = classes[i]; 467 const basesWithClass = PlasmidViewer.highlightedBases[targetClass]; 468 if (!basesWithClass) continue; 469 470 for (let j = 0; j < basesWithClass.length; j++) { 471 const base = basesWithClass[j]; 472 const baseIndex = parseInt(base.getAttribute("base-index")); 473 474 const parentStrandGroup = base.closest('#strand-fwd, #strand-rev'); 475 if (!parentStrandGroup.id.includes(direction)) { 476 continue; 477 }; 478 479 //console.log(baseIndex, highlightedRange, highlightedRange.map(v => v + plasmidSequenceLength)) 480 if ( 481 (highlightedRange[0] <= baseIndex && baseIndex <= highlightedRange[1]) 482 || (highlightedRange[0] + plasmidSequenceLength <= baseIndex && baseIndex <= highlightedRange[1] + plasmidSequenceLength) 483 ) { 484 base.classList.remove(targetClass); 485 }; 486 }; 487 }; 488 //console.log(highlightedRange) 489 }; 490 491 492 unhighlightPrimerBindingSites() { 493 const classes = Object.values(this.baseClasses); 494 for (let i = 0; i < classes.length; i++) { 495 PlasmidViewer.unhighlightBases(classes[i]); 496 }; 497 }; 498 499 500 /** 501 * Update the sidebar with the current features table. 502 */ 503 updateFeaturesTable() { 504 const features = Session.activePlasmid().features; 505 506 // Create feature table container 507 const featuresTable = document.createElement("DIV"); 508 featuresTable.id = "features-table"; 509 featuresTable.classList.add("features-table"); 510 511 // Iterate over the features and populate the container 512 for (const [featureID, featureDict] of Object.entries(features)) { 513 // Skip source feature type 514 if (featureDict["type"] && featureDict["type"].includes("source")) continue; 515 if (featureID && featureID.includes("LOCUS")) continue; 516 517 /** 518 * Create new feature container 519 */ 520 const featureDiv = document.createElement("DIV"); 521 featureDiv.id = featureID; 522 523 /** 524 * Collapsible header 525 */ 526 const collapsibleHeader = document.createElement("BUTTON"); 527 collapsibleHeader.type = "button"; 528 collapsibleHeader.classList.add("collapsible-header"); 529 const currFeatureColor = (featureDict["color"]) ? featureDict["color"]: featureDict["ivaprimeColor"]; 530 collapsibleHeader.style.color = Utilities.getTextColorBasedOnBg(currFeatureColor) // Text color 531 collapsibleHeader.style.backgroundColor = currFeatureColor; 532 collapsibleHeader.innerText = featureDict["label"]; 533 534 collapsibleHeader.addEventListener("mouseenter", () => { 535 PlasmidViewer.addFeatureHover(featureID); 536 }); 537 538 collapsibleHeader.addEventListener("mouseleave", () => { 539 PlasmidViewer.removeFeatureHover(featureID); 540 }); 541 542 let collapsibleHeaderClickTimeout; 543 collapsibleHeader.addEventListener("click", (e) => { 544 if (collapsibleHeaderClickTimeout) {return}; 545 546 const targetElement = e.currentTarget; 547 548 collapsibleHeaderClickTimeout = setTimeout(() => { 549 Sidebar.toggleCollapsibleHeader(targetElement); 550 collapsibleHeaderClickTimeout = null; 551 }, 150); 552 }); 553 554 collapsibleHeader.addEventListener("dblclick", () => { 555 clearTimeout(collapsibleHeaderClickTimeout); 556 collapsibleHeaderClickTimeout = null;
557 PlasmidViewer.selectFeature(featureID); 558 PlasmidViewer.scrollToFeature(featureID); 559 }); 560 561 /** 562 * Collapsible content 563 */ 564 const collapsibleContent = document.createElement("DIV"); 565 collapsibleContent.classList.add("collapsible-content"); 566 //collapsibleContent.style.display = "none"; 567 568 /** 569 * Label 570 */ 571 const labelDiv = document.createElement("DIV"); 572 labelDiv.classList.add("collapsible-content-hgroup"); 573 labelDiv.innerHTML = ` 574 <label class="collapsible-content-hgroup-label">Label</label> 575 <div class="collapsible-content-hgroup-input"> 576 <input id="label-input" value="${featureDict["label"]}"> 577 <div class="clr-field" style="color: ${currFeatureColor};"> 578 <button type="button" aria-labelledby="clr-open-label"></button> 579 <input id="color-input" type="text" class="coloris" data-coloris value="${currFeatureColor}"></div> 580 </div> 581 </div> 582 `; 583 collapsibleContent.appendChild(labelDiv); 584 585 586 /** 587 * Span 588 */ 589 const spanDiv = document.createElement("DIV"); 590 spanDiv.classList.add("collapsible-content-hgroup"); 591 592 const [spanStart, spanEnd] = featureDict["span"]; 593 const spanEndMax = Session.activePlasmid().sequence.length; 594 595 const spanLabel = document.createElement("label"); 596 spanLabel.classList.add("collapsible-content-hgroup-label"); 597 spanLabel.textContent = "Span"; 598 599 const inputContainer = document.createElement("DIV"); 600 inputContainer.classList.add("collapsibe-content-hgroup-input"); 601 602 const startInput = document.createElement("input"); 603 startInput.type = "number"; 604 startInput.id = "span-start-input"; 605 startInput.min = "1"; 606 startInput.max = spanEndMax; 607 startInput.value = spanStart; 608 Utilities.addInputValidator(startInput, "integer"); 609 610 const separator = document.createElement("span"); 611 separator.textContent = " .. "; 612 613 const endInput = document.createElement("input"); 614 endInput.type = "number"; 615 endInput.id = "span-end-input"; 616 endInput.min = "1"; 617 endInput.max = spanEndMax; 618 endInput.value = spanEnd; 619 Utilities.addInputValidator(endInput, "integer"); 620 621 inputContainer.appendChild(startInput); 622 inputContainer.appendChild(separator); 623 inputContainer.appendChild(endInput); 624 spanDiv.appendChild(spanLabel); 625 spanDiv.appendChild(inputContainer); 626 collapsibleContent.appendChild(spanDiv); 627 628 629 /** 630 * Directionality 631 */ 632 const directionDiv = document.createElement("DIV"); 633 directionDiv.classList.add("collapsible-content-hgroup"); 634 directionDiv.innerHTML = ` 635 <label class="collapsible-content-hgroup-label">Direction</label> 636 <div class="collapsible-content-hgroup-input"> 637 <select id="directionality-select"> 638 <option value="fwd">Forward</option> 639 <option value="rev">Reverse</option> 640 </select> 641 </div>`; 642 const options = directionDiv.getElementsByTagName("option"); 643 const featureDirection = featureDict["directionality"]; 644 for (let i = 0; i < options.length; i++) { 645 const option = options[i]; 646 if (option.value === featureDirection) { 647 option.setAttribute('selected','selected'); 648 }; 649 }; 650 collapsibleContent.appendChild(directionDiv); 651 652 653 /** 654 * Translate feature checkbox 655 */ 656 const featureIsTranslated = (featureDict["translation"] === "" || featureDict["translation"] === null || (typeof featureDict["translation"]) === 'undefined') ? false: true 657 const translateDiv = document.createElement("DIV"); 658 translateDiv.classList.add("collapsible-content-hgroup"); 659 translateDiv.innerHTML = ` 660 <label class="collapsible-content-hgroup-label">Translate</label> 661 <div class="collapsible-content-hgroup-input"> 662 <input type="checkbox" id="translated-checkbox" name="translated-checkbox" checked="${featureIsTranslated}"> 663 </div> 664 `; 665 /** 666 * <div class="collapsible-content-hgroup-input"> 667 <input type="checkbox" id="translated-checkbox" name="translated-checkbox" checked="${featureIsTranslated}"> 668 <label for="translated-checkbox" class="custom-checkbox"></label> 669 </div> 670 */ 671 translateDiv.getElementsByTagName("input")[0].checked = featureIsTranslated; 672 collapsibleContent.appendChild(translateDiv); 673 674 675 /** 676 * Feature type 677 */ 678 const typeDiv = document.createElement("DIV"); 679 typeDiv.classList.add("collapsible-content-hgroup"); 680 typeDiv.innerHTML = ` 681 <label class="collapsible-content-hgroup-label">Feature type</label> 682 <div class="collapsible-content-hgroup-input"> 683 <select id="type-select" onchange=" 684 if (this.options[this.selectedIndex].value=='customOption') { 685 toggleInputInSelect(this, this.nextElementSibling); 686 this.selectedIndex='0'} 687 "> 688 </select><input id="type-input" name="browser" style="display: none;" disabled="disabled" onblur=" 689 if (this.value === '') { 690 toggleInputInSelect(this, this.previousElementSibling);} 691 "> 692 </div> 693 `; 694 const defaultTypes = [ 695 "CDS", 696 "misc_feature", 697 "primer_bind", 698 "promoter", 699 "protein_bind", 700 "source", 701 "terminator" 702 ]; 703 const select = typeDiv.getElementsByTagName("select")[0]; 704 for (let i = 0; i < defaultTypes.length; i++) { 705 let newOption = new Option(defaultTypes[i], defaultTypes[i]); 706 if (defaultTypes[i] === featureDict["type"]) { 707 newOption.setAttribute('selected','selected'); 708 }; 709 select.add(newOption, undefined); 710 }; 711 if (!defaultTypes.includes(featureDict["type"])) { 712 let newOption = new Option(featureDict["type"], featureDict["type"]); 713 newOption.setAttribute('selected','selected'); 714 select.add(newOption, undefined); 715 }; 716 //const customOption = new Option("Custom type", "customOption"); 717 //select.add(customOption, undefined); 718 collapsibleContent.appendChild(typeDiv); 719 720 721 /** 722 * Note 723 */ 724 const noteDiv = document.createElement("DIV"); 725 noteDiv.classList.add("collapsible-content-hgroup"); 726 noteDiv.innerHTML = ` 727 <label class="collapsible-content-hgroup-label">Note</label> 728 <div class="collapsible-content-hgroup-input"> 729 <textarea id="note-text-area" spellcheck="false">${featureDict["note"]}</textarea> 730 </div> 731 `; 732 collapsibleContent.appendChild(noteDiv); 733 734 /** 735 * Update info buttons 736 */ 737 const updateButtonDiv = document.createElement("DIV"); 738 updateButtonDiv.classList.add("collapsible-content-hgroup"); 739 740 // Create the update button 741 const updateButton = document.createElement("SPAN"); 742 updateButton.classList.add("button-round", "button-green", "update-feature-button"); 743 updateButton.textContent = "Update"; 744 updateButton.onclick = function () { 745 if (!collapsibleContent.querySelector("[incorrect]")) { 746 Session.getPlasmid(Session.activePlasmidIndex).updateFeatureProperties(featureID); 747 }; 748 }; 749 750 // Create the remove button 751 const removeButton = document.createElement("SPAN"); 752 removeButton.classList.add("button-round", "button-red", "remove-feature-button"); 753 removeButton.textContent = "Remove"; 754 removeButton.onclick = function () { 755 Session.getPlasmid(Session.activePlasmidIndex).removeFeature
755(featureID); 756 }; 757 758 function checkInputsValidation() { 759 const inputFailedValidator = collapsibleContent.querySelector("[incorrect]") !== null; 760 761 if (inputFailedValidator) { 762 updateButton.setAttribute("disabled", ""); 763 } else { 764 updateButton.removeAttribute("disabled"); 765 }; 766 }; 767 collapsibleContent.querySelectorAll("input").forEach(input => { 768 input.addEventListener("input", checkInputsValidation); 769 }); 770 771 // Append buttons to the div 772 updateButtonDiv.appendChild(updateButton); 773 updateButtonDiv.appendChild(removeButton); 774 collapsibleContent.appendChild(updateButtonDiv); 775 776 777 featureDiv.appendChild(collapsibleHeader); 778 featureDiv.appendChild(collapsibleContent); 779 featuresTable.appendChild(featureDiv); 780 }; 781 782 // Update sidebar table 783 const featuresTableContainer = document.getElementById("features-table-container"); 784 const currFeaturesTable = document.getElementById("features-table"); 785 if (currFeaturesTable) { 786 featuresTableContainer.removeChild(currFeaturesTable) 787 }; 788 featuresTableContainer.appendChild(featuresTable); 789 }; 790 791 792 /** 793 * Expand collapsible header in features table. 794 * 795 * @param {Object} targetHeader - Target header. 796 */ 797 toggleCollapsibleHeader(targetHeader) { 798 console.log("Sidebar.toggleCollapsibleHeader", targetHeader) 799 const targetContent = targetHeader.nextElementSibling; 800 801 // Toggle active class 802 targetHeader.classList.toggle("collapsible-header-active"); 803 804 /** 805 * Expand. 806 */ 807 targetContent.toggleAttribute("visible"); 808 return; 809 }; 810 811 812 /** 813 * Close all collapsible headers. 814 */ 815 closeAllCollapsibleHeaders() { 816 for (const header of document.querySelectorAll(".collapsible-header-active")) { 817 header.classList.toggle("collapsible-header-active"); 818 const content = header.nextElementSibling; 819 content.style.display = "none"; 820 content.style.maxHeight = null; 821 }; 822 }; 823};
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.