PageSourceSearch

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

js ivaprime.com collected 2026-10-03 21:37:58 UTC 33,060 bytes, 823 lines download raw bytes

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.