RIFT-15726 - optimize download size -> lodash usage in UI
[osm/UI.git] / skyquake / plugins / composer / src / src / libraries / SelectionManager.js
1 /*global SVGSVGElement, SVGPathElement, SVGGElement*/
2 /**
3 * Created by onvelocity on 2/6/16.
4 */
5
6 'use strict';
7
8 import d3 from 'd3'
9 import UID from './UniqueId'
10 import React from 'react'
11 import PathBuilder from './graph/PathBuilder'
12
13 const SELECTIONS = '::private::selections';
14
15 /**
16 * SelectionManager provides two features:
17 * 1) given DescriptorModel instances mark them as 'selected'
18 * 2) given DOM elements draw an outline around them
19 *
20 * Note that the consumer must call addSelection(descriptor) and outlineElement(dom) separately. This allows for complex
21 * selections to be separate from the outline indicator. It also allows for selection sets that do not have a visual
22 * outline associated with them.
23 */
24 class SelectionManager {
25
26 static disableOutlineChanges() {
27 SelectionManager._disableOutlineChanges = true;
28 }
29
30 static enableOutlineChanges() {
31 SelectionManager._disableOutlineChanges = false;
32 }
33
34 static select(obj) {
35 const isSelected = SelectionManager.isSelected(obj);
36 if (!isSelected) {
37 SelectionManager.clearSelectionAndRemoveOutline();
38 SelectionManager.addSelection(obj);
39 return true;
40 }
41 }
42
43 static isSelected(obj) {
44 const uid = UID.from(obj);
45 if (uid) {
46 return SelectionManager[SELECTIONS].has(uid);
47 }
48 }
49
50 static getSelections() {
51 return Array.from(SelectionManager[SELECTIONS]).filter(d => d);
52 }
53
54 static addSelection(obj) {
55 const uid = UID.from(obj);
56 if (uid) {
57 SelectionManager[SELECTIONS].add(uid);
58 }
59 }
60
61 static updateSelection(container, addOrRemove = true) {
62 if (addOrRemove) {
63 SelectionManager.addSelection(container);
64 } else {
65 SelectionManager.removeSelection(container);
66 }
67 }
68
69 static removeSelection(obj) {
70 const uid = UID.from(obj);
71 if (uid) {
72 SelectionManager[SELECTIONS].delete(uid);
73 }
74 }
75
76 static get onClearSelection() {
77 return this._onClearSelection;
78 }
79
80 static set onClearSelection(callback) {
81 if (!this._onClearSelection) {
82 this._onClearSelection = new Set();
83 }
84 if (typeof callback !== 'function') {
85 throw new TypeError('onClearSelection only takes functions');
86 }
87 this._onClearSelection.add(callback);
88 }
89
90 static clearSelectionAndRemoveOutline() {
91 SelectionManager.removeOutline();
92 const unselected = SelectionManager.getSelections();
93 if (unselected.length) {
94 SelectionManager[SELECTIONS].clear();
95 if (SelectionManager.onClearSelection) {
96 SelectionManager.onClearSelection.forEach(callback => callback(unselected));
97 }
98 }
99 return unselected;
100 }
101
102 static removeOutline() {
103 Array.from(document.querySelectorAll('[data-outline-indicator-outline]')).forEach(n => d3.select(n).remove());
104 }
105
106 static refreshOutline() {
107 clearTimeout(SelectionManager.timeoutId);
108 SelectionManager.moveOutline();
109 SelectionManager.timeoutId = setTimeout(() => {
110 // note the timeout is to wait for the react digest to complete
111 // in the case this method is called from an Alt action....
112 SelectionManager.moveOutline();
113 }, 100);
114 }
115
116 static moveOutline() {
117 SelectionManager.getSelections().forEach(uid => {
118 Array.from(document.body.querySelectorAll(`[data-uid="${uid}"]`)).forEach(SelectionManager.outline.bind(this));
119 });
120 }
121
122 static outline(dom) {
123
124 const elements = Array.isArray(dom) ? dom : [dom];
125
126 elements.map(SelectionManager.getClosestElementWithUID).filter(d => d).forEach(element => {
127
128 if (element instanceof SVGElement) {
129 SelectionManager.outlineSvg(element);
130 } else {
131 SelectionManager.outlineDom(element);
132 }
133
134 });
135
136 }
137
138 static outlineSvg(element) {
139
140 if (SelectionManager._disableOutlineChanges) {
141 return
142 }
143
144 const svg = element.viewportElement;
145
146 if (!svg) {
147 return
148 }
149
150 const path = new PathBuilder();
151
152 const dim = element.getBBox();
153 const adjustPos = SelectionManager.sumTranslates(element);
154
155 let w = dim.width + 11;
156 let h = dim.height + 11;
157 let top = adjustPos[1] - 8 + dim.y;
158 let left = adjustPos[0] - 8 + dim.x;
159
160 let square = path.M(5, 5).L(w, 5).L(w, h).L(5, h).L(5, 5).Z.toString();
161
162 let border = element;
163
164 // strategy copy the element to use as a foreground mask
165 // - this allows the item to appear above other items and
166 // - it makes the outline apear outside the element
167
168 const mask = d3.select(element.cloneNode(true));
169
170 if (border instanceof SVGPathElement) {
171 const t = d3.transform(d3.select(element).attr('transform')).translate;
172 square = d3.select(element).attr('d');
173 top = t[1];
174 left = t[0];
175 } else if (border instanceof SVGGElement) {
176 const t = d3.transform(d3.select(element).attr('transform')).translate;
177 border = d3.select(element).select('path.border');
178 square = border.attr('d');
179 top = t[1];
180 left = t[0];
181 }
182
183
184 const data = {
185 top: top,
186 left: left,
187 path: square
188 };
189
190 const indicator = svg.querySelector(['[data-outline-indicator]']) || svg;
191
192 const outline = d3.select(indicator).selectAll('[data-outline-indicator-outline]').data([data]);
193
194 outline.exit().remove();
195
196 // move to top of z-order
197 element.parentNode.appendChild(element);
198
199 outline.enter().append('g').attr({
200 'data-outline-indicator-outline': true,
201 'class': '-with-transitions'
202 }).style({
203 'pointer-events': 'none'
204 }).append('g');
205
206 outline.attr({
207 transform: d => `translate(${d.left}, ${d.top})`
208 });
209
210 outline.select('g').append('path').attr({
211 'data-outline-indicator-svg-outline-path': true,
212 'stroke': 'red',
213 'fill': 'transparent',
214 'fill-rule': 'evenodd',
215 'stroke-width': 5,
216 'stroke-linejoin': 'round',
217 'stroke-dasharray': '4',
218 'opacity': 1,
219 d: d => d.path
220 }).transition().style({'stroke-width': 15});
221
222 const g = outline.select('g').node();
223 const children = Array.from(mask.node().childNodes);
224 if (children.length) {
225
226 children.forEach(child => {
227 g.appendChild(child);
228 });
229
230 // ensure the outline moves with the item during drag operations
231 const observer = new MutationObserver(function(mutations) {
232 mutations.forEach(function(mutation) {
233 const transform = d3.select(mutation.target).style('opacity', 0).attr('transform');
234 outline.attr({
235 transform: transform
236 });
237 });
238 });
239
240 const config = {attributes: true, attributeOldValue: true, attributeFilter: ['transform']};
241
242 observer.observe(element, config);
243
244 } else {
245
246 if (mask.classed('connection')) {
247 element.parentNode.appendChild(outline.node());
248 element.parentNode.appendChild(element);
249 } else {
250 //mask.attr('transform', null);
251 element.parentNode.appendChild(outline.node());
252 element.parentNode.appendChild(element);
253 }
254
255 }
256
257 }
258
259 static outlineDom(element) {
260
261 if (SelectionManager._disableOutlineChanges) {
262 return
263 }
264
265 element = SelectionManager.getClosestElementWithUID(element);
266
267 const offsetParent = SelectionManager.offsetParent(element);
268
269 const dim = SelectionManager.offsetDimensions(element);
270 const w = dim.width + 11;
271 const h = dim.height + 11;
272
273
274 const path = new PathBuilder();
275 const square = path.M(5, 5).L(w, 5).L(w, h).L(5, h).L(5, 5).Z.toString();
276
277 const parentDim = SelectionManager.offsetDimensions(offsetParent);
278
279 const top = dim.top - parentDim.top;
280 const left = dim.left - parentDim.left;
281 const svg = d3.select(offsetParent).selectAll('[data-outline-indicator-outline]').data([{}]);
282 svg.enter().append('svg').attr({
283 'data-outline-indicator-outline': true,
284 width: parentDim.width + 20,
285 height: parentDim.height + 23,
286 style: `pointer-events: none; position: absolute; z-index: 999; top: ${top - 8}px; left: ${left - 8}px; background: transparent;`
287 }).append('g').append('path').attr({
288 'data-outline-indicator-dom-outline-path': true,
289 'stroke': 'red',
290 'fill': 'transparent',
291 'stroke-width': '1.5px',
292 'stroke-linejoin': 'round',
293 'stroke-dasharray': '4',
294 d: square
295 });
296
297 svg.select('svg').attr({
298 width: parentDim.width + 20,
299 height: parentDim.height + 23,
300 }).select('path').attr({
301 d: square
302 });
303
304 svg.exit().remove();
305
306 }
307
308 static getClosestElementWithUID(element) {
309 let target = element;
310 while (target) {
311 if (UID.from(target)) {
312 return target;
313 }
314 if (target === target.parentNode) {
315 return;
316 }
317 target = target.parentNode;
318 }
319 }
320
321 static offsetParent(target) {
322 while (target) {
323 if ((d3.select(target).attr('data-offset-parent')) || target.nodeName === 'BODY') {
324 return target;
325 }
326 target = target.parentNode;
327 }
328 return document.body;
329 }
330
331 /**
332 * Util for determining the widest child of an offsetParent that is scrolled.
333 *
334 * @param offsetParent
335 * @returns {{top: Number, right: Number, bottom: Number, left: Number, height: Number, width: Number}}
336 */
337 static offsetDimensions (offsetParent) {
338
339 const elementDim = offsetParent.getBoundingClientRect();
340 const dim = {
341 top: elementDim.top,
342 right: elementDim.right,
343 bottom: elementDim.bottom,
344 left: elementDim.left,
345 height: elementDim.height,
346 width: elementDim.width
347 };
348
349 const overrideWidth = Array.from(offsetParent.querySelectorAll('[data-offset-width]'));
350 dim.width = overrideWidth.reduce((width, child) => {
351 const dim = child.getBoundingClientRect();
352 return Math.max(width, dim.width);
353 }, elementDim.width);
354
355 return dim;
356
357 }
358
359 static sumTranslates(element) {
360 let parent = element;
361 const result = [0, 0];
362 while (parent) {
363 const t = d3.transform(d3.select(parent).attr('transform')).translate;
364 result[0] += t[0];
365 result[1] += t[1];
366 if (!parent.parentNode || /svg/i.test(parent.nodeName) || parent === parent.parentNode) {
367 return result;
368 }
369 parent = parent.parentNode;
370 }
371 return result;
372 }
373
374 }
375
376 // warn static variable to store all selections across instances
377 SelectionManager[SELECTIONS] = SelectionManager[SELECTIONS] || new Set();
378
379 export default SelectionManager;