1 /*global SVGSVGElement, SVGPathElement, SVGGElement*/
3 * Created by onvelocity on 2/6/16.
9 import UID
from './UniqueId'
10 import React
from 'react'
11 import PathBuilder
from './graph/PathBuilder'
13 const SELECTIONS
= '::private::selections';
16 * SelectionManager provides two features:
17 * 1) given DescriptorModel instances mark them as 'selected'
18 * 2) given DOM elements draw an outline around them
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.
24 class SelectionManager
{
26 static disableOutlineChanges() {
27 SelectionManager
._disableOutlineChanges
= true;
30 static enableOutlineChanges() {
31 SelectionManager
._disableOutlineChanges
= false;
35 const isSelected
= SelectionManager
.isSelected(obj
);
37 SelectionManager
.clearSelectionAndRemoveOutline();
38 SelectionManager
.addSelection(obj
);
43 static isSelected(obj
) {
44 const uid
= UID
.from(obj
);
46 return SelectionManager
[SELECTIONS
].has(uid
);
50 static getSelections() {
51 return Array
.from(SelectionManager
[SELECTIONS
]).filter(d
=> d
);
54 static addSelection(obj
) {
55 const uid
= UID
.from(obj
);
57 SelectionManager
[SELECTIONS
].add(uid
);
61 static updateSelection(container
, addOrRemove
= true) {
63 SelectionManager
.addSelection(container
);
65 SelectionManager
.removeSelection(container
);
69 static removeSelection(obj
) {
70 const uid
= UID
.from(obj
);
72 SelectionManager
[SELECTIONS
].delete(uid
);
76 static get onClearSelection() {
77 return this._onClearSelection
;
80 static set onClearSelection(callback
) {
81 if (!this._onClearSelection
) {
82 this._onClearSelection
= new Set();
84 if (typeof callback
!== 'function') {
85 throw new TypeError('onClearSelection only takes functions');
87 this._onClearSelection
.add(callback
);
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
));
102 static removeOutline() {
103 Array
.from(document
.querySelectorAll('[data-outline-indicator-outline]')).forEach(n
=> d3
.select(n
).remove());
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();
116 static moveOutline() {
117 SelectionManager
.getSelections().forEach(uid
=> {
118 Array
.from(document
.body
.querySelectorAll(`[data-uid="${uid}"]`)).forEach(SelectionManager
.outline
.bind(this));
122 static outline(dom
) {
124 const elements
= Array
.isArray(dom
) ? dom
: [dom
];
126 elements
.map(SelectionManager
.getClosestElementWithUID
).filter(d
=> d
).forEach(element
=> {
128 if (element
instanceof SVGElement
) {
129 SelectionManager
.outlineSvg(element
);
131 SelectionManager
.outlineDom(element
);
138 static outlineSvg(element
) {
140 if (SelectionManager
._disableOutlineChanges
) {
144 const svg
= element
.viewportElement
;
150 const path
= new PathBuilder();
152 const dim
= element
.getBBox();
153 const adjustPos
= SelectionManager
.sumTranslates(element
);
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
;
160 let square
= path
.M(5, 5).L(w
, 5).L(w
, h
).L(5, h
).L(5, 5).Z
.toString();
162 let border
= element
;
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
168 const mask
= d3
.select(element
.cloneNode(true));
170 if (border
instanceof SVGPathElement
) {
171 const t
= d3
.transform(d3
.select(element
).attr('transform')).translate
;
172 square
= d3
.select(element
).attr('d');
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');
190 const indicator
= svg
.querySelector(['[data-outline-indicator]']) || svg
;
192 const outline
= d3
.select(indicator
).selectAll('[data-outline-indicator-outline]').data([data
]);
194 outline
.exit().remove();
196 // move to top of z-order
197 element
.parentNode
.appendChild(element
);
199 outline
.enter().append('g').attr({
200 'data-outline-indicator-outline': true,
201 'class': '-with-transitions'
203 'pointer-events': 'none'
207 transform
: d
=> `translate(${d.left}, ${d.top})`
210 outline
.select('g').append('path').attr({
211 'data-outline-indicator-svg-outline-path': true,
213 'fill': 'transparent',
214 'fill-rule': 'evenodd',
216 'stroke-linejoin': 'round',
217 'stroke-dasharray': '4',
220 }).transition().style({'stroke-width': 15});
222 const g
= outline
.select('g').node();
223 const children
= Array
.from(mask
.node().childNodes
);
224 if (children
.length
) {
226 children
.forEach(child
=> {
227 g
.appendChild(child
);
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');
240 const config
= {attributes
: true, attributeOldValue
: true, attributeFilter
: ['transform']};
242 observer
.observe(element
, config
);
246 if (mask
.classed('connection')) {
247 element
.parentNode
.appendChild(outline
.node());
248 element
.parentNode
.appendChild(element
);
250 //mask.attr('transform', null);
251 element
.parentNode
.appendChild(outline
.node());
252 element
.parentNode
.appendChild(element
);
259 static outlineDom(element
) {
261 if (SelectionManager
._disableOutlineChanges
) {
265 element
= SelectionManager
.getClosestElementWithUID(element
);
267 const offsetParent
= SelectionManager
.offsetParent(element
);
269 const dim
= SelectionManager
.offsetDimensions(element
);
270 const w
= dim
.width
+ 11;
271 const h
= dim
.height
+ 11;
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();
277 const parentDim
= SelectionManager
.offsetDimensions(offsetParent
);
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,
290 'fill': 'transparent',
291 'stroke-width': '1.5px',
292 'stroke-linejoin': 'round',
293 'stroke-dasharray': '4',
297 svg
.select('svg').attr({
298 width
: parentDim
.width
+ 20,
299 height
: parentDim
.height
+ 23,
300 }).select('path').attr({
308 static getClosestElementWithUID(element
) {
309 let target
= element
;
311 if (UID
.from(target
)) {
314 if (target
=== target
.parentNode
) {
317 target
= target
.parentNode
;
321 static offsetParent(target
) {
323 if ((d3
.select(target
).attr('data-offset-parent')) || target
.nodeName
=== 'BODY') {
326 target
= target
.parentNode
;
328 return document
.body
;
332 * Util for determining the widest child of an offsetParent that is scrolled.
334 * @param offsetParent
335 * @returns {{top: Number, right: Number, bottom: Number, left: Number, height: Number, width: Number}}
337 static offsetDimensions (offsetParent
) {
339 const elementDim
= offsetParent
.getBoundingClientRect();
342 right
: elementDim
.right
,
343 bottom
: elementDim
.bottom
,
344 left
: elementDim
.left
,
345 height
: elementDim
.height
,
346 width
: elementDim
.width
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
);
359 static sumTranslates(element
) {
360 let parent
= element
;
361 const result
= [0, 0];
363 const t
= d3
.transform(d3
.select(parent
).attr('transform')).translate
;
366 if (!parent
.parentNode
|| /svg/i.test(parent
.nodeName
) || parent
=== parent
.parentNode
) {
369 parent
= parent
.parentNode
;
376 // warn static variable to store all selections across instances
377 SelectionManager
[SELECTIONS
] = SelectionManager
[SELECTIONS
] || new Set();
379 export default SelectionManager
;