vanilla-jui 1.4.8 → 1.5.2

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 (58) hide show
  1. package/README.md +10 -4
  2. package/dist/index.d.ts +402 -977
  3. package/dist/index.js +1 -1
  4. package/dist/index.umd.js +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +3 -3
  7. package/src/components/accordion.ts +467 -349
  8. package/src/components/flow.ts +779 -1133
  9. package/src/components/form.ts +666 -509
  10. package/src/components/menu.ts +199 -326
  11. package/src/components/modal.ts +510 -747
  12. package/src/components/offcanvas.ts +281 -361
  13. package/src/components/pagination.ts +207 -374
  14. package/src/components/swiper.ts +688 -1168
  15. package/src/components/tabs.ts +468 -788
  16. package/src/components/toc.ts +191 -244
  17. package/src/core/component.ts +332 -0
  18. package/src/core/motion.ts +298 -0
  19. package/src/core/presence.ts +233 -0
  20. package/src/core/scheduler.ts +91 -0
  21. package/src/core/view.ts +31 -0
  22. package/src/css/accordion.css +94 -3
  23. package/src/css/color.css +4 -5
  24. package/src/css/color_p3.css +6 -5
  25. package/src/css/content.css +18 -24
  26. package/src/css/drop.css +4 -1
  27. package/src/css/form.css +8 -0
  28. package/src/css/gradient.css +64 -131
  29. package/src/css/layout.css +9 -23
  30. package/src/css/menu.css +10 -5
  31. package/src/css/modal.css +2 -1
  32. package/src/css/offcanvas.css +1 -17
  33. package/src/css/swiper.css +2 -1
  34. package/src/css/tabs.css +8 -17
  35. package/src/css/themes/default/accordion.css +0 -48
  36. package/src/css/themes/default/swiper.css +1 -1
  37. package/src/css/themes/default/tabs.css +4 -6
  38. package/src/css/toast.css +1 -17
  39. package/src/css/tokens.css +18 -35
  40. package/src/css/tooltip.css +2 -1
  41. package/src/index.ts +8 -3
  42. package/src/locales/index.ts +3 -0
  43. package/src/primitives/drop.ts +253 -262
  44. package/src/primitives/loading.ts +22 -4
  45. package/src/primitives/parabola.ts +123 -164
  46. package/src/primitives/sticky.ts +297 -0
  47. package/src/primitives/theme.ts +164 -222
  48. package/src/primitives/toast.ts +309 -284
  49. package/src/primitives/tooltip.ts +65 -66
  50. package/src/utilities/browser.ts +38 -0
  51. package/src/utilities/dom.ts +0 -54
  52. package/src/utilities/refs.ts +61 -0
  53. package/src/utilities/state.ts +73 -0
  54. package/src/utilities/timer.ts +11 -31
  55. package/src/validation/validator.ts +289 -389
  56. package/src/components/sticky.ts +0 -297
  57. package/src/core/Component.ts +0 -377
  58. package/src/utilities/scheduler.ts +0 -156
@@ -0,0 +1,332 @@
1
+ import { flushSync } from 'vanilla-signal';
2
+
3
+ import { isPlainObject } from '../utilities/object.ts';
4
+ import { createOwnedView } from './view.ts';
5
+
6
+ export type ComponentProps = Record<string, unknown>;
7
+ export type ComponentState = Record<string, unknown>;
8
+ export type ComponentCleanup =
9
+ | void
10
+ | (() => void)
11
+ | {
12
+ destroy: () => void;
13
+ };
14
+ export type ComponentPluginOptions = Record<string, unknown> | undefined;
15
+
16
+ export interface ComponentRuntime {
17
+ built: boolean;
18
+ mounted: boolean;
19
+ destroyed: boolean;
20
+ }
21
+
22
+ export type ComponentLifecycleEvent = 'build' | 'mount' | 'unmount' | 'destroy';
23
+ export type ComponentListener = (...args: unknown[]) => void;
24
+
25
+ export interface ComponentController<
26
+ TProps extends ComponentProps = ComponentProps,
27
+ TState extends ComponentState = ComponentState,
28
+ TElement extends Element = HTMLElement,
29
+ > {
30
+ readonly props: TProps;
31
+ readonly state: TState;
32
+ readonly runtime: ComponentRuntime;
33
+ readonly element: TElement | null;
34
+ build(): this;
35
+ mount(container: Element | DocumentFragment): this;
36
+ unmount(): this;
37
+ setState(patch?: Partial<TState> | null): this;
38
+ setState<TKey extends keyof TState>(key: TKey, value: TState[TKey]): this;
39
+ own(cleanup: ComponentCleanup): this;
40
+ use(
41
+ plugin: ComponentPlugin<this> | null | undefined,
42
+ options?: ComponentPluginOptions
43
+ ): this;
44
+ on(event: string, listener: ComponentListener): this;
45
+ off(event: string, listener: ComponentListener): this;
46
+ emit(event: string, ...args: unknown[]): this;
47
+ destroy(): void;
48
+ }
49
+
50
+ export type ComponentPlugin<TComponent = ComponentController> =
51
+ | ((
52
+ component: TComponent,
53
+ options?: ComponentPluginOptions
54
+ ) => ComponentCleanup)
55
+ | {
56
+ install: (
57
+ component: TComponent,
58
+ options?: ComponentPluginOptions
59
+ ) => ComponentCleanup;
60
+ };
61
+
62
+ export interface ComponentContext<
63
+ TProps extends ComponentProps,
64
+ TState extends ComponentState,
65
+ TElement extends Element,
66
+ > {
67
+ readonly props: TProps;
68
+ readonly state: TState;
69
+ readonly runtime: ComponentRuntime;
70
+ readonly element: TElement | null;
71
+ own: (cleanup: ComponentCleanup) => void;
72
+ assertActive: (operation: string) => void;
73
+ emit: (event: string, ...args: unknown[]) => void;
74
+ }
75
+
76
+ export interface ComponentDefinition<
77
+ TProps extends ComponentProps,
78
+ TState extends ComponentState,
79
+ TElement extends Element,
80
+ TActions extends object,
81
+ > {
82
+ name: string;
83
+ ownsElement?: boolean;
84
+ props: TProps;
85
+ state: TState;
86
+ view: (context: ComponentContext<TProps, TState, TElement>) => TElement;
87
+ actions?: TActions;
88
+ normalizeStatePatch?: (patch: Partial<TState>) => Partial<TState>;
89
+ validateStatePatch?: (patch: Partial<TState>, state: TState) => void;
90
+ onBuild?: (context: ComponentContext<TProps, TState, TElement>) => void;
91
+ onMount?: (context: ComponentContext<TProps, TState, TElement>) => void;
92
+ onUnmount?: (context: ComponentContext<TProps, TState, TElement>) => void;
93
+ onDestroy?: (context: ComponentContext<TProps, TState, TElement>) => void;
94
+ }
95
+
96
+ export type FunctionalComponent<
97
+ TProps extends ComponentProps,
98
+ TState extends ComponentState,
99
+ TElement extends Element,
100
+ TActions extends object = object,
101
+ > = ComponentController<TProps, TState, TElement> & TActions;
102
+
103
+ const globalPlugins = new Map<string, ComponentPlugin>();
104
+
105
+ export function useComponentPlugin(
106
+ name: string,
107
+ plugin: ComponentPlugin | null | undefined
108
+ ): void {
109
+ if (!name || !plugin) return;
110
+ globalPlugins.set(name, plugin);
111
+ }
112
+
113
+ export function removeComponentPlugin(name: string): void {
114
+ globalPlugins.delete(name);
115
+ }
116
+
117
+ function runCleanup(cleanup: ComponentCleanup): void {
118
+ if (typeof cleanup === 'function') cleanup();
119
+ else if (cleanup && typeof cleanup.destroy === 'function') cleanup.destroy();
120
+ }
121
+
122
+ export function defineComponent<
123
+ TProps extends ComponentProps,
124
+ TState extends ComponentState,
125
+ TElement extends Element,
126
+ TActions extends object = object,
127
+ >(
128
+ definition: ComponentDefinition<TProps, TState, TElement, TActions>
129
+ ): FunctionalComponent<TProps, TState, TElement, TActions> {
130
+ const runtime: ComponentRuntime = {
131
+ built: false,
132
+ mounted: false,
133
+ destroyed: false,
134
+ };
135
+ const listeners = new Map<string, Set<ComponentListener>>();
136
+ const resources = new Set<ComponentCleanup>();
137
+ const plugins = new Map<ComponentPlugin<unknown>, ComponentCleanup>();
138
+ let element: TElement | null = null;
139
+ let disposeView: (() => void) | null = null;
140
+ let component: FunctionalComponent<TProps, TState, TElement, TActions>;
141
+
142
+ const assertActive = (operation: string): void => {
143
+ if (runtime.destroyed) {
144
+ throw new Error(`${definition.name}.${operation}: instance destroyed`);
145
+ }
146
+ };
147
+
148
+ const emit = (event: string, ...args: unknown[]): void => {
149
+ for (const listener of listeners.get(event) || []) {
150
+ try {
151
+ listener(...args);
152
+ } catch {
153
+ // Listener failures must not interrupt the component lifecycle.
154
+ }
155
+ }
156
+ };
157
+
158
+ const context: ComponentContext<TProps, TState, TElement> = {
159
+ props: definition.props,
160
+ state: definition.state,
161
+ runtime,
162
+ get element() {
163
+ return element;
164
+ },
165
+ own(cleanup) {
166
+ if (cleanup) resources.add(cleanup);
167
+ },
168
+ assertActive,
169
+ emit,
170
+ };
171
+
172
+ const controller: ComponentController<TProps, TState, TElement> = {
173
+ props: definition.props,
174
+ state: definition.state,
175
+ runtime,
176
+ get element() {
177
+ return element;
178
+ },
179
+ build() {
180
+ assertActive('build');
181
+ if (runtime.built) return component;
182
+
183
+ const ownedView = createOwnedView(() => definition.view(context), {
184
+ removeOnDispose: definition.ownsElement,
185
+ });
186
+ element = ownedView.element;
187
+ disposeView = ownedView.dispose;
188
+ runtime.built = true;
189
+ definition.onBuild?.(context);
190
+ emit('build', component);
191
+ return component;
192
+ },
193
+ mount(container) {
194
+ assertActive('mount');
195
+ if (!runtime.built) component.build();
196
+ if (!element) {
197
+ throw new Error(
198
+ `${definition.name}.mount: view did not create an element.`
199
+ );
200
+ }
201
+ if (element.parentNode !== container) container.appendChild(element);
202
+ runtime.mounted = true;
203
+ definition.onMount?.(context);
204
+ emit('mount', component, container);
205
+ return component;
206
+ },
207
+ unmount() {
208
+ assertActive('unmount');
209
+ if (!runtime.mounted && !element?.parentNode) return component;
210
+ definition.onUnmount?.(context);
211
+ if (definition.ownsElement !== false) element?.remove();
212
+ runtime.mounted = false;
213
+ emit('unmount', component);
214
+ return component;
215
+ },
216
+ setState(
217
+ keyOrPatch: Partial<TState> | keyof TState | null | undefined = {},
218
+ value?: unknown
219
+ ) {
220
+ assertActive('setState');
221
+ const patch =
222
+ typeof keyOrPatch === 'string' && arguments.length > 1
223
+ ? { [keyOrPatch]: value }
224
+ : keyOrPatch;
225
+ if (!isPlainObject(patch)) {
226
+ throw new Error(
227
+ `${definition.name}.setState: expects a plain object patch.`
228
+ );
229
+ }
230
+
231
+ const normalized = definition.normalizeStatePatch
232
+ ? definition.normalizeStatePatch(patch as Partial<TState>)
233
+ : (patch as Partial<TState>);
234
+ if (!isPlainObject(normalized)) {
235
+ throw new Error(
236
+ `${definition.name}.setState: expects a plain object patch.`
237
+ );
238
+ }
239
+
240
+ if (definition.validateStatePatch) {
241
+ definition.validateStatePatch(normalized, definition.state);
242
+ } else {
243
+ for (const key of Object.keys(normalized)) {
244
+ if (!Object.hasOwn(definition.state, key)) {
245
+ throw new Error(
246
+ `${definition.name}.setState: "${key}" is not a supported state key.`
247
+ );
248
+ }
249
+ }
250
+ }
251
+
252
+ flushSync(() => {
253
+ Object.assign(definition.state, normalized);
254
+ });
255
+ return component;
256
+ },
257
+ own(cleanup) {
258
+ if (cleanup) resources.add(cleanup);
259
+ return component;
260
+ },
261
+ use(plugin, options) {
262
+ if (!plugin) return component;
263
+ const cleanup =
264
+ typeof plugin === 'function'
265
+ ? plugin(component, options)
266
+ : plugin.install(component, options);
267
+ plugins.set(plugin as ComponentPlugin<unknown>, cleanup);
268
+ return component;
269
+ },
270
+ on(event, listener) {
271
+ if (!event || typeof listener !== 'function') return component;
272
+ let eventListeners = listeners.get(event);
273
+ if (!eventListeners) {
274
+ eventListeners = new Set();
275
+ listeners.set(event, eventListeners);
276
+ }
277
+ eventListeners.add(listener);
278
+ return component;
279
+ },
280
+ off(event, listener) {
281
+ listeners.get(event)?.delete(listener);
282
+ return component;
283
+ },
284
+ emit(event, ...args) {
285
+ emit(event, ...args);
286
+ return component;
287
+ },
288
+ destroy() {
289
+ if (runtime.destroyed) return;
290
+ runtime.destroyed = true;
291
+ definition.onDestroy?.(context);
292
+ emit('destroy', component);
293
+
294
+ for (const cleanup of resources) {
295
+ try {
296
+ runCleanup(cleanup);
297
+ } catch {
298
+ // Cleanup is best-effort; remaining resources must still be released.
299
+ }
300
+ }
301
+ resources.clear();
302
+
303
+ for (const cleanup of plugins.values()) {
304
+ try {
305
+ runCleanup(cleanup);
306
+ } catch {
307
+ // Plugin cleanup must not prevent view disposal.
308
+ }
309
+ }
310
+ plugins.clear();
311
+
312
+ disposeView?.();
313
+ disposeView = null;
314
+ if (definition.ownsElement !== false) element?.remove();
315
+ element = null;
316
+ runtime.built = false;
317
+ runtime.mounted = false;
318
+ listeners.clear();
319
+ },
320
+ };
321
+
322
+ component = Object.assign(
323
+ controller,
324
+ definition.actions || {}
325
+ ) as FunctionalComponent<TProps, TState, TElement, TActions>;
326
+
327
+ for (const plugin of globalPlugins.values()) {
328
+ component.use(plugin as ComponentPlugin<typeof component>);
329
+ }
330
+
331
+ return component;
332
+ }
@@ -0,0 +1,298 @@
1
+ export type TransitionTarget = () => Element | null | undefined;
2
+
3
+ export interface TransitionDefinition {
4
+ keyframes: Keyframe[] | PropertyIndexedKeyframes;
5
+ options?: KeyframeAnimationOptions;
6
+ respectReducedMotion?: boolean;
7
+ }
8
+
9
+ export interface MotionController {
10
+ enter: (signal?: AbortSignal) => Promise<void>;
11
+ leave: (signal?: AbortSignal) => Promise<void>;
12
+ cancel: () => void;
13
+ }
14
+
15
+ export interface CollapseTransitionDefinition {
16
+ axis?: 'vertical' | 'horizontal';
17
+ options?: KeyframeAnimationOptions;
18
+ fade?: boolean;
19
+ respectReducedMotion?: boolean;
20
+ }
21
+
22
+ export interface CollapseMotionController extends MotionController {
23
+ setExpanded: (expanded: boolean) => void;
24
+ }
25
+
26
+ export function createMotionGroup(
27
+ ...motions: readonly MotionController[]
28
+ ): MotionController {
29
+ return {
30
+ async enter(signal) {
31
+ await Promise.all(motions.map((motion) => motion.enter(signal)));
32
+ },
33
+ async leave(signal) {
34
+ await Promise.all(motions.map((motion) => motion.leave(signal)));
35
+ },
36
+ cancel() {
37
+ for (const motion of motions) motion.cancel();
38
+ },
39
+ };
40
+ }
41
+
42
+ function waitForAnimation(
43
+ animation: Animation,
44
+ signal?: AbortSignal
45
+ ): Promise<void> {
46
+ const finished = animation.finished.then(
47
+ () => undefined,
48
+ () => undefined
49
+ );
50
+ if (!signal) return finished;
51
+ return new Promise((resolve) => {
52
+ const complete = (): void => {
53
+ signal.removeEventListener('abort', complete);
54
+ resolve();
55
+ };
56
+ if (signal.aborted) {
57
+ complete();
58
+ return;
59
+ }
60
+ signal.addEventListener('abort', complete, { once: true });
61
+ void finished.then(complete);
62
+ });
63
+ }
64
+
65
+ function resolvedOptions(
66
+ element: Element,
67
+ definition: TransitionDefinition
68
+ ): KeyframeAnimationOptions {
69
+ const options: KeyframeAnimationOptions = {
70
+ fill: 'both',
71
+ ...definition.options,
72
+ };
73
+ const view = element.ownerDocument.defaultView;
74
+ if (
75
+ definition.respectReducedMotion !== false &&
76
+ view?.matchMedia?.('(prefers-reduced-motion: reduce)').matches
77
+ ) {
78
+ options.duration = 0;
79
+ options.delay = 0;
80
+ options.endDelay = 0;
81
+ }
82
+ return options;
83
+ }
84
+
85
+ export function createTransition(
86
+ target: TransitionTarget,
87
+ definition: TransitionDefinition
88
+ ): MotionController {
89
+ let animation: Animation | null = null;
90
+
91
+ const resolveElement = (): Element | null => {
92
+ const nextElement = target() || null;
93
+ if (!nextElement || typeof nextElement.animate !== 'function') return null;
94
+ return nextElement;
95
+ };
96
+
97
+ const keyframesFor = (
98
+ direction: 1 | -1
99
+ ): Keyframe[] | PropertyIndexedKeyframes => {
100
+ if (direction === 1 || !Array.isArray(definition.keyframes)) {
101
+ return definition.keyframes;
102
+ }
103
+ return definition.keyframes.slice().reverse();
104
+ };
105
+
106
+ const play = async (
107
+ direction: 1 | -1,
108
+ signal?: AbortSignal
109
+ ): Promise<void> => {
110
+ if (signal?.aborted) return;
111
+ const currentElement = resolveElement();
112
+ if (!currentElement) return;
113
+ animation?.cancel();
114
+ animation = currentElement.animate(
115
+ keyframesFor(direction),
116
+ resolvedOptions(currentElement, definition)
117
+ );
118
+ const currentAnimation = animation;
119
+ await waitForAnimation(currentAnimation, signal);
120
+ if (signal?.aborted || animation !== currentAnimation) return;
121
+ };
122
+
123
+ return {
124
+ enter: (signal) => play(1, signal),
125
+ leave: (signal) => play(-1, signal),
126
+ cancel() {
127
+ animation?.cancel();
128
+ animation = null;
129
+ },
130
+ };
131
+ }
132
+
133
+ interface CollapseInlineStyles {
134
+ size: string;
135
+ opacity: string;
136
+ overflow: string;
137
+ visibility: string;
138
+ }
139
+
140
+ export function createCollapseTransition(
141
+ target: () => HTMLElement | null | undefined,
142
+ definition: CollapseTransitionDefinition = {}
143
+ ): CollapseMotionController {
144
+ const axis = definition.axis || 'vertical';
145
+ let element: HTMLElement | null = null;
146
+ let animation: Animation | null = null;
147
+ let original: CollapseInlineStyles | null = null;
148
+ let visibleOpacity = '1';
149
+ let operation = 0;
150
+
151
+ const readSize = (current: HTMLElement): string =>
152
+ axis === 'vertical' ? current.style.height : current.style.width;
153
+
154
+ const writeSize = (current: HTMLElement, value: string): void => {
155
+ if (axis === 'vertical') current.style.height = value;
156
+ else current.style.width = value;
157
+ };
158
+
159
+ const restore = (): void => {
160
+ if (!element || !original) return;
161
+ writeSize(element, original.size);
162
+ element.style.opacity = original.opacity;
163
+ element.style.overflow = original.overflow;
164
+ element.style.visibility = original.visibility;
165
+ };
166
+
167
+ const resolveElement = (): HTMLElement | null => {
168
+ const next = target() || null;
169
+ if (next === element) return element;
170
+
171
+ animation?.cancel();
172
+ restore();
173
+ animation = null;
174
+ element = next;
175
+ original = next
176
+ ? {
177
+ size: readSize(next),
178
+ opacity: next.style.opacity,
179
+ overflow: next.style.overflow,
180
+ visibility: next.style.visibility,
181
+ }
182
+ : null;
183
+ visibleOpacity =
184
+ next?.ownerDocument.defaultView?.getComputedStyle(next).opacity || '1';
185
+ return element;
186
+ };
187
+
188
+ const applyBoundary = (expanded: boolean): void => {
189
+ const current = resolveElement();
190
+ if (!current || !original) return;
191
+ if (expanded) {
192
+ restore();
193
+ return;
194
+ }
195
+ writeSize(current, '0px');
196
+ current.style.overflow = 'hidden';
197
+ current.style.visibility = 'hidden';
198
+ if (definition.fade !== false) current.style.opacity = '0';
199
+ };
200
+
201
+ const measureExpandedSize = (current: HTMLElement): number => {
202
+ if (!original) return 0;
203
+ const size = readSize(current);
204
+ const visibility = current.style.visibility;
205
+ writeSize(current, original.size);
206
+ current.style.visibility = original.visibility;
207
+ const bounds = current.getBoundingClientRect();
208
+ const measured =
209
+ axis === 'vertical'
210
+ ? Math.max(bounds.height, current.scrollHeight)
211
+ : Math.max(bounds.width, current.scrollWidth);
212
+ writeSize(current, size);
213
+ current.style.visibility = visibility;
214
+ return measured;
215
+ };
216
+
217
+ const play = async (
218
+ expanded: boolean,
219
+ signal?: AbortSignal
220
+ ): Promise<void> => {
221
+ if (signal?.aborted) return;
222
+ const current = resolveElement();
223
+ if (!current || !original) return;
224
+
225
+ const bounds = current.getBoundingClientRect();
226
+ const currentSize = axis === 'vertical' ? bounds.height : bounds.width;
227
+ const computedOpacity =
228
+ current.ownerDocument.defaultView?.getComputedStyle(current).opacity;
229
+ const currentOpacity = animation
230
+ ? computedOpacity || (expanded ? '0' : visibleOpacity)
231
+ : current.style.opacity ||
232
+ computedOpacity ||
233
+ (expanded ? '0' : visibleOpacity);
234
+ const token = ++operation;
235
+
236
+ animation?.cancel();
237
+ animation = null;
238
+ const targetSize = expanded ? measureExpandedSize(current) : 0;
239
+ current.style.overflow = 'hidden';
240
+ current.style.visibility = original.visibility;
241
+
242
+ if (typeof current.animate !== 'function') {
243
+ applyBoundary(expanded);
244
+ return;
245
+ }
246
+
247
+ const options = resolvedOptions(current, {
248
+ keyframes: [],
249
+ options: {
250
+ duration: 250,
251
+ easing: 'ease',
252
+ ...definition.options,
253
+ fill: 'both',
254
+ },
255
+ respectReducedMotion: definition.respectReducedMotion,
256
+ });
257
+ animation = current.animate(
258
+ [
259
+ {
260
+ [axis === 'vertical' ? 'height' : 'width']: `${currentSize}px`,
261
+ ...(definition.fade === false ? {} : { opacity: currentOpacity }),
262
+ },
263
+ {
264
+ [axis === 'vertical' ? 'height' : 'width']: `${targetSize}px`,
265
+ ...(definition.fade === false
266
+ ? {}
267
+ : { opacity: expanded ? visibleOpacity : '0' }),
268
+ },
269
+ ],
270
+ options
271
+ );
272
+ const currentAnimation = animation;
273
+ await waitForAnimation(currentAnimation, signal);
274
+ if (token !== operation || signal?.aborted) return;
275
+ currentAnimation.cancel();
276
+ if (animation === currentAnimation) animation = null;
277
+ applyBoundary(expanded);
278
+ };
279
+
280
+ return {
281
+ enter: (signal) => play(true, signal),
282
+ leave: (signal) => play(false, signal),
283
+ setExpanded(expanded) {
284
+ operation += 1;
285
+ animation?.cancel();
286
+ animation = null;
287
+ applyBoundary(expanded);
288
+ },
289
+ cancel() {
290
+ operation += 1;
291
+ animation?.cancel();
292
+ animation = null;
293
+ restore();
294
+ element = null;
295
+ original = null;
296
+ },
297
+ };
298
+ }