@startlibs/components 1.0.3 → 1.1.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 (114) hide show
  1. package/lib/BasicBulletList.mjs +9 -0
  2. package/lib/BasicBulletList.mjs.map +1 -0
  3. package/lib/Button.mjs +95 -0
  4. package/lib/Button.mjs.map +1 -0
  5. package/lib/ContextMenu.mjs +452 -0
  6. package/lib/ContextMenu.mjs.map +1 -0
  7. package/lib/Dialog.mjs +232 -0
  8. package/lib/Dialog.mjs.map +1 -0
  9. package/lib/Draggable.mjs +880 -0
  10. package/lib/Draggable.mjs.map +1 -0
  11. package/lib/ErrorBoundary.mjs +108 -0
  12. package/lib/ErrorBoundary.mjs.map +1 -0
  13. package/lib/FillLastLineResizer.mjs +238 -0
  14. package/lib/FillLastLineResizer.mjs.map +1 -0
  15. package/lib/Flip.mjs +604 -0
  16. package/lib/Flip.mjs.map +1 -0
  17. package/lib/FlipList.mjs +186 -0
  18. package/lib/FlipList.mjs.map +1 -0
  19. package/lib/FormatDate.mjs +25 -0
  20. package/lib/FormatDate.mjs.map +1 -0
  21. package/lib/Icon.mjs +50 -0
  22. package/lib/Icon.mjs.map +1 -0
  23. package/lib/Image.mjs +41 -0
  24. package/lib/Image.mjs.map +1 -0
  25. package/lib/Link.mjs +116 -0
  26. package/lib/Link.mjs.map +1 -0
  27. package/lib/Loading.mjs +10 -0
  28. package/lib/Loading.mjs.map +1 -0
  29. package/lib/LoadingDiv.mjs +83 -0
  30. package/lib/LoadingDiv.mjs.map +1 -0
  31. package/lib/Notifications.mjs +63 -0
  32. package/lib/Notifications.mjs.map +1 -0
  33. package/lib/PageLoader.mjs +42 -0
  34. package/lib/PageLoader.mjs.map +1 -0
  35. package/lib/PersistentNotifications.mjs +130 -0
  36. package/lib/PersistentNotifications.mjs.map +1 -0
  37. package/lib/Popup.mjs +49 -0
  38. package/lib/Popup.mjs.map +1 -0
  39. package/lib/SplitColumnsContainer.mjs +43 -0
  40. package/lib/SplitColumnsContainer.mjs.map +1 -0
  41. package/lib/SwitchDiv.mjs +61 -0
  42. package/lib/SwitchDiv.mjs.map +1 -0
  43. package/lib/TextButton.mjs +41 -0
  44. package/lib/TextButton.mjs.map +1 -0
  45. package/lib/ToastNotifications.mjs +215 -0
  46. package/lib/ToastNotifications.mjs.map +1 -0
  47. package/lib/Tooltip.mjs +160 -0
  48. package/lib/Tooltip.mjs.map +1 -0
  49. package/lib/TransitionDiv.mjs +56 -0
  50. package/lib/TransitionDiv.mjs.map +1 -0
  51. package/lib/WarningDialog.mjs +84 -0
  52. package/lib/WarningDialog.mjs.map +1 -0
  53. package/lib/alt/buttons/OnClickLoadingButton.mjs +36 -0
  54. package/lib/alt/buttons/OnClickLoadingButton.mjs.map +1 -0
  55. package/lib/alt/dialogs/DialogWithImageHeader.mjs +14 -0
  56. package/lib/alt/dialogs/DialogWithImageHeader.mjs.map +1 -0
  57. package/lib/alt/dialogs/DialogWithSidebar.mjs +106 -0
  58. package/lib/alt/dialogs/DialogWithSidebar.mjs.map +1 -0
  59. package/lib/alt/dialogs/MobileBubbleDialog.mjs +49 -0
  60. package/lib/alt/dialogs/MobileBubbleDialog.mjs.map +1 -0
  61. package/lib/containers/DialogContainer.mjs +413 -0
  62. package/lib/containers/DialogContainer.mjs.map +1 -0
  63. package/lib/containers/Reposition.mjs +35 -0
  64. package/lib/containers/Reposition.mjs.map +1 -0
  65. package/lib/containers/usePositioning.mjs +294 -0
  66. package/lib/containers/usePositioning.mjs.map +1 -0
  67. package/lib/index.cjs +5000 -0
  68. package/lib/index.cjs.map +1 -0
  69. package/lib/index.mjs +37 -0
  70. package/lib/index.mjs.map +1 -0
  71. package/lib/useDialogWithToggle.mjs +18 -0
  72. package/lib/useDialogWithToggle.mjs.map +1 -0
  73. package/lib/willUseIsSticky.mjs +123 -0
  74. package/lib/willUseIsSticky.mjs.map +1 -0
  75. package/package.json +44 -29
  76. package/src/BasicBulletList.jsx +21 -0
  77. package/src/Button.jsx +288 -0
  78. package/src/ContextMenu.jsx +621 -0
  79. package/src/Dialog.jsx +413 -0
  80. package/src/Draggable.jsx +801 -0
  81. package/src/ErrorBoundary.jsx +75 -0
  82. package/src/FillLastLineResizer.jsx +226 -0
  83. package/src/Flip.jsx +560 -0
  84. package/src/FlipList.jsx +136 -0
  85. package/src/FormatDate.jsx +9 -0
  86. package/src/Icon.jsx +49 -0
  87. package/src/Image.jsx +23 -0
  88. package/src/Link.jsx +68 -0
  89. package/src/Loading.jsx +29 -0
  90. package/src/LoadingDiv.jsx +75 -0
  91. package/src/Notifications.jsx +89 -0
  92. package/src/PageLoader.jsx +48 -0
  93. package/src/PersistentNotifications.jsx +182 -0
  94. package/src/Popup.jsx +49 -0
  95. package/src/SplitColumnsContainer.jsx +63 -0
  96. package/src/SwitchDiv.jsx +30 -0
  97. package/src/TextButton.jsx +34 -0
  98. package/src/ToastNotifications.jsx +298 -0
  99. package/src/Tooltip.jsx +158 -0
  100. package/src/TransitionDiv.jsx +34 -0
  101. package/src/WarningDialog.jsx +70 -0
  102. package/src/alt/buttons/OnClickLoadingButton.jsx +16 -0
  103. package/src/alt/dialogs/DialogWithImageHeader.jsx +42 -0
  104. package/src/alt/dialogs/DialogWithSidebar.jsx +135 -0
  105. package/src/alt/dialogs/MobileBubbleDialog.jsx +56 -0
  106. package/src/containers/DialogContainer.jsx +423 -0
  107. package/src/containers/Reposition.jsx +24 -0
  108. package/src/containers/usePositioning.jsx +305 -0
  109. package/src/index.js +35 -0
  110. package/src/useDialogWithToggle.jsx +12 -0
  111. package/src/willUseIsSticky.jsx +113 -0
  112. package/.babelrc +0 -27
  113. package/lib/index.js +0 -5196
  114. package/rollup.config.js +0 -52
package/src/Flip.jsx ADDED
@@ -0,0 +1,560 @@
1
+ import React, {useLayoutEffect} from 'react'
2
+ import _ from 'lodash/fp'
3
+ import {_s, animationTiming, callIfFunction, oneWayAnimation} from '@startlibs/utils'
4
+ import {useConstant, useHiddenFill, willUseSharedCallback} from '@startlibs/core'
5
+ import {ifUndefined} from '@startlibs/utils'
6
+
7
+ const getMargins = (domNode) => {
8
+ const {marginTop,marginLeft,marginBottom,marginRight,paddingTop} = window.getComputedStyle(domNode);
9
+ return {marginTop,marginLeft,marginBottom,marginRight,paddingTop}
10
+ }
11
+
12
+ const getParentProperties = (parentNode) => {
13
+ if (!parentNode) return {}
14
+ const {marginTop,marginLeft,marginBottom,marginRight,paddingBottom,paddingTop} = window.getComputedStyle(parentNode);
15
+ return {parentStyle:{marginTop,marginLeft,marginBottom,marginRight,paddingBottom,paddingTop},parentNode}
16
+
17
+ }
18
+
19
+ export const toPosition = (domNode, parentRect = (domNode.offsetParent ? domNode.offsetParent.getBoundingClientRect() : {
20
+ top: 0,
21
+ left: 0
22
+ }), {allowFixed, finalPosition,withMargins} = {}) => {
23
+ if ((!allowFixed && domNode.style.position === 'fixed') || domNode.style.display === 'none') return
24
+ const {left, top, height, width} = domNode.getBoundingClientRect()
25
+ const margins = withMargins ? getMargins(domNode) : {}
26
+
27
+ // const {offsetLeft, offsetTop} = domNode
28
+ return {
29
+ domNode,
30
+ nextSibling: domNode.nextSibling,
31
+ top: finalPosition ? domNode.offsetTop : (top - parentRect.top),
32
+ left: finalPosition ? domNode.offsetLeft : (left - parentRect.left),
33
+ height, width,
34
+ ...getParentProperties(domNode.parentNode),
35
+ ...margins
36
+ }
37
+ }
38
+
39
+ const sumWithPx = (...args) => args.map(parseFloat).reduce((a, b) => a + b, 0) + 'px'
40
+
41
+ const childMAp = (childs, fn) => {
42
+ for (var c of childs) {
43
+ fn(c)
44
+ }
45
+ }
46
+
47
+ export const defaultAnimationMaker = animationTiming('0.25s ease-out')
48
+ const defaultEnterAnimation = oneWayAnimation('opacity: 0;', 'opacity: 1;')
49
+ const defaultSwitchAnimation = oneWayAnimation(`opacity: 1;`, `opacity: 0;`, `opacity: 0;`)
50
+
51
+ const SHORT_ANIMATIONS = {
52
+ fadeIn: defaultEnterAnimation
53
+ }
54
+ const applyDefault = (k) => SHORT_ANIMATIONS[k] || k
55
+
56
+ export const formatAnimation = (animationName /* array or string or fn with array or string*/, animationTimer /* string or fn */, reverse) => {
57
+ const animationMaker = typeof animationTimer === 'string' ? ((k) => k + ' ' + animationTimer) : animationTimer
58
+ const animationNameExecuted = typeof animationName === 'function' ? animationName() : animationName
59
+ const keyframes = [].concat(typeof animationNameExecuted === 'string' ? animationNameExecuted.split(',') : animationNameExecuted)
60
+ const applyReverse = (k) => reverse ? k + ' reverse' : k
61
+ return keyframes.map(k => applyReverse(k.trim().indexOf(' ') < 0 ? animationMaker(k) : applyDefault(k))).join(', ')
62
+ }
63
+
64
+ const cloneWrapper = (element) => {
65
+ const cloned = element.cloneNode(true)
66
+ const tempWrapper = document.createElement('div');
67
+ [].slice.call(cloned.childNodes).forEach(c => tempWrapper.append(c))
68
+ const {display, width, height, padding, borderLeftWidth, borderRightWidth, borderBottomWidth, borderTopWidth} = window.getComputedStyle(element)
69
+ Object.assign(tempWrapper.style, {
70
+ display,
71
+ width: sumWithPx(width, '-' + borderLeftWidth, '-' + borderRightWidth),
72
+ height: sumWithPx(height, '-' + borderBottomWidth, '-' + borderTopWidth),
73
+ padding,
74
+ position: 'absolute',
75
+ top: 0,
76
+ left: 0
77
+ })
78
+ return tempWrapper
79
+ }
80
+
81
+ const morphingEndEvents = new Map()
82
+ const morphTime = new Map()
83
+ const continueSwitchCallbacks = new Map()
84
+
85
+ const continueSwitchOnClone = (oldWrapper) => (newWrapper) => {
86
+ const prevOldClone = oldWrapper.querySelector('.JS-old-snap')
87
+ const prevNewClone = oldWrapper.querySelector('.JS-new-snap')
88
+ const oldClone = newWrapper.querySelector('.JS-old-snap')
89
+ const newClone = newWrapper.querySelector('.JS-new-snap')
90
+ oldClone.style.animationDelay = getDelays(prevOldClone, oldClone.style.animation)
91
+ newClone.style.animationDelay = getDelays(prevNewClone, newClone.style.animation)
92
+ morphTime.set(oldClone, morphTime.get(prevOldClone))
93
+ morphTime.set(newClone, morphTime.get(prevNewClone))
94
+ oldClone.addEventListener('animationend', oldClone.remove)
95
+ newClone.addEventListener('animationend', finishSwitchNew(newClone, newWrapper))
96
+ continueSwitchCallbacks.delete(oldWrapper)
97
+ continueSwitchCallbacks.set(newWrapper, continueSwitchOnClone(newWrapper))
98
+ }
99
+
100
+ const finishSwitchNew = (clone, wrapper) => () => {
101
+ clone.remove()
102
+ const newChildren = [].slice.call(wrapper.children)
103
+ childMAp(newChildren, c => {
104
+ if (c.dataset) {
105
+ // Guard against stale "0" from a prior uncompleted cycle: always restore
106
+ // to empty when prev is "0" since "0" is never a legitimate captured value.
107
+ const prev = c.dataset.prevOpacity
108
+ c.style.opacity = (prev && prev !== '0') ? prev : ''
109
+ }
110
+ })
111
+ // wrapper.style.willChange = ''
112
+ continueSwitchCallbacks.delete(wrapper)
113
+ }
114
+ const supportsEvent = "onanimationend" in HTMLElement.prototype
115
+ export const morph = (node, animation, callback) => {
116
+ if (!animation) {return}
117
+ const [cachedAnimation, cachedCallback] = morphingEndEvents.get(node) || []
118
+ const oldAnimation = cachedAnimation || node.style.animation
119
+ morphTime.set(node, Date.now())
120
+ const didNotStartPromise = Promise
121
+ .race([
122
+ new Promise(res => node.addEventListener('animationstart', res)),
123
+ new Promise((res,rej) => setTimeout(rej,supportsEvent? 1000 : 200)),
124
+ ])
125
+
126
+ node.style.animation = animation
127
+ if (!cachedCallback) {
128
+ const end = ({target}) => {
129
+ if (target !== node) return
130
+ morphTime.delete(node)
131
+ morphingEndEvents.delete(node)
132
+ node.removeEventListener('animationend', end)
133
+ callback && callback()
134
+ node.style.animation = oldAnimation
135
+ }
136
+ didNotStartPromise
137
+ .catch(() => end({target:node}))
138
+ node.addEventListener('animationend', end)
139
+ morphingEndEvents.set(node, [oldAnimation, end])
140
+ }
141
+ }
142
+
143
+ const hasClientRectChanged = (oldSnap, newSnap) => !['top', 'left', 'height', 'width'].every(_ => oldSnap[_] === newSnap[_])
144
+
145
+ export const getClientRectAnimation = (oldSnap, newSnap, {skipTransform, smallJumpFix } = {}) => {
146
+ if (oldSnap && newSnap && hasClientRectChanged(oldSnap, newSnap)) {
147
+ const node = newSnap.domNode
148
+ const {offsetLeft, offsetTop, scrollHeight, clientHeight, scrollWidth, clientWidth} = node
149
+ const scrollTop = (node.parentNode && node.parentNode.scrollTop) || 0
150
+ const scrollLeft = (node.parentNode && node.parentNode.scrollLeft) || 0
151
+ const left = oldSnap.left - offsetLeft + scrollLeft
152
+ const top = oldSnap.top - offsetTop + scrollTop
153
+ const deltaHeight = oldSnap.height - newSnap.height
154
+ let heightFix = ''
155
+ let finalPadding = ''
156
+ if (deltaHeight > 0) {
157
+ const {paddingBottom} = window.getComputedStyle(node)
158
+ finalPadding = 'padding-bottom:' + paddingBottom + ';'
159
+ heightFix = `padding-bottom: ${sumWithPx(deltaHeight, paddingBottom) + (smallJumpFix || 0)};`
160
+ }
161
+ const overflowFix = scrollHeight > clientHeight || scrollWidth > clientWidth ? `overflow: hidden;` : ``
162
+ const transform = ((left !== 0 || top !== 0) && !skipTransform) ? `transform: translate(${left}px,${top}px);` : ''
163
+ const parentPaddingTopDelta = (parseInt(oldSnap.parentStyle?.paddingTop) || 0) - (parseInt(newSnap.parentStyle?.paddingTop) || 0)
164
+ const parentPaddingBottomDelta = (parseInt(oldSnap.parentStyle?.paddingBottom) || 0) - (parseInt(newSnap.parentStyle?.paddingBottom) || 0)
165
+ const oldMarginTop = (parseInt(oldSnap.marginTop) || 0) + parentPaddingTopDelta
166
+ const oldMarginBottom = (parseInt(oldSnap.marginBottom) || 0) + parentPaddingBottomDelta
167
+
168
+ const oldTransform = node.style.transform
169
+ return [oneWayAnimation(`
170
+ ${transform}
171
+ ${heightFix}
172
+ ${overflowFix}
173
+ margin-top: ${oldMarginTop}px;
174
+ margin-bottom: ${oldMarginBottom}px;
175
+ width: ${oldSnap.width}px;
176
+ height: ${Math.max(0,oldSnap.height+(smallJumpFix || 0))}px;
177
+ `, `
178
+ ${finalPadding}
179
+ ${oldTransform ? `transform:${oldTransform};` : ''}
180
+ ${overflowFix}
181
+ margin-top:${parseInt(newSnap.marginTop) || 0}px;
182
+ margin-bottom:${parseInt(newSnap.marginBottom) || 0}px;
183
+ width: ${newSnap.width}px;
184
+ height: ${Math.max(0,newSnap.height+(smallJumpFix || 0))}px;
185
+ `)]
186
+ } else {
187
+ return []
188
+ }
189
+ }
190
+
191
+ const getClientRectAnimationForSwitch = (oldSnap, newSnap, {skipTransform } = {}) => {
192
+ if (hasClientRectChanged(oldSnap, newSnap)) {
193
+ const node = newSnap.domNode
194
+ const {offsetLeft, offsetTop, scrollHeight, clientHeight, scrollWidth, clientWidth} = node
195
+ const scrollTop = node.parentNode?.scrollTop || 0
196
+ const scrollLeft = node.parentNode?.scrollLeft || 0
197
+
198
+
199
+ // get margin-bottom
200
+ const prevBorder = node.style.border
201
+ node.style.border = '1px solid transparent'
202
+ const borderHeight = node.clientHeight
203
+ node.style.border = prevBorder
204
+
205
+ // get negative margin top
206
+ const prevPadding = newSnap.paddingTop
207
+ const nonPaddingFixTop = newSnap.clonedNode.getBoundingClientRect().top
208
+ let topWithPaddingFix = nonPaddingFixTop
209
+ if (!parseInt(prevPadding)) {
210
+ node.style.paddingTop = '1px'
211
+ topWithPaddingFix = newSnap.clonedNode.getBoundingClientRect().top
212
+ node.style.paddingTop = ''
213
+ }
214
+
215
+ const newSnapNegativeTopMarginDelta = topWithPaddingFix - nonPaddingFixTop
216
+ const newSnapBottomMarginDelta = clientHeight - borderHeight
217
+
218
+ const left = oldSnap.left - offsetLeft + scrollLeft
219
+ const top = oldSnap.top - offsetTop + scrollTop - newSnapNegativeTopMarginDelta
220
+
221
+
222
+ const deltaHeight = oldSnap.height - newSnap.height
223
+ let heightFix = ''
224
+ let finalPadding = ''
225
+ if (deltaHeight > 0) {
226
+ const {paddingBottom} = window.getComputedStyle(node)
227
+ finalPadding = 'padding-bottom:' + paddingBottom + ';'
228
+ heightFix = `padding-bottom: ${sumWithPx(deltaHeight, paddingBottom)};`
229
+ }
230
+ const overflowFix = scrollHeight > clientHeight || scrollWidth > clientWidth ? `overflow: hidden;` : ``
231
+ const transform = ((left !== 0 || top !== 0) && !skipTransform) ? `transform: translate(${left}px,${top}px);` : ''
232
+
233
+
234
+ const oldTransform = node.style.transform
235
+ return [oneWayAnimation(`
236
+ ${transform}
237
+ ${heightFix}
238
+ ${overflowFix}
239
+ width: ${oldSnap.width}px;
240
+ height: ${oldSnap.height}px;
241
+ `, `
242
+ ${finalPadding}
243
+ ${oldTransform ? `transform:${oldTransform};` : ''}
244
+ ${overflowFix}
245
+ width: ${newSnap.width}px;
246
+ height: ${(newSnap.height - newSnapBottomMarginDelta)}px;
247
+ `)]
248
+ } else {
249
+ return []
250
+ }
251
+ }
252
+
253
+ const willPlaySwitch = (oldSnap, newSnap, {switchIn, switchOut, switchAnimation, timing, transformHeight = _.identity}) => {
254
+ const realWrapper = newSnap.domNode
255
+ const newChildren = [].slice.call(realWrapper.children)
256
+ // realWrapper.style.willChange = 'width,height,margin-top;'
257
+ if (realWrapper.firstChild) {
258
+ realWrapper.insertBefore(oldSnap.clonedNode, realWrapper.firstChild)
259
+ realWrapper.insertBefore(newSnap.clonedNode, realWrapper.firstChild)
260
+ } else {
261
+ realWrapper.append(oldSnap.clonedNode)
262
+ realWrapper.append(newSnap.clonedNode)
263
+ }
264
+
265
+ oldSnap.clonedNode.className = oldSnap.clonedNode.className + ' JS-old-snap'
266
+ newSnap.clonedNode.className = newSnap.clonedNode.className + ' JS-new-snap'
267
+ // newSnap.clonedNode.style.willChange = 'width,height,opacity'
268
+ // oldSnap.clonedNode.style.willChange = 'width,height,opacity'
269
+ const realWrapperRect = realWrapper.getBoundingClientRect()
270
+ const resizeToNew = oneWayAnimation(`
271
+ overflow: hidden;
272
+ width: ${oldSnap.width}px;
273
+ height: ${oldSnap.height}px;
274
+ `, `
275
+ overflow: hidden;
276
+ width: ${newSnap.clonedNode.style.width};
277
+ height: ${callIfFunction(transformHeight,newSnap.clonedNode.style.height,realWrapperRect)};
278
+ `)
279
+
280
+ childMAp(newChildren, c => {
281
+ if (c.dataset && !c.dataset.noFlip) {
282
+ // Don't capture "0" as prevOpacity — it's always a transient value from
283
+ // a prior uncompleted willPlaySwitch. Otherwise finishSwitchNew restores
284
+ // opacity to "0" permanently, leaving content invisible.
285
+ if (c.style.opacity && c.style.opacity !== '0') {
286
+ c.dataset.prevOpacity = c.style.opacity
287
+ }
288
+ c.style.opacity = '0'
289
+ }
290
+ })
291
+ childMAp([].slice.call(oldSnap.clonedNode.children), c => {
292
+ if (c.dataset && c.dataset.noFlip) {
293
+ c.style.opacity = '0'
294
+ }
295
+ })
296
+
297
+ const resizeOld = (oldSnap.width > newSnap.width || oldSnap.height > newSnap.height) ? [resizeToNew] : [resizeToNew]
298
+ const resizeNew = (oldSnap.width < newSnap.width || oldSnap.height < newSnap.height) ? [resizeToNew] : [resizeToNew]
299
+
300
+ return () => {
301
+ morph(oldSnap.clonedNode,
302
+ formatAnimation([].concat(switchOut || switchAnimation, resizeOld), timing),
303
+ () => oldSnap.clonedNode.remove()
304
+ )
305
+ newSnap.clonedNode.style.opacity = '0'
306
+ continueSwitchCallbacks.set(realWrapper, continueSwitchOnClone(realWrapper))
307
+
308
+ morph(newSnap.clonedNode,
309
+ formatAnimation([formatAnimation(
310
+ switchIn || switchAnimation,
311
+ timing,
312
+ !switchIn
313
+ )].concat(resizeNew), timing),
314
+ finishSwitchNew(newSnap.clonedNode, realWrapper)
315
+ )
316
+ }
317
+ }
318
+
319
+
320
+ const getDelays = (el, animation) => {
321
+ const delayStr = (morphTime.get(el) - Date.now()) + 'ms'
322
+ return animation.split(',').map(_ => delayStr).join(',')
323
+ }
324
+
325
+ class Snapshoter extends React.Component {
326
+
327
+ getSnapshotBeforeUpdate(oldProps) {
328
+ if (oldProps.on !== this.props.on) {
329
+ console.log('on', 'UHUL')
330
+ } else if (oldProps.moveOn !== this.props.moveOn) {
331
+ console.log('moveOn', 'UHUL')
332
+ }
333
+ return null
334
+ }
335
+
336
+ render() {
337
+ return null
338
+ }
339
+ }
340
+
341
+ export const useFlip = ({on, moveOn}) => {
342
+ useHiddenFill(<Snapshoter on={on} moveOn={moveOn} callback={console.log}/>)
343
+ }
344
+
345
+ const getAnimationNames = (animation) => animation.split(',').map(_ => _.replace(/\s*(\S+).*/, '$1'))
346
+
347
+ export const willUseFlip = () => {
348
+
349
+ let controlElement = null
350
+ let morphEnabled = false
351
+ const disableMorph = () => {
352
+ controlElement = null
353
+ morphEnabled = false
354
+ }
355
+ const [useRegisterEnter, enterChildrenCallback] = willUseSharedCallback(useLayoutEffect)
356
+ const [useRegisterLeave, leaveChildrenCallback] = willUseSharedCallback(useLayoutEffect)
357
+ const [useRegisterMorph, morphChildrenCallback, getSize] = willUseSharedCallback(useLayoutEffect)
358
+
359
+
360
+ const useEnterAnimation = (ref, onEnter = defaultEnterAnimation, {onEnterEnd, timing = defaultAnimationMaker, enterEffect} = {}) => {
361
+ useLayoutEffect(() => {
362
+ if (!morphEnabled && onEnter && ref.current) {
363
+ const el = ref.current
364
+ const animation = formatAnimation(onEnter, timing)
365
+ const endEffect = callIfFunction(enterEffect,el)
366
+ morph(
367
+ el,
368
+ animation,
369
+ () => {
370
+ callIfFunction(onEnterEnd)
371
+ callIfFunction(endEffect,el)
372
+ }
373
+ )
374
+ enterChildrenCallback(timing)
375
+ }
376
+ }, [onEnter])
377
+ }
378
+
379
+ const useLeaveAnimation = (ref, onLeave = defaultEnterAnimation, {ref: removeRef, leaveEffect, timing = defaultAnimationMaker, nestedFlip = false, events = {}, reverse = true} = {}) => {
380
+ useLayoutEffect(() => () => {
381
+ if (onLeave && ref.current) {
382
+ const el = ref.current
383
+ const animation = formatAnimation(onLeave, timing, reverse)
384
+ // on leave animations require a clone, as the real node will be intantaneousy removed by react
385
+ leaveChildrenCallback(timing)
386
+ const isNested = callIfFunction(nestedFlip)
387
+ const clone = isNested ? el : el.cloneNode(true)
388
+ _s.each((callback, type) => clone.addEventListener(type, callback), events)
389
+
390
+ const endEffect = callIfFunction(leaveEffect,clone,el)
391
+
392
+ if (removeRef) { removeRef.current = clone }
393
+ if (!isNested) {
394
+ el.parentNode.insertBefore(clone, el)
395
+ }
396
+ // sometimes the clone may need to trigger events (usual case: onmouseover to reopen the element)
397
+
398
+ // allow clone to be continued by another animation
399
+ controlElement = clone
400
+ // the animation
401
+ const animate = () => {
402
+ morph(
403
+ clone,
404
+ animation,
405
+ () => {
406
+ clone.remove()
407
+ disableMorph()
408
+ callIfFunction(endEffect,clone,el)
409
+ }
410
+ )
411
+ }
412
+
413
+ // wait for current animation to end and then animate
414
+ if (clone.style.animation) {
415
+ const maxAnimationTime = _.max((clone.style.animationDuration.split(",") || ["0.25s"]).map(s => parseFloat(s.trim() || "0.25") * (s.indexOf("m")>0 ? 1 : 1000)))
416
+ const timeoutRef = {}
417
+ const animationEnd = (e) => {
418
+ clearTimeout(timeoutRef.current || -1)
419
+ clone.removeEventListener('animationend', animationEnd)
420
+ clone.style.animationDelay = ''
421
+ e?.stopImmediatePropagation()
422
+ setTimeout(animate, 1)
423
+ }
424
+ timeoutRef.current = setTimeout(animationEnd,maxAnimationTime)
425
+ clone.addEventListener('animationend', animationEnd)
426
+
427
+ clone.style.animationDelay = getDelays(el, clone.style.animation)
428
+ } else {
429
+ animate()
430
+ }
431
+ }
432
+ }, [onLeave])
433
+ }
434
+
435
+ const useMorphAnimation = (ref, ready = true, {timing = defaultAnimationMaker} = {}) => {
436
+ useLayoutEffect(() => {
437
+ if (ready && controlElement && ref.current) {
438
+ // const controlElement =getControlElement()
439
+ const el = ref.current
440
+ const animation = oneWayAnimation(`opacity:${window.getComputedStyle(controlElement).opacity};`, `opacity: 1`)
441
+ const oldSnap = {
442
+ ...toPosition(controlElement, undefined, {allowFixed: true}),
443
+ clonedNode: cloneWrapper(controlElement)
444
+ }
445
+ const newSnap = {...toPosition(el, undefined, {allowFixed: true}), clonedNode: cloneWrapper(el)}
446
+ const rectAnimation = getClientRectAnimation(oldSnap, newSnap)
447
+ morph(newSnap.domNode, formatAnimation(rectAnimation.concat(animation), timing), disableMorph)
448
+ morphChildrenCallback((executeAnimation) => executeAnimation(timing))
449
+ if (rectAnimation.length) {
450
+ willPlaySwitch(oldSnap, newSnap, {switchAnimation: defaultSwitchAnimation, timing})()
451
+ }
452
+ controlElement.remove()
453
+ disableMorph()
454
+ }
455
+ return () => {
456
+ morphEnabled = true
457
+ // continue existing switch animation on the clone
458
+ if (continueSwitchCallbacks.has(ref.current)) {
459
+ continueSwitchCallbacks.get(ref.current)(controlElement)
460
+ }
461
+ }
462
+ }, [ready])
463
+ }
464
+
465
+ const useNestedEnterAnimation = (ref, onEnter, {timing} = {}) => {
466
+ useRegisterEnter((parentTiming) => {
467
+ const el = ref.current
468
+ const animation = formatAnimation(onEnter, timing || parentTiming)
469
+ morph(el, animation)
470
+ return getAnimationNames(animation)
471
+ })
472
+ }
473
+ const useNestLeaveAnimation = (ref, onLeave, {timing, events} = {}) => {
474
+ useRegisterLeave((parentTiming) => {
475
+ const el = ref.current
476
+ const animation = formatAnimation(onLeave, timing || parentTiming)
477
+ morph(el, animation)
478
+ return getAnimationNames(animation)
479
+ })
480
+ }
481
+
482
+ const willUseNestedMorphAnimation = () => {
483
+ const morphId = _.uniqueId('JS-')
484
+ return (ref) => {
485
+ useLayoutEffect(() => {
486
+ if (ref.current?.dataset) {
487
+ ref.current.dataset.morphId = morphId
488
+ }
489
+ })
490
+ const executeAnimation = useConstant((timing) => {
491
+ const prev = controlElement.querySelector(`[data-morph-id='${morphId}']`)
492
+ const next = ref.current
493
+ if (next.dataset) {
494
+ next.dataset.noFlip = true
495
+ }
496
+ next.style.opacity = '1'
497
+ const prevPos = prev.getBoundingClientRect()
498
+ const nextPos = next.getBoundingClientRect()
499
+ const originalTransform = next.style.transform || ''
500
+ const deltaX = Math.round(nextPos.left - prevPos.left)
501
+ const deltaY = Math.round(nextPos.top - prevPos.top)
502
+ const newLeft = deltaX > 0 ? -deltaX : -deltaX
503
+ const animation = oneWayAnimation(`
504
+ transform: translate(${newLeft}px,${deltaY}px) ${originalTransform};
505
+ `, `
506
+ transform: translate(0,0) ${originalTransform};
507
+ `)
508
+ morph(next, formatAnimation(animation, timing))
509
+ })
510
+ useRegisterMorph(executeAnimation)
511
+ }
512
+ }
513
+
514
+ return [[useEnterAnimation, useLeaveAnimation, useMorphAnimation], [useNestedEnterAnimation, useNestLeaveAnimation], willUseNestedMorphAnimation]
515
+ }
516
+
517
+ export const willUseFlipSwitch = () => {
518
+ let oldSnap = null
519
+ const getTempWrapper = (parent,positionConfig = {}) => {
520
+ const tempWrapper = cloneWrapper(parent)
521
+ return {...toPosition(parent,undefined,positionConfig), clonedNode: tempWrapper}
522
+ }
523
+
524
+ return (ref, onEnter, onLeave, {timing = defaultAnimationMaker, switchAnimation = defaultSwitchAnimation, effect} = {}) => {
525
+ useLayoutEffect(() => {
526
+ if (oldSnap && ref.current) {
527
+ const newSnap = getTempWrapper(ref.current,{withMargins:true})
528
+ let switchToPlay = _.identity
529
+ if (oldSnap.clonedNode) {
530
+ switchToPlay = willPlaySwitch(oldSnap, newSnap, {
531
+ switchIn: callIfFunction(onEnter),
532
+ switchOut: callIfFunction(onLeave),
533
+ switchAnimation: callIfFunction(switchAnimation),
534
+ timing
535
+ })
536
+ }
537
+ const animation = getClientRectAnimationForSwitch(oldSnap, newSnap)
538
+ const endEffect = callIfFunction(effect,oldSnap.clonedNode,newSnap.clonedNode,ref.current)
539
+ switchToPlay()
540
+ morph(newSnap.domNode, formatAnimation(animation, timing), () => callIfFunction(endEffect,oldSnap.clonedNode,newSnap.clonedNode))
541
+ }
542
+ return () => {
543
+ oldSnap = getTempWrapper(ref.current,{withMargins:true})
544
+ }
545
+ }, [])
546
+ }
547
+ }
548
+
549
+ const flipHooks = willUseFlip()
550
+ const useFlipEnter = flipHooks[0][0]
551
+ const useFlipLeave = flipHooks[0][1]
552
+ export {useFlipEnter, useFlipLeave}
553
+
554
+ export const useFlipAnimation = (ref, onEnterOrBoth, onLeaveOrIgnore = true, config = {}) => {
555
+ const onEnter = ifUndefined(onEnterOrBoth && onEnterOrBoth.onEnter,onEnterOrBoth)
556
+ const onLeave = ifUndefined(onEnterOrBoth && onEnterOrBoth.onLeave,onLeaveOrIgnore)
557
+ useFlipEnter(ref, onEnter, config)
558
+ useFlipLeave(ref, onLeave === true ? onEnter : onLeave, {...config, reverse: onLeave === true})
559
+ }
560
+
@@ -0,0 +1,136 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom'
3
+ import {defaultAnimationMaker, morph, formatAnimation, getClientRectAnimation, toPosition} from './Flip'
4
+ import {oneWayAnimation} from '@startlibs/utils'
5
+ import {callIfFunction} from '@startlibs/utils'
6
+
7
+
8
+ export class FlipList extends React.Component {
9
+
10
+ static defaultProps = {
11
+ timing: defaultAnimationMaker,
12
+ animation: oneWayAnimation(`transform: scale(1); transform-origin: center; opacity: 1;`, `transform: scale(0); transform-origin: center; opacity: 0;`)
13
+ }
14
+
15
+ componentDidUpdate(prevProps, prevState, snap) {
16
+ if (snap) {
17
+ const useLockedParentRect = (this.props.anchoredDownList) && snap.parentRect
18
+ const newSnap = this.calculatePositionForAnimation(useLockedParentRect ? snap.parentRect : undefined)
19
+ if (newSnap) { // se não tiver parentNode, verficar se há uma div sendo offsetParent, body não serve
20
+ this.play(snap, newSnap)
21
+ }
22
+ }
23
+ }
24
+
25
+ play = (oldSnap, newSnap) => {
26
+ const {animation,insertAnimation,removeAnimation, onEndAnimation, onStartAnimation, effect, skipChromeFix, timing, skipParentMorph} = this.props
27
+ newSnap.children.forEach(newItemSnap => {
28
+ const oldItemSnap = oldSnap.children.get(newItemSnap.domNode)
29
+ if (!oldItemSnap) return
30
+ const rectAnimation = getClientRectAnimation(oldItemSnap, newItemSnap)
31
+ morph(newItemSnap.domNode,formatAnimation(rectAnimation,timing))
32
+ })
33
+ if (newSnap.children.size > oldSnap.children.size) {
34
+ const newItems = Array.from(newSnap.children.values()).filter(item => !oldSnap.children.has(item.domNode))
35
+ newItems.forEach((newItem) => {
36
+ morph(newItem.domNode, formatAnimation(insertAnimation || animation, timing, !insertAnimation))
37
+ })
38
+ } else if (newSnap.children.size < oldSnap.children.size) {
39
+ const oldItems = Array.from(oldSnap.children.values()).filter(item => !newSnap.children.has(item.domNode))
40
+
41
+ oldItems.forEach((oldItem) => {
42
+ const reinserted = this.reinsertSnap(oldItem,oldSnap.scrollTop - newSnap.scrollTop, oldItems)
43
+ if (!reinserted) { return }
44
+ new Promise(res => {
45
+ morph(oldItem.domNode, formatAnimation(removeAnimation || animation, timing), res)
46
+ setTimeout(res,1500)
47
+ }).then(() => {
48
+ oldItem.domNode.remove()
49
+ })
50
+ })
51
+
52
+ }
53
+ const rectAnimation = getClientRectAnimation(oldSnap.parent, newSnap.parent,{skipTransform:true,smallJumpFix: this.props.smallJumpFix})
54
+ if (newSnap.parent?.domNode && !skipParentMorph) {
55
+ const endEffect = callIfFunction(effect, newSnap.parent.domNode, {newSnap, oldSnap})
56
+ callIfFunction(onStartAnimation)
57
+ morph(newSnap.parent.domNode,formatAnimation(rectAnimation, timing),endEffect || onEndAnimation)
58
+ }
59
+ }
60
+
61
+ reinsertSnap = (item,scrollDelta, allChildren) => {
62
+ if (item.nextSibling) {
63
+
64
+ if (!item.nextSibling.parentNode) {
65
+ const next = allChildren.find(possible => item.nextSibling === possible.domNode)
66
+ if (next) {
67
+ return this.reinsertSnap({...item,nextSibling:next.nextSibling},scrollDelta,allChildren)
68
+ }
69
+ return false;
70
+ }
71
+ item.nextSibling.parentNode?.insertBefore(item.domNode, item.nextSibling)
72
+ } else {
73
+ item.parentNode.appendChild(item.domNode)
74
+ }
75
+ const {width,height} = item.domNode.getBoundingClientRect()
76
+ item.domNode.style.left = item.domNode.offsetLeft + 'px'
77
+ item.domNode.style.top = item.domNode.offsetTop - scrollDelta + 'px'
78
+ if (!this.props.anchoredDownList) {
79
+ item.domNode.style.marginTop = 0
80
+ }
81
+ item.domNode.style.marginLeft = 0
82
+ item.domNode.style.width = width + 'px'
83
+ item.domNode.style.height = height + 'px'
84
+ item.domNode.style.position = 'absolute'
85
+ callIfFunction(this.props.onReinsert,item.domNode,item)
86
+ return true
87
+ }
88
+
89
+ getSnapshotBeforeUpdate(oldProps) {
90
+ if (this.props.play || this.props.on !== oldProps.on || (this.props.children.length !== oldProps.children.length)) {
91
+ return this.calculatePositionForAnimation()
92
+ }
93
+ return null
94
+ }
95
+
96
+ getParents = () => {
97
+ const node = ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode(this)
98
+ const {parentNode, offsetParent: possibleOffsetParent} = node
99
+ const offsetParent = possibleOffsetParent || (window.getComputedStyle(parentNode).position !== 'static' ? parentNode : parentNode.offsetParent)
100
+ if (!offsetParent) return {};
101
+ const parentRect = offsetParent.getBoundingClientRect()
102
+ return {
103
+ offsetParent,
104
+ parentNode,
105
+ parentRect
106
+ }
107
+ }
108
+
109
+ calculatePositionForAnimation = (lockedParentRect) => {
110
+ const {parentRect, parentNode, offsetParent} = this.getParents()
111
+ if (!offsetParent) return null
112
+ const effectiveParentRect = lockedParentRect || parentRect
113
+ const children = new Map();
114
+ [].slice.call(parentNode.children).map((node) => {
115
+ const position = toPosition(node, effectiveParentRect, {withMargins:true, numberOfSiblings: this.props.numberOfElements || 1})
116
+ if (position) { children.set(node, position) }
117
+ })
118
+ return {
119
+ children,
120
+ parent: toPosition(offsetParent,undefined, {withMargins:true}),
121
+ parentRect: effectiveParentRect,
122
+ scrollTop:offsetParent.scrollTop
123
+ }
124
+ }
125
+
126
+ render() {
127
+ const {children, as, play, on, onStartAnimation, onEndAnimation, ...rest} = this.props
128
+ const result = React.Children.count(children) ? children : <div style={{display: 'none'}}/>
129
+ if (as) {
130
+ const RenderAs = as
131
+ return <RenderAs {...rest}>{result}</RenderAs>
132
+ }
133
+ return result
134
+ }
135
+
136
+ }