pulsar-select-list 1.0.1 → 1.0.2

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.
Files changed (4) hide show
  1. package/README.md +410 -436
  2. package/package.json +2 -2
  3. package/select.js +876 -806
  4. package/select.less +66 -53
package/README.md CHANGED
@@ -1,436 +1,410 @@
1
- # pulsar-select-list
2
-
3
- This module is an [etch component](https://github.com/atom/etch) that can be used in Pulsar packages to show a select list with fuzzy filtering, keyboard/mouse navigation and other cool features.
4
-
5
- ## Installation
6
-
7
- ```json
8
- "dependencies": {
9
- "pulsar-select-list": "^1.0.0"
10
- }
11
- ```
12
-
13
- ## Usage
14
-
15
- After installing the module, you can simply require it and use it as a standalone component:
16
-
17
- ```js
18
- const SelectListView = require('pulsar-select-list')
19
-
20
- const usersSelectList = new SelectListView({
21
- items: ['Alice', 'Bob', 'Carol'],
22
- elementForItem: (item) => {
23
- const li = document.createElement('li')
24
- li.textContent = item
25
- return li
26
- },
27
- didConfirmSelection: (item) => {
28
- console.log('Selected:', item)
29
- }
30
- })
31
-
32
- // Show as modal panel
33
- usersSelectList.show()
34
- ```
35
-
36
- Or within another etch component:
37
-
38
- ```jsx
39
- render () {
40
- return (
41
- <SelectListView items={this.items} />
42
- )
43
- }
44
- ```
45
-
46
- ## API
47
-
48
- ### Constructor Props
49
-
50
- When creating a new instance of a select list, or when calling `update` on an existing one, you can supply a JavaScript object that can contain any of the following properties:
51
-
52
- #### Required
53
-
54
- * `items: [Object]`: an array containing the objects you want to show in the select list.
55
- * `elementForItem: (item: Object, options: Object) -> HTMLElement`: a function that is called whenever an item needs to be displayed.
56
- * `options: Object`:
57
- * `selected: Boolean`: indicating whether item is selected or not.
58
- * `index: Number`: item's index.
59
- * `visible: Boolean`: indicating whether item is visible in viewport or not.
60
-
61
- #### Optional
62
-
63
- * `className: String`: CSS class name(s) to add to the select list element. Multiple classes can be space-separated.
64
- * `maxResults: Number`: the number of maximum items that are shown.
65
- * `filter: (items: [Object], query: String) -> [Object]`: a function that allows to decide which items to show whenever the query changes. By default, it uses Pulsar's built-in fuzzy matcher.
66
- * `filterKeyForItem: (item: Object) -> String`: when `filter` is not provided, this function will be called to retrieve a string property on each item and that will be used to filter them.
67
- * `filterQuery: (query: String) -> String`: a function that allows to apply a transformation to the user query and whose return value will be used to filter items.
68
- * `replaceDiacritics: Boolean`: when `true` (default), removes diacritical marks from both the query and item text before filtering, enabling accent-insensitive matching (e.g., "cafe" matches "café"). Set to `false` to disable.
69
- * `filterScoreModifier: (score: Number, item: Object) -> Number`: a function to modify the fuzzy match score for each item. Useful for applying custom ranking factors (e.g., boosting by recency or proximity).
70
- * `filterThreshold: Number`: minimum score required for an item to be included in results; defaults to `0`. Scores at or below this threshold are filtered out.
71
- * `query: String`: a string that will replace the contents of the query editor.
72
- * `selectQuery: Boolean`: a boolean indicating whether the query text should be selected or not.
73
- * `order: (item1: Object, item2: Object) -> Number`: a function that allows to change the order in which items are shown.
74
- * `emptyMessage: String`: a string shown when the list is empty.
75
- * `errorMessage: String`: a string that needs to be set when you want to notify the user that an error occurred.
76
- * `infoMessage: String`: a string that needs to be set when you want to provide some information to the user.
77
- * `helpMessage: String|Array`: content to display when help is toggled. Use `toggleHelp()` to show/hide. Can be a string or JSX array for rich formatting.
78
- * `helpMarkdown: String`: markdown content to display when help is toggled. Rendered using Pulsar's built-in markdown renderer. Preferred over `helpMessage` for simple help text stored in `.md` files.
79
- * `loadingMessage: String`: a string that needs to be set when you are loading items in the background.
80
- * `loadingBadge: String/Number`: a string or number that needs to be set when the progress status changes.
81
- * `itemsClassList: [String]`: an array of strings that will be added as class names to the items element.
82
- * `initialSelectionIndex: Number`: the index of the item to initially select; defaults to `0`.
83
- * `initiallyVisibleItemCount: Number`: When provided, `SelectListView` observes visibility of items in viewport, visibility state is passed as `visible` option to `elementForItem`.
84
- * `placeholderText: String`: placeholder text to display in the query editor when empty.
85
- * `skipCommandsRegistration: Boolean`: when `true`, skips registering default keyboard commands.
86
-
87
- ### Registered Commands
88
-
89
- By default, the component registers these commands on its element:
90
-
91
- * `core:move-up` / `core:move-down`: Navigate items
92
- * `core:move-to-top` / `core:move-to-bottom`: Jump to first/last item
93
- * `core:confirm`: Confirm selection
94
- * `core:cancel`: Cancel selection
95
- * `select-list:help`: Toggle help message visibility (requires `helpMessage` or `helpMarkdown`)
96
-
97
- #### Callbacks
98
-
99
- * `didChangeQuery: (query: String) -> Void`: called when the query changes.
100
- * `didChangeSelection: (item: Object) -> Void`: called when the selected item changes.
101
- * `didConfirmSelection: (item: Object) -> Void`: called when the user clicks or presses Enter on an item.
102
- * `didConfirmEmptySelection: () -> Void`: called when the user presses Enter but the list is empty.
103
- * `didCancelSelection: () -> Void`: called when the user presses Esc or the list loses focus.
104
- * `willShow: () -> Void`: called when transitioning from hidden to visible, useful for data preparation.
105
-
106
- ### Instance Properties
107
-
108
- * `processedQuery: String`: The cached result of `getFilterQuery()`, updated after each query change. Useful in `elementForItem` to avoid calling `getFilterQuery()` multiple times.
109
- * `refs.queryEditor`: The underlying TextEditor component for the query input.
110
-
111
- ### Instance Methods
112
-
113
- #### Panel Management
114
-
115
- * `show()`: Shows the select list as a modal panel and focuses the query editor. Calls `willShow` callback if provided.
116
- * `hide()`: Hides the panel and restores focus to the previously focused element.
117
- * `toggle()`: Toggles the visibility of the panel.
118
- * `isVisible()`: Returns `true` if the panel is currently visible.
119
- * `isHelpMode()`: Returns `true` if help is currently displayed.
120
- * `toggleHelp()`: Toggles help message visibility. Only works if `helpMessage` is set.
121
- * `hideHelp()`: Hides help message if currently shown.
122
-
123
- #### Other Methods
124
-
125
- * `focus()`: Focuses the query editor.
126
- * `reset()`: Clears the query editor text.
127
- * `destroy()`: Disposes of the component and cleans up resources.
128
- * `update(props)`: Updates the component with new props.
129
- * `getQuery()`: Returns the current query string.
130
- * `getFilterQuery()`: Returns the filtered query string (applies `filterQuery` transformation).
131
- * `setQueryFromSelection()`: Sets the query text from the active editor's selection. Returns `true` if successful, `false` if no editor, no selection, or selection contains newlines.
132
- * `getSelectedItem()`: Returns the currently selected item.
133
- * `selectPrevious()`: Selects the previous item.
134
- * `selectNext()`: Selects the next item.
135
- * `selectFirst()`: Selects the first item.
136
- * `selectLast()`: Selects the last item.
137
- * `selectNone()`: Deselects all items.
138
- * `selectIndex(index)`: Selects the item at the given index.
139
- * `selectItem(item)`: Selects the given item.
140
- * `confirmSelection()`: Confirms the current selection.
141
- * `cancelSelection()`: Cancels the selection.
142
-
143
- ### Static Methods
144
-
145
- #### `SelectListView.highlightMatches(text, matchIndices, options)`
146
-
147
- Creates a DocumentFragment with highlighted match characters.
148
-
149
- ```js
150
- const matches = atom.ui.fuzzyMatcher.match(query, text, {
151
- recordMatchIndexes: true
152
- }).matchIndexes
153
-
154
- const fragment = SelectListView.highlightMatches(text, matches)
155
- element.appendChild(fragment)
156
-
157
- // With custom class name
158
- const fragment = SelectListView.highlightMatches(text, matches, {
159
- className: 'my-highlight'
160
- })
161
- ```
162
-
163
- * `text: String`: the text to highlight.
164
- * `matchIndices: [Number]`: array of character indices to highlight.
165
- * `options: Object` (optional):
166
- * `className: String`: CSS class for highlighted spans; defaults to `'character-match'`.
167
-
168
- Returns a `DocumentFragment` containing text nodes and `<span>` elements with the specified class.
169
-
170
- #### `SelectListView.replaceDiacritics(str)`
171
-
172
- Removes diacritical marks (accents) from a string.
173
-
174
- ```js
175
- SelectListView.replaceDiacritics('café') // => 'cafe'
176
- SelectListView.replaceDiacritics('naïve') // => 'naive'
177
- SelectListView.replaceDiacritics('Müller') // => 'Muller'
178
- ```
179
-
180
- * `str: String`: the string to process.
181
-
182
- Returns the string with diacritical marks removed. Uses `String.normalize('NFD')` internally.
183
-
184
- #### `SelectListView.createTwoLineItem(options)`
185
-
186
- Creates a two-line list item element with primary and optional secondary lines. This is a convenience helper for the common Atom/Pulsar two-line item pattern.
187
-
188
- ```js
189
- const li = SelectListView.createTwoLineItem({
190
- primary: SelectListView.highlightMatches(item.name, matches),
191
- secondary: item.description,
192
- icon: ['icon-file-text']
193
- })
194
- ```
195
-
196
- * `options: Object`:
197
- * `primary: String|Node`: Primary line content (text string or DOM node)
198
- * `secondary: String|Node` (optional): Secondary line content
199
- * `icon: [String]` (optional): Icon class names to add to primary line (adds `icon` class automatically)
200
-
201
- Returns an `HTMLLIElement` with the structure:
202
- ```html
203
- <li class="two-lines">
204
- <div class="primary-line icon [icon]">[primary]</div>
205
- <div class="secondary-line">[secondary]</div>
206
- </li>
207
- ```
208
-
209
- #### `SelectListView.setScheduler(scheduler)`
210
-
211
- Sets the etch scheduler.
212
-
213
- #### `SelectListView.getScheduler()`
214
-
215
- Gets the current etch scheduler.
216
-
217
- ## Example
218
-
219
- ```js
220
- const SelectListView = require('pulsar-select-list')
221
- const fs = require('fs')
222
- const path = require('path')
223
-
224
- class MyFileList {
225
- constructor() {
226
- this.selectList = new SelectListView({
227
- className: 'my-package my-file-list',
228
- items: [],
229
- filterKeyForItem: (item) => item.name,
230
- emptyMessage: 'No files found',
231
- helpMarkdown: fs.readFileSync(path.join(__dirname, 'help.md'), 'utf8'),
232
-
233
- willShow: () => {
234
- this.previouslyFocusedElement = document.activeElement
235
- this.loadFiles()
236
- },
237
-
238
- elementForItem: (item, options) => {
239
- const li = document.createElement('li')
240
- if (!options.visible) {
241
- return li
242
- }
243
-
244
- const query = this.selectList.processedQuery || ''
245
- const matches = query
246
- ? atom.ui.fuzzyMatcher.match(item.name, query, {
247
- recordMatchIndexes: true
248
- }).matchIndexes
249
- : []
250
-
251
- li.appendChild(SelectListView.highlightMatches(item.name, matches))
252
-
253
- li.addEventListener('contextmenu', () => {
254
- this.selectList.selectIndex(options.index)
255
- })
256
-
257
- return li
258
- },
259
-
260
- didConfirmSelection: (item) => {
261
- atom.workspace.open(item.path)
262
- this.selectList.hide()
263
- },
264
-
265
- didCancelSelection: () => {
266
- this.selectList.hide()
267
- },
268
- })
269
- }
270
-
271
- loadFiles() {
272
- // Load files and update items
273
- this.selectList.update({ items: this.files })
274
- }
275
-
276
- toggle() {
277
- this.selectList.toggle()
278
- }
279
-
280
- destroy() {
281
- this.selectList.destroy()
282
- }
283
- }
284
- ```
285
-
286
- ### Advanced: Custom Score Modifier
287
-
288
- Use `filterScoreModifier` and `filterThreshold` to customize ranking:
289
-
290
- ```js
291
- const selectList = new SelectListView({
292
- items: files,
293
- elementForItem: (item) => {
294
- const li = document.createElement('li')
295
- li.textContent = item.path
296
- return li
297
- },
298
- filterKeyForItem: (item) => item.path,
299
- // Boost score by proximity (items closer to current file rank higher)
300
- filterScoreModifier: (score, item) => score / item.distance,
301
- // Only show items with meaningful scores
302
- filterThreshold: 0.01,
303
- didConfirmSelection: (item) => {
304
- atom.workspace.open(item.path)
305
- }
306
- })
307
- ```
308
-
309
- ## Migration from atom-select-list
310
-
311
- If you're migrating from `atom-select-list`, here are the key changes:
312
-
313
- ### Package.json
314
-
315
- ```diff
316
- "dependencies": {
317
- - "atom-select-list": "^0.8.1",
318
- + "pulsar-select-list": "^1.0.0"
319
- }
320
- ```
321
-
322
- ### Import
323
-
324
- ```diff
325
- -const SelectListView = require('atom-select-list')
326
- +const SelectListView = require('pulsar-select-list')
327
- ```
328
-
329
- ### Panel Management
330
-
331
- The component now manages its own panel. Remove custom panel handling:
332
-
333
- ```diff
334
- -this.panel = null
335
- -this.previouslyFocusedElement = null
336
-
337
- this.slv = new SelectListView({
338
- + className: 'my-list',
339
- items: [],
340
- + willShow: () => this.onWillShow(),
341
- // ...
342
- })
343
- -this.slv.element.classList.add('my-list')
344
-
345
- -showView() {
346
- - this.previouslyFocusedElement = document.activeElement
347
- - if (!this.panel) {
348
- - this.panel = atom.workspace.addModalPanel({ item: this.slv })
349
- - }
350
- - this.panel.show()
351
- - this.slv.focus()
352
- -}
353
- -
354
- -hideView() {
355
- - this.panel.hide()
356
- - if (this.previouslyFocusedElement) {
357
- - this.previouslyFocusedElement.focus()
358
- - }
359
- -}
360
- -
361
- -toggleView() {
362
- - if (this.panel && this.panel.isVisible()) {
363
- - this.hideView()
364
- - } else {
365
- - this.showView()
366
- - }
367
- -}
368
-
369
- // Use built-in methods:
370
- -this.toggleView()
371
- +this.slv.toggle()
372
-
373
- -this.hideView()
374
- +this.slv.hide()
375
- ```
376
-
377
- ### Diacritics
378
-
379
- Replace external diacritics library with built-in static method:
380
-
381
- ```diff
382
- -const Diacritics = require('diacritic')
383
-
384
- -Diacritics.clean(text)
385
- +SelectListView.replaceDiacritics(text)
386
- ```
387
-
388
- ### Query Access
389
-
390
- Use `processedQuery` instead of storing query in filter:
391
-
392
- ```diff
393
- filter(items, query) {
394
- - this.query = query
395
- + // query is passed to filter, use this.slv.processedQuery in elementForItem
396
- }
397
-
398
- elementForItem(item, options) {
399
- - const query = this.query
400
- + const query = this.slv.processedQuery || ''
401
- }
402
- ```
403
-
404
- ### Highlighting
405
-
406
- Use built-in `highlightMatches` static method:
407
-
408
- ```diff
409
- -this.highlightInElement(el, text, indices)
410
- +el.appendChild(SelectListView.highlightMatches(text, indices))
411
- ```
412
-
413
-
414
- ### Help Message
415
-
416
- Replace `infoMessage` with `helpMarkdown` and the built-in `select-list:help` command:
417
-
418
- ```diff
419
- this.slv = new SelectListView({
420
- - // No help by default
421
- + helpMarkdown: fs.readFileSync(path.join(__dirname, 'help.md'), 'utf8'),
422
- })
423
-
424
- -atom.config.observe('my-package.showKeystrokes', (value) => {
425
- - this.slv.update({ infoMessage: value ? [...] : null })
426
- -})
427
- ```
428
-
429
- Create a `help.md` file with your help content:
430
-
431
- ```markdown
432
- - **Enter** — Confirm selection
433
- - **Alt+Enter** — Alternative action
434
- ```
435
-
436
- Press ` in editor to switch into help view.
1
+ # pulsar-select-list
2
+
3
+ This module is an [etch component](https://github.com/atom/etch) that can be used in Pulsar packages to show a select list with fuzzy filtering, keyboard/mouse navigation and other cool features.
4
+
5
+ ## Installation
6
+
7
+ ```json
8
+ "dependencies": {
9
+ "pulsar-select-list": "^1.0.0"
10
+ }
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ After installing the module, you can simply require it and use it as a standalone component:
16
+
17
+ ```js
18
+ const SelectListView = require("pulsar-select-list");
19
+
20
+ const usersSelectList = new SelectListView({
21
+ items: ["Alice", "Bob", "Carol"],
22
+ elementForItem: (item) => {
23
+ const li = document.createElement("li");
24
+ li.textContent = item;
25
+ return li;
26
+ },
27
+ didConfirmSelection: (item) => {
28
+ console.log("Selected:", item);
29
+ },
30
+ });
31
+
32
+ // Show as modal panel
33
+ usersSelectList.show();
34
+ ```
35
+
36
+ Or within another etch component:
37
+
38
+ ```jsx
39
+ render () {
40
+ return (
41
+ <SelectListView items={this.items} />
42
+ )
43
+ }
44
+ ```
45
+
46
+ ## API
47
+
48
+ ### Constructor Props
49
+
50
+ When creating a new instance of a select list, or when calling `update` on an existing one, you can supply a JavaScript object that can contain any of the following properties:
51
+
52
+ #### Required
53
+
54
+ - `items: [Object]`: an array containing the objects you want to show in the select list.
55
+ - `elementForItem: (item: Object, options: Object) -> HTMLElement`: a function that is called whenever an item needs to be displayed.
56
+ - `options: Object`:
57
+ - `selected: Boolean`: indicating whether item is selected or not.
58
+ - `index: Number`: item's index.
59
+
60
+ #### Optional
61
+
62
+ - `className: String`: CSS class name(s) to add to the select list element. Multiple classes can be space-separated.
63
+ - `maxResults: Number`: the number of maximum items that are shown.
64
+ - `filter: (items: [Object], query: String) -> [Object]`: a function that allows to decide which items to show whenever the query changes. By default, it uses Pulsar's built-in fuzzy matcher.
65
+ - `filterKeyForItem: (item: Object) -> String`: when `filter` is not provided, this function will be called to retrieve a string property on each item and that will be used to filter them.
66
+ - `filterQuery: (query: String) -> String`: a function that allows to apply a transformation to the user query and whose return value will be used to filter items.
67
+ - `replaceDiacritics: Boolean`: when `true` (default), removes diacritical marks from both the query and item text before filtering, enabling accent-insensitive matching (e.g., "cafe" matches "café"). Set to `false` to disable.
68
+ - `filterScoreModifier: (score: Number, item: Object) -> Number`: a function to modify the fuzzy match score for each item. Useful for applying custom ranking factors (e.g., boosting by recency or proximity).
69
+ - `query: String`: a string that will replace the contents of the query editor.
70
+ - `selectQuery: Boolean`: a boolean indicating whether the query text should be selected or not.
71
+ - `order: (item1: Object, item2: Object) -> Number`: a function that allows to change the order in which items are shown.
72
+ - `emptyMessage: String`: a string shown when the list is empty.
73
+ - `errorMessage: String`: a string that needs to be set when you want to notify the user that an error occurred.
74
+ - `infoMessage: String`: a string that needs to be set when you want to provide some information to the user.
75
+ - `helpMessage: String|Array`: content to display when help is toggled. Can be a string or JSX array for rich formatting.
76
+ - `helpMarkdown: String`: markdown content to display when help is toggled. Rendered using Pulsar's built-in markdown renderer.
77
+ - `loadingMessage: String`: a string that needs to be set when you are loading items in the background.
78
+ - `loadingBadge: String/Number`: a string or number that needs to be set when the progress status changes.
79
+ - `itemsClassList: [String]`: an array of strings that will be added as class names to the items element.
80
+ - `initialSelectionIndex: Number`: the index of the item to initially select; defaults to `0`.
81
+ - `placeholderText: String`: placeholder text to display in the query editor when empty.
82
+ - `skipCommandsRegistration: Boolean`: when `true`, skips registering default keyboard commands.
83
+
84
+ ### Registered Commands
85
+
86
+ By default, the component registers these commands on its element:
87
+
88
+ - `core:move-up` / `core:move-down`: Navigate items
89
+ - `core:move-to-top` / `core:move-to-bottom`: Jump to first/last item
90
+ - `core:confirm`: Confirm selection
91
+ - `core:cancel`: Cancel selection
92
+ - `select-list:help`: Toggle help message visibility (requires `helpMessage` or `helpMarkdown`)
93
+
94
+ #### Callbacks
95
+
96
+ - `didChangeQuery: (query: String) -> Void`: called when the query changes.
97
+ - `didChangeSelection: (item: Object) -> Void`: called when the selected item changes.
98
+ - `didConfirmSelection: (item: Object) -> Void`: called when the user clicks or presses Enter on an item.
99
+ - `didConfirmEmptySelection: () -> Void`: called when the user presses Enter but the list is empty.
100
+ - `didCancelSelection: () -> Void`: called when the user presses Esc or the list loses focus.
101
+ - `willShow: () -> Void`: called when transitioning from hidden to visible, useful for data preparation.
102
+
103
+ ### Instance Properties
104
+
105
+ - `processedQuery: String`: The cached result of `getFilterQuery()`, updated after each query change. Useful in `elementForItem` to avoid calling `getFilterQuery()` multiple times.
106
+ - `selectionIndex: Number|undefined`: The index of the currently selected item, or `undefined` if nothing is selected.
107
+ - `refs.queryEditor`: The underlying TextEditor component for the query input.
108
+
109
+ ### Instance Methods
110
+
111
+ #### Panel Management
112
+
113
+ - `show()`: Shows the select list as a modal panel and focuses the query editor. Calls `willShow` callback if provided.
114
+ - `hide()`: Hides the panel and restores focus to the previously focused element.
115
+ - `toggle()`: Toggles the visibility of the panel.
116
+ - `isVisible()`: Returns `true` if the panel is currently visible.
117
+ - `isHelpMode()`: Returns `true` if help is currently displayed.
118
+ - `toggleHelp()`: Toggles help message visibility. Only works if `helpMessage` is set.
119
+ - `hideHelp()`: Hides help message if currently shown.
120
+
121
+ #### Other Methods
122
+
123
+ - `focus()`: Focuses the query editor.
124
+ - `reset()`: Clears the query editor text.
125
+ - `destroy()`: Disposes of the component and cleans up resources.
126
+ - `update(props)`: Updates the component with new props.
127
+ - `getQuery()`: Returns the current query string.
128
+ - `getMatchIndices(item)`: Returns the cached match indices for an item from the last filter operation, or `null` if no matches. Use this in `elementForItem` instead of calling `atom.ui.fuzzyMatcher.match()` directly.
129
+ - `getFilterQuery()`: Returns the filtered query string (applies `filterQuery` transformation).
130
+ - `setQueryFromSelection()`: Sets the query text from the active editor's selection. Returns `true` if successful, `false` if no editor, no selection, or selection contains newlines.
131
+ - `getSelectedItem()`: Returns the currently selected item.
132
+ - `selectPrevious()`: Selects the previous item.
133
+ - `selectNext()`: Selects the next item.
134
+ - `selectFirst()`: Selects the first item.
135
+ - `selectLast()`: Selects the last item.
136
+ - `selectNone()`: Deselects all items.
137
+ - `selectIndex(index)`: Selects the item at the given index.
138
+ - `selectItem(item)`: Selects the given item.
139
+ - `confirmSelection()`: Confirms the current selection.
140
+ - `cancelSelection()`: Cancels the selection.
141
+
142
+ ### Static Methods
143
+
144
+ #### `SelectListView.highlightMatches(text, matchIndices, options)`
145
+
146
+ Creates a DocumentFragment with highlighted match characters.
147
+
148
+ ```js
149
+ // In elementForItem, use getMatchIndices() to get cached match indices:
150
+ const matches = this.selectList.getMatchIndices(item) || [];
151
+ const fragment = SelectListView.highlightMatches(item.name, matches);
152
+ element.appendChild(fragment);
153
+
154
+ // With custom class name
155
+ const fragment = SelectListView.highlightMatches(item.name, matches, {
156
+ className: "my-highlight",
157
+ });
158
+ ```
159
+
160
+ - `text: String`: the text to highlight.
161
+ - `matchIndices: [Number]`: array of character indices to highlight.
162
+ - `options: Object` (optional):
163
+ - `className: String`: CSS class for highlighted spans; defaults to `'character-match'`.
164
+
165
+ Returns a `DocumentFragment` containing text nodes and `<span>` elements with the specified class.
166
+
167
+ #### `SelectListView.replaceDiacritics(str)`
168
+
169
+ Removes diacritical marks (accents) from a string.
170
+
171
+ ```js
172
+ SelectListView.replaceDiacritics("café"); // => 'cafe'
173
+ SelectListView.replaceDiacritics("naïve"); // => 'naive'
174
+ SelectListView.replaceDiacritics("Müller"); // => 'Muller'
175
+ ```
176
+
177
+ - `str: String`: the string to process.
178
+
179
+ Returns the string with diacritical marks removed. Uses `String.normalize('NFD')` internally.
180
+
181
+ #### `SelectListView.createTwoLineItem(options)`
182
+
183
+ Creates a two-line list item element with primary and optional secondary lines. This is a convenience helper for the common Atom/Pulsar two-line item pattern.
184
+
185
+ ```js
186
+ const li = SelectListView.createTwoLineItem({
187
+ primary: SelectListView.highlightMatches(item.name, matches),
188
+ secondary: item.description,
189
+ icon: ["icon-file-text"],
190
+ });
191
+ ```
192
+
193
+ - `options: Object`:
194
+ - `primary: String|Node`: Primary line content (text string or DOM node)
195
+ - `secondary: String|Node` (optional): Secondary line content
196
+ - `icon: [String]` (optional): Icon class names to add to primary line (adds `icon` class automatically)
197
+
198
+ Returns an `HTMLLIElement` with the structure:
199
+
200
+ ```html
201
+ <li class="two-lines">
202
+ <div class="primary-line icon [icon]">[primary]</div>
203
+ <div class="secondary-line">[secondary]</div>
204
+ </li>
205
+ ```
206
+
207
+ #### `SelectListView.setScheduler(scheduler)`
208
+
209
+ Sets the etch scheduler.
210
+
211
+ #### `SelectListView.getScheduler()`
212
+
213
+ Gets the current etch scheduler.
214
+
215
+ ## Example
216
+
217
+ ```js
218
+ const SelectListView = require("pulsar-select-list");
219
+ const fs = require("fs");
220
+ const path = require("path");
221
+
222
+ class MyFileList {
223
+ constructor() {
224
+ this.selectList = new SelectListView({
225
+ className: "my-package my-file-list",
226
+ items: [],
227
+ filterKeyForItem: (item) => item.name,
228
+ emptyMessage: "No files found",
229
+ helpMarkdown: fs.readFileSync(path.join(__dirname, "help.md"), "utf8"),
230
+
231
+ willShow: () => {
232
+ this.loadFiles();
233
+ },
234
+
235
+ elementForItem: (item, options) => {
236
+ const li = document.createElement("li");
237
+ const matches = this.selectList.getMatchIndices(item) || [];
238
+ li.appendChild(SelectListView.highlightMatches(item.name, matches));
239
+
240
+ li.addEventListener("contextmenu", () => {
241
+ this.selectList.selectIndex(options.index);
242
+ });
243
+
244
+ return li;
245
+ },
246
+
247
+ didConfirmSelection: (item) => {
248
+ atom.workspace.open(item.path);
249
+ this.selectList.hide();
250
+ },
251
+
252
+ didCancelSelection: () => {
253
+ this.selectList.hide();
254
+ },
255
+ });
256
+ }
257
+
258
+ loadFiles() {
259
+ // Load files and update items
260
+ this.selectList.update({ items: this.files });
261
+ }
262
+
263
+ toggle() {
264
+ this.selectList.toggle();
265
+ }
266
+
267
+ destroy() {
268
+ this.selectList.destroy();
269
+ }
270
+ }
271
+ ```
272
+
273
+ ### Advanced: Custom Score Modifier
274
+
275
+ Use `filterScoreModifier` to customize ranking:
276
+
277
+ ```js
278
+ const selectList = new SelectListView({
279
+ items: files,
280
+ elementForItem: (item) => {
281
+ const li = document.createElement("li");
282
+ li.textContent = item.path;
283
+ return li;
284
+ },
285
+ filterKeyForItem: (item) => item.path,
286
+ // Boost score by proximity (items closer to current file rank higher)
287
+ filterScoreModifier: (score, item) => score / item.distance,
288
+ didConfirmSelection: (item) => {
289
+ atom.workspace.open(item.path);
290
+ },
291
+ });
292
+ ```
293
+
294
+ ## Migration from atom-select-list
295
+
296
+ If you're migrating from `atom-select-list`, here are the key changes:
297
+
298
+ ### Package.json
299
+
300
+ ```diff
301
+ "dependencies": {
302
+ - "atom-select-list": "^0.8.1",
303
+ + "pulsar-select-list": "^1.0.0"
304
+ }
305
+ ```
306
+
307
+ ### Import
308
+
309
+ ```diff
310
+ -const SelectListView = require('atom-select-list')
311
+ +const SelectListView = require('pulsar-select-list')
312
+ ```
313
+
314
+ ### Panel Management
315
+
316
+ The component now manages its own panel. Remove custom panel handling:
317
+
318
+ ```diff
319
+ -this.panel = null
320
+ -this.previouslyFocusedElement = null
321
+
322
+ this.selectList = new SelectListView({
323
+ + className: 'my-list',
324
+ items: [],
325
+ + willShow: () => this.onWillShow(),
326
+ // ...
327
+ })
328
+ -this.selectList.element.classList.add('my-list')
329
+
330
+ -showView() {
331
+ - this.previouslyFocusedElement = document.activeElement
332
+ - if (!this.panel) {
333
+ - this.panel = atom.workspace.addModalPanel({ item: this.selectList })
334
+ - }
335
+ - this.panel.show()
336
+ - this.selectList.focus()
337
+ -}
338
+ -
339
+ -hideView() {
340
+ - this.panel.hide()
341
+ - if (this.previouslyFocusedElement) {
342
+ - this.previouslyFocusedElement.focus()
343
+ - }
344
+ -}
345
+ -
346
+ -toggleView() {
347
+ - if (this.panel && this.panel.isVisible()) {
348
+ - this.hideView()
349
+ - } else {
350
+ - this.showView()
351
+ - }
352
+ -}
353
+
354
+ // Use built-in methods:
355
+ -this.toggleView()
356
+ +this.selectList.toggle()
357
+
358
+ -this.hideView()
359
+ +this.selectList.hide()
360
+ ```
361
+
362
+ ### Diacritics
363
+
364
+ Replace external diacritics library with built-in static method:
365
+
366
+ ```diff
367
+ -const Diacritics = require('diacritic')
368
+
369
+ -Diacritics.clean(text)
370
+ +SelectListView.replaceDiacritics(text)
371
+ ```
372
+
373
+ ### Match Highlighting
374
+
375
+ Use `getMatchIndices(item)` to get cached match indices from the filter:
376
+
377
+ ```diff
378
+ elementForItem(item, options) {
379
+ - const query = this.query || ''
380
+ - const matches = query
381
+ - ? atom.ui.fuzzyMatcher.match(item.name, query, { recordMatchIndexes: true }).matchIndexes
382
+ - : []
383
+ + const matches = this.selectList.getMatchIndices(item) || []
384
+ el.appendChild(SelectListView.highlightMatches(item.name, matches))
385
+ }
386
+ ```
387
+
388
+ ### Help Message
389
+
390
+ Replace `infoMessage` with `helpMarkdown` and the built-in `select-list:help` command:
391
+
392
+ ```diff
393
+ this.selectList = new SelectListView({
394
+ - // No help by default
395
+ + helpMarkdown: fs.readFileSync(path.join(__dirname, 'help.md'), 'utf8'),
396
+ })
397
+
398
+ -atom.config.observe('my-package.showKeystrokes', (value) => {
399
+ - this.selectList.update({ infoMessage: value ? [...] : null })
400
+ -})
401
+ ```
402
+
403
+ Create a `help.md` file with your help content:
404
+
405
+ ```markdown
406
+ - **Enter** Confirm selection
407
+ - **Alt+Enter** — Alternative action
408
+ ```
409
+
410
+ Press ` in editor to switch into help view.