Skip to content
Martin Wendt edited this page Dec 1, 2019 · 12 revisions

Vision

Trees should support selection of one ore more nodes.

Status

This feature is already implemented (demo).

Yet there are still open questions, so this feature is open for discussion and the API is subject to change.

Please discuss here: https://github.com/mar10/fancytree/issues/12

Requirements

  • Nodes may have a state ´selected´, which is independent from the active state.
  • There must be a visual representation, that makes this distinguishable.
  • The selection state must be easily queried by JS. It must also easily be submitted to the server.
  • Selection must be controllable using events (i.e. return false on beforeSelect).
  • Selection must possible using checkboxes.
    Checkboxes may be hidden globally or by node.
    Checkboxes may be disabled globally or by node.
  • Selection must be possible using the API.
  • We support different modes
    • 1: single
      Only one (or no) node is selected at any time. Selecting another node will deselect the previous.
    • 2: multi
      No, one, or more nodes may be selected at any time.
    • 3: multi-hier
      No, one, or more nodes may be selected at any time.
      In this mode only the selection status of end nodes (i.e. leaves) is relevant.
      (De)selecting a parent node will (de)select all children.
      If some (but not all) descendants of a node are selected, this node is displayed as 'partly selected'.

Discussion

Resources

Related issues

Problems

The feature is basically working, but currently there are some main issues and open questions:

  1. Precedence in simple mode
    When the tree source data (or initial Ajax response) contains more than one selected node, should we fix it? If so: how?
  2. 'unselectable' option
    Nodes may be marked 'unselectable', which should prevent changing the selection state by user clicks.
    An unselectable node by be initialized 'selected', which is fine. The user may not deselect it using a click.
    (De)Selection using the API should still be possible
  3. 'unselectable' option in multi-hier mode
    Should the unselectable node preserve its state when the parent is (de)selected? This would mean, that after selecting the parent, the parent is still marked 'partsel'.
    Or we mark the parent 'selected', even if some children are unselectable.
  4. Precedence in multi-hier mode
    When the tree source data (or initial Ajax response) contains inconsistent data, should we fix it?
    If so: how?
    Example: a node is marked as selected, but its children are not. Should we select all children, or deselect the parent?
  5. Lazy trees in multi-hier mode
    When we select a lazy/unloaded node, this means 'select all children'.
    Now we expand the parent, which triggers lazy loading. The response may contain unselected nodes; what now? (see issue #247)
  6. Persistence
    This feature must be compatible with persistence (see SpecPersistence)

Proposal (pre v2.23)

We assume the following use case as most likely:

The server does maintain the tree structure, but not the selection state. For example we have a hierarchical structure of options, but any user will choose its own selection set.
If the user has finished its selections, she will submit this data to the server (or the selection set is only used by client-side JavaScript).

This allows for the following decisions:

  • When a user expresses a wish by actively selecting a node, this takes precedence.
    So if we select a lazy parent, then expand it, all children are expected to be selected, even if the server sends them unselected.
  • If the server sends selected nodes, we treat this as initial default selection.
  • If this data is inconsistent, we don't repair it!

But we should also make the following - less likely - use case possible:

The tree represents a single data model that is maintained on the server - or the server maintains the tree model by user. Selecting a node will modify the global data. If the tree sends selected nodes, this should take precedence.

Thoughts about unselectable nodes

  • We have to consider that unselectable nodes can contain selected nodes (either defined in the source or lazy-loaded)
  • unselectable means 'can never be selected or deselected by user interaction'.
    (This covers 'unselectable means: can never be selected by user' if programmers take care not to set the selected flag in the child data). So basically unselectable disables the checkbox.
  • (De)Selecting a parent should never change unselectable subnodes.
    (But it is possible to implement simple custom select handlers to overide this using the API)
  • Clicking a parent checkbox must be consistent with the child statuses.
    and
    A selected parent could mean 'all children selected' or 'all children that a user can select are selected'.

    The first case could lead to situations where the user clicks a parent checkbox, but it remains 'partsel', since it still contains unchecked unselectable children -> this would feel like a bug, especially if the parent is collapsed. It also is hard to implement if the children are lazy-loaded.

    The second case would always toggle the parent checkbox and is easy to implment without knowing unloaded lazy child statuses.
    It may however produce situations where the parent is checked although it contains unchecked unselectable children -> depending on the use case this may feel like a bug. It could be mitigated by giving unselectable nodes a visual clue (e.g. gray out or remove the checkbox).

    Conclusion: A selected parent means 'all children that a user can select are selected'.

After all it is always possible to fallback to selectMode 2 and implement custom select event handlers.

Specification (v2.23+)

Selection behavior is controlled by several options:

  • Tree options .selectMode, .checkbox .unselectable, .unselectableIgnore, .unselectableStatus,
  • Node options checkbox .selected, .partsel, .unselectable, .unselectableIgnore, .unselectableStatus,
  • Node methods .setSelected(), .isSelected()
  • Tree methods .getSelectedNodes(), .fixSelection3AfterClick(), .fixSelection3FromEndNodes()
  • Tree events .beforeSelect(), .select()

A node is considered selected if, and only if node.selected === true.
The preferred way to access this property is by node.setSelected(bool) and node.isSelected().

NOTE: partsel is alway true if selected is true. It should only be considered for unselected nodes. TODO: #715: local configuration of tri-state behavior

'unselectable' nodes

For nodes with unselectable: true, these rules apply in general:

  • The checkbox (if display is enabled by the checkbox option) is grayed out.
  • Clicking the checkbox is ignored.
  • Nodes are still (de)selectable using the API (.setSelected(bool)).
  • If node.selected is true, the node is displayed as selected / checked.

Behavior of 'selectMode: 1' (single select)

  • Only one node at a time may be selected (like a radio group).
  • The node.setSelected(true) method, deselects the previously selected node (but does not scan the whole tree for nodes that may have selected set).
  • unselectable nodes are not considered here, regardless of unselectableStatus.
  • Lazy loaded children are not automatically processed.
    However this could be implemented in the .loadChildren() event.

Behavior of 'selectMode: 2' (multi-select)

No special handling is required in this mode.

  • Any number of nodes may be selected.
  • Lazy loaded children are not automatically processed.
  • General rules for 'unselectable' nodes apply.

Behavior of 'selectMode: 3' (multi-hierarchical)

  • Clicking a parent node will toggle the node's state and then propagate the status down to all descendants.

  • Propagation is also triggered by node.setSelected().

  • Propagation is also applied upwards to update all ancestor nodes' status:<br A parent node is marked selected/checked if all descendants are selected.
    It is marked unselected/unchecked if all descendent nodes are deselected.
    Otherwise (mixed status of descendants) it is marked 'partially selected'.

  • For nodes with unselectable: true, these rules apply:

    • The checkbox (if display is enabled by the checkbox option) is grayed out.
    • If node.selected is true, the node is displayed as selected / checked (but grayed out).
    • Clicking the checkbox is ignored.
    • Nodes are still (de)selectable using the API (.setSelected(bool)).
    • Selecting a parent node will propagate the status, even to children that are marked 'unselectable'.
      However, if the unselectable node has unselectableStatus set to a boolean value, this value will take precedence and be used as selected status.
    • TODO:
  • After lazy-loding children, the status will be fixed:

    • The server knows the thruth:
      If JSON data contains selected: true or selected: false, this will take precedence.
    • However, if 'selected' is omitted or undefined, the selection status is calculated from the TODO.
  • If the server sends selected nodes, we treat this as initial default selection.

  • If this data is inconsistent, we don't repair it!

Thoughts about unselectable nodes

  • We have to consider that unselectable nodes can contain selected nodes (either defined in the source or lazy-loaded)
  • unselectable means 'can never be selected or deselected by user interaction'.
    (This covers 'unselectable means: can never be selected by user' if programmers take care not to set the selected flag in the child data). So basically unselectable disables the checkbox.
  • (De)Selecting a parent should never change unselectable subnodes.
    (But it is possible to implement simple custom select handlers to overide this using the API)
  • Clicking a parent checkbox must be consistent with the child statuses.
    and
    A selected parent could mean 'all children selected' or 'all children that a user can select are selected'.

    The first case could lead to situations where the user clicks a parent checkbox, but it remains 'partsel', since it still contains unchecked unselectable children -> this would feel like a bug, especially if the parent is collapsed. It also is hard to implement if the children are lazy-loaded.

    The second case would always toggle the parent checkbox and is easy to implment without knowing unloaded lazy child statuses.
    It may however produce situations where the parent is checked although it contains unchecked unselectable children -> depending on the use case this may feel like a bug. It could be mitigated by giving unselectable nodes a visual clue (e.g. gray out or remove the checkbox).

    Conclusion: A selected parent means 'all children that a user can select are selected'.

After all it is always possible to fallback to selectMode 2 and implement custom select event handlers.

Current Implementation

(See the demo).

Select mode 3 (multi-hier) is implemented, so that (de-)selection of nodes by the user will update the tree automatically .
In addition, two API functions are available to implement desired behavior after lazy loading or manipulating the tree structure:

  • fixSelection3AfterClick()
    Fix selection status, after this node was (de)selected in multi-hier mode. This includes (de)selecting all children.
    This method is called internally after checkbox clicks, but may be handy in other situations as well.

  • fixSelection3FromEndNodes()
    Fix selection status for multi-hier mode. Only end-nodes are considered to update the descendants branch and parents.
    Should be called after this node has loaded new children or after children have been modified using the API.

Example:

Fix the check-status of children after lazy load. This assumes that the expanded parent node should apply its status to the children, even if the server sent different selection statuses:

$("#tree").fancytree({
    ...
    loadChildren: function(event, ctx) {
        ctx.node.fixSelection3AfterClick();
    },
});

Fix the check-status of all nodes, assuming that the end nodes currently contain the truth. This will repair the status, after nodes have been added or removed using the API:

$.ui.fancytree.getTree("#tree").getRootNode().fixSelection3FromEndNodes();
Clone this wiki locally