1 | 'use strict';
|
2 | import type {
|
3 | Component,
|
4 | ComponentClass,
|
5 | ComponentType,
|
6 | FunctionComponent,
|
7 | MutableRefObject,
|
8 | } from 'react';
|
9 | import React from 'react';
|
10 | import { findNodeHandle, Platform } from 'react-native';
|
11 | import { WorkletEventHandler } from '../reanimated2/WorkletEventHandler';
|
12 | import '../reanimated2/layoutReanimation/animationsManager';
|
13 | import invariant from 'invariant';
|
14 | import { adaptViewConfig } from '../ConfigHelper';
|
15 | import { RNRenderer } from '../reanimated2/platform-specific/RNRenderer';
|
16 | import { enableLayoutAnimations } from '../reanimated2/core';
|
17 | import {
|
18 | SharedTransition,
|
19 | LayoutAnimationType,
|
20 | } from '../reanimated2/layoutReanimation';
|
21 | import type { StyleProps, ShadowNodeWrapper } from '../reanimated2/commonTypes';
|
22 | import { getShadowNodeWrapperFromRef } from '../reanimated2/fabricUtils';
|
23 | import { removeFromPropsRegistry } from '../reanimated2/PropsRegistry';
|
24 | import { getReduceMotionFromConfig } from '../reanimated2/animation/util';
|
25 | import { maybeBuild } from '../animationBuilder';
|
26 | import { SkipEnteringContext } from '../reanimated2/component/LayoutAnimationConfig';
|
27 | import type { AnimateProps } from '../reanimated2';
|
28 | import JSPropsUpdater from './JSPropsUpdater';
|
29 | import type {
|
30 | AnimatedComponentProps,
|
31 | AnimatedProps,
|
32 | InitialComponentProps,
|
33 | AnimatedComponentRef,
|
34 | IAnimatedComponentInternal,
|
35 | ViewInfo,
|
36 | } from './commonTypes';
|
37 | import { has, flattenArray } from './utils';
|
38 | import setAndForwardRef from './setAndForwardRef';
|
39 | import {
|
40 | isFabric,
|
41 | isJest,
|
42 | isWeb,
|
43 | shouldBeUseWeb,
|
44 | } from '../reanimated2/PlatformChecker';
|
45 | import { InlinePropManager } from './InlinePropManager';
|
46 | import { PropsFilter } from './PropsFilter';
|
47 | import {
|
48 | startWebLayoutAnimation,
|
49 | tryActivateLayoutTransition,
|
50 | configureWebLayoutAnimations,
|
51 | getReducedMotionFromConfig,
|
52 | saveSnapshot,
|
53 | } from '../reanimated2/layoutReanimation/web';
|
54 | import { updateLayoutAnimations } from '../reanimated2/UpdateLayoutAnimations';
|
55 | import type { CustomConfig } from '../reanimated2/layoutReanimation/web/config';
|
56 | import type { FlatList, FlatListProps } from 'react-native';
|
57 | import { addHTMLMutationObserver } from '../reanimated2/layoutReanimation/web/domUtils';
|
58 | import { getViewInfo } from './getViewInfo';
|
59 |
|
60 | const IS_WEB = isWeb();
|
61 |
|
62 | if (IS_WEB) {
|
63 | configureWebLayoutAnimations();
|
64 | }
|
65 |
|
66 | function onlyAnimatedStyles(styles: StyleProps[]): StyleProps[] {
|
67 | return styles.filter((style) => style?.viewDescriptors);
|
68 | }
|
69 |
|
70 | type Options<P> = {
|
71 | setNativeProps: (ref: AnimatedComponentRef, props: P) => void;
|
72 | };
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 | export function createAnimatedComponent<P extends object>(
|
84 | component: FunctionComponent<P>,
|
85 | options?: Options<P>
|
86 | ): FunctionComponent<AnimateProps<P>>;
|
87 |
|
88 | export function createAnimatedComponent<P extends object>(
|
89 | component: ComponentClass<P>,
|
90 | options?: Options<P>
|
91 | ): ComponentClass<AnimateProps<P>>;
|
92 |
|
93 | export function createAnimatedComponent<P extends object>(
|
94 |
|
95 |
|
96 | component: ComponentType<P>,
|
97 | options?: Options<P>
|
98 | ): FunctionComponent<AnimateProps<P>> | ComponentClass<AnimateProps<P>>;
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 | export function createAnimatedComponent(
|
105 | component: typeof FlatList<unknown>,
|
106 | options?: Options<any>
|
107 | ): ComponentClass<AnimateProps<FlatListProps<unknown>>>;
|
108 |
|
109 | export function createAnimatedComponent(
|
110 | Component: ComponentType<InitialComponentProps>,
|
111 | options?: Options<InitialComponentProps>
|
112 | ): any {
|
113 | invariant(
|
114 | typeof Component !== 'function' ||
|
115 | (Component.prototype && Component.prototype.isReactComponent),
|
116 | `Looks like you're passing a function component \`${Component.name}\` to \`createAnimatedComponent\` function which supports only class components. Please wrap your function component with \`React.forwardRef()\` or use a class component instead.`
|
117 | );
|
118 |
|
119 | class AnimatedComponent
|
120 | extends React.Component<AnimatedComponentProps<InitialComponentProps>>
|
121 | implements IAnimatedComponentInternal
|
122 | {
|
123 | _styles: StyleProps[] | null = null;
|
124 | _animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
|
125 | _componentViewTag = -1;
|
126 | _eventViewTag = -1;
|
127 | _isFirstRender = true;
|
128 | jestAnimatedStyle: { value: StyleProps } = { value: {} };
|
129 | _component: AnimatedComponentRef | HTMLElement | null = null;
|
130 | _sharedElementTransition: SharedTransition | null = null;
|
131 | _jsPropsUpdater = new JSPropsUpdater();
|
132 | _InlinePropManager = new InlinePropManager();
|
133 | _PropsFilter = new PropsFilter();
|
134 | _viewInfo?: ViewInfo;
|
135 | static displayName: string;
|
136 | static contextType = SkipEnteringContext;
|
137 | context!: React.ContextType<typeof SkipEnteringContext>;
|
138 |
|
139 | constructor(props: AnimatedComponentProps<InitialComponentProps>) {
|
140 | super(props);
|
141 | if (isJest()) {
|
142 | this.jestAnimatedStyle = { value: {} };
|
143 | }
|
144 | }
|
145 |
|
146 | componentDidMount() {
|
147 | this._setComponentViewTag();
|
148 | this._setEventViewTag();
|
149 | this._attachNativeEvents();
|
150 | this._jsPropsUpdater.addOnJSPropsChangeListener(this);
|
151 | this._attachAnimatedStyles();
|
152 | this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
|
153 |
|
154 | const layout = this.props.layout;
|
155 | if (layout) {
|
156 | this._configureLayoutTransition();
|
157 | }
|
158 |
|
159 | if (IS_WEB) {
|
160 | if (this.props.exiting) {
|
161 | saveSnapshot(this._component as HTMLElement);
|
162 | }
|
163 |
|
164 | if (
|
165 | !this.props.entering ||
|
166 | getReducedMotionFromConfig(this.props.entering as CustomConfig)
|
167 | ) {
|
168 | this._isFirstRender = false;
|
169 | return;
|
170 | }
|
171 |
|
172 | startWebLayoutAnimation(
|
173 | this.props,
|
174 | this._component as HTMLElement,
|
175 | LayoutAnimationType.ENTERING
|
176 | );
|
177 | }
|
178 |
|
179 | this._isFirstRender = false;
|
180 | }
|
181 |
|
182 | componentWillUnmount() {
|
183 | this._detachNativeEvents();
|
184 | this._jsPropsUpdater.removeOnJSPropsChangeListener(this);
|
185 | this._detachStyles();
|
186 | this._InlinePropManager.detachInlineProps();
|
187 | if (this.props.sharedTransitionTag) {
|
188 | this._configureSharedTransition(true);
|
189 | }
|
190 | this._sharedElementTransition?.unregisterTransition(
|
191 | this._componentViewTag,
|
192 | true
|
193 | );
|
194 |
|
195 | const exiting = this.props.exiting;
|
196 | if (
|
197 | IS_WEB &&
|
198 | this._component &&
|
199 | this.props.exiting &&
|
200 | !getReducedMotionFromConfig(this.props.exiting as CustomConfig)
|
201 | ) {
|
202 | addHTMLMutationObserver();
|
203 |
|
204 | startWebLayoutAnimation(
|
205 | this.props,
|
206 | this._component as HTMLElement,
|
207 | LayoutAnimationType.EXITING
|
208 | );
|
209 | } else if (exiting) {
|
210 | const reduceMotionInExiting =
|
211 | 'getReduceMotion' in exiting &&
|
212 | typeof exiting.getReduceMotion === 'function'
|
213 | ? getReduceMotionFromConfig(exiting.getReduceMotion())
|
214 | : getReduceMotionFromConfig();
|
215 | if (!reduceMotionInExiting) {
|
216 | updateLayoutAnimations(
|
217 | this._componentViewTag,
|
218 | LayoutAnimationType.EXITING,
|
219 | maybeBuild(
|
220 | exiting,
|
221 | this.props?.style,
|
222 | AnimatedComponent.displayName
|
223 | )
|
224 | );
|
225 | }
|
226 | }
|
227 | }
|
228 |
|
229 | _setComponentViewTag() {
|
230 | this._componentViewTag = this._getViewInfo().viewTag as number;
|
231 | }
|
232 |
|
233 | _setEventViewTag() {
|
234 |
|
235 | const componentAnimatedRef = this._component as AnimatedComponentRef;
|
236 | if (componentAnimatedRef.getScrollableNode) {
|
237 | const scrollableNode = componentAnimatedRef.getScrollableNode();
|
238 | this._eventViewTag = findNodeHandle(scrollableNode) ?? -1;
|
239 | } else {
|
240 | this._eventViewTag =
|
241 | findNodeHandle(
|
242 | options?.setNativeProps ? this : componentAnimatedRef
|
243 | ) ?? -1;
|
244 | }
|
245 | }
|
246 |
|
247 | _attachNativeEvents() {
|
248 | for (const key in this.props) {
|
249 | const prop = this.props[key];
|
250 | if (
|
251 | has('workletEventHandler', prop) &&
|
252 | prop.workletEventHandler instanceof WorkletEventHandler
|
253 | ) {
|
254 | prop.workletEventHandler.registerForEvents(this._eventViewTag, key);
|
255 | }
|
256 | }
|
257 | }
|
258 |
|
259 | _detachNativeEvents() {
|
260 | for (const key in this.props) {
|
261 | const prop = this.props[key];
|
262 | if (
|
263 | has('workletEventHandler', prop) &&
|
264 | prop.workletEventHandler instanceof WorkletEventHandler
|
265 | ) {
|
266 | prop.workletEventHandler.unregisterFromEvents(this._eventViewTag);
|
267 | }
|
268 | }
|
269 | }
|
270 |
|
271 | _detachStyles() {
|
272 | if (IS_WEB && this._styles !== null) {
|
273 | for (const style of this._styles) {
|
274 | style.viewsRef.remove(this);
|
275 | }
|
276 | } else if (this._componentViewTag !== -1 && this._styles !== null) {
|
277 | for (const style of this._styles) {
|
278 | style.viewDescriptors.remove(this._componentViewTag);
|
279 | }
|
280 | if (this.props.animatedProps?.viewDescriptors) {
|
281 | this.props.animatedProps.viewDescriptors.remove(
|
282 | this._componentViewTag
|
283 | );
|
284 | }
|
285 | if (isFabric()) {
|
286 | removeFromPropsRegistry(this._componentViewTag);
|
287 | }
|
288 | }
|
289 | }
|
290 |
|
291 | _updateNativeEvents(
|
292 | prevProps: AnimatedComponentProps<InitialComponentProps>
|
293 | ) {
|
294 | for (const key in prevProps) {
|
295 | const prevProp = prevProps[key];
|
296 | if (
|
297 | has('workletEventHandler', prevProp) &&
|
298 | prevProp.workletEventHandler instanceof WorkletEventHandler
|
299 | ) {
|
300 | const newProp = this.props[key];
|
301 | if (!newProp) {
|
302 |
|
303 | prevProp.workletEventHandler.unregisterFromEvents(
|
304 | this._eventViewTag
|
305 | );
|
306 | } else if (
|
307 | has('workletEventHandler', newProp) &&
|
308 | newProp.workletEventHandler instanceof WorkletEventHandler &&
|
309 | newProp.workletEventHandler !== prevProp.workletEventHandler
|
310 | ) {
|
311 |
|
312 | prevProp.workletEventHandler.unregisterFromEvents(
|
313 | this._eventViewTag
|
314 | );
|
315 | newProp.workletEventHandler.registerForEvents(this._eventViewTag);
|
316 | }
|
317 | }
|
318 | }
|
319 |
|
320 | for (const key in this.props) {
|
321 | const newProp = this.props[key];
|
322 | if (
|
323 | has('workletEventHandler', newProp) &&
|
324 | newProp.workletEventHandler instanceof WorkletEventHandler &&
|
325 | !prevProps[key]
|
326 | ) {
|
327 |
|
328 | newProp.workletEventHandler.registerForEvents(this._eventViewTag);
|
329 | }
|
330 | }
|
331 | }
|
332 |
|
333 | _updateFromNative(props: StyleProps) {
|
334 | if (options?.setNativeProps) {
|
335 | options.setNativeProps(this._component as AnimatedComponentRef, props);
|
336 | } else {
|
337 | (this._component as AnimatedComponentRef)?.setNativeProps?.(props);
|
338 | }
|
339 | }
|
340 |
|
341 | _getViewInfo(): ViewInfo {
|
342 | if (this._viewInfo !== undefined) {
|
343 | return this._viewInfo;
|
344 | }
|
345 |
|
346 | let viewTag: number | HTMLElement | null;
|
347 | let viewName: string | null;
|
348 | let shadowNodeWrapper: ShadowNodeWrapper | null = null;
|
349 | let viewConfig;
|
350 |
|
351 |
|
352 | const component = (this._component as AnimatedComponentRef)
|
353 | ?.getAnimatableRef
|
354 | ? (this._component as AnimatedComponentRef).getAnimatableRef?.()
|
355 | : this;
|
356 |
|
357 | if (IS_WEB) {
|
358 |
|
359 |
|
360 | viewTag = this._component as HTMLElement;
|
361 | viewName = null;
|
362 | shadowNodeWrapper = null;
|
363 | viewConfig = null;
|
364 | } else {
|
365 |
|
366 | const hostInstance = RNRenderer.findHostInstance_DEPRECATED(component);
|
367 | if (!hostInstance) {
|
368 | throw new Error(
|
369 | '[Reanimated] Cannot find host instance for this component. Maybe it renders nothing?'
|
370 | );
|
371 | }
|
372 |
|
373 | const viewInfo = getViewInfo(hostInstance);
|
374 | viewTag = viewInfo.viewTag;
|
375 | viewName = viewInfo.viewName;
|
376 | viewConfig = viewInfo.viewConfig;
|
377 | shadowNodeWrapper = isFabric()
|
378 | ? getShadowNodeWrapperFromRef(this)
|
379 | : null;
|
380 | }
|
381 | this._viewInfo = { viewTag, viewName, shadowNodeWrapper, viewConfig };
|
382 | return this._viewInfo;
|
383 | }
|
384 |
|
385 | _attachAnimatedStyles() {
|
386 | const styles = this.props.style
|
387 | ? onlyAnimatedStyles(flattenArray<StyleProps>(this.props.style))
|
388 | : [];
|
389 | const prevStyles = this._styles;
|
390 | this._styles = styles;
|
391 |
|
392 | const prevAnimatedProps = this._animatedProps;
|
393 | this._animatedProps = this.props.animatedProps;
|
394 |
|
395 | const { viewTag, viewName, shadowNodeWrapper, viewConfig } =
|
396 | this._getViewInfo();
|
397 |
|
398 |
|
399 | const hasReanimated2Props =
|
400 | this.props.animatedProps?.viewDescriptors || styles.length;
|
401 | if (hasReanimated2Props && viewConfig) {
|
402 | adaptViewConfig(viewConfig);
|
403 | }
|
404 |
|
405 | this._componentViewTag = viewTag as number;
|
406 |
|
407 |
|
408 | if (prevStyles) {
|
409 |
|
410 | const hasOneSameStyle =
|
411 | styles.length === 1 &&
|
412 | prevStyles.length === 1 &&
|
413 | styles[0] === prevStyles[0];
|
414 |
|
415 | if (!hasOneSameStyle) {
|
416 |
|
417 | for (const prevStyle of prevStyles) {
|
418 | const isPresent = styles.some((style) => style === prevStyle);
|
419 | if (!isPresent) {
|
420 | prevStyle.viewDescriptors.remove(viewTag);
|
421 | }
|
422 | }
|
423 | }
|
424 | }
|
425 |
|
426 | styles.forEach((style) => {
|
427 | style.viewDescriptors.add({
|
428 | tag: viewTag,
|
429 | name: viewName,
|
430 | shadowNodeWrapper,
|
431 | });
|
432 | if (isJest()) {
|
433 | |
434 |
|
435 |
|
436 |
|
437 |
|
438 |
|
439 | this.jestAnimatedStyle.value = {
|
440 | ...this.jestAnimatedStyle.value,
|
441 | ...style.initial.value,
|
442 | };
|
443 | style.jestAnimatedStyle.current = this.jestAnimatedStyle;
|
444 | }
|
445 | });
|
446 |
|
447 |
|
448 | if (prevAnimatedProps && prevAnimatedProps !== this.props.animatedProps) {
|
449 | prevAnimatedProps.viewDescriptors!.remove(viewTag as number);
|
450 | }
|
451 |
|
452 |
|
453 | if (this.props.animatedProps?.viewDescriptors) {
|
454 | this.props.animatedProps.viewDescriptors.add({
|
455 | tag: viewTag as number,
|
456 | name: viewName!,
|
457 | shadowNodeWrapper: shadowNodeWrapper!,
|
458 | });
|
459 | }
|
460 | }
|
461 |
|
462 | componentDidUpdate(
|
463 | prevProps: AnimatedComponentProps<InitialComponentProps>,
|
464 | _prevState: Readonly<unknown>,
|
465 |
|
466 |
|
467 | snapshot: DOMRect | null
|
468 | ) {
|
469 | const layout = this.props.layout;
|
470 | const oldLayout = prevProps.layout;
|
471 | if (layout !== oldLayout) {
|
472 | this._configureLayoutTransition();
|
473 | }
|
474 | if (
|
475 | this.props.sharedTransitionTag !== undefined ||
|
476 | prevProps.sharedTransitionTag !== undefined
|
477 | ) {
|
478 | this._configureSharedTransition();
|
479 | }
|
480 | this._updateNativeEvents(prevProps);
|
481 | this._attachAnimatedStyles();
|
482 | this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
|
483 |
|
484 | if (IS_WEB && this.props.exiting) {
|
485 | saveSnapshot(this._component as HTMLElement);
|
486 | }
|
487 |
|
488 |
|
489 | if (
|
490 | IS_WEB &&
|
491 | snapshot !== null &&
|
492 | this.props.layout &&
|
493 | !getReducedMotionFromConfig(this.props.layout as CustomConfig)
|
494 | ) {
|
495 | tryActivateLayoutTransition(
|
496 | this.props,
|
497 | this._component as HTMLElement,
|
498 | snapshot
|
499 | );
|
500 | }
|
501 | }
|
502 |
|
503 | _configureLayoutTransition() {
|
504 | const layout = this.props.layout
|
505 | ? maybeBuild(
|
506 | this.props.layout,
|
507 | undefined ,
|
508 | AnimatedComponent.displayName
|
509 | )
|
510 | : undefined;
|
511 | updateLayoutAnimations(
|
512 | this._componentViewTag,
|
513 | LayoutAnimationType.LAYOUT,
|
514 | layout
|
515 | );
|
516 | }
|
517 |
|
518 | _configureSharedTransition(isUnmounting = false) {
|
519 | if (IS_WEB) {
|
520 | return;
|
521 | }
|
522 | const { sharedTransitionTag } = this.props;
|
523 | if (!sharedTransitionTag) {
|
524 | this._sharedElementTransition?.unregisterTransition(
|
525 | this._componentViewTag,
|
526 | isUnmounting
|
527 | );
|
528 | this._sharedElementTransition = null;
|
529 | return;
|
530 | }
|
531 | const sharedElementTransition =
|
532 | this.props.sharedTransitionStyle ??
|
533 | this._sharedElementTransition ??
|
534 | new SharedTransition();
|
535 | sharedElementTransition.registerTransition(
|
536 | this._componentViewTag,
|
537 | sharedTransitionTag,
|
538 | isUnmounting
|
539 | );
|
540 | this._sharedElementTransition = sharedElementTransition;
|
541 | }
|
542 |
|
543 | _setComponentRef = setAndForwardRef<Component | HTMLElement>({
|
544 | getForwardedRef: () =>
|
545 | this.props.forwardedRef as MutableRefObject<
|
546 | Component<Record<string, unknown>, Record<string, unknown>, unknown>
|
547 | >,
|
548 | setLocalRef: (ref) => {
|
549 |
|
550 |
|
551 | const tag = IS_WEB
|
552 | ? (ref as HTMLElement)
|
553 | : findNodeHandle(ref as Component);
|
554 |
|
555 | this._componentViewTag = tag as number;
|
556 |
|
557 | const { layout, entering, exiting, sharedTransitionTag } = this.props;
|
558 | if (
|
559 | (layout || entering || exiting || sharedTransitionTag) &&
|
560 | tag != null
|
561 | ) {
|
562 | if (!shouldBeUseWeb()) {
|
563 | enableLayoutAnimations(true, false);
|
564 | }
|
565 |
|
566 | if (sharedTransitionTag) {
|
567 | this._configureSharedTransition();
|
568 | }
|
569 |
|
570 | const skipEntering = this.context?.current;
|
571 | if (entering && !skipEntering) {
|
572 | updateLayoutAnimations(
|
573 | tag as number,
|
574 | LayoutAnimationType.ENTERING,
|
575 | maybeBuild(
|
576 | entering,
|
577 | this.props?.style,
|
578 | AnimatedComponent.displayName
|
579 | )
|
580 | );
|
581 | }
|
582 | }
|
583 |
|
584 | if (ref !== this._component) {
|
585 | this._component = ref;
|
586 | }
|
587 | },
|
588 | });
|
589 |
|
590 |
|
591 |
|
592 |
|
593 | getSnapshotBeforeUpdate() {
|
594 | if (
|
595 | IS_WEB &&
|
596 | (this._component as HTMLElement)?.getBoundingClientRect !== undefined
|
597 | ) {
|
598 | return (this._component as HTMLElement).getBoundingClientRect();
|
599 | }
|
600 |
|
601 | return null;
|
602 | }
|
603 |
|
604 | render() {
|
605 | const filteredProps = this._PropsFilter.filterNonAnimatedProps(this);
|
606 |
|
607 | if (isJest()) {
|
608 | filteredProps.jestAnimatedStyle = this.jestAnimatedStyle;
|
609 | }
|
610 |
|
611 |
|
612 |
|
613 |
|
614 |
|
615 | if (
|
616 | this._isFirstRender &&
|
617 | IS_WEB &&
|
618 | filteredProps.entering &&
|
619 | !getReducedMotionFromConfig(filteredProps.entering as CustomConfig)
|
620 | ) {
|
621 | filteredProps.style = {
|
622 | ...(filteredProps.style ?? {}),
|
623 | visibility: 'hidden',
|
624 | };
|
625 | }
|
626 |
|
627 | const platformProps = Platform.select({
|
628 | web: {},
|
629 | default: { collapsable: false },
|
630 | });
|
631 |
|
632 | return (
|
633 | <Component
|
634 | {...filteredProps}
|
635 |
|
636 |
|
637 | ref={this._setComponentRef as (ref: Component) => void}
|
638 | {...platformProps}
|
639 | />
|
640 | );
|
641 | }
|
642 | }
|
643 |
|
644 | AnimatedComponent.displayName = `AnimatedComponent(${
|
645 | Component.displayName || Component.name || 'Component'
|
646 | })`;
|
647 |
|
648 | return React.forwardRef<Component>((props, ref) => {
|
649 | return (
|
650 | <AnimatedComponent
|
651 | {...props}
|
652 | {...(ref === null ? null : { forwardedRef: ref })}
|
653 | />
|
654 | );
|
655 | });
|
656 | }
|
657 |
|
\ | No newline at end of file |