@jupyterlab/extensionmanager 4.0.0-alpha.9 → 4.0.0-beta.0

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/src/widget.tsx ADDED
@@ -0,0 +1,760 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ import { ITranslator, TranslationBundle } from '@jupyterlab/translation';
5
+ import {
6
+ Button,
7
+ FilterBox,
8
+ infoIcon,
9
+ jupyterIcon,
10
+ PanelWithToolbar,
11
+ ReactWidget,
12
+ refreshIcon,
13
+ SidePanel,
14
+ ToolbarButton,
15
+ ToolbarButtonComponent
16
+ } from '@jupyterlab/ui-components';
17
+ import { Message } from '@lumino/messaging';
18
+ import { AccordionLayout, AccordionPanel } from '@lumino/widgets';
19
+ import * as React from 'react';
20
+ import ReactPaginate from 'react-paginate';
21
+ import { Action, IEntry, ListModel } from './model';
22
+
23
+ const BADGE_SIZE = 32;
24
+ const BADGE_QUERY_SIZE = Math.floor(devicePixelRatio * BADGE_SIZE);
25
+
26
+ function getExtensionGitHubUser(entry: IEntry) {
27
+ if (
28
+ entry.homepage_url &&
29
+ entry.homepage_url.startsWith('https://github.com/')
30
+ ) {
31
+ return entry.homepage_url.split('/')[3];
32
+ } else if (
33
+ entry.repository_url &&
34
+ entry.repository_url.startsWith('https://github.com/')
35
+ ) {
36
+ return entry.repository_url.split('/')[3];
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * VDOM for visualizing an extension entry.
43
+ */
44
+ function ListEntry(props: ListEntry.IProperties): React.ReactElement<any> {
45
+ const { canFetch, entry, supportInstallation, trans } = props;
46
+ const flagClasses = [];
47
+ if (entry.status && ['ok', 'warning', 'error'].indexOf(entry.status) !== -1) {
48
+ flagClasses.push(`jp-extensionmanager-entry-${entry.status}`);
49
+ }
50
+ const githubUser = canFetch ? getExtensionGitHubUser(entry) : null;
51
+
52
+ if (!entry.allowed) {
53
+ flagClasses.push(`jp-extensionmanager-entry-should-be-uninstalled`);
54
+ }
55
+
56
+ return (
57
+ <li
58
+ className={`jp-extensionmanager-entry ${flagClasses.join(' ')}`}
59
+ style={{ display: 'flex' }}
60
+ >
61
+ <div style={{ marginRight: '8px' }}>
62
+ {githubUser ? (
63
+ <img
64
+ src={`https://github.com/${githubUser}.png?size=${BADGE_QUERY_SIZE}`}
65
+ style={{ width: '32px', height: '32px' }}
66
+ />
67
+ ) : (
68
+ <div
69
+ style={{ width: `${BADGE_SIZE}px`, height: `${BADGE_SIZE}px` }}
70
+ />
71
+ )}
72
+ </div>
73
+ <div className="jp-extensionmanager-entry-description">
74
+ <div className="jp-extensionmanager-entry-title">
75
+ <div className="jp-extensionmanager-entry-name">
76
+ {entry.homepage_url ? (
77
+ <a
78
+ href={entry.homepage_url}
79
+ target="_blank"
80
+ rel="noopener noreferrer"
81
+ title={trans.__('%1 extension home page', entry.name)}
82
+ >
83
+ {entry.name}
84
+ </a>
85
+ ) : (
86
+ <div>{entry.name}</div>
87
+ )}
88
+ </div>
89
+ <div className="jp-extensionmanager-entry-version">
90
+ <div title={trans.__('Version: %1', entry.installed_version)}>
91
+ {entry.installed_version}
92
+ </div>
93
+ </div>
94
+ {entry.installed && !entry.allowed && (
95
+ <ToolbarButtonComponent
96
+ icon={infoIcon}
97
+ iconLabel={trans.__(
98
+ '%1 extension is not allowed anymore. Please uninstall it immediately or contact your administrator.',
99
+ entry.name
100
+ )}
101
+ onClick={() =>
102
+ window.open(
103
+ 'https://jupyterlab.readthedocs.io/en/latest/user/extensions.html'
104
+ )
105
+ }
106
+ />
107
+ )}
108
+ {entry.approved && (
109
+ <jupyterIcon.react
110
+ className="jp-extensionmanager-is-approved"
111
+ top="1px"
112
+ height="auto"
113
+ width="1em"
114
+ title={trans.__(
115
+ 'This extension is approved by your security team.'
116
+ )}
117
+ />
118
+ )}
119
+ </div>
120
+ <div className="jp-extensionmanager-entry-content">
121
+ <div className="jp-extensionmanager-entry-description">
122
+ {entry.description}
123
+ </div>
124
+ {props.performAction && (
125
+ <div className="jp-extensionmanager-entry-buttons">
126
+ {entry.installed ? (
127
+ <>
128
+ {supportInstallation && (
129
+ <>
130
+ {ListModel.entryHasUpdate(entry) && (
131
+ <Button
132
+ onClick={() => props.performAction!('install', entry)}
133
+ title={trans.__(
134
+ 'Update "%1" to "%2"',
135
+ entry.name,
136
+ entry.latest_version
137
+ )}
138
+ minimal
139
+ small
140
+ >
141
+ {trans.__('Update to %1', entry.latest_version)}
142
+ </Button>
143
+ )}
144
+ <Button
145
+ onClick={() => props.performAction!('uninstall', entry)}
146
+ title={trans.__('Uninstall "%1"', entry.name)}
147
+ minimal
148
+ small
149
+ >
150
+ {trans.__('Uninstall')}
151
+ </Button>
152
+ </>
153
+ )}
154
+ {entry.enabled ? (
155
+ <Button
156
+ onClick={() => props.performAction!('disable', entry)}
157
+ title={trans.__('Disable "%1"', entry.name)}
158
+ minimal
159
+ small
160
+ >
161
+ {trans.__('Disable')}
162
+ </Button>
163
+ ) : (
164
+ <Button
165
+ onClick={() => props.performAction!('enable', entry)}
166
+ title={trans.__('Enable "%1"', entry.name)}
167
+ minimal
168
+ small
169
+ >
170
+ {trans.__('Enable')}
171
+ </Button>
172
+ )}
173
+ </>
174
+ ) : (
175
+ supportInstallation && (
176
+ <Button
177
+ onClick={() => props.performAction!('install', entry)}
178
+ title={trans.__('Install "%1"', entry.name)}
179
+ minimal
180
+ small
181
+ >
182
+ {trans.__('Install')}
183
+ </Button>
184
+ )
185
+ )}
186
+ </div>
187
+ )}
188
+ </div>
189
+ </div>
190
+ </li>
191
+ );
192
+ }
193
+
194
+ /**
195
+ * The namespace for extension entry statics.
196
+ */
197
+ namespace ListEntry {
198
+ export interface IProperties {
199
+ /**
200
+ * Whether thumbnails can be fetched from external webservices or not.
201
+ */
202
+ canFetch: boolean;
203
+
204
+ /**
205
+ * The entry to visualize.
206
+ */
207
+ entry: IEntry;
208
+
209
+ /**
210
+ * Whether the extension can be (un-)install or not.
211
+ */
212
+ supportInstallation: boolean;
213
+
214
+ /**
215
+ * Callback to use for performing an action on the entry.
216
+ *
217
+ * Not provided if actions are not allowed.
218
+ */
219
+ performAction?: (action: Action, entry: IEntry) => void;
220
+
221
+ /**
222
+ * The language translator.
223
+ */
224
+ trans: TranslationBundle;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * List view widget for extensions
230
+ */
231
+ function ListView(props: ListView.IProperties): React.ReactElement<any> {
232
+ const { canFetch, performAction, supportInstallation, trans } = props;
233
+
234
+ return (
235
+ <div className="jp-extensionmanager-listview-wrapper">
236
+ {props.entries.length > 0 ? (
237
+ <ul className="jp-extensionmanager-listview">
238
+ {props.entries.map(entry => (
239
+ <ListEntry
240
+ key={entry.name}
241
+ canFetch={canFetch}
242
+ entry={entry}
243
+ performAction={performAction}
244
+ supportInstallation={supportInstallation}
245
+ trans={trans}
246
+ />
247
+ ))}
248
+ </ul>
249
+ ) : (
250
+ <div key="message" className="jp-extensionmanager-listview-message">
251
+ {trans.__('No entries')}
252
+ </div>
253
+ )}
254
+ {props.numPages > 1 && (
255
+ <div className="jp-extensionmanager-pagination">
256
+ <ReactPaginate
257
+ previousLabel={'<'}
258
+ nextLabel={'>'}
259
+ breakLabel="..."
260
+ breakClassName={'break-me'}
261
+ initialPage={(props.initialPage ?? 1) - 1}
262
+ pageCount={props.numPages}
263
+ marginPagesDisplayed={2}
264
+ pageRangeDisplayed={3}
265
+ onPageChange={(data: { selected: number }) =>
266
+ props.onPage(data.selected + 1)
267
+ }
268
+ containerClassName={'pagination'}
269
+ activeClassName={'active'}
270
+ />
271
+ </div>
272
+ )}
273
+ </div>
274
+ );
275
+ }
276
+
277
+ /**
278
+ * The namespace for list view widget statics.
279
+ */
280
+ namespace ListView {
281
+ export interface IProperties {
282
+ /**
283
+ * Whether thumbnails can be fetched from external webservices or not.
284
+ */
285
+ canFetch: boolean;
286
+
287
+ /**
288
+ * The extension entries to display.
289
+ */
290
+ entries: ReadonlyArray<IEntry>;
291
+
292
+ /**
293
+ * Active page
294
+ */
295
+ initialPage?: number;
296
+
297
+ /**
298
+ * The number of pages that can be viewed via pagination.
299
+ */
300
+ numPages: number;
301
+
302
+ /**
303
+ * Whether the extension can be (un-)install or not.
304
+ */
305
+ supportInstallation: boolean;
306
+
307
+ /**
308
+ * The language translator.
309
+ */
310
+ trans: TranslationBundle;
311
+
312
+ /**
313
+ * The callback to use for changing the page
314
+ */
315
+ onPage: (page: number) => void;
316
+
317
+ /**
318
+ * Callback to use for performing an action on an entry.
319
+ *
320
+ * Not provided if actions are not allowed.
321
+ */
322
+ performAction?: (action: Action, entry: IEntry) => void;
323
+ }
324
+ }
325
+
326
+ function ErrorMessage(props: ErrorMessage.IProperties) {
327
+ return <div className="jp-extensionmanager-error">{props.children}</div>;
328
+ }
329
+
330
+ namespace ErrorMessage {
331
+ export interface IProperties {
332
+ children: React.ReactNode;
333
+ }
334
+ }
335
+
336
+ class Header extends ReactWidget {
337
+ constructor(
338
+ protected model: ListModel,
339
+ protected trans: TranslationBundle,
340
+ protected searchInputRef: React.RefObject<HTMLInputElement>
341
+ ) {
342
+ super();
343
+ model.stateChanged.connect(this.update, this);
344
+ this.addClass('jp-extensionmanager-header');
345
+ }
346
+
347
+ render(): JSX.Element {
348
+ return (
349
+ <>
350
+ <div className="jp-extensionmanager-title">
351
+ <span>{this.trans.__('%1 Manager', this.model.name)}</span>
352
+ {this.model.installPath && (
353
+ <infoIcon.react
354
+ className="jp-extensionmanager-path"
355
+ tag="span"
356
+ title={this.trans.__(
357
+ 'Extension installation path: %1',
358
+ this.model.installPath
359
+ )}
360
+ ></infoIcon.react>
361
+ )}
362
+ </div>
363
+ <FilterBox
364
+ placeholder={this.trans.__('Search')}
365
+ disabled={!this.model.isDisclaimed}
366
+ updateFilter={(fn, query) => {
367
+ this.model.query = query ?? '';
368
+ }}
369
+ useFuzzyFilter={false}
370
+ inputRef={this.searchInputRef}
371
+ />
372
+
373
+ <div
374
+ className={`jp-extensionmanager-pending ${
375
+ this.model.hasPendingActions() ? 'jp-mod-hasPending' : ''
376
+ }`}
377
+ />
378
+ {this.model.actionError && (
379
+ <ErrorMessage>
380
+ <p>{this.trans.__('Error when performing an action.')}</p>
381
+ <p>{this.trans.__('Reason given:')}</p>
382
+ <pre>{this.model.actionError}</pre>
383
+ </ErrorMessage>
384
+ )}
385
+ </>
386
+ );
387
+ }
388
+ }
389
+
390
+ class Warning extends ReactWidget {
391
+ constructor(protected model: ListModel, protected trans: TranslationBundle) {
392
+ super();
393
+ this.addClass('jp-extensionmanager-disclaimer');
394
+ model.stateChanged.connect(this.update, this);
395
+ }
396
+
397
+ render(): JSX.Element {
398
+ return (
399
+ <>
400
+ <p>
401
+ {this.trans
402
+ .__(`The JupyterLab development team is excited to have a robust
403
+ third-party extension community. However, we do not review
404
+ third-party extensions, and some extensions may introduce security
405
+ risks or contain malicious code that runs on your machine. Moreover in order
406
+ to work, this panel needs to fetch data from web services. Do you agree to
407
+ activate this feature?`)}
408
+ <br />
409
+ <a
410
+ href="https://jupyterlab.readthedocs.io/en/latest/privacy_policies.html"
411
+ target="_blank"
412
+ rel="noreferrer"
413
+ >
414
+ {this.trans.__('Please read the privacy policy.')}
415
+ </a>
416
+ </p>
417
+ {this.model.isDisclaimed ? (
418
+ <Button
419
+ className="jp-extensionmanager-disclaimer-disable"
420
+ onClick={(e: React.MouseEvent<Element, MouseEvent>) => {
421
+ this.model.isDisclaimed = false;
422
+ }}
423
+ title={this.trans.__('This will withdraw your consent.')}
424
+ >
425
+ {this.trans.__('No')}
426
+ </Button>
427
+ ) : (
428
+ <div>
429
+ <Button
430
+ className="jp-extensionmanager-disclaimer-enable"
431
+ onClick={() => {
432
+ this.model.isDisclaimed = true;
433
+ }}
434
+ >
435
+ {this.trans.__('Yes')}
436
+ </Button>
437
+ <Button
438
+ className="jp-extensionmanager-disclaimer-disable"
439
+ onClick={() => {
440
+ this.model.isEnabled = false;
441
+ }}
442
+ title={this.trans.__(
443
+ 'This will disable the extension manager panel; including the listing of installed extension.'
444
+ )}
445
+ >
446
+ {this.trans.__('No, disable')}
447
+ </Button>
448
+ </div>
449
+ )}
450
+ </>
451
+ );
452
+ }
453
+ }
454
+
455
+ class InstalledList extends ReactWidget {
456
+ constructor(protected model: ListModel, protected trans: TranslationBundle) {
457
+ super();
458
+ model.stateChanged.connect(this.update, this);
459
+ }
460
+
461
+ render(): JSX.Element {
462
+ return (
463
+ <>
464
+ {this.model.installedError !== null ? (
465
+ <ErrorMessage>
466
+ {`Error querying installed extensions${
467
+ this.model.installedError ? `: ${this.model.installedError}` : '.'
468
+ }`}
469
+ </ErrorMessage>
470
+ ) : this.model.isLoadingInstalledExtensions ? (
471
+ <div className="jp-extensionmanager-loader">
472
+ {this.trans.__('Updating extensions list…')}
473
+ </div>
474
+ ) : (
475
+ <ListView
476
+ canFetch={this.model.isDisclaimed}
477
+ entries={this.model.installed.filter(pkg =>
478
+ new RegExp(this.model.query.toLowerCase()).test(pkg.name)
479
+ )}
480
+ numPages={1}
481
+ trans={this.trans}
482
+ onPage={value => {
483
+ /* no-op */
484
+ }}
485
+ performAction={
486
+ this.model.isDisclaimed ? this.onAction.bind(this) : null
487
+ }
488
+ supportInstallation={
489
+ this.model.canInstall && this.model.isDisclaimed
490
+ }
491
+ />
492
+ )}
493
+ </>
494
+ );
495
+ }
496
+
497
+ /**
498
+ * Callback handler for when the user wants to perform an action on an extension.
499
+ *
500
+ * @param action The action to perform.
501
+ * @param entry The entry to perform the action on.
502
+ */
503
+ onAction(action: Action, entry: IEntry): Promise<void> {
504
+ switch (action) {
505
+ case 'install':
506
+ return this.model.install(entry);
507
+ case 'uninstall':
508
+ return this.model.uninstall(entry);
509
+ case 'enable':
510
+ return this.model.enable(entry);
511
+ case 'disable':
512
+ return this.model.disable(entry);
513
+ default:
514
+ throw new Error(`Invalid action: ${action}`);
515
+ }
516
+ }
517
+ }
518
+
519
+ class SearchResult extends ReactWidget {
520
+ constructor(protected model: ListModel, protected trans: TranslationBundle) {
521
+ super();
522
+ model.stateChanged.connect(this.update, this);
523
+ }
524
+
525
+ /**
526
+ * Callback handler for the user changes the page of the search result pagination.
527
+ *
528
+ * @param value The pagination page number.
529
+ */
530
+ onPage(value: number): void {
531
+ this.model.page = value;
532
+ }
533
+
534
+ /**
535
+ * Callback handler for when the user wants to perform an action on an extension.
536
+ *
537
+ * @param action The action to perform.
538
+ * @param entry The entry to perform the action on.
539
+ */
540
+ onAction(action: Action, entry: IEntry): Promise<void> {
541
+ switch (action) {
542
+ case 'install':
543
+ return this.model.install(entry);
544
+ case 'uninstall':
545
+ return this.model.uninstall(entry);
546
+ case 'enable':
547
+ return this.model.enable(entry);
548
+ case 'disable':
549
+ return this.model.disable(entry);
550
+ default:
551
+ throw new Error(`Invalid action: ${action}`);
552
+ }
553
+ }
554
+
555
+ render(): JSX.Element {
556
+ return (
557
+ <>
558
+ {this.model.searchError !== null ? (
559
+ <ErrorMessage>
560
+ {`Error searching for extensions${
561
+ this.model.searchError ? `: ${this.model.searchError}` : '.'
562
+ }`}
563
+ </ErrorMessage>
564
+ ) : this.model.isSearching ? (
565
+ <div className="jp-extensionmanager-loader">
566
+ {this.trans.__('Updating extensions list…')}
567
+ </div>
568
+ ) : (
569
+ <ListView
570
+ canFetch={this.model.isDisclaimed}
571
+ entries={this.model.searchResult}
572
+ initialPage={this.model.page}
573
+ numPages={this.model.lastPage}
574
+ onPage={value => {
575
+ this.onPage(value);
576
+ }}
577
+ performAction={
578
+ this.model.isDisclaimed ? this.onAction.bind(this) : null
579
+ }
580
+ supportInstallation={
581
+ this.model.canInstall && this.model.isDisclaimed
582
+ }
583
+ trans={this.trans}
584
+ />
585
+ )}
586
+ </>
587
+ );
588
+ }
589
+
590
+ update(): void {
591
+ this.title.label = this.model.query
592
+ ? this.trans.__('Search Results')
593
+ : this.trans.__('Discover');
594
+ super.update();
595
+ }
596
+ }
597
+
598
+ export namespace ExtensionsPanel {
599
+ export interface IOptions {
600
+ model: ListModel;
601
+ translator: ITranslator;
602
+ }
603
+ }
604
+
605
+ export class ExtensionsPanel extends SidePanel {
606
+ constructor(options: ExtensionsPanel.IOptions) {
607
+ const { model, translator } = options;
608
+ super({ translator });
609
+ this.model = model;
610
+ this._searchInputRef = React.createRef<HTMLInputElement>();
611
+ this.addClass('jp-extensionmanager-view');
612
+
613
+ this.trans = translator.load('jupyterlab');
614
+
615
+ this.header.addWidget(new Header(model, this.trans, this._searchInputRef));
616
+
617
+ const warning = new Warning(model, this.trans);
618
+ warning.title.label = this.trans.__('Warning');
619
+
620
+ this.addWidget(warning);
621
+
622
+ const installed = new PanelWithToolbar();
623
+ installed.addClass('jp-extensionmanager-installedlist');
624
+ installed.title.label = this.trans.__('Installed');
625
+
626
+ installed.toolbar.addItem(
627
+ 'refresh',
628
+ new ToolbarButton({
629
+ icon: refreshIcon,
630
+ onClick: () => {
631
+ model.refreshInstalled(true).catch(reason => {
632
+ console.error(
633
+ `Failed to refresh the installed extensions list:\n${reason}`
634
+ );
635
+ });
636
+ },
637
+ tooltip: this.trans.__('Refresh extensions list')
638
+ })
639
+ );
640
+
641
+ installed.addWidget(new InstalledList(model, this.trans));
642
+
643
+ this.addWidget(installed);
644
+
645
+ if (this.model.canInstall) {
646
+ const searchResults = new SearchResult(model, this.trans);
647
+ searchResults.addClass('jp-extensionmanager-searchresults');
648
+ this.addWidget(searchResults);
649
+ }
650
+
651
+ this._wasDisclaimed = this.model.isDisclaimed;
652
+ if (this.model.isDisclaimed) {
653
+ (this.content as AccordionPanel).collapse(0);
654
+ (this.content.layout as AccordionLayout).setRelativeSizes([0, 1, 1]);
655
+ } else {
656
+ // If warning is not disclaimed expand only the warning panel
657
+ (this.content as AccordionPanel).expand(0);
658
+ (this.content as AccordionPanel).collapse(1);
659
+ (this.content as AccordionPanel).collapse(2);
660
+ }
661
+
662
+ this.model.stateChanged.connect(this._onStateChanged, this);
663
+ }
664
+
665
+ /**
666
+ * Dispose of the widget and its descendant widgets.
667
+ */
668
+ dispose(): void {
669
+ if (this.isDisposed) {
670
+ return;
671
+ }
672
+ this.model.stateChanged.disconnect(this._onStateChanged, this);
673
+ super.dispose();
674
+ }
675
+
676
+ /**
677
+ * Handle the DOM events for the extension manager search bar.
678
+ *
679
+ * @param event - The DOM event sent to the extension manager search bar.
680
+ *
681
+ * #### Notes
682
+ * This method implements the DOM `EventListener` interface and is
683
+ * called in response to events on the search bar's DOM node.
684
+ * It should not be called directly by user code.
685
+ */
686
+ handleEvent(event: Event): void {
687
+ switch (event.type) {
688
+ case 'focus':
689
+ case 'blur':
690
+ this._toggleFocused();
691
+ break;
692
+ default:
693
+ break;
694
+ }
695
+ }
696
+
697
+ /**
698
+ * A message handler invoked on a `'before-attach'` message.
699
+ */
700
+ protected onBeforeAttach(msg: Message): void {
701
+ this.node.addEventListener('focus', this, true);
702
+ this.node.addEventListener('blur', this, true);
703
+ super.onBeforeAttach(msg);
704
+ }
705
+
706
+ protected onBeforeShow(msg: Message): void {
707
+ if (!this._wasInitialized) {
708
+ this._wasInitialized = true;
709
+ this.model.refreshInstalled().catch(reason => {
710
+ console.log(`Failed to refresh installed extension list:\n${reason}`);
711
+ });
712
+ }
713
+ }
714
+
715
+ /**
716
+ * A message handler invoked on an `'after-detach'` message.
717
+ */
718
+ protected onAfterDetach(msg: Message): void {
719
+ super.onAfterDetach(msg);
720
+ this.node.removeEventListener('focus', this, true);
721
+ this.node.removeEventListener('blur', this, true);
722
+ }
723
+
724
+ /**
725
+ * A message handler invoked on an `'activate-request'` message.
726
+ */
727
+ protected onActivateRequest(msg: Message): void {
728
+ if (this.isAttached) {
729
+ const input = this._searchInputRef.current;
730
+ if (input) {
731
+ input.focus();
732
+ input.select();
733
+ }
734
+ }
735
+ super.onActivateRequest(msg);
736
+ }
737
+
738
+ private _onStateChanged(): void {
739
+ if (!this._wasDisclaimed && this.model.isDisclaimed) {
740
+ (this.content as AccordionPanel).collapse(0);
741
+ (this.content as AccordionPanel).expand(1);
742
+ (this.content as AccordionPanel).expand(2);
743
+ }
744
+ this._wasDisclaimed = this.model.isDisclaimed;
745
+ }
746
+
747
+ /**
748
+ * Toggle the focused modifier based on the input node focus state.
749
+ */
750
+ private _toggleFocused(): void {
751
+ const focused = document.activeElement === this._searchInputRef.current;
752
+ this.toggleClass('lm-mod-focused', focused);
753
+ }
754
+
755
+ protected model: ListModel;
756
+ protected trans: TranslationBundle;
757
+ private _searchInputRef: React.RefObject<HTMLInputElement>;
758
+ private _wasInitialized = false;
759
+ private _wasDisclaimed = true;
760
+ }