@atlaskit/pragmatic-drag-and-drop 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/LICENSE.md +13 -0
  3. package/README.md +43 -0
  4. package/__perf__/add-example.todo +1 -0
  5. package/adapter/element/package.json +15 -0
  6. package/adapter/file/package.json +15 -0
  7. package/addon/cancel-unhandled/package.json +15 -0
  8. package/constellation/index/about.mdx +329 -0
  9. package/constellation/index/props.mdx +3 -0
  10. package/dist/cjs/adapter/element-adapter.js +151 -0
  11. package/dist/cjs/adapter/file-adapter.js +98 -0
  12. package/dist/cjs/addon/cancel-unhandled.js +50 -0
  13. package/dist/cjs/entry-point/adapter/element.js +24 -0
  14. package/dist/cjs/entry-point/adapter/file.js +18 -0
  15. package/dist/cjs/entry-point/addon/cancel-unhandled.js +12 -0
  16. package/dist/cjs/entry-point/experimental/cross-with-element-adapter.js +30 -0
  17. package/dist/cjs/entry-point/types.js +5 -0
  18. package/dist/cjs/entry-point/util/combine.js +12 -0
  19. package/dist/cjs/entry-point/util/disable-native-drag-preview.js +12 -0
  20. package/dist/cjs/entry-point/util/once.js +12 -0
  21. package/dist/cjs/entry-point/util/reorder.js +12 -0
  22. package/dist/cjs/entry-point/util/scroll-just-enough-into-view.js +12 -0
  23. package/dist/cjs/entry-point/util/set-custom-native-drag-preview.js +12 -0
  24. package/dist/cjs/experimental/cross-window-element-adapter.js +131 -0
  25. package/dist/cjs/index.js +12 -0
  26. package/dist/cjs/internal-types.js +5 -0
  27. package/dist/cjs/ledger/dispatch-consumer-event.js +132 -0
  28. package/dist/cjs/ledger/lifecycle-manager.js +335 -0
  29. package/dist/cjs/ledger/usage-ledger.js +37 -0
  30. package/dist/cjs/make-adapter/make-adapter.js +59 -0
  31. package/dist/cjs/make-adapter/make-drop-target.js +271 -0
  32. package/dist/cjs/make-adapter/make-monitor.js +100 -0
  33. package/dist/cjs/util/add-attribute.js +14 -0
  34. package/dist/cjs/util/combine.js +17 -0
  35. package/dist/cjs/util/disable-native-drag-preview.js +36 -0
  36. package/dist/cjs/util/entering-and-leaving-the-window.js +162 -0
  37. package/dist/cjs/util/fix-post-drag-pointer-bug.js +114 -0
  38. package/dist/cjs/util/get-input.js +20 -0
  39. package/dist/cjs/util/once.js +22 -0
  40. package/dist/cjs/util/reorder.js +26 -0
  41. package/dist/cjs/util/scroll-just-enough-into-view.js +17 -0
  42. package/dist/cjs/util/set-custom-native-drag-preview.js +109 -0
  43. package/dist/cjs/version.json +5 -0
  44. package/dist/es2019/adapter/element-adapter.js +143 -0
  45. package/dist/es2019/adapter/file-adapter.js +90 -0
  46. package/dist/es2019/addon/cancel-unhandled.js +43 -0
  47. package/dist/es2019/entry-point/adapter/element.js +1 -0
  48. package/dist/es2019/entry-point/adapter/file.js +1 -0
  49. package/dist/es2019/entry-point/addon/cancel-unhandled.js +1 -0
  50. package/dist/es2019/entry-point/experimental/cross-with-element-adapter.js +1 -0
  51. package/dist/es2019/entry-point/types.js +1 -0
  52. package/dist/es2019/entry-point/util/combine.js +1 -0
  53. package/dist/es2019/entry-point/util/disable-native-drag-preview.js +1 -0
  54. package/dist/es2019/entry-point/util/once.js +1 -0
  55. package/dist/es2019/entry-point/util/reorder.js +1 -0
  56. package/dist/es2019/entry-point/util/scroll-just-enough-into-view.js +1 -0
  57. package/dist/es2019/entry-point/util/set-custom-native-drag-preview.js +1 -0
  58. package/dist/es2019/experimental/cross-window-element-adapter.js +121 -0
  59. package/dist/es2019/index.js +7 -0
  60. package/dist/es2019/internal-types.js +1 -0
  61. package/dist/es2019/ledger/dispatch-consumer-event.js +128 -0
  62. package/dist/es2019/ledger/lifecycle-manager.js +333 -0
  63. package/dist/es2019/ledger/usage-ledger.js +32 -0
  64. package/dist/es2019/make-adapter/make-adapter.js +55 -0
  65. package/dist/es2019/make-adapter/make-drop-target.js +233 -0
  66. package/dist/es2019/make-adapter/make-monitor.js +80 -0
  67. package/dist/es2019/util/add-attribute.js +7 -0
  68. package/dist/es2019/util/combine.js +6 -0
  69. package/dist/es2019/util/disable-native-drag-preview.js +31 -0
  70. package/dist/es2019/util/entering-and-leaving-the-window.js +159 -0
  71. package/dist/es2019/util/fix-post-drag-pointer-bug.js +110 -0
  72. package/dist/es2019/util/get-input.js +14 -0
  73. package/dist/es2019/util/once.js +13 -0
  74. package/dist/es2019/util/reorder.js +17 -0
  75. package/dist/es2019/util/scroll-just-enough-into-view.js +12 -0
  76. package/dist/es2019/util/set-custom-native-drag-preview.js +106 -0
  77. package/dist/es2019/version.json +5 -0
  78. package/dist/esm/adapter/element-adapter.js +142 -0
  79. package/dist/esm/adapter/file-adapter.js +90 -0
  80. package/dist/esm/addon/cancel-unhandled.js +43 -0
  81. package/dist/esm/entry-point/adapter/element.js +1 -0
  82. package/dist/esm/entry-point/adapter/file.js +1 -0
  83. package/dist/esm/entry-point/addon/cancel-unhandled.js +1 -0
  84. package/dist/esm/entry-point/experimental/cross-with-element-adapter.js +1 -0
  85. package/dist/esm/entry-point/types.js +1 -0
  86. package/dist/esm/entry-point/util/combine.js +1 -0
  87. package/dist/esm/entry-point/util/disable-native-drag-preview.js +1 -0
  88. package/dist/esm/entry-point/util/once.js +1 -0
  89. package/dist/esm/entry-point/util/reorder.js +1 -0
  90. package/dist/esm/entry-point/util/scroll-just-enough-into-view.js +1 -0
  91. package/dist/esm/entry-point/util/set-custom-native-drag-preview.js +1 -0
  92. package/dist/esm/experimental/cross-window-element-adapter.js +120 -0
  93. package/dist/esm/index.js +7 -0
  94. package/dist/esm/internal-types.js +1 -0
  95. package/dist/esm/ledger/dispatch-consumer-event.js +125 -0
  96. package/dist/esm/ledger/lifecycle-manager.js +328 -0
  97. package/dist/esm/ledger/usage-ledger.js +31 -0
  98. package/dist/esm/make-adapter/make-adapter.js +53 -0
  99. package/dist/esm/make-adapter/make-drop-target.js +264 -0
  100. package/dist/esm/make-adapter/make-monitor.js +93 -0
  101. package/dist/esm/util/add-attribute.js +8 -0
  102. package/dist/esm/util/combine.js +11 -0
  103. package/dist/esm/util/disable-native-drag-preview.js +30 -0
  104. package/dist/esm/util/entering-and-leaving-the-window.js +156 -0
  105. package/dist/esm/util/fix-post-drag-pointer-bug.js +108 -0
  106. package/dist/esm/util/get-input.js +14 -0
  107. package/dist/esm/util/once.js +16 -0
  108. package/dist/esm/util/reorder.js +19 -0
  109. package/dist/esm/util/scroll-just-enough-into-view.js +11 -0
  110. package/dist/esm/util/set-custom-native-drag-preview.js +104 -0
  111. package/dist/esm/version.json +5 -0
  112. package/dist/types/adapter/element-adapter.d.ts +42 -0
  113. package/dist/types/adapter/file-adapter.d.ts +18 -0
  114. package/dist/types/addon/cancel-unhandled.d.ts +7 -0
  115. package/dist/types/entry-point/adapter/element.d.ts +2 -0
  116. package/dist/types/entry-point/adapter/file.d.ts +2 -0
  117. package/dist/types/entry-point/addon/cancel-unhandled.d.ts +1 -0
  118. package/dist/types/entry-point/experimental/cross-with-element-adapter.d.ts +1 -0
  119. package/dist/types/entry-point/types.d.ts +1 -0
  120. package/dist/types/entry-point/util/combine.d.ts +1 -0
  121. package/dist/types/entry-point/util/disable-native-drag-preview.d.ts +1 -0
  122. package/dist/types/entry-point/util/once.d.ts +1 -0
  123. package/dist/types/entry-point/util/reorder.d.ts +1 -0
  124. package/dist/types/entry-point/util/scroll-just-enough-into-view.d.ts +1 -0
  125. package/dist/types/entry-point/util/set-custom-native-drag-preview.d.ts +1 -0
  126. package/dist/types/experimental/cross-window-element-adapter.d.ts +17 -0
  127. package/dist/types/index.d.ts +2 -0
  128. package/dist/types/internal-types.d.ts +275 -0
  129. package/dist/types/ledger/dispatch-consumer-event.d.ts +26 -0
  130. package/dist/types/ledger/lifecycle-manager.d.ts +16 -0
  131. package/dist/types/ledger/usage-ledger.d.ts +5 -0
  132. package/dist/types/make-adapter/make-adapter.d.ts +14 -0
  133. package/dist/types/make-adapter/make-drop-target.d.ts +5 -0
  134. package/dist/types/make-adapter/make-monitor.d.ts +8 -0
  135. package/dist/types/util/add-attribute.d.ts +5 -0
  136. package/dist/types/util/combine.d.ts +3 -0
  137. package/dist/types/util/disable-native-drag-preview.d.ts +3 -0
  138. package/dist/types/util/entering-and-leaving-the-window.d.ts +6 -0
  139. package/dist/types/util/fix-post-drag-pointer-bug.d.ts +14 -0
  140. package/dist/types/util/get-input.d.ts +2 -0
  141. package/dist/types/util/once.d.ts +2 -0
  142. package/dist/types/util/reorder.d.ts +9 -0
  143. package/dist/types/util/scroll-just-enough-into-view.d.ts +7 -0
  144. package/dist/types/util/set-custom-native-drag-preview.d.ts +52 -0
  145. package/experimental/cross-window-element-adapter/package.json +15 -0
  146. package/package.json +87 -0
  147. package/report.api.md +35 -0
  148. package/tmp/api-report-tmp.d.ts +13 -0
  149. package/types/package.json +15 -0
  150. package/util/combine/package.json +15 -0
  151. package/util/disable-native-drag-preview/package.json +15 -0
  152. package/util/once/package.json +15 -0
  153. package/util/reorder/package.json +15 -0
  154. package/util/scroll-just-enough-into-view/package.json +15 -0
  155. package/util/set-custom-native-drag-preview/package.json +15 -0
@@ -0,0 +1,233 @@
1
+ import { addAttribute } from '../util/add-attribute';
2
+ import { combine } from '../util/combine';
3
+ function copyReverse(array) {
4
+ return array.slice(0).reverse();
5
+ }
6
+ export function makeDropTarget({
7
+ typeKey,
8
+ defaultDropEffect
9
+ }) {
10
+ const registry = new WeakMap();
11
+ const dropTargetDataAtt = `data-drop-target-for-${typeKey}`;
12
+ const dropTargetSelector = `[${dropTargetDataAtt}]`;
13
+ function addToRegistry(args) {
14
+ registry.set(args.element, args);
15
+ return () => registry.delete(args.element);
16
+ }
17
+ function dropTargetForConsumers(args) {
18
+ // Guardrail: warn if the draggable element is already registered
19
+ if (process.env.NODE_ENV !== 'production') {
20
+ const existing = registry.get(args.element);
21
+ if (existing) {
22
+ // eslint-disable-next-line no-console
23
+ console.warn('You have already registered a `droppable` on the same element', {
24
+ existing,
25
+ proposed: args
26
+ });
27
+ }
28
+ }
29
+ return combine(addAttribute(args.element, {
30
+ attribute: dropTargetDataAtt,
31
+ value: 'true'
32
+ }), addToRegistry(args));
33
+ }
34
+ function getActualDropTargets({
35
+ source,
36
+ target,
37
+ input,
38
+ result = []
39
+ }) {
40
+ var _args$getData, _args$getData2, _args$getDropEffect, _args$getDropEffect2, _args$getIsSticky;
41
+ if (!(target instanceof Element)) {
42
+ return result;
43
+ }
44
+ const closest = target.closest(dropTargetSelector);
45
+
46
+ // Cannot find anything else
47
+ if (closest == null) {
48
+ return result;
49
+ }
50
+ const args = registry.get(closest);
51
+
52
+ // error: something had a dropTargetSelector but we could not
53
+ // find a match. For now, failing silently
54
+ if (args == null) {
55
+ return result;
56
+ }
57
+ const feedback = {
58
+ input,
59
+ source,
60
+ element: args.element
61
+ };
62
+
63
+ // if dropping is not allowed, skip this drop target
64
+ // and continue looking up the DOM tree
65
+ if (args.canDrop && !args.canDrop(feedback)) {
66
+ return getActualDropTargets({
67
+ source,
68
+ target: args.element.parentElement,
69
+ input,
70
+ result
71
+ });
72
+ }
73
+ const data = (_args$getData = (_args$getData2 = args.getData) === null || _args$getData2 === void 0 ? void 0 : _args$getData2.call(args, feedback)) !== null && _args$getData !== void 0 ? _args$getData : {};
74
+ const dropEffect = (_args$getDropEffect = (_args$getDropEffect2 = args.getDropEffect) === null || _args$getDropEffect2 === void 0 ? void 0 : _args$getDropEffect2.call(args, feedback)) !== null && _args$getDropEffect !== void 0 ? _args$getDropEffect : defaultDropEffect;
75
+ const sticky = Boolean((_args$getIsSticky = args.getIsSticky) === null || _args$getIsSticky === void 0 ? void 0 : _args$getIsSticky.call(args, feedback));
76
+ const record = {
77
+ data,
78
+ element: args.element,
79
+ dropEffect,
80
+ sticky
81
+ };
82
+ return getActualDropTargets({
83
+ source,
84
+ target: args.element.parentElement,
85
+ input,
86
+ // Using bubble ordering. Same ordering as `event.getPath()`
87
+ result: [...result, record]
88
+ });
89
+ }
90
+ function notifyCurrent({
91
+ eventName,
92
+ payload
93
+ }) {
94
+ for (const record of payload.location.current.dropTargets) {
95
+ var _entry$eventName;
96
+ const entry = registry.get(record.element);
97
+ const args = {
98
+ ...payload,
99
+ self: record
100
+ };
101
+ entry === null || entry === void 0 ? void 0 : (_entry$eventName = entry[eventName]) === null || _entry$eventName === void 0 ? void 0 : _entry$eventName.call(entry,
102
+ // I cannot seem to get the types right here.
103
+ // TS doesn't seem to like that one event can need `nativeSetDragImage`
104
+ // @ts-expect-error
105
+ args);
106
+ }
107
+ }
108
+ const actions = {
109
+ onGenerateDragPreview: notifyCurrent,
110
+ onDrag: notifyCurrent,
111
+ onDragStart: notifyCurrent,
112
+ onDrop: notifyCurrent,
113
+ onDropTargetChange: ({
114
+ payload
115
+ }) => {
116
+ const isCurrent = new Set(payload.location.current.dropTargets.map(record => record.element));
117
+ const visited = new Set();
118
+ for (const record of payload.location.previous.dropTargets) {
119
+ var _entry$onDropTargetCh;
120
+ visited.add(record.element);
121
+ const entry = registry.get(record.element);
122
+ const isOver = isCurrent.has(record.element);
123
+ const args = {
124
+ ...payload,
125
+ self: record
126
+ };
127
+ entry === null || entry === void 0 ? void 0 : (_entry$onDropTargetCh = entry.onDropTargetChange) === null || _entry$onDropTargetCh === void 0 ? void 0 : _entry$onDropTargetCh.call(entry, args);
128
+
129
+ // if we cannot find the drop target in the current array, then it has been left
130
+ if (!isOver) {
131
+ var _entry$onDragLeave;
132
+ entry === null || entry === void 0 ? void 0 : (_entry$onDragLeave = entry.onDragLeave) === null || _entry$onDragLeave === void 0 ? void 0 : _entry$onDragLeave.call(entry, args);
133
+ }
134
+ }
135
+ for (const record of payload.location.current.dropTargets) {
136
+ var _entry$onDropTargetCh2, _entry$onDragEnter;
137
+ // already published an update to this drop target
138
+ if (visited.has(record.element)) {
139
+ continue;
140
+ }
141
+ // at this point we have a new drop target that is being entered into
142
+ const args = {
143
+ ...payload,
144
+ self: record
145
+ };
146
+ const entry = registry.get(record.element);
147
+ entry === null || entry === void 0 ? void 0 : (_entry$onDropTargetCh2 = entry.onDropTargetChange) === null || _entry$onDropTargetCh2 === void 0 ? void 0 : _entry$onDropTargetCh2.call(entry, args);
148
+ entry === null || entry === void 0 ? void 0 : (_entry$onDragEnter = entry.onDragEnter) === null || _entry$onDragEnter === void 0 ? void 0 : _entry$onDragEnter.call(entry, args);
149
+ }
150
+ }
151
+ };
152
+ function dispatchEvent(args) {
153
+ // This line does not work in TS 4.2
154
+ // It does work in TS 4.7
155
+ // @ts-expect-error
156
+ actions[args.eventName](args);
157
+ }
158
+ function getIsOver({
159
+ source,
160
+ target,
161
+ input,
162
+ current
163
+ }) {
164
+ const actual = getActualDropTargets({
165
+ source,
166
+ target,
167
+ input
168
+ });
169
+
170
+ // stickiness is only relevant when we have less
171
+ // drop targets than we did before
172
+ if (actual.length >= current.length) {
173
+ return actual;
174
+ }
175
+
176
+ // less 'actual' drop targets than before,
177
+ // we need to see if 'stickiness' applies
178
+
179
+ // An old drop target will continue to be dropped on if:
180
+ // 1. it has the same parent
181
+ // 2. nothing exists in it's previous index
182
+
183
+ const lastCaptureOrdered = copyReverse(current);
184
+ const actualCaptureOrdered = copyReverse(actual);
185
+ const resultCaptureOrdered = [];
186
+ for (let index = 0; index < lastCaptureOrdered.length; index++) {
187
+ const last = lastCaptureOrdered[index];
188
+ const fresh = actualCaptureOrdered[index];
189
+
190
+ // if a record is in the new index -> use that
191
+ // it will have the latest data + dropEffect
192
+ if (fresh != null) {
193
+ resultCaptureOrdered.push(fresh);
194
+ continue;
195
+ }
196
+
197
+ // At this point we have no drop target in the old spot
198
+ // Check to see if we can use a previous sticky drop target
199
+
200
+ // stickiness is based on relationships to a parent
201
+ // so if we hit a drop target that is not sticky we
202
+ // can finish our search
203
+ if (!last.sticky) {
204
+ break;
205
+ }
206
+
207
+ // We only want the previous sticky item to 'stick' if
208
+ // the parent of the sticky item is unchanged
209
+
210
+ // The "parent" is the one inside of `resultCaptureOrdered`
211
+ // (the parent might be a drop target that was sticky)
212
+ const parent = resultCaptureOrdered[index - 1];
213
+ const lastParent = lastCaptureOrdered[index - 1];
214
+
215
+ // parents are the same (might both be undefined for index == 0)
216
+ // we can add the last entry and keep searching
217
+ if ((parent === null || parent === void 0 ? void 0 : parent.element) === (lastParent === null || lastParent === void 0 ? void 0 : lastParent.element)) {
218
+ resultCaptureOrdered.push(last);
219
+ continue;
220
+ }
221
+ // parents are not the same, we can exit our search
222
+ break;
223
+ }
224
+
225
+ // return bubble ordered result
226
+ return copyReverse(resultCaptureOrdered);
227
+ }
228
+ return {
229
+ dropTargetForConsumers,
230
+ getIsOver,
231
+ dispatchEvent
232
+ };
233
+ }
@@ -0,0 +1,80 @@
1
+ export function makeMonitor() {
2
+ const registry = new Set();
3
+ let dragging = null;
4
+ function tryAddToActive(monitor) {
5
+ if (!dragging) {
6
+ return;
7
+ }
8
+ // Monitor is allowed to monitor events if:
9
+ // 1. It has no `canMonitor` function (default is that a monitor can listen to everything)
10
+ // 2. `canMonitor` returns true
11
+ if (!monitor.canMonitor || monitor.canMonitor(dragging.canMonitorArgs)) {
12
+ dragging.active.add(monitor);
13
+ }
14
+ }
15
+ function monitorForConsumers(args) {
16
+ // We are giving each `args` a new reference so that you
17
+ // can create multiple monitors with the same `args`.
18
+ const entry = {
19
+ ...args
20
+ };
21
+ registry.add(entry);
22
+
23
+ // if there is an active drag we need to see if this new monitor is relevant
24
+ tryAddToActive(entry);
25
+ return function cleanup() {
26
+ registry.delete(entry);
27
+
28
+ // We need to stop publishing events during a drag to this monitor!
29
+ if (dragging) {
30
+ dragging.active.delete(entry);
31
+ }
32
+ };
33
+ }
34
+ function dispatchEvent({
35
+ eventName,
36
+ payload
37
+ }) {
38
+ if (eventName === 'onGenerateDragPreview') {
39
+ dragging = {
40
+ canMonitorArgs: {
41
+ initial: payload.location.initial,
42
+ source: payload.source
43
+ },
44
+ active: new Set()
45
+ };
46
+ for (const monitor of registry) {
47
+ tryAddToActive(monitor);
48
+ }
49
+ }
50
+
51
+ // This should never happen.
52
+ if (!dragging) {
53
+ return;
54
+ }
55
+
56
+ // Creating an array from the set _before_ iterating
57
+ // This is so that monitors added during the current event will not be called.
58
+ // This behaviour matches native EventTargets where an event listener
59
+ // cannot add another event listener during an active event to the same
60
+ // event target in the same event (for us we have a single global event target)
61
+ const active = Array.from(dragging.active);
62
+ for (const monitor of active) {
63
+ // A monitor can be removed by another monitor during an event.
64
+ // We need to check that the monitor is still registered before calling it
65
+ if (dragging.active.has(monitor)) {
66
+ var _monitor$eventName;
67
+ // @ts-expect-error: I cannot get this type working!
68
+ (_monitor$eventName = monitor[eventName]) === null || _monitor$eventName === void 0 ? void 0 : _monitor$eventName.call(monitor, payload);
69
+ }
70
+ }
71
+ if (eventName === 'onDrop') {
72
+ dragging.active.clear();
73
+ dragging = null;
74
+ }
75
+ }
76
+ return {
77
+ dispatchEvent,
78
+ monitorForConsumers
79
+ };
80
+ }
@@ -0,0 +1,7 @@
1
+ export function addAttribute(element, {
2
+ attribute,
3
+ value
4
+ }) {
5
+ element.setAttribute(attribute, value);
6
+ return () => element.removeAttribute(attribute);
7
+ }
@@ -0,0 +1,6 @@
1
+ /** Create a new combined function that will call all the provided functions */
2
+ export function combine(...fns) {
3
+ return function cleanup() {
4
+ fns.forEach(fn => fn());
5
+ };
6
+ }
@@ -0,0 +1,31 @@
1
+ // In order to disable the native drag preview you can
2
+ // use `event.dataTransfer.setDragImage()` to set a small
3
+ // invisible image as the drag preview.
4
+ // There are alternative techniques,
5
+ // (eg setting opacity to in onGenerateDragPreview and then 1 in onDragStart)
6
+ // but the technique in this file worked best across browsers and platforms
7
+
8
+ // Here we are preloading the image so that it is ready for the first drag.
9
+ // Even though the image is base64 encoded, the browser queues an async task
10
+ // to decode the image. The image needs to be decoded before it can be used
11
+ const tinyTransparentImage = (() => {
12
+ // SSR safe
13
+ if (typeof window === 'undefined') {
14
+ return null;
15
+ }
16
+
17
+ // Image generated by: https://png-pixel.com/
18
+ // It is a 1x1 transparent gif
19
+ // It is the smallest possible transparent image we could find that works on all platforms
20
+ // Note: using an encoded SVG would be nicer code, but it doesn't work on iOS
21
+ const img = new Image();
22
+ img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
23
+ return img;
24
+ })();
25
+ export function disableNativeDragPreview({
26
+ nativeSetDragImage
27
+ }) {
28
+ if (nativeSetDragImage && tinyTransparentImage) {
29
+ nativeSetDragImage(tinyTransparentImage, 0, 0);
30
+ }
31
+ }
@@ -0,0 +1,159 @@
1
+ import { bindAll } from 'bind-event-listener';
2
+
3
+ // *Usually* to detect if you are entering / leaving a window you can
4
+ // use the `event.relatedTarget` property:
5
+
6
+ // "dragenter":
7
+ // - `event.relatedTarget` should point to the element that you are coming from
8
+ // - Scenario: A -> B
9
+ // - `event.target`: `B` (entering B)
10
+ // - `event.relatedTarget`: `A`: (leaving A - where you are coming from)
11
+ // - if `event.relatedTarget` is `null` then you are entering the window (coming from `null`)
12
+ //
13
+ // "dragleave"
14
+ // - `event.relatedTarget` should point to the element you are going to
15
+ // - Scenario: A -> B (entered B, leaving A)
16
+ // - `event.target`: `A` (leaving A)
17
+ // - `event.relatedTarget`: `B`: (entering into B - where you are going to)
18
+ // - if `event.relatedTarget` is `null` then you are leaving the window (going to `null`)
19
+ //
20
+ // Unfortunately in Safari `event.relatedTarget` is *always* set to `null`
21
+ // Safari bug: https://bugs.webkit.org/show_bug.cgi?id=242627
22
+ // To work around this we count "dragenter" and "dragleave" events
23
+ const safariFix = {
24
+ isSafari: false,
25
+ // Using symbols for event properties so we don't clash with
26
+ // anything on the `event` object
27
+ leavingWindow: Symbol('leaving'),
28
+ enteringWindow: Symbol('entering')
29
+ };
30
+ export function isEnteringWindow({
31
+ dragEnter
32
+ }) {
33
+ if (dragEnter.type !== 'dragenter') {
34
+ return false;
35
+ }
36
+ if (safariFix.isSafari) {
37
+ return dragEnter.hasOwnProperty(safariFix.enteringWindow);
38
+ }
39
+ // This is the standard check.
40
+ // if `relatedTarget` is `null` during a "dragenter"
41
+ // then we are entering the`window`
42
+ return dragEnter.relatedTarget == null;
43
+ }
44
+ export function isLeavingWindow({
45
+ dragLeave
46
+ }) {
47
+ if (dragLeave.type !== 'dragleave') {
48
+ return false;
49
+ }
50
+ if (safariFix.isSafari) {
51
+ return dragLeave.hasOwnProperty(safariFix.leavingWindow);
52
+ }
53
+ // This is the standard check.
54
+ // if `relatedTarget` is `null` during a "dragleave"
55
+ // then we are leave the `window`
56
+ return dragLeave.relatedTarget == null;
57
+ }
58
+
59
+ // NOTE: this fix currently has no test coverage
60
+ // - our drag and drop browser tests currently only run in Chrome due to tooling limitations
61
+ // - it didn't feel helpful to unit test as it is merely replicating a bug
62
+
63
+ (function fixSafari() {
64
+ // Don't do anything when server side rendering
65
+ if (typeof window === 'undefined') {
66
+ return;
67
+ }
68
+
69
+ // rather than checking the userAgent for "jsdom" we can do this check
70
+ // so that the check will be removed completely in production code
71
+ if (process.env.NODE_ENV === 'test') {
72
+ return;
73
+ }
74
+ const {
75
+ userAgent
76
+ } = navigator;
77
+ const isSafari = userAgent.includes('AppleWebKit') && !userAgent.includes('Chrome');
78
+ if (!isSafari) {
79
+ return;
80
+ }
81
+ safariFix.isSafari = true;
82
+ function getInitialState() {
83
+ return {
84
+ enterCount: 0,
85
+ isOverWindow: false
86
+ };
87
+ }
88
+ let state = getInitialState();
89
+ function resetState() {
90
+ state = getInitialState();
91
+ }
92
+
93
+ // These event listeners are bound _forever_ and _never_ removed
94
+ // We don't bother cleaning up these event listeners (for now)
95
+ // as this workaround is only for Safari
96
+
97
+ // This is how the event count works:
98
+ //
99
+ // lift (+1 enterCount)
100
+ // - dragstart(draggable) [enterCount: 0]
101
+ // - dragenter(draggable) [enterCount: 1]
102
+ // leaving draggable (+0 enterCount)
103
+ // - dragenter(document.body) [enterCount: 2]
104
+ // - dragleave(draggable) [enterCount: 1]
105
+ // leaving window (-1 enterCount)
106
+ // - dragleave(document.body) [enterCount: 0] {leaving the window}
107
+
108
+ // Things to note:
109
+ // - dragenter and dragleave bubble
110
+ // - the first dragenter when entering a window might not be on `window`
111
+ // - it could be on an element that is pressed up against the window
112
+ // - (so we cannot rely on `event.target` values)
113
+
114
+ bindAll(window, [{
115
+ type: 'dragstart',
116
+ listener: () => {
117
+ state.enterCount = 0;
118
+ // drag start occurs in the source window
119
+ state.isOverWindow = true;
120
+
121
+ // When a drag first starts it will also trigger a "dragenter" on the draggable element
122
+ }
123
+ }, {
124
+ type: 'drop',
125
+ listener: resetState
126
+ }, {
127
+ type: 'dragend',
128
+ listener: resetState
129
+ }, {
130
+ type: 'dragenter',
131
+ listener: event => {
132
+ if (!state.isOverWindow && state.enterCount === 0) {
133
+ // Patching the `event` object
134
+ // The `event` object is shared with all event listeners for the event
135
+ // @ts-ignore
136
+ event[safariFix.enteringWindow] = true;
137
+ }
138
+ state.isOverWindow = true;
139
+ state.enterCount++;
140
+ }
141
+ }, {
142
+ type: 'dragleave',
143
+ listener: event => {
144
+ state.enterCount--;
145
+ if (state.isOverWindow && state.enterCount === 0) {
146
+ // Patching the `event` object as it is shared with all event listeners
147
+ // The `event` object is shared with all event listeners for the event
148
+ // @ts-ignore
149
+ event[safariFix.leavingWindow] = true;
150
+ state.isOverWindow = false;
151
+ }
152
+ }
153
+ }],
154
+ // using `capture: true` so that adding event listeners
155
+ // in bubble phase will have the correct symbols
156
+ {
157
+ capture: true
158
+ });
159
+ })();
@@ -0,0 +1,110 @@
1
+ import { bindAll } from 'bind-event-listener';
2
+ /** Set a `style` property on a `HTMLElement`
3
+ *
4
+ * @returns a `cleanup` function to restore the `style` property to it's original state
5
+ */
6
+ function setStyle(el, {
7
+ property,
8
+ rule,
9
+ priority = ''
10
+ }) {
11
+ const originalValue = el.style.getPropertyValue(property);
12
+ const originalPriority = el.style.getPropertyPriority(property);
13
+ el.style.setProperty(property, rule, priority);
14
+ return function cleanup() {
15
+ el.style.setProperty(property, originalValue, originalPriority);
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Allow the user to continue to interact with the element their pointer is over at the end of the drag.
21
+ * This is important to allow the user to be able to click, drag (etc) after they have finished a drag
22
+ *
23
+ * @returns a `cleanup` function to restore all elements under the users pointer to their original state
24
+ */
25
+ function allowPointerEventsOnElementUnderPointer({
26
+ current
27
+ }) {
28
+ const underUsersPointer = document.elementFromPoint(current.input.clientX, current.input.clientY);
29
+ if (!(underUsersPointer instanceof HTMLElement)) {
30
+ return null;
31
+ }
32
+
33
+ // Debug note: change from 'pointer-events: none' to 'background: green'
34
+ // to get a better sense of what is being achieved
35
+ return setStyle(underUsersPointer, {
36
+ property: 'pointer-events',
37
+ rule: 'auto',
38
+ priority: 'important'
39
+ });
40
+ }
41
+ function blockPointerEventsOnEverything() {
42
+ var _element$sheet;
43
+ const element = document.createElement('style');
44
+ // Adding a data attribute so to make it super clear to consumers
45
+ // (and to our tests) what this temporary style tag is for
46
+ element.setAttribute('pdnd-post-drag-fix', 'true');
47
+ document.head.appendChild(element);
48
+
49
+ // Debug note: change from 'pointer-events: none' to 'background: red'
50
+ // to get a better sense of what is being achieved
51
+ (_element$sheet = element.sheet) === null || _element$sheet === void 0 ? void 0 : _element$sheet.insertRule('* { pointer-events: none !important; }');
52
+ return function cleanup() {
53
+ document.head.removeChild(element);
54
+ };
55
+ }
56
+
57
+ /** 🔥🤮 Fix (Chrome, Safari and Firefox) bug where the element under where the user started dragging
58
+ * (on the viewport) is entered into by the browser after a drag finishes ("drop" or "dragend")
59
+ *
60
+ * @description
61
+ *
62
+ * Block pointer events on all elements except for the specific element that pointer is currently over
63
+ *
64
+ * - [Visual explanation of bug](https://twitter.com/alexandereardon/status/1633614212873465856)
65
+ * - [Chrome bug](https://bugs.chromium.org/p/chromium/issues/detail?id=410328)
66
+ */
67
+ export function fixPostDragPointerBug({
68
+ current
69
+ }) {
70
+ // Queuing a microtask to give any opportunity for frameworks to update their UI in a microtask
71
+ // Note: react@18 does standard state updates in a microtask
72
+ // We do this so our `atDestination` gets the _actual_ element that is under the users pointer
73
+ // at the end of the drag.
74
+ queueMicrotask(() => {
75
+ const undoUnderPointer = allowPointerEventsOnElementUnderPointer({
76
+ current
77
+ });
78
+
79
+ // This will also block pointer-events on the children of the element under the users pointer.
80
+ // This is what we want. If the user drops on a container element we don't want the children
81
+ // of the container to be incorrectly entered into
82
+ const undoGlobalBlock = blockPointerEventsOnEverything();
83
+ function cleanup() {
84
+ unbindEvents();
85
+ undoUnderPointer === null || undoUnderPointer === void 0 ? void 0 : undoUnderPointer();
86
+ undoGlobalBlock();
87
+ }
88
+ const unbindEvents = bindAll(window, [{
89
+ type: 'pointerdown',
90
+ listener: cleanup
91
+ }, {
92
+ type: 'pointermove',
93
+ listener: cleanup
94
+ }, {
95
+ type: 'focusin',
96
+ listener: cleanup
97
+ }, {
98
+ type: 'focusout',
99
+ listener: cleanup
100
+ },
101
+ // a 'pointerdown' should happen before 'dragstart', but just being super safe
102
+ {
103
+ type: 'dragstart',
104
+ listener: cleanup
105
+ }], {
106
+ // Using `capture` is more likely to not be impacted by consumers stopping events
107
+ capture: true
108
+ });
109
+ });
110
+ }
@@ -0,0 +1,14 @@
1
+ export function getInput(event) {
2
+ return {
3
+ altKey: event.altKey,
4
+ button: event.button,
5
+ buttons: event.buttons,
6
+ ctrlKey: event.ctrlKey,
7
+ metaKey: event.metaKey,
8
+ shiftKey: event.shiftKey,
9
+ clientX: event.clientX,
10
+ clientY: event.clientY,
11
+ pageX: event.pageX,
12
+ pageY: event.pageY
13
+ };
14
+ }
@@ -0,0 +1,13 @@
1
+ /** Provide a function that you only ever want to be called a single time */
2
+ export function once(fn) {
3
+ let cache = null;
4
+ return function wrapped(...args) {
5
+ if (!cache) {
6
+ const result = fn.apply(this, args);
7
+ cache = {
8
+ result: result
9
+ };
10
+ }
11
+ return cache.result;
12
+ };
13
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Reorder a provided `list`
3
+ * Returns a new array and does not modify the original array
4
+ */
5
+ export function reorder({
6
+ list,
7
+ startIndex,
8
+ finishIndex
9
+ }) {
10
+ if (startIndex === -1 || finishIndex === -1) {
11
+ return list;
12
+ }
13
+ const result = Array.from(list);
14
+ const [removed] = result.splice(startIndex, 1);
15
+ result.splice(finishIndex, 0, removed);
16
+ return result;
17
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Scroll an `element` just enough into view so that the element becomes totally visible.
3
+ * If the element is already totally visible then no scrolling will occur.
4
+ */
5
+ export function scrollJustEnoughIntoView({
6
+ element
7
+ }) {
8
+ element.scrollIntoView({
9
+ block: 'nearest',
10
+ inline: 'nearest'
11
+ });
12
+ }