SelectTree.ts 9.67 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

import * as d3selection from 'd3-selection';
15
import {SvgTree, SvgTreeSettings, TreeNodeSelection} from '../../SvgTree';
16
import {TreeNode} from '../../Tree/TreeNode';
Benjamin Franzke's avatar
Benjamin Franzke committed
17
import {customElement} from 'lit/decorators';
18

19
20
21
22
23
24
25
interface SelectTreeSettings extends SvgTreeSettings {
  exclusiveNodesIdentifiers: '';
  validation: {[keys: string]: any};
  unselectableElements: Array<any>,
  readOnlyMode: false
}

26
@customElement('typo3-backend-form-selecttree')
27
28
export class SelectTree extends SvgTree
{
29
  public textPosition: number = 30;
30
31
32
33
34
  public settings: SelectTreeSettings = {
    unselectableElements: [],
    exclusiveNodesIdentifiers: '',
    validation: {},
    readOnlyMode: false,
35
    showIcons: true,
36
37
38
39
40
41
42
43
44
    marginTop: 15,
    nodeHeight: 20,
    indentWidth: 16,
    width: 300,
    duration: 400,
    dataUrl: '',
    defaultProperties: {},
    expandUpToLevel: null as any,
  };
45

46
  /**
47
   * Exclusive node which is currently selected
48
   */
49
50
  private exclusiveSelectedNode: TreeNode = null;

51
52
  constructor() {
    super();
53
    this.addIcons();
54
    this.addEventListener('typo3:svg-tree:nodes-prepared', this.prepareLoadedNodes);
55
56
  }

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
  /**
   * Expand all nodes and refresh view
   */
  public expandAll(): void {
    this.nodes.forEach((node: TreeNode) => { this.showChildren(node); });
    this.prepareDataForVisibleNodes();
    this.updateVisibleNodes();
  }

  /**
   * Collapse all nodes recursively and refresh view
   */
  public collapseAll(): void {
    this.nodes.forEach((node: TreeNode) => { this.hideChildren(node); });
    this.prepareDataForVisibleNodes();
    this.updateVisibleNodes();
  }

75
  /**
76
77
   * Node selection logic (triggered by different events) to select multiple
   * nodes (unlike SVG Tree itself).
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
   */
  public selectNode(node: TreeNode): void {
    if (!this.isNodeSelectable(node)) {
      return;
    }

    const checked = node.checked;
    this.handleExclusiveNodeSelection(node);

    if (this.settings.validation && this.settings.validation.maxItems) {
      if (!checked && this.getSelectedNodes().length >= this.settings.validation.maxItems) {
        return;
      }
    }

    node.checked = !checked;

95
    this.dispatchEvent(new CustomEvent('typo3:svg-tree:node-selected', {detail: {node: node}}));
96
    this.updateVisibleNodes();
97
98
  }

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
  public filter(searchTerm?: string|null): void {
    this.searchTerm = searchTerm;
    if (this.nodes.length) {
      this.nodes[0].expanded = false;
    }
    const regex = new RegExp(searchTerm, 'i');

    this.nodes.forEach((node: any) => {
      if (regex.test(node.name)) {
        this.showParents(node);
        node.expanded = true;
        node.hidden = false;
      } else {
        node.hidden = true;
        node.expanded = false;
      }
    });

    this.prepareDataForVisibleNodes();
118
    this.updateVisibleNodes();
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
  }

  /**
   * Finds and show all parents of node
   */
  public showParents(node: any): void {
    if (node.parents.length === 0) {
      return;
    }
    const parent = this.nodes[node.parents[0]];
    parent.hidden = false;
    // expand parent node
    parent.expanded = true;
    this.showParents(parent);
  }

135
136
  /**
   * Function relays on node.indeterminate state being up to date
137
138
   *
   * Fetches all visible nodes
139
   */
140
141
142
143
144
145
146
147
  public updateVisibleNodes(): void {
    super.updateVisibleNodes();
    const visibleRows = Math.ceil(this.viewportHeight / this.settings.nodeHeight + 1);
    const position = Math.floor(Math.max(this.scrollTop - (this.settings.nodeHeight * 2), 0) / this.settings.nodeHeight);

    const visibleNodes = this.data.nodes.slice(position, position + visibleRows);
    let nodes = this.nodesContainer.selectAll('.node')
      .data(visibleNodes, (node: TreeNode) => node.stateIdentifier);
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
    nodes
      .selectAll('.tree-check use')
      .attr('visibility', function(this: SVGUseElement, node: TreeNode): string {
        const checked = Boolean(node.checked);
        const selection = d3selection.select(this);
        if (selection.classed('icon-checked') && checked) {
          return 'visible';
        } else if (selection.classed('icon-indeterminate') && node.indeterminate && !checked) {
          return 'visible';
        } else if (selection.classed('icon-check') && !node.indeterminate && !checked) {
          return 'visible';
        } else {
          return 'hidden';
        }
      });
  }

165
  /**
166
167
168
   * Check whether node can be selected.
   * In some cases (e.g. selecting a parent) it should not be possible to select
   * element (as it's own parent).
169
   */
170
171
172
173
174
175
176
177
178
  protected isNodeSelectable(node: TreeNode): boolean {
    return !this.settings.readOnlyMode && this.settings.unselectableElements.indexOf(node.identifier) === -1;
  }
  /**
   * Add checkbox before the text element
   */
  protected appendTextElement(nodes: TreeNodeSelection): TreeNodeSelection {
    this.renderCheckbox(nodes);
    return super.appendTextElement(nodes)
179
180
  }

181
182
183
184
185
186
  /**
   * Adds svg elements for checkbox rendering.
   *
   * @param {Selection} nodeSelection ENTER selection (only new DOM objects)
   */
  private renderCheckbox(nodeSelection: TreeNodeSelection): void {
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
    // this can be simplified to single "use" element with changing href on click
    // when we drop IE11 on WIN7 support
    const g = nodeSelection.filter((node: TreeNode) => {
      // do not render checkbox if node is not selectable
      return this.isNodeSelectable(node) || Boolean(node.checked);
    })
      .append('g')
      .attr('class', 'tree-check')
      .on('click', (evt: MouseEvent, node: TreeNode) => this.selectNode(node));

    g.append('use')
      .attr('x', 28)
      .attr('y', -8)
      .attr('visibility', 'hidden')
      .attr('class', 'icon-check')
      .attr('xlink:href', '#icon-check');
    g.append('use')
      .attr('x', 28)
      .attr('y', -8)
      .attr('visibility', 'hidden')
      .attr('class', 'icon-checked')
      .attr('xlink:href', '#icon-checked');
    g.append('use')
      .attr('x', 28)
      .attr('y', -8)
      .attr('visibility', 'hidden')
      .attr('class', 'icon-indeterminate')
      .attr('xlink:href', '#icon-indeterminate');
  }
216
217

  /**
218
219
   * Check if a node has all information to be used.
   * create stateIdentifier if doesn't exist (for category tree)
220
   */
221
222
223
224
225
226
  private prepareLoadedNodes(evt: CustomEvent): void {
    let nodes = evt.detail.nodes as Array<TreeNode>;
    evt.detail.nodes = nodes.map((node: TreeNode) => {
      if (!node.stateIdentifier) {
        const parentId = (node.parents.length) ? node.parents[node.parents.length - 1] : node.identifier;
        node.stateIdentifier = parentId + '_' + node.identifier;
227
      }
228
229
230
231
      if (node.selectable === false) {
        this.settings.unselectableElements.push(node.identifier);
      }
      return node;
232
233
234
    });
  }

235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
  /**
   * Handle exclusive nodes functionality
   * If a node is one of the exclusiveNodesIdentifiers list,
   * all other nodes has to be unselected before selecting this node.
   *
   * @param {Node} node
   */
  private handleExclusiveNodeSelection(node: TreeNode): void {
    const exclusiveKeys = this.settings.exclusiveNodesIdentifiers.split(',');
    if (this.settings.exclusiveNodesIdentifiers.length && node.checked === false) {
      if (exclusiveKeys.indexOf('' + node.identifier) > -1) {
        // this key is exclusive, so uncheck all others
        this.disableSelectedNodes();
        this.exclusiveSelectedNode = node;
      } else if (exclusiveKeys.indexOf('' + node.identifier) === -1 && this.exclusiveSelectedNode) {

        // current node is not exclusive, but other exclusive node is already selected
        this.exclusiveSelectedNode.checked = false;
        this.exclusiveSelectedNode = null;
      }
    }
  }

258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
  /**
   * Add icons imitating checkboxes
   */
  private addIcons(): void {
    this.icons = {
      check: {
        identifier: 'check',
        icon: '<g width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">' +
          '<rect height="16" width="16" fill="transparent"></rect><path transform="scale(0.01)" d="M1312 256h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-832q0-66-47-113t-113-47zm288 160v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z"></path></g>'
      },
      checked: {
        identifier: 'checked',
        icon: '<g width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><rect height="16" width="16" fill="transparent"></rect><path transform="scale(0.01)" d="M813 1299l614-614q19-19 19-45t-19-45l-102-102q-19-19-45-19t-45 19l-467 467-211-211q-19-19-45-19t-45 19l-102 102q-19 19-19 45t19 45l358 358q19 19 45 19t45-19zm851-883v960q0 119-84.5 203.5t-203.5 84.5h-960q-119 0-203.5-84.5t-84.5-203.5v-960q0-119 84.5-203.5t203.5-84.5h960q119 0 203.5 84.5t84.5 203.5z"></path></g>'
      },
      indeterminate: {
        identifier: 'indeterminate',
        icon: '<g width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><rect height="16" width="16" fill="transparent"></rect><path transform="scale(0.01)" d="M1344 800v64q0 14-9 23t-23 9h-832q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h832q14 0 23 9t9 23zm128 448v-832q0-66-47-113t-113-47h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113zm128-832v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z"></path></g>'
      }
    }
  }
}