@datagrok/sequence-translator 1.10.9 → 1.10.10
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.
- package/dist/package-test.js +1 -1
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/package.json +2 -2
- package/src/polytool/pt-enumerate-seq-dialog.ts +1 -1
- package/test-console-output-1.log +84 -83
- package/test-record-1.mp4 +0 -0
- package/src/polytool/pt-monomer-selection-dialog.ts +0 -352
package/test-record-1.mp4
CHANGED
|
Binary file
|
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
/* eslint-disable max-len */
|
|
2
|
-
import * as ui from 'datagrok-api/ui';
|
|
3
|
-
import * as DG from 'datagrok-api/dg';
|
|
4
|
-
|
|
5
|
-
import {HelmType, PolymerType} from '@datagrok-libraries/bio/src/helm/types';
|
|
6
|
-
import {IMonomerLib, IMonomerLibHelper, Monomer} from '@datagrok-libraries/bio/src/types/monomer-library';
|
|
7
|
-
import {polymerTypeToHelmType} from '@datagrok-libraries/bio/src/utils/macromolecule/utils';
|
|
8
|
-
|
|
9
|
-
import {parseMonomerSymbolList} from './pt-placeholders-input';
|
|
10
|
-
|
|
11
|
-
const MAX_SUGGESTIONS = 20;
|
|
12
|
-
|
|
13
|
-
/** Shows a dialog for selecting monomers with autocomplete, tag-based display,
|
|
14
|
-
* and bulk-add from monomer libraries or collections.
|
|
15
|
-
* @returns monomer symbols array, or null if cancelled */
|
|
16
|
-
export async function showMonomerSelectionDialog(
|
|
17
|
-
monomerLib: IMonomerLib, polymerType: PolymerType, presetMonomers?: string[],
|
|
18
|
-
libHelper?: IMonomerLibHelper,
|
|
19
|
-
): Promise<string[] | null> {
|
|
20
|
-
return new Promise<string[] | null>((resolve) => {
|
|
21
|
-
const helmType: HelmType = polymerTypeToHelmType(polymerType);
|
|
22
|
-
const allSymbols = monomerLib.getMonomerSymbolsByType(polymerType);
|
|
23
|
-
|
|
24
|
-
const selectedMonomers: string[] = presetMonomers ? [...presetMonomers] : [];
|
|
25
|
-
|
|
26
|
-
// --- Tags display (selected monomers) ---
|
|
27
|
-
const tagsHost = ui.div([], {style: {display: 'flex', flexWrap: 'wrap', gap: '4px', marginTop: '4px', maxHeight: '150px', overflowY: 'auto'}});
|
|
28
|
-
const countLabel = ui.divText('', {style: {fontSize: '11px', color: 'var(--grey-4)', marginTop: '4px'}});
|
|
29
|
-
|
|
30
|
-
function updateCountLabel(): void {
|
|
31
|
-
countLabel.textContent = selectedMonomers.length > 0 ? `${selectedMonomers.length} monomer(s) selected` : '';
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// --- Autocomplete input ---
|
|
35
|
-
const input = ui.input.string('Search', {value: ''});
|
|
36
|
-
const inputEl = input.input as HTMLInputElement;
|
|
37
|
-
inputEl.setAttribute('autocomplete', 'off');
|
|
38
|
-
inputEl.placeholder = 'Type to search monomers...';
|
|
39
|
-
|
|
40
|
-
let currentMenu: DG.Menu | null = null;
|
|
41
|
-
let menuItems: HTMLElement[] = [];
|
|
42
|
-
let highlightedIndex = -1;
|
|
43
|
-
|
|
44
|
-
function renderTags(): void {
|
|
45
|
-
tagsHost.innerHTML = '';
|
|
46
|
-
for (const symbol of selectedMonomers) {
|
|
47
|
-
const removeBtn = ui.iconFA('times', () => {
|
|
48
|
-
const idx = selectedMonomers.indexOf(symbol);
|
|
49
|
-
if (idx >= 0) {
|
|
50
|
-
selectedMonomers.splice(idx, 1);
|
|
51
|
-
renderTags();
|
|
52
|
-
updateCountLabel();
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
removeBtn.style.marginLeft = '4px';
|
|
56
|
-
removeBtn.style.cursor = 'pointer';
|
|
57
|
-
|
|
58
|
-
const tag = ui.div([ui.span([symbol]), removeBtn], {
|
|
59
|
-
style: {
|
|
60
|
-
display: 'inline-flex', alignItems: 'center',
|
|
61
|
-
padding: '2px 6px', borderRadius: '4px',
|
|
62
|
-
backgroundColor: 'var(--grey-2)', border: '1px solid var(--grey-3)',
|
|
63
|
-
fontSize: '12px', cursor: 'default',
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
// Tooltip on hover
|
|
67
|
-
tag.addEventListener('mouseenter', () => {
|
|
68
|
-
const tooltip = monomerLib.getTooltip(helmType, symbol);
|
|
69
|
-
ui.tooltip.show(tooltip, tag.getBoundingClientRect().left, tag.getBoundingClientRect().bottom + 16);
|
|
70
|
-
});
|
|
71
|
-
tag.addEventListener('mouseleave', () => { ui.tooltip.hide(); });
|
|
72
|
-
|
|
73
|
-
tagsHost.appendChild(tag);
|
|
74
|
-
}
|
|
75
|
-
updateCountLabel();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function addMonomer(symbol: string): void {
|
|
79
|
-
if (!selectedMonomers.includes(symbol)) {
|
|
80
|
-
selectedMonomers.push(symbol);
|
|
81
|
-
renderTags();
|
|
82
|
-
}
|
|
83
|
-
inputEl.value = '';
|
|
84
|
-
hideMenu();
|
|
85
|
-
inputEl.focus();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function addMonomers(symbols: string[]): void {
|
|
89
|
-
let added = false;
|
|
90
|
-
for (const symbol of symbols) {
|
|
91
|
-
if (!selectedMonomers.includes(symbol)) {
|
|
92
|
-
selectedMonomers.push(symbol);
|
|
93
|
-
added = true;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (added)
|
|
97
|
-
renderTags();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function hideMenu(): void {
|
|
101
|
-
if (currentMenu) {
|
|
102
|
-
currentMenu.hide();
|
|
103
|
-
currentMenu = null;
|
|
104
|
-
}
|
|
105
|
-
menuItems = [];
|
|
106
|
-
highlightedIndex = -1;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function getSuggestions(query: string): {symbol: string, monomer: Monomer | null}[] {
|
|
110
|
-
const q = query.toLowerCase();
|
|
111
|
-
const results: {symbol: string, monomer: Monomer | null, rank: number}[] = [];
|
|
112
|
-
|
|
113
|
-
for (const symbol of allSymbols) {
|
|
114
|
-
if (selectedMonomers.includes(symbol))
|
|
115
|
-
continue;
|
|
116
|
-
const monomer = monomerLib.getMonomer(polymerType, symbol);
|
|
117
|
-
const symLower = symbol.toLowerCase();
|
|
118
|
-
const nameLower = monomer?.name?.toLowerCase() ?? '';
|
|
119
|
-
|
|
120
|
-
if (symLower.startsWith(q))
|
|
121
|
-
results.push({symbol, monomer, rank: 0});
|
|
122
|
-
else if (symLower.includes(q))
|
|
123
|
-
results.push({symbol, monomer, rank: 1});
|
|
124
|
-
else if (nameLower.includes(q))
|
|
125
|
-
results.push({symbol, monomer, rank: 2});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
results.sort((a, b) => a.rank - b.rank || a.symbol.localeCompare(b.symbol));
|
|
129
|
-
return results.slice(0, MAX_SUGGESTIONS);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function showSuggestions(): void {
|
|
133
|
-
hideMenu();
|
|
134
|
-
const query = inputEl.value.trim();
|
|
135
|
-
if (!query)
|
|
136
|
-
return;
|
|
137
|
-
|
|
138
|
-
const suggestions = getSuggestions(query);
|
|
139
|
-
if (suggestions.length === 0)
|
|
140
|
-
return;
|
|
141
|
-
|
|
142
|
-
currentMenu = DG.Menu.popup();
|
|
143
|
-
const maxElement = suggestions.reduce((max, s) => {
|
|
144
|
-
const label = s.monomer?.name ? `${s.symbol} - ${s.monomer.name}` : s.symbol;
|
|
145
|
-
if (max.length < label.length)
|
|
146
|
-
return label;
|
|
147
|
-
return max;
|
|
148
|
-
}, '');
|
|
149
|
-
currentMenu.item(maxElement, () => {}); // Dummy item to set menu width
|
|
150
|
-
|
|
151
|
-
const causedBy = new MouseEvent('mousemove', {clientX: inputEl.getBoundingClientRect().left, clientY: inputEl.getBoundingClientRect().bottom});
|
|
152
|
-
currentMenu.show({causedBy: causedBy,
|
|
153
|
-
y: inputEl.offsetHeight + inputEl.offsetTop, x: inputEl.offsetLeft});
|
|
154
|
-
|
|
155
|
-
// collect menu items for keyboard navigation
|
|
156
|
-
setTimeout(() => {
|
|
157
|
-
currentMenu?.clear();
|
|
158
|
-
for (const s of suggestions) {
|
|
159
|
-
const label = s.monomer?.name ? `${s.symbol} - ${s.monomer.name}` : s.symbol;
|
|
160
|
-
currentMenu?.item(label, () => { addMonomer(s.symbol); });
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const menuRoot = document.querySelector('.d4-menu-popup:last-of-type');
|
|
164
|
-
if (menuRoot)
|
|
165
|
-
menuItems = Array.from(menuRoot.querySelectorAll('.d4-menu-item')) as HTMLElement[];
|
|
166
|
-
|
|
167
|
-
highlightedIndex = -1;
|
|
168
|
-
}, 0);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function updateHighlight(newIndex: number): void {
|
|
172
|
-
if (menuItems.length === 0)
|
|
173
|
-
return;
|
|
174
|
-
if (highlightedIndex >= 0 && highlightedIndex < menuItems.length)
|
|
175
|
-
menuItems[highlightedIndex].classList.remove('d4-menu-item-hover');
|
|
176
|
-
highlightedIndex = newIndex;
|
|
177
|
-
if (highlightedIndex >= 0 && highlightedIndex < menuItems.length) {
|
|
178
|
-
menuItems[highlightedIndex].classList.add('d4-menu-item-hover');
|
|
179
|
-
menuItems[highlightedIndex].scrollIntoView({block: 'nearest'});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
inputEl.addEventListener('input', () => { showSuggestions(); });
|
|
184
|
-
|
|
185
|
-
// Handle pasting multiple monomers (space / comma / newline separated)
|
|
186
|
-
inputEl.addEventListener('paste', (e: ClipboardEvent) => {
|
|
187
|
-
const pastedText = e.clipboardData?.getData('text');
|
|
188
|
-
if (!pastedText)
|
|
189
|
-
return;
|
|
190
|
-
|
|
191
|
-
// Split on newlines first, then parse each line (handles parenthesized symbols like hArg(Et,Et))
|
|
192
|
-
const lines = pastedText.split(/[\n\r]+/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
193
|
-
const candidates: string[] = [];
|
|
194
|
-
for (const line of lines) {
|
|
195
|
-
const parsed = parseMonomerSymbolList(line);
|
|
196
|
-
// If parseMonomerSymbolList returned a single token but the line has spaces and no commas,
|
|
197
|
-
// split on spaces as well (e.g. "K P F" -> ["K", "P", "F"])
|
|
198
|
-
if (parsed.length === 1 && line.includes(' ') && !line.includes(','))
|
|
199
|
-
candidates.push(...line.split(/\s+/).map((s) => s.trim()).filter((s) => s.length > 0));
|
|
200
|
-
else
|
|
201
|
-
candidates.push(...parsed);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (candidates.length <= 1)
|
|
205
|
-
return; // Single symbol: let default paste + autocomplete handle it
|
|
206
|
-
|
|
207
|
-
e.preventDefault();
|
|
208
|
-
|
|
209
|
-
for (const candidate of candidates) {
|
|
210
|
-
if (allSymbols.includes(candidate) && !selectedMonomers.includes(candidate))
|
|
211
|
-
selectedMonomers.push(candidate);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
renderTags();
|
|
215
|
-
inputEl.value = '';
|
|
216
|
-
hideMenu();
|
|
217
|
-
inputEl.focus();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
inputEl.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
221
|
-
if (e.key === 'ArrowDown') {
|
|
222
|
-
e.preventDefault();
|
|
223
|
-
if (menuItems.length > 0) {
|
|
224
|
-
const next = (highlightedIndex + 1) % menuItems.length;
|
|
225
|
-
updateHighlight(next);
|
|
226
|
-
}
|
|
227
|
-
} else if (e.key === 'ArrowUp') {
|
|
228
|
-
e.preventDefault();
|
|
229
|
-
if (menuItems.length > 0) {
|
|
230
|
-
const prev = (highlightedIndex - 1 + menuItems.length) % menuItems.length;
|
|
231
|
-
updateHighlight(prev);
|
|
232
|
-
}
|
|
233
|
-
} else if (e.key === 'Enter') {
|
|
234
|
-
e.preventDefault();
|
|
235
|
-
e.stopPropagation();
|
|
236
|
-
if (highlightedIndex >= 0 && highlightedIndex < menuItems.length) {
|
|
237
|
-
menuItems[highlightedIndex].click();
|
|
238
|
-
} else {
|
|
239
|
-
// If input exactly matches a symbol, add it directly
|
|
240
|
-
const val = inputEl.value.trim();
|
|
241
|
-
if (val && allSymbols.includes(val))
|
|
242
|
-
addMonomer(val);
|
|
243
|
-
}
|
|
244
|
-
} else if (e.key === 'Escape') {
|
|
245
|
-
hideMenu();
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
// --- Bulk add: from monomer library ---
|
|
250
|
-
const librarySection = ui.div([], {style: {marginTop: '8px'}});
|
|
251
|
-
if (libHelper) {
|
|
252
|
-
const libraryInput = ui.input.choice('Add from library', {items: [] as string[], nullable: true}) as DG.ChoiceInput<string>;
|
|
253
|
-
const libraryStatus = ui.divText('', {style: {fontSize: '11px', color: 'var(--green-2)', marginLeft: '8px', minHeight: '16px'}});
|
|
254
|
-
|
|
255
|
-
// Load library names asynchronously
|
|
256
|
-
libHelper.getAvaliableLibraryNames().then((libNames) => {
|
|
257
|
-
libraryInput.items = libNames;
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
libraryInput.onChanged.subscribe(async () => {
|
|
261
|
-
const libName = libraryInput.value;
|
|
262
|
-
if (!libName)
|
|
263
|
-
return;
|
|
264
|
-
|
|
265
|
-
libraryStatus.textContent = 'Loading...';
|
|
266
|
-
try {
|
|
267
|
-
const lib = await libHelper.readSingleLibraryByName(libName);
|
|
268
|
-
if (lib) {
|
|
269
|
-
const symbols = lib.getMonomerSymbolsByType(polymerType);
|
|
270
|
-
const before = selectedMonomers.length;
|
|
271
|
-
addMonomers(symbols);
|
|
272
|
-
const added = selectedMonomers.length - before;
|
|
273
|
-
libraryStatus.textContent = `Added ${added} monomer(s) from "${libName}"`;
|
|
274
|
-
} else {
|
|
275
|
-
libraryStatus.textContent = `Library "${libName}" not found`;
|
|
276
|
-
}
|
|
277
|
-
} catch (err: any) {
|
|
278
|
-
libraryStatus.textContent = `Error loading library`;
|
|
279
|
-
}
|
|
280
|
-
// Clear the selector after use
|
|
281
|
-
libraryInput.value = null as any;
|
|
282
|
-
setTimeout(() => { libraryStatus.textContent = ''; }, 4000);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
librarySection.appendChild(ui.divV([libraryInput.root, libraryStatus]));
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// --- Bulk add: from monomer collection ---
|
|
289
|
-
const collectionSection = ui.div([], {style: {marginTop: '4px'}});
|
|
290
|
-
if (libHelper) {
|
|
291
|
-
const collectionInput = ui.input.choice('Add from collection', {items: [] as string[], nullable: true}) as DG.ChoiceInput<string>;
|
|
292
|
-
const collectionStatus = ui.divText('', {style: {fontSize: '11px', color: 'var(--green-2)', marginLeft: '8px', minHeight: '16px'}});
|
|
293
|
-
|
|
294
|
-
// Load collection names asynchronously
|
|
295
|
-
libHelper.listMonomerCollections().then((collectionNames) => {
|
|
296
|
-
collectionInput.items = collectionNames;
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
collectionInput.onChanged.subscribe(async () => {
|
|
300
|
-
const collectionName = collectionInput.value;
|
|
301
|
-
if (!collectionName)
|
|
302
|
-
return;
|
|
303
|
-
|
|
304
|
-
collectionStatus.textContent = 'Loading...';
|
|
305
|
-
try {
|
|
306
|
-
const collection = await libHelper.readMonomerCollection(collectionName);
|
|
307
|
-
const symbols = collection.monomerSymbols ?? [];
|
|
308
|
-
const before = selectedMonomers.length;
|
|
309
|
-
addMonomers(symbols);
|
|
310
|
-
const added = selectedMonomers.length - before;
|
|
311
|
-
const displayName = collectionName.replace(/\.json$/, '');
|
|
312
|
-
collectionStatus.textContent = `Added ${added} monomer(s) from "${displayName}"`;
|
|
313
|
-
} catch (err: any) {
|
|
314
|
-
collectionStatus.textContent = `Error loading collection`;
|
|
315
|
-
}
|
|
316
|
-
// Clear the selector after use
|
|
317
|
-
collectionInput.value = null as any;
|
|
318
|
-
setTimeout(() => { collectionStatus.textContent = ''; }, 4000);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
collectionSection.appendChild(ui.divV([collectionInput.root, collectionStatus]));
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// --- Clear all button ---
|
|
325
|
-
const clearBtn = ui.button('Clear all', () => {
|
|
326
|
-
selectedMonomers.length = 0;
|
|
327
|
-
renderTags();
|
|
328
|
-
});
|
|
329
|
-
clearBtn.style.fontSize = '11px';
|
|
330
|
-
clearBtn.style.marginTop = '4px';
|
|
331
|
-
|
|
332
|
-
renderTags();
|
|
333
|
-
|
|
334
|
-
// --- Dialog layout ---
|
|
335
|
-
const contentDiv = ui.div([
|
|
336
|
-
input.root,
|
|
337
|
-
librarySection,
|
|
338
|
-
collectionSection,
|
|
339
|
-
ui.divH([countLabel, clearBtn], {style: {justifyContent: 'space-between', alignItems: 'center'}}),
|
|
340
|
-
tagsHost,
|
|
341
|
-
], {style: {minWidth: '400px', minHeight: '250px'}});
|
|
342
|
-
|
|
343
|
-
const _dlg = ui.dialog({title: 'Select Monomers', showFooter: true})
|
|
344
|
-
.add(contentDiv)
|
|
345
|
-
.onOK(() => { resolve(selectedMonomers); })
|
|
346
|
-
.onCancel(() => { resolve(null); })
|
|
347
|
-
.show({resizable: true});
|
|
348
|
-
|
|
349
|
-
inputEl.focus();
|
|
350
|
-
setTimeout(() => { showSuggestions(); }, 0);
|
|
351
|
-
});
|
|
352
|
-
}
|