Skip to content

Commit 6d03bba

Browse files
committed
wip
1 parent f3d41e6 commit 6d03bba

6 files changed

Lines changed: 343 additions & 10 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {filterOptionsByPattern} from './helpers';
2+
import {InternalOption, Option, FilterMethod} from './types';
3+
4+
export class OptionCollection {
5+
private _activeIndex = 0;
6+
private _combobox = false;
7+
private _options: InternalOption[] = [];
8+
// cached filtered options
9+
private _filterMethod: FilterMethod = 'fuzzy';
10+
private _filteredOptions: InternalOption[] | null = null;
11+
private _filterPattern = '';
12+
13+
private _filter() {
14+
this._options.forEach((op, i) => {
15+
16+
});
17+
}
18+
19+
toggleComboboxMode(enabled: boolean) {
20+
this._filteredOptions = null;
21+
this._combobox = enabled;
22+
}
23+
24+
populate(options: Option[]) {
25+
this._filteredOptions = null;
26+
27+
this._options = options.map((op, index) => ({
28+
description: op.description ?? '',
29+
disabled: op.disabled ?? false,
30+
label: op.label ?? '',
31+
selected: op.selected ?? false,
32+
value: op.value ?? '',
33+
index,
34+
absoluteIndex: index,
35+
ranges: [],
36+
// TODO: hidden
37+
}));
38+
}
39+
40+
getConfiguration(): Option[] {
41+
return this._options.map(
42+
({description, disabled, label, selected, value, absoluteIndex}) => ({
43+
description,
44+
disabled,
45+
label,
46+
selected,
47+
value,
48+
index: absoluteIndex,
49+
})
50+
);
51+
}
52+
53+
setFilterPattern(filterPattern: string) {
54+
this._filteredOptions = null;
55+
this._filterPattern = filterPattern;
56+
}
57+
58+
setFilterMethod(method: FilterMethod) {
59+
this._filteredOptions = null;
60+
this._filterMethod = method;
61+
}
62+
63+
getOptions(): InternalOption[] {
64+
return this._options;
65+
}
66+
67+
getVisibleOptions(): InternalOption[] {
68+
if (!this._combobox || this._filterPattern === '') {
69+
return this._options;
70+
}
71+
72+
if (!this._filteredOptions) {
73+
this._filteredOptions = filterOptionsByPattern(
74+
this._options,
75+
this._filterPattern,
76+
this._filterMethod
77+
);
78+
}
79+
80+
return this._filteredOptions;
81+
}
82+
83+
/**
84+
* Sets the index of the active (highlighted) option.
85+
*
86+
* @param index Index of the active option in the unfiltered option list.
87+
*/
88+
setActiveIndex(index: number) {
89+
this._activeIndex = index;
90+
}
91+
92+
/**
93+
* @returns Index of the active option in the unfiltered option list.
94+
*/
95+
getActiveIndex(): number {
96+
return this._activeIndex;
97+
}
98+
99+
/**
100+
* Get next selectable option from the list of visible options.
101+
* @see getVisibleOptions()
102+
* @param fromIndex
103+
*/
104+
getNextSelectableOption(fromIndex = 0): InternalOption | null {
105+
let nextIndex = 0;
106+
const options = this.getVisibleOptions();
107+
108+
if (!options[fromIndex]) {
109+
return null;
110+
}
111+
112+
if (!options[fromIndex + 1]) {
113+
return options[fromIndex];
114+
}
115+
116+
for (let i = fromIndex + 1; i < options.length; i++) {
117+
if (!options[i].disabled) {
118+
nextIndex = i;
119+
break;
120+
}
121+
}
122+
123+
return options[nextIndex];
124+
}
125+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import {ReactiveController, ReactiveControllerHost} from 'lit';
2+
import {FilterMethod, InternalOption, Option} from './types';
3+
import {
4+
containsSearch,
5+
fuzzySearch,
6+
SearchResult,
7+
startsWithPerTermSearch,
8+
startsWithSearch,
9+
} from './helpers';
10+
11+
export class OptionListController implements ReactiveController {
12+
private _host: ReactiveControllerHost;
13+
private _options: InternalOption[] = [];
14+
private _filterPattern = '';
15+
private _filterMethod: FilterMethod = 'fuzzy';
16+
private _combobox = false;
17+
private _indexByValue: Map<string, number> = new Map();
18+
private _selectedIndex = -1;
19+
private _selectedIndexes: number[] = [];
20+
private _multiSelect = false;
21+
22+
constructor(host: ReactiveControllerHost) {
23+
(this._host = host).addController(this);
24+
}
25+
26+
hostConnected(): void {}
27+
28+
set multiSelect(multiSelect: boolean) {
29+
this._multiSelect = multiSelect;
30+
}
31+
32+
get multiSelect() {
33+
return this._multiSelect;
34+
}
35+
36+
set selectedIndex(value: number) {
37+
this._selectedIndex = value;
38+
}
39+
40+
get selectedIndex() {
41+
return this._selectedIndex;
42+
}
43+
44+
get value(): string | string[] {
45+
if (this._multiSelect) {
46+
return this._selectedIndexes.map((v) => this._options[v].value);
47+
} else {
48+
return this._options[this._selectedIndex].value;
49+
}
50+
}
51+
52+
set value(newValue: string | string[]) {
53+
if (this._multiSelect) {
54+
this._selectedIndexes = (newValue as string[])
55+
.map((v) => this._indexByValue.get(v))
56+
.filter((v) => v !== undefined);
57+
} else {
58+
this._selectedIndex = this._indexByValue.get(newValue as string) ?? -1;
59+
}
60+
}
61+
62+
set filterPattern(pattern: string) {
63+
this._filterPattern = pattern;
64+
this._updateState();
65+
}
66+
67+
get filterPattern() {
68+
return this._filterPattern;
69+
}
70+
71+
populate(options: Option[]) {
72+
this._indexByValue.clear();
73+
74+
this._options = options.map((op, index) => {
75+
this._indexByValue.set(op.value, index);
76+
77+
return {
78+
description: op.description ?? '',
79+
disabled: op.disabled ?? false,
80+
label: op.label ?? '',
81+
selected: op.selected ?? false,
82+
value: op.value ?? '',
83+
index,
84+
absoluteIndex: index,
85+
ranges: [],
86+
visible: true,
87+
};
88+
});
89+
}
90+
91+
toggleComboboxMode(enabled: boolean) {
92+
this._combobox = enabled;
93+
}
94+
95+
get options(): InternalOption[] {
96+
return this._options;
97+
}
98+
99+
getOptionByValue(value: string): InternalOption | null {
100+
const index = this._indexByValue.get(value) ?? -1;
101+
102+
if (index === -1) {
103+
return null;
104+
}
105+
106+
return this._options[index];
107+
}
108+
109+
getNextSelectableOption(fromIndex = 0): InternalOption | null {
110+
if (this._options.length === 0) {
111+
return null;
112+
}
113+
114+
if (this._options.length === 1) {
115+
return this._options[0];
116+
}
117+
118+
if (!this._options[fromIndex] || !this._options[fromIndex + 1]) {
119+
return this._options[this._options.length - 1];
120+
}
121+
122+
let nextIndex = -1;
123+
124+
for (let i = fromIndex + 1; i < this._options.length; i++) {
125+
if (
126+
this._options[i] &&
127+
!this._options[i].disabled &&
128+
this._options[i].visible
129+
) {
130+
nextIndex = i;
131+
break;
132+
}
133+
}
134+
135+
return nextIndex > -1 ? this._options[nextIndex] : null;
136+
}
137+
138+
_updateState() {
139+
if (!this._combobox || this._filterPattern === '') {
140+
this._options.forEach((_, i) => {
141+
this._options[i].visible = true;
142+
});
143+
144+
this._host.requestUpdate();
145+
return;
146+
}
147+
148+
this._options.forEach((op, i) => {
149+
let result: SearchResult;
150+
151+
switch (this._filterMethod) {
152+
case 'startsWithPerTerm':
153+
result = startsWithPerTermSearch(op.label, this._filterPattern);
154+
break;
155+
case 'startsWith':
156+
result = startsWithSearch(op.label, this._filterPattern);
157+
break;
158+
case 'contains':
159+
result = containsSearch(op.label, this._filterPattern);
160+
break;
161+
default:
162+
result = fuzzySearch(op.label, this._filterPattern);
163+
}
164+
165+
this._options[i].visible = result.match;
166+
this._options[i].ranges = result.ranges;
167+
});
168+
169+
this._host.requestUpdate();
170+
}
171+
}

src/includes/vscode-select/helpers.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {html, TemplateResult} from 'lit';
2-
import {InternalOption, SearchMethod} from './types.js';
2+
import {InternalOption, FilterMethod} from './types.js';
33

44
export type SearchResult = {
55
match: boolean;
@@ -112,8 +112,9 @@ export const fuzzySearch = (subject: string, pattern: string): SearchResult => {
112112
export const filterOptionsByPattern = (
113113
list: InternalOption[],
114114
pattern: string,
115-
method: SearchMethod
115+
method: FilterMethod
116116
): InternalOption[] => {
117+
console.log('filter options by patern');
117118
const filtered: InternalOption[] = [];
118119

119120
list.forEach((op) => {
@@ -204,6 +205,8 @@ export function findNextSelectableOptionIndex(
204205
) {
205206
let result = 0;
206207

208+
console.log(options);
209+
207210
if (fromIndex < 0 || !options[fromIndex] || !options[fromIndex + 1]) {
208211
return result;
209212
}

src/includes/vscode-select/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// TODO: Make all property optional
12
export interface Option {
23
label: string;
34
value: string;
@@ -8,10 +9,14 @@ export interface Option {
89

910
export interface InternalOption extends Option {
1011
index: number;
12+
/** The original index of the option in the non-filtered list. */
13+
absoluteIndex?: number;
14+
/** Character ranges to highlight matches in the filtered list. */
1115
ranges?: [number, number][];
16+
visible?: boolean;
1217
}
1318

14-
export type SearchMethod =
19+
export type FilterMethod =
1520
| 'startsWithPerTerm'
1621
| 'startsWith'
1722
| 'contains'

0 commit comments

Comments
 (0)