1 /*global SVGSVGElement, SVGPathElement, SVGGElement*/
3 * Created by onvelocity on 2/6/16.
10 import UID
from './UniqueId'
11 import React
from 'react'
12 import PathBuilder
from './graph/PathBuilder'
14 const SELECTIONS
= '::private::selections';
17 * SelectionManager provides two features:
18 * 1) given DescriptorModel instances mark them as 'selected'
19 * 2) given DOM elements draw an outline around them
21 * Note that the consumer must call addSelection(descriptor) and outlineElement(dom) separately. This allows for complex
22 * selections to be separate from the outline indicator. It also allows for selection sets that do not have a visual
23 * outline associated with them.
25 class SelectionManager
{
27 static disableOutlineChanges() {
28 SelectionManager
._disableOutlineChanges
= true;
31 static enableOutlineChanges() {
32 SelectionManager
._disableOutlineChanges
= false;
36 const isSelected
= SelectionManager
.isSelected(obj
);
38 SelectionManager
.clearSelectionAndRemoveOutline();
39 SelectionManager
.addSelection(obj
);
44 static isSelected(obj
) {
45 const uid
= UID
.from(obj
);
47 return SelectionManager
[SELECTIONS
].has(uid
);
51 static getSelections() {
52 return Array
.from(SelectionManager
[SELECTIONS
]).filter(d
=> d
);
55 static addSelection(obj
) {
56 const uid
= UID
.from(obj
);
58 SelectionManager
[SELECTIONS
].add(uid
);
62 static updateSelection(container
, addOrRemove
= true) {
64 SelectionManager
.addSelection(container
);
66 SelectionManager
.removeSelection(container
);
70 static removeSelection(obj
) {
71 const uid
= UID
.from(obj
);
73 SelectionManager
[SELECTIONS
].delete(uid
);
77 static get onClearSelection() {
78 return this._onClearSelection
;
81 static set onClearSelection(callback
) {
82 if (!this._onClearSelection
) {
83 this._onClearSelection
= new Set();
85 if (typeof callback
!== 'function') {
86 throw new TypeError('onClearSelection only takes functions');
88 this._onClearSelection
.add(callback
);
91 static clearSelectionAndRemoveOutline() {
92 SelectionManager
.removeOutline();
93 const unselected
= SelectionManager
.getSelections();
94 if (unselected
.length
) {
95 SelectionManager
[SELECTIONS
].clear();
96 if (SelectionManager
.onClearSelection
) {
97 SelectionManager
.onClearSelection
.forEach(callback
=> callback(unselected
));
103 static removeOutline() {
104 Array
.from(document
.querySelectorAll('[data-outline-indicator-outline]')).forEach(n
=> d3
.select(n
).remove());
107 static refreshOutline() {
108 clearTimeout(SelectionManager
.timeoutId
);
109 SelectionManager
.moveOutline();
110 SelectionManager
.timeoutId
= setTimeout(() => {
111 // note the timeout is to wait for the react digest to complete
112 // in the case this method is called from an Alt action....
113 SelectionManager
.moveOutline();
117 static moveOutline() {
118 SelectionManager
.getSelections().forEach(uid
=> {
119 Array
.from(document
.body
.querySelectorAll(`[data-uid="${uid}"]`)).forEach(SelectionManager
.outline
.bind(this));
123 static outline(dom
) {
125 const elements
= Array
.isArray(dom
) ? dom
: [dom
];
127 elements
.map(SelectionManager
.getClosestElementWithUID
).filter(d
=> d
).forEach(element
=> {
129 if (element
instanceof SVGElement
) {
130 SelectionManager
.outlineSvg(element
);
132 SelectionManager
.outlineDom(element
);
139 static outlineSvg(element
) {
141 if (SelectionManager
._disableOutlineChanges
) {
145 const svg
= element
.viewportElement
;
151 const path
= new PathBuilder();
153 const dim
= element
.getBBox();
154 const adjustPos
= SelectionManager
.sumTranslates(element
);
156 let w
= dim
.width
+ 11;
157 let h
= dim
.height
+ 11;
158 let top
= adjustPos
[1] - 8 + dim
.y
;
159 let left
= adjustPos
[0] - 8 + dim
.x
;
161 let square
= path
.M(5, 5).L(w
, 5).L(w
, h
).L(5, h
).L(5, 5).Z
.toString();
163 let border
= element
;
165 // strategy copy the element to use as a foreground mask
166 // - this allows the item to appear above other items and
167 // - it makes the outline apear outside the element
169 const mask
= d3
.select(element
.cloneNode(true));
171 if (border
instanceof SVGPathElement
) {
172 const t
= d3
.transform(d3
.select(element
).attr('transform')).translate
;
173 square
= d3
.select(element
).attr('d');
176 } else if (border
instanceof SVGGElement
) {
177 const t
= d3
.transform(d3
.select(element
).attr('transform')).translate
;
178 border
= d3
.select(element
).select('path.border');
179 square
= border
.attr('d');
191 const indicator
= svg
.querySelector(['[data-outline-indicator]']) || svg
;
193 const outline
= d3
.select(indicator
).selectAll('[data-outline-indicator-outline]').data([data
]);
195 outline
.exit().remove();
197 // move to top of z-order
198 element
.parentNode
.appendChild(element
);
200 outline
.enter().append('g').attr({
201 'data-outline-indicator-outline': true,
202 'class': '-with-transitions'
204 'pointer-events': 'none'
208 transform
: d
=> `translate(${d.left}, ${d.top})`
211 outline
.select('g').append('path').attr({
212 'data-outline-indicator-svg-outline-path': true,
214 'fill': 'transparent',
215 'fill-rule': 'evenodd',
217 'stroke-linejoin': 'round',
218 'stroke-dasharray': '4',
221 }).transition().style({'stroke-width': 15});
223 const g
= outline
.select('g').node();
224 const children
= Array
.from(mask
.node().childNodes
);
225 if (children
.length
) {
227 children
.forEach(child
=> {
228 g
.appendChild(child
);
231 // ensure the outline moves with the item during drag operations
232 const observer
= new MutationObserver(function(mutations
) {
233 mutations
.forEach(function(mutation
) {
234 const transform
= d3
.select(mutation
.target
).style('opacity', 0).attr('transform');
241 const config
= {attributes
: true, attributeOldValue
: true, attributeFilter
: ['transform']};
243 observer
.observe(element
, config
);
247 if (mask
.classed('connection')) {
248 element
.parentNode
.appendChild(outline
.node());
249 element
.parentNode
.appendChild(element
);
251 //mask.attr('transform', null);
252 element
.parentNode
.appendChild(outline
.node());
253 element
.parentNode
.appendChild(element
);
260 static outlineDom(element
) {
262 if (SelectionManager
._disableOutlineChanges
) {
266 element
= SelectionManager
.getClosestElementWithUID(element
);
268 const offsetParent
= SelectionManager
.offsetParent(element
);
270 const dim
= SelectionManager
.offsetDimensions(element
);
271 const w
= dim
.width
+ 11;
272 const h
= dim
.height
+ 11;
275 const path
= new PathBuilder();
276 const square
= path
.M(5, 5).L(w
, 5).L(w
, h
).L(5, h
).L(5, 5).Z
.toString();
278 const parentDim
= SelectionManager
.offsetDimensions(offsetParent
);
280 const top
= dim
.top
- parentDim
.top
;
281 const left
= dim
.left
- parentDim
.left
;
282 const svg
= d3
.select(offsetParent
).selectAll('[data-outline-indicator-outline]').data([{}]);
283 svg
.enter().append('svg').attr({
284 'data-outline-indicator-outline': true,
285 width
: parentDim
.width
+ 20,
286 height
: parentDim
.height
+ 23,
287 style
: `pointer-events: none; position: absolute; z-index: 999; top: ${top - 8}px; left: ${left - 8}px; background: transparent;`
288 }).append('g').append('path').attr({
289 'data-outline-indicator-dom-outline-path': true,
291 'fill': 'transparent',
292 'stroke-width': '1.5px',
293 'stroke-linejoin': 'round',
294 'stroke-dasharray': '4',
298 svg
.select('svg').attr({
299 width
: parentDim
.width
+ 20,
300 height
: parentDim
.height
+ 23,
301 }).select('path').attr({
309 static getClosestElementWithUID(element
) {
310 let target
= element
;
312 if (UID
.from(target
)) {
315 if (target
=== target
.parentNode
) {
318 target
= target
.parentNode
;
322 static offsetParent(target
) {
324 if ((d3
.select(target
).attr('data-offset-parent')) || target
.nodeName
=== 'BODY') {
327 target
= target
.parentNode
;
329 return document
.body
;
333 * Util for determining the widest child of an offsetParent that is scrolled.
335 * @param offsetParent
336 * @returns {{top: Number, right: Number, bottom: Number, left: Number, height: Number, width: Number}}
338 static offsetDimensions (offsetParent
) {
340 const elementDim
= offsetParent
.getBoundingClientRect();
343 right
: elementDim
.right
,
344 bottom
: elementDim
.bottom
,
345 left
: elementDim
.left
,
346 height
: elementDim
.height
,
347 width
: elementDim
.width
350 const overrideWidth
= Array
.from(offsetParent
.querySelectorAll('[data-offset-width]'));
351 dim
.width
= overrideWidth
.reduce((width
, child
) => {
352 const dim
= child
.getBoundingClientRect();
353 return Math
.max(width
, dim
.width
);
354 }, elementDim
.width
);
360 static sumTranslates(element
) {
361 let parent
= element
;
362 const result
= [0, 0];
364 const t
= d3
.transform(d3
.select(parent
).attr('transform')).translate
;
367 if (!parent
.parentNode
|| /svg/i.test(parent
.nodeName
) || parent
=== parent
.parentNode
) {
370 parent
= parent
.parentNode
;
377 // warn static variable to store all selections across instances
378 SelectionManager
[SELECTIONS
] = SelectionManager
[SELECTIONS
] || new Set();
380 export default SelectionManager
;