TSUMUGI 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- TSUMUGI/annotator.py +103 -0
- TSUMUGI/argparser.py +599 -0
- TSUMUGI/core.py +185 -0
- TSUMUGI/data/impc_phenodigm.csv +3406 -0
- TSUMUGI/data/mp.obo +143993 -0
- TSUMUGI/filterer.py +36 -0
- TSUMUGI/formatter.py +122 -0
- TSUMUGI/genewise_annotation_builder.py +94 -0
- TSUMUGI/io_handler.py +189 -0
- TSUMUGI/main.py +300 -0
- TSUMUGI/network_constructor.py +603 -0
- TSUMUGI/ontology_handler.py +62 -0
- TSUMUGI/pairwise_similarity_builder.py +66 -0
- TSUMUGI/report_generator.py +122 -0
- TSUMUGI/similarity_calculator.py +498 -0
- TSUMUGI/subcommands/count_filterer.py +47 -0
- TSUMUGI/subcommands/genes_filterer.py +89 -0
- TSUMUGI/subcommands/graphml_builder.py +158 -0
- TSUMUGI/subcommands/life_stage_filterer.py +48 -0
- TSUMUGI/subcommands/mp_filterer.py +142 -0
- TSUMUGI/subcommands/score_filterer.py +22 -0
- TSUMUGI/subcommands/sex_filterer.py +48 -0
- TSUMUGI/subcommands/webapp_builder.py +358 -0
- TSUMUGI/subcommands/zygosity_filterer.py +48 -0
- TSUMUGI/validator.py +65 -0
- TSUMUGI/web/app/css/app.css +1129 -0
- TSUMUGI/web/app/genelist/network_genelist.html +339 -0
- TSUMUGI/web/app/genelist/network_genelist.js +421 -0
- TSUMUGI/web/app/js/data/dataLoader.js +41 -0
- TSUMUGI/web/app/js/export/graphExporter.js +214 -0
- TSUMUGI/web/app/js/graph/centrality.js +495 -0
- TSUMUGI/web/app/js/graph/components.js +30 -0
- TSUMUGI/web/app/js/graph/filters.js +158 -0
- TSUMUGI/web/app/js/graph/highlighter.js +52 -0
- TSUMUGI/web/app/js/graph/layoutController.js +454 -0
- TSUMUGI/web/app/js/graph/valueScaler.js +43 -0
- TSUMUGI/web/app/js/search/geneSearcher.js +93 -0
- TSUMUGI/web/app/js/search/phenotypeSearcher.js +292 -0
- TSUMUGI/web/app/js/ui/dynamicFontSize.js +30 -0
- TSUMUGI/web/app/js/ui/mobilePanel.js +77 -0
- TSUMUGI/web/app/js/ui/slider.js +22 -0
- TSUMUGI/web/app/js/ui/tooltips.js +514 -0
- TSUMUGI/web/app/js/viewer/pageSetup.js +217 -0
- TSUMUGI/web/app/viewer.html +515 -0
- TSUMUGI/web/app/viewer.js +1593 -0
- TSUMUGI/web/css/sanitize.css +363 -0
- TSUMUGI/web/css/top.css +391 -0
- TSUMUGI/web/image/tsumugi-favicon.ico +0 -0
- TSUMUGI/web/image/tsumugi-icon.png +0 -0
- TSUMUGI/web/image/tsumugi-logo.png +0 -0
- TSUMUGI/web/image/tsumugi-logo.svg +69 -0
- TSUMUGI/web/js/genelist_formatter.js +123 -0
- TSUMUGI/web/js/top.js +338 -0
- TSUMUGI/web/open_webapp_linux.sh +25 -0
- TSUMUGI/web/open_webapp_mac.command +25 -0
- TSUMUGI/web/open_webapp_windows.bat +37 -0
- TSUMUGI/web/serve_index.py +110 -0
- TSUMUGI/web/template/template_index.html +197 -0
- TSUMUGI/web_deployer.py +150 -0
- tsumugi-1.0.1.dist-info/METADATA +504 -0
- tsumugi-1.0.1.dist-info/RECORD +64 -0
- tsumugi-1.0.1.dist-info/WHEEL +4 -0
- tsumugi-1.0.1.dist-info/entry_points.txt +3 -0
- tsumugi-1.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// ############################################################
|
|
2
|
+
// Phenotype Search and Highlight Functions
|
|
3
|
+
// ############################################################
|
|
4
|
+
|
|
5
|
+
// Hold the available and selected phenotype lists
|
|
6
|
+
let allPhenotypes = [];
|
|
7
|
+
let selectedPhenotypes = new Set();
|
|
8
|
+
let cytoscapeInstance = null;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize the phenotype search feature.
|
|
12
|
+
* @param {Object} params - Initialization parameters
|
|
13
|
+
* @param {Object} params.cy - Cytoscape instance
|
|
14
|
+
* @param {Array} params.elements - Network elements
|
|
15
|
+
*/
|
|
16
|
+
export function setupPhenotypeSearch({ cy, elements }) {
|
|
17
|
+
cytoscapeInstance = cy;
|
|
18
|
+
initializePhenotypeSearch(cy);
|
|
19
|
+
setupPhenotypeSearchInput();
|
|
20
|
+
|
|
21
|
+
// Expose helper for the inline onclick handlers
|
|
22
|
+
window.removeSelectedPhenotype = removeSelectedPhenotype;
|
|
23
|
+
|
|
24
|
+
// Make updatePhenotypeHighlight available to other modules
|
|
25
|
+
window.updatePhenotypeHighlight = () => updatePhenotypeHighlight(cy);
|
|
26
|
+
|
|
27
|
+
// Allow other modules to refresh the phenotype list after filtering
|
|
28
|
+
window.refreshPhenotypeList = () => refreshPhenotypeList();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the phenotype list from the currently visible genes.
|
|
33
|
+
* @param {Object} cy - Cytoscape instance
|
|
34
|
+
*/
|
|
35
|
+
function initializePhenotypeSearch(cy) {
|
|
36
|
+
const phenotypeSet = new Set();
|
|
37
|
+
|
|
38
|
+
// Extract phenotypes only from visible nodes
|
|
39
|
+
cy.nodes().forEach((node) => {
|
|
40
|
+
// Skip nodes that are hidden
|
|
41
|
+
if (node.style("display") !== "none" && !node.hidden()) {
|
|
42
|
+
const nodeData = node.data();
|
|
43
|
+
if (nodeData.phenotype) {
|
|
44
|
+
const phenotypes = Array.isArray(nodeData.phenotype) ? nodeData.phenotype : [nodeData.phenotype];
|
|
45
|
+
phenotypes.forEach((phenotype) => {
|
|
46
|
+
if (phenotype && phenotype.trim() !== "") {
|
|
47
|
+
phenotypeSet.add(phenotype.trim());
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
allPhenotypes = Array.from(phenotypeSet).sort();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Wire up event handlers for the phenotype search input.
|
|
59
|
+
*/
|
|
60
|
+
function setupPhenotypeSearchInput() {
|
|
61
|
+
const searchInput = document.getElementById("phenotype-search");
|
|
62
|
+
const suggestionsList = document.getElementById("phenotype-suggestions");
|
|
63
|
+
|
|
64
|
+
if (!searchInput || !suggestionsList) {
|
|
65
|
+
console.warn("Phenotype search elements not found in DOM");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
searchInput.addEventListener("input", function () {
|
|
70
|
+
const searchTerm = this.value.toLowerCase().trim();
|
|
71
|
+
|
|
72
|
+
if (searchTerm.length === 0) {
|
|
73
|
+
suggestionsList.hidden = true;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Filter out phenotypes that are already selected
|
|
78
|
+
const filteredPhenotypes = allPhenotypes.filter(
|
|
79
|
+
(phenotype) => phenotype.toLowerCase().includes(searchTerm) && !selectedPhenotypes.has(phenotype),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
displayPhenotypeSuggestions(filteredPhenotypes);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Display suggestions when the field is clicked
|
|
86
|
+
searchInput.addEventListener("click", function () {
|
|
87
|
+
const searchTerm = this.value.toLowerCase().trim();
|
|
88
|
+
|
|
89
|
+
if (searchTerm.length === 0) {
|
|
90
|
+
// If empty, show all phenotypes except the selected ones
|
|
91
|
+
const availablePhenotypes = allPhenotypes.filter((phenotype) => !selectedPhenotypes.has(phenotype));
|
|
92
|
+
displayPhenotypeSuggestions(availablePhenotypes);
|
|
93
|
+
} else {
|
|
94
|
+
// Otherwise, show matching phenotypes
|
|
95
|
+
const filteredPhenotypes = allPhenotypes.filter(
|
|
96
|
+
(phenotype) => phenotype.toLowerCase().includes(searchTerm) && !selectedPhenotypes.has(phenotype),
|
|
97
|
+
);
|
|
98
|
+
displayPhenotypeSuggestions(filteredPhenotypes);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Also display suggestions when the field gains focus
|
|
103
|
+
searchInput.addEventListener("focus", function () {
|
|
104
|
+
const searchTerm = this.value.toLowerCase().trim();
|
|
105
|
+
|
|
106
|
+
if (searchTerm.length === 0) {
|
|
107
|
+
// If empty, show all phenotypes except the selected ones
|
|
108
|
+
const availablePhenotypes = allPhenotypes.filter((phenotype) => !selectedPhenotypes.has(phenotype));
|
|
109
|
+
displayPhenotypeSuggestions(availablePhenotypes);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Hide the suggestions when clicking outside the input
|
|
114
|
+
document.addEventListener("click", function (event) {
|
|
115
|
+
if (!searchInput.contains(event.target) && !suggestionsList.contains(event.target)) {
|
|
116
|
+
suggestionsList.hidden = true;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Render the phenotype suggestions list.
|
|
123
|
+
* @param {Array} phenotypes - Phenotypes to display
|
|
124
|
+
*/
|
|
125
|
+
function displayPhenotypeSuggestions(phenotypes) {
|
|
126
|
+
const suggestionsList = document.getElementById("phenotype-suggestions");
|
|
127
|
+
suggestionsList.innerHTML = "";
|
|
128
|
+
|
|
129
|
+
if (phenotypes.length === 0) {
|
|
130
|
+
suggestionsList.hidden = true;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Use the full list (scroll is possible in the UI)
|
|
135
|
+
const displayPhenotypes = phenotypes;
|
|
136
|
+
|
|
137
|
+
displayPhenotypes.forEach((phenotype) => {
|
|
138
|
+
const li = document.createElement("li");
|
|
139
|
+
li.textContent = phenotype;
|
|
140
|
+
li.addEventListener("click", function () {
|
|
141
|
+
addSelectedPhenotype(phenotype);
|
|
142
|
+
document.getElementById("phenotype-search").value = "";
|
|
143
|
+
suggestionsList.hidden = true;
|
|
144
|
+
});
|
|
145
|
+
suggestionsList.appendChild(li);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
suggestionsList.hidden = false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Add a phenotype to the selection list.
|
|
153
|
+
* @param {string} phenotype - Phenotype to add
|
|
154
|
+
*/
|
|
155
|
+
function addSelectedPhenotype(phenotype) {
|
|
156
|
+
if (selectedPhenotypes.has(phenotype)) return;
|
|
157
|
+
|
|
158
|
+
selectedPhenotypes.add(phenotype);
|
|
159
|
+
displaySelectedPhenotypes();
|
|
160
|
+
|
|
161
|
+
// Invoke the globally exposed updatePhenotypeHighlight helper
|
|
162
|
+
if (window.updatePhenotypeHighlight) {
|
|
163
|
+
window.updatePhenotypeHighlight();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Remove a selected phenotype (called from inline handlers).
|
|
169
|
+
* @param {string} phenotype - Phenotype to remove
|
|
170
|
+
*/
|
|
171
|
+
function removeSelectedPhenotype(phenotype) {
|
|
172
|
+
selectedPhenotypes.delete(phenotype);
|
|
173
|
+
displaySelectedPhenotypes();
|
|
174
|
+
|
|
175
|
+
// Invoke the globally exposed updatePhenotypeHighlight helper
|
|
176
|
+
if (window.updatePhenotypeHighlight) {
|
|
177
|
+
window.updatePhenotypeHighlight();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Render the currently selected phenotypes as tags.
|
|
183
|
+
*/
|
|
184
|
+
function displaySelectedPhenotypes() {
|
|
185
|
+
const container = document.getElementById("selected-phenotypes");
|
|
186
|
+
if (!container) return;
|
|
187
|
+
|
|
188
|
+
container.innerHTML = "";
|
|
189
|
+
|
|
190
|
+
selectedPhenotypes.forEach((phenotype) => {
|
|
191
|
+
const tag = document.createElement("div");
|
|
192
|
+
tag.className = "selected-phenotype-tag";
|
|
193
|
+
tag.innerHTML = `
|
|
194
|
+
<span class="phenotype-text">${phenotype}</span>
|
|
195
|
+
<button class="remove-btn" onclick="removeSelectedPhenotype('${phenotype.replace(/'/g, "\\'")}')">×</button>
|
|
196
|
+
`;
|
|
197
|
+
container.appendChild(tag);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Highlight genes that match any of the selected phenotypes.
|
|
203
|
+
* @param {Object} cy - Cytoscape instance
|
|
204
|
+
*/
|
|
205
|
+
function updatePhenotypeHighlight(cy) {
|
|
206
|
+
// Reset existing highlights
|
|
207
|
+
cy.nodes().removeClass("phenotype-highlight");
|
|
208
|
+
|
|
209
|
+
if (selectedPhenotypes.size === 0) {
|
|
210
|
+
return; // Nothing to highlight
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Highlight genes that match the selected phenotypes
|
|
214
|
+
cy.nodes().forEach((node) => {
|
|
215
|
+
const nodeData = node.data();
|
|
216
|
+
|
|
217
|
+
if (nodeData.phenotype) {
|
|
218
|
+
const nodePhenotypes = Array.isArray(nodeData.phenotype) ? nodeData.phenotype : [nodeData.phenotype];
|
|
219
|
+
|
|
220
|
+
// Look for any overlap between the node's phenotypes and the selected ones
|
|
221
|
+
const hasSelectedPhenotype = Array.from(selectedPhenotypes).some((selectedPhenotype) =>
|
|
222
|
+
nodePhenotypes.some(
|
|
223
|
+
(nodePhenotype) => nodePhenotype && nodePhenotype.trim() === selectedPhenotype.trim(),
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (hasSelectedPhenotype) {
|
|
228
|
+
node.addClass("phenotype-highlight");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Return the set of currently selected phenotypes.
|
|
236
|
+
* @returns {Set} Selected phenotype set
|
|
237
|
+
*/
|
|
238
|
+
export function getSelectedPhenotypes() {
|
|
239
|
+
return new Set(selectedPhenotypes);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Clear all selected phenotypes.
|
|
244
|
+
*/
|
|
245
|
+
export function clearSelectedPhenotypes() {
|
|
246
|
+
selectedPhenotypes.clear();
|
|
247
|
+
displaySelectedPhenotypes();
|
|
248
|
+
|
|
249
|
+
if (window.updatePhenotypeHighlight) {
|
|
250
|
+
window.updatePhenotypeHighlight();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Rebuild the phenotype list after filters change.
|
|
256
|
+
*/
|
|
257
|
+
function refreshPhenotypeList() {
|
|
258
|
+
if (!cytoscapeInstance) return;
|
|
259
|
+
|
|
260
|
+
// Preserve the current search term
|
|
261
|
+
const searchInput = document.getElementById("phenotype-search");
|
|
262
|
+
const currentSearchValue = searchInput ? searchInput.value : "";
|
|
263
|
+
|
|
264
|
+
// Reinitialize the phenotype list
|
|
265
|
+
initializePhenotypeSearch(cytoscapeInstance);
|
|
266
|
+
|
|
267
|
+
// Drop selected phenotypes that no longer exist
|
|
268
|
+
const updatedSelectedPhenotypes = new Set();
|
|
269
|
+
selectedPhenotypes.forEach((phenotype) => {
|
|
270
|
+
if (allPhenotypes.includes(phenotype)) {
|
|
271
|
+
updatedSelectedPhenotypes.add(phenotype);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
selectedPhenotypes = updatedSelectedPhenotypes;
|
|
275
|
+
|
|
276
|
+
// Refresh the UI
|
|
277
|
+
displaySelectedPhenotypes();
|
|
278
|
+
|
|
279
|
+
// Update suggestions if a search term was present
|
|
280
|
+
if (searchInput && currentSearchValue.trim().length > 0) {
|
|
281
|
+
const searchTerm = currentSearchValue.toLowerCase().trim();
|
|
282
|
+
const filteredPhenotypes = allPhenotypes.filter(
|
|
283
|
+
(phenotype) => phenotype.toLowerCase().includes(searchTerm) && !selectedPhenotypes.has(phenotype),
|
|
284
|
+
);
|
|
285
|
+
displayPhenotypeSuggestions(filteredPhenotypes);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Refresh highlights
|
|
289
|
+
if (window.updatePhenotypeHighlight) {
|
|
290
|
+
window.updatePhenotypeHighlight();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Dynamic header font sizing
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
export function initDynamicFontSize() {
|
|
6
|
+
const adjustHeaderFontSize = () => {
|
|
7
|
+
const header = document.querySelector(".header-container h1");
|
|
8
|
+
if (!header) return;
|
|
9
|
+
|
|
10
|
+
const viewportWidth = window.innerWidth;
|
|
11
|
+
|
|
12
|
+
// Base font size derived from viewport width.
|
|
13
|
+
let baseFontSize = viewportWidth * 0.04;
|
|
14
|
+
baseFontSize = Math.max(14, Math.min(36, baseFontSize));
|
|
15
|
+
|
|
16
|
+
if (viewportWidth <= 600) {
|
|
17
|
+
baseFontSize = Math.min(baseFontSize, viewportWidth * 0.045);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
header.style.fontSize = `${baseFontSize}px`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
adjustHeaderFontSize();
|
|
24
|
+
|
|
25
|
+
let resizeTimer;
|
|
26
|
+
window.addEventListener("resize", () => {
|
|
27
|
+
clearTimeout(resizeTimer);
|
|
28
|
+
resizeTimer = setTimeout(adjustHeaderFontSize, 100);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Mobile panel toggles for small screens
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
export function initMobilePanel() {
|
|
6
|
+
const menuToggle = document.getElementById("menu-toggle");
|
|
7
|
+
const leftPanel = document.querySelector(".left-control-panel-container");
|
|
8
|
+
const rightPanel = document.querySelector(".right-control-panel-container");
|
|
9
|
+
const closeButton = document.getElementById("close-panel");
|
|
10
|
+
|
|
11
|
+
if (!menuToggle || !leftPanel || !rightPanel || !closeButton) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// On small screens move the right panel beneath the left panel.
|
|
16
|
+
const reorganizePanels = () => {
|
|
17
|
+
if (window.innerWidth <= 600) {
|
|
18
|
+
if (rightPanel.parentNode !== leftPanel) {
|
|
19
|
+
leftPanel.appendChild(rightPanel);
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
const bodyContainer = document.querySelector(".body-container");
|
|
23
|
+
if (rightPanel.parentNode === leftPanel && bodyContainer) {
|
|
24
|
+
bodyContainer.appendChild(rightPanel);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
reorganizePanels();
|
|
30
|
+
window.addEventListener("resize", reorganizePanels);
|
|
31
|
+
|
|
32
|
+
const openPanel = (event) => {
|
|
33
|
+
event.stopPropagation();
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
|
|
36
|
+
menuToggle.style.display = "none";
|
|
37
|
+
leftPanel.classList.add("active");
|
|
38
|
+
if (window.innerWidth <= 600) {
|
|
39
|
+
rightPanel.classList.add("active");
|
|
40
|
+
}
|
|
41
|
+
closeButton.style.display = "block";
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const closePanel = (event) => {
|
|
45
|
+
if (event) {
|
|
46
|
+
event.stopPropagation();
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
leftPanel.classList.remove("active");
|
|
51
|
+
rightPanel.classList.remove("active");
|
|
52
|
+
closeButton.style.display = "none";
|
|
53
|
+
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
menuToggle.style.display = "block";
|
|
56
|
+
}, 50);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
["click", "touchstart"].forEach((eventName) => {
|
|
60
|
+
menuToggle.addEventListener(eventName, openPanel);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
["click", "touchstart"].forEach((eventName) => {
|
|
64
|
+
closeButton.addEventListener(eventName, closePanel);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
document.addEventListener("click", (event) => {
|
|
68
|
+
if (
|
|
69
|
+
leftPanel.classList.contains("active") &&
|
|
70
|
+
!leftPanel.contains(event.target) &&
|
|
71
|
+
!menuToggle.contains(event.target) &&
|
|
72
|
+
!closeButton.contains(event.target)
|
|
73
|
+
) {
|
|
74
|
+
closePanel();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Generic slider factory supporting single-value and range sliders
|
|
2
|
+
export function createSlider(id, start, min, max, step, updateCallback, isRange = false) {
|
|
3
|
+
const slider = document.getElementById(id);
|
|
4
|
+
if (!slider) {
|
|
5
|
+
console.error(`Slider with ID '${id}' not found.`);
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
noUiSlider.create(slider, {
|
|
10
|
+
start: isRange ? start : [start], // Use [start, end] when range mode is enabled
|
|
11
|
+
connect: true,
|
|
12
|
+
range: { min: min, max: max },
|
|
13
|
+
step: step,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
slider.noUiSlider.on("update", function (value) {
|
|
17
|
+
const intValues = isRange ? value.map(Math.round) : Math.round(value);
|
|
18
|
+
updateCallback(intValues);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return slider.noUiSlider;
|
|
22
|
+
}
|