Passed
Push — master ( 2d0947...bb6643 )
by
unknown
02:34
created

NodeHoverSubscription   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
1
import { event, select } from 'd3-selection';
2
import { DependencyNode, TreeNode } from '../../components/types';
3
import {
4
    centerScreenToDimension,
5
    changeZoom,
6
    findGroupBackgroundDimension,
7
    findMaxDependencyLevel,
8
    getHighLightedLabelColor,
9
    hideHighlightBackground,
10
    highlight,
11
    zoomToHighLightedNodes,
12
} from './GraphHelpers';
13
import {
14
    LabelColors,
15
    MAXIMUM_ZOOM_SCALE,
16
    MINIMUM_ZOOM_SCALE,
17
    ElementIds,
18
    TextColors,
19
    ZOOM_DECREASE,
20
    ZOOM_INCREASE,
21
    ElementColors,
22
    FAST_TRANSITION_DURATION,
23
    TRANSITION_DURATION,
24
} from '../AppConsts';
25
import { zoom, zoomIdentity } from 'd3-zoom';
26
import {
27
    selectAllNodes,
28
    selectOverviewContainer,
29
    selectDetailsButtonWrapper,
30
    selectDetailsExitButtonWrapper,
31
    selectHighLightedNodes,
32
    selectOverviewZoom,
33
    selectTooltip,
34
} from './Selectors';
35
import { initializeDetailsView, shutdownDetailsView } from './DetailsDrawHelpers';
36
37
enum Subscriptions {
38
    HIGHLIGHT = 'click.highlight',
39
    RESET_HIGHLIGHT = 'click.resetHighlight',
40
    CHANGE_HIGHLIGHT_RANGE = 'keydown.changeHighlightRange',
41
    ZOOM_ON_ARROW_KEY = 'keydown.zoom',
42
    OPEN_DETAILS = 'click.openDetails',
43
    CLOSE_DETAILS = 'click.closeDetails',
44
    SHOW_TOOLTIP = 'mouseover.tooltip',
45
    HIDE_TOOLTIP = 'mouseout.tooltip',
46
}
47
48
export function subscribeToHighlight() {
49
    selectAllNodes().on(Subscriptions.HIGHLIGHT, (node: DependencyNode) => {
50
        LevelStorage.reset();
51
        if (node.links.length) {
52
            highlight(node);
53
            zoomToHighLightedNodes();
54
        }
55
        event.stopPropagation();
56
    });
57
}
58
59
export function subscribeToChangeHighlightRangeOnArrowKey() {
60
    select('body').on(Subscriptions.CHANGE_HIGHLIGHT_RANGE, () => {
61
        if (event.code === 'ArrowRight' || event.code === 'ArrowLeft') {
62
            const labelNodesGroup = select<SVGGElement, DependencyNode>('g#labels');
63
            LevelStorage.setMaxLevel(findMaxDependencyLevel(labelNodesGroup));
64
65
            if (!isFinite(LevelStorage.getMaxLevel())) {
66
                return;
67
            }
68
69
            if (LevelStorage.isBelowMax() && event.code === 'ArrowRight') {
70
                LevelStorage.increase();
71
            }
72
73
            if (LevelStorage.isAboveMin() && event.code === 'ArrowLeft') {
74
                LevelStorage.decrease();
75
            }
76
77
            // TODO refactor it to share logic with GraphHelpers/highlight function
78
            labelNodesGroup
79
                .selectAll<HTMLElement, DependencyNode>('g')
80
                .filter((node: DependencyNode) => node.level > 0)
81
                .each(function(this: HTMLElement, node: DependencyNode) {
82
                    const labelElement = this.firstElementChild;
83
                    const textElement = this.lastElementChild;
84
85
                    if (!labelElement || !textElement) {
86
                        return;
87
                    }
88
89
                    let labelColor = LabelColors.DEFAULT;
90
                    let textColor = TextColors.DEFAULT;
91
                    if (node.level - 1 <= LevelStorage.getLevel()) {
92
                        labelColor = getHighLightedLabelColor(node);
93
                        textColor = TextColors.HIGHLIGHTED;
94
                    }
95
96
                    select<Element, DependencyNode>(labelElement).attr('fill', labelColor);
97
                    select<Element, DependencyNode>(textElement).style('fill', textColor);
98
                });
99
100
            zoomToHighLightedNodes();
101
        }
102
    });
103
}
104
105
export function subscribeToResetHighlight() {
106
    selectOverviewContainer().on(Subscriptions.RESET_HIGHLIGHT, () => {
107
        const highlightedNodes = selectHighLightedNodes();
108
        if (highlightedNodes.data().length) {
109
            selectAllNodes().each((node: DependencyNode) => (node.level = 0));
110
111
            const dimension = findGroupBackgroundDimension(highlightedNodes.data());
112
113
            highlightedNodes.each(function(this: SVGGElement) {
114
                const labelElement = this.firstElementChild;
115
                const textElement = this.lastElementChild;
116
117
                if (!labelElement || !textElement) {
118
                    return;
119
                }
120
121
                select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
122
                select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
123
            });
124
125
            hideHighlightBackground();
126
127
            centerScreenToDimension(dimension, 1);
128
        }
129
    });
130
}
131
132
export function subscribeToZoomOnArrowKey() {
133
    select('body').on(Subscriptions.ZOOM_ON_ARROW_KEY, () => {
134
        switch (event.code) {
135
            case 'ArrowUp': {
136
                const currentScaleValue = ZoomScaleStorage.getScale();
137
                const newScaleValue = currentScaleValue * ZOOM_INCREASE;
138
                if (newScaleValue <= MAXIMUM_ZOOM_SCALE) {
139
                    ZoomScaleStorage.setScale(newScaleValue);
140
                    const container = selectOverviewContainer();
141
                    container.call(() => {
142
                        return zoom<any, any>()
143
                            .on('zoom', changeZoom(ElementIds.OVERVIEW_ZOOM))
144
                            .scaleBy(container, ZOOM_INCREASE);
145
                    }, zoomIdentity);
146
                }
147
                break;
148
            }
149
            case 'ArrowDown': {
150
                const currentScaleValue = ZoomScaleStorage.getScale();
151
                const newScaleValue = currentScaleValue * ZOOM_DECREASE;
152
                if (newScaleValue >= MINIMUM_ZOOM_SCALE) {
153
                    ZoomScaleStorage.setScale(newScaleValue);
154
                    const container = selectOverviewContainer();
155
                    container.call(() => {
156
                        return zoom<any, any>()
157
                            .on('zoom', changeZoom(ElementIds.OVERVIEW_ZOOM))
158
                            .scaleBy(container, ZOOM_DECREASE);
159
                    }, zoomIdentity);
160
                }
161
                break;
162
            }
163
        }
164
    });
165
}
166
167
export function subscribeToOpenDetails(detailsNodes: TreeNode[]) {
168
    selectDetailsButtonWrapper().on(Subscriptions.OPEN_DETAILS, () => {
169
        if (selectHighLightedNodes().data().length === 0) {
170
            return;
171
        }
172
        event.stopPropagation();
173
        const selectedNode = selectAllNodes()
174
            .data()
175
            .find(node => node.level === 1);
176
        if (selectedNode) {
177
            const selectedTreeNode = detailsNodes.find(treeNode => treeNode.name === selectedNode.name);
178
            if (selectedTreeNode) {
179
                initializeDetailsView(selectedTreeNode);
180
            }
181
        }
182
    });
183
}
184
185
export function subscribeToCloseDetails() {
186
    selectDetailsExitButtonWrapper().on(Subscriptions.CLOSE_DETAILS, () => {
187
        shutdownDetailsView();
188
    });
189
}
190
191
const TOOLTIP_PADDING = 10;
192
193
export function subscribeToShowTooltipOnNodeHover() {
194
    const tooltipElement = selectOverviewZoom()
195
        .append('g')
196
        .attr('id', 'tooltip')
197
        .style('opacity', 0);
198
    const tooltipBackground = tooltipElement
199
        .append('rect')
200
        .attr('fill', ElementColors.BUTTON)
201
        .attr('rx', 5)
202
        .attr('ry', 5);
203
    const tooltipText = tooltipElement.append('text').attr('fill', TextColors.HIGHLIGHTED);
204
205
    selectAllNodes()
206
        .on(Subscriptions.SHOW_TOOLTIP, function(node) {
207
            const { x = 0, y = 0 } = node;
208
            tooltipElement
209
                .transition()
210
                .duration(FAST_TRANSITION_DURATION)
211
                .style('opacity', 0.9);
212
            tooltipText
213
                .text(node.version)
214
                .attr('x', x)
215
                .attr('y', y - 25 - TOOLTIP_PADDING);
216
            const { width, height } = tooltipText.node() ? tooltipText.node()!.getBBox() : { width: 0, height: 0 };
217
            tooltipBackground
218
                .attr('x', x - TOOLTIP_PADDING)
219
                .attr('y', y - 42 - TOOLTIP_PADDING)
220
                .attr('width', width + 2 * TOOLTIP_PADDING)
221
                .attr('height', height + TOOLTIP_PADDING);
222
        })
223
        .on(Subscriptions.HIDE_TOOLTIP, function() {
224
            tooltipElement
225
                .transition()
226
                .duration(TRANSITION_DURATION)
227
                .style('opacity', 0);
228
        });
229
}
230
231
export function removeTooltipOnNodeHoverSubscription() {
232
    selectTooltip().remove();
233
}
234
235
class LevelStorage {
236
    private static level: number = 1;
237
    private static maxLevel: number = 1;
238
239
    public static getLevel(): number {
240
        return this.level;
241
    }
242
243
    public static increase() {
244
        this.level = this.level + 1;
245
    }
246
247
    public static decrease() {
248
        this.level = this.level - 1;
249
    }
250
251
    public static isBelowMax() {
252
        return this.level < this.maxLevel;
253
    }
254
255
    static isAboveMin() {
256
        return this.level > 1;
257
    }
258
259
    static setMaxLevel(maxLevel: number) {
260
        this.maxLevel = maxLevel;
261
    }
262
263
    static getMaxLevel(): number {
264
        return this.maxLevel;
265
    }
266
267
    public static reset() {
268
        this.level = 1;
269
        this.maxLevel = 1;
270
    }
271
}
272
273
export class ZoomScaleStorage {
274
    private static currentScale = 1;
275
276
    public static setScale(newScale: number) {
277
        this.currentScale = newScale;
278
    }
279
280
    public static getScale() {
281
        return this.currentScale;
282
    }
283
}
284