-
Notifications
You must be signed in to change notification settings - Fork 149
/
Copy pathdom-utils.ts
219 lines (167 loc) · 7.41 KB
/
dom-utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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
216
217
218
219
import { CLASS_DRAG_IMAGE_SNAPBACK } from "./constants";
import { supportsPassiveEventListener } from "./feature-detection";
// evaluate once on startup
const supportsPassive = supportsPassiveEventListener();
export interface Point {
x:number;
y:number;
}
export function isDOMElement( object:Element ) {
return object && object.tagName;
}
export function addDocumentListener( ev:string, handler:EventListener, passive:boolean = true ) {
document.addEventListener( ev, handler, supportsPassive ? { passive: passive } : false );
}
export function removeDocumentListener( ev:string, handler:EventListener ) {
document.removeEventListener( ev, handler );
}
export function onEvt( el:EventTarget, event:string, handler:() => any, context:any ) {
if( context ) {
handler = handler.bind( context );
}
el.addEventListener( event, handler );
return {
off() {
return el.removeEventListener( event, handler );
}
};
}
function prepareNodeCopyAsDragImage( srcNode:HTMLElement, dstNode:HTMLElement ) {
// Is this node an element?
if( srcNode.nodeType === 1 ) {
// Clone the style
const cs = getComputedStyle( srcNode );
for( let i = 0; i < cs.length; i++ ) {
const csName = cs[ i ];
dstNode.style.setProperty( csName, cs.getPropertyValue( csName ), cs.getPropertyPriority( csName ) );
}
// no interaction with the drag image, pls! this is also important to make the drag image transparent for hit-testing
// hit testing is done in the drag and drop iteration to find the element the user currently is hovering over while dragging.
// if pointer-events is not none or a browser does behave in an unexpected way than the hit test transparency on the drag image
// will break
dstNode.style.pointerEvents = "none";
// Remove any potential conflict attributes
dstNode.removeAttribute( "id" );
dstNode.removeAttribute( "class" );
dstNode.removeAttribute( "draggable" );
// canvas elements need special handling by copying canvas image data
if( dstNode.nodeName === "CANVAS" ) {
const canvasSrc = srcNode as HTMLCanvasElement;
const canvasDst = dstNode as HTMLCanvasElement;
const canvasSrcImgData = canvasSrc.getContext( "2d" ).getImageData( 0, 0, canvasSrc.width, canvasSrc.height );
canvasDst.getContext( "2d" ).putImageData( canvasSrcImgData, 0, 0 );
}
}
// Do the same for the children
if( srcNode.hasChildNodes() ) {
for( let i = 0; i < srcNode.childNodes.length; i++ ) {
prepareNodeCopyAsDragImage( <HTMLElement>srcNode.childNodes[ i ], <HTMLElement>dstNode.childNodes[ i ] );
}
}
}
export function createDragImage( sourceNode:HTMLElement ):HTMLElement {
const dragImage = <HTMLElement>sourceNode.cloneNode( true );
// this removes any id's and stuff that could interfere with drag and drop
prepareNodeCopyAsDragImage( sourceNode, dragImage );
return dragImage;
}
function average( array:Array<number> ) {
if( array.length === 0 ) {
return 0;
}
return array.reduce( (function( s, v ) {
return v + s;
}), 0 ) / array.length;
}
export function isTouchIdentifierContainedInTouchEvent( touchEvent:TouchEvent, touchIdentifier:number ) {
for( let i = 0; i < touchEvent.changedTouches.length; i++ ) {
const touch = touchEvent.changedTouches[ i ];
if( touch.identifier === touchIdentifier ) {
return true;
}
}
return false;
}
/**
* Calc center of polygon spanned by multiple touches in page (full page size, with hidden scrollable area) coordinates
* or in viewport (screen coordinates) coordinates.
*/
export function updateCentroidCoordinatesOfTouchesIn( coordinateProp:"page" | "client", event:TouchEvent, outPoint:Point ):void {
const pageXs:Array<number> = [], pageYs:Array<number> = [];
for( let i = 0; i < event.touches.length; i++ ) {
const touch = event.touches[ i ];
pageXs.push( touch[ coordinateProp + "X" ] );
pageYs.push( touch[ coordinateProp + "Y" ] );
}
outPoint.x = average( pageXs );
outPoint.y = average( pageYs );
}
// cross-browser css transform property prefixes
const TRANSFORM_CSS_VENDOR_PREFIXES = [ "", "-webkit-" ];
export function extractTransformStyles( sourceNode:HTMLElement ):string[] {
return TRANSFORM_CSS_VENDOR_PREFIXES.map( function( prefix:string ) {
let transform = sourceNode.style[ prefix + "transform" ];
if( !transform || transform === "none" ) {
return "";
}
// removes translate(x,y)
return transform.replace( /translate\(\D*\d+[^,]*,\D*\d+[^,]*\)\s*/g, "" );
} );
}
export function translateElementToPoint( element:HTMLElement, pnt:Point, originalTransforms:string[], offset?:Point, centerOnCoordinates = true ):void {
let x = pnt.x, y = pnt.y;
if( offset ) {
x += offset.x;
y += offset.y;
}
if( centerOnCoordinates ) {
x -= (parseInt( <any>element.offsetWidth, 10 ) / 2);
y -= (parseInt( <any>element.offsetHeight, 10 ) / 2);
}
// using translate3d for max performance
const translate = "translate3d(" + x + "px," + y + "px, 0)";
for( let i = 0; i < TRANSFORM_CSS_VENDOR_PREFIXES.length; i++ ) {
const transformProp = TRANSFORM_CSS_VENDOR_PREFIXES[ i ] + "transform";
element.style[ transformProp ] = translate + " " + originalTransforms[ i ];
}
}
/**
* calculates the coordinates of the drag source and transitions the drag image to those coordinates.
* the drag operation is finished after the transition has ended.
*/
export function applyDragImageSnapback( sourceEl:HTMLElement, dragImage:HTMLElement, dragImageTransforms:string[], transitionEndCb:Function ):void {
const cs = getComputedStyle( sourceEl );
if( cs.visibility === "hidden" || cs.display === "none" ) {
console.log( "dnd-poly: source node is not visible. skipping snapback transition." );
// shortcut to end the drag operation
transitionEndCb();
return;
}
// add class containing transition rules
dragImage.classList.add( CLASS_DRAG_IMAGE_SNAPBACK );
const csDragImage = getComputedStyle( dragImage );
const durationInS = parseFloat( csDragImage.transitionDuration );
if( isNaN( durationInS ) || durationInS === 0 ) {
console.log( "dnd-poly: no transition used - skipping snapback" );
transitionEndCb();
return;
}
console.log( "dnd-poly: starting dragimage snap back" );
// calc source node position
const rect = sourceEl.getBoundingClientRect();
const pnt:Point = {
x: rect.left,
y: rect.top
};
// add scroll offset of document
pnt.x += (document.body.scrollLeft || document.documentElement.scrollLeft);
pnt.y += (document.body.scrollTop || document.documentElement.scrollTop);
//TODO this sometimes fails to calculate the correct origin position.. find out when exactly and how to detect
pnt.x -= parseInt( cs.marginLeft, 10 );
pnt.y -= parseInt( cs.marginTop, 10 );
const delayInS = parseFloat( csDragImage.transitionDelay );
const durationInMs = Math.round( (durationInS + delayInS) * 1000 );
// apply the translate
translateElementToPoint( dragImage, pnt, dragImageTransforms, undefined, false );
setTimeout( transitionEndCb, durationInMs );
}