@lvce-editor/extension-search-view 1.1.0 → 1.3.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/README.md CHANGED
@@ -1 +1,7 @@
1
- # extension-search-view
1
+ # Extension Search View
2
+
3
+ Webworker for the extension search view in Lvce Editor.
4
+
5
+ ## Gitpod
6
+
7
+ [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/lvce-editor/extension-search-view)
@@ -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';
@@ -68,6 +196,46 @@ const parseValue = value => {
68
196
 
69
197
  const assetDir = '';
70
198
 
199
+ const Web = 1;
200
+ const Electron = 2;
201
+ const Remote = 3;
202
+ const Test = 4;
203
+
204
+ // TODO treeshake this function out
205
+
206
+ /**
207
+ * @returns {number}
208
+ */
209
+ const getPlatform = () => {
210
+ // @ts-ignore
211
+ if (typeof PLATFORM !== 'undefined') {
212
+ // @ts-ignore
213
+ return PLATFORM;
214
+ }
215
+ if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
216
+ return Test;
217
+ }
218
+ // TODO find a better way to pass runtime environment
219
+ if (typeof name !== 'undefined' && name.endsWith('(Electron)')) {
220
+ return Electron;
221
+ }
222
+ if (typeof name !== 'undefined' && name.endsWith('(Web)')) {
223
+ return Web;
224
+ }
225
+ return Remote;
226
+ };
227
+ const platform = getPlatform();
228
+
229
+ const getRemoteUrl = extension => {
230
+ if (platform === Remote || platform === Electron) {
231
+ if (extension.builtin) {
232
+ return `${assetDir}/extensions/${extension.id}/${extension.icon}`;
233
+ }
234
+ return `/remote/${extension.path}/${extension.icon}`; // TODO support windows paths
235
+ }
236
+ return '';
237
+ };
238
+
71
239
  const ExtensionDefaultIcon = `${assetDir}/icons/extensionDefaultIcon.png`;
72
240
  const ExtensionLanguageBasics = `${assetDir}/icons/language-icon.svg`;
73
241
  const ExtensionTheme = `${assetDir}/icons/theme-icon.png`;
@@ -91,7 +259,7 @@ const getIcon = extension => {
91
259
  }
92
260
  return ExtensionDefaultIcon;
93
261
  }
94
- return '';
262
+ return getRemoteUrl(extension);
95
263
  };
96
264
  const RE_PUBLISHER = /^[a-z\d\-]+/;
97
265
 
@@ -236,8 +404,395 @@ const searchExtensions = async (extensions, value) => {
236
404
  }
237
405
  };
238
406
 
407
+ // TODO debounce
408
+ const handleInput = async (state, value) => {
409
+ try {
410
+ const {
411
+ allExtensions,
412
+ itemHeight,
413
+ minimumSliderSize,
414
+ height
415
+ } = state;
416
+ // TODO cancel ongoing requests
417
+ // TODO handle errors
418
+ const items = await searchExtensions(allExtensions, value);
419
+ if (items.length === 0) {
420
+ return {
421
+ ...state,
422
+ items,
423
+ minLineY: 0,
424
+ deltaY: 0,
425
+ allExtensions,
426
+ maxLineY: 0,
427
+ scrollBarHeight: 0,
428
+ finalDeltaY: 0,
429
+ message: noExtensionsFound(),
430
+ searchValue: value,
431
+ placeholder: searchExtensionsInMarketPlace()
432
+ };
433
+ }
434
+ // @ts-ignore
435
+ const listHeight = getListHeight$1(state);
436
+ const total = items.length;
437
+ const contentHeight = total * itemHeight;
438
+ const scrollBarHeight = getScrollBarSize(height, contentHeight, minimumSliderSize);
439
+ const numberOfVisible = getNumberOfVisibleItems(listHeight, itemHeight);
440
+ const maxLineY = Math.min(numberOfVisible, total);
441
+ const finalDeltaY = getFinalDeltaY(listHeight, itemHeight, total);
442
+ return {
443
+ ...state,
444
+ items,
445
+ minLineY: 0,
446
+ deltaY: 0,
447
+ allExtensions,
448
+ maxLineY,
449
+ scrollBarHeight,
450
+ finalDeltaY,
451
+ message: '',
452
+ searchValue: value,
453
+ placeholder: searchExtensionsInMarketPlace()
454
+ };
455
+
456
+ // TODO handle out of order responses (a bit complicated)
457
+ // for now just assume everything comes back in order
458
+ } catch (error) {
459
+ await handleError(error);
460
+ return {
461
+ ...state,
462
+ searchValue: value,
463
+ message: `${error}`
464
+ };
465
+ }
466
+ };
467
+
468
+ const clearSearchResults = state => {
469
+ return handleInput(state, '');
470
+ };
471
+
472
+ const Button = 1;
473
+
474
+ const ExtensionActions = 'ExtensionActions';
475
+ const ExtensionActive = 'ExtensionActive';
476
+ const ExtensionHeader = 'ExtensionHeader';
477
+ const ExtensionListItem = 'ExtensionListItem';
478
+ const ExtensionListItemAuthorName = 'ExtensionListItemAuthorName';
479
+ const ExtensionListItemDescription = 'ExtensionListItemDescription';
480
+ const ExtensionListItemDetail = 'ExtensionListItemDetail';
481
+ const ExtensionListItemFooter = 'ExtensionListItemFooter';
482
+ const ExtensionListItemIcon = 'ExtensionListItemIcon';
483
+ const ExtensionListItemName = 'ExtensionListItemName';
484
+ const ListItems = 'ListItems';
485
+ const MultilineInputBox = 'MultilineInputBox';
486
+ const ScrollBarThumb = 'ScrollBarThumb';
487
+ const ScrollBarThumbActive = 'ScrollBarThumbActive';
488
+ const SearchField = 'SearchField';
489
+ const SearchFieldButtons = 'SearchFieldButtons';
490
+ const SearchFieldContainer = 'SearchFieldContainer';
491
+
492
+ const CheckBox = 'checkbox';
493
+ const List = 'list';
494
+ const ListItem = 'listitem';
495
+ const None = 'none';
496
+
497
+ const Div = 4;
498
+ const Text = 12;
499
+ const Img = 17;
500
+ const TextArea = 62;
501
+
502
+ const getSearchFieldButtonVirtualDom = button => {
503
+ const {
504
+ icon,
505
+ checked,
506
+ title
507
+ } = button;
508
+ return [{
509
+ type: Div,
510
+ className: `SearchFieldButton ${checked ? 'SearchFieldButtonChecked' : ''}`,
511
+ title,
512
+ role: CheckBox,
513
+ ariaChecked: checked,
514
+ tabIndex: 0,
515
+ childCount: 1
516
+ }, {
517
+ type: Div,
518
+ className: `MaskIcon ${icon}`,
519
+ childCount: 0
520
+ }];
521
+ };
522
+
523
+ const getSearchFieldVirtualDom = (name, placeholder, onInput, insideButtons, outsideButtons, onFocus = '') => {
524
+ const dom = [{
525
+ type: Div,
526
+ className: SearchField,
527
+ role: None,
528
+ childCount: 2
529
+ }, {
530
+ type: TextArea,
531
+ className: MultilineInputBox,
532
+ spellcheck: false,
533
+ autocapitalize: 'off',
534
+ autocorrect: 'off',
535
+ placeholder,
536
+ name,
537
+ onInput,
538
+ onFocus,
539
+ childCount: 0
540
+ }, {
541
+ type: Div,
542
+ className: SearchFieldButtons,
543
+ childCount: insideButtons.length
544
+ }, ...insideButtons.flatMap(getSearchFieldButtonVirtualDom)];
545
+ if (outsideButtons.length > 0) {
546
+ dom.unshift({
547
+ type: Div,
548
+ className: SearchFieldContainer,
549
+ role: None,
550
+ childCount: 1 + outsideButtons.length
551
+ });
552
+ dom.push(...outsideButtons.flatMap(getSearchFieldButtonVirtualDom));
553
+ }
554
+ return dom;
555
+ };
556
+
557
+ const getExtensionHeaderVirtualDom = (placeholder, actions) => {
558
+ return [{
559
+ type: Div,
560
+ className: ExtensionHeader,
561
+ childCount: 1
562
+ }, ...getSearchFieldVirtualDom('extensions', placeholder, 'handleExtensionsInput', actions, [])];
563
+ };
564
+
565
+ const HandleContextMenu = 'handleContextMenu';
566
+ const HandlePointerDown = 'handlePointerDown';
567
+ const HandleTouchStart = 'handleTouchStart';
568
+ const HandleWheel = 'handleWheel';
569
+
570
+ const Extension = 'Extension';
571
+
572
+ const text = data => {
573
+ return {
574
+ type: Text,
575
+ text: data,
576
+ childCount: 0
577
+ };
578
+ };
579
+
580
+ const listItemDetail = {
581
+ type: Div,
582
+ className: ExtensionListItemDetail,
583
+ childCount: 3
584
+ };
585
+ const listItemName = {
586
+ type: Div,
587
+ className: ExtensionListItemName,
588
+ childCount: 1
589
+ };
590
+ const listItemDescription = {
591
+ type: Div,
592
+ className: ExtensionListItemDescription,
593
+ childCount: 1
594
+ };
595
+ const listItemFooter = {
596
+ type: Div,
597
+ className: ExtensionListItemFooter,
598
+ childCount: 2
599
+ };
600
+ const listItemAuthorName = {
601
+ type: Div,
602
+ className: ExtensionListItemAuthorName,
603
+ childCount: 1
604
+ };
605
+ const getExtensionListItemVirtualDom = extension => {
606
+ const {
607
+ posInSet,
608
+ setSize,
609
+ top,
610
+ icon,
611
+ name,
612
+ description,
613
+ publisher,
614
+ focused
615
+ } = extension;
616
+ const dom = [{
617
+ type: Div,
618
+ role: ListItem,
619
+ ariaRoleDescription: Extension,
620
+ className: ExtensionListItem,
621
+ ariaPosInSet: posInSet,
622
+ ariaSetSize: setSize,
623
+ top,
624
+ childCount: 2
625
+ }, {
626
+ type: Img,
627
+ src: icon,
628
+ className: ExtensionListItemIcon,
629
+ role: None,
630
+ childCount: 0
631
+ }, listItemDetail, listItemName, text(name), listItemDescription, text(description), listItemFooter, listItemAuthorName, text(publisher), {
632
+ type: Div,
633
+ className: ExtensionActions,
634
+ childCount: 0
635
+ }];
636
+ if (focused) {
637
+ dom[0].id = 'ExtensionActive';
638
+ dom[0].className += ' ' + ExtensionActive;
639
+ }
640
+ return dom;
641
+ };
642
+
643
+ const getExtensionsListVirtualDom = visibleExtensions => {
644
+ const dom = [{
645
+ type: Div,
646
+ className: ListItems,
647
+ tabIndex: 0,
648
+ ariaLabel: extensions(),
649
+ role: List,
650
+ oncontextmenu: HandleContextMenu,
651
+ onpointerdown: HandlePointerDown,
652
+ ontouchstart: HandleTouchStart,
653
+ onwheelpassive: HandleWheel,
654
+ childCount: visibleExtensions.length
655
+ }, ...visibleExtensions.flatMap(getExtensionListItemVirtualDom)];
656
+ return dom;
657
+ };
658
+
659
+ const getExtensionsVirtualDom = visibleExtensions => {
660
+ const dom = getExtensionsListVirtualDom(visibleExtensions);
661
+ // TODO
662
+ return dom;
663
+ };
664
+
665
+ const getVisibleItem = (item, setSize, itemHeight, minLineY, relative, i, focusedIndex) => {
666
+ return {
667
+ ...item,
668
+ setSize,
669
+ posInSet: i + 1,
670
+ top: (i - minLineY) * itemHeight - relative,
671
+ focused: i === focusedIndex
672
+ };
673
+ };
674
+ const getVisible = state => {
675
+ const {
676
+ minLineY,
677
+ maxLineY,
678
+ items,
679
+ itemHeight,
680
+ deltaY,
681
+ focusedIndex
682
+ } = state;
683
+ const setSize = items.length;
684
+ const visible = [];
685
+ const relative = deltaY % itemHeight;
686
+ for (let i = minLineY; i < maxLineY; i++) {
687
+ const item = items[i];
688
+ visible.push(getVisibleItem(item, setSize, itemHeight, minLineY, relative, i, focusedIndex));
689
+ }
690
+ return visible;
691
+ };
692
+
693
+ const ClearAll = 'ClearAll';
694
+ const Filter = 'Filter';
695
+
696
+ const px = value => {
697
+ return `${value}px`;
698
+ };
699
+ const position = (x, y) => {
700
+ return `${x}px ${y}px`;
701
+ };
702
+
703
+ const SetMessage = 'setMessage';
704
+ const SetScrollBar = 'setScrollBar';
705
+ const SetSearchValue = 'setSearchValue';
706
+
707
+ const getListHeight = state => {
708
+ const {
709
+ height,
710
+ headerHeight
711
+ } = state;
712
+ return height - headerHeight;
713
+ };
714
+ const renderExtensions = {
715
+ isEqual(oldState, newState) {
716
+ return oldState.items === newState.items && oldState.minLineY === newState.minLineY && oldState.maxLineY === newState.maxLineY && oldState.deltaY === newState.deltaY && oldState.focusedIndex === newState.focusedIndex;
717
+ },
718
+ apply(oldState, newState) {
719
+ // TODO render extensions incrementally when scrolling
720
+ const visibleExtensions = getVisible(newState);
721
+ const dom = getExtensionsVirtualDom(visibleExtensions);
722
+ return ['setExtensionsDom', dom];
723
+ }
724
+ };
725
+ const renderScrollBar = {
726
+ isEqual(oldState, newState) {
727
+ 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;
728
+ },
729
+ apply(oldState, newState) {
730
+ // @ts-ignore
731
+ const listHeight = getListHeight(newState);
732
+ const total = newState.items.length;
733
+ const contentHeight = total * newState.itemHeight;
734
+ const scrollBarHeight = getScrollBarSize(listHeight, contentHeight, newState.minimumSliderSize);
735
+ const scrollBarY = getScrollBarY(newState.deltaY, newState.finalDeltaY, newState.height - newState.headerHeight, scrollBarHeight);
736
+ const roundedScrollBarY = Math.round(scrollBarY);
737
+ const heightString = px(scrollBarHeight);
738
+ const translateString = position(0, roundedScrollBarY);
739
+ let className = ScrollBarThumb;
740
+ if (newState.scrollBarActive) {
741
+ className += ' ' + ScrollBarThumbActive;
742
+ }
743
+ return [/* method */SetScrollBar, translateString, heightString, className];
744
+ }
745
+ };
746
+ const renderMessage = {
747
+ isEqual(oldState, newState) {
748
+ return oldState.message === newState.message;
749
+ },
750
+ apply(oldState, newState) {
751
+ return [/* method */SetMessage, /* message */newState.message];
752
+ }
753
+ };
754
+ const renderSearchValue = {
755
+ isEqual(oldState, newState) {
756
+ return oldState.searchValue === newState.searchValue;
757
+ },
758
+ apply(oldState, newState) {
759
+ return [/* method */SetSearchValue, oldState.searchValue, newState.searchValue];
760
+ }
761
+ };
762
+ const renderHeader = {
763
+ isEqual(oldState, newState) {
764
+ return oldState.placeholder === newState.placeholder;
765
+ },
766
+ apply(oldState, newState) {
767
+ const actions = [{
768
+ type: Button,
769
+ title: clearExtensionSearchResults(),
770
+ icon: `MaskIcon${ClearAll}`,
771
+ command: 'Extensions.clearSearchResults'
772
+ }, {
773
+ type: Button,
774
+ title: filter(),
775
+ icon: `MaskIcon${Filter}`
776
+ }];
777
+ const dom = getExtensionHeaderVirtualDom(newState.placeholder, actions);
778
+ return ['setHeaderDom', dom];
779
+ }
780
+ };
781
+ const render = [renderScrollBar, renderMessage, renderExtensions, renderSearchValue, renderHeader];
782
+ const doRender = (oldState, newState) => {
783
+ const commands = [];
784
+ for (const item of render) {
785
+ if (!item.isEqual(oldState, newState)) {
786
+ commands.push(item.apply(oldState, newState));
787
+ }
788
+ }
789
+ return commands;
790
+ };
791
+
239
792
  const commandMap = {
240
- 'SearchExtensions.searchExtensions': searchExtensions
793
+ 'SearchExtensions.searchExtensions': searchExtensions,
794
+ 'SearchExtensions.render': doRender,
795
+ 'SearchExtensions.clearSearchResults': clearSearchResults
241
796
  };
242
797
 
243
798
  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.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "",
5
5
  "main": "dist/extensionSearchViewWorkerMain.js",
6
6
  "type": "module",