@lvce-editor/extension-search-view 1.0.0 → 1.2.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.
@@ -18,6 +18,134 @@ const execute = (command, ...args) => {
18
18
  return fn(...args);
19
19
  };
20
20
 
21
+ const handleError = async (error, notify = true, prefix = '') => {
22
+ console.error(error);
23
+ };
24
+
25
+ const emptyObject = {};
26
+ const RE_PLACEHOLDER = /\{(PH\d+)\}/g;
27
+ const i18nString = (key, placeholders = emptyObject) => {
28
+ if (placeholders === emptyObject) {
29
+ return key;
30
+ }
31
+ const replacer = (match, rest) => {
32
+ // @ts-ignore
33
+ return placeholders[rest];
34
+ };
35
+ return key.replaceAll(RE_PLACEHOLDER, replacer);
36
+ };
37
+
38
+ /**
39
+ * @enum {string}
40
+ */
41
+ const UiStrings = {
42
+ NoExtensionsFound: 'No extensions found.',
43
+ Filter: 'Filter',
44
+ Refresh: 'Refresh',
45
+ ClearExtensionSearchResults: 'Clear extension search results',
46
+ Enable: 'Enable',
47
+ Disable: 'Disable',
48
+ Uninstall: 'Uninstall',
49
+ InstallAnotherVersion: 'Install Another Version',
50
+ SearchExtensionsInMarketplace: 'Search Extensions in Marketplace',
51
+ ViewsAndMoreActions: 'Views and more Actions...',
52
+ Extensions: 'Extensions',
53
+ Installed: 'Installed'
54
+ };
55
+ const noExtensionsFound = () => {
56
+ return i18nString(UiStrings.NoExtensionsFound);
57
+ };
58
+ const filter = () => {
59
+ return i18nString(UiStrings.Filter);
60
+ };
61
+ const extensions = () => {
62
+ return i18nString(UiStrings.Extensions);
63
+ };
64
+ const clearExtensionSearchResults = () => {
65
+ return i18nString(UiStrings.ClearExtensionSearchResults);
66
+ };
67
+ const searchExtensionsInMarketPlace = () => {
68
+ return i18nString(UiStrings.SearchExtensionsInMarketplace);
69
+ };
70
+
71
+ const getFinalDeltaY = (height, itemHeight, itemsLength) => {
72
+ const contentHeight = itemsLength * itemHeight;
73
+ const finalDeltaY = Math.max(contentHeight - height, 0);
74
+ return finalDeltaY;
75
+ };
76
+
77
+ let AssertionError$1 = class AssertionError extends Error {
78
+ constructor(message) {
79
+ super(message);
80
+ this.name = 'AssertionError';
81
+ }
82
+ };
83
+ const getType$2 = value => {
84
+ switch (typeof value) {
85
+ case 'number':
86
+ return 'number';
87
+ case 'function':
88
+ return 'function';
89
+ case 'string':
90
+ return 'string';
91
+ case 'object':
92
+ if (value === null) {
93
+ return 'null';
94
+ }
95
+ if (Array.isArray(value)) {
96
+ return 'array';
97
+ }
98
+ return 'object';
99
+ case 'boolean':
100
+ return 'boolean';
101
+ default:
102
+ return 'unknown';
103
+ }
104
+ };
105
+ const number$1 = value => {
106
+ const type = getType$2(value);
107
+ if (type !== 'number') {
108
+ throw new AssertionError$1('expected value to be of type number');
109
+ }
110
+ };
111
+
112
+ const getListHeight$1 = (itemsLength, itemHeight, maxHeight) => {
113
+ number$1(itemsLength);
114
+ number$1(itemHeight);
115
+ number$1(maxHeight);
116
+ if (itemsLength === 0) {
117
+ return itemHeight;
118
+ }
119
+ const totalHeight = itemsLength * itemHeight;
120
+ return Math.min(totalHeight, maxHeight);
121
+ };
122
+
123
+ // TODO optimize this function to return the minimum number
124
+ // of visible items needed, e.g. when not scrolled 5 items with
125
+ // 20px fill 100px but when scrolled 6 items are needed
126
+ const getNumberOfVisibleItems = (listHeight, itemHeight) => {
127
+ return Math.ceil(listHeight / itemHeight) + 1;
128
+ };
129
+
130
+ /**
131
+ *
132
+ * @param {number} size
133
+ * @param {number} contentSize
134
+ * @param {number} minimumSliderSize
135
+ * @returns
136
+ */
137
+ const getScrollBarSize = (size, contentSize, minimumSliderSize) => {
138
+ if (size >= contentSize) {
139
+ return 0;
140
+ }
141
+ return Math.max(Math.round(size ** 2 / contentSize), minimumSliderSize);
142
+ };
143
+ const getScrollBarOffset = (delta, finalDelta, size, scrollBarSize) => {
144
+ const scrollBarOffset = delta / finalDelta * (size - scrollBarSize);
145
+ return scrollBarOffset;
146
+ };
147
+ const getScrollBarY = getScrollBarOffset;
148
+
21
149
  const Installed = '@installed';
22
150
  const Enabled = '@enabled';
23
151
  const Disabled = '@disabled';
@@ -129,17 +257,6 @@ const getId = extension => {
129
257
  return extension.id;
130
258
  };
131
259
 
132
- const toSorted = (array, compare) => {
133
- return [...array].sort(compare);
134
- };
135
-
136
- const compareExtension = (extensionA, extensionB) => {
137
- return extensionA.name.localeCompare(extensionB.name) || extensionA.id.localeCompare(extensionB.id);
138
- };
139
- const sortExtensions = extensions => {
140
- return toSorted(extensions, compareExtension);
141
- };
142
-
143
260
  const matchesParsedValue = (extension, parsedValue) => {
144
261
  if (extension && typeof extension.name === 'string') {
145
262
  const extensionNameLower = extension.name.toLowerCase();
@@ -151,6 +268,19 @@ const matchesParsedValue = (extension, parsedValue) => {
151
268
  }
152
269
  return false;
153
270
  };
271
+
272
+ const toSorted = (array, compare) => {
273
+ return [...array].sort(compare);
274
+ };
275
+
276
+ const compareExtension = (extensionA, extensionB) => {
277
+ return extensionA.name.localeCompare(extensionB.name) || extensionA.id.localeCompare(extensionB.id);
278
+ };
279
+
280
+ const sortExtensions = extensions => {
281
+ return toSorted(extensions, compareExtension);
282
+ };
283
+
154
284
  const getExtensions = async (extensions, parsedValue) => {
155
285
  const filteredExtensions = [];
156
286
  for (const extension of extensions) {
@@ -234,8 +364,395 @@ const searchExtensions = async (extensions, value) => {
234
364
  }
235
365
  };
236
366
 
367
+ // TODO debounce
368
+ const handleInput = async (state, value) => {
369
+ try {
370
+ const {
371
+ allExtensions,
372
+ itemHeight,
373
+ minimumSliderSize,
374
+ height
375
+ } = state;
376
+ // TODO cancel ongoing requests
377
+ // TODO handle errors
378
+ const items = await searchExtensions(allExtensions, value);
379
+ if (items.length === 0) {
380
+ return {
381
+ ...state,
382
+ items,
383
+ minLineY: 0,
384
+ deltaY: 0,
385
+ allExtensions,
386
+ maxLineY: 0,
387
+ scrollBarHeight: 0,
388
+ finalDeltaY: 0,
389
+ message: noExtensionsFound(),
390
+ searchValue: value,
391
+ placeholder: searchExtensionsInMarketPlace()
392
+ };
393
+ }
394
+ // @ts-ignore
395
+ const listHeight = getListHeight$1(state);
396
+ const total = items.length;
397
+ const contentHeight = total * itemHeight;
398
+ const scrollBarHeight = getScrollBarSize(height, contentHeight, minimumSliderSize);
399
+ const numberOfVisible = getNumberOfVisibleItems(listHeight, itemHeight);
400
+ const maxLineY = Math.min(numberOfVisible, total);
401
+ const finalDeltaY = getFinalDeltaY(listHeight, itemHeight, total);
402
+ return {
403
+ ...state,
404
+ items,
405
+ minLineY: 0,
406
+ deltaY: 0,
407
+ allExtensions,
408
+ maxLineY,
409
+ scrollBarHeight,
410
+ finalDeltaY,
411
+ message: '',
412
+ searchValue: value,
413
+ placeholder: searchExtensionsInMarketPlace()
414
+ };
415
+
416
+ // TODO handle out of order responses (a bit complicated)
417
+ // for now just assume everything comes back in order
418
+ } catch (error) {
419
+ await handleError(error);
420
+ return {
421
+ ...state,
422
+ searchValue: value,
423
+ message: `${error}`
424
+ };
425
+ }
426
+ };
427
+
428
+ const clearSearchResults = state => {
429
+ return handleInput(state, '');
430
+ };
431
+
432
+ const Button = 1;
433
+
434
+ const ExtensionActions = 'ExtensionActions';
435
+ const ExtensionActive = 'ExtensionActive';
436
+ const ExtensionHeader = 'ExtensionHeader';
437
+ const ExtensionListItem = 'ExtensionListItem';
438
+ const ExtensionListItemAuthorName = 'ExtensionListItemAuthorName';
439
+ const ExtensionListItemDescription = 'ExtensionListItemDescription';
440
+ const ExtensionListItemDetail = 'ExtensionListItemDetail';
441
+ const ExtensionListItemFooter = 'ExtensionListItemFooter';
442
+ const ExtensionListItemIcon = 'ExtensionListItemIcon';
443
+ const ExtensionListItemName = 'ExtensionListItemName';
444
+ const ListItems = 'ListItems';
445
+ const MultilineInputBox = 'MultilineInputBox';
446
+ const ScrollBarThumb = 'ScrollBarThumb';
447
+ const ScrollBarThumbActive = 'ScrollBarThumbActive';
448
+ const SearchField = 'SearchField';
449
+ const SearchFieldButtons = 'SearchFieldButtons';
450
+ const SearchFieldContainer = 'SearchFieldContainer';
451
+
452
+ const CheckBox = 'checkbox';
453
+ const List = 'list';
454
+ const ListItem = 'listitem';
455
+ const None = 'none';
456
+
457
+ const Div = 4;
458
+ const Text = 12;
459
+ const Img = 17;
460
+ const TextArea = 62;
461
+
462
+ const getSearchFieldButtonVirtualDom = button => {
463
+ const {
464
+ icon,
465
+ checked,
466
+ title
467
+ } = button;
468
+ return [{
469
+ type: Div,
470
+ className: `SearchFieldButton ${checked ? 'SearchFieldButtonChecked' : ''}`,
471
+ title,
472
+ role: CheckBox,
473
+ ariaChecked: checked,
474
+ tabIndex: 0,
475
+ childCount: 1
476
+ }, {
477
+ type: Div,
478
+ className: `MaskIcon ${icon}`,
479
+ childCount: 0
480
+ }];
481
+ };
482
+
483
+ const getSearchFieldVirtualDom = (name, placeholder, onInput, insideButtons, outsideButtons, onFocus = '') => {
484
+ const dom = [{
485
+ type: Div,
486
+ className: SearchField,
487
+ role: None,
488
+ childCount: 2
489
+ }, {
490
+ type: TextArea,
491
+ className: MultilineInputBox,
492
+ spellcheck: false,
493
+ autocapitalize: 'off',
494
+ autocorrect: 'off',
495
+ placeholder,
496
+ name,
497
+ onInput,
498
+ onFocus,
499
+ childCount: 0
500
+ }, {
501
+ type: Div,
502
+ className: SearchFieldButtons,
503
+ childCount: insideButtons.length
504
+ }, ...insideButtons.flatMap(getSearchFieldButtonVirtualDom)];
505
+ if (outsideButtons.length > 0) {
506
+ dom.unshift({
507
+ type: Div,
508
+ className: SearchFieldContainer,
509
+ role: None,
510
+ childCount: 1 + outsideButtons.length
511
+ });
512
+ dom.push(...outsideButtons.flatMap(getSearchFieldButtonVirtualDom));
513
+ }
514
+ return dom;
515
+ };
516
+
517
+ const getExtensionHeaderVirtualDom = (placeholder, actions) => {
518
+ return [{
519
+ type: Div,
520
+ className: ExtensionHeader,
521
+ childCount: 1
522
+ }, ...getSearchFieldVirtualDom('extensions', placeholder, 'handleExtensionsInput', actions, [])];
523
+ };
524
+
525
+ const HandleContextMenu = 'handleContextMenu';
526
+ const HandlePointerDown = 'handlePointerDown';
527
+ const HandleTouchStart = 'handleTouchStart';
528
+ const HandleWheel = 'handleWheel';
529
+
530
+ const Extension = 'Extension';
531
+
532
+ const text = data => {
533
+ return {
534
+ type: Text,
535
+ text: data,
536
+ childCount: 0
537
+ };
538
+ };
539
+
540
+ const listItemDetail = {
541
+ type: Div,
542
+ className: ExtensionListItemDetail,
543
+ childCount: 3
544
+ };
545
+ const listItemName = {
546
+ type: Div,
547
+ className: ExtensionListItemName,
548
+ childCount: 1
549
+ };
550
+ const listItemDescription = {
551
+ type: Div,
552
+ className: ExtensionListItemDescription,
553
+ childCount: 1
554
+ };
555
+ const listItemFooter = {
556
+ type: Div,
557
+ className: ExtensionListItemFooter,
558
+ childCount: 2
559
+ };
560
+ const listItemAuthorName = {
561
+ type: Div,
562
+ className: ExtensionListItemAuthorName,
563
+ childCount: 1
564
+ };
565
+ const getExtensionListItemVirtualDom = extension => {
566
+ const {
567
+ posInSet,
568
+ setSize,
569
+ top,
570
+ icon,
571
+ name,
572
+ description,
573
+ publisher,
574
+ focused
575
+ } = extension;
576
+ const dom = [{
577
+ type: Div,
578
+ role: ListItem,
579
+ ariaRoleDescription: Extension,
580
+ className: ExtensionListItem,
581
+ ariaPosInSet: posInSet,
582
+ ariaSetSize: setSize,
583
+ top,
584
+ childCount: 2
585
+ }, {
586
+ type: Img,
587
+ src: icon,
588
+ className: ExtensionListItemIcon,
589
+ role: None,
590
+ childCount: 0
591
+ }, listItemDetail, listItemName, text(name), listItemDescription, text(description), listItemFooter, listItemAuthorName, text(publisher), {
592
+ type: Div,
593
+ className: ExtensionActions,
594
+ childCount: 0
595
+ }];
596
+ if (focused) {
597
+ dom[0].id = 'ExtensionActive';
598
+ dom[0].className += ' ' + ExtensionActive;
599
+ }
600
+ return dom;
601
+ };
602
+
603
+ const getExtensionsListVirtualDom = visibleExtensions => {
604
+ const dom = [{
605
+ type: Div,
606
+ className: ListItems,
607
+ tabIndex: 0,
608
+ ariaLabel: extensions(),
609
+ role: List,
610
+ oncontextmenu: HandleContextMenu,
611
+ onpointerdown: HandlePointerDown,
612
+ ontouchstart: HandleTouchStart,
613
+ onwheelpassive: HandleWheel,
614
+ childCount: visibleExtensions.length
615
+ }, ...visibleExtensions.flatMap(getExtensionListItemVirtualDom)];
616
+ return dom;
617
+ };
618
+
619
+ const getExtensionsVirtualDom = visibleExtensions => {
620
+ const dom = getExtensionsListVirtualDom(visibleExtensions);
621
+ // TODO
622
+ return dom;
623
+ };
624
+
625
+ const getVisibleItem = (item, setSize, itemHeight, minLineY, relative, i, focusedIndex) => {
626
+ return {
627
+ ...item,
628
+ setSize,
629
+ posInSet: i + 1,
630
+ top: (i - minLineY) * itemHeight - relative,
631
+ focused: i === focusedIndex
632
+ };
633
+ };
634
+ const getVisible = state => {
635
+ const {
636
+ minLineY,
637
+ maxLineY,
638
+ items,
639
+ itemHeight,
640
+ deltaY,
641
+ focusedIndex
642
+ } = state;
643
+ const setSize = items.length;
644
+ const visible = [];
645
+ const relative = deltaY % itemHeight;
646
+ for (let i = minLineY; i < maxLineY; i++) {
647
+ const item = items[i];
648
+ visible.push(getVisibleItem(item, setSize, itemHeight, minLineY, relative, i, focusedIndex));
649
+ }
650
+ return visible;
651
+ };
652
+
653
+ const ClearAll = 'ClearAll';
654
+ const Filter = 'Filter';
655
+
656
+ const px = value => {
657
+ return `${value}px`;
658
+ };
659
+ const position = (x, y) => {
660
+ return `${x}px ${y}px`;
661
+ };
662
+
663
+ const SetMessage = 'setMessage';
664
+ const SetScrollBar = 'setScrollBar';
665
+ const SetSearchValue = 'setSearchValue';
666
+
667
+ const getListHeight = state => {
668
+ const {
669
+ height,
670
+ headerHeight
671
+ } = state;
672
+ return height - headerHeight;
673
+ };
674
+ const renderExtensions = {
675
+ isEqual(oldState, newState) {
676
+ return oldState.items === newState.items && oldState.minLineY === newState.minLineY && oldState.maxLineY === newState.maxLineY && oldState.deltaY === newState.deltaY && oldState.focusedIndex === newState.focusedIndex;
677
+ },
678
+ apply(oldState, newState) {
679
+ // TODO render extensions incrementally when scrolling
680
+ const visibleExtensions = getVisible(newState);
681
+ const dom = getExtensionsVirtualDom(visibleExtensions);
682
+ return ['setExtensionsDom', dom];
683
+ }
684
+ };
685
+ const renderScrollBar = {
686
+ isEqual(oldState, newState) {
687
+ return oldState.negativeMargin === newState.negativeMargin && oldState.deltaY === newState.deltaY && oldState.height === newState.height && oldState.finalDeltaY === newState.finalDeltaY && oldState.items.length === newState.items.length && oldState.scrollBarActive === newState.scrollBarActive;
688
+ },
689
+ apply(oldState, newState) {
690
+ // @ts-ignore
691
+ const listHeight = getListHeight(newState);
692
+ const total = newState.items.length;
693
+ const contentHeight = total * newState.itemHeight;
694
+ const scrollBarHeight = getScrollBarSize(listHeight, contentHeight, newState.minimumSliderSize);
695
+ const scrollBarY = getScrollBarY(newState.deltaY, newState.finalDeltaY, newState.height - newState.headerHeight, scrollBarHeight);
696
+ const roundedScrollBarY = Math.round(scrollBarY);
697
+ const heightString = px(scrollBarHeight);
698
+ const translateString = position(0, roundedScrollBarY);
699
+ let className = ScrollBarThumb;
700
+ if (newState.scrollBarActive) {
701
+ className += ' ' + ScrollBarThumbActive;
702
+ }
703
+ return [/* method */SetScrollBar, translateString, heightString, className];
704
+ }
705
+ };
706
+ const renderMessage = {
707
+ isEqual(oldState, newState) {
708
+ return oldState.message === newState.message;
709
+ },
710
+ apply(oldState, newState) {
711
+ return [/* method */SetMessage, /* message */newState.message];
712
+ }
713
+ };
714
+ const renderSearchValue = {
715
+ isEqual(oldState, newState) {
716
+ return oldState.searchValue === newState.searchValue;
717
+ },
718
+ apply(oldState, newState) {
719
+ return [/* method */SetSearchValue, oldState.searchValue, newState.searchValue];
720
+ }
721
+ };
722
+ const renderHeader = {
723
+ isEqual(oldState, newState) {
724
+ return oldState.placeholder === newState.placeholder;
725
+ },
726
+ apply(oldState, newState) {
727
+ const actions = [{
728
+ type: Button,
729
+ title: clearExtensionSearchResults(),
730
+ icon: `MaskIcon${ClearAll}`,
731
+ command: 'Extensions.clearSearchResults'
732
+ }, {
733
+ type: Button,
734
+ title: filter(),
735
+ icon: `MaskIcon${Filter}`
736
+ }];
737
+ const dom = getExtensionHeaderVirtualDom(newState.placeholder, actions);
738
+ return ['setHeaderDom', dom];
739
+ }
740
+ };
741
+ const render = [renderScrollBar, renderMessage, renderExtensions, renderSearchValue, renderHeader];
742
+ const doRender = (oldState, newState) => {
743
+ const commands = [];
744
+ for (const item of render) {
745
+ if (!item.isEqual(oldState, newState)) {
746
+ commands.push(item.apply(oldState, newState));
747
+ }
748
+ }
749
+ return commands;
750
+ };
751
+
237
752
  const commandMap = {
238
- 'SearchExtensions.searchExtensions': searchExtensions
753
+ 'SearchExtensions.searchExtensions': searchExtensions,
754
+ 'SearchExtensions.render': doRender,
755
+ 'SearchExtensions.clearSearchResults': clearSearchResults
239
756
  };
240
757
 
241
758
  const Two = '2.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/extension-search-view",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "",
5
5
  "main": "dist/extensionSearchViewWorkerMain.js",
6
6
  "type": "module",