[Atomパッケージ]SelectListViewの使い方覚書


## はじめに

Atomでコマンドパレットを開いたときなど、１行入力欄の下に選択候補が並んで表示されるのだが、自作のパッケージでどうやって表示するのか調べてみた。

## 準備

基本的には、'atom-space-pen-views'に含まれるSelectListViewを継承したクラスを使う。
一般的に、Atomのコンポーネント（？）をどうやって表示したら良いのかは、コマンドパレットから```Styleguide```を表示すると書いてある。今回もStyleguideを参考にした。

'atom-space-pen-views'は普通にnodeのパッケージなので、ターミナルから

```shell:
$ npm install --save atom-space-pen-views
$ npm install --save fuzzaldrin
```

でインストールしておく。
今回は、ファジー検索パッケージfuzzaldrinも使用したので、一緒にインストールした。

## 実装

[コマンドパレットの実装](https://github.com/atom/command-palette/blob/master/lib/command-palette-view.coffee)や[SelectListViewの実装](https://github.com/atom/atom-space-pen-views/blob/master/src/select-list-view.coffee)を参考に（写経して）書いていく。

基本的には、viewForItem()とpopulateList()を適切に書いてやれば動くようだが、populateList()のほうは、@itemsに適切なデータ（配列）を入れておいて、getFilterKey()で配列内のオブジェクトからソート時のキーになる名前を返してやれば動くっぽい。

@itemsにいれるデータだが、

```javascript:
@items = [
  {
    key1: value1_1
    key2: value2_1
  },
  {
    key1: value1_2
    key2: value2_2
  }
];
```

のような配列だと、SelectListViewがよきに計らってくれるので楽に実装出来る。

実際のコードは以下。

```coffee:
{SelectListView, $, $$} = require 'atom-space-pen-views'
{match} = require 'fuzzaldrin'

module.exports =
class MacroNameSelectListView extends SelectListView
  panel: null
  callback: null

  initialize: (@listOfItems) ->
    super
    @setItems(@listOfItems)

  show: ->
    # この関数はひたすら写経
    @panel ?= atom.workspace.addModalPanel(item: this)
    @panel.show()

    @storeFocusedElement()

    if @previouslyFocusedElement[0] and @previouslyFocusedElement[0] isnt document.body
      @eventElement = @previouslyFocusedElement[0]
    else
      @eventElement = atom.views.getView(atom.workspace)

    @setItems(@items)

    @focusFilterEditor()  # これで入力フォームにフォーカスする。

  hide: ->
    @panel?.hide()

  addItem: (item) ->
    @items ?= []
    @items.push
      name: item    # ここの'name'が、getFilterKey()で返すキーになる

    @setItems @items

  getFilterKey: ->  # これがないとviewForItem()が動かない
    'name'

  clearText: ->
    @filterEditorView.setText('')

  setCallback: (callback) ->
    @callback = callback

  focus: ->
    @focusFilterEditor()

  getElement: ->
    @element

  cancel: ->
    # キャンセル時(ESCキー)の処理
    @hide()

  confirmed: ({name}) ->
    # 決定時(Enterキーなど)の処理
    @clearText()
    @callback?(name)

  viewForItem: ({name}) ->
    # だいたい写経
    # Style matched characters in search results
    filterQuery = @getFilterQuery()  # 入力欄に入っているテキスト
    matches = match(name, filterQuery)

    $$ ->
      highlighter = (command, matches, offsetIndex) =>
        lastIndex = 0
        matchedChars = [] # Build up a set of matched chars to be more semantic

        for matchIndex in matches
          matchIndex -= offsetIndex
          continue if matchIndex < 0 # If marking up the basename, omit command matches
          unmatched = command.substring(lastIndex, matchIndex)
          if unmatched
            @span matchedChars.join(''), class: 'character-match' if matchedChars.length
            matchedChars = []
            @text unmatched
          matchedChars.push(command[matchIndex])
          lastIndex = matchIndex + 1

        @span matchedChars.join(''), class: 'character-match' if matchedChars.length

        # Remaining characters are plain text
        @text command.substring(lastIndex)

      @li class: 'event', 'data-event-name': name, =>
        @span title: name, -> highlighter(name, matches, 0)
```

## 追記：使用上の注意点

SelectListViewはあくまでもリストから選ぶために使うものなので、リストにないテキストを入力してもconfirmedは呼ばれない。