",
6 | "license": "MIT",
7 | "homepage": "https://94726.github.io/aioli",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/94726/aioli"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/94726/aioli/issues"
14 | },
15 | "keywords": [
16 | "vue",
17 | "drawer",
18 | "bottom sheet",
19 | "dialog",
20 | "drawer",
21 | "vaul"
22 | ],
23 | "type": "module",
24 | "main": "dist/index.js",
25 | "module": "./dist/index.js",
26 | "types": "dist/index.d.ts",
27 | "sideEffects": false,
28 | "files": [
29 | "dist"
30 | ],
31 | "exports": {
32 | ".": {
33 | "import": "./dist/index.js",
34 | "types": "./dist/index.d.ts"
35 | },
36 | "./nuxt": {
37 | "import": "./dist/nuxt.js",
38 | "types": "./dist/nuxt.d.ts"
39 | },
40 | "./styles": {
41 | "import": "./dist/styles.css"
42 | },
43 | "./dist/*": "./dist/*"
44 | },
45 | "scripts": {
46 | "build": "vite build",
47 | "typecheck": "vue-tsc --noEmit -p .",
48 | "prepack": "cp ../README.md ./",
49 | "postpack": "rm ./README.md"
50 | },
51 | "devDependencies": {
52 | "@nuxt/schema": "~3.12.4",
53 | "@vitejs/plugin-vue": "^5.1.2",
54 | "@vueuse/core": "^10.11.0",
55 | "radix-vue": "^1.9.2",
56 | "vite": "^5.3.5",
57 | "vite-plugin-dts": "^3.9.1",
58 | "vue": "^3.4.35"
59 | },
60 | "dependencies": {
61 | "@nuxt/kit": "~3.12.4"
62 | },
63 | "peerDependencies": {
64 | "radix-vue": "1.x",
65 | "vue": "3.x"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/aioli/src/Content.vue:
--------------------------------------------------------------------------------
1 |
2 | {
9 | emit('openAutoFocus', e)
10 | e.preventDefault()
11 | drawerRef?.focus?.()
12 | }
13 | "
14 | @pointer-down-outside="
15 | (e) => {
16 | if (!modal || e.detail.originalEvent.button != 0) {
17 | e.preventDefault()
18 | return
19 | }
20 | if (keyboardIsOpen) {
21 | keyboardIsOpen = false
22 | }
23 | e.preventDefault()
24 |
25 | if (persistent) return
26 |
27 | openProp = false
28 | emit('pointerDownOutside', e)
29 | }
30 | "
31 | @interact-outside="emit('interactOutside', $event)"
32 | @pointerdown.passive="onPress"
33 | @pointermove.passive="onDrag"
34 | @pointerup.passive="onRelease"
35 | >
36 |
37 |
38 |
39 |
40 |
63 |
--------------------------------------------------------------------------------
/aioli/src/helpers.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | interface Style {
3 | [key: string]: string
4 | }
5 |
6 | const cache = new WeakMap()
7 |
8 | export function isInView(el: HTMLElement): boolean {
9 | const rect = el.getBoundingClientRect()
10 |
11 | if (!window.visualViewport) return false
12 |
13 | return (
14 | rect.top >= 0 &&
15 | rect.left >= 0 &&
16 | // Need + 40 for safari detection
17 | rect.bottom <= window.visualViewport.height - 40 &&
18 | rect.right <= window.visualViewport.width
19 | )
20 | }
21 |
22 | export function set(el?: Element | HTMLElement | null, styles?: Style, ignoreCache = false) {
23 | if (!el || !(el instanceof HTMLElement) || !styles) return
24 | const originalStyles: Style = {}
25 |
26 | Object.entries(styles).forEach(([key, value]: [string, string]) => {
27 | if (key.startsWith('--')) {
28 | el.style.setProperty(key, value)
29 | return
30 | }
31 |
32 | originalStyles[key] = (el.style as any)[key]
33 | ;(el.style as any)[key] = value
34 | })
35 |
36 | if (ignoreCache) return
37 |
38 | cache.set(el, originalStyles)
39 | }
40 |
41 | export function reset(el: Element | HTMLElement | null, prop?: string) {
42 | if (!el || !(el instanceof HTMLElement)) return
43 | const originalStyles = cache.get(el)
44 |
45 | if (!originalStyles) {
46 | return
47 | }
48 |
49 | if (prop) {
50 | ;(el.style as any)[prop] = originalStyles[prop]
51 | } else {
52 | Object.entries(originalStyles).forEach(([key, value]) => {
53 | ;(el.style as any)[key] = value
54 | })
55 | }
56 | }
57 |
58 | export function getTranslateY(element: HTMLElement): number | null {
59 | const style = window.getComputedStyle(element)
60 | const transform =
61 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
62 | // @ts-ignore
63 | style.transform || style.webkitTransform || style.mozTransform
64 | let mat = transform.match(/^matrix3d\((.+)\)$/)
65 | if (mat) return parseFloat(mat[1].split(', ')[13])
66 | mat = transform.match(/^matrix\((.+)\)$/)
67 | return mat ? parseFloat(mat[1].split(', ')[5]) : null
68 | }
69 |
70 | export function dampenValue(v: number) {
71 | return Math.sign(v) * 8 * (Math.log(Math.abs(v) + 1) - 2)
72 | }
73 |
74 | const nonTextInputTypes = new Set(['checkbox', 'radio', 'range', 'color', 'file', 'image', 'button', 'submit', 'reset'])
75 | export function isInput(target: Element | EventTarget | null): target is HTMLInputElement | HTMLTextAreaElement {
76 | return (
77 | (target instanceof HTMLInputElement && !nonTextInputTypes.has(target.type)) ||
78 | target instanceof HTMLTextAreaElement ||
79 | (target instanceof HTMLElement && target.isContentEditable)
80 | )
81 | }
82 |
--------------------------------------------------------------------------------
/website/app/app.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
25 |
40 |
41 |
Show More Examples
42 |
43 |
44 |
45 |
46 |
49 | {{ example.buttonText }}
50 |
51 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
72 |
73 |
74 |
75 |
Drawer for Vue.
76 |
77 | This component is a port of the lovely React-Library
78 | Vaul
79 | by
80 | Emil Kowalski .
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/website/app/components/example/ExampleSystemTray.vue:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
52 |
53 |
57 |
58 |
59 |
60 |
61 |
71 |
72 |
73 |
74 | Keep your Secret Phrase safe
75 |
76 |
77 |
78 | Don’t share it with anyone else
79 |
80 |
81 |
82 | If you lose it, we can’t recover it
83 |
84 |
85 |
86 |
87 |
91 | Cancel
92 |
93 |
97 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | Reveal
119 |
120 |
121 |
122 |
123 |
124 |
134 |
135 |
136 |
137 | Keep your Secret Phrase safe
138 |
139 |
140 |
141 | Don’t share it with anyone else
142 |
143 |
144 |
145 | If you lose it, we can’t recover it
146 |
147 |
148 |
149 |
150 |
154 | Cancel
155 |
156 |
160 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 | Reveal
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 | Are you sure?
192 |
193 |
194 | You haven’t backed up your wallet yet. If you remove it, you could lose access forever. We suggest
195 | tapping and backing up your wallet first with a valid recovery method.
196 |
197 |
198 |
199 |
203 | Cancel
204 |
205 |
209 | Continue
210 |
211 |
212 |
213 |
214 |
217 |
218 |
222 |
223 | View Private Key
224 |
225 |
229 |
230 | View Recovery Phase
231 |
232 |
236 |
237 | Remove Wallet
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
257 |
--------------------------------------------------------------------------------
/aioli/src/Root.vue:
--------------------------------------------------------------------------------
1 |
2 | {
7 | openProp = open || persistent
8 | }
9 | "
10 | >
11 |
12 |
13 |
14 |
15 |
408 |
--------------------------------------------------------------------------------
/website/app/auto-animate.ts:
--------------------------------------------------------------------------------
1 | // necessary until https://github.com/formkit/auto-animate/pull/211 gets merged
2 | /* eslint-disable */
3 | // @ts-nocheck
4 |
5 | /**
6 | * Absolute coordinate positions adjusted for scroll.
7 | */
8 | interface Coordinates {
9 | top: number
10 | left: number
11 | width: number
12 | height: number
13 | }
14 |
15 | /**
16 | * Allows start/stop control of the animation
17 | */
18 | export interface AnimationController {
19 | /**
20 | * The original animation parent.
21 | */
22 | readonly parent: Element
23 | /**
24 | * A function that enables future animations.
25 | */
26 | enable: () => void
27 | /**
28 | * A function that disables future animations.
29 | */
30 | disable: () => void
31 | /**
32 | * A function that returns true if the animations are currently enabled.
33 | */
34 | isEnabled: () => boolean
35 | /**
36 | * (Svelte Specific) A function that runs if the parameters are changed.
37 | */
38 | update?: (newParams: P) => void
39 | /**
40 | * (Svelte Specific) A function that runs when the component is removed from the DOM.
41 | */
42 | destroy?: () => void
43 | }
44 |
45 | /**
46 | * A set of all the parents currently being observe. This is the only non weak
47 | * registry.
48 | */
49 | const parents = new Set()
50 | /**
51 | * Element coordinates that is constantly kept up to date.
52 | */
53 | const coords = new WeakMap()
54 | /**
55 | * Siblings of elements that have been removed from the dom.
56 | */
57 | const siblings = new WeakMap()
58 | /**
59 | * Animations that are currently running.
60 | */
61 | const animations = new WeakMap()
62 | /**
63 | * A map of existing intersection observers used to track element movements.
64 | */
65 | const intersections = new WeakMap()
66 | /**
67 | * Intervals for automatically checking the position of elements occasionally.
68 | */
69 | const intervals = new WeakMap()
70 | /**
71 | * The configuration options for each group of elements.
72 | */
73 | const options = new WeakMap()
74 | /**
75 | * Debounce counters by id, used to debounce calls to update positions.
76 | */
77 | const debounces = new WeakMap()
78 | /**
79 | * All parents that are currently enabled are tracked here.
80 | */
81 | const enabled = new WeakSet()
82 | /**
83 | * The document used to calculate transitions.
84 | */
85 | let root: HTMLElement
86 |
87 | /**
88 | * The root’s XY scroll positions.
89 | */
90 | let scrollX = 0
91 | let scrollY = 0
92 | /**
93 | * Used to sign an element as the target.
94 | */
95 | const TGT = '__aa_tgt'
96 | /**
97 | * Used to sign an element as being part of a removal.
98 | */
99 | const DEL = '__aa_del'
100 | /**
101 | * Used to sign an element as being "new". When an element is removed from the
102 | * dom, but may cycle back in we can sign it with new to ensure the next time
103 | * it is recognized we consider it new.
104 | */
105 | const NEW = '__aa_new'
106 |
107 | /**
108 | * Callback for handling all mutations.
109 | * @param mutations - A mutation list
110 | */
111 | const handleMutations: MutationCallback = (mutations) => {
112 | const elements = getElements(mutations)
113 | // If elements is "false" that means this mutation that should be ignored.
114 | if (elements) {
115 | elements.forEach((el) => animate(el))
116 | }
117 | }
118 |
119 | /**
120 | *
121 | * @param entries - Elements that have been resized.
122 | */
123 | const handleResizes: ResizeObserverCallback = (entries) => {
124 | entries.forEach((entry) => {
125 | if (entry.target === root) updateAllPos()
126 | if (coords.has(entry.target)) updatePos(entry.target)
127 | })
128 | }
129 |
130 | /**
131 | * Observe this elements position.
132 | * @param el - The element to observe the position of.
133 | */
134 | function observePosition(el: Element) {
135 | const oldObserver = intersections.get(el)
136 | oldObserver?.disconnect()
137 | let rect = coords.get(el)
138 | let invocations = 0
139 | const buffer = 5
140 | if (!rect) {
141 | rect = getCoords(el)
142 | coords.set(el, rect)
143 | }
144 | const { offsetWidth, offsetHeight } = root
145 | const rootMargins = [
146 | rect.top - buffer,
147 | offsetWidth - (rect.left + buffer + rect.width),
148 | offsetHeight - (rect.top + buffer + rect.height),
149 | rect.left - buffer,
150 | ]
151 | const rootMargin = rootMargins.map((px) => `${-1 * Math.floor(px)}px`).join(' ')
152 | const observer = new IntersectionObserver(
153 | () => {
154 | ++invocations > 1 && updatePos(el)
155 | },
156 | {
157 | root,
158 | threshold: 1,
159 | rootMargin,
160 | },
161 | )
162 | observer.observe(el)
163 | intersections.set(el, observer)
164 | }
165 |
166 | /**
167 | * Update the exact position of a given element.
168 | * @param el - An element to update the position of.
169 | * @param debounce - Whether or not to debounce the update. After an animation is finished, it should update as soon as possible to prevent flickering on quick toggles.
170 | */
171 | function updatePos(el: Element, debounce = true) {
172 | clearTimeout(debounces.get(el))
173 | const optionsOrPlugin = getOptions(el)
174 | const delay = debounce ? (isPlugin(optionsOrPlugin) ? 500 : optionsOrPlugin.duration) : 0
175 | debounces.set(
176 | el,
177 | setTimeout(async () => {
178 | const currentAnimation = animations.get(el)
179 |
180 | try {
181 | await currentAnimation?.finished
182 |
183 | coords.set(el, getCoords(el))
184 | observePosition(el)
185 | } catch {
186 | // ignore errors as the `.finished` promise is rejected when animations were cancelled
187 | }
188 | }, delay),
189 | )
190 | }
191 |
192 | /**
193 | * Updates all positions that are currently being tracked.
194 | */
195 | function updateAllPos() {
196 | clearTimeout(debounces.get(root))
197 | debounces.set(
198 | root,
199 | setTimeout(() => {
200 | parents.forEach((parent) => forEach(parent, (el) => lowPriority(() => updatePos(el))))
201 | }, 100),
202 | )
203 | }
204 |
205 | /**
206 | * Its possible for a quick scroll or other fast events to get past the
207 | * intersection observer, so occasionally we need want "cold-poll" for the
208 | * latests and greatest position. We try to do this in the most non-disruptive
209 | * fashion possible. First we only do this ever couple seconds, staggard by a
210 | * random offset.
211 | * @param el - Element
212 | */
213 | function poll(el: Element) {
214 | setTimeout(
215 | () => {
216 | intervals.set(
217 | el,
218 | setInterval(() => lowPriority(updatePos.bind(null, el)), 2000),
219 | )
220 | },
221 | Math.round(2000 * Math.random()),
222 | )
223 | }
224 |
225 | /**
226 | * Perform some operation that is non critical at some point.
227 | * @param callback
228 | */
229 | function lowPriority(callback: CallableFunction) {
230 | if (typeof requestIdleCallback === 'function') {
231 | requestIdleCallback(() => callback())
232 | } else {
233 | requestAnimationFrame(() => callback())
234 | }
235 | }
236 |
237 | /**
238 | * The mutation observer responsible for watching each root element.
239 | */
240 | let mutations: MutationObserver | undefined
241 |
242 | /**
243 | * A resize observer, responsible for recalculating elements on resize.
244 | */
245 | let resize: ResizeObserver | undefined
246 |
247 | /**
248 | * Ensure the browser is supported.
249 | */
250 | const supportedBrowser = typeof window !== 'undefined' && 'ResizeObserver' in window
251 |
252 | /**
253 | * If this is in a browser, initialize our Web APIs
254 | */
255 | if (supportedBrowser) {
256 | root = document.documentElement
257 | mutations = new MutationObserver(handleMutations)
258 | resize = new ResizeObserver(handleResizes)
259 | window.addEventListener('scroll', () => {
260 | scrollY = window.scrollY
261 | scrollX = window.scrollX
262 | })
263 | resize.observe(root)
264 | }
265 | /**
266 | * Retrieves all the elements that may have been affected by the last mutation
267 | * including ones that have been removed and are no longer in the DOM.
268 | * @param mutations - A mutation list.
269 | * @returns
270 | */
271 | function getElements(mutations: MutationRecord[]): Set | false {
272 | const observedNodes = mutations.reduce((nodes: Node[], mutation) => {
273 | return [...nodes, ...Array.from(mutation.addedNodes), ...Array.from(mutation.removedNodes)]
274 | }, [])
275 | // Short circuit if _only_ comment nodes are observed
276 | const onlyCommentNodesObserved = observedNodes.every((node) => node.nodeName === '#comment')
277 |
278 | if (onlyCommentNodesObserved) return false
279 |
280 | return mutations.reduce((elements: Set | false, mutation) => {
281 | // Short circuit if we find a purposefully deleted node.
282 | if (elements === false) return false
283 |
284 | if (mutation.target instanceof Element) {
285 | target(mutation.target)
286 | if (!elements.has(mutation.target)) {
287 | elements.add(mutation.target)
288 | for (let i = 0; i < mutation.target.children.length; i++) {
289 | const child = mutation.target.children.item(i)
290 | if (!child) continue
291 | if (DEL in child) {
292 | return false
293 | }
294 | target(mutation.target, child)
295 | elements.add(child)
296 | }
297 | }
298 | if (mutation.removedNodes.length) {
299 | for (let i = 0; i < mutation.removedNodes.length; i++) {
300 | const child = mutation.removedNodes[i]
301 | if (DEL in child) {
302 | return false
303 | }
304 | if (child instanceof Element) {
305 | elements.add(child)
306 | target(mutation.target, child)
307 | siblings.set(child, [mutation.previousSibling, mutation.nextSibling])
308 | }
309 | }
310 | }
311 | }
312 | return elements
313 | }, new Set())
314 | }
315 |
316 | /**
317 | * Assign the target to an element.
318 | * @param el - The root element
319 | * @param child
320 | */
321 | function target(el: Element, child?: Element) {
322 | if (!child && !(TGT in el)) Object.defineProperty(el, TGT, { value: el })
323 | else if (child && !(TGT in child)) Object.defineProperty(child, TGT, { value: el })
324 | }
325 |
326 | /**
327 | * Determines what kind of change took place on the given element and then
328 | * performs the proper animation based on that.
329 | * @param el - The specific element to animate.
330 | */
331 | function animate(el: Element) {
332 | const isMounted = el.isConnected
333 | const preExisting = coords.has(el)
334 | if (isMounted && siblings.has(el)) siblings.delete(el)
335 |
336 | if (animations.get(el)?.playState !== 'finished') {
337 | animations.get(el)?.cancel()
338 | }
339 | if (NEW in el) {
340 | add(el)
341 | } else if (preExisting && isMounted) {
342 | remain(el)
343 | } else if (preExisting && !isMounted) {
344 | remove(el)
345 | } else {
346 | add(el)
347 | }
348 | }
349 |
350 | /**
351 | * Removes all non-digits from a string and casts to a number.
352 | * @param str - A string containing a pixel value.
353 | * @returns
354 | */
355 | function raw(str: string): number {
356 | return Number(str.replace(/[^0-9.\-]/g, ''))
357 | }
358 |
359 | /**
360 | * Get the scroll offset of elements
361 | * @param el - Element
362 | * @returns
363 | */
364 | function getScrollOffset(el: Element) {
365 | let p = el.parentElement
366 | while (p) {
367 | if (p.scrollLeft || p.scrollTop) {
368 | return { x: p.scrollLeft, y: p.scrollTop }
369 | }
370 | p = p.parentElement
371 | }
372 | return { x: 0, y: 0 }
373 | }
374 |
375 | /**
376 | * Get the coordinates of elements adjusted for scroll position.
377 | * @param el - Element
378 | * @returns
379 | */
380 | function getCoords(el: Element): Coordinates {
381 | const rect = el.getBoundingClientRect()
382 | const { x, y } = getScrollOffset(el)
383 | return {
384 | top: rect.top + y,
385 | left: rect.left + x,
386 | width: rect.width,
387 | height: rect.height,
388 | }
389 | }
390 |
391 | /**
392 | * Returns the width/height that the element should be transitioned between.
393 | * This takes into account box-sizing.
394 | * @param el - Element being animated
395 | * @param oldCoords - Old set of Coordinates coordinates
396 | * @param newCoords - New set of Coordinates coordinates
397 | * @returns
398 | */
399 | export function getTransitionSizes(el: Element, oldCoords: Coordinates, newCoords: Coordinates) {
400 | let widthFrom = oldCoords.width
401 | let heightFrom = oldCoords.height
402 | let widthTo = newCoords.width
403 | let heightTo = newCoords.height
404 | const styles = getComputedStyle(el)
405 | const sizing = styles.getPropertyValue('box-sizing')
406 |
407 | if (sizing === 'content-box') {
408 | const paddingY =
409 | raw(styles.paddingTop) + raw(styles.paddingBottom) + raw(styles.borderTopWidth) + raw(styles.borderBottomWidth)
410 | const paddingX =
411 | raw(styles.paddingLeft) + raw(styles.paddingRight) + raw(styles.borderRightWidth) + raw(styles.borderLeftWidth)
412 | widthFrom -= paddingX
413 | widthTo -= paddingX
414 | heightFrom -= paddingY
415 | heightTo -= paddingY
416 | }
417 |
418 | return [widthFrom, widthTo, heightFrom, heightTo].map(Math.round)
419 | }
420 |
421 | /**
422 | * Retrieves animation options for the current element.
423 | * @param el - Element to retrieve options for.
424 | * @returns
425 | */
426 | function getOptions(el: Element): AutoAnimateOptions | AutoAnimationPlugin {
427 | return TGT in el && options.has((el as Element & { __aa_tgt: Element })[TGT])
428 | ? options.get((el as Element & { __aa_tgt: Element })[TGT])!
429 | : { duration: 250, easing: 'ease-in-out' }
430 | }
431 |
432 | /**
433 | * Returns the target of a given animation (generally the parent).
434 | * @param el - An element to check for a target
435 | * @returns
436 | */
437 | function getTarget(el: Element): Element | undefined {
438 | if (TGT in el) return (el as Element & { __aa_tgt: Element })[TGT]
439 | return undefined
440 | }
441 |
442 | /**
443 | * Checks if animations are enabled or disabled for a given element.
444 | * @param el - Any element
445 | * @returns
446 | */
447 | function isEnabled(el: Element): boolean {
448 | const target = getTarget(el)
449 | return target ? enabled.has(target) : false
450 | }
451 |
452 | /**
453 | * Iterate over the children of a given parent.
454 | * @param parent - A parent element
455 | * @param callback - A callback
456 | */
457 | function forEach(parent: Element, ...callbacks: Array<(el: Element, isRoot?: boolean) => void>) {
458 | callbacks.forEach((callback) => callback(parent, options.has(parent)))
459 | for (let i = 0; i < parent.children.length; i++) {
460 | const child = parent.children.item(i)
461 | if (child) {
462 | callbacks.forEach((callback) => callback(child, options.has(child)))
463 | }
464 | }
465 | }
466 |
467 | /**
468 | * Always return tuple to provide consistent interface
469 | */
470 | function getPluginTuple(
471 | pluginReturn: ReturnType,
472 | ): [KeyframeEffect, AutoAnimationPluginOptions] | [KeyframeEffect] {
473 | if (Array.isArray(pluginReturn)) return pluginReturn
474 |
475 | return [pluginReturn]
476 | }
477 |
478 | /**
479 | * Determine if config is plugin
480 | */
481 | function isPlugin(config: Partial | AutoAnimationPlugin): config is AutoAnimationPlugin {
482 | return typeof config === 'function'
483 | }
484 |
485 | /**
486 | * The element in question is remaining in the DOM.
487 | * @param el - Element to flip
488 | * @returns
489 | */
490 | function remain(el: Element) {
491 | const oldCoords = coords.get(el)
492 | const newCoords = getCoords(el)
493 | if (!isEnabled(el)) return coords.set(el, newCoords)
494 | let animation: Animation
495 | if (!oldCoords) return
496 | const pluginOrOptions = getOptions(el)
497 | if (typeof pluginOrOptions !== 'function') {
498 | let deltaLeft = oldCoords.left - newCoords.left
499 | let deltaTop = oldCoords.top - newCoords.top
500 | const deltaRight = oldCoords.left + oldCoords.width - (newCoords.left + newCoords.width)
501 | const deltaBottom = oldCoords.top + oldCoords.height - (newCoords.top + newCoords.height)
502 |
503 | // element is probably anchored and doesn't need to be offset
504 | if (deltaBottom == 0) deltaTop = 0
505 | if (deltaRight == 0) deltaLeft = 0
506 |
507 | const [widthFrom, widthTo, heightFrom, heightTo] = getTransitionSizes(el, oldCoords, newCoords)
508 | const start: Record = {
509 | transform: `translate(${deltaLeft}px, ${deltaTop}px)`,
510 | }
511 | const end: Record = {
512 | transform: `translate(0, 0)`,
513 | }
514 | if (widthFrom !== widthTo) {
515 | start.width = `${widthFrom}px`
516 | end.width = `${widthTo}px`
517 | }
518 | if (heightFrom !== heightTo) {
519 | start.height = `${heightFrom}px`
520 | end.height = `${heightTo}px`
521 | }
522 | animation = el.animate([start, end], {
523 | duration: pluginOrOptions.duration,
524 | easing: pluginOrOptions.easing,
525 | })
526 | } else {
527 | const [keyframes] = getPluginTuple(pluginOrOptions(el, 'remain', oldCoords, newCoords))
528 | animation = new Animation(keyframes)
529 | animation.play()
530 | }
531 | animations.set(el, animation)
532 | coords.set(el, newCoords)
533 | animation.addEventListener('finish', updatePos.bind(null, el, false))
534 | }
535 |
536 | /**
537 | * Adds the element with a transition.
538 | * @param el - Animates the element being added.
539 | */
540 | function add(el: Element) {
541 | if (NEW in el) delete el[NEW]
542 | const newCoords = getCoords(el)
543 | coords.set(el, newCoords)
544 | const pluginOrOptions = getOptions(el)
545 | if (!isEnabled(el)) return
546 | let animation: Animation
547 | if (typeof pluginOrOptions !== 'function') {
548 | animation = el.animate(
549 | [
550 | { transform: 'scale(.98)', opacity: 0 },
551 | { transform: 'scale(0.98)', opacity: 0, offset: 0.5 },
552 | { transform: 'scale(1)', opacity: 1 },
553 | ],
554 | {
555 | duration: pluginOrOptions.duration * 1.5,
556 | easing: 'ease-in',
557 | },
558 | )
559 | } else {
560 | const [keyframes] = getPluginTuple(pluginOrOptions(el, 'add', newCoords))
561 | animation = new Animation(keyframes)
562 | animation.play()
563 | }
564 | animations.set(el, animation)
565 | animation.addEventListener('finish', updatePos.bind(null, el, false))
566 | }
567 |
568 | /**
569 | * Clean up after removing an element from the dom.
570 | * @param el - Element being removed
571 | * @param styles - Optional styles that should be removed from the element.
572 | */
573 | function cleanUp(el: Element, styles?: Partial) {
574 | el.remove()
575 | coords.delete(el)
576 | siblings.delete(el)
577 | animations.delete(el)
578 | intersections.get(el)?.disconnect()
579 | setTimeout(() => {
580 | if (DEL in el) delete el[DEL]
581 | Object.defineProperty(el, NEW, { value: true, configurable: true })
582 | if (styles && el instanceof HTMLElement) {
583 | for (const style in styles) {
584 | el.style[style as any] = ''
585 | }
586 | }
587 | }, 0)
588 | }
589 |
590 | /**
591 | * Animates the removal of an element.
592 | * @param el - Element to remove
593 | */
594 | function remove(el: Element) {
595 | if (!siblings.has(el) || !coords.has(el)) return
596 |
597 | const [prev, next] = siblings.get(el)!
598 | Object.defineProperty(el, DEL, { value: true, configurable: true })
599 | const finalX = window.scrollX
600 | const finalY = window.scrollY
601 |
602 | if (next && next.parentNode && next.parentNode instanceof Element) {
603 | next.parentNode.insertBefore(el, next)
604 | } else if (prev && prev.parentNode) {
605 | prev.parentNode.appendChild(el)
606 | } else {
607 | getTarget(el)?.appendChild(el)
608 | }
609 | if (!isEnabled(el)) return cleanUp(el)
610 |
611 | const [top, left, width, height] = deletePosition(el)
612 | const optionsOrPlugin = getOptions(el)
613 | const oldCoords = coords.get(el)!
614 | if (finalX !== scrollX || finalY !== scrollY) {
615 | adjustScroll(el, finalX, finalY, optionsOrPlugin)
616 | }
617 |
618 | let animation: Animation
619 | let styleReset: Partial = {
620 | position: 'absolute',
621 | top: `${top}px`,
622 | left: `${left}px`,
623 | width: `${width}px`,
624 | height: `${height}px`,
625 | margin: '0',
626 | pointerEvents: 'none',
627 | transformOrigin: 'center',
628 | zIndex: '100',
629 | }
630 |
631 | if (!isPlugin(optionsOrPlugin)) {
632 | Object.assign((el as HTMLElement).style, styleReset)
633 | animation = el.animate(
634 | [
635 | {
636 | transform: 'scale(1)',
637 | opacity: 1,
638 | },
639 | {
640 | transform: 'scale(.98)',
641 | opacity: 0,
642 | },
643 | ],
644 | { duration: optionsOrPlugin.duration, easing: 'ease-out' },
645 | )
646 | } else {
647 | const [keyframes, options] = getPluginTuple(optionsOrPlugin(el, 'remove', oldCoords))
648 | if (options?.styleReset !== false) {
649 | styleReset = options?.styleReset || styleReset
650 | Object.assign((el as HTMLElement).style, styleReset)
651 | }
652 | animation = new Animation(keyframes)
653 | animation.play()
654 | }
655 | animations.set(el, animation)
656 | animation.addEventListener('finish', cleanUp.bind(null, el, styleReset))
657 | }
658 |
659 | /**
660 | * If the element being removed is at the very bottom of the page, and the
661 | * the page was scrolled into a space being "made available" by the element
662 | * that was removed, the page scroll will have jumped up some amount. We need
663 | * to offset the jump by the amount that the page was "automatically" scrolled
664 | * up. We can do this by comparing the scroll position before and after the
665 | * element was removed, and then offsetting by that amount.
666 | *
667 | * @param el - The element being deleted
668 | * @param finalX - The final X scroll position
669 | * @param finalY - The final Y scroll position
670 | * @param optionsOrPlugin - The options or plugin
671 | * @returns
672 | */
673 | function adjustScroll(
674 | el: Element,
675 | finalX: number,
676 | finalY: number,
677 | optionsOrPlugin: AutoAnimateOptions | AutoAnimationPlugin,
678 | ) {
679 | const scrollDeltaX = scrollX - finalX
680 | const scrollDeltaY = scrollY - finalY
681 | const scrollBefore = document.documentElement.style.scrollBehavior
682 | const scrollBehavior = getComputedStyle(root).scrollBehavior
683 | if (scrollBehavior === 'smooth') {
684 | document.documentElement.style.scrollBehavior = 'auto'
685 | }
686 | window.scrollTo(window.scrollX + scrollDeltaX, window.scrollY + scrollDeltaY)
687 | if (!el.parentElement) return
688 | const parent = el.parentElement
689 | let lastHeight = parent.clientHeight
690 | let lastWidth = parent.clientWidth
691 | const startScroll = performance.now()
692 | // Here we use a manual scroll animation to keep the element using the same
693 | // easing and timing as the parent’s scroll animation.
694 | function smoothScroll() {
695 | requestAnimationFrame(() => {
696 | if (!isPlugin(optionsOrPlugin)) {
697 | const deltaY = lastHeight - parent.clientHeight
698 | const deltaX = lastWidth - parent.clientWidth
699 | if (startScroll + optionsOrPlugin.duration > performance.now()) {
700 | window.scrollTo({
701 | left: window.scrollX - deltaX!,
702 | top: window.scrollY - deltaY!,
703 | })
704 | lastHeight = parent.clientHeight
705 | lastWidth = parent.clientWidth
706 | smoothScroll()
707 | } else {
708 | document.documentElement.style.scrollBehavior = scrollBefore
709 | }
710 | }
711 | })
712 | }
713 | smoothScroll()
714 | }
715 |
716 | /**
717 | * Determines the position of the element being removed.
718 | * @param el - The element being deleted
719 | * @returns
720 | */
721 | function deletePosition(el: Element): [top: number, left: number, width: number, height: number] {
722 | const oldCoords = coords.get(el)!
723 | const [width, , height] = getTransitionSizes(el, oldCoords, getCoords(el))
724 |
725 | let offsetParent: Element | null = el.parentElement
726 | while (
727 | offsetParent &&
728 | (getComputedStyle(offsetParent).position === 'static' || offsetParent instanceof HTMLBodyElement)
729 | ) {
730 | offsetParent = offsetParent.parentElement
731 | }
732 | if (!offsetParent) offsetParent = document.body
733 | const parentStyles = getComputedStyle(offsetParent)
734 | const parentCoords =
735 | !animations.has(el) || animations.get(el)?.playState === 'finished'
736 | ? getCoords(offsetParent)
737 | : coords.get(offsetParent)!
738 |
739 | const top = Math.round(oldCoords.top - parentCoords.top) - raw(parentStyles.borderTopWidth)
740 | const left = Math.round(oldCoords.left - parentCoords.left) - raw(parentStyles.borderLeftWidth)
741 | return [top, left, width, height]
742 | }
743 |
744 | export interface AutoAnimateOptions {
745 | /**
746 | * The time it takes to run a single sequence of animations in milliseconds.
747 | */
748 | duration: number
749 | /**
750 | * The type of easing to use.
751 | * Default: ease-in-out
752 | */
753 | easing: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | ({} & string)
754 | /**
755 | * Ignore a user’s "reduce motion" setting and enable animations. It is not
756 | * recommended to use this.
757 | */
758 | disrespectUserMotionPreference?: boolean
759 | }
760 |
761 | /**
762 | * A custom plugin config object
763 | */
764 | export interface AutoAnimationPluginOptions {
765 | // provide your own css styles or disable style reset
766 | styleReset: CSSStyleDeclaration | false
767 | }
768 |
769 | /**
770 | * A custom plugin that determines what the effects to run
771 | */
772 | export interface AutoAnimationPlugin {
773 | (
774 | el: Element,
775 | action: T,
776 | newCoordinates?: T extends 'add' | 'remain' | 'remove' ? Coordinates : undefined,
777 | oldCoordinates?: T extends 'remain' ? Coordinates : undefined,
778 | ): KeyframeEffect | [KeyframeEffect, AutoAnimationPluginOptions]
779 | }
780 |
781 | /**
782 | * A function that automatically adds animation effects to itself and its
783 | * immediate children. Specifically it adds effects for adding, moving, and
784 | * removing DOM elements.
785 | * @param el - A parent element to add animations to.
786 | * @param options - An optional object of options.
787 | */
788 | export default function autoAnimate(
789 | el: HTMLElement,
790 | config: Partial | AutoAnimationPlugin = {},
791 | ): AnimationController {
792 | if (mutations && resize) {
793 | const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
794 | const isDisabledDueToReduceMotion =
795 | mediaQuery.matches && !isPlugin(config) && !config.disrespectUserMotionPreference
796 | if (!isDisabledDueToReduceMotion) {
797 | enabled.add(el)
798 | if (getComputedStyle(el).position === 'static') {
799 | Object.assign(el.style, { position: 'relative' })
800 | }
801 | forEach(el, updatePos, poll, (element) => resize?.observe(element))
802 | if (isPlugin(config)) {
803 | options.set(el, config)
804 | } else {
805 | options.set(el, { duration: 250, easing: 'ease-in-out', ...config })
806 | }
807 | mutations.observe(el, { childList: true })
808 | parents.add(el)
809 | }
810 | }
811 | return Object.freeze({
812 | parent: el,
813 | enable: () => {
814 | enabled.add(el)
815 | },
816 | disable: () => {
817 | enabled.delete(el)
818 | },
819 | isEnabled: () => enabled.has(el),
820 | })
821 | }
822 |
823 | /**
824 | * The vue directive.
825 | */
826 | export const vAutoAnimate = {
827 | mounted: (
828 | el: HTMLElement,
829 | binding: {
830 | value: Partial | AutoAnimationPlugin | undefined
831 | },
832 | ) => {
833 | autoAnimate(el, binding.value || {})
834 | },
835 | // ignore ssr see #96:
836 | getSSRProps: () => ({}),
837 | }
838 |
--------------------------------------------------------------------------------