ツリー (tree)

ツリー では、表や階層ツリーを作成できます。

  • tree … ツリー全体を内包する要素
  • treecols … treecolを内包する要素
  • treecol … ツリーの列を定義し、列の見出しとなる要素
  • treechildren … ツリーの行を内包する要素
<tree flex="1">

  <treecols>
    <treecol id="a" label="AAA" flex="1" />
    <treecol id="b" label="BBB" flex="2" />
  </treecols>

  <treechildren>

    <treeitem>
      <treerow>
        <treecell label="1" />
        <treecell label="2" />
      </treerow>
    </treeitem>

    <treeitem>
      <treerow>
        <treecell label="3" />
        <treecell label="4" />
      </treerow>
    </treeitem>

  </treechildren>

</tree>

tree要素

seltype属性

選択可能な範囲を設定します。seltype - Mozilla | MDN

選択可能な範囲 選択状態の例
multiple 複数の行
single 1つの行のみ
cell 1つのセルのみ
text 1つのセルのテキストのみ

editable属性

editable="true"とすることで、ユーザーがツリーのセルを編集できるようになります。tree.editable - Mozilla | MDN

<tree editable="true" flex="1">

特定のセルの編集を認めないならば、そのセルにeditable="false"を指定します。

<treecell editable="false" label="TEST" />

hidecolumnpicker属性

hidecolumnpicker="true"とすると、表示列を選択するコラムピッカを隠せます。

rows属性

表示する行数を指定できます。rows - Mozilla | MDN

<tree rows="3">

ただし行数の制限が不要ならば、flex属性を指定しコンテナのサイズに合わせて伸縮するようにした方が、表示領域を活用できます。

<tree flex="1">

treecols要素

ツリーの列とその見出しを定義します。

treecol要素

treecolのid属性はMozilla 1.8以降では必須ではありませんが、列の位置が適切に処理されるために指定すべきです。ツリーウィジェット関連の変更点 | MDN

列のリサイズ

列の幅を調整できるようにするには、treecol要素の間にsplitter要素を設定します。さらにいずれかのtreecol要素に、flex属性を指定します。

<treecols>
  <treecol id="a" label="A" />
  <splitter class="tree-splitter" />
  <treecol id="b" label="B" flex="1" />
</treecols>

ここでスタイルに用いている'tree-splitter'は、ツリーのスプリッター用に定義されているクラスです。ちなみにその定義はchrome://global/content/xul.cssにあり、次のような内容です。tree-splitter - Mozilla | MDN

.tree-splitter {
  width: 0px;
  max-width: 0px;
  min-width: 0% ! important;
  min-height: 0% ! important;
  -moz-box-ordinal-group: 2147483646;
}

複数の列間を持つツリーに対して、特定の列間にのみスプリッターを設定しても、それが機能しないことがあります。よってリサイズに対応させるならば、すべての列間にスプリッターを設定します。

列の状態の復元

ユーザーに列の幅、順序、表示状態の変更を認めるならば、ツリーが再び表示されたときにその状態を復元すべきです。幸いこれは永続化で簡単に実現でき、treecol要素にidとpersist属性を設定するだけです。列の状態を保存する - 高度なツリー機能 - XUL | MDN

<treecols>
  <treecol id="a" label="A" flex="1" persist="width ordinal hidden" />
  <splitter class="tree-splitter" />
  <treecol id="b" label="B" flex="1" persist="width ordinal hidden" />
</treecols>

階層ツリー (Hierarchical trees)

子要素をインデントして表示し、それを折りたためるようにしたツリーです。

子となる項目をtreechildren要素に内包させ、その親のtreeitemにcontainer="true"とopen="true"を指定します。こうすることで要素が階層化され、子要素が折りたたみ可能となります。また見出しとなるtreecolにprimary属性を設定することで、その列に開閉可能を表すアイコンが表示され、子要素はインデントされるようになります。

<tree flex="1">

  <treecols>
    <treecol id="a" label="AAA" primary="true" flex="1" />
    <treecol id="b" label="BBB" flex="2" />
  </treecols>

  <treechildren>
    <treeitem container="true" open="true">
      <treerow>
        <treecell label="1" />
      </treerow>

      <treechildren>
        <treeitem>
          <treerow>
            <treecell label="3" />
            <treecell label="4" />
          </treerow>
        </treeitem>
        <treeitem>
          <treerow>
            <treecell label="5" />
            <treecell label="6" />
          </treerow>
        </treeitem>
      </treechildren>

    </treeitem>
  </treechildren>
</tree>

カスタムツリービュー (Custom Tree Views)

カスタムツリービューとはカスタムビューを用いて生成されたツリーのことで、ツリーをスクリプトから効率よく制御できます。それにはTree.viewプロパティに、nsITreeViewインターフェイスを実装したオブジェクトを設定します。

<tree id="tree" flex="1">
  <treecols>
    <treecol id="a" label="AAA" flex="1" />
    <treecol id="b" label="BBB" flex="1" />
  </treecols>
  <treechildren />
</tree>

<script>
  var data = [ [1,2], [3,4], [5,6] ];
  var treeView =
  {
    get rowCount() { return data.length; },
    getCellText: function( row,col )
    {
      return data[ row ][ col.index ];
    }
  };

  document.getElementById( 'tree' ).view = treeView;
</script>

セルを表示するだけならばrowCountとgetCellText()を実装するだけで十分ですが、これだけではユーザーの操作によってNS_ERROR_XPC_JSOBJECT_HAS_NO_FUNCTION_NAMED例外が発生することがあります。よって不要であってもいくつかのメソッドは実装すべきです。それには次のメソッドが該当します。

cycleHeader: function( col ) {},
isContainer: function( index ) { return false; },
getParentIndex: function( rowIndex ) { return -1; },

なお、このようにコンテンツビルダを用いず構築したツリーはDOMノードを持たないため、viewプロパティからTreeContentViewの関数を利用することはできません。 contentView - tree - XUL | MDN nsITreeContentView - Mozilla | MDN javascript - getItemAtIndex is not a function error - Stack Overflow

getCellText()

AString getCellText(
    in long row,         // 行の番号
    in nsITreeColumn col // セルの列 (列の番号ではない)
    );
getCellText() - nsITreeView - Mozilla | MDN

引数のrowは行の番号ですが、colは列の番号ではありません。列の番号が必要ならば、このnsITreeColumnオブジェクトのindexプロパティからcol.indexとして取得できます。Attributes - nsITreeColumn - Mozilla | MDN

このメソッドからは、文字列と評価される値を必ず返すようにします。さもなくば編集状態ではないときにキー入力を受けた場合、keyNavigate()から「cellText is null」としてTypeError例外が発生します。

ユーザーによるセルの編集

ユーザーによるセルの編集を可能にするには、tree要素のediable属性をtrueに設定すると共に、treeViewのisEditable()がtrueを返すようにします。さらにユーザーによるセルの編集時に呼び出されるsetCellText()でセルの情報を更新するようにし、getCellText()ではその情報を返すようにします。

<tree id="tree" editable="true" flex="1">
  <treecols>
    <treecol id="a" label="AAA" flex="1" />
    <treecol id="b" label="BBB" flex="1" />
  </treecols>
  <treechildren/>
</tree>

<script>
  var data = [ [1,2], [3,4], [5,6] ];

  var treeView =
  {
    get rowCount() { return data.length; },
    getCellText: function( row,col )
    {
      return data[ row ][ col.index ];
    },
    setCellText: function( row, col, value )
    {
      data[ row ][ col.index ] = value;
    },
    isEditable( row, col ) { return true; },
  };

  document.getElementById('tree').view = treeView;
</script>

rowCountが実際より小さな行数を返すと、その行数を超える位置の行は編集可能となりません。

編集状態のセルの取得

Tree.editingColumnとTee.editingRowから、編集状態となっているセルの位置を取得できます。

対象 プロパティ 編集中ではない場合の値
Tree.editingColumn nsITreeColumn null
Tee.editingRow 整数 -1

編集状態のセルの指定

スクリプトからセルを編集状態とするには、Tree.startEditing()を呼びます。

startEditing(
    in long row,
    in nsITreeColumn column
    )
startEditing - XUL | MDN

たとえば選択されている行の最初の列を編集状態とするには、次のようにします。

tree.startEditing(
    tree.view.selection.currentIndex,
    tree.columns.getFirstColumn()
    );
Change edit cell in XUL Tree with keyboard events • mozillaZine Forums

選択されている項目の取得

選択されている行の情報は、nsITreeView.selectionで取得できます。複数選択を無効にしているならば、選択されている行は多くても1つしかないため、nsITreeSelection.currentIndexを参照するだけで取得できます。選択されている行がない場合は、このプロパティは-1を返します。

tree.view.selection.currentIndex;
Attributes - nsITreeSelection - Mozilla | MDN

このプロパティはtreeにも実装されているため、

tree.currentIndex;
currentIndex - tree - Mozilla | MDN

のように簡潔に記述することもできます。一方で複数選択が有効ならば選択範囲を個別に取得する必要があるため、次のように少し手間がかかります。

var indexes = [];

var start = {};
var end = {};

var numRanges = tree.view.selection.getRangeCount();
for( var k = 0; k < numRanges; k++ )
{
    tree.view.selection.getRangeAt( k, start, end );
    for( var i = start.value; i <= end.value; i++ ) indexes.push( i );
}
Multiple Selection - Tree Selection - Mozilla | MDN

getRangeAt()はインデックスの昇順で結果を返すため、このようにして得た選択範囲のインデックスも、昇順にソートされた状態で取得できます。

選択されている列の取得

seltype属性でcellかtextを指定しセル単位の選択を可能としているならば、選択されている列の情報も取得できます。それはnsITreeSelection.currentColumnから得られ、選択されている列がnsITreeColumn型で返されます。セル単位の選択が無効な状態、つまりseltypeがmultipleかsingleの場合には、このプロパティはつねにnullを返します。

tree.view.selection.currentColumn;

ところでこのcurrentColumnは現在選択されている行が存在せず、nsITreeSelection.currentIndexが-1を返す状況でも、最後に選択されていた列の情報を返します。よって正確な情報を得るには、行の選択状態も同時に確認すべきです。

項目の選択

スクリプトから特定の項目を選択できます。1つの行を選択するだけならば、nsITreeSelection.select()でそのインデックスを指定するだけです。

void select( in long index );
select() - nsITreeSelection - Mozilla | MDN

複数の行を選択するにはnsITreeSelection.rangedSelect()を呼び出します。ただし複数選択を無効にしているならば当然それはできず、このメソッドで1つの行だけを選択するように指定しても、すでに選択されている行があるならばそれもできません。

void rangedSelect(
    in long startIndex,
    in long endIndex,
    in boolean augment
    );
rangedSelect() - nsITreeSelection - Mozilla | MDN

ドラッグによる行の移動

ツリーの行をドラッグして、順番を入れ替える方法を考えます。

<tree id="tree" seltype="single" flex="1">
  <treecols>
    <treecol id="a" label="AAA" flex="1" />
    <treecol id="b" label="BBB" flex="1" />
  </treecols>
  <treechildren ondragstart="handleDragEvent( event );" />
</tree>

<script>
  var DRAG_TYPE = 'application/x-moz-tree';
  var data = [ [1,2], [3,4], [5,6] ];

  function handleDragEvent( event )
  {
    var tree = event.currentTarget.parentNode;
    switch( event.type )
    {
      // ドラッグが開始された
      case 'dragstart':
        if( tree.view.selection.currentIndex != -1 )
        {
          event.dataTransfer.setData( DRAG_TYPE, 'dummy' );
          event.dataTransfer.dropEffect = 'move';
        }
        break;
    }
  }

  var treeView =
  {
    get rowCount() { return data.length; },
    getCellText: function( row,col )
    {
      return data[ row ][ col.index ];
    },
    canDrop: function( index, orientation, dataTransfer )
    {
      // 指定のドラッグ型であり、かつ選択されている行とドロップされた行のインデックスが異なるならば、ドロップ可能とする
      return ( dataTransfer.types.contains( DRAG_TYPE )
        && this.selection.currentIndex != index );
    },
    drop: function( row, orientation, dataTransfer )
    {
      if( !this.canDrop( row, orientation, dataTransfer ) ) return;

      // ドロップされた行の位置へ、選択されている行を移動する
      var targetIndex = this.selection.currentIndex;
      data.splice( row, 0, data.splice( targetIndex, 1 )[ 0 ] );
    }
  };

  document.getElementById( 'tree' ).view = treeView;
</script>

この例では、複数行のドラッグを認めていません。

列の操作

Tree.columnsプロパティから、Treeの列をnsITreeColumnsオブジェクトとして取得できます。そしてそのnsITreeColumnsからはgetColumnAt()などで、個々の列をnsITreeColumnオブジェクトとして取得できます。

nsITreeColumn getColumnAt(
    in long index
    );
getColumnAt() - nsITreeColumns - Mozilla | MDN

ツリーボックスオブジェクト (Tree Box Objects)

ツリーボックスオブジェクトは、Tree.treeBoxObjectまたはTree.boxObject※1から取得できます。これはnsITreeView.setTree()で渡されるオブジェクトと同値です。treeBoxObject - tree - Mozilla | MDN

※1 boxObjectからはnsIBoxObjectが返され、nsITreeBoxObjectのメソッドにアクセスできない場合があります。よってTree.treeBoxObjectから取得するようにします。

ツリーの再描画

ツリーの要素を修正したのにそれが反映されないならば、その範囲の応じて下表のメソッドで再描画を要求します。

セル  
1つ × ×
void invalidateCell(
    in long row,
    in nsITreeColumn col
    );
複数 1つ ×
void invalidateRow(in long index);
複数 × 1つ
void invalidateColumn(in nsITreeColumn col);
複数 複数 ×
void invalidateRange(
    in long startIndex,
    in long endIndex
    );
複数 複数 1つ
void invalidateColumnRange(
    in long startIndex,
    in long endIndex,
    in nsITreeColumn col
    );
複数 複数 複数
void invalidate();
invalidate() - nsITreeBoxObject - Mozilla | MDN
Redrawing the Tree - Tree Box Objects - Mozilla | MDN

行の追加や削除

ツリーの行数を変化させたならばrowCountChanged()を呼び出し、それを通知せねばなりません。

void rowCountChanged(
    in long index, // 追加または削除された行の位置
    in long count  // 追加または削除された行数。削除した場合は負数とする
    );
rowCountChanged() - nsITreeBoxObject - Mozilla | MDN

例示されているコードでは、rowIndexの行を削除したならばindexにはrowIndex+1を指定するものとされています。しかしいくつかのアドオンのソースで確認しましたが、ここはやはりrowIndexとすべきです。

たとえば配列dataを参照しているtreeへの行の追加や削除は、つぎのようにそれを通知します。

data.push( 'SAMPLE' ); // 末尾に項目を追加
tree.treeBoxObject.rowCountChanged( data.length - 1, 1 );
tree.treeBoxObject.ensureRowIsVisible( data.length - 1 ); // 追加した行が見えるようにスクロールする
data.splice( index, 1 ); // indexの位置の項目を削除
tree.treeBoxObject.rowCountChanged( index, -1 );

動的に行数を変更するならば、viewもその行数rowCountをgetterにより動的に返す必要があります。adding items to a custom tree view • mozillaZine Forums

Firefoxアドオンの情報サイトから、まとめて検索